feat: initialize project backend structure and document system specifications across all modules

This commit is contained in:
SonPhung
2026-07-24 09:05:41 +07:00
parent 506008c69f
commit 4534e4ecb8
74 changed files with 7989 additions and 44 deletions

View File

@@ -6,7 +6,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LoyaltyAgentApplication {
public static void main(String[] args) {
static void main(String[] args) {
SpringApplication.run(LoyaltyAgentApplication.class, args);
}
}

View File

@@ -1,25 +1,22 @@
package dev.sonpx.loyalty.agent.config;
import org.flywaydb.core.Flyway;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import javax.sql.DataSource;
@Configuration
public class FlywayConfig implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException {
if (bean instanceof DataSource) {
System.out.println("--- Running Flyway Migration before DataSource is used ---");
Flyway.configure()
.dataSource((DataSource) bean)
.locations("classpath:db/migration")
.load()
.migrate();
}
return bean;
}
}
//package dev.sonpx.loyalty.agent.config;
//
//import org.flywaydb.core.Flyway;
//import org.springframework.beans.BeansException;
//import org.springframework.beans.factory.config.BeanPostProcessor;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.lang.NonNull;
//import javax.sql.DataSource;
//
//@Configuration
//public class FlywayConfig {
//
// @Bean
// public FlywayMigrationStrategy flywayMigrationStrategy() {
// return flyway -> {
// // Ví dụ: Xóa hết schema cũ rồi mới chạy migration (chỉ áp dụng thử nghiệm!)
// // flyway.clean();
// flyway.migrate();
// };
// }
//}

View File

@@ -0,0 +1,20 @@
package dev.sonpx.loyalty.agent.config;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class McpConfig {
@Bean
public static BeanFactoryPostProcessor lazyMcpClientPostProcessor() {
return beanFactory -> {
for (String beanName : beanFactory.getBeanDefinitionNames()) {
if (beanName.toLowerCase().contains("mcp") || beanName.toLowerCase().contains("toolcallbackprovider")) {
beanFactory.getBeanDefinition(beanName).setLazyInit(true);
}
}
};
}
}

View File

