refactor: restructure MCP server by replacing client-specific tool logic with a centralized campaign service and standardized models.

This commit is contained in:
2026-07-20 18:34:11 +07:00
parent 2de63d0bad
commit 00e26cedcf
155 changed files with 1039 additions and 2273 deletions

View File

@@ -1,5 +1,6 @@
package dev.sonpx.loyalty.agent.config;
import java.util.UUID;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
@@ -25,7 +26,7 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
protected java.security.Principal determineUser(org.springframework.http.server.ServerHttpRequest request, org.springframework.web.socket.WebSocketHandler wsHandler, java.util.Map<String, Object> attributes) {
return new java.security.Principal() {
private final String id = java.util.UUID.randomUUID().toString();
private final String id = UUID.randomUUID().toString();
@Override
public String getName() {
return id;

View File

@@ -1,12 +1,12 @@
package dev.sonpx.loyalty.agent.controller;
import dev.sonpx.loyalty.agent.service.LoyaltyAgentService;
import org.springframework.messaging.handler.annotation.Header;
import java.security.Principal;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@CrossOrigin(origins = "*")
@@ -24,7 +24,7 @@ public class AgentController {
public record ChatRequest(String conversationId, String messageId, String prompt, String activeCampaignId) {}
@MessageMapping("/chat")
public void chat(@Payload ChatRequest request, java.security.Principal principal) {
public void chat(@Payload ChatRequest request, Principal principal) {
agentService.chat(request, principal.getName());
}

View File

@@ -1,24 +1,27 @@
package dev.sonpx.loyalty.agent.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.stereotype.Service;
import dev.sonpx.loyalty.agent.controller.AgentController;
import dev.sonpx.loyalty.agent.domain.AgentEvent;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LoyaltyAgentService {
private final org.springframework.ai.chat.model.ChatModel chatModel;
private final ChatModel chatModel;
private final List<ToolCallbackProvider> toolProviders;
private final org.springframework.messaging.simp.SimpMessagingTemplate messagingTemplate;
private final SimpMessagingTemplate messagingTemplate;
public LoyaltyAgentService(org.springframework.ai.chat.model.ChatModel chatModel, List<ToolCallbackProvider> toolProviders, org.springframework.messaging.simp.SimpMessagingTemplate messagingTemplate) {
this.chatModel = chatModel;
this.toolProviders = toolProviders;
this.messagingTemplate = messagingTemplate;
}
public void chat(dev.sonpx.loyalty.agent.controller.AgentController.ChatRequest request, String sessionId) {
public void chat(AgentController.ChatRequest request, String sessionId) {
String prompt = request.prompt();
if (request.activeCampaignId() != null && !request.activeCampaignId().isEmpty()) {
prompt = "Context: Active Campaign ID is " + request.activeCampaignId() + ".\n\n" + prompt;
@@ -27,21 +30,21 @@ public class LoyaltyAgentService {
prompt = "System Context: The current conversationId (session ID) is " + request.conversationId() + ". You MUST provide this conversationId as a parameter to any tool that requires it.\n\n" + prompt;
}
List<org.springframework.ai.tool.ToolCallback> wrappedTools = new java.util.ArrayList<>();
List<ToolCallback> wrappedTools = new ArrayList<>();
for (ToolCallbackProvider provider : toolProviders) {
for (org.springframework.ai.tool.ToolCallback tool : provider.getToolCallbacks()) {
wrappedTools.add(new org.springframework.ai.tool.ToolCallback() {
for (ToolCallback tool : provider.getToolCallbacks()) {
wrappedTools.add(new ToolCallback() {
@Override
public org.springframework.ai.tool.definition.ToolDefinition getToolDefinition() {
public ToolDefinition getToolDefinition() {
return tool.getToolDefinition();
}
@Override
public String call(String toolInput) {
messagingTemplate.convertAndSendToUser(sessionId, "/queue/chat-events", dev.sonpx.loyalty.agent.domain.AgentEvent.toolStatus(request.messageId(), tool.getToolDefinition().name(), "running"));
messagingTemplate.convertAndSendToUser(sessionId, "/queue/chat-events", AgentEvent.toolStatus(request.messageId(), tool.getToolDefinition().name(), "running"));
try {
return tool.call(toolInput);
} finally {
messagingTemplate.convertAndSendToUser(sessionId, "/queue/chat-events", dev.sonpx.loyalty.agent.domain.AgentEvent.toolStatus(request.messageId(), tool.getToolDefinition().name(), "done"));
messagingTemplate.convertAndSendToUser(sessionId, "/queue/chat-events", AgentEvent.toolStatus(request.messageId(), tool.getToolDefinition().name(), "done"));
}
}
});
@@ -54,14 +57,8 @@ public class LoyaltyAgentService {
Your primary job is to orchestrate operations for our Loyalty core system.
CRITICAL RULES:
1. DO NOT create a model.client.dev.sonpx.loyalty.agent.Campaign unless you verify that Member Tiers and Point Pools exist first.
2. Use the `listMemberTiers` tool to check for valid tier IDs.
3. Use the `listPointPools` tool to check for valid pool IDs and ensure they have a sufficient balance.
4. Use the `listStaticAttributes` tool when asked about configurations, properties, or static values.
5. Only use the `createCampaign` tool once verification is complete.
6. Always answer the user in the language they used (e.g., Vietnamese).
7. If a tool response contains a `presentation.content` field, you MUST display its content exactly as provided to the user. Do not summarize or alter its formatting. Use the `data` field only for your internal reasoning.
8. When asked for information, details, or status of an existing campaign, ALWAYS use the `listCampaigns` tool. DO NOT use `createCampaign` unless explicitly asked to create or add a new one.
1. Always answer the user in the language they used (e.g., Vietnamese).
2. If a tool response contains a `presentation.markdown` field, you MUST display its content exactly as provided to the user. Do not summarize or alter its formatting. Use the `data` field only for your internal reasoning.
""")
.defaultTools(wrappedTools)
.build();
@@ -70,14 +67,14 @@ public class LoyaltyAgentService {
.user(prompt)
.stream()
.content()
.doOnComplete(() -> messagingTemplate.convertAndSendToUser(sessionId, "/queue/chat-events", dev.sonpx.loyalty.agent.domain.AgentEvent.done(request.messageId())))
.doOnComplete(() -> messagingTemplate.convertAndSendToUser(sessionId, "/queue/chat-events", AgentEvent.done(request.messageId())))
.subscribe(
content -> {
if (content != null && !content.isEmpty()) {
messagingTemplate.convertAndSendToUser(sessionId, "/queue/chat-events", dev.sonpx.loyalty.agent.domain.AgentEvent.token(request.messageId(), content));
messagingTemplate.convertAndSendToUser(sessionId, "/queue/chat-events", AgentEvent.token(request.messageId(), content));
}
},
error -> messagingTemplate.convertAndSendToUser(sessionId, "/queue/chat-events", dev.sonpx.loyalty.agent.domain.AgentEvent.error(request.messageId(), error.getMessage()))
error -> messagingTemplate.convertAndSendToUser(sessionId, "/queue/chat-events", AgentEvent.error(request.messageId(), error.getMessage()))
);
}
}