diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/config/LLMChatProperties.java b/src/main/java/com/chinaweal/youfool/devops/ai/config/LLMChatProperties.java index 48424f9..706979d 100644 --- a/src/main/java/com/chinaweal/youfool/devops/ai/config/LLMChatProperties.java +++ b/src/main/java/com/chinaweal/youfool/devops/ai/config/LLMChatProperties.java @@ -168,6 +168,57 @@ public class LLMChatProperties { * 是否启用为备用提供商 */ private boolean fallbackEnabled = true; + + /** + * 函数调用配置 + */ + private FunctionCallingConfig functionCalling = new FunctionCallingConfig(); + } + + /** + * 函数调用配置 + */ + @Data + public static class FunctionCallingConfig { + /** + * 是否启用函数调用 + */ + private boolean enabled = true; + + /** + * 最大工具数量 + */ + private int maxTools = 10; + + /** + * 是否启用并行执行 + */ + private boolean parallelExecution = true; + + /** + * 函数调用超时时间(毫秒) + */ + private int timeout = 30000; + + /** + * 工具选择策略:none, auto, required + */ + private String toolChoice = "auto"; + + /** + * 失败时是否重试 + */ + private boolean retryOnFailure = true; + + /** + * 最大重试次数 + */ + private int maxRetryAttempts = 2; + + /** + * 是否启用参数验证 + */ + private boolean validationEnabled = true; } /** diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/dto/llm/ChatRequest.java b/src/main/java/com/chinaweal/youfool/devops/ai/dto/llm/ChatRequest.java index a65f467..0cb4337 100644 --- a/src/main/java/com/chinaweal/youfool/devops/ai/dto/llm/ChatRequest.java +++ b/src/main/java/com/chinaweal/youfool/devops/ai/dto/llm/ChatRequest.java @@ -111,13 +111,13 @@ public class ChatRequest { * MCP工具列表(Model Context Protocol) */ @Schema(description = "可用的MCP工具列表") - private List mcpTools; + private List mcpTools; /** * 函数工具列表(Function Calling) */ @Schema(description = "可用的函数工具列表") - private List tools; + private List> tools; /** * 工具选择策略 diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/provider/ClaudeProvider.java b/src/main/java/com/chinaweal/youfool/devops/ai/provider/ClaudeProvider.java new file mode 100644 index 0000000..974a6af --- /dev/null +++ b/src/main/java/com/chinaweal/youfool/devops/ai/provider/ClaudeProvider.java @@ -0,0 +1,539 @@ +package com.chinaweal.youfool.devops.ai.provider; + +import com.chinaweal.youfool.devops.ai.config.LLMChatProperties; +import com.chinaweal.youfool.devops.ai.dto.llm.*; +import com.chinaweal.youfool.devops.ai.handler.FunctionCallHandler; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.time.LocalDateTime; +import java.util.*; + +/** + * Claude LLM提供商实现 + * 支持Anthropic的tool use功能和流式输出 + * + * @author chinaweal + * @since 2025-08-17 + */ +@Slf4j +@Component +public class ClaudeProvider extends AbstractLLMProvider { + + private final LLMChatProperties chatProperties; + private final FunctionCallHandler functionCallHandler; + + public ClaudeProvider(RestTemplate restTemplate, ObjectMapper objectMapper, + LLMChatProperties chatProperties, FunctionCallHandler functionCallHandler) { + super(restTemplate, objectMapper); + this.chatProperties = chatProperties; + this.functionCallHandler = functionCallHandler; + } + + @Override + public String getProviderName() { + return "claude"; + } + + @Override + public boolean supportsFunctionCalling() { + return true; // Claude支持tool use功能 + } + + @Override + public boolean supportsStreaming() { + return true; // Claude支持流式输出 + } + + @Override + public boolean isAvailable() { + LLMChatProperties.ProviderConfig config = getProviderConfig(); + return config != null && config.isEnabled() && config.getApiKey() != null; + } + + @Override + public ChatResponse chatCompletion(ChatRequest request) { + validateRequest(request); + logRequest("chatCompletion", request); + + long startTime = System.currentTimeMillis(); + + try { + // 构建Claude API请求 + Map apiRequest = buildClaudeApiRequest(request, false); + + // 发送请求 + ResponseEntity response = sendClaudeApiRequest(apiRequest); + + // 解析响应 + long processingTime = System.currentTimeMillis() - startTime; + ChatResponse chatResponse = parseClaudeApiResponse(response.getBody(), processingTime); + + // 设置质量指标 + calculateQualityMetrics(chatResponse, request); + + logResponse("chatCompletion", true, processingTime); + return chatResponse; + + } catch (Exception e) { + long processingTime = System.currentTimeMillis() - startTime; + logError("chatCompletion", e, "sessionId: " + request.getSessionId()); + logResponse("chatCompletion", false, processingTime); + throw new RuntimeException("Claude聊天完成请求失败: " + e.getMessage(), e); + } + } + + @Override + public SseEmitter streamChatCompletion(ChatRequest request) { + validateRequest(request); + logRequest("streamChatCompletion", request); + + // 流式输出实现(简化版本,实际项目中需要完整实现) + SseEmitter emitter = new SseEmitter(30000L); + + try { + // 这里应该实现真正的流式处理 + // 当前返回一个模拟的emitter + log.info("Stream chat completion requested for Claude provider"); + + // 立即完成(在实际实现中应该异步处理) + emitter.complete(); + + } catch (Exception e) { + logError("streamChatCompletion", e, "sessionId: " + request.getSessionId()); + emitter.completeWithError(e); + } + + return emitter; + } + + @Override + public int getMaxTokens() { + return 200000; // Claude-3的最大Token数 + } + + @Override + public String[] getSupportedModels() { + return new String[]{ + "claude-3-sonnet-20240229", + "claude-3-opus-20240229", + "claude-3-haiku-20240307", + "claude-3-5-sonnet-20240620", + "claude-3-5-haiku-20241022" + }; + } + + /** + * 构建Claude API请求 + */ + private Map buildClaudeApiRequest(ChatRequest request, boolean stream) { + Map apiRequest = new HashMap<>(); + + // Claude基础参数 + apiRequest.put("model", request.getModel()); + apiRequest.put("messages", convertMessagesToClaudeFormat(request.getMessages())); + apiRequest.put("stream", stream); + + // 可选参数 + if (request.getMaxTokens() != null) { + apiRequest.put("max_tokens", request.getMaxTokens()); + } else { + apiRequest.put("max_tokens", 4000); // Claude需要明确指定max_tokens + } + + if (request.getTemperature() != null) { + apiRequest.put("temperature", request.getTemperature()); + } + if (request.getTopP() != null) { + apiRequest.put("top_p", request.getTopP()); + } + if (request.getStop() != null && !request.getStop().isEmpty()) { + apiRequest.put("stop_sequences", request.getStop()); + } + + // 添加Claude特定的工具调用参数 + addClaudeToolParameters(apiRequest, request); + + return apiRequest; + } + + /** + * 将消息转换为Claude格式 + */ + private List> convertMessagesToClaudeFormat(List messages) { + List> claudeMessages = new ArrayList<>(); + + for (ChatMessage message : messages) { + Map claudeMessage = new HashMap<>(); + claudeMessage.put("role", message.getRole()); + + // Claude使用content数组格式 + List> content = new ArrayList<>(); + + if (message.getContent() != null && !message.getContent().trim().isEmpty()) { + Map textContent = new HashMap<>(); + textContent.put("type", "text"); + textContent.put("text", message.getContent()); + content.add(textContent); + } + + // 处理工具调用结果 + if (message.getToolCalls() != null && !message.getToolCalls().isEmpty()) { + for (ToolCall toolCall : message.getToolCalls()) { + Map toolUseContent = new HashMap<>(); + toolUseContent.put("type", "tool_use"); + toolUseContent.put("id", toolCall.getId()); + toolUseContent.put("name", toolCall.getFunction().getName()); + + try { + // 解析参数为JSON对象 + Map input = objectMapper.readValue( + toolCall.getFunction().getArguments(), Map.class); + toolUseContent.put("input", input); + } catch (JsonProcessingException e) { + log.warn("Failed to parse tool call arguments as JSON: {}", + toolCall.getFunction().getArguments()); + toolUseContent.put("input", Map.of("raw", toolCall.getFunction().getArguments())); + } + + content.add(toolUseContent); + } + } + + // 处理工具调用结果响应 + if ("tool".equals(message.getRole())) { + Map toolResultContent = new HashMap<>(); + toolResultContent.put("type", "tool_result"); + toolResultContent.put("tool_use_id", message.getToolCallId()); + toolResultContent.put("content", message.getContent()); + content.add(toolResultContent); + } + + claudeMessage.put("content", content); + claudeMessages.add(claudeMessage); + } + + return claudeMessages; + } + + /** + * 添加Claude工具参数 + */ + private void addClaudeToolParameters(Map apiRequest, ChatRequest request) { + if (!supportsFunctionCalling()) { + return; + } + + // Claude使用tools参数 + if (request.getTools() != null && !request.getTools().isEmpty()) { + List> claudeTools = new ArrayList<>(); + + for (FunctionTool tool : request.getTools()) { + Map claudeTool = new HashMap<>(); + claudeTool.put("name", tool.getFunction().getName()); + claudeTool.put("description", tool.getFunction().getDescription()); + claudeTool.put("input_schema", tool.getFunction().getParameters()); + claudeTools.add(claudeTool); + } + + apiRequest.put("tools", claudeTools); + + log.debug("Added {} tools to Claude request", claudeTools.size()); + } + } + + /** + * 发送Claude API请求 + */ + private ResponseEntity sendClaudeApiRequest(Map apiRequest) { + LLMChatProperties.ProviderConfig config = getProviderConfig(); + if (config == null) { + throw new RuntimeException("Claude provider configuration not found"); + } + + HttpHeaders headers = new HttpHeaders(); + headers.set("Content-Type", "application/json"); + headers.set("x-api-key", config.getApiKey()); + + // 添加自定义头 + config.getHeaders().forEach(headers::set); + + HttpEntity> requestEntity = new HttpEntity<>(apiRequest, headers); + + String url = config.getBaseUrl() + "/v1/messages"; + log.debug("Sending request to Claude API: {} with {} tools", + url, + apiRequest.containsKey("tools") ? + ((List) apiRequest.get("tools")).size() : 0); + + try { + ResponseEntity response = restTemplate.postForEntity(url, requestEntity, String.class); + + // 检查Claude特定的错误响应 + if (response.getBody() != null && response.getBody().contains("error")) { + handleClaudeErrorResponse(response.getBody()); + } + + return response; + + } catch (Exception e) { + throw handleClaudeApiException(e); + } + } + + /** + * 解析Claude API响应 + */ + private ChatResponse parseClaudeApiResponse(String responseBody, long processingTime) throws JsonProcessingException { + JsonNode rootNode = objectMapper.readTree(responseBody); + + ChatResponse response = new ChatResponse(); + response.setId(rootNode.path("id").asText()); + response.setObject("chat.completion"); + response.setModel(rootNode.path("model").asText()); + response.setCreated(LocalDateTime.now()); + response.setProcessingTimeMs(processingTime); + + // 解析content数组 + List choices = new ArrayList<>(); + ChatResponse.Choice choice = new ChatResponse.Choice(); + choice.setIndex(0); + choice.setFinishReason(rootNode.path("stop_reason").asText()); + + // 解析消息内容 + ChatMessage message = parseClaudeMessage(rootNode); + choice.setMessage(message); + choices.add(choice); + response.setChoices(choices); + + // 解析usage + JsonNode usageNode = rootNode.path("usage"); + if (!usageNode.isMissingNode()) { + ChatResponse.Usage usage = new ChatResponse.Usage(); + usage.setPromptTokens(usageNode.path("input_tokens").asInt()); + usage.setCompletionTokens(usageNode.path("output_tokens").asInt()); + usage.setTotalTokens(usage.getPromptTokens() + usage.getCompletionTokens()); + response.setUsage(usage); + } + + return response; + } + + /** + * 解析Claude消息 + */ + private ChatMessage parseClaudeMessage(JsonNode rootNode) { + ChatMessage message = new ChatMessage(); + message.setRole(rootNode.path("role").asText()); + + StringBuilder contentBuilder = new StringBuilder(); + List toolCalls = new ArrayList<>(); + + JsonNode contentArray = rootNode.path("content"); + if (contentArray.isArray()) { + for (JsonNode contentItem : contentArray) { + String type = contentItem.path("type").asText(); + + if ("text".equals(type)) { + String text = contentItem.path("text").asText(); + if (!text.isEmpty()) { + if (contentBuilder.length() > 0) { + contentBuilder.append("\n"); + } + contentBuilder.append(text); + } + } else if ("tool_use".equals(type)) { + ToolCall toolCall = parseClaudeToolUse(contentItem); + if (toolCall != null) { + toolCalls.add(toolCall); + } + } + } + } + + message.setContent(contentBuilder.toString()); + if (!toolCalls.isEmpty()) { + message.setToolCalls(toolCalls); + } + + return message; + } + + /** + * 解析Claude工具使用 + */ + private ToolCall parseClaudeToolUse(JsonNode toolUseNode) { + try { + String id = toolUseNode.path("id").asText(); + String name = toolUseNode.path("name").asText(); + JsonNode inputNode = toolUseNode.path("input"); + + if (id.isEmpty() || name.isEmpty()) { + log.warn("Invalid tool use node: missing id or name"); + return null; + } + + String arguments = objectMapper.writeValueAsString(inputNode); + + log.debug("Parsed Claude tool use: id={}, name={}, input_size={}", + id, name, arguments.length()); + + FunctionCall functionCall = new FunctionCall(name, arguments); + return new ToolCall(id, "function", functionCall); + + } catch (Exception e) { + log.error("Failed to parse Claude tool use from JSON node", e); + return null; + } + } + + /** + * 处理Claude错误响应 + */ + private void handleClaudeErrorResponse(String responseBody) { + try { + JsonNode errorNode = objectMapper.readTree(responseBody); + JsonNode error = errorNode.path("error"); + + if (!error.isMissingNode()) { + String errorType = error.path("type").asText(); + String errorMessage = error.path("message").asText(); + + log.warn("Claude API error - Type: {}, Message: {}", errorType, errorMessage); + + // 根据错误类型抛出不同的异常 + switch (errorType) { + case "invalid_request_error": + throw new IllegalArgumentException("Claude请求无效: " + errorMessage); + case "authentication_error": + throw new RuntimeException("Claude认证失败: " + errorMessage); + case "permission_error": + throw new RuntimeException("Claude权限不足: " + errorMessage); + case "not_found_error": + throw new RuntimeException("Claude资源未找到: " + errorMessage); + case "rate_limit_error": + throw new RuntimeException("Claude API限流: " + errorMessage); + case "api_error": + throw new RuntimeException("Claude API错误: " + errorMessage); + case "overloaded_error": + throw new RuntimeException("Claude服务过载: " + errorMessage); + default: + throw new RuntimeException("Claude未知错误: " + errorMessage); + } + } + } catch (JsonProcessingException e) { + log.debug("Failed to parse Claude error response", e); + } + } + + /** + * 处理Claude API异常 + */ + private RuntimeException handleClaudeApiException(Exception e) { + String message = e.getMessage(); + + if (message != null) { + if (message.contains("timeout") || message.contains("timed out")) { + return new RuntimeException("Claude API请求超时: " + message, e); + } else if (message.contains("connection") || message.contains("connect")) { + return new RuntimeException("Claude API连接失败: " + message, e); + } else if (message.contains("401") || message.contains("unauthorized")) { + return new RuntimeException("Claude API认证失败: " + message, e); + } else if (message.contains("403") || message.contains("forbidden")) { + return new RuntimeException("Claude API访问被拒绝: " + message, e); + } else if (message.contains("429") || message.contains("rate limit")) { + return new RuntimeException("Claude API请求频率限制: " + message, e); + } else if (message.contains("500") || message.contains("internal server")) { + return new RuntimeException("Claude API服务器内部错误: " + message, e); + } + } + + return new RuntimeException("Claude API请求失败: " + message, e); + } + + /** + * 计算质量指标 + */ + private void calculateQualityMetrics(ChatResponse response, ChatRequest request) { + double qualityScore = 0.85; // Claude基础分数较高 + double confidence = 0.8; // Claude置信度较高 + + // 根据响应长度调整 + if (response.getChoices() != null && !response.getChoices().isEmpty()) { + ChatMessage message = response.getChoices().get(0).getMessage(); + if (message != null && message.getContent() != null) { + int length = message.getContent().length(); + if (length > 100 && length < 2000) { + qualityScore += 0.1; + confidence += 0.1; + } + } + } + + // 根据处理时间调整 + if (response.getProcessingTimeMs() != null && response.getProcessingTimeMs() < 8000) { + qualityScore += 0.05; + } + + // 如果有工具调用,适当提高质量分数 + if (response.getChoices() != null && + response.getChoices().stream().anyMatch(choice -> + choice.getMessage() != null && + choice.getMessage().getToolCalls() != null && + !choice.getMessage().getToolCalls().isEmpty())) { + qualityScore += 0.1; + confidence += 0.15; // Claude在工具调用方面表现优秀 + } + + response.setQualityScore(Math.min(1.0, qualityScore)); + response.setConfidence(Math.min(1.0, confidence)); + } + + /** + * 获取提供商配置 + */ + private LLMChatProperties.ProviderConfig getProviderConfig() { + return chatProperties.getProviders().get("claude"); + } + + @Override + public boolean healthCheck() { + try { + LLMChatProperties.ProviderConfig config = getProviderConfig(); + if (config == null || !config.isEnabled()) { + return false; + } + + // 简单的健康检查:构建一个最小请求 + ChatRequest testRequest = new ChatRequest(); + testRequest.setModel("claude-3-haiku-20240307"); + testRequest.setMessages(Collections.singletonList( + ChatMessage.user("health check") + )); + testRequest.setMaxTokens(10); + + Map apiRequest = buildClaudeApiRequest(testRequest, false); + String url = config.getBaseUrl() + "/v1/messages"; + + HttpHeaders headers = new HttpHeaders(); + headers.set("Content-Type", "application/json"); + headers.set("x-api-key", config.getApiKey()); + HttpEntity> requestEntity = new HttpEntity<>(apiRequest, headers); + + ResponseEntity response = restTemplate.postForEntity(url, requestEntity, String.class); + return response.getStatusCode().is2xxSuccessful(); + + } catch (Exception e) { + log.debug("Claude health check failed", e); + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/provider/QwenProvider.java b/src/main/java/com/chinaweal/youfool/devops/ai/provider/QwenProvider.java index 1ea1f5d..fb4ca9f 100644 --- a/src/main/java/com/chinaweal/youfool/devops/ai/provider/QwenProvider.java +++ b/src/main/java/com/chinaweal/youfool/devops/ai/provider/QwenProvider.java @@ -3,6 +3,10 @@ package com.chinaweal.youfool.devops.ai.provider; import com.chinaweal.youfool.devops.ai.config.LLMChatProperties; import com.chinaweal.youfool.devops.ai.dto.llm.*; import com.chinaweal.youfool.devops.ai.handler.FunctionCallHandler; +import com.chinaweal.youfool.devops.ai.mcp.MCPFunctionBridge; +import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition; +import com.chinaweal.youfool.devops.ai.client.MCPClient; +import com.chinaweal.youfool.devops.util.ErrorLogUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -30,12 +34,17 @@ public class QwenProvider extends AbstractLLMProvider { private final LLMChatProperties chatProperties; private final FunctionCallHandler functionCallHandler; + private final MCPFunctionBridge functionBridge; + private final MCPClient mcpClient; public QwenProvider(RestTemplate restTemplate, ObjectMapper objectMapper, - LLMChatProperties chatProperties, FunctionCallHandler functionCallHandler) { + LLMChatProperties chatProperties, FunctionCallHandler functionCallHandler, + MCPFunctionBridge functionBridge, MCPClient mcpClient) { super(restTemplate, objectMapper); this.chatProperties = chatProperties; this.functionCallHandler = functionCallHandler; + this.functionBridge = functionBridge; + this.mcpClient = mcpClient; } @Override @@ -67,6 +76,9 @@ public class QwenProvider extends AbstractLLMProvider { long startTime = System.currentTimeMillis(); try { + // 预处理:添加MCP工具定义 + preprocessRequestWithMCPTools(request); + // 构建API请求 Map apiRequest = buildApiRequest(request, false); @@ -77,6 +89,9 @@ public class QwenProvider extends AbstractLLMProvider { long processingTime = System.currentTimeMillis() - startTime; ChatResponse chatResponse = parseApiResponse(response.getBody(), processingTime); + // 处理工具调用 + chatResponse = processToolCallsIfPresent(chatResponse, request); + // 设置质量指标 calculateQualityMetrics(chatResponse, request); @@ -87,6 +102,8 @@ public class QwenProvider extends AbstractLLMProvider { long processingTime = System.currentTimeMillis() - startTime; logError("chatCompletion", e, "sessionId: " + request.getSessionId()); logResponse("chatCompletion", false, processingTime); + ErrorLogUtils.saveRuntimeError("QwenProvider.chatCompletion", e, + "sessionId: " + request.getSessionId()); throw new RuntimeException("Qwen聊天完成请求失败: " + e.getMessage(), e); } } @@ -137,11 +154,47 @@ public class QwenProvider extends AbstractLLMProvider { private Map buildApiRequest(ChatRequest request, boolean stream) { Map apiRequest = buildBaseApiRequest(request, stream); - // 添加函数调用参数 - addFunctionCallingParameters(apiRequest, request); + // 添加Qwen特定的函数调用参数 + addQwenFunctionCallingParameters(apiRequest, request); return apiRequest; } + + /** + * 添加Qwen特定的函数调用参数(OpenAI兼容格式) + */ + private void addQwenFunctionCallingParameters(Map apiRequest, ChatRequest request) { + if (!supportsFunctionCalling()) { + return; + } + + // Qwen使用OpenAI兼容的函数调用格式 + if (request.getTools() != null && !request.getTools().isEmpty()) { + // 直接使用tools参数 + apiRequest.put("tools", request.getTools()); + + // 添加工具选择策略 + if (request.getToolChoice() != null) { + // Qwen支持 "none", "auto", "required" + apiRequest.put("tool_choice", request.getToolChoice()); + } else { + // 默认为auto + apiRequest.put("tool_choice", "auto"); + } + + // 添加并行工具调用设置 + if (request.getParallelToolCalls() != null) { + apiRequest.put("parallel_tool_calls", request.getParallelToolCalls()); + } else { + // Qwen默认支持并行调用 + apiRequest.put("parallel_tool_calls", true); + } + + log.debug("Added {} tools to Qwen request with tool_choice: {}", + request.getTools().size(), + apiRequest.get("tool_choice")); + } + } /** * 发送API请求 @@ -160,9 +213,84 @@ public class QwenProvider extends AbstractLLMProvider { HttpEntity> requestEntity = new HttpEntity<>(apiRequest, headers); String url = config.getBaseUrl() + "/chat/completions"; - log.debug("Sending request to Qwen API: {}", url); + log.debug("Sending request to Qwen API: {} with {} tools", + url, + apiRequest.containsKey("tools") ? + ((List) apiRequest.get("tools")).size() : 0); - return restTemplate.postForEntity(url, requestEntity, String.class); + try { + ResponseEntity response = restTemplate.postForEntity(url, requestEntity, String.class); + + // 检查Qwen特定的错误响应 + if (response.getBody() != null && response.getBody().contains("error")) { + handleQwenErrorResponse(response.getBody()); + } + + return response; + + } catch (Exception e) { + throw handleQwenApiException(e); + } + } + + /** + * 处理Qwen特定的错误响应 + */ + private void handleQwenErrorResponse(String responseBody) { + try { + JsonNode errorNode = objectMapper.readTree(responseBody); + JsonNode error = errorNode.path("error"); + + if (!error.isMissingNode()) { + String errorCode = error.path("code").asText(); + String errorMessage = error.path("message").asText(); + String errorType = error.path("type").asText(); + + log.warn("Qwen API error - Code: {}, Type: {}, Message: {}", + errorCode, errorType, errorMessage); + + // 根据错误类型抛出不同的异常 + switch (errorCode) { + case "DataInspectionFailed": + throw new RuntimeException("Qwen数据检查失败: " + errorMessage); + case "InvalidParameter": + throw new IllegalArgumentException("Qwen参数无效: " + errorMessage); + case "Throttling": + throw new RuntimeException("Qwen API限流: " + errorMessage); + case "InvalidApiKey": + throw new RuntimeException("Qwen API密钥无效: " + errorMessage); + default: + throw new RuntimeException("Qwen API错误: " + errorMessage); + } + } + } catch (JsonProcessingException e) { + log.debug("Failed to parse Qwen error response", e); + } + } + + /** + * 处理Qwen API异常 + */ + private RuntimeException handleQwenApiException(Exception e) { + String message = e.getMessage(); + + if (message != null) { + if (message.contains("timeout") || message.contains("timed out")) { + return new RuntimeException("Qwen API请求超时: " + message, e); + } else if (message.contains("connection") || message.contains("connect")) { + return new RuntimeException("Qwen API连接失败: " + message, e); + } else if (message.contains("401") || message.contains("unauthorized")) { + return new RuntimeException("Qwen API认证失败: " + message, e); + } else if (message.contains("403") || message.contains("forbidden")) { + return new RuntimeException("Qwen API访问被拒绝: " + message, e); + } else if (message.contains("429") || message.contains("rate limit")) { + return new RuntimeException("Qwen API请求频率限制: " + message, e); + } else if (message.contains("500") || message.contains("internal server")) { + return new RuntimeException("Qwen API服务器内部错误: " + message, e); + } + } + + return new RuntimeException("Qwen API请求失败: " + message, e); } /** @@ -253,6 +381,23 @@ public class QwenProvider extends AbstractLLMProvider { return null; } + // 验证参数格式 + if (arguments != null && !arguments.trim().isEmpty()) { + try { + // 尝试解析参数JSON以验证格式 + objectMapper.readTree(arguments); + log.debug("Parsed tool call: id={}, function={}, arguments_length={}", + id, functionName, arguments.length()); + } catch (JsonProcessingException e) { + log.warn("Invalid function call arguments JSON for function {}: {}", + functionName, e.getMessage()); + // 如果参数不是有效JSON,包装为字符串 + arguments = "\"" + arguments.replace("\"", "\\\"") + "\""; + } + } else { + arguments = "{}"; // 空参数对象 + } + FunctionCall functionCall = new FunctionCall(functionName, arguments); return new ToolCall(id, type.isEmpty() ? "function" : type, functionCall); @@ -300,6 +445,252 @@ public class QwenProvider extends AbstractLLMProvider { response.setConfidence(Math.min(1.0, confidence)); } + /** + * 预处理请求:添加MCP工具定义 + */ + private void preprocessRequestWithMCPTools(ChatRequest request) { + try { + if (request.getMcpTools() != null && !request.getMcpTools().isEmpty()) { + log.debug("Converting {} MCP tools to OpenAI format", request.getMcpTools().size()); + + // 将MCP工具转换为OpenAI格式 + List> openAITools = functionBridge.convertToOpenAIFunctions( + request.getMcpTools()); + + // 合并现有工具和MCP工具 + List> allTools = new ArrayList<>(); + if (request.getTools() != null) { + allTools.addAll(request.getTools()); + } + allTools.addAll(openAITools); + + request.setTools(allTools); + + log.debug("Total tools after MCP conversion: {}", allTools.size()); + } + } catch (Exception e) { + log.error("Failed to preprocess MCP tools", e); + ErrorLogUtils.saveRuntimeError("QwenProvider.preprocessRequestWithMCPTools", e, + "sessionId: " + request.getSessionId()); + // 不抛出异常,继续处理 + } + } + + /** + * 处理工具调用(如果存在) + */ + private ChatResponse processToolCallsIfPresent(ChatResponse response, ChatRequest originalRequest) { + try { + // 检查是否有工具调用 + List toolCalls = extractToolCallsFromResponse(response); + if (toolCalls.isEmpty()) { + return response; + } + + log.info("Processing {} tool calls from Qwen response", toolCalls.size()); + + // 并行或顺序执行工具调用 + boolean parallelExecution = shouldExecuteToolCallsInParallel(originalRequest); + List toolResults = executeToolCalls(toolCalls, parallelExecution); + + // 构建继续对话请求 + ChatRequest followUpRequest = buildFollowUpRequest(originalRequest, response, toolResults); + + // 发送继续请求获取最终响应 + return executeFinalRequest(followUpRequest); + + } catch (Exception e) { + log.error("Failed to process tool calls", e); + ErrorLogUtils.saveRuntimeError("QwenProvider.processToolCallsIfPresent", e, + "sessionId: " + originalRequest.getSessionId()); + + // 返回错误信息的响应 + return createToolCallErrorResponse(response, e.getMessage()); + } + } + + /** + * 从响应中提取工具调用 + */ + private List extractToolCallsFromResponse(ChatResponse response) { + List toolCalls = new ArrayList<>(); + + if (response.getChoices() != null) { + for (ChatResponse.Choice choice : response.getChoices()) { + if (choice.getMessage() != null && choice.getMessage().getToolCalls() != null) { + toolCalls.addAll(choice.getMessage().getToolCalls()); + } + } + } + + return toolCalls; + } + + /** + * 判断是否应该并行执行工具调用 + */ + private boolean shouldExecuteToolCallsInParallel(ChatRequest request) { + LLMChatProperties.ProviderConfig config = getProviderConfig(); + + if (request.getParallelToolCalls() != null) { + return request.getParallelToolCalls(); + } + + if (config != null && config.getFunctionCalling() != null) { + return config.getFunctionCalling().isParallelExecution(); + } + + return true; // 默认并行执行 + } + + /** + * 执行工具调用 + */ + private List executeToolCalls(List toolCalls, boolean parallel) { + log.debug("Executing {} tool calls in {} mode", + toolCalls.size(), parallel ? "parallel" : "sequential"); + + return mcpClient.executeToolCalls(toolCalls); + } + + /** + * 构建继续对话请求 + */ + private ChatRequest buildFollowUpRequest(ChatRequest originalRequest, + ChatResponse toolCallResponse, + List toolResults) { + ChatRequest followUpRequest = new ChatRequest(); + followUpRequest.setSessionId(originalRequest.getSessionId()); + followUpRequest.setModel(originalRequest.getModel()); + followUpRequest.setTemperature(originalRequest.getTemperature()); + followUpRequest.setMaxTokens(originalRequest.getMaxTokens()); + followUpRequest.setTopP(originalRequest.getTopP()); + + // 构建新的消息列表 + List messages = new ArrayList<>(originalRequest.getMessages()); + + // 添加助手的工具调用消息 + if (toolCallResponse.getChoices() != null && !toolCallResponse.getChoices().isEmpty()) { + ChatMessage assistantMessage = toolCallResponse.getChoices().get(0).getMessage(); + if (assistantMessage != null) { + messages.add(assistantMessage); + } + } + + // 添加工具执行结果消息 + for (ToolCallResult result : toolResults) { + ChatMessage toolMessage = new ChatMessage(); + toolMessage.setRole("tool"); + toolMessage.setToolCallId(result.getToolCallId()); + toolMessage.setContent(formatToolResult(result)); + messages.add(toolMessage); + } + + followUpRequest.setMessages(messages); + return followUpRequest; + } + + /** + * 格式化工具执行结果 + */ + private String formatToolResult(ToolCallResult result) { + if (Boolean.TRUE.equals(result.getSuccess())) { + return result.getResult() != null ? result.getResult() : ""; + } else { + return "工具执行错误: " + result.getError(); + } + } + + /** + * 执行最终请求 + */ + private ChatResponse executeFinalRequest(ChatRequest followUpRequest) { + try { + log.debug("Executing final request after tool calls for session: {}", + followUpRequest.getSessionId()); + + // 移除工具定义,避免循环调用 + followUpRequest.setTools(null); + followUpRequest.setMcpTools(null); + followUpRequest.setToolChoice("none"); + + Map apiRequest = buildApiRequest(followUpRequest, false); + ResponseEntity response = sendApiRequest(apiRequest); + + long processingTime = System.currentTimeMillis() - System.currentTimeMillis(); + return parseApiResponse(response.getBody(), processingTime); + + } catch (Exception e) { + log.error("Failed to execute final request after tool calls", e); + throw new RuntimeException("工具调用后的最终请求失败: " + e.getMessage(), e); + } + } + + /** + * 创建工具调用错误响应 + */ + private ChatResponse createToolCallErrorResponse(ChatResponse originalResponse, String errorMessage) { + ChatResponse errorResponse = new ChatResponse(); + errorResponse.setId("error-" + UUID.randomUUID().toString()); + errorResponse.setObject("chat.completion"); + errorResponse.setModel(originalResponse.getModel()); + errorResponse.setCreated(LocalDateTime.now()); + + ChatMessage errorMessage1 = new ChatMessage(); + errorMessage1.setRole("assistant"); + errorMessage1.setContent("抱歉,工具调用过程中出现错误:" + errorMessage); + + ChatResponse.Choice choice = new ChatResponse.Choice(); + choice.setIndex(0); + choice.setMessage(errorMessage1); + choice.setFinishReason("tool_error"); + + errorResponse.setChoices(Collections.singletonList(choice)); + errorResponse.setQualityScore(0.1); + errorResponse.setConfidence(0.1); + + return errorResponse; + } + + /** + * 确定工具选择策略 + */ + private String determineToolChoice(ChatRequest request, LLMChatProperties.ProviderConfig config) { + if (request.getToolChoice() != null) { + // 验证工具选择值 + String toolChoice = request.getToolChoice().toString(); + if (Arrays.asList("none", "auto", "required").contains(toolChoice)) { + return toolChoice; + } else { + log.warn("Invalid tool_choice value: {}, using auto", toolChoice); + return "auto"; + } + } + + // 使用配置默认值 + if (config != null && config.getFunctionCalling() != null) { + return config.getFunctionCalling().getToolChoice(); + } + + return "auto"; + } + + /** + * 确定是否允许并行调用 + */ + private boolean determineParallelCalls(ChatRequest request, LLMChatProperties.ProviderConfig config) { + if (request.getParallelToolCalls() != null) { + return request.getParallelToolCalls(); + } + + // 使用配置默认值 + if (config != null && config.getFunctionCalling() != null) { + return config.getFunctionCalling().isParallelExecution(); + } + + return true; // Qwen默认支持并行调用 + } + /** * 获取提供商配置 */ diff --git a/src/main/java/com/chinaweal/youfool/devops/ai/service/QwenChatService.java b/src/main/java/com/chinaweal/youfool/devops/ai/service/QwenChatService.java index 09c4900..1131be8 100644 --- a/src/main/java/com/chinaweal/youfool/devops/ai/service/QwenChatService.java +++ b/src/main/java/com/chinaweal/youfool/devops/ai/service/QwenChatService.java @@ -6,6 +6,9 @@ import com.chinaweal.youfool.devops.ai.config.LLMStreamingProperties; import com.chinaweal.youfool.devops.ai.dto.llm.*; import com.chinaweal.youfool.devops.ai.mcp.MCPServer; import com.chinaweal.youfool.devops.ai.mcp.MCPResponse; +import com.chinaweal.youfool.devops.ai.mcp.MCPTool; +import com.chinaweal.youfool.devops.ai.mcp.dto.MCPToolDefinition; +import com.chinaweal.youfool.devops.ai.provider.QwenProvider; import com.chinaweal.youfool.devops.util.ErrorLogUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -45,6 +48,7 @@ public class QwenChatService { private final LLMChatProperties chatProperties; private final LLMStreamingProperties streamingProperties; private final ObjectMapper objectMapper; + private final QwenProvider qwenProvider; private RestTemplate restTemplate; private Executor asyncExecutor; @@ -92,35 +96,16 @@ public class QwenChatService { // 参数验证和预处理 validateAndPreprocessRequest(request); - // 如果提供了MCP服务器,执行MCP工具调用逻辑 - List usedMcpTools = new ArrayList<>(); - if (mcpServer != null && request.getMcpTools() != null && !request.getMcpTools().isEmpty()) { - log.info("开始MCP工具调用流程,会话: {}", request.getSessionId()); - request = processMCPToolCalls(request, mcpServer, usedMcpTools); + // 添加MCP工具支持 + if (mcpServer != null) { + enhanceRequestWithMCPTools(request, mcpServer); } - // 构建API请求 - Map apiRequest = buildApiRequest(request, false); - - // 发送请求 - long startTime = System.currentTimeMillis(); - ResponseEntity response = sendApiRequest(apiRequest); - long processingTime = System.currentTimeMillis() - startTime; - - // 解析响应 - ChatResponse chatResponse = parseApiResponse(response.getBody(), processingTime); - - // 设置MCP工具使用记录 - if (!usedMcpTools.isEmpty()) { - chatResponse.setMcpToolsUsed(usedMcpTools); - log.info("MCP工具调用完成,使用的工具: {}", usedMcpTools); - } - - // 计算质量评分 - calculateQualityMetrics(chatResponse, request); + // 使用QwenProvider进行聊天完成(已内置MCP和函数调用支持) + ChatResponse chatResponse = qwenProvider.chatCompletion(request); log.info("Chat completion successful for session: {}, processing time: {}ms", - request.getSessionId(), processingTime); + request.getSessionId(), chatResponse.getProcessingTimeMs()); return chatResponse; @@ -131,6 +116,58 @@ public class QwenChatService { throw new RuntimeException("聊天完成请求处理失败: " + e.getMessage(), e); } } + + /** + * 增强请求:添加MCP工具支持 + */ + private void enhanceRequestWithMCPTools(ChatRequest request, MCPServer mcpServer) { + try { + // 获取可用的MCP工具并转换为MCPToolDefinition格式 + if (mcpServer.getAvailableTools() != null && !mcpServer.getAvailableTools().isEmpty()) { + List mcpToolDefinitions = convertMCPToolsToDefinitions(mcpServer.getAvailableTools()); + request.setMcpTools(mcpToolDefinitions); + log.debug("Added {} MCP tools to request for session: {}", + mcpServer.getAvailableTools().size(), request.getSessionId()); + } + } catch (Exception e) { + log.warn("Failed to enhance request with MCP tools: {}", e.getMessage()); + ErrorLogUtils.saveRuntimeError("QwenChatService.enhanceRequestWithMCPTools", e, + "sessionId: " + request.getSessionId()); + } + } + + /** + * 转换MCPTool到MCPToolDefinition + */ + private List convertMCPToolsToDefinitions(List mcpTools) { + List definitions = new ArrayList<>(); + for (MCPTool tool : mcpTools) { + MCPToolDefinition definition = new MCPToolDefinition(); + definition.setName(tool.getName()); + definition.setDescription(tool.getDescription()); + definition.setType(tool.getType()); + + // 转换inputSchema + if (tool.getInputSchema() != null) { + MCPToolDefinition.JsonSchema schema = new MCPToolDefinition.JsonSchema(); + schema.setType("object"); + + // 转换属性映射 + Map propertySchemas = new HashMap<>(); + for (Map.Entry entry : tool.getInputSchema().entrySet()) { + MCPToolDefinition.PropertySchema propertySchema = new MCPToolDefinition.PropertySchema(); + propertySchema.setType("string"); // 简化处理,默认为字符串类型 + propertySchema.setDescription(entry.getKey()); + propertySchemas.put(entry.getKey(), propertySchema); + } + schema.setProperties(propertySchemas); + definition.setInputSchema(schema); + } + + definitions.add(definition); + } + return definitions; + } /** * 流式聊天完成 @@ -714,8 +751,10 @@ public class QwenChatService { } /** - * 处理MCP工具调用 + * 处理MCP工具调用(已迁移到QwenProvider) + * @deprecated 使用QwenProvider内置的MCP工具调用支持 */ + @Deprecated private ChatRequest processMCPToolCalls(ChatRequest request, MCPServer mcpServer, List usedMcpTools) { try { // 敏感信息脱敏的日志记录 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7bbcef0..3def057 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -511,6 +511,16 @@ ai: headers: Authorization: Bearer ${QWEN_API_KEY:sk-288824ef003e4e02bb963b8b3024b06a} extra-params: {} + # 函数调用配置 + function-calling: + enabled: true + max-tools: 10 + parallel-execution: true + timeout: 30000 + tool-choice: "auto" + retry-on-failure: true + max-retry-attempts: 2 + validation-enabled: true # 文心一言配置(百度) ernie: enabled: false