NODEDC_1C/llm_normalizer/backend/src/services/assistantAnswerPackageBuild...

160 lines
5.6 KiB
TypeScript

import { FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 } from "../config";
import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../types/assistant";
import {
ANSWER_STRUCTURE_SCHEMA_VERSION,
type AnswerStructureV11,
type EvidenceLimitationReasonCode
} from "../types/stage1Contracts";
export interface BuildAssistantAnswerStructureV11Input {
assistantReply: string;
coverageReport: RequirementCoverageReport;
groundingCheck: AnswerGroundingCheck;
retrievalResults: UnifiedRetrievalResult[];
options?: {
enableEvidenceEnrichment?: boolean;
};
}
const EVIDENCE_LIMITATION_REASON_CODE_SET: ReadonlySet<EvidenceLimitationReasonCode> = new Set([
"snapshot_only",
"heuristic_inference",
"missing_mechanism",
"weak_source_mapping",
"insufficient_detail",
"unknown"
]);
function summarizeUnique(values: Array<string | null | undefined>, limit = 6): string[] {
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
}
function isEvidenceLimitationReasonCode(value: string): value is EvidenceLimitationReasonCode {
return EVIDENCE_LIMITATION_REASON_CODE_SET.has(value as EvidenceLimitationReasonCode);
}
function firstNonEmptyLine(text: string): string {
const line = String(text ?? "")
.split("\n")
.map((item) => item.trim())
.find((item) => item.length > 0);
return (line ?? String(text ?? "")).slice(0, 220);
}
function buildClaimEvidenceLinks(
retrievalResults: UnifiedRetrievalResult[]
): NonNullable<AnswerStructureV11["evidence_block"]["claim_evidence_links"]> {
const byClaim = new Map<string, string[]>();
for (const result of retrievalResults) {
for (const evidence of result.evidence) {
const claimRef = String(evidence.claim_ref ?? "").trim();
if (!claimRef) {
continue;
}
const evidenceId = String(evidence.evidence_id ?? "").trim();
if (!evidenceId) {
continue;
}
const current = byClaim.get(claimRef) ?? [];
current.push(evidenceId);
byClaim.set(claimRef, current);
}
}
return Array.from(byClaim.entries())
.slice(0, 10)
.map(([claimRef, evidenceIds]) => ({
claim_ref: claimRef,
evidence_ids: summarizeUnique(evidenceIds, 10)
}));
}
export function buildAssistantAnswerStructureV11(input: BuildAssistantAnswerStructureV11Input): AnswerStructureV11 {
const evidenceIds = summarizeUnique(
input.retrievalResults.flatMap((item) => item.evidence.map((evidence) => evidence.evidence_id)),
10
);
const mechanismNotes = summarizeUnique(
input.retrievalResults.flatMap((item) =>
item.evidence
.map((evidence) => evidence.mechanism_note)
.filter((note): note is string => typeof note === "string" && note.trim().length > 0)
),
6
);
const sourceRefs = summarizeUnique(
input.retrievalResults.flatMap((item) =>
item.evidence
.map((evidence) => evidence.source_ref?.canonical_ref)
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
),
8
);
const limitationReasonCodes: EvidenceLimitationReasonCode[] = summarizeUnique(
input.retrievalResults.flatMap((item) =>
item.evidence.flatMap((evidence) => {
const code = evidence.limitation?.reason_code;
return typeof code === "string" && code.trim().length > 0 ? [code] : [];
})
),
8
).filter(isEvidenceLimitationReasonCode);
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
const limitations = summarizeUnique(
[...input.retrievalResults.flatMap((item) => item.limitations), ...input.groundingCheck.reasons],
8
);
const clarificationQuestions = input.coverageReport.clarification_needed_for.map(
(item) => `Уточните требование ${item}.`
);
const recommendedActions = summarizeUnique(
[
...input.coverageReport.requirements_uncovered.map((item) => `Проверить непокрытое требование ${item}.`),
...input.coverageReport.requirements_partially_covered.map(
(item) => `Доуточнить частично покрытое требование ${item}.`
)
],
6
);
const mechanismStatus: AnswerStructureV11["mechanism_block"]["status"] =
mechanismNotes.length === 0
? "unresolved"
: limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
? "limited"
: "grounded";
const enableEvidenceEnrichment =
input.options?.enableEvidenceEnrichment ?? FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1;
return {
schema_version: ANSWER_STRUCTURE_SCHEMA_VERSION,
answer_summary: firstNonEmptyLine(input.assistantReply),
direct_answer: input.assistantReply,
mechanism_block: {
status: mechanismStatus,
mechanism_notes: mechanismNotes,
limitation_reason_codes: limitationReasonCodes
},
evidence_block: {
evidence_ids: evidenceIds,
source_refs: sourceRefs,
mechanism_notes: mechanismNotes,
coverage_note:
input.coverageReport.requirements_total === input.coverageReport.requirements_covered
? "coverage_full_or_near_full"
: "coverage_partial_or_limited",
...(enableEvidenceEnrichment && claimEvidenceLinks.length > 0
? {
claim_evidence_links: claimEvidenceLinks
}
: {})
},
uncertainty_block: {
open_uncertainties: input.groundingCheck.missing_requirements,
limitations
},
next_step_block: {
recommended_actions: recommendedActions,
clarification_questions: clarificationQuestions
}
};
}