-
Notifications
You must be signed in to change notification settings - Fork 77
fix: issues#100 protocol routing drops #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import type { ProtocolRunState, RoutingContext } from "@datafoundry/agent-runtime"; | ||
| import type { MetadataStore } from "@datafoundry/metadata"; | ||
|
|
||
| import { MetadataProtocolStateStore } from "./protocol-state-store.js"; | ||
|
|
||
| type ResolveRoutingContextInput = { | ||
| metadataStore: MetadataStore; | ||
| /** Current run id (excluded when looking up the previous run). */ | ||
| runId: string; | ||
| selectedDatasourceId?: string; | ||
| /** Skill IDs active this run, if any. */ | ||
| selectedSkillIds?: string[]; | ||
| sessionId: string; | ||
| userId: string; | ||
| }; | ||
|
|
||
| /** | ||
| * Resolve the compact routing context for the protocol classifier from the | ||
| * previous run in the same session. Lets short follow-ups such as "再次尝试" | ||
| * ("try again") inherit the prior data-analysis intent instead of being routed | ||
| * to general-task on the ambiguous text alone. | ||
| * | ||
| * Returns undefined when there is no prior run in the session (first turn), | ||
| * so callers without history are unaffected. | ||
| */ | ||
| export const resolveRoutingContext = (input: ResolveRoutingContextInput): RoutingContext | undefined => { | ||
| const previousRun = input.metadataStore.runs.findPreviousRunBySession({ | ||
| user_id: input.userId, | ||
| session_id: input.sessionId, | ||
| exclude_run_id: input.runId | ||
| }); | ||
| if (!previousRun) { | ||
| return undefined; | ||
| } | ||
| const protocolStateStore = new MetadataProtocolStateStore(input.metadataStore, input.userId); | ||
| const previousProtocolState = protocolStateStore.find(previousRun.id); | ||
| const previousQuery = previousRun.user_input.trim() ? previousRun.user_input : null; | ||
| const previousProtocol = previousProtocolState | ||
| ? buildPreviousProtocol(previousProtocolState) | ||
| : null; | ||
| const selectedSkillIds = input.selectedSkillIds?.length ? [...input.selectedSkillIds] : null; | ||
| const selectedDatasourceId = input.selectedDatasourceId ?? null; | ||
| if (!previousQuery && !previousProtocol && !selectedSkillIds && !selectedDatasourceId) { | ||
| return undefined; | ||
| } | ||
| const routingContext: RoutingContext = { | ||
| ...(previousQuery ? { previousQuery } : {}), | ||
| ...(previousProtocol ? { previousProtocol } : {}), | ||
| ...(selectedSkillIds ? { selectedSkillIds } : {}), | ||
| ...(selectedDatasourceId ? { selectedDatasourceId } : {}) | ||
| }; | ||
| return routingContext; | ||
| }; | ||
|
|
||
| const buildPreviousProtocol = (state: ProtocolRunState): { | ||
| protocolId: string; | ||
| protocolVersion: string; | ||
| terminalStatus?: string; | ||
| } => { | ||
| const terminalStatus = state.status === "terminal" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 正确性风险 #2 —
|
||
| ? state.terminalDecision?.status | ||
| : state.status; | ||
| return { | ||
| protocolId: state.protocolId, | ||
| protocolVersion: state.protocolVersion, | ||
| ...(terminalStatus ? { terminalStatus } : {}) | ||
| }; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,17 +16,45 @@ const classificationSchema = z.object({ | |
| export const createProtocolClassificationPrompt = (input: { | ||
| candidates: ProtocolIdentity[]; | ||
| value: unknown; | ||
| }): string => [ | ||
| "你是协议路由分类器,不是执行任务的 Agent。", | ||
| "只能选择候选集合中的协议,不得发明协议或调用工具。", | ||
| "data-analysis 用于需要数据源、schema、SQL、指标、统计或数据结论的任务。", | ||
| "general-task 用于日常问答、解释、总结、文件、知识检索和普通协作任务。", | ||
| `候选集合: ${input.candidates.map((item) => `${item.protocolId}@${item.protocolVersion}`).join(", ")}`, | ||
| `分类输入: ${JSON.stringify(input.value)}`, | ||
| "只返回一个 JSON 对象,不要 Markdown。字段为 protocolId、protocolVersion、confidence、reasonCodes。", | ||
| '格式示例: {"protocolId":"data-analysis","protocolVersion":"1","confidence":0.91,"reasonCodes":["ANALYTIC_INTENT"]}', | ||
| "reasonCodes 只能使用大写英文与下划线。" | ||
| ].join("\n"); | ||
| }): string => { | ||
| const value = isRecord(input.value) ? input.value : {}; | ||
| const fields: string[] = []; | ||
| if (typeof value.userText === "string") { | ||
| fields.push(`当前用户查询: ${value.userText}`); | ||
| } | ||
| if (typeof value.previousQuery === "string") { | ||
| fields.push(`上一轮用户查询: ${value.previousQuery}`); | ||
| } | ||
| if (isRecord(value.previousProtocol)) { | ||
| const prev = value.previousProtocol; | ||
| const parts: string[] = []; | ||
| if (typeof prev.protocolId === "string") parts.push(prev.protocolId); | ||
| if (typeof prev.terminalStatus === "string") parts.push(`终态=${prev.terminalStatus}`); | ||
| if (parts.length > 0) fields.push(`上一轮协议: ${parts.join(" ")}`); | ||
| } | ||
| if (Array.isArray(value.selectedSkillIds) && value.selectedSkillIds.length > 0) { | ||
| fields.push(`已选 skill: ${value.selectedSkillIds.join(", ")}`); | ||
| } | ||
| if (typeof value.selectedDatasourceId === "string") { | ||
| fields.push(`已选数据源: ${value.selectedDatasourceId}`); | ||
| } | ||
| return [ | ||
| "你是协议路由分类器,不是执行任务的 Agent。", | ||
| "只能选择候选集合中的协议,不得发明协议或调用工具。", | ||
| "data-analysis 用于需要数据源、schema、SQL、指标、统计或数据结论的任务。", | ||
| "general-task 用于日常问答、解释、总结、文件、知识检索和普通协作任务。", | ||
| "当当前查询是弱后续(如 继续、重试、再试、再来一次、try again 等)且上一轮使用的是 data-analysis 时,", | ||
| "应倾向于延续 data-analysis,除非用户明确切换了任务主题。", | ||
| `候选集合: ${input.candidates.map((item) => `${item.protocolId}@${item.protocolVersion}`).join(", ")}`, | ||
| ...(fields.length > 0 ? ["分类输入:", ...fields] : [`分类输入: ${JSON.stringify(input.value)}`]), | ||
| "只返回一个 JSON 对象,不要 Markdown。字段为 protocolId、protocolVersion、confidence、reasonCodes。", | ||
| '格式示例: {"protocolId":"data-analysis","protocolVersion":"1","confidence":0.91,"reasonCodes":["INHERITED_PRIOR_PROTOCOL"]}', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 正确性风险 #5 —
|
||
| "reasonCodes 只能使用大写英文与下划线。" | ||
| ].join("\n"); | ||
| }; | ||
|
|
||
| const isRecord = (value: unknown): value is Record<string, unknown> => | ||
| typeof value === "object" && value !== null && !Array.isArray(value); | ||
|
|
||
| /** Parse model text into the strict classifier contract without trusting provider-specific JSON modes. */ | ||
| export const parseProtocolClassificationText = (text: string): z.infer<typeof classificationSchema> => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
正确性风险 #1 — "最近的终态 run" ≠ "用户正在延续的 run"
findPreviousRunBySession返回最近的终态 run。考虑以下序列:data-analysis,completedgeneral-task,completeddata-analysis,但resolveRoutingContext把第 2 轮视作previousProtocol = general-task喂给分类器。分类器看到previousProtocol=general-task很可能维持general-task—— 这加剧了该 bug,而非修复。设计把"最近的终态 run"与"承载用户正在延续意图的 run"等同起来,但二者并不总是相同。测试套件只覆盖了前一轮是
data-analysis的情况,没有覆盖这条间隙的general-task场景。建议: 要么向后遍历终态 run 直到遇到非默认协议,要么在 session 元数据里持久化一个粘性的
lastNonDefaultProtocol,让后续提示能可靠继承。并补一个测试:前一轮是general-task而其祖先是data-analysis。