Files
Jyotisha/frontend/src/lib/rectification-agentic/v9/turn-intent-classifier.ts
T
Jesse_ChenandCursor 3a0319694e
Independent Staging Quality Gate / validate (push) Successful in 13m7s
Independent Staging Quality Gate / publish (push) Successful in 29m19s
fix(rectification): keep same-sentence dated events after structured answers
The choice and collect fast paths applied the answer then returned,
so a dated event in the same utterance never reached the evidence
ledger. Idle persist also prechecked follow-up with a hardcoded
collect_evidence outcome instead of the dossier decision.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 23:18:07 +08:00

106 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Agent } from "@mastra/core/agent";
import { z } from "zod";
import type { ResolvedLanguageModel } from "@/mastra/model";
import type { AnswerClass } from "../core/types";
import { parseAgentChoiceCopy, type ChoiceKey } from "./choice-card";
import { isCollectFocusSchema } from "./server-focus";
import type { ConversationFocus } from "./tool-service";
const turnIntentSchema = z.object({
intent: z.enum([
"answer_current_focus",
"provide_new_evidence",
"stop_rectification",
"ask_about_result",
"unclear",
]),
answer_class: z.enum(["yes", "weak_yes", "no", "unsure"]).nullable(),
has_new_dated_event: z.boolean().optional(),
}).strict();
export type RectificationTurnIntent = z.infer<typeof turnIntentSchema>;
export function parseRectificationTurnIntent(value: unknown): RectificationTurnIntent | null {
const parsed = turnIntentSchema.safeParse(value);
if (!parsed.success) return null;
if (parsed.data.intent === "answer_current_focus") {
return parsed.data.answer_class ? parsed.data : null;
}
return parsed.data.answer_class === null ? parsed.data : null;
}
export function shouldDeclineCollectFocus(
classified: RectificationTurnIntent | null,
): boolean {
return classified?.intent === "answer_current_focus" && classified.answer_class === "no";
}
export function shouldContinueAgentForDatedEvent(
classified: RectificationTurnIntent | null,
): boolean {
return classified?.has_new_dated_event === true;
}
export function optionIdForAnswerClass(
focus: ConversationFocus,
answerClass: AnswerClass,
): ChoiceKey | null {
const copy = parseAgentChoiceCopy(focus.expectedAnswerSchema);
return copy?.options.find((option) => option.answer_class === answerClass)?.key ?? null;
}
export async function classifyRectificationTurnIntent(
model: ResolvedLanguageModel,
input: {
focus: ConversationFocus;
userMessage: string;
caseStatus: string;
signal?: AbortSignal;
},
): Promise<RectificationTurnIntent | null> {
const choice = parseAgentChoiceCopy(input.focus.expectedAnswerSchema);
const collectPrompt = isCollectFocusSchema(input.focus.expectedAnswerSchema)
? String(input.focus.expectedAnswerSchema.prompt ?? "").trim()
: "";
if (!choice && !collectPrompt) return null;
const agent = new Agent({
id: `rectification-focus-intent-${model.id}`,
name: "Rectification Focus Intent Classifier",
model: model.model,
instructions: choice
? `你只做当前生时校正问题的意图分类,不回答用户,也不修改任何状态。
结合当前问题和动态选项判断用户是在回答当前问题、提供新的带时间经历、要求停止整个校正、询问结果,还是语义不清。
若是在回答当前问题,answer_class 必须使用某个选项提供的 answer_class;否则 answer_class 必须为 null。
has_new_dated_event 仅在用户同一句里除了回答当前问题之外,还提供了新的、带大概时间的经历时为 true;单纯的否定或单纯的选项回答必须为 false。
若同一句话既回答了当前问题又补充了新的带时间经历,intent 仍为 answer_current_focushas_new_dated_event 为 true。
“当前方面没有、那段时间没有变化”通常是回答当前问题,不是停止整个流程;只有用户明确要求停止整个校正时才分类为 stop_rectification。
不要按 A/B/C/D 的位置猜语义,只按选项 label 与 answer_class 判断。`
: `你只做当前生时校正采集题的意图分类,不回答用户,也不修改任何状态。
当前问题没有点选选项。判断用户是在回答当前采集题、提供新的带时间经历、要求停止整个校正、询问结果,还是语义不清。
若用户明确表示这个方面没有发生过、没有这类事,intent 为 answer_current_focusanswer_class 为 no。
若用户只在补充带时间的经历、并没有回答当前采集题,intent 为 provide_new_evidenceanswer_class 必须为 null。
has_new_dated_event 仅在用户同一句里除了回答当前采集题之外,还提供了新的、带大概时间的经历时为 true;单纯的否定必须为 false。
若同一句话既明确否定当前采集题又补充了新的带时间经历,intent 仍为 answer_current_focus 且 answer_class 为 no,不要改成 provide_new_evidence。
“当前方面没有”通常是回答当前采集题,不是停止整个流程;只有用户明确要求停止整个校正时才分类为 stop_rectification。
不要按关键词表或正则猜测,只根据当前问题与用户这句话的语义分类。`,
});
const result = await agent.generate([{
role: "user",
content: JSON.stringify({
current_question: choice?.prompt ?? collectPrompt,
options: choice?.options ?? [],
user_message: input.userMessage,
case_status: input.caseStatus,
}),
}], {
abortSignal: input.signal,
structuredOutput: {
schema: turnIntentSchema,
jsonPromptInjection: "inline",
},
});
return parseRectificationTurnIntent(result.object);
}