flzxsqc-demo 1.4.13 → 1.4.15
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/agent-backend/docs/digital-analysis-current-implementation-design.md +41 -3
- package/agent-backend/docs/digital-analysis-global-context-filter-flow.md +328 -0
- package/agent-backend/src/main/java/com/example/agent/application/harness/analysis/AnalysisPromptCompiler.java +1 -1
- package/agent-backend/src/main/java/com/example/agent/application/harness/prompt/RuntimeInstructionFactory.java +11 -3
- package/agent-backend/src/main/java/com/example/agent/application/harness/state/DefaultAsyncCallbackPayloadBuilder.java +7 -0
- package/agent-backend/src/main/java/com/example/agent/application/service/DslCompilerService.java +40 -2
- package/agent-backend/src/main/java/com/example/agent/application/service/ReportAssemblyService.java +46 -9
- package/agent-backend/src/main/java/com/example/agent/application/service/ToolExecutionPipeline.java +14 -1
- package/agent-backend/src/main/java/com/example/agent/application/service/ToolMessageCompactor.java +26 -14
- package/agent-backend/src/main/java/com/example/agent/application/service/localtool/AgentSessionBatchService.java +16 -12
- package/agent-backend/src/main/java/com/example/agent/application/service/localtool/AnalysisResultAggregationService.java +163 -30
- package/agent-backend/src/main/java/com/example/agent/application/service/tool/argument/ReportToolArgumentResolver.java +5 -5
- package/agent-backend/src/main/java/com/example/agent/interfaces/rest/ToolsController.java +7 -2
- package/agent-backend/src/main/resources/data.sql +28 -24
- package/agent-backend/src/test/java/com/example/agent/application/harness/analysis/AnalysisPromptCompilerTest.java +1 -0
- package/agent-backend/src/test/java/com/example/agent/application/harness/prompt/RuntimeInstructionFactoryTest.java +2 -2
- package/agent-backend/src/test/java/com/example/agent/application/harness/state/DefaultAsyncCallbackPayloadBuilderTest.java +10 -1
- package/agent-backend/src/test/java/com/example/agent/application/service/AgentTurnRunnerIntegrationTest.java +1 -1
- package/agent-backend/src/test/java/com/example/agent/application/service/ContextBudgetServiceTest.java +4 -4
- package/agent-backend/src/test/java/com/example/agent/application/service/DslCompilerServiceTest.java +18 -25
- package/agent-backend/src/test/java/com/example/agent/application/service/ReportAssemblyServiceTest.java +43 -88
- package/agent-backend/src/test/java/com/example/agent/application/service/ToolArgumentPlannerServiceTest.java +2 -2
- package/agent-backend/src/test/java/com/example/agent/application/service/ToolExecutionPipelineTest.java +83 -2
- package/agent-backend/src/test/java/com/example/agent/application/service/ToolMessageCompactorTest.java +2 -2
- package/agent-backend/src/test/java/com/example/agent/application/service/localtool/AgentSessionBatchServiceTest.java +85 -0
- package/agent-backend/src/test/java/com/example/agent/application/service/localtool/AnalysisResultAggregationServiceTest.java +103 -20
- package/agent-front-react/src/constants/sessionFilters.ts +134 -114
- package/agent-front-react/src/pages/AnalysisTaskPage.tsx +176 -56
- package/agent-front-react/src/pages/analysis-task/SessionBatchFilterModal.tsx +7 -24
- package/agent-front-react/src/pages/analysis-task/SessionBatchPanel.tsx +144 -193
- package/agent-front-react/src/pages/analysis-task/analysisTaskTypes.ts +0 -2
- package/package.json +1 -1
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
7. 多目标分析底层链路
|
|
61
61
|
- `analyze_sessions_per_record_async` 支持 `analysis_targets`。
|
|
62
62
|
- prompt 编译要求每条结构化 item 带 `target`。
|
|
63
|
+
- `aggregate_analysis_results` 要求传 `targets`,按 `items[].target` 输出 `primary_counts_by_target` 和 `category_sample_rows_by_target`。
|
|
63
64
|
- `cluster_analysis_results` 支持按 target 输出 `clusters_by_target` 和 `assignment_trace_sample_by_target`。
|
|
64
65
|
- 长耗时分析确认文案支持把多个分析目标拆成编号列表,例如“客户意图”和“客户要求转人工的原因”会分别展示,避免用户只能看到一段合并后的目标描述。
|
|
65
66
|
|
|
@@ -509,19 +510,56 @@ agent-backend/src/main/java/com/example/agent/application/harness/analysis/Analy
|
|
|
509
510
|
agent-backend/src/main/java/com/example/agent/application/service/localtool/AnalysisResultAggregationService.java
|
|
510
511
|
```
|
|
511
512
|
|
|
513
|
+
`aggregate_analysis_results` 负责确定性计数聚合,适用于 TopN 统计、按会话数降序、分类计数等需求。新链路要求所有调用都传 `targets`,单目标也传一个元素;不要在 `aggregation_spec` 中传 `target`。
|
|
514
|
+
|
|
515
|
+
固定入参形态:
|
|
516
|
+
|
|
517
|
+
```json
|
|
518
|
+
{
|
|
519
|
+
"analysis_result_id": "result-xxx",
|
|
520
|
+
"targets": ["customer_intent", "handoff_reason"],
|
|
521
|
+
"top_n": 5,
|
|
522
|
+
"aggregation_spec": {
|
|
523
|
+
"item_path": "items",
|
|
524
|
+
"value_path": "label",
|
|
525
|
+
"output_key": "label",
|
|
526
|
+
"description_path": "description",
|
|
527
|
+
"evidence_path": "evidence",
|
|
528
|
+
"category_path": "category"
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
多目标聚合输出:
|
|
534
|
+
|
|
535
|
+
- `primary_items_by_target`
|
|
536
|
+
- `primary_counts_by_target`
|
|
537
|
+
- `category_sample_rows_by_target`
|
|
538
|
+
- `available_targets`
|
|
539
|
+
- `target_item_counts`
|
|
540
|
+
- `target_record_counts`
|
|
541
|
+
- `empty_targets`
|
|
542
|
+
|
|
543
|
+
报告引用聚合结果时使用:
|
|
544
|
+
|
|
545
|
+
- `aggregate_analysis_results:exports.primary_counts_by_target.<target>`
|
|
546
|
+
- `aggregate_analysis_results:exports.category_sample_rows_by_target.<target>`
|
|
547
|
+
|
|
512
548
|
语义聚类:
|
|
513
549
|
|
|
514
550
|
```text
|
|
515
551
|
agent-backend/src/main/java/com/example/agent/application/service/localtool/AnalysisResultClusteringService.java
|
|
516
552
|
```
|
|
517
553
|
|
|
554
|
+
`cluster_analysis_results` 负责无监督语义聚类,适用于未知类型发现、相似语义归纳、负面情绪类别发现等需求;不要用 cluster 替代明确的 TopN 计数聚合。
|
|
555
|
+
|
|
518
556
|
多目标聚类输出:
|
|
519
557
|
|
|
520
558
|
- `clusters_by_target`
|
|
521
559
|
- `cluster_run_ids_by_target`
|
|
522
560
|
- `assignment_trace_sample_by_target`
|
|
523
561
|
|
|
524
|
-
其中 `assignment_trace_sample` / representative cases 带 `session_code
|
|
562
|
+
其中 `assignment_trace_sample` / representative cases 带 `session_code`,用于会话溯源。报告引用聚类结果时使用 `cluster_analysis_results:exports.clusters_by_target.<target>.clusters` 和 `cluster_analysis_results:exports.clusters_by_target.<target>.category_sample_rows`。
|
|
525
563
|
|
|
526
564
|
### 7.4 多目标用户确认展示
|
|
527
565
|
|
|
@@ -548,7 +586,7 @@ agent-backend/src/main/java/com/example/agent/application/harness/requirements/S
|
|
|
548
586
|
- 输出形式:Top5聚类
|
|
549
587
|
```
|
|
550
588
|
|
|
551
|
-
该确认只负责让用户判断“目标识别是否正确”。真正执行逐条分析时,仍由 `analyze_sessions_per_record_async.analysis_targets` 传入稳定目标 key,并由 prompt 编译要求每个 item 写入 `target
|
|
589
|
+
该确认只负责让用户判断“目标识别是否正确”。真正执行逐条分析时,仍由 `analyze_sessions_per_record_async.analysis_targets` 传入稳定目标 key,并由 prompt 编译要求每个 item 写入 `target`;下游 aggregate/cluster 的 `targets` 必须与这组 key 保持一致。
|
|
552
590
|
|
|
553
591
|
### 7.5 异步逐条结果解析容错
|
|
554
592
|
|
|
@@ -623,7 +661,7 @@ build_keyword_report -> ReportAssemblyService -> latestAnswer -> AnalysisTaskPag
|
|
|
623
661
|
- 报告按分类值采样。
|
|
624
662
|
- 从报告跳转会话详情。
|
|
625
663
|
|
|
626
|
-
因此当前报告是否按目标分组、是否每条问题举例都带会话 ID,取决于 LLM 生成的 report blueprint
|
|
664
|
+
因此当前报告是否按目标分组、是否每条问题举例都带会话 ID,取决于 LLM 生成的 report blueprint 是否引用了对应 by-target 证据路径。计数聚合报告应引用 `aggregate_analysis_results:exports.primary_counts_by_target.<target>` 和 `aggregate_analysis_results:exports.category_sample_rows_by_target.<target>`;语义聚类报告应引用 `clusters_by_target`、`assignment_trace_sample_by_target` 或其他带 `session_code` 的工具输出。
|
|
627
665
|
|
|
628
666
|
## 10. DSL 重放关联优化
|
|
629
667
|
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# globalContext 数据筛选参数处理链路
|
|
2
|
+
|
|
3
|
+
本文说明前端提交 `globalContext.digitalAnalysis` 后,后端如何把数据筛选条件融入分析任务的工具调用参数中。重点覆盖数据从前端创建任务、后端会话保存、工具执行前预处理,到本地查询兼容层的完整代码路径。
|
|
4
|
+
|
|
5
|
+
## 1. 入口数据形态
|
|
6
|
+
|
|
7
|
+
前端创建分析任务时,会把数据筛选信息放入 `globalContext.digitalAnalysis`。当前主要有两类来源:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"globalContext": {
|
|
12
|
+
"digitalAnalysis": {
|
|
13
|
+
"source": "agent_session_batch",
|
|
14
|
+
"batchId": "batch-xxx",
|
|
15
|
+
"queryInstanceId": "query-xxx",
|
|
16
|
+
"matchedCount": 20,
|
|
17
|
+
"sessionIds": ["call-1", "call-2"],
|
|
18
|
+
"filters": {
|
|
19
|
+
"start_time": "2026-01-01 00:00:00",
|
|
20
|
+
"end_time": "2026-06-18 23:59:59",
|
|
21
|
+
"agentId": "A001",
|
|
22
|
+
"teamId": ["T1", "T2"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
没有批次筛选结果、但填写了固定或动态筛选条件时,前端会提交:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"globalContext": {
|
|
34
|
+
"digitalAnalysis": {
|
|
35
|
+
"source": "agent_session_filter",
|
|
36
|
+
"filters": {
|
|
37
|
+
"channel": "渠道1",
|
|
38
|
+
"batchCode": "batchId",
|
|
39
|
+
"queueId": "Q001",
|
|
40
|
+
"groupId": ["G1"]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
相关前端组装代码:
|
|
48
|
+
|
|
49
|
+
- `want-agent/agent-front-react/src/pages/AnalysisTaskPage.tsx`
|
|
50
|
+
- `want-agent/agent-front-react/src/constants/sessionFilters.ts`
|
|
51
|
+
- `want-agent/agent-front-react/src/pages/analysis-task/SessionBatchPanel.tsx`
|
|
52
|
+
|
|
53
|
+
## 2. 后端会话创建与保存
|
|
54
|
+
|
|
55
|
+
创建任务接口接收前端传入的 `globalContext`,并随会话一起保存。
|
|
56
|
+
|
|
57
|
+
核心代码:
|
|
58
|
+
|
|
59
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/controller/AgentSessionController.java`
|
|
60
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/application/service/AgentTurnRunner.java`
|
|
61
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/domain/AgentSession.java`
|
|
62
|
+
|
|
63
|
+
处理要点:
|
|
64
|
+
|
|
65
|
+
1. `AgentSessionController.createSession(...)` 接收创建任务请求。
|
|
66
|
+
2. 请求中的 `globalContext` 会进入会话创建流程。
|
|
67
|
+
3. `AgentTurnRunner` 创建 `AgentSession` 记录时,把 `globalContext` 挂到 `AgentSession.globalContext`。
|
|
68
|
+
4. 后续工具执行阶段通过当前 `AgentSession` 读取这份上下文。
|
|
69
|
+
|
|
70
|
+
也就是说,`globalContext.digitalAnalysis` 不是直接作为某个查询接口的 body 传入工具,而是先成为会话级上下文,再在工具调用前被后端预处理器读取并转换。
|
|
71
|
+
|
|
72
|
+
## 3. 工具执行前的参数处理顺序
|
|
73
|
+
|
|
74
|
+
分析任务运行时,LLM 会选择工具并生成初始参数。工具真正执行前,会经过后端统一执行管线。
|
|
75
|
+
|
|
76
|
+
核心代码:
|
|
77
|
+
|
|
78
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/application/service/tool/ToolExecutionPipeline.java`
|
|
79
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/application/service/tool/AgentSessionBatchToolContextApplier.java`
|
|
80
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/application/service/tool/argument/ToolArgumentResolverRegistry.java`
|
|
81
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/application/service/tool/argument/CompanyStructuredQueryToolArgumentResolver.java`
|
|
82
|
+
|
|
83
|
+
当前关键顺序是:
|
|
84
|
+
|
|
85
|
+
1. `agentTimeContextService.normalizeArgs(...)`
|
|
86
|
+
2. `AgentSessionBatchToolContextApplier.apply(...)`
|
|
87
|
+
3. `toolInvocationGuard.validateBeforeExecution(...)`
|
|
88
|
+
4. `resolveExecutionArgs(...)`
|
|
89
|
+
5. 进入具体工具实现或 MCP 调用
|
|
90
|
+
|
|
91
|
+
其中第 2 步和第 4 步都与数据筛选相关,但职责不同。
|
|
92
|
+
|
|
93
|
+
## 4. AgentSessionBatchToolContextApplier 的作用
|
|
94
|
+
|
|
95
|
+
`AgentSessionBatchToolContextApplier` 会读取:
|
|
96
|
+
|
|
97
|
+
```java
|
|
98
|
+
session.getGlobalContext().get("digitalAnalysis")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
它的作用是把会话级数据筛选上下文合并到传统工具参数中,主要用于让工具前置校验、执行保护和旧参数路径能看到约束。
|
|
102
|
+
|
|
103
|
+
相关代码:
|
|
104
|
+
|
|
105
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/application/service/tool/AgentSessionBatchToolContextApplier.java`
|
|
106
|
+
|
|
107
|
+
典型行为:
|
|
108
|
+
|
|
109
|
+
- 读取 `digitalAnalysis.filters`
|
|
110
|
+
- 读取 `digitalAnalysis.sessionIds`
|
|
111
|
+
- 合并成规范化筛选条件
|
|
112
|
+
- 注入到工具参数中的 `filters` 或相关字段
|
|
113
|
+
|
|
114
|
+
这一步保留了对旧式本地查询参数的兼容,但它不是公司 ES 查询参数最终成型的位置。
|
|
115
|
+
|
|
116
|
+
## 5. request_preprocessor 决定最终工具参数形态
|
|
117
|
+
|
|
118
|
+
真正把筛选条件组装成公司查询接口参数形态的是 `request_preprocessor`。
|
|
119
|
+
|
|
120
|
+
工具定义在种子数据里配置:
|
|
121
|
+
|
|
122
|
+
- `want-agent/agent-backend/src/main/resources/data.sql`
|
|
123
|
+
|
|
124
|
+
当前两个工具使用公司结构化查询预处理器:
|
|
125
|
+
|
|
126
|
+
```sql
|
|
127
|
+
query_customer_sessions_structured -> COMPANY_STRUCTURED_DETECT_QUERY
|
|
128
|
+
query_sessions_handle -> COMPANY_STRUCTURED_HANDLE_QUERY
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
对应解析器:
|
|
132
|
+
|
|
133
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/application/service/tool/argument/CompanyStructuredQueryToolArgumentResolver.java`
|
|
134
|
+
|
|
135
|
+
注意:如果本地数据库里已经存在旧的 `tool_definition` 数据,需要重新执行或迁移 `data.sql` 中的 `request_preprocessor` 配置,否则运行时不会进入这个解析器。
|
|
136
|
+
|
|
137
|
+
## 6. CompanyStructuredQueryToolArgumentResolver 的优先级
|
|
138
|
+
|
|
139
|
+
`CompanyStructuredQueryToolArgumentResolver` 的核心规则是:只要会话 `globalContext.digitalAnalysis` 里有可识别筛选条件,就优先使用这些条件组装工具参数;否则才使用模型从自然语言问题里解析出的参数。
|
|
140
|
+
|
|
141
|
+
优先级如下:
|
|
142
|
+
|
|
143
|
+
1. 有 `globalContext.digitalAnalysis` 筛选条件:使用会话上下文组装公司结构化查询参数。
|
|
144
|
+
2. 没有数据筛选上下文,但模型已经生成 `queryConditions`:保留并补齐默认字段。
|
|
145
|
+
3. 没有 `queryConditions`,但模型生成了旧式 `filters` 或扁平字段:转换成公司结构化查询参数。
|
|
146
|
+
|
|
147
|
+
这对应用户期望的逻辑:
|
|
148
|
+
|
|
149
|
+
- 使用了前端数据筛选时,以 `globalContext` 里的条件为准。
|
|
150
|
+
- 没有使用数据筛选时,继续使用原来的客户自然语言问题解析结果。
|
|
151
|
+
|
|
152
|
+
## 7. 生成的公司查询参数形态
|
|
153
|
+
|
|
154
|
+
预处理后,`query_customer_sessions_structured` 和 `query_sessions_handle` 的工具参数会被整理为类似下面的形态:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"boolOperator": "FILTER",
|
|
159
|
+
"queryConditions": [
|
|
160
|
+
{
|
|
161
|
+
"conditionType": "RANGE",
|
|
162
|
+
"field": "sessionTime",
|
|
163
|
+
"fieldType": "DATE",
|
|
164
|
+
"gte": "2026-01-01 00:00:00",
|
|
165
|
+
"lte": "2026-06-18 23:59:59"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"conditionType": "MATCH_PHRASE",
|
|
169
|
+
"field": "sourceId",
|
|
170
|
+
"fieldType": "KEYWORD",
|
|
171
|
+
"value": ["call-1", "call-2"]
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"conditionType": "MATCH_PHRASE",
|
|
175
|
+
"field": "agentId",
|
|
176
|
+
"fieldType": "KEYWORD",
|
|
177
|
+
"value": ["A001"]
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"conditionType": "MATCH_PHRASE",
|
|
181
|
+
"field": "teamId",
|
|
182
|
+
"fieldType": "KEYWORD",
|
|
183
|
+
"value": ["T1", "T2"]
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
"queryPurpose": "DETECT",
|
|
187
|
+
"sourceTypes": ["VOICE", "TEXT"],
|
|
188
|
+
"includeFields": ["allText", "queueId", "agentId"],
|
|
189
|
+
"queryType": "STRUCTURED",
|
|
190
|
+
"size": 10
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`queryPurpose` 会按工具场景设置:
|
|
195
|
+
|
|
196
|
+
- `COMPANY_STRUCTURED_DETECT_QUERY` -> `DETECT`
|
|
197
|
+
- `COMPANY_STRUCTURED_HANDLE_QUERY` -> `ANALYSE`
|
|
198
|
+
|
|
199
|
+
## 8. 筛选字段到 queryConditions 的映射
|
|
200
|
+
|
|
201
|
+
字段映射由 `CompanyStructuredQueryToolArgumentResolver` 完成。
|
|
202
|
+
|
|
203
|
+
| canonical filter | 公司查询字段 | 条件类型 | 说明 |
|
|
204
|
+
| --- | --- | --- | --- |
|
|
205
|
+
| `start_time` / `end_time` | `sessionTime` | `RANGE` | 时间范围 |
|
|
206
|
+
| `session_ids` / `sessionIds` | `sourceId` | `MATCH_PHRASE` | 会话 ID 列表 |
|
|
207
|
+
| `agentId` | `agentId` | `MATCH_PHRASE` | 坐席 ID |
|
|
208
|
+
| `queueId` | `queueId` | `MATCH_PHRASE` | 队列 |
|
|
209
|
+
| `teamId` | `teamId` | `MATCH_PHRASE` | 团队 ID |
|
|
210
|
+
| `groupId` | `groupId` | `MATCH_PHRASE` | 组 ID |
|
|
211
|
+
| `batchCode` | `batchCode` | `MATCH_PHRASE` | 批次号 |
|
|
212
|
+
| `channel` | `channel` | `MATCH_PHRASE` | 数据渠道 |
|
|
213
|
+
| `dataSource` | `dataSource` | `MATCH_PHRASE` | 数据来源 |
|
|
214
|
+
| `callType` | `callType` | `MATCH_PHRASE` | 会话类型 |
|
|
215
|
+
| `businessLine` | `businessLine` | `MATCH_PHRASE` | 业务线 |
|
|
216
|
+
| `customerId` | `customerId` | `MATCH_PHRASE` | 客户 ID |
|
|
217
|
+
| `keywords` | `allText` | `MATCH_PHRASE` | 文本关键词 |
|
|
218
|
+
| `limit` | `size` | - | 查询条数 |
|
|
219
|
+
|
|
220
|
+
`source`、`batchId`、`queryInstanceId`、`matchedCount` 等字段是上下文元数据,不直接生成查询条件。真正参与过滤的是 `filters` 和 `sessionIds` 中可识别的字段。
|
|
221
|
+
|
|
222
|
+
## 9. 本地查询兼容逻辑
|
|
223
|
+
|
|
224
|
+
本地开发环境并不直接查公司 ES,而是仍然通过本地查询实现或模拟接口执行。因此后端保留了对两种参数形态的兼容:
|
|
225
|
+
|
|
226
|
+
1. 旧式参数:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"filters": {
|
|
231
|
+
"start_time": "2026-01-01 00:00:00",
|
|
232
|
+
"end_time": "2026-06-18 23:59:59",
|
|
233
|
+
"session_ids": ["call-1"]
|
|
234
|
+
},
|
|
235
|
+
"limit": 10
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
2. 公司结构化参数:
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"queryConditions": [
|
|
244
|
+
{
|
|
245
|
+
"conditionType": "MATCH_PHRASE",
|
|
246
|
+
"field": "sourceId",
|
|
247
|
+
"fieldType": "KEYWORD",
|
|
248
|
+
"value": ["call-1"]
|
|
249
|
+
}
|
|
250
|
+
],
|
|
251
|
+
"size": 10
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
兼容转换代码:
|
|
256
|
+
|
|
257
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/application/service/filter/AgentSessionFilterMapper.java`
|
|
258
|
+
|
|
259
|
+
它负责:
|
|
260
|
+
|
|
261
|
+
- 规范化 `filters`
|
|
262
|
+
- 识别字段别名
|
|
263
|
+
- 把公司结构化 `queryConditions` 反解析成本地 `CustomerSessionQueryRequest` 可用的 canonical filters
|
|
264
|
+
- 把 `size` 兼容为 `limit`
|
|
265
|
+
|
|
266
|
+
本地工具入口使用该 mapper:
|
|
267
|
+
|
|
268
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/controller/ToolsController.java`
|
|
269
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/application/service/localtool/QueryHandleService.java`
|
|
270
|
+
|
|
271
|
+
最终查询请求对象和 SQL 映射:
|
|
272
|
+
|
|
273
|
+
- `want-agent/agent-backend/src/main/java/com/example/agent/domain/CustomerSessionQueryRequest.java`
|
|
274
|
+
- `want-agent/agent-backend/src/main/resources/mappers/CustomerSessionMapper.xml`
|
|
275
|
+
|
|
276
|
+
因此,即使工具参数已经被预处理成公司 ES 风格,本地查询路径也能反解析并继续执行。
|
|
277
|
+
|
|
278
|
+
## 10. 完整调用链概览
|
|
279
|
+
|
|
280
|
+
```mermaid
|
|
281
|
+
sequenceDiagram
|
|
282
|
+
participant FE as 前端 AnalysisTaskPage
|
|
283
|
+
participant API as AgentSessionController
|
|
284
|
+
participant Runner as AgentTurnRunner
|
|
285
|
+
participant Pipeline as ToolExecutionPipeline
|
|
286
|
+
participant Applier as AgentSessionBatchToolContextApplier
|
|
287
|
+
participant Resolver as CompanyStructuredQueryToolArgumentResolver
|
|
288
|
+
participant Tool as query_customer_sessions_structured / query_sessions_handle
|
|
289
|
+
participant Local as 本地 QueryHandleService / ToolsController
|
|
290
|
+
|
|
291
|
+
FE->>API: createSession(question, globalContext.digitalAnalysis)
|
|
292
|
+
API->>Runner: 创建分析会话
|
|
293
|
+
Runner->>Runner: 保存 AgentSession.globalContext
|
|
294
|
+
Runner->>Pipeline: 执行 LLM 选择的工具
|
|
295
|
+
Pipeline->>Applier: 从 session.globalContext.digitalAnalysis 合并筛选上下文
|
|
296
|
+
Pipeline->>Resolver: 根据 request_preprocessor 解析最终工具参数
|
|
297
|
+
Resolver->>Resolver: globalContext 优先;否则使用模型解析参数
|
|
298
|
+
Resolver->>Tool: 输出公司结构化 queryConditions 参数
|
|
299
|
+
Tool->>Local: 本地环境执行时进入兼容查询
|
|
300
|
+
Local->>Local: AgentSessionFilterMapper 反解析 queryConditions
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## 11. 关键行为约束
|
|
304
|
+
|
|
305
|
+
- 前端使用数据筛选时,`globalContext.digitalAnalysis` 的筛选条件优先级高于模型从自然语言中解析出的查询条件。
|
|
306
|
+
- 前端没有使用数据筛选时,后端仍沿用自然语言问题解析出的工具参数。
|
|
307
|
+
- `sample_sessions_by_keywords` 不走公司结构化查询预处理器,避免影响关键字采样类工具的原行为。
|
|
308
|
+
- `query_customer_sessions_structured` 和 `query_sessions_handle` 才走公司结构化查询预处理器。
|
|
309
|
+
- `sampleCount`、采样功能和数据筛选查询是不同链路;采样读取已有分析结果,不重新生成数据筛选查询。
|
|
310
|
+
|
|
311
|
+
## 12. 相关测试
|
|
312
|
+
|
|
313
|
+
覆盖该链路的主要测试:
|
|
314
|
+
|
|
315
|
+
- `want-agent/agent-backend/src/test/java/com/example/agent/application/service/tool/argument/CompanyStructuredQueryToolArgumentResolverTest.java`
|
|
316
|
+
- `want-agent/agent-backend/src/test/java/com/example/agent/application/service/filter/AgentSessionFilterMapperTest.java`
|
|
317
|
+
|
|
318
|
+
常用验证命令:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
mvn -Dtest=CompanyStructuredQueryToolArgumentResolverTest,AgentSessionFilterMapperTest test
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
如果同时验证采样功能相关改动:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
mvn -Dtest=CompanyStructuredQueryToolArgumentResolverTest,AgentSessionFilterMapperTest,AgentSessionSampleServiceTest test
|
|
328
|
+
```
|
|
@@ -51,7 +51,7 @@ public class AnalysisPromptCompiler {
|
|
|
51
51
|
"- items:array,只放当前会话中可用于后续聚合的结构化条目,最多 " + limit + " 个;没有命中返回 []。",
|
|
52
52
|
"- summary:string,用一句话概括当前会话的分析结论;没有命中也要说明未发现目标现象。",
|
|
53
53
|
"- items 中每个对象的字段必须与固定输出 JSON 结构中的 items 声明保持一致。",
|
|
54
|
-
"- items[].target:必须填写当前条目所属分析目标 key
|
|
54
|
+
"- items[].target:必须填写当前条目所属分析目标 key;必须使用 analysis_targets 中的 key。",
|
|
55
55
|
"- 所有结论必须能从当前会话原文找到依据。"
|
|
56
56
|
);
|
|
57
57
|
}
|
|
@@ -12,6 +12,7 @@ import java.util.Map;
|
|
|
12
12
|
|
|
13
13
|
@Service
|
|
14
14
|
public class RuntimeInstructionFactory {
|
|
15
|
+
private static final int SOURCE_REF_LIMIT = 80;
|
|
15
16
|
private final ObjectMapper objectMapper;
|
|
16
17
|
|
|
17
18
|
public RuntimeInstructionFactory(ObjectMapper objectMapper) {
|
|
@@ -131,11 +132,18 @@ public class RuntimeInstructionFactory {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
private void addRefs(List<String> refs, String toolName, String namespace, Object value) {
|
|
134
|
-
if (!(value instanceof Map)) {
|
|
135
|
+
if (!(value instanceof Map) || refs.size() >= SOURCE_REF_LIMIT) {
|
|
135
136
|
return;
|
|
136
137
|
}
|
|
137
|
-
for (
|
|
138
|
-
|
|
138
|
+
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
|
|
139
|
+
String path = namespace + "." + String.valueOf(entry.getKey());
|
|
140
|
+
refs.add(toolName + ":" + path);
|
|
141
|
+
if (entry.getValue() instanceof Map) {
|
|
142
|
+
addRefs(refs, toolName, path, entry.getValue());
|
|
143
|
+
}
|
|
144
|
+
if (refs.size() >= SOURCE_REF_LIMIT) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
149
|
|
|
@@ -32,6 +32,9 @@ public class DefaultAsyncCallbackPayloadBuilder implements AsyncCallbackPayloadB
|
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
|
35
|
+
if (isExecutionIdentity(entry.getKey()) && target.containsKey(entry.getKey())) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
35
38
|
Object targetValue = target.get(entry.getKey());
|
|
36
39
|
Object sourceValue = entry.getValue();
|
|
37
40
|
if (targetValue instanceof Map && sourceValue instanceof Map) {
|
|
@@ -42,6 +45,10 @@ public class DefaultAsyncCallbackPayloadBuilder implements AsyncCallbackPayloadB
|
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
private boolean isExecutionIdentity(String key) {
|
|
49
|
+
return "tool".equals(key) || "args".equals(key);
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
private Map<String, Object> readJsonMap(Object raw) {
|
|
46
53
|
if (raw == null) {
|
|
47
54
|
return new LinkedHashMap<String, Object>();
|
package/agent-backend/src/main/java/com/example/agent/application/service/DslCompilerService.java
CHANGED
|
@@ -218,7 +218,10 @@ public class DslCompilerService {
|
|
|
218
218
|
template.put("report_blueprint", sanitizedBlueprint);
|
|
219
219
|
Object rawReportInputs = executedStep.getArgs().get("report_inputs");
|
|
220
220
|
if (rawReportInputs instanceof Map) {
|
|
221
|
-
|
|
221
|
+
Object reportInputTemplate = buildReportInputTemplate("report_inputs", rawReportInputs, priorSteps, step);
|
|
222
|
+
template.put("report_inputs", reportInputTemplate instanceof Map
|
|
223
|
+
? reportInputTemplate
|
|
224
|
+
: new LinkedHashMap<String, Object>());
|
|
222
225
|
} else {
|
|
223
226
|
template.put("report_inputs", new LinkedHashMap<String, Object>());
|
|
224
227
|
}
|
|
@@ -333,6 +336,22 @@ public class DslCompilerService {
|
|
|
333
336
|
List<CompiledStepContext> priorSteps,
|
|
334
337
|
ExecutableDslStep step) {
|
|
335
338
|
ToolScopedPath scopedPath = findToolScopedPath(targetPath, priorSteps);
|
|
339
|
+
if (rawValue instanceof Map) {
|
|
340
|
+
Map<String, Object> source = objectMapper.convertValue(rawValue, Map.class);
|
|
341
|
+
if (isSourceDescriptor(source)) {
|
|
342
|
+
return source;
|
|
343
|
+
}
|
|
344
|
+
if (isResolvedToolScopedMap(scopedPath, priorSteps)) {
|
|
345
|
+
Map<String, Object> nestedTemplate = new LinkedHashMap<String, Object>();
|
|
346
|
+
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
|
347
|
+
Object nestedValue = buildReportInputTemplate(targetPath + "." + entry.getKey(), entry.getValue(), priorSteps, step);
|
|
348
|
+
if (nestedValue != null) {
|
|
349
|
+
nestedTemplate.put(entry.getKey(), nestedValue);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return nestedTemplate.isEmpty() ? null : nestedTemplate;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
336
355
|
if (scopedPath != null) {
|
|
337
356
|
ExecutableDslInputBinding scopedBinding = inferToolScopedBinding(targetPath, scopedPath, rawValue, priorSteps);
|
|
338
357
|
if (scopedBinding != null) {
|
|
@@ -357,7 +376,7 @@ public class DslCompilerService {
|
|
|
357
376
|
nestedTemplate.put(entry.getKey(), nestedValue);
|
|
358
377
|
}
|
|
359
378
|
}
|
|
360
|
-
return nestedTemplate;
|
|
379
|
+
return nestedTemplate.isEmpty() ? null : nestedTemplate;
|
|
361
380
|
}
|
|
362
381
|
if (rawValue instanceof List && scopedPath != null) {
|
|
363
382
|
return null;
|
|
@@ -365,6 +384,15 @@ public class DslCompilerService {
|
|
|
365
384
|
return rawValue;
|
|
366
385
|
}
|
|
367
386
|
|
|
387
|
+
private boolean isResolvedToolScopedMap(ToolScopedPath scopedPath, List<CompiledStepContext> priorSteps) {
|
|
388
|
+
if (scopedPath == null) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
CompiledStepContext priorStep = findPriorStepByToolName(scopedPath.toolName, priorSteps);
|
|
392
|
+
String sourcePath = resolveToolScopedSourcePath(priorStep, scopedPath.remainder);
|
|
393
|
+
return resolveSourceValue(priorStep, sourcePath) instanceof Map;
|
|
394
|
+
}
|
|
395
|
+
|
|
368
396
|
private void attachBlueprintSourceBindings(Map<String, Object> sanitizedBlueprint,
|
|
369
397
|
List<CompiledStepContext> priorSteps,
|
|
370
398
|
ExecutableDslStep step) {
|
|
@@ -574,15 +602,25 @@ public class DslCompilerService {
|
|
|
574
602
|
if (priorStep.exports.containsKey(remainder)) {
|
|
575
603
|
return "exports." + remainder;
|
|
576
604
|
}
|
|
605
|
+
if (resolveNestedValue(priorStep.exports, remainder) != null) {
|
|
606
|
+
return "exports." + remainder;
|
|
607
|
+
}
|
|
577
608
|
if ("records".equals(remainder) && resolveSourceValue(priorStep, "data.records") != null) {
|
|
578
609
|
return "data.records";
|
|
579
610
|
}
|
|
580
611
|
if (priorStep.artifacts.containsKey(remainder)) {
|
|
581
612
|
return "artifacts." + remainder;
|
|
582
613
|
}
|
|
614
|
+
if (resolveNestedValue(priorStep.artifacts, remainder) != null) {
|
|
615
|
+
return "artifacts." + remainder;
|
|
616
|
+
}
|
|
583
617
|
if (priorStep.outputData instanceof Map && ((Map<?, ?>) priorStep.outputData).containsKey(remainder)) {
|
|
584
618
|
return "data." + remainder;
|
|
585
619
|
}
|
|
620
|
+
if (priorStep.outputData instanceof Map
|
|
621
|
+
&& resolveNestedValue((Map<String, Object>) priorStep.outputData, remainder) != null) {
|
|
622
|
+
return "data." + remainder;
|
|
623
|
+
}
|
|
586
624
|
return null;
|
|
587
625
|
}
|
|
588
626
|
|
package/agent-backend/src/main/java/com/example/agent/application/service/ReportAssemblyService.java
CHANGED
|
@@ -24,7 +24,7 @@ import java.util.regex.Pattern;
|
|
|
24
24
|
public class ReportAssemblyService {
|
|
25
25
|
private static final Pattern TEMPLATE_VARIABLE = Pattern.compile("\\{\\{\\s*([a-zA-Z0-9_\\.]+)\\s*}}");
|
|
26
26
|
private static final Pattern SIMPLE_TEMPLATE_VARIABLE = Pattern.compile("(?<!\\{)\\{\\s*([a-zA-Z0-9_\\.]+)\\s*}(?!})");
|
|
27
|
-
private static final String
|
|
27
|
+
private static final String AGGREGATE_CATEGORY_SAMPLE_ROWS_BY_TARGET = "aggregate_analysis_results:exports.category_sample_rows_by_target";
|
|
28
28
|
private static final String CLUSTER_CATEGORY_SAMPLE_ROWS = "cluster_analysis_results:exports.category_sample_rows";
|
|
29
29
|
private final ObjectMapper objectMapper;
|
|
30
30
|
private final BindingResolver bindingResolver;
|
|
@@ -81,7 +81,7 @@ public class ReportAssemblyService {
|
|
|
81
81
|
private String renderMissingCategorySampleTables(ReportBlueprint blueprint, Map<String, Object> inputs) {
|
|
82
82
|
List<String> tables = new ArrayList<String>();
|
|
83
83
|
List<String> referencedSourceLabels = referencedSourceLabels(blueprint);
|
|
84
|
-
|
|
84
|
+
appendAggregateCategorySampleTablesByTarget(tables, inputs, referencedSourceLabels);
|
|
85
85
|
appendCategorySampleTable(tables, inputs, referencedSourceLabels, CLUSTER_CATEGORY_SAMPLE_ROWS, "聚类分类样本");
|
|
86
86
|
if (tables.isEmpty()) {
|
|
87
87
|
return "";
|
|
@@ -89,6 +89,33 @@ public class ReportAssemblyService {
|
|
|
89
89
|
return "## 分类样本明细\n\n" + String.join("\n\n", tables);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
@SuppressWarnings("unchecked")
|
|
93
|
+
private void appendAggregateCategorySampleTablesByTarget(List<String> tables,
|
|
94
|
+
Map<String, Object> inputs,
|
|
95
|
+
List<String> referencedSourceLabels) {
|
|
96
|
+
if (isCategorySampleSourceReferenced(referencedSourceLabels, AGGREGATE_CATEGORY_SAMPLE_ROWS_BY_TARGET)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
Object rawRowsByTarget = inputs.get(AGGREGATE_CATEGORY_SAMPLE_ROWS_BY_TARGET);
|
|
100
|
+
if (!(rawRowsByTarget instanceof Map)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
Map<String, Object> rowsByTarget = objectMapper.convertValue(rawRowsByTarget, new TypeReference<Map<String, Object>>() {});
|
|
104
|
+
for (Map.Entry<String, Object> entry : rowsByTarget.entrySet()) {
|
|
105
|
+
String target = entry.getKey();
|
|
106
|
+
Object rows = entry.getValue();
|
|
107
|
+
if (normalizeTableRows(rows).isEmpty()) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
String instruction = "聚合分类样本 - " + target;
|
|
111
|
+
ReportBlockBlueprint block = categorySampleTableBlock(AGGREGATE_CATEGORY_SAMPLE_ROWS_BY_TARGET + "." + target, instruction, false);
|
|
112
|
+
String table = renderStructuredTable(instruction, rows, block);
|
|
113
|
+
if (hasText(table)) {
|
|
114
|
+
tables.add(table);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
private void appendCategorySampleTable(List<String> tables,
|
|
93
120
|
Map<String, Object> inputs,
|
|
94
121
|
List<String> referencedSourceLabels,
|
|
@@ -101,23 +128,25 @@ public class ReportAssemblyService {
|
|
|
101
128
|
if (normalizeTableRows(rows).isEmpty()) {
|
|
102
129
|
return;
|
|
103
130
|
}
|
|
104
|
-
ReportBlockBlueprint block = categorySampleTableBlock(sourceRef, instruction);
|
|
131
|
+
ReportBlockBlueprint block = categorySampleTableBlock(sourceRef, instruction, true);
|
|
105
132
|
String table = renderStructuredTable(instruction, rows, block);
|
|
106
133
|
if (hasText(table)) {
|
|
107
134
|
tables.add(table);
|
|
108
135
|
}
|
|
109
136
|
}
|
|
110
137
|
|
|
111
|
-
private ReportBlockBlueprint categorySampleTableBlock(String sourceRef, String instruction) {
|
|
138
|
+
private ReportBlockBlueprint categorySampleTableBlock(String sourceRef, String instruction, boolean includeDescription) {
|
|
112
139
|
ReportBlockBlueprint block = new ReportBlockBlueprint();
|
|
113
140
|
block.setBlockType("table");
|
|
114
141
|
block.setInstruction(instruction);
|
|
115
142
|
block.setSourceRef(ReportSourceRef.fromString(sourceRef));
|
|
116
143
|
List<ReportTableColumnBlueprint> columns = new ArrayList<ReportTableColumnBlueprint>();
|
|
117
144
|
columns.add(column("分类", "category"));
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
145
|
+
if (includeDescription) {
|
|
146
|
+
columns.add(column("描述", "description"));
|
|
147
|
+
}
|
|
148
|
+
columns.add(column("抽样原文依据", "sample_key_sessions"));
|
|
149
|
+
columns.add(column("对应会话ID", "sample_session_ids"));
|
|
121
150
|
block.setColumns(columns);
|
|
122
151
|
return block;
|
|
123
152
|
}
|
|
@@ -175,8 +204,16 @@ public class ReportAssemblyService {
|
|
|
175
204
|
if (!hasText(label)) {
|
|
176
205
|
continue;
|
|
177
206
|
}
|
|
178
|
-
if (sourceRef.equals(label)
|
|
179
|
-
|| "
|
|
207
|
+
if (sourceRef.equals(label)
|
|
208
|
+
|| "category_sample_rows".equals(label)
|
|
209
|
+
|| "category_sample_rows_by_target".equals(label)
|
|
210
|
+
|| "exports.category_sample_rows".equals(label)
|
|
211
|
+
|| "exports.category_sample_rows_by_target".equals(label)) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (sourceRef.startsWith("aggregate_analysis_results:")
|
|
215
|
+
&& label.startsWith("aggregate_analysis_results:")
|
|
216
|
+
&& label.contains("category_sample_rows_by_target")) {
|
|
180
217
|
return true;
|
|
181
218
|
}
|
|
182
219
|
if (sourceRef.startsWith("cluster_analysis_results:")
|