diff --git a/src/main/java/com/company/agentfoundation/agent/AgentCapabilityResponse.java b/src/main/java/com/company/agentfoundation/agent/AgentCapabilityResponse.java new file mode 100644 index 0000000..1d10beb --- /dev/null +++ b/src/main/java/com/company/agentfoundation/agent/AgentCapabilityResponse.java @@ -0,0 +1,29 @@ +package com.company.agentfoundation.agent; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +public record AgentCapabilityResponse( + String intentCode, + String title, + String mcpGroup, + String status, + String message, + List dataSources, + Map data, + Instant timestamp +) { + public static AgentCapabilityResponse placeholder(AgentIntent intent, String message, List dataSources) { + return new AgentCapabilityResponse( + intent.code(), + intent.title(), + intent.mcpGroup(), + "building", + message, + dataSources, + Map.of(), + Instant.now() + ); + } +} diff --git a/src/main/java/com/company/agentfoundation/agent/AgentIntent.java b/src/main/java/com/company/agentfoundation/agent/AgentIntent.java new file mode 100644 index 0000000..c0ccf12 --- /dev/null +++ b/src/main/java/com/company/agentfoundation/agent/AgentIntent.java @@ -0,0 +1,46 @@ +package com.company.agentfoundation.agent; + +public enum AgentIntent { + DATA_SYNC_STATUS("data_sync_status", "integration", "\u4e09\u7cfb\u7edf\u6570\u636e\u540c\u6b65\u72b6\u6001"), + CUSTOMER_DASHBOARD_DAILY("customer_dashboard_daily", "customer", "\u5ba2\u8d44\u7ba1\u7406\u65e5\u62a5"), + STUDENT_ATTENDANCE_DAILY("student_attendance_daily", "education", "\u5b66\u5458\u6559\u5b66\u51fa\u52e4\u65e5\u62a5"), + WECOM_OPERATION_DAILY("wecom_operation_daily", "wecom", "\u4f01\u4e1a\u5fae\u4fe1\u5ba2\u6237\u8fd0\u8425\u65e5\u62a5"), + EC_CALL_COMPLIANCE_DAILY("ec_call_compliance_daily", "ec", "EC \u7535\u8bdd\u5408\u89c4\u76d1\u63a7\u65e5\u62a5"), + PERIOD_SUMMARY_REPORT("period_summary_report", "management", "\u5468\u671f\u5316\u7ecf\u8425\u6c47\u603b\u62a5\u8868"), + CONTENT_GENERATION("content_generation", "content", "\u7ecf\u8425\u5185\u5bb9\u667a\u80fd\u751f\u6210"), + GENERAL_CHAT("general_chat", "general", "\u901a\u7528\u5bf9\u8bdd"); + + private final String code; + private final String mcpGroup; + private final String title; + + AgentIntent(String code, String mcpGroup, String title) { + this.code = code; + this.mcpGroup = mcpGroup; + this.title = title; + } + + public String code() { + return code; + } + + public String mcpGroup() { + return mcpGroup; + } + + public String title() { + return title; + } + + public static AgentIntent fromCode(String code) { + if (code == null || code.isBlank()) { + return GENERAL_CHAT; + } + for (AgentIntent intent : values()) { + if (intent.code.equalsIgnoreCase(code.trim())) { + return intent; + } + } + return GENERAL_CHAT; + } +} diff --git a/src/main/java/com/company/agentfoundation/agent/AgentOrchestratorService.java b/src/main/java/com/company/agentfoundation/agent/AgentOrchestratorService.java new file mode 100644 index 0000000..1558e64 --- /dev/null +++ b/src/main/java/com/company/agentfoundation/agent/AgentOrchestratorService.java @@ -0,0 +1,68 @@ +package com.company.agentfoundation.agent; + +import com.company.agentfoundation.domain.ChatRequest; +import com.company.agentfoundation.domain.ChatResponse; +import com.company.agentfoundation.security.AgentContext; +import com.company.agentfoundation.service.ChatService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class AgentOrchestratorService { + private static final Logger log = LoggerFactory.getLogger(AgentOrchestratorService.class); + + private final IntentRecognitionService intentRecognitionService; + private final OperationReportService operationReportService; + private final ChatService chatService; + + public AgentOrchestratorService( + IntentRecognitionService intentRecognitionService, + OperationReportService operationReportService, + ChatService chatService + ) { + this.intentRecognitionService = intentRecognitionService; + this.operationReportService = operationReportService; + this.chatService = chatService; + } + + public String handleMessage(String agentId, String fromUser, String userText) { + AgentIntent intent = intentRecognitionService.recognize(agentId, userText); + log.info("agent orchestrator route fromUser={} intent={} group={}", fromUser, intent.code(), intent.mcpGroup()); + if (intent != AgentIntent.GENERAL_CHAT) { + return toWecomText(operationReportService.handle(intent)); + } + + ChatRequest request = new ChatRequest( + null, + List.of( + new ChatRequest.Message("system", "You are an enterprise CRM assistant. Reply concisely in Chinese."), + new ChatRequest.Message("user", userText) + ), + 0.3 + ); + ChatResponse response = chatService.chat(new AgentContext(agentId), request); + return response.content(); + } + + private String toWecomText(AgentCapabilityResponse response) { + return """ + \u3010%s\u3011 + %s + + \u80fd\u529b\u5206\u7ec4\uff1a%s + \u80fd\u529b\u7f16\u7801\uff1a%s + \u6570\u636e\u6e90\uff1a%s + \u72b6\u6001\uff1a%s + """.formatted( + response.title(), + response.message(), + response.mcpGroup(), + response.intentCode(), + String.join("\u3001", response.dataSources()), + response.status() + ).trim(); + } +} diff --git a/src/main/java/com/company/agentfoundation/agent/IntentRecognitionService.java b/src/main/java/com/company/agentfoundation/agent/IntentRecognitionService.java new file mode 100644 index 0000000..ec64dfb --- /dev/null +++ b/src/main/java/com/company/agentfoundation/agent/IntentRecognitionService.java @@ -0,0 +1,110 @@ +package com.company.agentfoundation.agent; + +import com.company.agentfoundation.domain.ChatRequest; +import com.company.agentfoundation.domain.ChatResponse; +import com.company.agentfoundation.security.AgentContext; +import com.company.agentfoundation.service.ChatService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Locale; + +@Service +public class IntentRecognitionService { + private static final Logger log = LoggerFactory.getLogger(IntentRecognitionService.class); + + private final ChatService chatService; + + public IntentRecognitionService(ChatService chatService) { + this.chatService = chatService; + } + + public AgentIntent recognize(String agentId, String userText) { + AgentIntent keywordIntent = fallback(userText); + if (keywordIntent != AgentIntent.GENERAL_CHAT) { + log.info("agent intent recognized by keywords intent={} textPreview={}", keywordIntent.code(), preview(userText)); + return keywordIntent; + } + + try { + ChatRequest request = new ChatRequest( + null, + List.of( + new ChatRequest.Message("system", """ + You classify user intent for a sports venue CRM agent. + Return exactly one code: + data_sync_status + customer_dashboard_daily + student_attendance_daily + wecom_operation_daily + ec_call_compliance_daily + period_summary_report + content_generation + general_chat + Do not explain. + """), + new ChatRequest.Message("user", userText) + ), + 0.0 + ); + ChatResponse response = chatService.chat(new AgentContext(agentId), request); + AgentIntent intent = AgentIntent.fromCode(clean(response.content())); + log.info("agent intent recognized by llm intent={} textPreview={}", intent.code(), preview(userText)); + return intent; + } catch (Exception ex) { + log.warn("agent intent llm recognition failed fallback=general_chat err={}", ex.getMessage()); + return AgentIntent.GENERAL_CHAT; + } + } + + private AgentIntent fallback(String userText) { + String text = userText == null ? "" : userText.toLowerCase(Locale.ROOT); + if (containsAny(text, "\u5ba2\u8d44", "\u5ba2\u6237\u8d44\u6599", "\u7ebf\u7d22", "\u65b0\u589e\u5ba2\u6237", "\u4eca\u65e5\u5ba2\u8d44")) { + return AgentIntent.CUSTOMER_DASHBOARD_DAILY; + } + if (containsAny(text, "\u51fa\u52e4", "\u7b7e\u5230", "\u6392\u8bfe", "\u8bfe\u540e\u8bc4\u4ef7", "\u6559\u5b66\u65e5\u62a5", "\u5b66\u5458")) { + return AgentIntent.STUDENT_ATTENDANCE_DAILY; + } + if (containsAny(text, "\u4f01\u4e1a\u5fae\u4fe1", "\u4f01\u5fae", "\u5ba2\u6237\u8fd0\u8425", "\u7fa4\u53d1", "\u89e6\u8fbe")) { + return AgentIntent.WECOM_OPERATION_DAILY; + } + if (containsAny(text, "ec", "\u7535\u8bdd", "\u5916\u547c", "\u63a5\u901a\u7387", "\u5f55\u97f3", "\u7535\u9500")) { + return AgentIntent.EC_CALL_COMPLIANCE_DAILY; + } + if (containsAny(text, "\u5468\u62a5", "\u6708\u62a5", "\u5468\u671f", "\u590d\u76d8", "\u7ecf\u8425\u5206\u6790")) { + return AgentIntent.PERIOD_SUMMARY_REPORT; + } + if (containsAny(text, "\u8bdd\u672f", "sop", "\u6587\u6848", "\u6d3b\u52a8\u65b9\u6848", "\u5ba3\u4f20", "\u5236\u5ea6", "\u8003\u6838")) { + return AgentIntent.CONTENT_GENERATION; + } + if (containsAny(text, "\u540c\u6b65", "\u63a5\u53e3", "\u4e09\u7cfb\u7edf", "\u6570\u636e\u6253\u901a")) { + return AgentIntent.DATA_SYNC_STATUS; + } + return AgentIntent.GENERAL_CHAT; + } + + private boolean containsAny(String text, String... keywords) { + for (String keyword : keywords) { + if (text.contains(keyword.toLowerCase(Locale.ROOT))) { + return true; + } + } + return false; + } + + private String clean(String content) { + if (content == null) { + return ""; + } + return content.trim().replace("`", "").replace("\"", ""); + } + + private String preview(String text) { + if (text == null || text.isBlank()) { + return ""; + } + return text.length() <= 80 ? text : text.substring(0, 80) + "..."; + } +} diff --git a/src/main/java/com/company/agentfoundation/agent/OperationReportController.java b/src/main/java/com/company/agentfoundation/agent/OperationReportController.java new file mode 100644 index 0000000..7993b7e --- /dev/null +++ b/src/main/java/com/company/agentfoundation/agent/OperationReportController.java @@ -0,0 +1,62 @@ +package com.company.agentfoundation.agent; + +import com.company.agentfoundation.security.AgentAuthService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v1/agent/capabilities") +public class OperationReportController { + private final AgentAuthService authService; + private final OperationReportService operationReportService; + + public OperationReportController(AgentAuthService authService, OperationReportService operationReportService) { + this.authService = authService; + this.operationReportService = operationReportService; + } + + @GetMapping("/data-sync/status") + public ResponseEntity dataSyncStatus(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) { + authService.authenticate(agentId, internalKey); + return ResponseEntity.ok(operationReportService.dataSyncStatus()); + } + + @GetMapping("/customer/dashboard/daily") + public ResponseEntity customerDashboardDaily(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) { + authService.authenticate(agentId, internalKey); + return ResponseEntity.ok(operationReportService.customerDashboardDaily()); + } + + @GetMapping("/student/attendance/daily") + public ResponseEntity studentAttendanceDaily(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) { + authService.authenticate(agentId, internalKey); + return ResponseEntity.ok(operationReportService.studentAttendanceDaily()); + } + + @GetMapping("/wecom/operation/daily") + public ResponseEntity wecomOperationDaily(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) { + authService.authenticate(agentId, internalKey); + return ResponseEntity.ok(operationReportService.wecomOperationDaily()); + } + + @GetMapping("/ec/call-compliance/daily") + public ResponseEntity ecCallComplianceDaily(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) { + authService.authenticate(agentId, internalKey); + return ResponseEntity.ok(operationReportService.ecCallComplianceDaily()); + } + + @GetMapping("/reports/period-summary") + public ResponseEntity periodSummaryReport(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) { + authService.authenticate(agentId, internalKey); + return ResponseEntity.ok(operationReportService.periodSummaryReport()); + } + + @GetMapping("/content/generation") + public ResponseEntity contentGeneration(@RequestHeader("X-Agent-Id") String agentId, @RequestHeader("X-Internal-Key") String internalKey) { + authService.authenticate(agentId, internalKey); + return ResponseEntity.ok(operationReportService.contentGeneration()); + } +} diff --git a/src/main/java/com/company/agentfoundation/agent/OperationReportService.java b/src/main/java/com/company/agentfoundation/agent/OperationReportService.java new file mode 100644 index 0000000..e73b7e4 --- /dev/null +++ b/src/main/java/com/company/agentfoundation/agent/OperationReportService.java @@ -0,0 +1,77 @@ +package com.company.agentfoundation.agent; + +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class OperationReportService { + public AgentCapabilityResponse handle(AgentIntent intent) { + return switch (intent) { + case DATA_SYNC_STATUS -> dataSyncStatus(); + case CUSTOMER_DASHBOARD_DAILY -> customerDashboardDaily(); + case STUDENT_ATTENDANCE_DAILY -> studentAttendanceDaily(); + case WECOM_OPERATION_DAILY -> wecomOperationDaily(); + case EC_CALL_COMPLIANCE_DAILY -> ecCallComplianceDaily(); + case PERIOD_SUMMARY_REPORT -> periodSummaryReport(); + case CONTENT_GENERATION -> contentGeneration(); + case GENERAL_CHAT -> AgentCapabilityResponse.placeholder(intent, "\u901a\u7528\u5bf9\u8bdd\u80fd\u529b\u6b63\u5728\u5b8c\u5584\u4e2d\u3002", List.of("DeepSeek")); + }; + } + + public AgentCapabilityResponse dataSyncStatus() { + return AgentCapabilityResponse.placeholder( + AgentIntent.DATA_SYNC_STATUS, + "\u4e09\u7cfb\u7edf\u63a5\u53e3\u6253\u901a\u4e0e\u6570\u636e\u540c\u6b65\u80fd\u529b\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u63a5\u5165\u5b66\u90a6\u6559\u52a1\u7cfb\u7edf\u3001\u4f01\u4e1a\u5fae\u4fe1\u3001EC \u7535\u8bdd\u76d1\u63a7\u5e73\u53f0\u3002", + List.of("\u5b66\u90a6\u6559\u52a1\u7cfb\u7edf", "\u4f01\u4e1a\u5fae\u4fe1", "EC \u7535\u8bdd\u76d1\u63a7\u5e73\u53f0") + ); + } + + public AgentCapabilityResponse customerDashboardDaily() { + return AgentCapabilityResponse.placeholder( + AgentIntent.CUSTOMER_DASHBOARD_DAILY, + "\u5ba2\u8d44\u5927\u76d8\u6570\u636e\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u8986\u76d6\u5ba2\u8d44\u65b0\u589e\u3001\u5206\u914d\u3001\u8ddf\u8fdb\u3001\u8f6c\u5316\u3001\u6d41\u5931\u4e0e\u5f02\u5e38\u98ce\u9669\u9884\u8b66\u3002", + List.of("\u5b66\u90a6\u6559\u52a1\u7cfb\u7edf", "\u4f01\u4e1a\u5fae\u4fe1", "CRM \u6570\u636e\u5e93") + ); + } + + public AgentCapabilityResponse studentAttendanceDaily() { + return AgentCapabilityResponse.placeholder( + AgentIntent.STUDENT_ATTENDANCE_DAILY, + "\u5b66\u5458\u6559\u5b66\u51fa\u52e4\u65e5\u62a5\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u6c47\u603b\u6392\u8bfe\u3001\u7b7e\u5230\u51fa\u52e4\u3001\u8bfe\u540e\u8bc4\u4ef7\u548c\u8bfe\u7a0b\u6210\u679c\u7d20\u6750\u3002", + List.of("\u5b66\u90a6\u6559\u52a1\u7cfb\u7edf") + ); + } + + public AgentCapabilityResponse wecomOperationDaily() { + return AgentCapabilityResponse.placeholder( + AgentIntent.WECOM_OPERATION_DAILY, + "\u4f01\u4e1a\u5fae\u4fe1\u5ba2\u6237\u8fd0\u8425\u65e5\u62a5\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u7edf\u8ba1\u5ba2\u6237\u65b0\u589e\u3001\u6c9f\u901a\u6d3b\u8dc3\u5ea6\u3001\u89e6\u8fbe\u6548\u679c\u4e0e\u5ba2\u6237\u6d41\u5931\u3002", + List.of("\u4f01\u4e1a\u5fae\u4fe1") + ); + } + + public AgentCapabilityResponse ecCallComplianceDaily() { + return AgentCapabilityResponse.placeholder( + AgentIntent.EC_CALL_COMPLIANCE_DAILY, + "EC \u7535\u8bdd\u5408\u89c4\u76d1\u63a7\u65e5\u62a5\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u7edf\u8ba1\u5916\u547c\u603b\u91cf\u3001\u63a5\u901a\u7387\u3001\u6709\u6548\u901a\u8bdd\u65f6\u957f\u3001\u5f55\u97f3\u5408\u89c4\u4e0e\u8d26\u53f7\u98ce\u63a7\u3002", + List.of("EC \u7535\u8bdd\u76d1\u63a7\u5e73\u53f0") + ); + } + + public AgentCapabilityResponse periodSummaryReport() { + return AgentCapabilityResponse.placeholder( + AgentIntent.PERIOD_SUMMARY_REPORT, + "\u5468\u671f\u5316\u7ecf\u8425\u5468\u62a5\u3001\u6708\u62a5\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u6574\u5408\u4e09\u5e73\u53f0\u6bcf\u65e5\u6570\u636e\u7528\u4e8e\u7ba1\u7406\u590d\u76d8\u548c\u7ecf\u8425\u5206\u6790\u3002", + List.of("\u5b66\u90a6\u6559\u52a1\u7cfb\u7edf", "\u4f01\u4e1a\u5fae\u4fe1", "EC \u7535\u8bdd\u76d1\u63a7\u5e73\u53f0") + ); + } + + public AgentCapabilityResponse contentGeneration() { + return AgentCapabilityResponse.placeholder( + AgentIntent.CONTENT_GENERATION, + "\u7ecf\u8425\u5185\u5bb9\u667a\u80fd\u751f\u6210\u80fd\u529b\u6b63\u5728\u5b8c\u5584\u4e2d\uff0c\u540e\u7eed\u5c06\u652f\u6301\u5ba2\u670d\u8bdd\u672f\u3001\u670d\u52a1 SOP\u3001\u8425\u9500\u6d3b\u52a8\u65b9\u6848\u3001\u83b7\u5ba2\u6587\u6848\u548c\u7ba1\u7406\u5236\u5ea6\u751f\u6210\u3002", + List.of("DeepSeek", "\u8fd0\u8425\u77e5\u8bc6\u5e93") + ); + } +} diff --git a/src/main/java/com/company/agentfoundation/integration/wecom/WecomCallbackController.java b/src/main/java/com/company/agentfoundation/integration/wecom/WecomCallbackController.java index 4ad246b..bbe12e6 100644 --- a/src/main/java/com/company/agentfoundation/integration/wecom/WecomCallbackController.java +++ b/src/main/java/com/company/agentfoundation/integration/wecom/WecomCallbackController.java @@ -1,10 +1,7 @@ package com.company.agentfoundation.integration.wecom; +import com.company.agentfoundation.agent.AgentOrchestratorService; import com.company.agentfoundation.config.FoundationProperties; -import com.company.agentfoundation.domain.ChatRequest; -import com.company.agentfoundation.domain.ChatResponse; -import com.company.agentfoundation.security.AgentContext; -import com.company.agentfoundation.service.ChatService; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,28 +13,26 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - @RestController @RequestMapping("/v1/integrations/wecom") public class WecomCallbackController { private static final Logger log = LoggerFactory.getLogger(WecomCallbackController.class); private final FoundationProperties properties; - private final ChatService chatService; private final WecomClient wecomClient; private final WecomCryptoService wecomCryptoService; + private final AgentOrchestratorService agentOrchestratorService; private final XmlMapper xmlMapper = new XmlMapper(); public WecomCallbackController( FoundationProperties properties, - ChatService chatService, WecomClient wecomClient, - WecomCryptoService wecomCryptoService + WecomCryptoService wecomCryptoService, + AgentOrchestratorService agentOrchestratorService ) { this.properties = properties; - this.chatService = chatService; this.wecomClient = wecomClient; this.wecomCryptoService = wecomCryptoService; + this.agentOrchestratorService = agentOrchestratorService; } @GetMapping("/callback") @@ -84,21 +79,12 @@ public class WecomCallbackController { return "success"; } - ChatRequest req = new ChatRequest( - null, - List.of( - new ChatRequest.Message("system", "You are an enterprise CRM assistant. Provide concise, actionable follow-up advice."), - new ChatRequest.Message("user", userText) - ), - 0.3 - ); - - log.info("wecom callback invoking crm agent agentId={} fromUser={}", properties.getWecom().getCrmAgentId(), msg.getFromUserName()); - ChatResponse chat = chatService.chat(new AgentContext(properties.getWecom().getCrmAgentId()), req); - String reply = (chat.content() == null || chat.content().isBlank()) + log.info("wecom callback invoking agent orchestrator agentId={} fromUser={}", properties.getWecom().getCrmAgentId(), msg.getFromUserName()); + String reply = agentOrchestratorService.handleMessage(properties.getWecom().getCrmAgentId(), msg.getFromUserName(), userText); + reply = (reply == null || reply.isBlank()) ? "Request received, but I could not generate advice right now." - : chat.content(); - log.info("wecom callback agent response traceId={} replyPreview={}", chat.traceId(), preview(reply)); + : reply; + log.info("wecom callback agent response replyPreview={}", preview(reply)); wecomClient.sendText(msg.getFromUserName(), reply); log.info("wecom callback reply sent toUser={}", msg.getFromUserName()); return "success"; @@ -117,7 +103,6 @@ public class WecomCallbackController { } String plainXml = wecomCryptoService.decrypt(envelope.getEncrypt()); log.info("wecom callback decrypt success plainXmlLength={}", plainXml.length()); - log.info("wecom callback decrypted xml={}", plainXml); return xmlMapper.readValue(plainXml, WecomIncomingMessage.class); }