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>
106 lines
5.0 KiB
TypeScript
106 lines
5.0 KiB
TypeScript
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_focus,has_new_dated_event 为 true。
|
||
“当前方面没有、那段时间没有变化”通常是回答当前问题,不是停止整个流程;只有用户明确要求停止整个校正时才分类为 stop_rectification。
|
||
不要按 A/B/C/D 的位置猜语义,只按选项 label 与 answer_class 判断。`
|
||
: `你只做当前生时校正采集题的意图分类,不回答用户,也不修改任何状态。
|
||
当前问题没有点选选项。判断用户是在回答当前采集题、提供新的带时间经历、要求停止整个校正、询问结果,还是语义不清。
|
||
若用户明确表示这个方面没有发生过、没有这类事,intent 为 answer_current_focus,answer_class 为 no。
|
||
若用户只在补充带时间的经历、并没有回答当前采集题,intent 为 provide_new_evidence,answer_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);
|
||
}
|