fix(rectification): resolve typed focus answers deterministically
This commit is contained in:
@@ -2,6 +2,8 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getRectificationV9Agent, type RectificationAgentAction } from "@/mastra/agentic-rectification";
|
||||
import {
|
||||
loadV9CaseDossier,
|
||||
persistV9DeterministicTurn,
|
||||
RectificationToolServiceError,
|
||||
transitionV9CaseStatus,
|
||||
} from "@/lib/rectification-agentic/v9/tool-service";
|
||||
@@ -19,10 +21,33 @@ import { resolveSessionLanguageModel } from "@/lib/model-catalog";
|
||||
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { defaultMessageOrigin, isRectificationMessageOrigin } from "@/lib/rectification-agentic/v9/message-origin";
|
||||
import { previousInferenceFromReceipt } from "@/lib/rectification-agentic/v9/inference-adapter";
|
||||
import { parseAgentChoiceCopy } from "@/lib/rectification-agentic/v9/choice-card";
|
||||
import {
|
||||
classifyRectificationTurnIntent,
|
||||
optionIdForAnswerClass,
|
||||
} from "@/lib/rectification-agentic/v9/turn-intent-classifier";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 240;
|
||||
|
||||
function completedMessageResponse(text: string, requestId: string, caseId: string) {
|
||||
const body = [
|
||||
JSON.stringify({ type: "answer.delta", text }),
|
||||
JSON.stringify({ type: "run.completed" }),
|
||||
"",
|
||||
].join("\n");
|
||||
return new Response(body, {
|
||||
headers: {
|
||||
"cache-control": "no-cache, no-transform",
|
||||
"content-type": "application/x-ndjson; charset=utf-8",
|
||||
"x-accel-buffering": "no",
|
||||
"x-ayanam-request-id": requestId,
|
||||
"x-rectification-case-id": caseId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const agentRequestSchema = z.object({
|
||||
caseId: z.string().uuid(),
|
||||
sessionId: z.string().uuid(),
|
||||
@@ -279,6 +304,98 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "message") {
|
||||
try {
|
||||
const dossier = await loadV9CaseDossier(accounting, userId, caseId);
|
||||
const focus = dossier.conversationSummary.activeFocus;
|
||||
if (focus) {
|
||||
const choice = parseAgentChoiceCopy(focus.expectedAnswerSchema);
|
||||
if (!choice) {
|
||||
return completedMessageResponse("当前问题已更新,请刷新后重新作答。", requestId, caseId);
|
||||
}
|
||||
let classified = null;
|
||||
try {
|
||||
classified = await classifyRectificationTurnIntent(selectedModel, {
|
||||
focus,
|
||||
userMessage: parsed.data.message ?? "",
|
||||
caseStatus,
|
||||
signal: request.signal,
|
||||
});
|
||||
} catch {
|
||||
classified = null;
|
||||
}
|
||||
if (!classified || classified.intent === "unclear") {
|
||||
const narration = "我没能确定这句话是否在回答当前问题。请点选下面的选项,或换一种说法。";
|
||||
await persistV9DeterministicTurn(accounting, userId, caseId, {
|
||||
requestId,
|
||||
userMessage: parsed.data.message ?? null,
|
||||
assistantMessage: narration,
|
||||
});
|
||||
return completedMessageResponse(narration, requestId, caseId);
|
||||
}
|
||||
if (classified.intent === "answer_current_focus") {
|
||||
if (!classified.answer_class) {
|
||||
return completedMessageResponse("当前问题已更新,请刷新后重新作答。", requestId, caseId);
|
||||
}
|
||||
const optionId = optionIdForAnswerClass(focus, classified.answer_class);
|
||||
if (!optionId) {
|
||||
return completedMessageResponse("当前问题已更新,请刷新后重新作答。", requestId, caseId);
|
||||
}
|
||||
const previous = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
|
||||
const applied = await applyRectificationChoice(accounting, {
|
||||
userId,
|
||||
caseId,
|
||||
sessionId,
|
||||
actionId: requestId,
|
||||
action: CHOICE_ACTION,
|
||||
focusId: focus.id,
|
||||
questionId: focus.questionId,
|
||||
probeId: typeof focus.expectedAnswerSchema.probe_id === "string"
|
||||
? focus.expectedAnswerSchema.probe_id
|
||||
: null,
|
||||
optionId,
|
||||
expectedRevision: previous?.revision ?? 0,
|
||||
userDisplay: parsed.data.message ?? null,
|
||||
});
|
||||
return completedMessageResponse(applied.narration, requestId, caseId);
|
||||
}
|
||||
if (classified.intent === "stop_rectification") {
|
||||
const previous = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
|
||||
const applied = await applyRectificationChoice(accounting, {
|
||||
userId,
|
||||
caseId,
|
||||
sessionId,
|
||||
actionId: requestId,
|
||||
action: STOP_ACTION,
|
||||
focusId: focus.id,
|
||||
questionId: focus.questionId,
|
||||
probeId: typeof focus.expectedAnswerSchema.probe_id === "string"
|
||||
? focus.expectedAnswerSchema.probe_id
|
||||
: null,
|
||||
optionId: "stop",
|
||||
expectedRevision: previous?.revision ?? 0,
|
||||
userDisplay: parsed.data.message ?? null,
|
||||
});
|
||||
await transitionV9CaseStatus(accounting, userId, caseId, "paused");
|
||||
return completedMessageResponse(applied.narration, requestId, caseId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof RectificationToolServiceError) {
|
||||
const mapped = mapRectificationRpcError(error);
|
||||
return NextResponse.json(
|
||||
{ error: mapped.message, message: mapped.message, code: mapped.code },
|
||||
{ status: mapped.status },
|
||||
);
|
||||
}
|
||||
console.error(`[rectification-v9] focus answer failed case=${caseId} reason=${error instanceof Error ? error.name : "Unknown"}`);
|
||||
return NextResponse.json(
|
||||
{ error: "校正服务暂时不可用", message: "请稍后重试。", code: "rectification_service_failed" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const requestTime = new Date();
|
||||
const chinaTime = new Date(requestTime.getTime() + 8 * 60 * 60 * 1000)
|
||||
.toISOString()
|
||||
|
||||
Reference in New Issue
Block a user