diff --git a/.gitignore b/.gitignore index 3101a90..01a7dc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,38 +1,72 @@ -HELP.md -target/ -!.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ +``` +# Compiled and build artifacts +*.class +*.o +*.obj +*.exe +*.dll +*.so +*.a +*.out -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache +# Dependencies +target/ +.m2/ +.gradle/ +node_modules/ +venv/ +.venv/ +__pycache__/ +.mypy_cache/ +.pytest_cache/ -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr +# Logs and temp files +*.log +*.tmp +*.swp +*.swo -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ -build/ -!**/src/main/**/build/ -!**/src/test/**/build/ +# Environment +.env +.env.local +*.env.* -### VS Code ### +# Editors .vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +Thumbs.db + +# Coverage reports +coverage/ +htmlcov/ +.coverage -*.mvn -*.sample -mvnw -*.cmd \ No newline at end of file +# Package files +*.zip +*.gz +*.tar +*.tgz +*.bz2 +*.xz +*.7z +*.rar +*.zst +*.lz4 +*.lzh +*.cab +*.arj +*.rpm +*.deb +*.Z +*.lz +*.lzo +*.tar.gz +*.tar.bz2 +*.tar.xz +*.tar.zst +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 1fe14f2..e2ff787 100644 --- a/pom.xml +++ b/pom.xml @@ -50,13 +50,36 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.ai + spring-ai-openai-spring-boot-starter + org.springframework.ai - spring-ai-dashscope-spring-boot-starter + spring-ai-transformers-spring-boot-starter + org.springframework.ai - spring-ai-deepseek-spring-boot-starter + spring-ai-core + + + + org.springframework.boot + spring-boot-starter-websocket + + + org.springframework + spring-messaging org.projectlombok diff --git a/src/main/java/com/ai/agent/SmartAgentApplication.java b/src/main/java/com/ai/agent/SmartAgentApplication.java new file mode 100644 index 0000000..902578c --- /dev/null +++ b/src/main/java/com/ai/agent/SmartAgentApplication.java @@ -0,0 +1,13 @@ +package com.ai.agent; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SmartAgentApplication { + + public static void main(String[] args) { + SpringApplication.run(SmartAgentApplication.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/com/ai/agent/controller/McpBaiduMapsController.java b/src/main/java/com/ai/agent/controller/McpBaiduMapsController.java new file mode 100644 index 0000000..5467332 --- /dev/null +++ b/src/main/java/com/ai/agent/controller/McpBaiduMapsController.java @@ -0,0 +1,109 @@ +package com.ai.agent.controller; + +import com.ai.agent.mcp.McpServer; +import com.ai.agent.mcp.model.McpRequest; +import com.ai.agent.mcp.model.McpResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/baidu-maps") +public class McpBaiduMapsController { + + @Autowired + private McpServer sseMcpServer; + + // Initialize MCP handlers for Baidu Maps service + @javax.annotation.PostConstruct + public void initBaiduMapsMcpHandlers() { + // Handler for geocoding requests + sseMcpServer.registerHandler("baidu/maps/geocode", request -> { + Map params = (Map) request.getParams(); + String address = (String) params.get("address"); + return geocodeAddress(address); + }); + + // Handler for reverse geocoding requests + sseMcpServer.registerHandler("baidu/maps/reverse-geocode", request -> { + Map params = (Map) request.getParams(); + Double lat = ((Number) params.get("latitude")).doubleValue(); + Double lng = ((Number) params.get("longitude")).doubleValue(); + return reverseGeocode(lat, lng); + }); + + // Handler for place search + sseMcpServer.registerHandler("baidu/maps/search-place", request -> { + Map params = (Map) request.getParams(); + String query = (String) params.get("query"); + String city = (String) params.get("city"); + return searchPlace(query, city); + }); + } + + @PostMapping("/geocode") + public Mono> geocode(@RequestBody Map request) { + String address = request.get("address"); + return Mono.just(geocodeAddress(address)); + } + + @PostMapping("/reverse-geocode") + public Mono> reverseGeocode(@RequestBody Map request) { + Double lat = Double.valueOf(request.get("latitude").toString()); + Double lng = Double.valueOf(request.get("longitude").toString()); + return Mono.just(reverseGeocode(lat, lng)); + } + + @PostMapping("/search") + public Mono> searchPlace(@RequestBody Map request) { + String query = request.get("query"); + String city = request.get("city"); + return Mono.just(searchPlace(query, city)); + } + + // Mock implementations of Baidu Maps API calls + private Map geocodeAddress(String address) { + Map result = new HashMap<>(); + result.put("status", "success"); + result.put("address", address); + result.put("location", Map.of( + "lat", 39.9042, + "lng", 116.4074 + )); + result.put("formatted_address", "北京市东城区天安门广场"); + return result; + } + + private Map reverseGeocode(Double lat, Double lng) { + Map result = new HashMap<>(); + result.put("status", "success"); + result.put("latitude", lat); + result.put("longitude", lng); + result.put("address", "北京市东城区天安门广场"); + result.put("formatted_address", "北京市东城区天安门广场"); + return result; + } + + private Map searchPlace(String query, String city) { + Map result = new HashMap<>(); + result.put("status", "success"); + result.put("query", query); + result.put("city", city); + result.put("places", new Object[] { + Map.of( + "name", "天安门广场", + "address", "北京市东城区天安门广场", + "location", Map.of("lat", 39.9042, "lng", 116.4074) + ), + Map.of( + "name", "故宫博物院", + "address", "北京市东城区景山前街4号", + "location", Map.of("lat", 39.9163, "lng", 116.3972) + ) + }); + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/agent/controller/SmartAgentController.java b/src/main/java/com/ai/agent/controller/SmartAgentController.java new file mode 100644 index 0000000..7b20b7f --- /dev/null +++ b/src/main/java/com/ai/agent/controller/SmartAgentController.java @@ -0,0 +1,96 @@ +package com.ai.agent.controller; + +import com.ai.agent.mcp.McpClient; +import com.ai.agent.mcp.model.McpRequest; +import com.ai.agent.service.Qwen3Service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import reactor.core.publisher.Flux; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/agent") +public class SmartAgentController { + + @Autowired + private Qwen3Service qwen3Service; + + @Autowired + private McpClient sseMcpClient; + + @PostMapping("/chat") + public Map chat(@RequestBody Map request) { + String message = request.get("message"); + String response = qwen3Service.generateText(message); + + Map result = new HashMap<>(); + result.put("input", message); + result.put("output", response); + result.put("model", "Qwen3-32B"); + + return result; + } + + @GetMapping("/stream-chat") + public SseEmitter streamChat(@RequestParam String message) { + SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); + + try { + Flux stream = qwen3Service.streamChat(message); + + stream.subscribe( + response -> { + try { + emitter.send(SseEmitter.event() + .name("chunk") + .data(response.getResult().getOutput().getContent())); + } catch (IOException e) { + emitter.completeWithError(e); + } + }, + error -> emitter.completeWithError(error), + () -> { + try { + emitter.send(SseEmitter.event().name("complete").data("Stream completed")); + emitter.complete(); + } catch (IOException e) { + emitter.completeWithError(e); + } + } + ); + } catch (Exception e) { + emitter.completeWithError(e); + } + + return emitter; + } + + @PostMapping("/mcp-request") + public Map mcpRequest(@RequestBody Map request) { + String method = (String) request.get("method"); + Object params = request.get("params"); + + McpRequest mcpRequest = new McpRequest(method, "req-" + System.currentTimeMillis(), params); + com.ai.agent.mcp.model.McpResponse response = sseMcpClient.sendRequest(mcpRequest); + + Map result = new HashMap<>(); + result.put("request", mcpRequest); + result.put("response", response); + + return result; + } + + @GetMapping("/health") + public Map health() { + Map health = new HashMap<>(); + health.put("status", "healthy"); + health.put("model", "Qwen3-32B"); + health.put("framework", "vLLM/MindIE"); + health.put("mcp", "SSE/STDIO"); + return health; + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/agent/mcp/McpClient.java b/src/main/java/com/ai/agent/mcp/McpClient.java new file mode 100644 index 0000000..c1c89ee --- /dev/null +++ b/src/main/java/com/ai/agent/mcp/McpClient.java @@ -0,0 +1,12 @@ +package com.ai.agent.mcp; + +import com.ai.agent.mcp.model.McpRequest; +import com.ai.agent.mcp.model.McpResponse; +import reactor.core.publisher.Flux; + +public interface McpClient { + McpResponse sendRequest(McpRequest request); + Flux sendStreamRequest(McpRequest request); + void connect(); + void disconnect(); +} \ No newline at end of file diff --git a/src/main/java/com/ai/agent/mcp/McpServer.java b/src/main/java/com/ai/agent/mcp/McpServer.java new file mode 100644 index 0000000..a81563c --- /dev/null +++ b/src/main/java/com/ai/agent/mcp/McpServer.java @@ -0,0 +1,16 @@ +package com.ai.agent.mcp; + +import com.ai.agent.mcp.model.McpRequest; +import com.ai.agent.mcp.model.McpResponse; + +public interface McpServer { + void start(); + void stop(); + McpResponse handleRequest(McpRequest request); + void registerHandler(String method, McpRequestHandler handler); + + @FunctionalInterface + interface McpRequestHandler { + Object handle(McpRequest request); + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/agent/mcp/impl/SseMcpServer.java b/src/main/java/com/ai/agent/mcp/impl/SseMcpServer.java new file mode 100644 index 0000000..8b3104a --- /dev/null +++ b/src/main/java/com/ai/agent/mcp/impl/SseMcpServer.java @@ -0,0 +1,71 @@ +package com.ai.agent.mcp.impl; + +import com.ai.agent.mcp.McpServer; +import com.ai.agent.mcp.model.McpRequest; +import com.ai.agent.mcp.model.McpResponse; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.*; +import org.springframework.http.codec.ServerSentEvent; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; + +@Service +@RestController +@RequestMapping("/mcp") +public class SseMcpServer implements McpServer { + private Map handlers = new HashMap<>(); + private volatile boolean running = false; + + @Override + public void start() { + running = true; + System.out.println("SSE MCP Server started"); + } + + @Override + public void stop() { + running = false; + System.out.println("SSE MCP Server stopped"); + } + + @Override + public McpResponse handleRequest(McpRequest request) { + McpRequestHandler handler = handlers.get(request.getMethod()); + if (handler != null) { + Object result = handler.handle(request); + return new McpResponse(request.getId(), result); + } else { + return new McpResponse(request.getId(), "Method not found: " + request.getMethod()); + } + } + + @Override + public void registerHandler(String method, McpRequestHandler handler) { + handlers.put(method, handler); + } + + @PostMapping("/request") + public Mono handleRequest(@RequestBody McpRequest request) { + return Mono.fromCallable(() -> handleRequest(request)); + } + + @PostMapping("/stream") + public Flux> handleStreamRequest(@RequestBody McpRequest request) { + return Flux.interval(java.time.Duration.ofMillis(100)) + .take(10) // Limit for demo purposes + .map(i -> { + McpRequestHandler handler = handlers.get(request.getMethod()); + if (handler != null) { + Object result = handler.handle(request); + McpResponse response = new McpResponse(request.getId(), result); + return ServerSentEvent.builder(response).build(); + } else { + McpResponse response = new McpResponse(request.getId(), "Method not found: " + request.getMethod()); + return ServerSentEvent.builder(response).build(); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/agent/mcp/impl/StdioMcpClient.java b/src/main/java/com/ai/agent/mcp/impl/StdioMcpClient.java new file mode 100644 index 0000000..072ed20 --- /dev/null +++ b/src/main/java/com/ai/agent/mcp/impl/StdioMcpClient.java @@ -0,0 +1,104 @@ +package com.ai.agent.mcp.impl; + +import com.ai.agent.mcp.McpClient; +import com.ai.agent.mcp.model.McpRequest; +import com.ai.agent.mcp.model.McpResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; + +import java.io.*; +import java.util.concurrent.CompletableFuture; + +@Component +public class StdioMcpClient implements McpClient { + private Process process; + private BufferedReader reader; + private BufferedWriter writer; + private ObjectMapper objectMapper = new ObjectMapper(); + private boolean connected = false; + + @Override + public McpResponse sendRequest(McpRequest request) { + if (!connected) { + throw new IllegalStateException("MCP client not connected"); + } + + try { + // Serialize request to JSON + String jsonRequest = objectMapper.writeValueAsString(request); + + // Write to STDIO + writer.write(jsonRequest + "\n"); + writer.flush(); + + // Read response + String jsonResponse = reader.readLine(); + if (jsonResponse != null) { + return objectMapper.readValue(jsonResponse, McpResponse.class); + } + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + @Override + public Flux sendStreamRequest(McpRequest request) { + // For STDIO, streaming is implemented through continuous reading + // This is a simplified implementation + return Flux.create(sink -> { + try { + String jsonRequest = objectMapper.writeValueAsString(request); + writer.write(jsonRequest + "\n"); + writer.flush(); + + // Continuously read responses + String line; + while ((line = reader.readLine()) != null && !sink.isCancelled()) { + McpResponse response = objectMapper.readValue(line, McpResponse.class); + sink.next(response); + } + } catch (Exception e) { + sink.error(e); + } + }); + } + + @Override + public void connect() { + try { + // Start an external process that implements MCP via STDIO + ProcessBuilder processBuilder = new ProcessBuilder("echo", "MCP process started"); + process = processBuilder.start(); + + reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream())); + + connected = true; + System.out.println("STDIO MCP Client connected"); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void disconnect() { + try { + if (writer != null) { + writer.close(); + } + if (reader != null) { + reader.close(); + } + if (process != null) { + process.destroy(); + } + connected = false; + System.out.println("STDIO MCP Client disconnected"); + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/agent/mcp/model/McpNotification.java b/src/main/java/com/ai/agent/mcp/model/McpNotification.java new file mode 100644 index 0000000..5e484d2 --- /dev/null +++ b/src/main/java/com/ai/agent/mcp/model/McpNotification.java @@ -0,0 +1,16 @@ +package com.ai.agent.mcp.model; + +import lombok.Data; + +@Data +public class McpNotification { + private String method; + private Object params; + + public McpNotification() {} + + public McpNotification(String method, Object params) { + this.method = method; + this.params = params; + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/agent/mcp/model/McpRequest.java b/src/main/java/com/ai/agent/mcp/model/McpRequest.java new file mode 100644 index 0000000..6617f61 --- /dev/null +++ b/src/main/java/com/ai/agent/mcp/model/McpRequest.java @@ -0,0 +1,18 @@ +package com.ai.agent.mcp.model; + +import lombok.Data; + +@Data +public class McpRequest { + private String method; + private String id; + private Object params; + + public McpRequest() {} + + public McpRequest(String method, String id, Object params) { + this.method = method; + this.id = id; + this.params = params; + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/agent/mcp/model/McpResponse.java b/src/main/java/com/ai/agent/mcp/model/McpResponse.java new file mode 100644 index 0000000..2ee178a --- /dev/null +++ b/src/main/java/com/ai/agent/mcp/model/McpResponse.java @@ -0,0 +1,27 @@ +package com.ai.agent.mcp.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.Map; + +@Data +public class McpResponse { + private String id; + @JsonProperty("jsonrpc") + private String jsonrpc = "2.0"; + private Object result; + private Object error; + + public McpResponse() {} + + public McpResponse(String id, Object result) { + this.id = id; + this.result = result; + } + + public McpResponse(String id, String error) { + this.id = id; + this.error = Map.of("message", error); + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/agent/service/Qwen3Service.java b/src/main/java/com/ai/agent/service/Qwen3Service.java new file mode 100644 index 0000000..c3c9637 --- /dev/null +++ b/src/main/java/com/ai/agent/service/Qwen3Service.java @@ -0,0 +1,76 @@ +package com.ai.agent.service; + +import com.ai.agent.config.QwenConfigProperties; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.ModelRequest; +import org.springframework.ai.model.ModelResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +@Service +public class Qwen3Service { + + private final QwenConfigProperties configProperties; + private ChatModel chatModel; + + @Autowired + public Qwen3Service(QwenConfigProperties configProperties) { + this.configProperties = configProperties; + initializeChatModel(); + } + + private void initializeChatModel() { + // Initialize based on the selected inference framework + if (configProperties.getVllm().isEnabled()) { + // For vLLM, we use OpenAI-compatible API + initializeVllmChatModel(); + } else if (configProperties.getMindie().isEnabled()) { + // For MindIE, we might need a custom implementation + initializeMindieChatModel(); + } + } + + private void initializeVllmChatModel() { + // Using OpenAI-compatible client for vLLM + org.springframework.ai.openai.OpenAiChatModel.Builder builder = + new org.springframework.ai.openai.OpenAiChatModel.Builder() + .withBaseUrl(configProperties.getVllm().getBaseUrl()) + .withApiKey(configProperties.getApiKey()) + .withModel(configProperties.getModel()) + .withTemperature((float) configProperties.getTemperature()) + .withMaxTokens(configProperties.getMaxTokens()); + + this.chatModel = builder.build(); + } + + private void initializeMindieChatModel() { + // For MindIE, we would need to implement a custom client + // This is a placeholder - actual implementation would depend on MindIE API + org.springframework.ai.openai.OpenAiChatModel.Builder builder = + new org.springframework.ai.openai.OpenAiChatModel.Builder() + .withBaseUrl(configProperties.getMindie().getBaseUrl()) + .withApiKey(configProperties.getApiKey()) + .withModel(configProperties.getModel()) + .withTemperature((float) configProperties.getTemperature()) + .withMaxTokens(configProperties.getMaxTokens()); + + this.chatModel = builder.build(); + } + + public ChatResponse chat(String message) { + Prompt prompt = new Prompt(message); + return chatModel.call(prompt); + } + + public Flux streamChat(String message) { + Prompt prompt = new Prompt(message); + return chatModel.stream(prompt); + } + + public String generateText(String input) { + return chatModel.call(new Prompt(input)).getResult().getOutput().getContent(); + } +} \ No newline at end of file