flzxsqc-demo 1.4.26 → 1.4.28
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/customer-service-excellent-case-mining/customer-service-excellent-case-mining-requirement-breakdown-v1.docx +0 -0
- package/agent-backend/src/main/java/com/example/agent/application/harness/tool/ToolInvocationGuard.java +42 -9
- package/agent-backend/src/main/java/com/example/agent/application/service/AgentTurnRunner.java +86 -4
- package/agent-backend/src/main/java/com/example/agent/application/service/RuntimeEventBus.java +42 -5
- package/agent-backend/src/main/java/com/example/agent/application/service/ToolArgumentPlannerService.java +68 -21
- package/agent-backend/src/main/java/com/example/agent/application/service/filter/AgentSessionFilter.java +30 -8
- package/agent-backend/src/main/java/com/example/agent/application/service/report/MarkdownReportDocxConverter.java +94 -38
- package/agent-backend/src/main/java/com/example/agent/application/service/tool/argument/SkillPerRecordAnalysisApiToolArgumentResolver.java +98 -0
- package/agent-backend/src/main/java/com/example/agent/domain/model/ToolPrecondition.java +5 -0
- package/agent-backend/src/main/java/com/example/agent/infrastructure/es/ElasticsearchLargeContentStore.java +5 -23
- package/agent-backend/src/main/java/com/example/agent/infrastructure/llm/DummyLlmClient.java +226 -8
- package/agent-backend/src/main/java/com/example/agent/infrastructure/repository/AgentEventRepository.java +2 -2
- package/agent-backend/src/main/java/com/example/agent/infrastructure/tool/ToolsApiToolExecutor.java +1 -0
- package/agent-backend/src/main/java/com/example/agent/interfaces/rest/ToolsController.java +7 -2
- package/agent-backend/src/main/resources/data.sql +172 -3
- package/agent-backend/src/test/java/com/example/agent/application/harness/tool/ToolInvocationGuardTest.java +55 -0
- package/agent-backend/src/test/java/com/example/agent/application/service/RuntimeEventBusTest.java +67 -0
- package/agent-backend/src/test/java/com/example/agent/application/service/ToolArgumentPlannerServiceTest.java +81 -0
- package/agent-backend/src/test/java/com/example/agent/application/service/tool/argument/SkillPerRecordAnalysisApiToolArgumentResolverTest.java +94 -0
- package/agent-backend/src/test/java/com/example/agent/infrastructure/repository/AgentEventRepositoryTest.java +34 -0
- package/package.json +1 -1
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig01-technical-overview.png +0 -0
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig01-technical-overview.svg +0 -171
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig02-harness-controlled-agent.png +0 -0
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig02-harness-controlled-agent.svg +0 -131
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig03-harness-state-contract.png +0 -0
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig03-harness-state-contract.svg +0 -118
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig04-evidence-batch-analysis.png +0 -0
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig04-evidence-batch-analysis.svg +0 -145
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig05-analysis-discovery.png +0 -0
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig05-analysis-discovery.svg +0 -111
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig06-human-ai-improvement-loop.png +0 -0
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig06-human-ai-improvement-loop.svg +0 -108
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig07-prompt-dynamic-injection.png +0 -0
- package/agent-backend/docs/innovation-capability-review/assets-v8/fig07-prompt-dynamic-injection.svg +0 -129
- package/agent-backend/docs/innovation-capability-review/company-innovation-capability-review-v10.docx +0 -0
|
Binary file
|
|
@@ -14,6 +14,7 @@ import com.example.agent.domain.model.ToolValidationException;
|
|
|
14
14
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
15
15
|
import org.springframework.stereotype.Component;
|
|
16
16
|
|
|
17
|
+
import java.util.ArrayList;
|
|
17
18
|
import java.util.List;
|
|
18
19
|
import java.util.Map;
|
|
19
20
|
|
|
@@ -125,25 +126,57 @@ public class ToolInvocationGuard {
|
|
|
125
126
|
|
|
126
127
|
private void validateRequiredArtifact(AgentSession session, ToolDefinition definition, ToolPrecondition precondition) {
|
|
127
128
|
String artifact = trimToNull(precondition.getArtifact());
|
|
128
|
-
String
|
|
129
|
-
if (artifact == null ||
|
|
129
|
+
List<String> sourceTools = sourceTools(precondition);
|
|
130
|
+
if (artifact == null || sourceTools.isEmpty()) {
|
|
130
131
|
return;
|
|
131
132
|
}
|
|
133
|
+
if (hasRequiredArtifact(session, sourceTools, artifact)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
throw validation(
|
|
137
|
+
"TOOL_PRECONDITION_FAILED",
|
|
138
|
+
"Tool '" + definition.getToolId() + "' requires artifact '" + artifact
|
|
139
|
+
+ "' from one of tools " + sourceTools + ".",
|
|
140
|
+
"Call one of " + sourceTools + " first, then bind artifacts." + artifact + " before retrying."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private List<String> sourceTools(ToolPrecondition precondition) {
|
|
145
|
+
List<String> result = new ArrayList<String>();
|
|
146
|
+
addSourceTool(result, precondition.getSourceTool());
|
|
147
|
+
if (precondition.getSourceTools() != null) {
|
|
148
|
+
for (String sourceTool : precondition.getSourceTools()) {
|
|
149
|
+
addSourceTool(result, sourceTool);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private void addSourceTool(List<String> sourceTools, String sourceTool) {
|
|
156
|
+
String normalized = trimToNull(sourceTool);
|
|
157
|
+
if (normalized != null && !sourceTools.contains(normalized)) {
|
|
158
|
+
sourceTools.add(normalized);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private boolean hasRequiredArtifact(AgentSession session, List<String> sourceTools, String artifact) {
|
|
132
163
|
for (int i = session.getExecutedSteps().size() - 1; i >= 0; i--) {
|
|
133
164
|
AgentExecutedStep step = session.getExecutedSteps().get(i);
|
|
134
|
-
if (
|
|
165
|
+
if (!isSuccessfulSource(step, sourceTools)) {
|
|
135
166
|
continue;
|
|
136
167
|
}
|
|
137
168
|
Object value = step.getArtifacts().get(artifact);
|
|
138
169
|
if (value != null && trimToNull(String.valueOf(value)) != null) {
|
|
139
|
-
return;
|
|
170
|
+
return true;
|
|
140
171
|
}
|
|
141
172
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private boolean isSuccessfulSource(AgentExecutedStep step, List<String> sourceTools) {
|
|
177
|
+
return step != null
|
|
178
|
+
&& "SUCCEEDED".equals(step.getStatus())
|
|
179
|
+
&& sourceTools.contains(step.getToolName());
|
|
147
180
|
}
|
|
148
181
|
|
|
149
182
|
private void validateRequiredFields(ToolDefinition definition, String root, Map<String, Object> source, List<String> requiredPaths) {
|
package/agent-backend/src/main/java/com/example/agent/application/service/AgentTurnRunner.java
CHANGED
|
@@ -20,6 +20,8 @@ import com.example.agent.domain.model.chat.ChatRole;
|
|
|
20
20
|
import com.example.agent.domain.port.LlmClient;
|
|
21
21
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
22
22
|
import lombok.RequiredArgsConstructor;
|
|
23
|
+
import org.slf4j.Logger;
|
|
24
|
+
import org.slf4j.LoggerFactory;
|
|
23
25
|
import org.springframework.beans.factory.annotation.Qualifier;
|
|
24
26
|
import org.springframework.core.task.TaskExecutor;
|
|
25
27
|
import org.springframework.stereotype.Service;
|
|
@@ -46,6 +48,7 @@ import java.util.UUID;
|
|
|
46
48
|
*/
|
|
47
49
|
public class AgentTurnRunner {
|
|
48
50
|
private static final int MAX_ROUNDS = 16;
|
|
51
|
+
private static final Logger log = LoggerFactory.getLogger(AgentTurnRunner.class);
|
|
49
52
|
|
|
50
53
|
private final SkillSelector skillSelector;
|
|
51
54
|
private final SkillRegistry skillRegistry;
|
|
@@ -298,13 +301,30 @@ public class AgentTurnRunner {
|
|
|
298
301
|
planningDetails.put("round", roundNumber);
|
|
299
302
|
harnessWorkflowService.started(session.getSessionId(), turn.getTurnId(), HarnessPhase.PLAN_NEXT_ACTION, planningDetails);
|
|
300
303
|
ToolActionRouter.ToolActionDecision decision;
|
|
304
|
+
long actionRouterStartedAt = System.currentTimeMillis();
|
|
305
|
+
log.info("Agent action router started sessionId={} turnId={} round={} allowedToolCount={}",
|
|
306
|
+
session.getSessionId(), turn.getTurnId(), roundNumber, allowedTools == null ? 0 : allowedTools.size());
|
|
301
307
|
try {
|
|
302
308
|
decision = toolActionRouter.decide(session, skills, allowedTools, roundNumber);
|
|
303
309
|
} catch (RuntimeException e) {
|
|
304
310
|
// 路由模型不可用时退回旧的组合式规划器以维持可用性;错误被写入 Harness 详情,便于后续删除该兼容路径。
|
|
311
|
+
log.warn("Agent action router failed; combined fallback will start sessionId={} turnId={} round={} elapsedMs={} errorType={} error={}",
|
|
312
|
+
session.getSessionId(),
|
|
313
|
+
turn.getTurnId(),
|
|
314
|
+
roundNumber,
|
|
315
|
+
System.currentTimeMillis() - actionRouterStartedAt,
|
|
316
|
+
e.getClass().getSimpleName(),
|
|
317
|
+
e.getMessage());
|
|
305
318
|
planningDetails.put("action_router_fallback_error", e.getMessage());
|
|
306
319
|
return fallbackCombinedPlannerDecision(session, turn, allowedTools, roundNumber, planningDetails);
|
|
307
320
|
}
|
|
321
|
+
log.info("Agent action router completed sessionId={} turnId={} round={} elapsedMs={} action={} selectedToolId={}",
|
|
322
|
+
session.getSessionId(),
|
|
323
|
+
turn.getTurnId(),
|
|
324
|
+
roundNumber,
|
|
325
|
+
System.currentTimeMillis() - actionRouterStartedAt,
|
|
326
|
+
decision.getActionType(),
|
|
327
|
+
decision.getToolId());
|
|
308
328
|
planningDetails.put("selected_tool_id", decision.getToolId());
|
|
309
329
|
planningDetails.put("decision_reason", decision.getReason());
|
|
310
330
|
planningDetails.put("candidate_tool_ids", toolIds(allowedTools));
|
|
@@ -320,8 +340,30 @@ public class AgentTurnRunner {
|
|
|
320
340
|
}
|
|
321
341
|
ToolDefinition selectedTool = selectedTool(decision.getToolId(), allowedTools);
|
|
322
342
|
// 动作路由只选择工具;参数规划仅暴露被选中工具的 Schema,避免模型混淆多个工具字段并降低上下文开销。
|
|
323
|
-
|
|
324
|
-
|
|
343
|
+
long argumentPlannerStartedAt = System.currentTimeMillis();
|
|
344
|
+
log.info("Agent argument planner started sessionId={} turnId={} round={} selectedToolId={}",
|
|
345
|
+
session.getSessionId(), turn.getTurnId(), roundNumber, selectedTool.getToolId());
|
|
346
|
+
ToolArgumentPlannerService.ArgumentPlanningResult argumentPlanningResult;
|
|
347
|
+
try {
|
|
348
|
+
argumentPlanningResult = toolArgumentPlannerService.planToolCall(session, skills, selectedTool, roundNumber);
|
|
349
|
+
} catch (RuntimeException e) {
|
|
350
|
+
log.warn("Agent argument planner failed sessionId={} turnId={} round={} selectedToolId={} elapsedMs={} errorType={} error={}",
|
|
351
|
+
session.getSessionId(),
|
|
352
|
+
turn.getTurnId(),
|
|
353
|
+
roundNumber,
|
|
354
|
+
selectedTool.getToolId(),
|
|
355
|
+
System.currentTimeMillis() - argumentPlannerStartedAt,
|
|
356
|
+
e.getClass().getSimpleName(),
|
|
357
|
+
e.getMessage());
|
|
358
|
+
throw e;
|
|
359
|
+
}
|
|
360
|
+
log.info("Agent argument planner completed sessionId={} turnId={} round={} selectedToolId={} elapsedMs={} retryCount={}",
|
|
361
|
+
session.getSessionId(),
|
|
362
|
+
turn.getTurnId(),
|
|
363
|
+
roundNumber,
|
|
364
|
+
selectedTool.getToolId(),
|
|
365
|
+
System.currentTimeMillis() - argumentPlannerStartedAt,
|
|
366
|
+
argumentPlanningResult.getRetryCount());
|
|
325
367
|
ChatMessage reply = argumentPlanningResult.getReply();
|
|
326
368
|
runtimeEventBus.publish(session.getSessionId(), turn.getTurnId(), RuntimeEventType.MODEL_DECISION,
|
|
327
369
|
payload("content", reply.getContent(), "toolCalls", reply.getToolCalls(), "round", roundNumber,
|
|
@@ -341,7 +383,33 @@ public class AgentTurnRunner {
|
|
|
341
383
|
int roundNumber,
|
|
342
384
|
Map<String, Object> planningDetails) {
|
|
343
385
|
List<ChatMessage> messagesForLlm = contextBudgetService.prepareMessagesForToolPlanning(session, allowedTools);
|
|
344
|
-
|
|
386
|
+
long fallbackStartedAt = System.currentTimeMillis();
|
|
387
|
+
log.info("Agent combined fallback started sessionId={} turnId={} round={} messageCount={} allowedToolCount={}",
|
|
388
|
+
session.getSessionId(),
|
|
389
|
+
turn.getTurnId(),
|
|
390
|
+
roundNumber,
|
|
391
|
+
messagesForLlm.size(),
|
|
392
|
+
allowedTools == null ? 0 : allowedTools.size());
|
|
393
|
+
ChatMessage reply;
|
|
394
|
+
try {
|
|
395
|
+
reply = LlmRoleContext.call(ModelRole.PLANNER, () -> llmClient.chat(messagesForLlm, allowedTools));
|
|
396
|
+
} catch (RuntimeException e) {
|
|
397
|
+
log.warn("Agent combined fallback failed sessionId={} turnId={} round={} elapsedMs={} errorType={} error={}",
|
|
398
|
+
session.getSessionId(),
|
|
399
|
+
turn.getTurnId(),
|
|
400
|
+
roundNumber,
|
|
401
|
+
System.currentTimeMillis() - fallbackStartedAt,
|
|
402
|
+
e.getClass().getSimpleName(),
|
|
403
|
+
e.getMessage());
|
|
404
|
+
throw e;
|
|
405
|
+
}
|
|
406
|
+
log.info("Agent combined fallback completed sessionId={} turnId={} round={} elapsedMs={} contentChars={} toolCallCount={}",
|
|
407
|
+
session.getSessionId(),
|
|
408
|
+
turn.getTurnId(),
|
|
409
|
+
roundNumber,
|
|
410
|
+
System.currentTimeMillis() - fallbackStartedAt,
|
|
411
|
+
reply.getContent() == null ? 0 : reply.getContent().length(),
|
|
412
|
+
reply.getToolCalls() == null ? 0 : reply.getToolCalls().size());
|
|
345
413
|
runtimeEventBus.publish(session.getSessionId(), turn.getTurnId(), RuntimeEventType.MODEL_DECISION,
|
|
346
414
|
payload("content", reply.getContent(), "toolCalls", reply.getToolCalls(), "round", roundNumber,
|
|
347
415
|
"allowed_tool_ids", toolIds(allowedTools), "report_tool_allowed", containsTool(allowedTools, "build_keyword_report"),
|
|
@@ -413,13 +481,27 @@ public class AgentTurnRunner {
|
|
|
413
481
|
}
|
|
414
482
|
|
|
415
483
|
private List<SkillDefinition> questionRequirementSkills(List<SkillDefinition> selectedSkills) {
|
|
416
|
-
if (
|
|
484
|
+
if (hasQuestionRequirementsConfiguration(selectedSkills)) {
|
|
417
485
|
return selectedSkills;
|
|
418
486
|
}
|
|
419
487
|
List<SkillDefinition> allSkills = skillRegistry.allEnabled();
|
|
420
488
|
return hasEnabledQuestionRequirements(allSkills) ? allSkills : selectedSkills;
|
|
421
489
|
}
|
|
422
490
|
|
|
491
|
+
private boolean hasQuestionRequirementsConfiguration(List<SkillDefinition> skills) {
|
|
492
|
+
if (skills == null) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
for (SkillDefinition skill : skills) {
|
|
496
|
+
if (skill != null
|
|
497
|
+
&& skill.getQuestionRequirements() != null
|
|
498
|
+
&& skill.getQuestionRequirements().containsKey("enabled")) {
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
|
|
423
505
|
private boolean hasEnabledQuestionRequirements(List<SkillDefinition> skills) {
|
|
424
506
|
if (skills == null) {
|
|
425
507
|
return false;
|
package/agent-backend/src/main/java/com/example/agent/application/service/RuntimeEventBus.java
CHANGED
|
@@ -6,6 +6,8 @@ import com.example.agent.infrastructure.repository.AgentEventRepository;
|
|
|
6
6
|
import com.fasterxml.jackson.core.type.TypeReference;
|
|
7
7
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
8
8
|
import lombok.RequiredArgsConstructor;
|
|
9
|
+
import org.slf4j.Logger;
|
|
10
|
+
import org.slf4j.LoggerFactory;
|
|
9
11
|
import org.springframework.stereotype.Service;
|
|
10
12
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
11
13
|
|
|
@@ -30,6 +32,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|
|
30
32
|
* 但 trace/span/turn/event 等稳定字段由本类统一写入。</p>
|
|
31
33
|
*/
|
|
32
34
|
public class RuntimeEventBus {
|
|
35
|
+
private static final Logger log = LoggerFactory.getLogger(RuntimeEventBus.class);
|
|
36
|
+
|
|
33
37
|
private final AgentEventRepository repository;
|
|
34
38
|
private final ObjectMapper objectMapper;
|
|
35
39
|
private final Map<String, CopyOnWriteArrayList<SseEmitter>> subscribers = new ConcurrentHashMap<String, CopyOnWriteArrayList<SseEmitter>>();
|
|
@@ -62,16 +66,49 @@ public class RuntimeEventBus {
|
|
|
62
66
|
event.setTurnId((String) row.get("turn_id"));
|
|
63
67
|
event.setEventType(RuntimeEventType.valueOf(String.valueOf(row.get("event_type"))));
|
|
64
68
|
event.setCreatedAt(toInstant(row.get("created_at")));
|
|
65
|
-
|
|
66
|
-
event.setPayload(objectMapper.readValue(String.valueOf(row.get("payload_json")), new TypeReference<Map<String, Object>>() {}));
|
|
67
|
-
} catch (Exception e) {
|
|
68
|
-
throw new RuntimeException("failed to parse event payload", e);
|
|
69
|
-
}
|
|
69
|
+
event.setPayload(readHistoryPayload(row, event));
|
|
70
70
|
events.add(event);
|
|
71
71
|
}
|
|
72
72
|
return events;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
private Map<String, Object> readHistoryPayload(Map<String, Object> row, RuntimeEvent event) {
|
|
76
|
+
Object rawPayload = row.get("payload_json");
|
|
77
|
+
if (rawPayload == null) {
|
|
78
|
+
log.warn("Runtime event payload unavailable eventId={} sessionId={} turnId={} eventType={} payloadRefId={}",
|
|
79
|
+
event.getId(), event.getSessionId(), event.getTurnId(), event.getEventType(), row.get("payload_ref_id"));
|
|
80
|
+
return unavailablePayload(row, "missing");
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
Map<String, Object> payload = objectMapper.readValue(
|
|
84
|
+
String.valueOf(rawPayload),
|
|
85
|
+
new TypeReference<Map<String, Object>>() {});
|
|
86
|
+
return payload == null ? unavailablePayload(row, "empty") : payload;
|
|
87
|
+
} catch (Exception e) {
|
|
88
|
+
log.warn("Runtime event payload is invalid JSON eventId={} sessionId={} turnId={} eventType={} payloadRefId={} error={}",
|
|
89
|
+
event.getId(),
|
|
90
|
+
event.getSessionId(),
|
|
91
|
+
event.getTurnId(),
|
|
92
|
+
event.getEventType(),
|
|
93
|
+
row.get("payload_ref_id"),
|
|
94
|
+
e.getMessage());
|
|
95
|
+
return unavailablePayload(row, "invalid_json");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private Map<String, Object> unavailablePayload(Map<String, Object> row, String reason) {
|
|
100
|
+
Map<String, Object> payload = new HashMap<String, Object>();
|
|
101
|
+
payload.put("payload_unavailable", true);
|
|
102
|
+
payload.put("payload_error", reason);
|
|
103
|
+
if (row.get("payload_ref_id") != null) {
|
|
104
|
+
payload.put("payload_ref_id", row.get("payload_ref_id"));
|
|
105
|
+
}
|
|
106
|
+
if (row.get("payload_preview") != null) {
|
|
107
|
+
payload.put("payload_preview", row.get("payload_preview"));
|
|
108
|
+
}
|
|
109
|
+
return payload;
|
|
110
|
+
}
|
|
111
|
+
|
|
75
112
|
public SseEmitter subscribe(String sessionId) {
|
|
76
113
|
SseEmitter emitter = new SseEmitter(0L);
|
|
77
114
|
subscribers.computeIfAbsent(sessionId, ignored -> new CopyOnWriteArrayList<SseEmitter>()).add(emitter);
|
|
@@ -31,6 +31,7 @@ import java.util.UUID;
|
|
|
31
31
|
*/
|
|
32
32
|
public class ToolArgumentPlannerService {
|
|
33
33
|
private static final int SKILL_PROMPT_LIMIT = 10000;
|
|
34
|
+
private static final String SKILL_PER_RECORD_ANALYSIS_TOOL_ID = "analyze_sessions_per_record_by_skill_async";
|
|
34
35
|
|
|
35
36
|
private final LlmClient llmClient;
|
|
36
37
|
private final ContextBudgetService contextBudgetService;
|
|
@@ -44,26 +45,26 @@ public class ToolArgumentPlannerService {
|
|
|
44
45
|
List<ChatMessage> messages = argumentPlanningMessages(session, skills, selectedTool, roundNumber);
|
|
45
46
|
List<ToolDefinition> selectedOnly = Collections.singletonList(selectedTool);
|
|
46
47
|
ChatMessage reply = LlmRoleContext.call(ModelRole.PLANNER, () -> llmClient.chat(messages, selectedOnly));
|
|
47
|
-
if (
|
|
48
|
+
if (hasSelectedToolCall(reply, selectedTool)) {
|
|
48
49
|
return new ArgumentPlanningResult(reply, tokenEstimateService.estimateMessages(messages), 0, false);
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
messages.add(message(ChatRole.ASSISTANT, reply.getContent()));
|
|
54
|
-
messages.add(message(ChatRole.SYSTEM, malformedToolCallRecoveryInstruction(selectedTool)));
|
|
51
|
+
boolean malformedTextToolCall = looksLikeTextToolCall(reply == null ? null : reply.getContent());
|
|
52
|
+
addRetryContext(messages, reply, selectedTool, malformedTextToolCall);
|
|
55
53
|
ChatMessage retryReply = LlmRoleContext.call(ModelRole.PLANNER, () -> llmClient.chat(messages, selectedOnly));
|
|
56
|
-
if (
|
|
57
|
-
return new ArgumentPlanningResult(
|
|
54
|
+
if (hasSelectedToolCall(retryReply, selectedTool)) {
|
|
55
|
+
return new ArgumentPlanningResult(
|
|
56
|
+
retryReply,
|
|
57
|
+
tokenEstimateService.estimateMessages(messages),
|
|
58
|
+
1,
|
|
59
|
+
malformedTextToolCall
|
|
60
|
+
);
|
|
58
61
|
}
|
|
59
|
-
ChatMessage repaired =
|
|
60
|
-
if (
|
|
61
|
-
repaired = repairTextToolCall(reply.getContent(), selectedTool);
|
|
62
|
-
}
|
|
63
|
-
if (hasToolCalls(repaired)) {
|
|
62
|
+
ChatMessage repaired = repairTextToolCallFromReplies(reply, retryReply, selectedTool);
|
|
63
|
+
if (hasSelectedToolCall(repaired, selectedTool)) {
|
|
64
64
|
return new ArgumentPlanningResult(repaired, tokenEstimateService.estimateMessages(messages), 1, true);
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
throw new IllegalStateException("selected tool `" + selectedTool.getToolId()
|
|
67
|
+
+ "` produced no executable function/tool call after one correction");
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
private List<ChatMessage> argumentPlanningMessages(AgentSession session,
|
|
@@ -88,11 +89,28 @@ public class ToolArgumentPlannerService {
|
|
|
88
89
|
builder.append("Use the selected tool schema supplied by the API. Do not call any other tool.\n");
|
|
89
90
|
builder.append("Use source_ref/source_refs for prior evidence when possible. Do not copy large prior tool outputs into arguments.\n");
|
|
90
91
|
builder.append("Relevant skill details:\n");
|
|
92
|
+
appendRelevantSkillDetails(builder, skills, selectedTool);
|
|
93
|
+
return builder.toString();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private void appendRelevantSkillDetails(StringBuilder builder,
|
|
97
|
+
List<SkillDefinition> skills,
|
|
98
|
+
ToolDefinition selectedTool) {
|
|
99
|
+
if (isSkillPerRecordAnalysisTool(selectedTool)) {
|
|
100
|
+
builder.append("### server-managed-skill-analysis\n");
|
|
101
|
+
builder.append("The runtime injects the excellent-case analysis prompt and fixed analysis_targets. ");
|
|
102
|
+
builder.append("Do not generate analysis_prompt or analysis_targets. ");
|
|
103
|
+
builder.append("Generate the queryId from the prior query handle.\n\n");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
91
106
|
for (SkillDefinition skill : relevantSkills(skills, selectedTool.getToolId())) {
|
|
92
107
|
builder.append("### ").append(skill.getSkillId()).append("\n");
|
|
93
108
|
builder.append(truncate(skill.getPromptMarkdown(), SKILL_PROMPT_LIMIT)).append("\n\n");
|
|
94
109
|
}
|
|
95
|
-
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private boolean isSkillPerRecordAnalysisTool(ToolDefinition selectedTool) {
|
|
113
|
+
return selectedTool != null && SKILL_PER_RECORD_ANALYSIS_TOOL_ID.equals(selectedTool.getToolId());
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
private List<SkillDefinition> relevantSkills(List<SkillDefinition> skills, String toolId) {
|
|
@@ -108,10 +126,33 @@ public class ToolArgumentPlannerService {
|
|
|
108
126
|
return result.isEmpty() ? skills : result;
|
|
109
127
|
}
|
|
110
128
|
|
|
111
|
-
private
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
129
|
+
private void addRetryContext(List<ChatMessage> messages,
|
|
130
|
+
ChatMessage reply,
|
|
131
|
+
ToolDefinition selectedTool,
|
|
132
|
+
boolean malformedTextToolCall) {
|
|
133
|
+
if (reply != null && hasText(reply.getContent())) {
|
|
134
|
+
messages.add(message(ChatRole.ASSISTANT, reply.getContent()));
|
|
135
|
+
}
|
|
136
|
+
messages.add(message(ChatRole.SYSTEM, toolCallRecoveryInstruction(selectedTool, malformedTextToolCall)));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private String toolCallRecoveryInstruction(ToolDefinition selectedTool, boolean malformedTextToolCall) {
|
|
140
|
+
String reason = malformedTextToolCall
|
|
141
|
+
? "the previous response wrote a tool call as assistant text"
|
|
142
|
+
: "the previous response did not emit an executable tool call";
|
|
143
|
+
return "Runtime correction: " + reason + ". "
|
|
144
|
+
+ "Call `" + selectedTool.getToolId()
|
|
145
|
+
+ "` using the actual function/tool channel now. Emit exactly one call and no prose or raw tool_call JSON.";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private ChatMessage repairTextToolCallFromReplies(ChatMessage firstReply,
|
|
149
|
+
ChatMessage retryReply,
|
|
150
|
+
ToolDefinition selectedTool) {
|
|
151
|
+
ChatMessage repaired = repairTextToolCall(retryReply == null ? null : retryReply.getContent(), selectedTool);
|
|
152
|
+
if (hasSelectedToolCall(repaired, selectedTool)) {
|
|
153
|
+
return repaired;
|
|
154
|
+
}
|
|
155
|
+
return repairTextToolCall(firstReply == null ? null : firstReply.getContent(), selectedTool);
|
|
115
156
|
}
|
|
116
157
|
|
|
117
158
|
private ChatMessage repairTextToolCall(String content, ToolDefinition selectedTool) {
|
|
@@ -171,8 +212,14 @@ public class ToolArgumentPlannerService {
|
|
|
171
212
|
&& payload.containsKey("arguments");
|
|
172
213
|
}
|
|
173
214
|
|
|
174
|
-
private boolean
|
|
175
|
-
|
|
215
|
+
private boolean hasSelectedToolCall(ChatMessage reply, ToolDefinition selectedTool) {
|
|
216
|
+
if (reply == null || reply.getToolCalls() == null || reply.getToolCalls().size() != 1) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
ToolCall toolCall = reply.getToolCalls().get(0);
|
|
220
|
+
return toolCall != null
|
|
221
|
+
&& toolCall.getFunction() != null
|
|
222
|
+
&& selectedTool.getToolId().equals(toolCall.getFunction().getName());
|
|
176
223
|
}
|
|
177
224
|
|
|
178
225
|
private ChatMessage message(ChatRole role, String content) {
|
|
@@ -34,21 +34,43 @@ public class AgentSessionFilter {
|
|
|
34
34
|
private Map<String, Object> extensions = new LinkedHashMap<String, Object>();
|
|
35
35
|
|
|
36
36
|
public boolean isEmpty() {
|
|
37
|
+
return isBasicScopeEmpty()
|
|
38
|
+
&& isRoutingScopeEmpty()
|
|
39
|
+
&& isBusinessScopeEmpty()
|
|
40
|
+
&& isResultControlEmpty();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private boolean isBasicScopeEmpty() {
|
|
37
44
|
return sessionIds.isEmpty()
|
|
38
45
|
&& channel == null
|
|
39
46
|
&& batchCode == null
|
|
40
|
-
&&
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
&& isTimeRangeEmpty();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private boolean isTimeRangeEmpty() {
|
|
51
|
+
return startTime == null && endTime == null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private boolean isRoutingScopeEmpty() {
|
|
55
|
+
return agentId == null
|
|
43
56
|
&& queueId == null
|
|
44
57
|
&& teamIds.isEmpty()
|
|
45
|
-
&& groupIds.isEmpty()
|
|
46
|
-
|
|
58
|
+
&& groupIds.isEmpty();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private boolean isBusinessScopeEmpty() {
|
|
62
|
+
return fullJourney == null
|
|
47
63
|
&& dataSource == null
|
|
48
64
|
&& conversationType == null
|
|
49
|
-
&&
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
&& isBusinessIdentityEmpty();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private boolean isBusinessIdentityEmpty() {
|
|
69
|
+
return businessLine == null && customerId == null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private boolean isResultControlEmpty() {
|
|
73
|
+
return keywords.isEmpty()
|
|
52
74
|
&& limit == null
|
|
53
75
|
&& extensions.isEmpty();
|
|
54
76
|
}
|