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,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 召回与重排的**真实运行面板**。
|
|
3
|
+
*
|
|
4
|
+
* 从 `KnowledgePanel` 里抽出来,是因为召回 / 重排现在是知识库下的两个分栏,
|
|
5
|
+
* 而评测 Tab 还要用同一套能力。三处判定口径若各写一遍,就会出现「同一个问题在
|
|
6
|
+
* 召回栏命中 3 条、在评测栏命中 5 条」这种最伤信任的差异。
|
|
7
|
+
*
|
|
8
|
+
* 只报真数据:命中分块、命中路线、得分都不省略。WeKnora 的检索测试同样把
|
|
9
|
+
* `chunk_index` / `score` 一起给出 —— 只报「命中 N 条」看不出召回质量。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
13
|
+
import { Check, CircleHelp, Play, Search } from '@tita-data-agent/ui/icons'
|
|
14
|
+
import { Badge, Button, Card, Input, SelectMenu, cn } from '@tita-data-agent/ui/components'
|
|
15
|
+
import { styles } from './styles.ts'
|
|
16
|
+
|
|
17
|
+
/** 一条命中的 wikiNote。`match` 是**真正贡献了这次命中**的路线。 */
|
|
18
|
+
export interface RecallHitView {
|
|
19
|
+
unitId: string
|
|
20
|
+
title: string
|
|
21
|
+
heading?: string
|
|
22
|
+
snippet: string
|
|
23
|
+
match: string
|
|
24
|
+
score: number
|
|
25
|
+
status?: string
|
|
26
|
+
entityType?: string
|
|
27
|
+
rerankScore?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 进入 Context Pack 的一条:命中的 wikiNote + 它解析出的父级 wiki 标题。 */
|
|
31
|
+
export interface ContextItemView {
|
|
32
|
+
unitId: string
|
|
33
|
+
title: string
|
|
34
|
+
snippet: string
|
|
35
|
+
score: number
|
|
36
|
+
rerankScore?: number
|
|
37
|
+
wikiTitle?: string
|
|
38
|
+
wikiId?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RecallResultView {
|
|
42
|
+
count: number
|
|
43
|
+
backend: string
|
|
44
|
+
routes?: string[]
|
|
45
|
+
contextCount: number
|
|
46
|
+
recallTopK: number
|
|
47
|
+
/** 融合与重排作用的候选池深度(0031)。它与 `recallTopK` 是两个数。 */
|
|
48
|
+
candidatePool?: number
|
|
49
|
+
contextTopN: number
|
|
50
|
+
rerank?: string
|
|
51
|
+
rerankProvider?: string
|
|
52
|
+
rerankThreshold?: number
|
|
53
|
+
rerankDropped?: number
|
|
54
|
+
/** 候选池里最高的原始 logit(0031)—— `dropped: 0` 时用它区分「都相关」与「阈值没在工作」。 */
|
|
55
|
+
rerankTopLogit?: number
|
|
56
|
+
/** cross-encoder 分词器词表大小;退化成 5 就该报警(0031)。 */
|
|
57
|
+
rerankVocabSize?: number
|
|
58
|
+
/** 一路都没过阈值:知识库里没有能回答这个问题的东西(0031)。 */
|
|
59
|
+
insufficientEvidence?: boolean
|
|
60
|
+
/** 每一路的真实状态与降级原因。 */
|
|
61
|
+
routesDetail?: Array<{ route: string; status: string; candidates?: number; kept?: number; topScore?: number; threshold?: number; reason?: string }>
|
|
62
|
+
graphBoost?: { applied: number; weight: number }
|
|
63
|
+
abstention?: string
|
|
64
|
+
diagnosticsReason?: string
|
|
65
|
+
encoder?: { ok: boolean; reason?: string }
|
|
66
|
+
hits: RecallHitView[]
|
|
67
|
+
context: ContextItemView[]
|
|
68
|
+
kbName?: string
|
|
69
|
+
indexVersion?: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface Envelope<T> { ok?: boolean; data?: T; error?: { code?: string; message?: string; suggestion?: string } | null }
|
|
73
|
+
|
|
74
|
+
async function requestJson<T>(url: string): Promise<Envelope<T>> {
|
|
75
|
+
const response = await fetch(url)
|
|
76
|
+
const text = await response.text()
|
|
77
|
+
if (!text.trim()) throw new Error(`服务端返回空响应(HTTP ${response.status})`)
|
|
78
|
+
let payload: Envelope<T>
|
|
79
|
+
try { payload = JSON.parse(text) as Envelope<T> } catch { throw new Error(`服务端返回了无效响应(HTTP ${response.status})`) }
|
|
80
|
+
const error = payload.error
|
|
81
|
+
if (!response.ok || payload.ok === false) {
|
|
82
|
+
const detail = [error?.code, error?.message].filter(Boolean).join(' · ') || `请求失败(${response.status})`
|
|
83
|
+
throw new Error(error?.suggestion ? `${detail} — ${error.suggestion}` : detail)
|
|
84
|
+
}
|
|
85
|
+
return payload
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 召回是**两路**(关键词 BM25 + 向量)加一个图谱 boost(0031)。
|
|
90
|
+
*
|
|
91
|
+
* 图谱不再是独立一路:它不能制造候选,只给已经在候选池里的条目加权。所以这一行显示的是
|
|
92
|
+
* 「加权了几条」,而不是「跑没跑」—— 后者对它已经没有意义。
|
|
93
|
+
*/
|
|
94
|
+
export const RECALL_ROUTES: Array<{ id: string; label: string; hint: string }> = [
|
|
95
|
+
{ id: 'fts', label: '关键词', hint: 'FTS5 + BM25(中文按双字词切分)' },
|
|
96
|
+
{ id: 'vector', label: '向量', hint: 'WeMM 向量近邻(binary 预筛 + int8 精算余弦)' },
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
export const RECALL_MODES: Array<{ id: string; label: string }> = [
|
|
100
|
+
{ id: 'hybrid', label: '融合 + 图谱 boost(默认)' },
|
|
101
|
+
{ id: 'fts', label: '仅关键词' },
|
|
102
|
+
{ id: 'vector', label: '仅向量' },
|
|
103
|
+
{ id: 'graph', label: '同融合(图谱已不是独立一路)' },
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
const HIT_ROUTES: Record<string, string> = {
|
|
107
|
+
fts: styles.hitRouteFts, vector: styles.hitRouteVector, graph: styles.hitRouteGraph, hybrid: styles.hitRouteHybrid,
|
|
108
|
+
}
|
|
109
|
+
const HIT_ROUTE_LABEL: Record<string, string> = { fts: '关键词', vector: '向量', graph: '知识图谱', hybrid: '三路融合' }
|
|
110
|
+
|
|
111
|
+
/** 去掉 markdown 脚手架,让命中读起来是一段正文而不是源码。 */
|
|
112
|
+
export function snippetOf(content: string, limit = 200): string {
|
|
113
|
+
const plain = content
|
|
114
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
|
|
115
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
116
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
117
|
+
.replace(/\s+/g, ' ')
|
|
118
|
+
.trim()
|
|
119
|
+
return plain.length > limit ? `${plain.slice(0, limit)}…` : plain
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 路线状态行。
|
|
124
|
+
*
|
|
125
|
+
* 0031 起它读的是 `diagnostics.routes` 而不是「`routes` 里有没有这个 id」:**降级也要看得见**。
|
|
126
|
+
* 原来的写法只能表达「跑没跑」,于是向量路坏掉时芯片显示「(未跑)」而 `routes` 里却写着
|
|
127
|
+
* `vector` —— 同一屏上两处互相矛盾,而且都没说为什么。
|
|
128
|
+
*/
|
|
129
|
+
export function RecallRoutes({ routes, backend, routesDetail, graphBoost }: {
|
|
130
|
+
routes: string[]
|
|
131
|
+
backend?: string
|
|
132
|
+
routesDetail?: RecallResultView['routesDetail']
|
|
133
|
+
graphBoost?: RecallResultView['graphBoost']
|
|
134
|
+
}): ReactNode {
|
|
135
|
+
const active = new Set(routes.map(route => route.toLocaleLowerCase()))
|
|
136
|
+
type RouteDetail = NonNullable<RecallResultView['routesDetail']>[number]
|
|
137
|
+
const detailOf = (id: string): RouteDetail | undefined => routesDetail?.find(item => item.route === id)
|
|
138
|
+
return <div className={styles.routeRow} aria-label="召回路线">
|
|
139
|
+
{RECALL_ROUTES.map(route => {
|
|
140
|
+
const detail = detailOf(route.id)
|
|
141
|
+
const ran = detail ? detail.status === 'ran' : (active.has(route.id) || (route.id === 'fts' && active.has('keyword')))
|
|
142
|
+
const degraded = detail?.status === 'degraded'
|
|
143
|
+
const title = degraded && detail?.reason ? `${route.hint} —— ${detail.reason}` : route.hint
|
|
144
|
+
const suffix = degraded ? '(降级)' : ran ? (detail?.kept !== undefined ? ` ${detail.kept}` : '') : '(未跑)'
|
|
145
|
+
return <span key={route.id} className={cn(styles.routeChip, ran && styles.routeChipOn, degraded && styles.routeChipOff)} title={title}>
|
|
146
|
+
{route.label}{suffix}
|
|
147
|
+
</span>
|
|
148
|
+
})}
|
|
149
|
+
{/* 图谱是 boost:显示它加权了几条,而不是「跑没跑」。 */}
|
|
150
|
+
<span className={styles.routeChip} title="图谱只给已在候选池里的条目加权,不能自己制造候选">
|
|
151
|
+
图谱 boost {graphBoost && graphBoost.applied > 0 ? `+${graphBoost.applied}` : '未生效'}
|
|
152
|
+
</span>
|
|
153
|
+
<span className={styles.routeBackend}>{backend ? backend.toUpperCase() : 'FTS'}</span>
|
|
154
|
+
</div>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function HitList({ hits }: { hits: RecallHitView[] }): ReactNode {
|
|
158
|
+
if (hits.length === 0) return null
|
|
159
|
+
return <ul className={styles.hitList}>
|
|
160
|
+
{hits.map(hit => <li key={hit.unitId} className={styles.hitRow}>
|
|
161
|
+
<div className={styles.hitHead}>
|
|
162
|
+
<span className={cn(styles.hitRoute, HIT_ROUTES[hit.match] ?? styles.hitRouteFts)}>{HIT_ROUTE_LABEL[hit.match] ?? hit.match}</span>
|
|
163
|
+
<span className={styles.hitTitle} title={hit.unitId}>{hit.title}{hit.heading ? ` · ${hit.heading}` : ''}</span>
|
|
164
|
+
<span className={styles.hitScore}>{hit.score.toFixed(3)}</span>
|
|
165
|
+
</div>
|
|
166
|
+
<p className={styles.hitSnippet}>{hit.snippet || '(这条 wikiNote 没有正文)'}</p>
|
|
167
|
+
<div className={styles.hitFoot}>
|
|
168
|
+
<span className={styles.kbCode}>{hit.unitId}</span>
|
|
169
|
+
{hit.status && <span>状态 {hit.status}</span>}
|
|
170
|
+
{hit.entityType && <span>{hit.entityType}</span>}
|
|
171
|
+
{hit.rerankScore !== undefined && <span>rerank {hit.rerankScore.toFixed(3)}</span>}
|
|
172
|
+
</div>
|
|
173
|
+
</li>)}
|
|
174
|
+
</ul>
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 进入 Context Pack 的 topN。重排这一栏必须显示得分的**来源**:
|
|
179
|
+
* rerank 真跑过就给 rerank 分数,降级时按融合分数排就只给融合分数,不混着显示。
|
|
180
|
+
*/
|
|
181
|
+
function ContextList({ items, provider }: { items: ContextItemView[]; provider?: string }): ReactNode {
|
|
182
|
+
if (items.length === 0) return null
|
|
183
|
+
const reranked = provider !== 'none' && items.some(item => item.rerankScore !== undefined)
|
|
184
|
+
return <ol className={styles.contextList}>
|
|
185
|
+
{items.map((item, index) => <li key={item.unitId} className={styles.contextRow}>
|
|
186
|
+
<span className={styles.contextRank}>{index + 1}</span>
|
|
187
|
+
<div className={styles.contextCopy}>
|
|
188
|
+
<div className={styles.contextHead}>
|
|
189
|
+
<span className={styles.hitTitle} title={item.unitId}>{item.title}</span>
|
|
190
|
+
<span className={styles.hitScore}>{reranked && item.rerankScore !== undefined ? `rerank ${item.rerankScore.toFixed(3)}` : `融合 ${item.score.toFixed(3)}`}</span>
|
|
191
|
+
</div>
|
|
192
|
+
<p className={styles.hitSnippet}>{item.snippet || '(这条 wikiNote 没有正文)'}</p>
|
|
193
|
+
<div className={styles.hitFoot}>
|
|
194
|
+
<span className={styles.kbCode}>{item.unitId}</span>
|
|
195
|
+
{item.wikiTitle && <span>父级 wiki:{item.wikiTitle}</span>}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</li>)}
|
|
199
|
+
</ol>
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface RecallPanelProps {
|
|
203
|
+
/** 作用范围:当前知识库;空表示默认库。 */
|
|
204
|
+
kbId?: string
|
|
205
|
+
kbName?: string
|
|
206
|
+
visible: boolean
|
|
207
|
+
/** `recall` 看命中切片,`rerank` 看进入上下文的 topN,`runner` 是评测里的紧凑版。 */
|
|
208
|
+
/**
|
|
209
|
+
* `recall` = 知识库 → **召回重排**那一栏(0033 起召回与重排合成一栏,一次运行
|
|
210
|
+
* 从上到下给出三路命中 → 融合排序 → 精排丢了多少 → 进上下文的 topN)。
|
|
211
|
+
* `runner` = 评测里的紧凑版单问题检查。
|
|
212
|
+
*/
|
|
213
|
+
variant?: 'recall' | 'runner'
|
|
214
|
+
defaults?: { mode?: string; topK?: number; topN?: number }
|
|
215
|
+
/** 设置写完或知识库切换时,用它把面板重置回初始值。 */
|
|
216
|
+
resetKey?: string
|
|
217
|
+
/**
|
|
218
|
+
* 卡头上的「参数」入口(0033)。面板**不认识设置**:它只在自己的卡头上腾一个位置,
|
|
219
|
+
* 具体挂哪一块由调用方决定(召回栏挂召回设置、重排栏挂重排设置、评测栏不挂)。
|
|
220
|
+
* 参数住在设置抽屉里,这里只是那扇离现场更近的门。
|
|
221
|
+
*/
|
|
222
|
+
settingsChip?: ReactNode
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function RecallPanel({ kbId, kbName, visible, variant = 'recall', defaults, resetKey, settingsChip }: RecallPanelProps): ReactNode {
|
|
226
|
+
const [query, setQuery] = useState('销售额')
|
|
227
|
+
const [mode, setMode] = useState(defaults?.mode ?? 'hybrid')
|
|
228
|
+
const [topK, setTopK] = useState(defaults?.topK ?? 10)
|
|
229
|
+
const [topN, setTopN] = useState(defaults?.topN ?? 5)
|
|
230
|
+
const [busy, setBusy] = useState(false)
|
|
231
|
+
const [error, setError] = useState<string | null>(null)
|
|
232
|
+
const [result, setResult] = useState<RecallResultView | null>(null)
|
|
233
|
+
|
|
234
|
+
// 知识库换了或设置存了:面板上的默认值跟着走,否则显示的还是上一个库的参数。
|
|
235
|
+
// 用 effect 而不是渲染期 setState:这条同步是「外部输入变了」而不是渲染推导。
|
|
236
|
+
const modeDefault = defaults?.mode
|
|
237
|
+
const topKDefault = defaults?.topK
|
|
238
|
+
const topNDefault = defaults?.topN
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (!visible) return
|
|
241
|
+
if (modeDefault) setMode(modeDefault)
|
|
242
|
+
if (topKDefault) setTopK(topKDefault)
|
|
243
|
+
if (topNDefault) setTopN(topNDefault)
|
|
244
|
+
setResult(null)
|
|
245
|
+
}, [visible, resetKey, modeDefault, topKDefault, topNDefault])
|
|
246
|
+
|
|
247
|
+
const run = async (): Promise<void> => {
|
|
248
|
+
const trimmed = query.trim()
|
|
249
|
+
if (!trimmed) { setError('请输入要测试的召回问题。'); return }
|
|
250
|
+
setBusy(true); setError(null)
|
|
251
|
+
try {
|
|
252
|
+
const boundedTopN = Math.min(topN, topK)
|
|
253
|
+
const params = new URLSearchParams({ query: trimmed, top_k: String(topK), top_n: String(boundedTopN), mode })
|
|
254
|
+
if (kbId) params.set('kb', kbId)
|
|
255
|
+
const payload = await requestJson<{
|
|
256
|
+
hits?: Array<Partial<RecallHitView> & { content?: string }>
|
|
257
|
+
context?: Array<{ note?: Partial<RecallHitView> & { content?: string }; wiki?: { entityId?: string; title?: string } | null }>
|
|
258
|
+
backend?: string
|
|
259
|
+
routes?: string[]
|
|
260
|
+
recallTopK?: number
|
|
261
|
+
candidatePool?: number
|
|
262
|
+
insufficientEvidence?: boolean
|
|
263
|
+
rerank?: { status?: string; provider?: string; threshold?: number; dropped?: number; topLogit?: number; vocabSize?: number }
|
|
264
|
+
diagnostics?: {
|
|
265
|
+
routes?: RecallResultView['routesDetail']
|
|
266
|
+
graphBoost?: { applied: number; weight: number }
|
|
267
|
+
abstention?: string
|
|
268
|
+
reason?: string
|
|
269
|
+
encoder?: { ok: boolean; reason?: string }
|
|
270
|
+
}
|
|
271
|
+
indexVersion?: number
|
|
272
|
+
}>(`/knowledge/search?${params.toString()}`)
|
|
273
|
+
const data = payload.data ?? {}
|
|
274
|
+
const hits: RecallHitView[] = (data.hits ?? []).map(hit => ({
|
|
275
|
+
unitId: hit.unitId ?? hit.title ?? 'unknown',
|
|
276
|
+
title: hit.title ?? hit.unitId ?? '未命名切片',
|
|
277
|
+
...(hit.heading ? { heading: hit.heading } : {}),
|
|
278
|
+
snippet: snippetOf(hit.content ?? ''),
|
|
279
|
+
match: hit.match ?? 'fts',
|
|
280
|
+
score: Number(hit.score ?? 0),
|
|
281
|
+
...(hit.status ? { status: hit.status } : {}),
|
|
282
|
+
...(hit.entityType ? { entityType: hit.entityType } : {}),
|
|
283
|
+
...(hit.rerankScore === undefined ? {} : { rerankScore: hit.rerankScore }),
|
|
284
|
+
}))
|
|
285
|
+
const context: ContextItemView[] = (data.context ?? []).map(item => ({
|
|
286
|
+
unitId: item.note?.unitId ?? item.note?.title ?? 'unknown',
|
|
287
|
+
title: item.note?.title ?? item.note?.unitId ?? '未命名切片',
|
|
288
|
+
snippet: snippetOf(item.note?.content ?? ''),
|
|
289
|
+
score: Number(item.note?.score ?? 0),
|
|
290
|
+
...(item.note?.rerankScore === undefined ? {} : { rerankScore: item.note.rerankScore }),
|
|
291
|
+
...(item.wiki?.title ? { wikiTitle: item.wiki.title } : {}),
|
|
292
|
+
...(item.wiki?.entityId ? { wikiId: item.wiki.entityId } : {}),
|
|
293
|
+
}))
|
|
294
|
+
setResult({
|
|
295
|
+
count: hits.length,
|
|
296
|
+
backend: data.backend ?? 'fts',
|
|
297
|
+
routes: data.routes,
|
|
298
|
+
contextCount: context.length,
|
|
299
|
+
recallTopK: topK,
|
|
300
|
+
candidatePool: data.candidatePool,
|
|
301
|
+
contextTopN: Math.min(topN, topK),
|
|
302
|
+
rerank: data.rerank?.status,
|
|
303
|
+
rerankProvider: data.rerank?.provider,
|
|
304
|
+
...(data.rerank?.threshold === undefined ? {} : { rerankThreshold: data.rerank.threshold }),
|
|
305
|
+
...(data.rerank?.dropped === undefined ? {} : { rerankDropped: data.rerank.dropped }),
|
|
306
|
+
...(data.rerank?.topLogit === undefined ? {} : { rerankTopLogit: data.rerank.topLogit }),
|
|
307
|
+
...(data.rerank?.vocabSize === undefined ? {} : { rerankVocabSize: data.rerank.vocabSize }),
|
|
308
|
+
...(data.insufficientEvidence === undefined ? {} : { insufficientEvidence: data.insufficientEvidence }),
|
|
309
|
+
...(data.diagnostics?.routes === undefined ? {} : { routesDetail: data.diagnostics.routes }),
|
|
310
|
+
...(data.diagnostics?.graphBoost === undefined ? {} : { graphBoost: data.diagnostics.graphBoost }),
|
|
311
|
+
...(data.diagnostics?.abstention === undefined ? {} : { abstention: data.diagnostics.abstention }),
|
|
312
|
+
...(data.diagnostics?.reason === undefined ? {} : { diagnosticsReason: data.diagnostics.reason }),
|
|
313
|
+
...(data.diagnostics?.encoder === undefined ? {} : { encoder: data.diagnostics.encoder }),
|
|
314
|
+
hits,
|
|
315
|
+
context,
|
|
316
|
+
...(kbName ? { kbName } : {}),
|
|
317
|
+
...(data.indexVersion === undefined ? {} : { indexVersion: data.indexVersion }),
|
|
318
|
+
})
|
|
319
|
+
} catch (cause) {
|
|
320
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
321
|
+
} finally {
|
|
322
|
+
setBusy(false)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const title = variant === 'runner' ? '单问题召回检查' : '召回重排 · 三路召回 + 精排'
|
|
327
|
+
const description = variant === 'runner'
|
|
328
|
+
? '换路线、调 topK,然后看真正命中的切片与得分 —— 不是只看一个命中总数。'
|
|
329
|
+
: '跑一次真实召回,从上到下看完整条链:三路各自命中了什么 → 融合后的排序 → cross-encoder 丢了多少 → 最终进入模型上下文的 topN 是哪几条。'
|
|
330
|
+
|
|
331
|
+
return <Card className={styles.workspaceCard}>
|
|
332
|
+
<div className={styles.cardHeader}>
|
|
333
|
+
<div><h3><Search size={14} />{title}</h3></div>
|
|
334
|
+
<div className={styles.cardHeaderActions}>
|
|
335
|
+
{settingsChip}
|
|
336
|
+
<Badge tone={result?.backend === 'hybrid' ? 'success' : 'neutral'}>{result?.backend === 'hybrid' ? '三路融合' : 'FTS fallback'}</Badge>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
<div className={styles.recallTop}>
|
|
340
|
+
<p className={styles.cardDescription}>{description}</p>
|
|
341
|
+
<span className={styles.recallScope}>作用范围 <b>{result?.kbName ?? kbName ?? '默认知识库'}</b></span>
|
|
342
|
+
</div>
|
|
343
|
+
<div className={styles.inputRow}>
|
|
344
|
+
<Input value={query} placeholder="例如:销售额如何计算"
|
|
345
|
+
onChange={event => setQuery(event.target.value)}
|
|
346
|
+
onKeyDown={event => { if (event.key === 'Enter') void run() }} />
|
|
347
|
+
<Button variant="default" onClick={() => void run()} disabled={busy}>
|
|
348
|
+
{variant === 'runner' && <Play size={13} />}{busy ? '测试中…' : variant === 'runner' ? '运行检查' : '测试'}
|
|
349
|
+
</Button>
|
|
350
|
+
</div>
|
|
351
|
+
<div className={styles.recallControls}>
|
|
352
|
+
<div className={styles.recallControl}>
|
|
353
|
+
<span>路线</span>
|
|
354
|
+
<SelectMenu
|
|
355
|
+
className="w-auto"
|
|
356
|
+
value={mode}
|
|
357
|
+
disabled={busy}
|
|
358
|
+
onValueChange={setMode}
|
|
359
|
+
aria-label="召回路线"
|
|
360
|
+
options={RECALL_MODES.map(item => ({ value: item.id, label: item.label }))}
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
363
|
+
<label>召回 topK
|
|
364
|
+
<Input type="number" min={1} max={50} value={topK} disabled={busy}
|
|
365
|
+
onChange={event => setTopK(Math.max(1, Math.min(50, Number(event.target.value) || 1)))} />
|
|
366
|
+
</label>
|
|
367
|
+
<label>上下文 topN
|
|
368
|
+
<Input type="number" min={1} max={topK} value={Math.min(topN, topK)} disabled={busy}
|
|
369
|
+
onChange={event => setTopN(Math.max(1, Math.min(topK, Number(event.target.value) || 1)))} />
|
|
370
|
+
</label>
|
|
371
|
+
</div>
|
|
372
|
+
{error && <p className={cn(styles.kbNotice, styles.kbNoticeError)} role="alert">{error}</p>}
|
|
373
|
+
<RecallRoutes routes={result?.routes ?? []} backend={result?.backend} routesDetail={result?.routesDetail} graphBoost={result?.graphBoost} />
|
|
374
|
+
{/* 弃答不是失败,是一个结论:知识库里没有能回答这个问题的东西。如实说出来,
|
|
375
|
+
而不是把 5 条噪声塞给模型让它编。 */}
|
|
376
|
+
{result?.insufficientEvidence && <p className={cn(styles.kbNotice, styles.kbNoticeError)} role="status">
|
|
377
|
+
<CircleHelp size={13} /><span>知识库里没有能回答这个问题的内容{result.diagnosticsReason ? `:${result.diagnosticsReason}` : ''}(判据:{result.abstention === 'rerank-threshold' ? '重排阈值' : result.abstention === 'vector-threshold' ? '向量余弦阈值' : '关键词相对阈值,判据较弱'})</span>
|
|
378
|
+
</p>}
|
|
379
|
+
{result && <Badge className={styles.resultBadge} tone={result.insufficientEvidence ? 'warning' : 'success'}>
|
|
380
|
+
<Check size={13} />命中 {result.count} 条 wikiNotes · 上下文 {result.contextCount} 条{result.candidatePool ? ` · 候选池 ${result.candidatePool}` : ''}{result.rerank ? ` · rerank ${result.rerank}${result.rerankThreshold === undefined ? '' : `(阈值 ${result.rerankThreshold}${result.rerankDropped ? `,丢 ${result.rerankDropped}` : ''}${result.rerankTopLogit === undefined ? '' : `,最高 ${result.rerankTopLogit.toFixed(2)}`})`}` : ''}
|
|
381
|
+
</Badge>}
|
|
382
|
+
{result && (result.hits.length > 0
|
|
383
|
+
? <HitList hits={result.hits} />
|
|
384
|
+
: <div className={styles.emptyState}><CircleHelp size={16} />这个路线组合没有命中任何 wikiNote</div>)}
|
|
385
|
+
{result && result.context.length > 0 && <>
|
|
386
|
+
<div className={styles.kbSubHead}><h4>进入上下文包的 topN</h4><span>{result.context.length} 条</span></div>
|
|
387
|
+
<ContextList items={result.context} provider={result.rerankProvider} />
|
|
388
|
+
</>}
|
|
389
|
+
<div className={styles.infoList}>
|
|
390
|
+
<div><span>召回 topK</span><strong>{result?.recallTopK ?? topK}</strong></div>
|
|
391
|
+
<div><span>上下文 topN</span><strong>{result?.contextTopN ?? Math.min(topN, topK)}</strong></div>
|
|
392
|
+
<div><span>默认召回路线</span><strong>{mode}</strong></div>
|
|
393
|
+
{/* 词表是重排能不能用的**可见证据**:只有 5 个特殊 token 时模型照样出分,
|
|
394
|
+
所以「真的加载了 250002 条词表」这件事得看得见(0031)。 */}
|
|
395
|
+
{result?.rerankVocabSize !== undefined && <div><span>重排词表</span><strong>{result.rerankVocabSize}</strong></div>}
|
|
396
|
+
<div><span>索引版本</span><strong>{result?.indexVersion ? `v${result.indexVersion}` : '—'}</strong></div>
|
|
397
|
+
</div>
|
|
398
|
+
</Card>
|
|
399
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「类型定义」抽屉 —— **一个知识库的词汇表**:来源分类注册表 + Wiki 卡片 schema。
|
|
3
|
+
*
|
|
4
|
+
* 为什么它是抽屉,而不是步骤轨上的第六步(0034):
|
|
5
|
+
*
|
|
6
|
+
* 1. 步骤轨是一条**流水线**(收录 → 编译 → 关联 → 检索 → 精排),每一步回答「这一段
|
|
7
|
+
* 链路上发生了什么」,而且与架构图 B 带的五个节点一一对应。类型定义不属于任何一段:
|
|
8
|
+
* 它是所有段共享的词汇表 —— `source_type` 被收录消费,wiki 类型被编译消费。塞进箭头
|
|
9
|
+
* 链里会读成「流水线以定义类型收尾」,而定义其实在收录之前。
|
|
10
|
+
* 2. 它与「设置」抽屉是**两种作用域**,这才是不能合容器的地方:这两个注册表**按知识库**
|
|
11
|
+
* 持久化(写的是 base 记录,切库就换一套),而存储 / 收录闸门 / 构建 / 召回 / 重排是
|
|
12
|
+
* **方案级参数**(`tita-data-agent-knowledge` 一个 namespace,整个方案一份)。放同一栏
|
|
13
|
+
* 里,用户会合理地以为「切到另一个库,Embedding 模型和 topK 也跟着换」——而它们不会。
|
|
14
|
+
* 所以两个抽屉各自在标题下写明作用域,一个写着库名,一个写着「本方案」。
|
|
15
|
+
*
|
|
16
|
+
* 写路径仍在 `KnowledgeBaseTab`(它持有 base 与 busy 状态机):这个组件只管呈现与它
|
|
17
|
+
* 自己的编辑弹窗 —— 同一份数据不因为多了一个入口就多一个写入口。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useState, type ChangeEvent, type ReactNode } from 'react'
|
|
21
|
+
import { Check, Pencil, Plus, ShieldCheck, Trash2 } from '@tita-data-agent/ui/icons'
|
|
22
|
+
import { Button, Input, Label, Switch, cn } from '@tita-data-agent/ui/components'
|
|
23
|
+
import {
|
|
24
|
+
Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle,
|
|
25
|
+
Drawer, DrawerBody, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle,
|
|
26
|
+
} from '@tita-data-agent/ui/overlays'
|
|
27
|
+
import { styles } from './styles.ts'
|
|
28
|
+
import type { SourceTypeCatalog, SourceTypeDef, SourceTypeDraft } from './KnowledgeBaseTab.tsx'
|
|
29
|
+
import { WikiTypeSection, type WikiTypeView } from './WikiTypeEditor.tsx'
|
|
30
|
+
|
|
31
|
+
export interface TypeDefinitionDrawerProps {
|
|
32
|
+
open: boolean
|
|
33
|
+
onOpenChange: (open: boolean) => void
|
|
34
|
+
/** 作用域说明里要写库名 —— 这两个注册表是按库的,切库就换一套。 */
|
|
35
|
+
kbName?: string
|
|
36
|
+
/** Wiki 类型目录(模板与只读规则)按库拉取,所以这一层也要库 id。 */
|
|
37
|
+
kbId: string
|
|
38
|
+
sourceTypes: SourceTypeDef[]
|
|
39
|
+
wikiTypes: WikiTypeView[]
|
|
40
|
+
schemaMode: 'strict' | 'report'
|
|
41
|
+
customized: boolean
|
|
42
|
+
catalog: SourceTypeCatalog | null
|
|
43
|
+
busy: string | null
|
|
44
|
+
onSaveSourceType: (draft: SourceTypeDraft) => Promise<void>
|
|
45
|
+
onToggleSourceType: (def: SourceTypeDef, enabled: boolean) => Promise<void>
|
|
46
|
+
onRemoveSourceType: (def: SourceTypeDef) => Promise<void>
|
|
47
|
+
onApplyWikiTypes: (next: WikiTypeView[], describe: string) => Promise<void>
|
|
48
|
+
onModeChange: (mode: 'strict' | 'report') => Promise<void>
|
|
49
|
+
onResetWikiTypes: () => Promise<void>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const EMPTY_DRAFT: SourceTypeDraft = { mode: 'create', id: '', label: '', description: '', signals: '', excludes: '' }
|
|
53
|
+
|
|
54
|
+
export function TypeDefinitionDrawer({
|
|
55
|
+
open, onOpenChange, kbName, kbId, sourceTypes, wikiTypes, schemaMode, customized, catalog, busy,
|
|
56
|
+
onSaveSourceType, onToggleSourceType, onRemoveSourceType, onApplyWikiTypes, onModeChange, onResetWikiTypes,
|
|
57
|
+
}: TypeDefinitionDrawerProps): ReactNode {
|
|
58
|
+
/**
|
|
59
|
+
* 分类编辑弹窗的草稿。它属于这个抽屉 —— 注册表搬进来之后,只有这里会开这个弹窗。
|
|
60
|
+
* 是一个 Radix Dialog 套在 Radix Drawer 上(两者同源,`RestoreDialog` 也这么用):
|
|
61
|
+
* 编辑一份判定文案需要一块**受保护的焦点**,它不是可以边看列表边改的东西。
|
|
62
|
+
*/
|
|
63
|
+
const [draft, setDraft] = useState<SourceTypeDraft & { open: boolean }>({ ...EMPTY_DRAFT, open: false })
|
|
64
|
+
const closeEditor = (): void => setDraft(current => ({ ...current, open: false }))
|
|
65
|
+
|
|
66
|
+
const openEditor = (def?: SourceTypeDef): void => {
|
|
67
|
+
setDraft(def
|
|
68
|
+
? { open: true, mode: 'edit', id: def.id, label: def.label, description: def.description, signals: def.signals.join('\n'), excludes: def.excludes.join('\n') }
|
|
69
|
+
: { ...EMPTY_DRAFT, open: true })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return <Drawer open={open} onOpenChange={onOpenChange}>
|
|
73
|
+
{open && <DrawerContent className={styles.settingsDrawer} resizable resizeKey="tita-typedef-drawer">
|
|
74
|
+
<DrawerHeader>
|
|
75
|
+
<DrawerTitle>类型定义</DrawerTitle>
|
|
76
|
+
{/* 作用域写在标题下面,只写一次:切库换的是**这一套**,不是方案的参数。 */}
|
|
77
|
+
{/* <DrawerDescription>
|
|
78
|
+
本库定义{kbName ? <> · <b>{kbName}</b></> : null} —— 这两个注册表只属于这个知识库,切库就换一套。
|
|
79
|
+
方案级参数(存储 / 收录闸门 / 构建 / 召回 / 重排)在右上角齿轮的「设置」里。
|
|
80
|
+
</DrawerDescription> */}
|
|
81
|
+
</DrawerHeader>
|
|
82
|
+
<DrawerBody>
|
|
83
|
+
<div className={styles.settingsStack}>
|
|
84
|
+
<section className={styles.settingsSection} aria-labelledby="tita-typedef-source-title">
|
|
85
|
+
<div className={styles.settingsSectionHeader}>
|
|
86
|
+
<div>
|
|
87
|
+
<h3 id="tita-typedef-source-title"><ShieldCheck size={13} /> 来源类型</h3>
|
|
88
|
+
<p><b>source_type</b> 描述业务场景,不描述文件格式(载体走 source_format)。</p>
|
|
89
|
+
</div>
|
|
90
|
+
<span className={styles.kbCount}>{sourceTypes.length} 个分类</span>
|
|
91
|
+
</div>
|
|
92
|
+
<p className={styles.kbHint}>
|
|
93
|
+
三档默认值:<b>兜底分类</b>必须存在且不可停用;<b>内置分类</b>可改文案、可停用,但不能删除;
|
|
94
|
+
<b>自定义分类</b>可增可删,新增时必须写清判定文案——它会拼进下面那段内置提示词里交给 Agent 判断。
|
|
95
|
+
分类下还有来源时不能删除,先迁移。
|
|
96
|
+
</p>
|
|
97
|
+
<ul className={styles.kbSourceList}>
|
|
98
|
+
{sourceTypes.map(def => <li key={def.id} className={styles.kbSourceRow}>
|
|
99
|
+
<span className={styles.kbSourceName}>
|
|
100
|
+
<span className={styles.kbSourceLabel}>{def.label}</span>
|
|
101
|
+
<code className={styles.kbSourceId}>{def.id}</code>
|
|
102
|
+
<span className={cn(styles.kbSourceTag, !def.fallback && styles.kbSourceTagLock)}>{def.fallback ? '兜底' : def.builtin ? '内置' : '自定义'}</span>
|
|
103
|
+
</span>
|
|
104
|
+
<span className={styles.kbTypeCount}>{def.sourceCount} 个来源</span>
|
|
105
|
+
<Switch
|
|
106
|
+
checked={def.enabled}
|
|
107
|
+
disabled={def.fallback || busy !== null}
|
|
108
|
+
title={def.fallback ? '兜底分类不能停用:判定链全不命中时必须有一个归属' : `启用 / 停用 ${def.label}`}
|
|
109
|
+
onCheckedChange={checked => void onToggleSourceType(def, checked)}
|
|
110
|
+
/>
|
|
111
|
+
<Button variant="ghost" size="icon" title={`编辑 ${def.label} 的判定文案`} aria-label={`编辑分类 ${def.label}`} disabled={busy !== null} onClick={() => openEditor(def)}><Pencil size={13} /></Button>
|
|
112
|
+
<Button
|
|
113
|
+
variant="ghost"
|
|
114
|
+
size="icon"
|
|
115
|
+
disabled={busy !== null || def.builtin || def.fallback || def.sourceCount > 0}
|
|
116
|
+
title={def.builtin || def.fallback ? '内置 / 兜底分类不可删除,只能停用' : def.sourceCount > 0 ? `该分类下还有 ${def.sourceCount} 个来源,先迁移再删除` : `删除分类 ${def.label}`}
|
|
117
|
+
aria-label={`删除分类 ${def.label}`}
|
|
118
|
+
onClick={() => void onRemoveSourceType(def)}
|
|
119
|
+
>
|
|
120
|
+
<Trash2 size={13} />
|
|
121
|
+
</Button>
|
|
122
|
+
</li>)}
|
|
123
|
+
</ul>
|
|
124
|
+
<div className={styles.kbSourceAdd}>
|
|
125
|
+
<p className={styles.kbHint}>追加分类(例如 <b>test_unit 测试用例</b>)时必须写清「怎么判断它是这一类」。</p>
|
|
126
|
+
<Button variant="outline" size="sm" onClick={() => openEditor()} disabled={busy !== null}><Plus size={13} />新增类型</Button>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{catalog && <details className={styles.kbPrompt}>
|
|
130
|
+
<summary><ShieldCheck size={12} /> 判定规则(内置,只读)· {catalog.classifierPrompt.title} v{catalog.classifierPrompt.version}</summary>
|
|
131
|
+
<p className={styles.kbPromptNote}>{catalog.classifierPrompt.confidenceRule}</p>
|
|
132
|
+
<ol className={styles.kbPromptSteps}>
|
|
133
|
+
{catalog.classifierPrompt.steps.map(step => <li key={step.type} className={cn(!step.enabled && styles.kbPromptOff)}>
|
|
134
|
+
<code>{step.type}</code> {step.rule}
|
|
135
|
+
{step.exists ? (step.enabled ? '' : '(已停用,不要落到它上面)') : '(该分类已不存在)'}
|
|
136
|
+
</li>)}
|
|
137
|
+
</ol>
|
|
138
|
+
<p className={styles.kbPromptNote}><b>兜底:</b>{catalog.classifierPrompt.fallback.note}</p>
|
|
139
|
+
<p className={styles.kbPromptNote}><b>拿不准时:</b>{catalog.classifierPrompt.escalation.note}</p>
|
|
140
|
+
<p className={styles.kbPromptNote}><b>自定义分类怎么参与:</b>{catalog.classifierPrompt.extensionRule}</p>
|
|
141
|
+
<p className={styles.kbPromptNote}>这段规则内置在代码里,不随知识库变化,也不允许修改;可编辑的只有每个分类自己的描述、signals 与 excludes。</p>
|
|
142
|
+
</details>}
|
|
143
|
+
</section>
|
|
144
|
+
|
|
145
|
+
<section className={styles.settingsSection} aria-labelledby="tita-typedef-wiki-title">
|
|
146
|
+
<div className={styles.settingsSectionHeader}>
|
|
147
|
+
<div>
|
|
148
|
+
<h3 id="tita-typedef-wiki-title">Wiki 类型(卡片 schema)</h3>
|
|
149
|
+
<p>每个类型规定必填小节与必填字段,写卡片时按它校验。</p>
|
|
150
|
+
</div>
|
|
151
|
+
<span className={styles.kbCount}>{wikiTypes.length} 个类型</span>
|
|
152
|
+
</div>
|
|
153
|
+
<p className={styles.kbHint}>
|
|
154
|
+
这就是编译设置里的 <b>Schema 类型</b>。早先那种「逗号分隔的类型名文本框」已撤掉 ——
|
|
155
|
+
它既不参与校验也不被生成逻辑读取,是个改了不生效的假开关。
|
|
156
|
+
</p>
|
|
157
|
+
<WikiTypeSection
|
|
158
|
+
kbId={kbId}
|
|
159
|
+
types={wikiTypes}
|
|
160
|
+
mode={schemaMode}
|
|
161
|
+
customized={customized}
|
|
162
|
+
canEdit={busy === null}
|
|
163
|
+
busy={busy !== null}
|
|
164
|
+
onApply={onApplyWikiTypes}
|
|
165
|
+
onModeChange={onModeChange}
|
|
166
|
+
onReset={onResetWikiTypes}
|
|
167
|
+
/>
|
|
168
|
+
</section>
|
|
169
|
+
</div>
|
|
170
|
+
</DrawerBody>
|
|
171
|
+
|
|
172
|
+
<Dialog open={draft.open} onOpenChange={next => { if (!next) closeEditor() }}>
|
|
173
|
+
{draft.open && <DialogContent>
|
|
174
|
+
<DialogHeader><DialogTitle>{draft.mode === 'create' ? '新增来源分类' : `编辑来源分类 · ${draft.label}`}</DialogTitle></DialogHeader>
|
|
175
|
+
<div className={styles.kbForm}>
|
|
176
|
+
<label className={styles.kbField}><Label>分类 id</Label>
|
|
177
|
+
<Input value={draft.id} disabled={draft.mode === 'edit'} spellCheck={false} placeholder="test_unit"
|
|
178
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, id: event.target.value })} />
|
|
179
|
+
<span className={styles.kbHint}>小写字母 / 数字 / 下划线,创建后不可修改——它是写进 SourceRaw frontmatter 的稳定标识。</span>
|
|
180
|
+
</label>
|
|
181
|
+
<label className={styles.kbField}><Label>显示名</Label>
|
|
182
|
+
<Input value={draft.label} placeholder="测试用例"
|
|
183
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) => setDraft({ ...draft, label: event.target.value })} />
|
|
184
|
+
</label>
|
|
185
|
+
<label className={styles.kbField}><Label>一句话定义(必填)</Label>
|
|
186
|
+
<textarea className={styles.kbTextarea} value={draft.description} spellCheck={false}
|
|
187
|
+
placeholder="以单个可执行验证点为中心、用于判定某项功能是否正确的用例记录。"
|
|
188
|
+
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, description: event.target.value })} />
|
|
189
|
+
<span className={styles.kbHint}>写清「是什么」,不要写成「包含哪些文件」——载体格式走 source_format,Agent 靠这句话判断。</span>
|
|
190
|
+
</label>
|
|
191
|
+
<label className={styles.kbField}><Label>signals · 典型信号(每行一条,至少 2 条)</Label>
|
|
192
|
+
<textarea className={styles.kbTextarea} value={draft.signals} spellCheck={false}
|
|
193
|
+
placeholder={'包含前置条件 / 操作步骤 / 预期结果三要素\n带 case id、优先级、关联需求编号'}
|
|
194
|
+
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, signals: event.target.value })} />
|
|
195
|
+
</label>
|
|
196
|
+
<label className={styles.kbField}><Label>excludes · 排除项(每行一条,至少 1 条)</Label>
|
|
197
|
+
<textarea className={styles.kbTextarea} value={draft.excludes} spellCheck={false}
|
|
198
|
+
placeholder={'整份测试方案或测试计划归 document\n线上故障根因排查归 question'}
|
|
199
|
+
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setDraft({ ...draft, excludes: event.target.value })} />
|
|
200
|
+
<span className={styles.kbHint}>每条都要写出「该归到哪个分类」(用 id 或标签);指向不存在的分类会被拒绝,否则边界是悬空的。</span>
|
|
201
|
+
</label>
|
|
202
|
+
</div>
|
|
203
|
+
<DialogFooter>
|
|
204
|
+
<Button variant="ghost" onClick={closeEditor}>取消</Button>
|
|
205
|
+
<Button
|
|
206
|
+
variant="default"
|
|
207
|
+
disabled={busy !== null || !draft.id.trim() || !draft.label.trim() || !draft.description.trim()}
|
|
208
|
+
onClick={() => void onSaveSourceType({ mode: draft.mode, id: draft.id, label: draft.label, description: draft.description, signals: draft.signals, excludes: draft.excludes })}
|
|
209
|
+
><Check size={13} />保存</Button>
|
|
210
|
+
</DialogFooter>
|
|
211
|
+
</DialogContent>}
|
|
212
|
+
</Dialog>
|
|
213
|
+
</DrawerContent>}
|
|
214
|
+
</Drawer>
|
|
215
|
+
}
|