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.
Files changed (32) hide show
  1. package/agent-backend/docs/digital-analysis-current-implementation-design.md +41 -3
  2. package/agent-backend/docs/digital-analysis-global-context-filter-flow.md +328 -0
  3. package/agent-backend/src/main/java/com/example/agent/application/harness/analysis/AnalysisPromptCompiler.java +1 -1
  4. package/agent-backend/src/main/java/com/example/agent/application/harness/prompt/RuntimeInstructionFactory.java +11 -3
  5. package/agent-backend/src/main/java/com/example/agent/application/harness/state/DefaultAsyncCallbackPayloadBuilder.java +7 -0
  6. package/agent-backend/src/main/java/com/example/agent/application/service/DslCompilerService.java +40 -2
  7. package/agent-backend/src/main/java/com/example/agent/application/service/ReportAssemblyService.java +46 -9
  8. package/agent-backend/src/main/java/com/example/agent/application/service/ToolExecutionPipeline.java +14 -1
  9. package/agent-backend/src/main/java/com/example/agent/application/service/ToolMessageCompactor.java +26 -14
  10. package/agent-backend/src/main/java/com/example/agent/application/service/localtool/AgentSessionBatchService.java +16 -12
  11. package/agent-backend/src/main/java/com/example/agent/application/service/localtool/AnalysisResultAggregationService.java +163 -30
  12. package/agent-backend/src/main/java/com/example/agent/application/service/tool/argument/ReportToolArgumentResolver.java +5 -5
  13. package/agent-backend/src/main/java/com/example/agent/interfaces/rest/ToolsController.java +7 -2
  14. package/agent-backend/src/main/resources/data.sql +28 -24
  15. package/agent-backend/src/test/java/com/example/agent/application/harness/analysis/AnalysisPromptCompilerTest.java +1 -0
  16. package/agent-backend/src/test/java/com/example/agent/application/harness/prompt/RuntimeInstructionFactoryTest.java +2 -2
  17. package/agent-backend/src/test/java/com/example/agent/application/harness/state/DefaultAsyncCallbackPayloadBuilderTest.java +10 -1
  18. package/agent-backend/src/test/java/com/example/agent/application/service/AgentTurnRunnerIntegrationTest.java +1 -1
  19. package/agent-backend/src/test/java/com/example/agent/application/service/ContextBudgetServiceTest.java +4 -4
  20. package/agent-backend/src/test/java/com/example/agent/application/service/DslCompilerServiceTest.java +18 -25
  21. package/agent-backend/src/test/java/com/example/agent/application/service/ReportAssemblyServiceTest.java +43 -88
  22. package/agent-backend/src/test/java/com/example/agent/application/service/ToolArgumentPlannerServiceTest.java +2 -2
  23. package/agent-backend/src/test/java/com/example/agent/application/service/ToolExecutionPipelineTest.java +83 -2
  24. package/agent-backend/src/test/java/com/example/agent/application/service/ToolMessageCompactorTest.java +2 -2
  25. package/agent-backend/src/test/java/com/example/agent/application/service/localtool/AgentSessionBatchServiceTest.java +85 -0
  26. package/agent-backend/src/test/java/com/example/agent/application/service/localtool/AnalysisResultAggregationServiceTest.java +103 -20
  27. package/agent-front-react/src/constants/sessionFilters.ts +134 -114
  28. package/agent-front-react/src/pages/AnalysisTaskPage.tsx +176 -56
  29. package/agent-front-react/src/pages/analysis-task/SessionBatchFilterModal.tsx +7 -24
  30. package/agent-front-react/src/pages/analysis-task/SessionBatchPanel.tsx +144 -193
  31. package/agent-front-react/src/pages/analysis-task/analysisTaskTypes.ts +0 -2
  32. 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 是否引用了 `clusters_by_target`、`assignment_trace_sample_by_target` 或其他带 `session_code` 的工具输出,不是后端结构化强约束。
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;单目标任务使用 default,多目标任务只能使用 analysis_targets 中的 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 (Object key : ((Map<?, ?>) value).keySet()) {
138
- refs.add(toolName + ":" + namespace + "." + String.valueOf(key));
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>();
@@ -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
- template.put("report_inputs", buildReportInputTemplate("report_inputs", rawReportInputs, priorSteps, step));
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
 
@@ -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 AGGREGATE_CATEGORY_SAMPLE_ROWS = "aggregate_analysis_results:exports.category_sample_rows";
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
- appendCategorySampleTable(tables, inputs, referencedSourceLabels, AGGREGATE_CATEGORY_SAMPLE_ROWS, "聚合分类样本");
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
- columns.add(column("描述", "description"));
119
- columns.add(column("样本关键会话", "sample_key_sessions"));
120
- columns.add(column("样本会话ID", "sample_session_ids"));
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) || "category_sample_rows".equals(label)
179
- || "exports.category_sample_rows".equals(label)) {
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:")