Initial import NDC_1C
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import type {
|
||||
AssistantRequirement,
|
||||
RequirementCoverageReport,
|
||||
UnifiedRetrievalResult
|
||||
} from "../types/assistant";
|
||||
import type { RouteHintSummary } from "../types/normalizer";
|
||||
import type {
|
||||
InvestigationLastAnswerMode,
|
||||
InvestigationNarrowingStatus,
|
||||
InvestigationState
|
||||
} from "../types/stage1Contracts";
|
||||
import {
|
||||
INVESTIGATION_MAX_EVIDENCE_REFS,
|
||||
INVESTIGATION_MAX_PRIMARY_ACCOUNTS,
|
||||
INVESTIGATION_MAX_REQUIREMENT_LINKS,
|
||||
INVESTIGATION_MAX_UNCERTAINTIES,
|
||||
INVESTIGATION_STATE_SCHEMA_VERSION
|
||||
} from "../types/stage1Contracts";
|
||||
|
||||
interface UpdateInvestigationStateInput {
|
||||
previous: InvestigationState;
|
||||
timestamp: string;
|
||||
questionId: string;
|
||||
userMessage: string;
|
||||
routeSummary: RouteHintSummary | null;
|
||||
requirements: AssistantRequirement[];
|
||||
coverageReport: RequirementCoverageReport;
|
||||
retrievalResults: UnifiedRetrievalResult[];
|
||||
replyType: InvestigationLastAnswerMode;
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]): string[] {
|
||||
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean)));
|
||||
}
|
||||
|
||||
function capStrings(values: string[], max: number): string[] {
|
||||
return uniqueStrings(values).slice(0, max);
|
||||
}
|
||||
|
||||
function detectAccounts(text: string): string[] {
|
||||
return capStrings(text.match(/\b\d{2}(?:\.\d{2})?\b/g) ?? [], INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
|
||||
}
|
||||
|
||||
function detectPeriod(text: string): string | null {
|
||||
const monthly = text.match(/\b(20\d{2})[-/.](0[1-9]|1[0-2])\b/);
|
||||
if (monthly) return `${monthly[1]}-${monthly[2]}`;
|
||||
const yearly = text.match(/\b(20\d{2})\b/);
|
||||
if (yearly) return yearly[1];
|
||||
return null;
|
||||
}
|
||||
|
||||
function deriveDomain(routeSummary: RouteHintSummary | null): string | null {
|
||||
if (!routeSummary) return null;
|
||||
if (routeSummary.mode === "legacy_v1") {
|
||||
return routeSummary.route_hint;
|
||||
}
|
||||
const routes = routeSummary.decisions.map((item) => item.route).filter((route) => route !== "no_route");
|
||||
const uniqueRoutes = uniqueStrings(routes);
|
||||
if (uniqueRoutes.length === 0) {
|
||||
return "no_route";
|
||||
}
|
||||
return uniqueRoutes.join(",");
|
||||
}
|
||||
|
||||
function deriveNarrowingStatus(
|
||||
routeSummary: RouteHintSummary | null,
|
||||
coverageReport: RequirementCoverageReport
|
||||
): InvestigationNarrowingStatus {
|
||||
if (!routeSummary) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
if (routeSummary.mode === "legacy_v1") {
|
||||
return "not_needed";
|
||||
}
|
||||
|
||||
if (routeSummary.fallback.type === "clarification" || coverageReport.clarification_needed_for.length > 0) {
|
||||
return "needs_clarification";
|
||||
}
|
||||
|
||||
const hasNoRoute = routeSummary.decisions.some((item) => item.route === "no_route");
|
||||
if (hasNoRoute) {
|
||||
return "broad_guarded";
|
||||
}
|
||||
|
||||
return routeSummary.decisions.length > 1 ? "applied" : "not_needed";
|
||||
}
|
||||
|
||||
function deriveQueryModeHint(routeSummary: RouteHintSummary | null): InvestigationState["query_mode_hint"] {
|
||||
if (!routeSummary) {
|
||||
return "investigation_candidate";
|
||||
}
|
||||
if (routeSummary.mode === "legacy_v1") {
|
||||
return "direct_answer";
|
||||
}
|
||||
return routeSummary.fallback.type === "none" ? "direct_answer" : "investigation_candidate";
|
||||
}
|
||||
|
||||
function collectEvidenceRefs(retrievalResults: UnifiedRetrievalResult[]): string[] {
|
||||
const refs = retrievalResults.flatMap((result) => result.evidence.map((item) => item.evidence_id));
|
||||
return capStrings(refs, INVESTIGATION_MAX_EVIDENCE_REFS);
|
||||
}
|
||||
|
||||
function collectOpenUncertainties(
|
||||
coverageReport: RequirementCoverageReport,
|
||||
retrievalResults: UnifiedRetrievalResult[]
|
||||
): string[] {
|
||||
const requirementNotes = [
|
||||
...coverageReport.requirements_uncovered.map((item) => `uncovered:${item}`),
|
||||
...coverageReport.requirements_partially_covered.map((item) => `partial:${item}`),
|
||||
...coverageReport.clarification_needed_for.map((item) => `clarify:${item}`),
|
||||
...coverageReport.out_of_scope_requirements.map((item) => `out_of_scope:${item}`)
|
||||
];
|
||||
const limitationNotes = retrievalResults.flatMap((result) => result.limitations).slice(0, 6);
|
||||
return capStrings([...requirementNotes, ...limitationNotes], INVESTIGATION_MAX_UNCERTAINTIES);
|
||||
}
|
||||
|
||||
export function cloneInvestigationState(state: InvestigationState | null): InvestigationState | null {
|
||||
if (!state) return null;
|
||||
return {
|
||||
...state,
|
||||
focus: {
|
||||
...state.focus,
|
||||
primary_accounts: [...state.focus.primary_accounts]
|
||||
},
|
||||
evidence_refs: [...state.evidence_refs],
|
||||
open_uncertainties: [...state.open_uncertainties],
|
||||
followup_context: state.followup_context
|
||||
? {
|
||||
...state.followup_context,
|
||||
referenced_requirement_ids: [...state.followup_context.referenced_requirement_ids]
|
||||
}
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
export function createEmptyInvestigationState(sessionId: string, timestamp = new Date().toISOString()): InvestigationState {
|
||||
return {
|
||||
schema_version: INVESTIGATION_STATE_SCHEMA_VERSION,
|
||||
session_id: sessionId,
|
||||
status: "idle",
|
||||
turn_index: 0,
|
||||
updated_at: timestamp,
|
||||
question_id: null,
|
||||
focus: {
|
||||
domain: null,
|
||||
period: null,
|
||||
primary_accounts: [],
|
||||
active_query_subject: null
|
||||
},
|
||||
narrowing_status: "unknown",
|
||||
evidence_refs: [],
|
||||
open_uncertainties: [],
|
||||
last_answer_mode: null,
|
||||
followup_context: null,
|
||||
query_mode_hint: "direct_answer"
|
||||
};
|
||||
}
|
||||
|
||||
export function updateInvestigationState(input: UpdateInvestigationStateInput): InvestigationState {
|
||||
const previous = input.previous;
|
||||
const focusFromMessage = capStrings(detectAccounts(input.userMessage), INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
|
||||
const requirementIds = capStrings(
|
||||
input.requirements.map((item) => item.requirement_id),
|
||||
INVESTIGATION_MAX_REQUIREMENT_LINKS
|
||||
);
|
||||
const mainRequirement = input.requirements[0]?.requirement_text ?? input.userMessage;
|
||||
|
||||
return {
|
||||
schema_version: INVESTIGATION_STATE_SCHEMA_VERSION,
|
||||
session_id: previous.session_id,
|
||||
status: "active",
|
||||
turn_index: previous.turn_index + 1,
|
||||
updated_at: input.timestamp,
|
||||
question_id: input.questionId,
|
||||
focus: {
|
||||
domain: deriveDomain(input.routeSummary) ?? previous.focus.domain,
|
||||
period: detectPeriod(input.userMessage) ?? previous.focus.period,
|
||||
primary_accounts: capStrings(
|
||||
[...focusFromMessage, ...previous.focus.primary_accounts],
|
||||
INVESTIGATION_MAX_PRIMARY_ACCOUNTS
|
||||
),
|
||||
active_query_subject: mainRequirement.slice(0, 180)
|
||||
},
|
||||
narrowing_status: deriveNarrowingStatus(input.routeSummary, input.coverageReport),
|
||||
evidence_refs: capStrings(
|
||||
[...collectEvidenceRefs(input.retrievalResults), ...previous.evidence_refs],
|
||||
INVESTIGATION_MAX_EVIDENCE_REFS
|
||||
),
|
||||
open_uncertainties: collectOpenUncertainties(input.coverageReport, input.retrievalResults),
|
||||
last_answer_mode: input.replyType,
|
||||
followup_context: {
|
||||
previous_question_id: previous.question_id,
|
||||
last_user_message: input.userMessage.slice(0, 240),
|
||||
referenced_requirement_ids: requirementIds
|
||||
},
|
||||
query_mode_hint: deriveQueryModeHint(input.routeSummary)
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user