refactor(rectification): let director agent own interview flow
This commit is contained in:
@@ -8,6 +8,7 @@ import type {
|
||||
import { rectificationAgentV5Protocol, rectificationV4AlgorithmVersion, rectificationV4Protocol } from "./contracts.ts";
|
||||
import { selectRectificationDeploymentMode } from "../rectification-agent/feature-policy.ts";
|
||||
import { CURRENT_RECTIFICATION_PROMPT_VERSION, CURRENT_RECTIFICATION_SKILL_VERSION } from "../rectification-agent/contracts.ts";
|
||||
import { regenerateDirectorQuestion } from "../rectification-agent/director-agent.ts";
|
||||
import { regenerateQuestionRealization } from "../rectification-agent/renderer-agent.ts";
|
||||
import { calculationSpecHash, evidenceSetHash } from "./fingerprints.ts";
|
||||
import { openingQuestion } from "./opening-question.ts";
|
||||
@@ -20,10 +21,12 @@ export function createRectificationV4CaseService(
|
||||
options: {
|
||||
readonly now?: () => Date;
|
||||
readonly regenerateQuestion?: typeof regenerateQuestionRealization;
|
||||
readonly regenerateDirectorQuestion?: typeof regenerateDirectorQuestion;
|
||||
} = {},
|
||||
) {
|
||||
const now = options.now ?? (() => new Date());
|
||||
const realizeQuestion = options.regenerateQuestion ?? regenerateQuestionRealization;
|
||||
const redirectQuestion = options.regenerateDirectorQuestion ?? regenerateDirectorQuestion;
|
||||
|
||||
async function response(userId: string, caseValue: RectificationV4Case, jobId?: string): Promise<RectificationV4ApiResponse> {
|
||||
const [events, turns, analysis, job] = await Promise.all([
|
||||
@@ -123,19 +126,30 @@ export function createRectificationV4CaseService(
|
||||
const current = await store.loadCase(input.userId, input.caseId);
|
||||
if (!current?.currentQuestion || current.deploymentMode !== "v5_agent") return null;
|
||||
const validated = await store.loadLatestValidatedDecision(input.userId, input.caseId);
|
||||
const opportunity = validated?.selectedOpportunity;
|
||||
if (!opportunity) return null;
|
||||
if (!validated || validated.decision.action !== "ask_question") return null;
|
||||
const [events, turns] = await Promise.all([
|
||||
store.loadEvents(input.userId, input.caseId),
|
||||
store.loadTurns(input.userId, input.caseId),
|
||||
]);
|
||||
const prompt = await realizeQuestion({
|
||||
caseValue: current,
|
||||
currentPrompt: current.currentQuestion.prompt,
|
||||
latestAnswer: turns.at(-1)?.answer ?? "",
|
||||
acceptedEvents: events,
|
||||
opportunity,
|
||||
});
|
||||
let prompt: string;
|
||||
if (validated.selectedOpportunity) {
|
||||
prompt = await realizeQuestion({
|
||||
caseValue: current,
|
||||
currentPrompt: current.currentQuestion.prompt,
|
||||
latestAnswer: turns.at(-1)?.answer ?? "",
|
||||
acceptedEvents: events,
|
||||
opportunity: validated.selectedOpportunity,
|
||||
});
|
||||
} else {
|
||||
if (!("focus" in validated.decision)) return null;
|
||||
prompt = await redirectQuestion({
|
||||
caseValue: current,
|
||||
currentQuestion: current.currentQuestion.prompt,
|
||||
latestAnswer: turns.at(-1)?.answer ?? "",
|
||||
acceptedEvents: events,
|
||||
focus: validated.decision.focus,
|
||||
});
|
||||
}
|
||||
return store.replaceCurrentQuestion({
|
||||
...input,
|
||||
question: { ...current.currentQuestion, id: randomUUID(), prompt },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { extractLifeEventEvidence, type ExtractedLifeEventEvidence } from "../conversational-rectification/evidence-extractor.ts";
|
||||
import { extractLifeEventEvidence, parseDeclaredDateText, validatedModelAssistedEvidence, type ExtractedLifeEventEvidence } from "../conversational-rectification/evidence-extractor.ts";
|
||||
import type { EvidenceProposal } from "../rectification-agent/contracts.ts";
|
||||
import type {
|
||||
EventKind,
|
||||
EvidenceDomain,
|
||||
@@ -215,3 +216,72 @@ export function reconcileV4Evidence(input: {
|
||||
export function extractV4EventRevisions(input: Omit<Parameters<typeof reconcileV4Evidence>[0], "caseId"> & { readonly caseId?: string }): readonly LifeEventRevision[] {
|
||||
return reconcileV4Evidence({ ...input, caseId: input.caseId ?? "00000000-0000-4000-8000-000000000000" }).revisions;
|
||||
}
|
||||
|
||||
|
||||
export function stageAgentEvidenceProposals(input: Readonly<{
|
||||
caseId: string;
|
||||
rawText: string;
|
||||
sourceTurnId: string;
|
||||
asOfDate: string;
|
||||
existing: readonly LifeEventRevision[];
|
||||
proposals: readonly EvidenceProposal[];
|
||||
now?: Date;
|
||||
}>): ReconciledV4Evidence {
|
||||
const revisions: LifeEventRevision[] = [];
|
||||
const pending: PendingEvidence[] = [];
|
||||
const active = latestEventRevisions(input.existing);
|
||||
for (const proposal of input.proposals) {
|
||||
if (proposal.operation === "ignore") continue;
|
||||
if (!input.rawText.includes(proposal.sourceSpan) || !proposal.dateText || !input.rawText.includes(proposal.dateText)) {
|
||||
pending.push(pendingEvidence({ caseId: input.caseId, turnId: input.sourceTurnId, rawText: input.rawText, reasonCode: proposal.dateText ? "event_unparsed" : "date_unresolved", targetEventId: proposal.targetEventId, now: input.now }));
|
||||
continue;
|
||||
}
|
||||
const extracted = validatedModelAssistedEvidence({
|
||||
rawText: input.rawText,
|
||||
sourceTurnId: input.sourceTurnId,
|
||||
asOfDate: input.asOfDate,
|
||||
extraction: {
|
||||
sourceSpan: proposal.sourceSpan,
|
||||
summary: proposal.proposedSummary,
|
||||
domain: proposal.proposedDomain,
|
||||
eventKind: proposal.proposedEventKind,
|
||||
subject: proposal.proposedSubject,
|
||||
relatedPerson: proposal.proposedRelatedPerson,
|
||||
dateText: proposal.dateText,
|
||||
},
|
||||
});
|
||||
if (!extracted?.dateValue || extracted.datePrecision === "unknown") {
|
||||
pending.push(pendingEvidence({ caseId: input.caseId, turnId: input.sourceTurnId, rawText: input.rawText, reasonCode: "event_unparsed", targetEventId: proposal.targetEventId, now: input.now }));
|
||||
continue;
|
||||
}
|
||||
if (proposal.operation === "create") {
|
||||
const revision = newRevision(extracted, [...input.existing, ...revisions], input.now);
|
||||
if (revision && !revisions.some((value) => value.eventId === revision.eventId)) revisions.push(revision);
|
||||
continue;
|
||||
}
|
||||
const target = proposal.targetEventId ? active.find((event) => event.eventId === proposal.targetEventId) : null;
|
||||
const parsedDate = parseDeclaredDateText(proposal.dateText.normalize("NFKC"), input.asOfDate);
|
||||
if (!target || !parsedDate) {
|
||||
pending.push(pendingEvidence({ caseId: input.caseId, turnId: input.sourceTurnId, rawText: input.rawText, reasonCode: "event_unparsed", targetEventId: proposal.targetEventId, now: input.now }));
|
||||
continue;
|
||||
}
|
||||
revisions.push(appendEventRevision([...input.existing, ...revisions], {
|
||||
eventId: target.eventId,
|
||||
domain: extracted.domain as EvidenceDomain,
|
||||
eventKind: normalizeKind(extracted.domain as EvidenceDomain, extracted.eventKind, extracted.eventSummary),
|
||||
subject: extracted.subject as EventSubject,
|
||||
relatedPerson: extracted.relatedPerson as RelatedPerson | null,
|
||||
summary: proposal.proposedSummary,
|
||||
rawText: input.rawText,
|
||||
dateRange: dateRangeFromDeclared(parsedDate.value, parsedDate.precision),
|
||||
...eventDateProvenance(target),
|
||||
scoreability: extracted.scoreability as Scoreability,
|
||||
}, { now: input.now }));
|
||||
}
|
||||
return {
|
||||
revisions,
|
||||
pending,
|
||||
unansweredTargetEventId: null,
|
||||
targetDisposition: revisions.length ? "resolved" : "unresolved",
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user