|
|
@@ -11,7 +11,12 @@ import com.emoon.ai.agent.domain.RouteDecision;
|
|
|
import com.emoon.ai.card.application.CardInstanceService;
|
|
|
import com.emoon.ai.device.api.DeviceRegistryFacade;
|
|
|
import com.emoon.ai.device.api.SceneProfileResult;
|
|
|
+import com.emoon.ai.device.application.DeviceCommandService;
|
|
|
+import com.emoon.ai.mcp.application.McpToolService;
|
|
|
+import com.emoon.ai.meter.application.MeterEventProducer;
|
|
|
+import com.emoon.mcp.domain.AiCardInstance;
|
|
|
import com.emoon.mcp.domain.AiConversation;
|
|
|
+import com.emoon.mcp.his.domain.HisDepartment;
|
|
|
import com.emoon.openplatform.service.IAgentChatApplicationService;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
@@ -36,6 +41,9 @@ public class AgentChatApplicationServiceImpl implements IAgentChatApplicationSer
|
|
|
private final TaskStateService taskStateService;
|
|
|
private final CardInstanceService cardInstanceService;
|
|
|
private final DeviceRegistryFacade deviceRegistryFacade;
|
|
|
+ private final DeviceCommandService deviceCommandService;
|
|
|
+ private final McpToolService mcpToolService;
|
|
|
+ private final MeterEventProducer meterProducer;
|
|
|
private final Environment env;
|
|
|
|
|
|
private static final List<String> FULL_AGENTS = List.of(
|
|
|
@@ -46,43 +54,39 @@ public class AgentChatApplicationServiceImpl implements IAgentChatApplicationSer
|
|
|
@Override
|
|
|
public void chatStream(AgentChatCommand command, SseEmitter emitter) {
|
|
|
String traceId = "trace_" + UUID.randomUUID().toString().substring(0, 12);
|
|
|
- Integer projectId = command.projectId() != null
|
|
|
- ? Integer.valueOf(command.projectId()) : 1;
|
|
|
+ Integer projectId = command.projectId() != null ? Integer.valueOf(command.projectId()) : 1;
|
|
|
String projectIdStr = String.valueOf(projectId);
|
|
|
+ String intentCode = "UNKNOWN";
|
|
|
+ String taskId = null;
|
|
|
|
|
|
try {
|
|
|
AiConversation conv = conversationService.createOrLoad(
|
|
|
- projectId, command.conversationId(),
|
|
|
- command.deviceId(), command.hospitalId(),
|
|
|
- command.agentId(), command.userId());
|
|
|
+ projectId, command.conversationId(), command.deviceId(),
|
|
|
+ command.hospitalId(), command.agentId(), command.userId());
|
|
|
String conversationId = conv.getConversationId();
|
|
|
|
|
|
HashMap<String, String> userMeta = new HashMap<>();
|
|
|
- userMeta.put("clientMessageId",
|
|
|
- command.clientMessageId() != null ? command.clientMessageId() : "");
|
|
|
- conversationService.appendMessage(conversationId, "user",
|
|
|
- command.text(), JSONUtil.toJsonStr(userMeta), traceId);
|
|
|
-
|
|
|
- // Resolve device scene — strict in non-dev profiles
|
|
|
- List<String> allowedAgents = resolveAllowedAgents(
|
|
|
- projectIdStr, command.deviceId());
|
|
|
- RouterContext ctx = new RouterContext(
|
|
|
- conversationId, command.text(),
|
|
|
+ userMeta.put("clientMessageId", command.clientMessageId() != null ? command.clientMessageId() : "");
|
|
|
+ conversationService.appendMessage(conversationId, "user", command.text(),
|
|
|
+ JSONUtil.toJsonStr(userMeta), traceId);
|
|
|
+
|
|
|
+ List<String> allowedAgents = resolveAllowedAgents(projectIdStr, command.deviceId());
|
|
|
+ RouterContext ctx = new RouterContext(conversationId, command.text(),
|
|
|
command.deviceType(), allowedAgents, projectIdStr);
|
|
|
RouteDecision decision = routerService.route(ctx);
|
|
|
|
|
|
if (decision.blocked()) {
|
|
|
- HashMap<String, String> blocked = new HashMap<>();
|
|
|
- blocked.put("text", decision.blockedMessage());
|
|
|
- emitter.send(event("message_delta", blocked));
|
|
|
- HashMap<String, String> done = new HashMap<>();
|
|
|
- done.put("conversationId", conversationId);
|
|
|
- emitter.send(event("completed", done));
|
|
|
+ sendSSE(emitter, "message_delta", map("text", decision.blockedMessage()));
|
|
|
+ sendSSE(emitter, "completed", map("conversationId", conversationId));
|
|
|
emitter.complete();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- String taskId = null;
|
|
|
+ intentCode = decision.intentCode() != null ? decision.intentCode() : "UNKNOWN";
|
|
|
+ String replyText = replyTemplates.reply(intentCode);
|
|
|
+ String messageId = "msg_" + UUID.randomUUID().toString().substring(0, 8);
|
|
|
+
|
|
|
+ // ——— Task & Card & Command creation ———
|
|
|
if ("CREATE_NEW_TASK".equals(decision.taskPolicy()) && decision.taskType() != null) {
|
|
|
String currentStep = switch (decision.taskType()) {
|
|
|
case "REGISTRATION" -> "COLLECT_SYMPTOM";
|
|
|
@@ -92,90 +96,114 @@ public class AgentChatApplicationServiceImpl implements IAgentChatApplicationSer
|
|
|
};
|
|
|
var task = taskStateService.createTask(conversationId, projectIdStr,
|
|
|
decision.taskType(), decision.routeAgentCode(),
|
|
|
- currentStep, decision.slots(),
|
|
|
- null, command.deviceId(), traceId);
|
|
|
+ currentStep, decision.slots(), null, command.deviceId(), traceId);
|
|
|
taskId = task.getTaskId();
|
|
|
- HashMap<String, String> taskEvent = new HashMap<>();
|
|
|
- taskEvent.put("taskId", taskId);
|
|
|
- taskEvent.put("taskType", decision.taskType());
|
|
|
- taskEvent.put("currentStep", currentStep);
|
|
|
- emitter.send(event("task_updated", taskEvent));
|
|
|
+ sendSSE(emitter, "task_updated", map("taskId", taskId,
|
|
|
+ "taskType", decision.taskType(), "currentStep", currentStep));
|
|
|
+
|
|
|
+ switch (decision.taskType()) {
|
|
|
+ case "REGISTRATION" -> {
|
|
|
+ List<HisDepartment> departments = mcpToolService.queryDepartments();
|
|
|
+ var deptList = departments.stream()
|
|
|
+ .map(d -> Map.<String, Object>of("deptId", d.getDepartmentId(), "name", d.getDepartmentName()))
|
|
|
+ .collect(java.util.stream.Collectors.toList());
|
|
|
+ AiCardInstance card = cardInstanceService.createCard(
|
|
|
+ conversationId, taskId, "department-selection", "1.0",
|
|
|
+ Map.of("departments", deptList), traceId);
|
|
|
+ if (card != null) sendSSE(emitter, "card_created",
|
|
|
+ map("cardInstanceId", card.getInstanceId(), "cardKey", "department-selection"));
|
|
|
+ }
|
|
|
+ case "TONGUE_DIAGNOSIS" -> {
|
|
|
+ AiCardInstance card = cardInstanceService.createCard(
|
|
|
+ conversationId, taskId, "tongue-capture", "1.0",
|
|
|
+ Map.of("uploadField", "请拍摄舌象图片"), traceId);
|
|
|
+ if (card != null) sendSSE(emitter, "card_created",
|
|
|
+ map("cardInstanceId", card.getInstanceId(), "cardKey", "tongue-capture"));
|
|
|
+ }
|
|
|
+ case "GUIDE" -> {
|
|
|
+ // Only ROUTE_NAVIGATION creates route-card + device command
|
|
|
+ if (!"ROUTE_NAVIGATION".equals(decision.intentCode())) break;
|
|
|
+ AiCardInstance card = cardInstanceService.createCard(
|
|
|
+ conversationId, taskId, "route-card", "1.0",
|
|
|
+ Map.of("routeText", replyText), traceId);
|
|
|
+ if (card != null) sendSSE(emitter, "card_created",
|
|
|
+ map("cardInstanceId", card.getInstanceId(), "cardKey", "route-card"));
|
|
|
+
|
|
|
+ if ("robot".equals(command.deviceType()) && decision.slots() != null) {
|
|
|
+ String location = (String) decision.slots().getOrDefault("targetLocation", "未知地点");
|
|
|
+ deviceCommandService.createCommand(command.deviceId(),
|
|
|
+ "NAVIGATE_TO_LOCATION",
|
|
|
+ Map.of("locationId", location, "routeText", replyText), traceId);
|
|
|
+ meterProducer.produce("DEVICE_COMMAND_CREATED", projectIdStr, traceId,
|
|
|
+ Map.of("commandType", "NAVIGATE_TO_LOCATION", "deviceId", command.deviceId()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- String intentCode = decision.intentCode() != null ? decision.intentCode() : "UNKNOWN";
|
|
|
- String replyText = replyTemplates.reply(intentCode);
|
|
|
- String messageId = "msg_" + UUID.randomUUID().toString().substring(0, 8);
|
|
|
-
|
|
|
- HashMap<String, String> delta = new HashMap<>();
|
|
|
- delta.put("text", replyText);
|
|
|
- emitter.send(event("message_delta", delta));
|
|
|
-
|
|
|
- HashMap<String, String> completed = new HashMap<>();
|
|
|
- completed.put("conversationId", conversationId);
|
|
|
- completed.put("messageId", messageId);
|
|
|
- emitter.send(event("message_completed", completed));
|
|
|
+ // ——— SSE reply ———
|
|
|
+ sendSSE(emitter, "message_delta", map("text", replyText));
|
|
|
+ sendSSE(emitter, "message_completed", map("conversationId", conversationId, "messageId", messageId));
|
|
|
|
|
|
HashMap<String, String> assistantMeta = new HashMap<>();
|
|
|
assistantMeta.put("intentCode", intentCode);
|
|
|
assistantMeta.put("routeAgentCode", decision.routeAgentCode());
|
|
|
assistantMeta.put("taskId", taskId != null ? taskId : "");
|
|
|
- conversationService.appendMessage(conversationId, "assistant",
|
|
|
- replyText, JSONUtil.toJsonStr(assistantMeta), traceId);
|
|
|
-
|
|
|
- HashMap<String, String> fin = new HashMap<>();
|
|
|
- fin.put("conversationId", conversationId);
|
|
|
- if (taskId != null) fin.put("taskId", taskId);
|
|
|
- fin.put("traceId", traceId);
|
|
|
- emitter.send(event("completed", fin));
|
|
|
+ conversationService.appendMessage(conversationId, "assistant", replyText,
|
|
|
+ JSONUtil.toJsonStr(assistantMeta), traceId);
|
|
|
+
|
|
|
+ sendSSE(emitter, "completed", map("conversationId", conversationId,
|
|
|
+ "taskId", taskId, "traceId", traceId));
|
|
|
emitter.complete();
|
|
|
|
|
|
+ meterProducer.produce("AGENT_CHAT_COMPLETED", projectIdStr, traceId,
|
|
|
+ Map.of("intentCode", intentCode, "taskId", taskId != null ? taskId : ""));
|
|
|
+
|
|
|
} catch (IOException e) {
|
|
|
log.error("[AgentChat SSE] send error", e);
|
|
|
emitter.completeWithError(e);
|
|
|
} catch (Exception e) {
|
|
|
log.error("[AgentChat SSE] pipeline error traceId={}", traceId, e);
|
|
|
try {
|
|
|
- HashMap<String, String> err = new HashMap<>();
|
|
|
- err.put("message", "系统处理异常,请稍后重试");
|
|
|
- emitter.send(event("error", err));
|
|
|
+ sendSSE(emitter, "error", map("message", "系统处理异常,请稍后重试"));
|
|
|
emitter.complete();
|
|
|
- } catch (IOException ex) {
|
|
|
- emitter.completeWithError(ex);
|
|
|
- }
|
|
|
+ } catch (IOException ex) { emitter.completeWithError(ex); }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * Resolve allowed agents from device scene profile.
|
|
|
- * Dev profile: fall back to full agents. Non-dev: restricted to guide-agent only.
|
|
|
- */
|
|
|
List<String> resolveAllowedAgents(String projectId, String deviceId) {
|
|
|
if (deviceId == null) return restrictedFallback();
|
|
|
-
|
|
|
try {
|
|
|
SceneProfileResult scene = deviceRegistryFacade.scene(projectId, deviceId);
|
|
|
- if (scene != null && scene.allowedAgents() != null
|
|
|
- && !scene.allowedAgents().isEmpty()) {
|
|
|
+ if (scene != null && scene.allowedAgents() != null && !scene.allowedAgents().isEmpty())
|
|
|
return scene.allowedAgents();
|
|
|
- }
|
|
|
- } catch (Exception e) {
|
|
|
- log.warn("[AgentChat] failed to resolve scene deviceId={}", deviceId, e);
|
|
|
- }
|
|
|
-
|
|
|
+ } catch (Exception e) { log.warn("[AgentChat] scene resolve failed deviceId={}", deviceId, e); }
|
|
|
return restrictedFallback();
|
|
|
}
|
|
|
|
|
|
private List<String> restrictedFallback() {
|
|
|
- boolean isDev = env != null && env.matchesProfiles("dev", "local");
|
|
|
- if (isDev) {
|
|
|
- log.debug("[AgentChat] dev fallback: full agents");
|
|
|
- return FULL_AGENTS;
|
|
|
- }
|
|
|
- log.warn("[AgentChat] non-dev: scene not resolved, restricting to guide-agent only");
|
|
|
- return RESTRICTED_AGENTS;
|
|
|
+ return (env != null && env.matchesProfiles("dev", "local")) ? FULL_AGENTS : RESTRICTED_AGENTS;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SafeVarargs
|
|
|
+ private static <K, V> Map<K, V> map(Map.Entry<K, V>... entries) {
|
|
|
+ HashMap<K, V> m = new HashMap<>();
|
|
|
+ for (Map.Entry<K, V> e : entries) if (e.getValue() != null) m.put(e.getKey(), e.getValue());
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static <K, V> Map<K, V> map(K k1, V v1) {
|
|
|
+ if (v1 == null) return Map.of();
|
|
|
+ HashMap<K, V> m = new HashMap<>(); m.put(k1, v1); return m;
|
|
|
+ }
|
|
|
+ private static <K, V> Map<K, V> map(K k1, V v1, K k2, V v2) {
|
|
|
+ HashMap<K, V> m = new HashMap<>(); m.put(k1, v1); if (v2 != null) m.put(k2, v2); return m;
|
|
|
+ }
|
|
|
+ private static <K, V> Map<K, V> map(K k1, V v1, K k2, V v2, K k3, V v3) {
|
|
|
+ HashMap<K, V> m = new HashMap<>(); m.put(k1, v1); if (v2 != null) m.put(k2, v2); if (v3 != null) m.put(k3, v3); return m;
|
|
|
}
|
|
|
|
|
|
- private SseEmitter.SseEventBuilder event(String name, Object data) {
|
|
|
- return SseEmitter.event().name(name).data(JSONUtil.toJsonStr(data));
|
|
|
+ private void sendSSE(SseEmitter emitter, String name, Object data) throws IOException {
|
|
|
+ emitter.send(SseEmitter.event().name(name).data(JSONUtil.toJsonStr(data)));
|
|
|
}
|
|
|
}
|