dsh-data-domain-knowledge 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/cordis.patch.yml +410 -0
- package/eval/README.md +76 -0
- package/eval/evaluate-retrieval.mjs +121 -0
- package/eval/queries.example.jsonl +2 -0
- package/lib/client.js +111415 -0
- package/lib/types/config-schema.d.ts +35 -0
- package/lib/types/config-schema.d.ts.map +1 -0
- package/lib/types/config-schema.js +34 -0
- package/lib/types/config-schema.js.map +1 -0
- package/lib/types/convert/core-loader.d.ts +32 -0
- package/lib/types/convert/core-loader.d.ts.map +1 -0
- package/lib/types/convert/core-loader.js +60 -0
- package/lib/types/convert/core-loader.js.map +1 -0
- package/lib/types/convert/host.d.ts +88 -0
- package/lib/types/convert/host.d.ts.map +1 -0
- package/lib/types/convert/host.js +479 -0
- package/lib/types/convert/host.js.map +1 -0
- package/lib/types/convert/ingest.d.ts +125 -0
- package/lib/types/convert/ingest.d.ts.map +1 -0
- package/lib/types/convert/ingest.js +228 -0
- package/lib/types/convert/ingest.js.map +1 -0
- package/lib/types/convert/settings-schema.d.ts +18 -0
- package/lib/types/convert/settings-schema.d.ts.map +1 -0
- package/lib/types/convert/settings-schema.js +26 -0
- package/lib/types/convert/settings-schema.js.map +1 -0
- package/lib/types/convert/types.d.ts +46 -0
- package/lib/types/convert/types.d.ts.map +1 -0
- package/lib/types/convert/types.js +41 -0
- package/lib/types/convert/types.js.map +1 -0
- package/lib/types/diagnosis/cards.d.ts +71 -0
- package/lib/types/diagnosis/cards.d.ts.map +1 -0
- package/lib/types/diagnosis/cards.js +482 -0
- package/lib/types/diagnosis/cards.js.map +1 -0
- package/lib/types/diagnosis/problem-classes.d.ts +51 -0
- package/lib/types/diagnosis/problem-classes.d.ts.map +1 -0
- package/lib/types/diagnosis/problem-classes.js +66 -0
- package/lib/types/diagnosis/problem-classes.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +65 -0
- package/lib/types/diagnosis/schema.d.ts.map +1 -0
- package/lib/types/diagnosis/schema.js +208 -0
- package/lib/types/diagnosis/schema.js.map +1 -0
- package/lib/types/diagnosis/seed-source.d.ts +20 -0
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -0
- package/lib/types/diagnosis/seed-source.js +96 -0
- package/lib/types/diagnosis/seed-source.js.map +1 -0
- package/lib/types/diagnosis/seed.d.ts +50 -0
- package/lib/types/diagnosis/seed.d.ts.map +1 -0
- package/lib/types/diagnosis/seed.js +242 -0
- package/lib/types/diagnosis/seed.js.map +1 -0
- package/lib/types/diagnosis/store.d.ts +81 -0
- package/lib/types/diagnosis/store.d.ts.map +1 -0
- package/lib/types/diagnosis/store.js +409 -0
- package/lib/types/diagnosis/store.js.map +1 -0
- package/lib/types/diagnosis/types.d.ts +124 -0
- package/lib/types/diagnosis/types.d.ts.map +1 -0
- package/lib/types/diagnosis/types.js +13 -0
- package/lib/types/diagnosis/types.js.map +1 -0
- package/lib/types/directory-picker.d.ts +94 -0
- package/lib/types/directory-picker.d.ts.map +1 -0
- package/lib/types/directory-picker.js +110 -0
- package/lib/types/directory-picker.js.map +1 -0
- package/lib/types/extract-model.d.ts +76 -0
- package/lib/types/extract-model.d.ts.map +1 -0
- package/lib/types/extract-model.js +193 -0
- package/lib/types/extract-model.js.map +1 -0
- package/lib/types/extract.d.ts +121 -0
- package/lib/types/extract.d.ts.map +1 -0
- package/lib/types/extract.js +549 -0
- package/lib/types/extract.js.map +1 -0
- package/lib/types/fts.d.ts +96 -0
- package/lib/types/fts.d.ts.map +1 -0
- package/lib/types/fts.js +184 -0
- package/lib/types/fts.js.map +1 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +1112 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/intent/engine.d.ts +108 -0
- package/lib/types/intent/engine.d.ts.map +1 -0
- package/lib/types/intent/engine.js +501 -0
- package/lib/types/intent/engine.js.map +1 -0
- package/lib/types/intent/host.d.ts +88 -0
- package/lib/types/intent/host.d.ts.map +1 -0
- package/lib/types/intent/host.js +389 -0
- package/lib/types/intent/host.js.map +1 -0
- package/lib/types/intent/preset-tools.d.ts +37 -0
- package/lib/types/intent/preset-tools.d.ts.map +1 -0
- package/lib/types/intent/preset-tools.js +45 -0
- package/lib/types/intent/preset-tools.js.map +1 -0
- package/lib/types/intent/settings-schema.d.ts +59 -0
- package/lib/types/intent/settings-schema.d.ts.map +1 -0
- package/lib/types/intent/settings-schema.js +57 -0
- package/lib/types/intent/settings-schema.js.map +1 -0
- package/lib/types/intent/store.d.ts +75 -0
- package/lib/types/intent/store.d.ts.map +1 -0
- package/lib/types/intent/store.js +139 -0
- package/lib/types/intent/store.js.map +1 -0
- package/lib/types/intent/tools.d.ts +63 -0
- package/lib/types/intent/tools.d.ts.map +1 -0
- package/lib/types/intent/tools.js +108 -0
- package/lib/types/intent/tools.js.map +1 -0
- package/lib/types/intent/types.d.ts +189 -0
- package/lib/types/intent/types.d.ts.map +1 -0
- package/lib/types/intent/types.js +62 -0
- package/lib/types/intent/types.js.map +1 -0
- package/lib/types/knowledge-bases.d.ts +266 -0
- package/lib/types/knowledge-bases.d.ts.map +1 -0
- package/lib/types/knowledge-bases.js +1107 -0
- package/lib/types/knowledge-bases.js.map +1 -0
- package/lib/types/layout.d.ts +262 -0
- package/lib/types/layout.d.ts.map +1 -0
- package/lib/types/layout.js +312 -0
- package/lib/types/layout.js.map +1 -0
- package/lib/types/migrate.d.ts +110 -0
- package/lib/types/migrate.d.ts.map +1 -0
- package/lib/types/migrate.js +280 -0
- package/lib/types/migrate.js.map +1 -0
- package/lib/types/recall-rank.d.ts +158 -0
- package/lib/types/recall-rank.d.ts.map +1 -0
- package/lib/types/recall-rank.js +261 -0
- package/lib/types/recall-rank.js.map +1 -0
- package/lib/types/runtime.d.ts +568 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/runtime.js +3045 -0
- package/lib/types/runtime.js.map +1 -0
- package/lib/types/settings-schema.d.ts +117 -0
- package/lib/types/settings-schema.d.ts.map +1 -0
- package/lib/types/settings-schema.js +92 -0
- package/lib/types/settings-schema.js.map +1 -0
- package/lib/types/settings-volatile.d.ts +102 -0
- package/lib/types/settings-volatile.d.ts.map +1 -0
- package/lib/types/settings-volatile.js +114 -0
- package/lib/types/settings-volatile.js.map +1 -0
- package/lib/types/types.d.ts +799 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +387 -0
- package/lib/types/types.js.map +1 -0
- package/lib/types/wiki-types.d.ts +68 -0
- package/lib/types/wiki-types.d.ts.map +1 -0
- package/lib/types/wiki-types.js +322 -0
- package/lib/types/wiki-types.js.map +1 -0
- package/package.json +101 -0
- package/preset/tita-data-agent/agent.cordis.yml +376 -0
- package/preset/tita-data-agent/preset.yml +3 -0
- package/python/requirements.txt +8 -0
- package/scripts/doctor.mjs +70 -0
- package/scripts/e2e-pipeline.mjs +355 -0
- package/scripts/install-agent-preset.mjs +92 -0
- package/scripts/setup-embedding.sh +34 -0
- package/scripts/sync-agent-preset.mjs +188 -0
- package/src/client/AgentPresetCard.tsx +247 -0
- package/src/client/DirectoryPicker.tsx +357 -0
- package/src/client/InfoHint.tsx +65 -0
- package/src/client/IntentLogicTab.tsx +185 -0
- package/src/client/IntentTab.tsx +715 -0
- package/src/client/KnowledgeBaseTab.tsx +1454 -0
- package/src/client/KnowledgeGraphView.tsx +306 -0
- package/src/client/KnowledgePanel.tsx +381 -0
- package/src/client/KnowledgeStepRail.tsx +60 -0
- package/src/client/NodeSettings.tsx +392 -0
- package/src/client/PreviewSections.tsx +116 -0
- package/src/client/RecallPanel.tsx +399 -0
- package/src/client/TypeDefinitionDrawer.tsx +215 -0
- package/src/client/WikiFlowCard.tsx +226 -0
- package/src/client/WikiTypeEditor.tsx +332 -0
- package/src/client/agent-preset.ts +121 -0
- package/src/client/arch/AgentArchitectureGraph.tsx +219 -0
- package/src/client/arch/NodeLogicPanel.tsx +133 -0
- package/src/client/arch/ProblemDetailPanel.tsx +88 -0
- package/src/client/arch/model.ts +691 -0
- package/src/client/architecture.css +191 -0
- package/src/client/architecture.tsx +530 -0
- package/src/client/attribution/AttributionGraph.tsx +104 -0
- package/src/client/attribution/AttributionTab.tsx +332 -0
- package/src/client/attribution/CaseView.tsx +102 -0
- package/src/client/attribution/GraphStage.tsx +87 -0
- package/src/client/attribution/RunView.tsx +184 -0
- package/src/client/attribution/StageChrome.tsx +403 -0
- package/src/client/attribution/ViewSwitch.tsx +107 -0
- package/src/client/attribution/graph-options.ts +381 -0
- package/src/client/attribution/model.ts +462 -0
- package/src/client/config-scope.ts +119 -0
- package/src/client/convert/ConvertPanel.tsx +594 -0
- package/src/client/convert/styles.css +138 -0
- package/src/client/convert/styles.ts +49 -0
- package/src/client/format.ts +39 -0
- package/src/client/graph/layout.ts +181 -0
- package/src/client/graph.css +113 -0
- package/src/client/index.ts +98 -0
- package/src/client/problem-domains.ts +114 -0
- package/src/client/styles.css +940 -0
- package/src/client/styles.ts +11 -0
- package/src/client/useFitScale.ts +68 -0
- package/src/config-schema.ts +45 -0
- package/src/convert/core-loader.ts +68 -0
- package/src/convert/host.ts +579 -0
- package/src/convert/ingest.ts +367 -0
- package/src/convert/settings-schema.ts +27 -0
- package/src/convert/types.ts +65 -0
- package/src/diagnosis/cards.ts +498 -0
- package/src/diagnosis/problem-classes.ts +117 -0
- package/src/diagnosis/schema.ts +260 -0
- package/src/diagnosis/seed-source.ts +107 -0
- package/src/diagnosis/seed.ts +291 -0
- package/src/diagnosis/store.ts +432 -0
- package/src/diagnosis/types.ts +152 -0
- package/src/directory-picker.ts +164 -0
- package/src/extract-model.ts +227 -0
- package/src/extract.ts +632 -0
- package/src/fts.ts +203 -0
- package/src/index.ts +1091 -0
- package/src/intent/README.md +63 -0
- package/src/intent/engine.ts +527 -0
- package/src/intent/host.ts +462 -0
- package/src/intent/preset-tools.ts +48 -0
- package/src/intent/settings-schema.ts +63 -0
- package/src/intent/store.ts +143 -0
- package/src/intent/tools.ts +139 -0
- package/src/intent/types.ts +245 -0
- package/src/knowledge-bases.ts +1160 -0
- package/src/layout.ts +350 -0
- package/src/migrate.ts +262 -0
- package/src/python/embedding_worker.py +471 -0
- package/src/recall-rank.ts +299 -0
- package/src/runtime.ts +3069 -0
- package/src/settings-schema.ts +94 -0
- package/src/settings-volatile.ts +122 -0
- package/src/types.ts +1140 -0
- package/src/wiki-types.ts +357 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# dsh-data-domain-knowledge/intent
|
|
2
|
+
|
|
3
|
+
> 本模块原为独立插件 `@tita-data-agent/data-domain-intent`,于 0020 并入本插件,
|
|
4
|
+
> 因为「意图」Tab 早已长在 `KnowledgePanel` 里。对外契约(三个 `intent_*` 工具名、
|
|
5
|
+
> `/intent/*` 路由、`tita-data-agent-intent` 设置命名空间、`titaDataAgentIntent` 服务)
|
|
6
|
+
> 一个都没变。
|
|
7
|
+
|
|
8
|
+
Tita Data Agent 的意图**记录器**。Host 侧注册 `intent_record`、
|
|
9
|
+
`intent_store_view`、`intent_registry_list` 三个工具。
|
|
10
|
+
|
|
11
|
+
判断完全交给大模型:`intent_record` 只把模型给出的结论落成一条审计记录,不做
|
|
12
|
+
关键词匹配、不做状态机、不反问、不阻断当前请求,也不返回失败。模型漏调时,Host
|
|
13
|
+
在轮次结束(`turn/end`)补一条 `source: 'fallback'` 的 `unknown` 记录。
|
|
14
|
+
|
|
15
|
+
分类规则(两段式判定 + 预置分类 + act 词表)**就是 `intent_record` 的工具描述**,
|
|
16
|
+
0023 起不再单独注入 system prompt 段。工具描述是静态字符串,所以设置一变宿主就
|
|
17
|
+
dispose 旧的、按新文本重新注册(`ensureRecordTool`);没有开关,它始终在场。
|
|
18
|
+
|
|
19
|
+
## 记录与 ID
|
|
20
|
+
|
|
21
|
+
- 按 `session + turn` upsert:同一轮重复调用只留一条记录。
|
|
22
|
+
- 每个 actionable 轮次由 Host 在 `turn/start` 预分配 `subtaskId`,模型用
|
|
23
|
+
`relation` 决定这轮是新任务(`new_task`)还是已有任务的子任务
|
|
24
|
+
(`subtask_of` + `task_id`)。引用不存在的 task 会降级为 `new_task` 并给出
|
|
25
|
+
warning。
|
|
26
|
+
- `task_id` / `run_id` 可显式传入以续接链路或重试;`parentSubtaskId` 指向同一
|
|
27
|
+
task 下最近一条 subtask。
|
|
28
|
+
|
|
29
|
+
## 配置
|
|
30
|
+
|
|
31
|
+
分类、act、predicate 由 settings namespace `tita-data-agent-intent` 提供,`intent_registry_list`
|
|
32
|
+
与 `intent_record` 的工具描述都读同一份配置。
|
|
33
|
+
|
|
34
|
+
「意图」页分三个 tab:**意图记录**(只读审计流)/ **意图配置**(可写)/ **意图的逻辑**
|
|
35
|
+
(说明:什么是意图、为什么要配置、字段 schema、引用文献)。
|
|
36
|
+
|
|
37
|
+
**分类与 act 都可配置**(0023):卡片可以增删改停用、一键恢复出厂预置,写入走
|
|
38
|
+
`settings.<field>`;宿主在 `onChange` 里立刻把新配置灌回引擎并重挂工具描述,所以保存后
|
|
39
|
+
**下一轮就生效**,不需要重启。predicate 是真实工具名,保持只读。
|
|
40
|
+
|
|
41
|
+
每个分类带 `description`(这一类"是什么")与 `examples`(用户可能怎么说);每个 act 带
|
|
42
|
+
`label` 与 `description`("这一轮在做什么")。它们都进工具描述,判定因此是**两段式**的:
|
|
43
|
+
|
|
44
|
+
1. 先逐条比对预置分类,命中就**直接用该分类 id**;
|
|
45
|
+
2. 全不命中才自行判断——新起一个 id 或落 `unknown`。
|
|
46
|
+
|
|
47
|
+
预置项是模型的先验,不是"仅供参考的清单";模型不必每轮从零分类。停用的分类不出现在可用
|
|
48
|
+
清单里(只报数量),历史记录仍按原 id 解析。
|
|
49
|
+
|
|
50
|
+
## 下游 ID 透传
|
|
51
|
+
|
|
52
|
+
`tools/pre-execute` 明确禁止改写调用入参,因此 Host 不能在派发前注入 ID。透传
|
|
53
|
+
由消费方在解析 lineage 时回退读取:intent 插件通过 cordis 服务
|
|
54
|
+
`titaDataAgentIntent.activeLineage(sessionId)` 暴露当前任务 ID,data-table 的
|
|
55
|
+
`lineageOfArgs` 已在缺省时合并该值(显式传参优先)。data-notebook 与
|
|
56
|
+
data-domain-knowledge 的同类回退待接入。
|
|
57
|
+
|
|
58
|
+
## 已知限制
|
|
59
|
+
|
|
60
|
+
- 记录保存在进程内 `IntentStore`,按 TTL(默认 600 秒)过期,重启即丢;按
|
|
61
|
+
`taskId` 的历史反查与持久化尚未实现。
|
|
62
|
+
- 派发竞态:与 `intent_record` 并行的业务调用可能先于记录提交,此时回退到
|
|
63
|
+
「活跃 taskId + 本轮占位 subtaskId」,不保证完全精确。
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ACTS,
|
|
3
|
+
DEFAULT_INTENT_SETTINGS,
|
|
4
|
+
type IntentActDef,
|
|
5
|
+
type IntentRecord,
|
|
6
|
+
type IntentRecordInput,
|
|
7
|
+
type IntentRef,
|
|
8
|
+
type IntentRegistry,
|
|
9
|
+
type IntentRelation,
|
|
10
|
+
type IntentResult,
|
|
11
|
+
type IntentSettings,
|
|
12
|
+
type IntentSlotMap,
|
|
13
|
+
type IntentSource,
|
|
14
|
+
type IntentStoreView,
|
|
15
|
+
type IntentTaskSummary,
|
|
16
|
+
type IntentTurnState,
|
|
17
|
+
} from './types.js'
|
|
18
|
+
import { envelopeSuccess } from 'dsh-data-contracts/envelope'
|
|
19
|
+
import type { LineageContext } from 'dsh-data-contracts/refs'
|
|
20
|
+
|
|
21
|
+
function id(prefix = 'intent'): string {
|
|
22
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function clamp01(value: unknown): number | undefined {
|
|
26
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return undefined
|
|
27
|
+
return Math.max(0, Math.min(1, value))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function slotsOf(value: unknown): IntentSlotMap {
|
|
31
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
|
|
32
|
+
return Object.fromEntries(Object.entries(value as Record<string, unknown>).filter(([, item]) => item !== null && item !== undefined))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function refsOf(value: unknown): IntentRef[] {
|
|
36
|
+
if (!Array.isArray(value)) return []
|
|
37
|
+
return value.flatMap((ref): IntentRef[] => {
|
|
38
|
+
if (typeof ref === 'string' && ref.trim()) return [{ id: ref.trim() }]
|
|
39
|
+
if (ref && typeof ref === 'object' && typeof (ref as { id?: unknown }).id === 'string') {
|
|
40
|
+
const item = ref as { id: string; kind?: unknown; label?: unknown }
|
|
41
|
+
return [{ id: item.id, ...(typeof item.kind === 'string' ? { kind: item.kind } : {}), ...(typeof item.label === 'string' ? { label: item.label } : {}) }]
|
|
42
|
+
}
|
|
43
|
+
return []
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function safeLineage(value: unknown): Partial<LineageContext> {
|
|
48
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
|
|
49
|
+
const record = value as Record<string, unknown>
|
|
50
|
+
const result: Partial<LineageContext> = {}
|
|
51
|
+
for (const key of ['sessionId', 'turnId', 'intentId', 'taskId', 'subtaskId', 'parentSubtaskId', 'runId', 'traceId', 'domain'] as const) {
|
|
52
|
+
if (typeof record[key] === 'string' && record[key].trim()) result[key] = record[key].trim().slice(0, 160)
|
|
53
|
+
}
|
|
54
|
+
return result
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 规整 act 词表。
|
|
59
|
+
*
|
|
60
|
+
* 兼容两种形态:0023 之前的持久化配置是 `string[]`,现在是带 label 的对象数组。
|
|
61
|
+
* 老配置读回来只会缺 label / description,不会丢动作。
|
|
62
|
+
*/
|
|
63
|
+
function actsOf(value: unknown): IntentActDef[] {
|
|
64
|
+
if (!Array.isArray(value)) return DEFAULT_ACTS.map(def => ({ ...def }))
|
|
65
|
+
return value.flatMap((item): IntentActDef[] => {
|
|
66
|
+
if (typeof item === 'string' && item.trim()) {
|
|
67
|
+
const id = item.trim()
|
|
68
|
+
return [{ id, label: id, description: '', enabled: true }]
|
|
69
|
+
}
|
|
70
|
+
if (!item || typeof item !== 'object') return []
|
|
71
|
+
const record = item as Record<string, unknown>
|
|
72
|
+
const id = typeof record.id === 'string' ? record.id.trim() : ''
|
|
73
|
+
if (!id) return []
|
|
74
|
+
return [{
|
|
75
|
+
id,
|
|
76
|
+
label: typeof record.label === 'string' && record.label.trim() ? record.label.trim() : id,
|
|
77
|
+
description: typeof record.description === 'string' ? record.description : '',
|
|
78
|
+
enabled: record.enabled !== false,
|
|
79
|
+
}]
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 谓词词表。
|
|
85
|
+
*
|
|
86
|
+
* 与 `categories` / `acts` 同一个理由:`predicates` 是 schema 里唯一一个「数组 +
|
|
87
|
+
* volatile」的标量字段,而 0.1.7 解析出来的 volatile 字段是引用、不是值 —— 一个
|
|
88
|
+
* 漏解包的引用在这里会变成 `predicates.join('、')` 抛
|
|
89
|
+
* `join is not a function`(0029 实测,抛出点在 preset 的挂载行上,整个 preset
|
|
90
|
+
* 组合不出会话)。所以这里也兜一层:不是字符串数组就退回出厂谓词,而不是让整个
|
|
91
|
+
* preset 挂掉 —— 与 `normalizeKnowledge` 的取舍一致(一个配置疏漏不该把插件打挂)。
|
|
92
|
+
*/
|
|
93
|
+
function predicatesOf(value: unknown): string[] {
|
|
94
|
+
if (!Array.isArray(value)) return DEFAULT_INTENT_SETTINGS.predicates
|
|
95
|
+
const items = value.filter((item): item is string => typeof item === 'string')
|
|
96
|
+
return items.length ? items : DEFAULT_INTENT_SETTINGS.predicates
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 标量字段的同类兜底:类型不对就退回默认值(引用对象、`undefined`、字符串都算不对)。 */
|
|
100
|
+
function numberOr(value: unknown, fallback: number): number {
|
|
101
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function booleanOr(value: unknown, fallback: boolean): boolean {
|
|
105
|
+
return typeof value === 'boolean' ? value : fallback
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeSettings(settings: IntentSettings): IntentSettings {
|
|
109
|
+
return {
|
|
110
|
+
...settings,
|
|
111
|
+
clarifyThreshold: numberOr(settings.clarifyThreshold, DEFAULT_INTENT_SETTINGS.clarifyThreshold),
|
|
112
|
+
ttlSeconds: numberOr(settings.ttlSeconds, DEFAULT_INTENT_SETTINGS.ttlSeconds),
|
|
113
|
+
fallbackEnabled: booleanOr(settings.fallbackEnabled, DEFAULT_INTENT_SETTINGS.fallbackEnabled),
|
|
114
|
+
categories: Array.isArray(settings.categories) ? settings.categories : DEFAULT_INTENT_SETTINGS.categories,
|
|
115
|
+
acts: actsOf(settings.acts),
|
|
116
|
+
predicates: predicatesOf(settings.predicates),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Bounded audit store: one record per session turn, expiring after the configured TTL.
|
|
122
|
+
*
|
|
123
|
+
* 0033 起它同时是**工作集**与**留痕的入口**,两者的分工要说清:
|
|
124
|
+
*
|
|
125
|
+
* - 内存里这份是有 TTL 的**工作集**(默认 600 秒):血缘判定只关心最近的轮次,
|
|
126
|
+
* 一个十轮之前就结束的会话不该让新的一轮接到它的 task 上。
|
|
127
|
+
* - 磁盘上那份(`intent/records.ndjson`)是**留痕**,只追加、不随 TTL 消失。
|
|
128
|
+
* `sink` 在每次 `save` 时被调用,`hydrate` 在启动时把留痕读回工作集 ——
|
|
129
|
+
* 重启之后同一个会话的血缘因此还在(在 TTL 窗口内)。
|
|
130
|
+
*
|
|
131
|
+
* 为什么不干脆让 `records()` 直接读文件:那会让 TTL 名存实亡(文件永远有),
|
|
132
|
+
* 于是「工作集」这个概念消失,血缘会接到很久以前的 task 上。两件事分开,
|
|
133
|
+
* 各自的契约才都成立。
|
|
134
|
+
*/
|
|
135
|
+
export class IntentStore {
|
|
136
|
+
private readonly sessions = new Map<string, { records: IntentRecord[]; touched: number }>()
|
|
137
|
+
/**
|
|
138
|
+
* 每条被保存的记录都会经过它(落盘留痕)。
|
|
139
|
+
*
|
|
140
|
+
* 同步签名、由调用方自己吞异常:`save` 在记录路径的关键路径上,而留痕失败
|
|
141
|
+
* 不该让这一轮的意图记录失败 —— 「记录不拦截请求」这条契约对留痕也成立。
|
|
142
|
+
*/
|
|
143
|
+
private sink?: (record: IntentRecord) => void
|
|
144
|
+
constructor(private ttlSeconds = DEFAULT_INTENT_SETTINGS.ttlSeconds, private readonly now = () => Date.now()) {}
|
|
145
|
+
|
|
146
|
+
/** 装上一个留痕出口。多次调用以最后一次为准。 */
|
|
147
|
+
setSink(sink: (record: IntentRecord) => void): void {
|
|
148
|
+
this.sink = sink
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 把留痕读回工作集(启动时调用一次)。
|
|
153
|
+
*
|
|
154
|
+
* `touched` 用记录自己的 `updatedAt`,于是「重启」不会把 TTL 归零:一份十小时
|
|
155
|
+
* 前的记录在 TTL 600 秒下本来就该过期,重启不该让它复活。
|
|
156
|
+
*/
|
|
157
|
+
hydrate(records: IntentRecord[]): number {
|
|
158
|
+
let loaded = 0
|
|
159
|
+
for (const record of records) {
|
|
160
|
+
const item = this.sessions.get(record.sessionKey) ?? { records: [], touched: 0 }
|
|
161
|
+
const index = item.records.findIndex(existing => existing.turnKey === record.turnKey)
|
|
162
|
+
if (index >= 0) item.records[index] = record
|
|
163
|
+
else item.records.push(record)
|
|
164
|
+
item.records = item.records.slice(-100)
|
|
165
|
+
const at = Date.parse(String(record.updatedAt ?? record.createdAt))
|
|
166
|
+
item.touched = Math.max(item.touched, Number.isFinite(at) ? at : this.now())
|
|
167
|
+
this.sessions.set(record.sessionKey, item)
|
|
168
|
+
loaded += 1
|
|
169
|
+
}
|
|
170
|
+
return loaded
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
setTtl(seconds: number): void {
|
|
174
|
+
this.ttlSeconds = Math.max(1, Math.min(86400, Math.round(seconds)))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private entry(sessionKey: string) {
|
|
178
|
+
const current = this.sessions.get(sessionKey)
|
|
179
|
+
if (!current || this.now() - current.touched > this.ttlSeconds * 1000) {
|
|
180
|
+
const fresh: { records: IntentRecord[]; touched: number } = { records: [], touched: this.now() }
|
|
181
|
+
this.sessions.set(sessionKey, fresh)
|
|
182
|
+
return fresh
|
|
183
|
+
}
|
|
184
|
+
current.touched = this.now()
|
|
185
|
+
return current
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Upsert by turn key so repeated records within one turn stay a single fact. */
|
|
189
|
+
save(record: IntentRecord): IntentRecord {
|
|
190
|
+
const item = this.entry(record.sessionKey)
|
|
191
|
+
const index = item.records.findIndex(existing => existing.turnKey === record.turnKey)
|
|
192
|
+
if (index >= 0) item.records[index] = record
|
|
193
|
+
else item.records.push(record)
|
|
194
|
+
item.records = item.records.slice(-100)
|
|
195
|
+
try { this.sink?.(record) } catch { /* 留痕失败不影响这一轮 */ }
|
|
196
|
+
return record
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
records(sessionKey: string): IntentRecord[] {
|
|
200
|
+
return this.entry(sessionKey).records
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
last(sessionKey: string): IntentRecord | undefined {
|
|
204
|
+
return this.entry(sessionKey).records.at(-1)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
findByTurn(sessionKey: string, turnKey: string): IntentRecord | undefined {
|
|
208
|
+
return this.entry(sessionKey).records.find(record => record.turnKey === turnKey)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
taskExists(sessionKey: string, taskId: string): boolean {
|
|
212
|
+
return this.entry(sessionKey).records.some(record => record.lineage.taskId === taskId)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
lastSubtaskOf(sessionKey: string, taskId: string): string | undefined {
|
|
216
|
+
return this.entry(sessionKey).records.filter(record => record.lineage.taskId === taskId).at(-1)?.lineage.subtaskId
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
expiresAt(sessionKey: string): string | undefined {
|
|
220
|
+
const item = this.entry(sessionKey)
|
|
221
|
+
return item.records.length > 0 ? new Date(item.touched + this.ttlSeconds * 1000).toISOString() : undefined
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
clear(sessionKey: string): void {
|
|
225
|
+
this.sessions.delete(sessionKey)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** When the session was last touched (ms since epoch); 0 when unknown. */
|
|
229
|
+
touchedAt(sessionKey: string): number {
|
|
230
|
+
return this.sessions.get(sessionKey)?.touched ?? 0
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Live session keys, most recently touched first. Expired sessions are skipped. */
|
|
234
|
+
sessionKeys(): string[] {
|
|
235
|
+
const now = this.now()
|
|
236
|
+
const live: Array<{ key: string; touched: number }> = []
|
|
237
|
+
for (const [key, item] of this.sessions) {
|
|
238
|
+
if (now - item.touched > this.ttlSeconds * 1000) continue
|
|
239
|
+
live.push({ key, touched: item.touched })
|
|
240
|
+
}
|
|
241
|
+
return live.sort((left, right) => right.touched - left.touched).map(item => item.key)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Intent recorder. Every entry point is total: an unrecognized act, an unknown
|
|
247
|
+
* task reference, or a missing classification degrade into warnings on a stored
|
|
248
|
+
* record. Nothing here blocks, confirms, or rewrites the user's request.
|
|
249
|
+
*/
|
|
250
|
+
export class IntentEngine {
|
|
251
|
+
readonly store: IntentStore
|
|
252
|
+
private settings: IntentSettings
|
|
253
|
+
private readonly turns = new Map<string, IntentTurnState>()
|
|
254
|
+
|
|
255
|
+
constructor(settings: Partial<IntentSettings> = {}, store?: IntentStore) {
|
|
256
|
+
this.settings = normalizeSettings({ ...DEFAULT_INTENT_SETTINGS, ...settings })
|
|
257
|
+
this.store = store ?? new IntentStore(this.settings.ttlSeconds)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
setSettings(settings: Partial<IntentSettings>): void {
|
|
261
|
+
this.settings = normalizeSettings({ ...this.settings, ...settings })
|
|
262
|
+
this.store.setTtl(this.settings.ttlSeconds)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
getSettings(): IntentSettings {
|
|
266
|
+
return this.settings
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Open a turn and allocate its provisional ids before the model records.
|
|
271
|
+
* Downstream calls dispatched in parallel with the record can already carry
|
|
272
|
+
* an id; the record later commits the authoritative task binding.
|
|
273
|
+
*/
|
|
274
|
+
beginTurn(sessionKey: string, turn: number, userInput: string): IntentTurnState {
|
|
275
|
+
const state: IntentTurnState = {
|
|
276
|
+
turn,
|
|
277
|
+
userInput: userInput.slice(0, 2000),
|
|
278
|
+
subtaskId: id('subtask'),
|
|
279
|
+
taskId: this.store.last(sessionKey)?.lineage.taskId,
|
|
280
|
+
committed: false,
|
|
281
|
+
}
|
|
282
|
+
this.turns.set(sessionKey, state)
|
|
283
|
+
return state
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Backfill one `unknown` record when a turn ends without a model record. */
|
|
287
|
+
fallbackWhenMissing(sessionKey: string, turn: number): IntentResult | undefined {
|
|
288
|
+
if (!this.settings.fallbackEnabled) return undefined
|
|
289
|
+
const state = this.turns.get(sessionKey)
|
|
290
|
+
if (!state || state.turn !== turn || state.committed) return undefined
|
|
291
|
+
const result = this.record({
|
|
292
|
+
userInput: state.userInput,
|
|
293
|
+
act: 'unknown',
|
|
294
|
+
category: 'unknown',
|
|
295
|
+
relation: 'unknown',
|
|
296
|
+
sessionKey,
|
|
297
|
+
}, 'fallback')
|
|
298
|
+
if (result.ok) {
|
|
299
|
+
state.committed = true
|
|
300
|
+
this.turns.delete(sessionKey)
|
|
301
|
+
}
|
|
302
|
+
return result
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Record the model's intent judgement. Always stores a record. */
|
|
306
|
+
record(input: IntentRecordInput, source: IntentSource = 'model'): IntentResult {
|
|
307
|
+
const sessionKey = input.sessionKey?.trim() || 'default'
|
|
308
|
+
const timestamp = new Date(input.now ?? Date.now()).toISOString()
|
|
309
|
+
const warnings: string[] = []
|
|
310
|
+
const state = this.turns.get(sessionKey)
|
|
311
|
+
const turnKey = state ? `${sessionKey}#${state.turn}` : `${sessionKey}#${input.turnId?.trim() || 'manual'}`
|
|
312
|
+
const existing = this.store.findByTurn(sessionKey, turnKey)
|
|
313
|
+
const lineageInput = safeLineage(input.lineage)
|
|
314
|
+
|
|
315
|
+
const act = input.act?.trim() || 'unknown'
|
|
316
|
+
const category = input.category?.trim() || undefined
|
|
317
|
+
const confidence = clamp01(input.confidence)
|
|
318
|
+
if (!input.userInput?.trim()) warnings.push('user_input 为空,仅记录了一条空意图')
|
|
319
|
+
if (act !== 'unknown' && !this.settings.acts.some(item => item.id === act && item.enabled)) warnings.push(`act "${act}" 不在已启用的 act 列表中,已按原样记录`)
|
|
320
|
+
if (category && !this.settings.categories.some(item => item.id === category && item.enabled)) warnings.push(`category "${category}" 不是已启用的分类,已按原样记录`)
|
|
321
|
+
const lowConfidence = confidence !== undefined && confidence < this.settings.clarifyThreshold
|
|
322
|
+
if (lowConfidence) warnings.push(`confidence ${confidence.toFixed(2)} 低于阈值 ${this.settings.clarifyThreshold.toFixed(2)},已标记 lowConfidence`)
|
|
323
|
+
|
|
324
|
+
const requestedTaskId = lineageInput.taskId
|
|
325
|
+
let relation: IntentRelation = input.relation ?? (requestedTaskId ? 'subtask_of' : 'new_task')
|
|
326
|
+
let taskId: string | undefined
|
|
327
|
+
let parentSubtaskId: string | undefined
|
|
328
|
+
if (relation === 'subtask_of') {
|
|
329
|
+
if (requestedTaskId && this.store.taskExists(sessionKey, requestedTaskId)) {
|
|
330
|
+
taskId = requestedTaskId
|
|
331
|
+
parentSubtaskId = this.store.lastSubtaskOf(sessionKey, requestedTaskId)
|
|
332
|
+
} else {
|
|
333
|
+
warnings.push(requestedTaskId
|
|
334
|
+
? `task_id "${requestedTaskId}" 在本会话不存在,本轮已按 new_task 记录`
|
|
335
|
+
: 'relation=subtask_of 缺少 task_id,本轮已按 new_task 记录')
|
|
336
|
+
relation = 'new_task'
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (!taskId) {
|
|
340
|
+
taskId = relation === 'new_task'
|
|
341
|
+
? id('task')
|
|
342
|
+
: existing?.lineage.taskId ?? state?.taskId ?? id('task')
|
|
343
|
+
}
|
|
344
|
+
const subtaskId = existing?.lineage.subtaskId ?? state?.subtaskId ?? id('subtask')
|
|
345
|
+
const runId = lineageInput.runId ?? existing?.lineage.runId ?? id('run')
|
|
346
|
+
const intentId = existing?.id ?? id('intent')
|
|
347
|
+
const turnId = input.turnId?.trim() || existing?.turnId || (state ? `turn-${state.turn}` : id('turn'))
|
|
348
|
+
|
|
349
|
+
const record: IntentRecord = {
|
|
350
|
+
id: intentId,
|
|
351
|
+
sessionKey,
|
|
352
|
+
turnKey,
|
|
353
|
+
turnId,
|
|
354
|
+
userInput: input.userInput ?? '',
|
|
355
|
+
act,
|
|
356
|
+
...(category ? { category } : {}),
|
|
357
|
+
...(input.predicate?.trim() ? { predicate: input.predicate.trim() } : {}),
|
|
358
|
+
slots: slotsOf(input.slots),
|
|
359
|
+
...(confidence !== undefined ? { confidence } : {}),
|
|
360
|
+
...(lowConfidence ? { lowConfidence: true } : {}),
|
|
361
|
+
refs: refsOf(input.refs),
|
|
362
|
+
relation,
|
|
363
|
+
source,
|
|
364
|
+
createdAt: existing?.createdAt ?? timestamp,
|
|
365
|
+
updatedAt: timestamp,
|
|
366
|
+
lineage: {
|
|
367
|
+
...lineageInput,
|
|
368
|
+
sessionId: lineageInput.sessionId ?? sessionKey,
|
|
369
|
+
turnId,
|
|
370
|
+
intentId,
|
|
371
|
+
taskId,
|
|
372
|
+
subtaskId,
|
|
373
|
+
...(parentSubtaskId ? { parentSubtaskId } : {}),
|
|
374
|
+
runId,
|
|
375
|
+
},
|
|
376
|
+
meta: {},
|
|
377
|
+
}
|
|
378
|
+
this.store.save(record)
|
|
379
|
+
if (state) {
|
|
380
|
+
state.committed = true
|
|
381
|
+
state.taskId = taskId
|
|
382
|
+
state.subtaskId = subtaskId
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Built through the shared constructor. This literal used to omit
|
|
386
|
+
// `error: null`, which made an intent result the one envelope in the repo
|
|
387
|
+
// that did not satisfy spec/contracts/envelope.json.
|
|
388
|
+
return envelopeSuccess({
|
|
389
|
+
intentId,
|
|
390
|
+
taskId,
|
|
391
|
+
subtaskId,
|
|
392
|
+
...(parentSubtaskId ? { parentSubtaskId } : {}),
|
|
393
|
+
runId,
|
|
394
|
+
turnId,
|
|
395
|
+
act,
|
|
396
|
+
...(category ? { category } : {}),
|
|
397
|
+
relation,
|
|
398
|
+
source,
|
|
399
|
+
lowConfidence,
|
|
400
|
+
activeTasks: this.tasks(sessionKey),
|
|
401
|
+
}, warnings)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Ids a downstream tool call in this session should carry. */
|
|
405
|
+
activeLineage(sessionKey: string): { lineage: Partial<LineageContext>; committed: boolean } | undefined {
|
|
406
|
+
const last = this.store.last(sessionKey)
|
|
407
|
+
if (last) return { lineage: { ...last.lineage }, committed: this.turns.get(sessionKey)?.committed ?? true }
|
|
408
|
+
const state = this.turns.get(sessionKey)
|
|
409
|
+
if (!state) return undefined
|
|
410
|
+
return { lineage: { sessionId: sessionKey, ...(state.taskId ? { taskId: state.taskId } : {}), subtaskId: state.subtaskId }, committed: false }
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
tasks(sessionKey: string): IntentTaskSummary[] {
|
|
414
|
+
const now = Date.now()
|
|
415
|
+
const seen = new Map<string, IntentTaskSummary>()
|
|
416
|
+
for (const record of this.store.records(sessionKey)) {
|
|
417
|
+
const taskId = record.lineage.taskId
|
|
418
|
+
if (!taskId) continue
|
|
419
|
+
seen.set(taskId, {
|
|
420
|
+
taskId,
|
|
421
|
+
...(record.lineage.subtaskId ? { lastSubtaskId: record.lineage.subtaskId } : {}),
|
|
422
|
+
...(record.category ? { category: record.category } : {}),
|
|
423
|
+
ageSeconds: Math.max(0, Math.round((now - Date.parse(record.updatedAt)) / 1000)),
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
return [...seen.values()].slice(-3).reverse()
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
view(sessionKey = 'default', includePrompt = false): IntentResult {
|
|
430
|
+
const last = this.store.last(sessionKey)
|
|
431
|
+
const view: IntentStoreView = {
|
|
432
|
+
sessionKey,
|
|
433
|
+
...(last?.lineage.taskId ? { activeTaskId: last.lineage.taskId } : {}),
|
|
434
|
+
...(last ? { activeIntentId: last.id } : {}),
|
|
435
|
+
tasks: this.tasks(sessionKey),
|
|
436
|
+
recent: this.store.records(sessionKey).slice(-10).reverse(),
|
|
437
|
+
...(this.store.expiresAt(sessionKey) ? { expiresAt: this.store.expiresAt(sessionKey) } : {}),
|
|
438
|
+
}
|
|
439
|
+
return envelopeSuccess({ store: view, ...(includePrompt ? { prompt: this.routingPrompt() } : {}) })
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Session-level index for the 意图 panel: every live session with its last
|
|
444
|
+
* classification and active task ids. Records themselves stay per-session.
|
|
445
|
+
*/
|
|
446
|
+
overview(): { sessions: Array<Record<string, unknown>> } {
|
|
447
|
+
const sessions = this.store.sessionKeys().map(sessionKey => {
|
|
448
|
+
const last = this.store.last(sessionKey)
|
|
449
|
+
return {
|
|
450
|
+
sessionKey,
|
|
451
|
+
recordCount: this.store.records(sessionKey).length,
|
|
452
|
+
lastAt: new Date(this.store.touchedAt(sessionKey)).toISOString(),
|
|
453
|
+
tasks: this.tasks(sessionKey),
|
|
454
|
+
...(last ? { activeIntentId: last.id } : {}),
|
|
455
|
+
...(last?.act ? { lastAct: last.act } : {}),
|
|
456
|
+
...(last?.category ? { lastCategory: last.category } : {}),
|
|
457
|
+
...(last?.relation ? { lastRelation: last.relation } : {}),
|
|
458
|
+
...(last?.lineage.taskId ? { activeTaskId: last.lineage.taskId } : {}),
|
|
459
|
+
...(last && last.confidence !== undefined ? { lastConfidence: last.confidence } : {}),
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
return { sessions }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
registry(): IntentRegistry {
|
|
466
|
+
return {
|
|
467
|
+
acts: this.settings.acts.map(def => ({ ...def })),
|
|
468
|
+
categories: this.settings.categories.map(category => ({
|
|
469
|
+
id: category.id,
|
|
470
|
+
label: category.label,
|
|
471
|
+
route: category.route,
|
|
472
|
+
description: category.description ?? '',
|
|
473
|
+
examples: [...(category.examples ?? [])],
|
|
474
|
+
enabled: category.enabled,
|
|
475
|
+
})),
|
|
476
|
+
predicates: [...this.settings.predicates],
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Model-facing contract: record first, then continue; the record never gates the request.
|
|
482
|
+
*
|
|
483
|
+
* 这段文本就是 `intent_record` 的**工具描述**(0023 起不再单独注入 system prompt 段),
|
|
484
|
+
* 所以它**没有开关**:分类规则必须始终随工具在场。宿主在设置变更时重新注册工具,
|
|
485
|
+
* 因此改完分类下一轮就是新文本。
|
|
486
|
+
*
|
|
487
|
+
* 分类判定是**两段式**的(issue 0023):
|
|
488
|
+
* 1. 先拿用户意思去比对预置分类的 `description` / `examples`,命中就**直接用该 id**;
|
|
489
|
+
* 2. 全不命中才允许自行判断(新起一个 id 或落 `unknown`)。
|
|
490
|
+
*
|
|
491
|
+
* 这样预置项就是"先验",而不是一份仅供参考的清单——模型不必每轮从零分类。
|
|
492
|
+
*/
|
|
493
|
+
routingPrompt(): string {
|
|
494
|
+
const enabled = this.settings.categories.filter(category => category.enabled)
|
|
495
|
+
const disabledCount = this.settings.categories.length - enabled.length
|
|
496
|
+
const routes = enabled
|
|
497
|
+
.map(category => {
|
|
498
|
+
const head = `- ${category.id}(${category.label})→ ${category.route}${category.description ? `:${category.description}` : ''}`
|
|
499
|
+
const examples = (category.examples ?? []).filter(Boolean)
|
|
500
|
+
return examples.length > 0 ? `${head}\n 例如:${examples.join(' / ')}` : head
|
|
501
|
+
})
|
|
502
|
+
.join('\n')
|
|
503
|
+
const acts = this.settings.acts.filter(def => def.enabled)
|
|
504
|
+
.map(def => `- ${def.id}${def.label && def.label !== def.id ? `(${def.label})` : ''}${def.description ? `:${def.description}` : ''}`)
|
|
505
|
+
.join('\n')
|
|
506
|
+
return [
|
|
507
|
+
'记录本轮用户意图:先判断,再调用 intent_record 落一条审计记录,然后继续完成请求——记录不改变、不阻塞、也不需要用户确认。',
|
|
508
|
+
'当前轮是新的任务用 relation=new_task;如果是在延续某个已有任务(追问、补充、纠正),用 relation=subtask_of 并带上该任务的 task_id;无法判断时省略 relation。',
|
|
509
|
+
'',
|
|
510
|
+
'分类判定分两步,必须先匹配预置、再自行判断:',
|
|
511
|
+
'1) 先把用户的意思和下面的「预置分类」逐条比对——看定义,也看"例如"里的说法。命中就直接用该分类的 id,不要另起一个近义的新 id,也不要因为措辞不同就换一个分类。',
|
|
512
|
+
'2) 所有预置分类都不命中时,才自行判断:可以按语义新起一个短语作为分类 id,或落 unknown。新起的 id 用英文小写下划线,并在 slots 里带上关键参数。',
|
|
513
|
+
'',
|
|
514
|
+
'预置分类(category 参数传 id):',
|
|
515
|
+
routes,
|
|
516
|
+
...(disabledCount > 0 ? [`(另有 ${disabledCount} 个分类已停用,不要使用。)`] : []),
|
|
517
|
+
'',
|
|
518
|
+
'act 回答"这一轮用户在做什么",挑最贴近的一个:',
|
|
519
|
+
acts,
|
|
520
|
+
`可用 predicate:${this.settings.predicates.join('、')}`,
|
|
521
|
+
].join('\n')
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function createIntentEngine(settings?: Partial<IntentSettings>): IntentEngine {
|
|
526
|
+
return new IntentEngine(settings)
|
|
527
|
+
}
|