@@ -1,11 +1,16 @@
package dev.sonpx.loyalty.agent.config;
import java.security.Principal;
import java.util.Map;
import java.util.UUID;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
@Configuration
@EnableWebSocketMessageBroker
@@ -22,10 +27,10 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.setHandshakeHandler(new org.springframework.web.socket.server.support.DefaultHandshakeHandler() {
.setHandshakeHandler(new DefaultHandshakeHandler() {
@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() {
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
return new Principal() {
private final String id = UUID.randomUUID().toString();
@Override
public String getName() {

View File

@@ -58,8 +58,9 @@ public class SimpleAgentExecutor {
* The LLM decides which tools to call via ReAct pattern.
*/
public void executeWithTools(ChatRequest request, String connectionSessionId, String conversationKey, Intent intent) {
List<ToolCallback> wrappedTools = toolInterceptor.getWrappedTools(
connectionSessionId, request.messageId(), AgentToolScope.READ_ONLY);
try {
List<ToolCallback> wrappedTools = toolInterceptor.getWrappedTools(
connectionSessionId, request.messageId(), AgentToolScope.READ_ONLY);
String systemPrompt = promptBuilder.buildSystemPrompt(intent);
String userPrompt = promptBuilder.buildUserPrompt(request);
@@ -94,10 +95,34 @@ public class SimpleAgentExecutor {
},
error -> {
log.error("Error during SimpleAgent execution for connection {}: {}", connectionSessionId, error.getMessage(), error);
eventPublisher.error(connectionSessionId, request.messageId(),
"Xin lỗi, đã xảy ra lỗi khi xử lý yêu cầu. Vui lòng thử lại.");
String errorMessage = "Xin lỗi, đã xảy ra lỗi khi xử lý yêu cầu. Vui lòng thử lại.";
if (isConnectionError(error)) {
errorMessage = "Hiện tại dịch vụ AI (Ollama) đang tạm thời gián đoạn. Vui lòng thử lại sau.";
}
eventPublisher.error(connectionSessionId, request.messageId(), errorMessage);
}
);
} catch (Exception e) {
log.error("Error setting up SimpleAgent for connection {}: {}", connectionSessionId, e.getMessage(), e);
String errorMessage = "Xin lỗi, không thể khởi tạo tool. Vui lòng thử lại.";
if (isConnectionError(e)) {
errorMessage = "Hiện tại dịch vụ tool (MCP) đang tạm thời gián đoạn. Vui lòng thử lại sau.";
}
eventPublisher.error(connectionSessionId, request.messageId(), errorMessage);
}
}
private boolean isConnectionError(Throwable t) {
if (t == null) return false;
Throwable current = t;
while (current != null) {
String name = current.getClass().getName();
if (name.contains("ConnectException") || name.contains("SocketException") || name.contains("ResourceAccessException")) {
return true;
}
current = current.getCause();
}
return false;
}
/**
@@ -136,8 +161,11 @@ public class SimpleAgentExecutor {
},
error -> {
log.error("Error during DirectChat execution for connection {}: {}", connectionSessionId, error.getMessage(), error);
eventPublisher.error(connectionSessionId, request.messageId(),
"Xin lỗi, đã xảy ra lỗi. Vui lòng thử lại.");
String errorMessage = "Xin lỗi, đã xảy ra lỗi. Vui lòng thử lại.";
if (isConnectionError(error)) {
errorMessage = "Hiện tại dịch vụ AI (Ollama) đang tạm thời gián đoạn. Vui lòng thử lại sau.";
}
eventPublisher.error(connectionSessionId, request.messageId(), errorMessage);
}
);
}

View File

@@ -33,7 +33,7 @@ import java.util.List;
@RequiredArgsConstructor
public class ToolInterceptor {
private final List<ToolCallbackProvider> toolProviders;
private final ObjectProvider<ToolCallbackProvider> toolProviders;
private final AgentEventPublisher eventPublisher;
private final ToolResultPresentationProcessor toolResultProcessor;
private final AgentProperties agentProperties;
@@ -46,7 +46,7 @@ public class ToolInterceptor {
public List<ToolCallback> getWrappedTools(String sessionId, String messageId, AgentToolScope scope) {
List<ToolCallback> wrappedTools = new ArrayList<>();
for (ToolCallbackProvider provider : toolProviders) {
for (ToolCallbackProvider provider : toolProviders.stream().toList()) {
for (ToolCallback tool : provider.getToolCallbacks()) {
String toolName = tool.getToolDefinition().name();
if (scope.allows(toolName)) {
@@ -107,9 +107,10 @@ public class ToolInterceptor {
try {
return tool.call(toolInput);
} catch (Exception e) {
if (isConnectionOrSessionError(e)) {
Throwable rootCause = getRootCause(e);
if (isConnectionOrSessionError(rootCause)) {
log.warn("Detected MCP connection/session drop on tool {}: {}. Attempting auto-reconnect and retry...",
toolName, e.getMessage());
toolName, rootCause.getMessage());
attemptReconnect();
try {
return tool.call(toolInput);
@@ -122,7 +123,7 @@ public class ToolInterceptor {
}
}
private void attemptReconnect() {
private synchronized void attemptReconnect() {
if (mcpSyncClientsProvider != null) {
List<McpSyncClient> clients = mcpSyncClientsProvider.getIfAvailable();
if (clients != null && !clients.isEmpty()) {
@@ -138,6 +139,14 @@ public class ToolInterceptor {
}
}
private Throwable getRootCause(Throwable t) {
if (t == null) return null;
Throwable current = t;
while (current.getCause() != null && current.getCause() != current) {
current = current.getCause();
}
return current;
}
public boolean isConnectionOrSessionError(Throwable t) {
if (t == null) {
return false;
@@ -147,7 +156,9 @@ public class ToolInterceptor {
if (current instanceof EOFException
|| current instanceof ConnectException
|| current instanceof SocketException
|| current instanceof ClosedChannelException) {
|| current instanceof ClosedChannelException
|| current.getClass().getName().contains("ConnectException")
|| current.getClass().getName().contains("SocketException")) {
return true;
}
String msg = current.getMessage();

View File

@@ -40,7 +40,7 @@ spring:
num_ctx: 32768
mcp:
client:
request-timeout: 60000
request-timeout: 3000
enabled: true
type: sync
sse: