From 25eb277a2a65461b4e2eff100ae83c1f4588ba09 Mon Sep 17 00:00:00 2001 From: "DESKTOP-72TV0V4\\caoxiaozhu" Date: Wed, 11 Mar 2026 17:22:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Agent=20=E6=9C=8D=E5=8A=A1=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E5=8A=9F=E8=83=BD=E5=92=8C=E5=90=8E=E7=AB=AF=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agent/main.py: 添加日志记录功能 - agent/llm: 更新 anthropic, openai, factory - agent/core/agent.py: 更新 - server: agent_handler, agent_service, chat_service 更新 Co-Authored-By: Claude Opus 4.6 --- agent/app/agent/core/agent.py | 12 ++- agent/app/agent/llm/anthropic.py | 6 +- agent/app/agent/llm/factory.py | 12 ++- agent/app/agent/llm/openai.py | 19 +++- agent/app/main.py | 132 ++++++++++++++++++++++- server/cmd/api/main.go | 2 +- server/internal/handler/agent_handler.go | 2 + server/internal/model/agent.go | 1 + server/internal/service/agent_service.go | 53 ++++++++- server/internal/service/chat_service.go | 46 +++++++- 10 files changed, 260 insertions(+), 25 deletions(-) diff --git a/agent/app/agent/core/agent.py b/agent/app/agent/core/agent.py index 3445570..541e3c0 100644 --- a/agent/app/agent/core/agent.py +++ b/agent/app/agent/core/agent.py @@ -1,6 +1,7 @@ """ Agent Core - 单智能体核心 """ +import logging from typing import Optional, List, Dict, Any from pydantic import BaseModel from app.agent.memory.manager import MemoryManager @@ -8,6 +9,8 @@ from app.agent.skills.router import SkillRouter from app.agent.skills.executor import SkillExecutor from app.agent.llm.factory import LLMFactory +logger = logging.getLogger("agent.core") + class AgentConfig(BaseModel): """智能体配置""" @@ -16,6 +19,8 @@ class AgentConfig(BaseModel): role_description: str model_provider: str = "openai" model_name: str = "gpt-4" + api_key: Optional[str] = None # API Key(可选,用于覆盖默认配置) + base_url: Optional[str] = None # Base URL(可选,用于覆盖默认配置) skills: List[int] = [] # 技能 ID 列表 knowledge_base_ids: List[int] = [] timeout: int = 60 @@ -36,7 +41,12 @@ class AgentCore: def __init__(self, config: AgentConfig): self.config = config - self.llm = LLMFactory.create(config.model_provider, config.model_name) + + # 记录 LLM 初始化信息 + api_key_info = f"{config.api_key[:10]}..." if config.api_key else "None" + logger.info(f"初始化 AgentCore: name={config.name}, provider={config.model_provider}, model={config.model_name}, api_key={api_key_info}, base_url={config.base_url}") + + self.llm = LLMFactory.create(config.model_provider, config.model_name, config.api_key, config.base_url) self.memory = MemoryManager(config.id) self.skill_router = SkillRouter(config.skills) self.skill_executor = SkillExecutor() diff --git a/agent/app/agent/llm/anthropic.py b/agent/app/agent/llm/anthropic.py index 8854f71..b288845 100644 --- a/agent/app/agent/llm/anthropic.py +++ b/agent/app/agent/llm/anthropic.py @@ -2,17 +2,17 @@ Anthropic LLM 实现 """ import os -from typing import Dict, Any, List +from typing import Dict, Any, List, Optional from anthropic import AsyncAnthropic class AnthropicLLM: """Anthropic Claude LLM""" - def __init__(self, model_name: str = "claude-3-sonnet-20240229"): + def __init__(self, model_name: str = "claude-3-sonnet-20240229", api_key: Optional[str] = None): self.model_name = model_name self.client = AsyncAnthropic( - api_key=os.getenv("ANTHROPIC_API_KEY", "") + api_key=api_key or os.getenv("ANTHROPIC_API_KEY", "") ) async def decide(self, prompt: str) -> Dict[str, Any]: diff --git a/agent/app/agent/llm/factory.py b/agent/app/agent/llm/factory.py index 2eaa648..3062477 100644 --- a/agent/app/agent/llm/factory.py +++ b/agent/app/agent/llm/factory.py @@ -1,7 +1,7 @@ """ LLM Factory - LLM 工厂类 """ -from typing import Dict, Any +from typing import Optional from app.agent.llm.openai import OpenAILLM from app.agent.llm.anthropic import AnthropicLLM @@ -10,21 +10,23 @@ class LLMFactory: """LLM 工厂类""" @staticmethod - def create(provider: str, model_name: str): + def create(provider: str, model_name: str, api_key: Optional[str] = None, base_url: Optional[str] = None): """ 创建 LLM 实例 Args: provider: 模型提供商 (openai/anthropic) model_name: 模型名称 + api_key: API Key(可选) + base_url: Base URL(可选) Returns: LLM 实例 """ if provider.lower() == "openai": - return OpenAILLM(model_name) + return OpenAILLM(model_name, api_key, base_url) elif provider.lower() == "anthropic": - return AnthropicLLM(model_name) + return AnthropicLLM(model_name, api_key) else: # 默认使用 OpenAI - return OpenAILLM(model_name) + return OpenAILLM(model_name, api_key, base_url) diff --git a/agent/app/agent/llm/openai.py b/agent/app/agent/llm/openai.py index 25709b7..080c328 100644 --- a/agent/app/agent/llm/openai.py +++ b/agent/app/agent/llm/openai.py @@ -2,18 +2,31 @@ OpenAI LLM 实现 """ import os +import logging from typing import Dict, Any, List, Optional from openai import AsyncOpenAI +logger = logging.getLogger("llm.openai") + class OpenAILLM: """OpenAI LLM""" - def __init__(self, model_name: str = "gpt-4"): + def __init__(self, model_name: str = "gpt-4", api_key: Optional[str] = None, base_url: Optional[str] = None): self.model_name = model_name + # 优先使用传入的参数,否则使用环境变量 + self.api_key = api_key or os.getenv("OPENAI_API_KEY", "") + self.base_url = base_url or os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") + + api_key_info = f"{self.api_key[:10]}..." if self.api_key else "None" + logger.info(f"初始化 OpenAI LLM: model={model_name}, api_key={api_key_info}, base_url={self.base_url}") + + if not self.api_key: + logger.warning("⚠️ WARNING: No API key provided!") + self.client = AsyncOpenAI( - api_key=os.getenv("OPENAI_API_KEY", ""), - base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") + api_key=self.api_key, + base_url=self.base_url ) async def decide(self, prompt: str) -> Dict[str, Any]: diff --git a/agent/app/main.py b/agent/app/main.py index a19885c..4737b3d 100644 --- a/agent/app/main.py +++ b/agent/app/main.py @@ -2,7 +2,10 @@ FastAPI Agent Engine Server """ import os +import sys import time +import logging +from datetime import datetime from typing import Optional from fastapi import FastAPI, HTTPException from pydantic import BaseModel @@ -12,6 +15,90 @@ from app.agent.core import AgentCore, Supervisor, AgentConfig from app.agent.llm import LLMFactory +# 日志目录 - 放在 server/logs 下 +LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "server", "logs", datetime.now().strftime("%Y-%m-%d")) +os.makedirs(LOG_DIR, exist_ok=True) + +# 成功/失败日志文件 +today = datetime.now().strftime("%Y-%m-%d") +success_log_file = os.path.join(LOG_DIR, f"python_success.log") +failure_log_file = os.path.join(LOG_DIR, f"python_failure.log") + + +def setup_logging(): + """配置日志系统""" + log_level = os.getenv("LOG_LEVEL", "INFO") + + # 创建格式化器 + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # 控制台处理器 + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(formatter) + + # 成功日志文件处理器 + success_handler = logging.FileHandler(success_log_file, encoding='utf-8') + success_handler.setFormatter(formatter) + success_handler.setLevel(logging.INFO) + + # 失败日志文件处理器 + failure_handler = logging.FileHandler(failure_log_file, encoding='utf-8') + failure_handler.setFormatter(formatter) + failure_handler.setLevel(logging.WARNING) + + # 根日志配置 + root_logger = logging.getLogger() + root_logger.setLevel(getattr(logging, log_level)) + root_logger.addHandler(console_handler) + root_logger.addHandler(success_handler) + root_logger.addHandler(failure_handler) + + return root_logger + + +# 成功日志记录器(只记录 INFO 级别到成功日志) +class SuccessLogger: + """成功日志记录器""" + + @staticmethod + def log(message: str): + """记录成功日志""" + logger = logging.getLogger("success") + logger.setLevel(logging.INFO) + handler = logging.FileHandler(success_log_file, encoding='utf-8') + handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')) + logger.addHandler(handler) + logger.info(message) + + # 同时输出到控制台 + print(f"✅ {message}") + + +# 失败日志记录器 +class FailureLogger: + """失败日志记录器""" + + @staticmethod + def log(message: str, error: str = ""): + """记录失败日志""" + logger = logging.getLogger("failure") + logger.setLevel(logging.WARNING) + handler = logging.FileHandler(failure_log_file, encoding='utf-8') + handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s - %(error)s', datefmt='%Y-%m-%d %H:%M:%S')) + logger.addHandler(handler) + + full_message = f"{message} - Error: {error}" if error else message + logger.warning(full_message) + + # 同时输出到控制台 + print(f"❌ {full_message}") + + +logger = setup_logging() + app = FastAPI(title="X-Agents Python Engine", version="1.0.0") # CORS @@ -32,6 +119,12 @@ class ChatRequest(BaseModel): message: str user_id: int = 1 session_id: Optional[str] = None + # 模型参数(可选,如果传了就使用,否则用智能体配置的默认模型) + model_id: Optional[str] = None + model_name: Optional[str] = None + model_provider: Optional[str] = None + api_key: Optional[str] = None + base_url: Optional[str] = None class TeamChatRequest(BaseModel): @@ -76,7 +169,7 @@ _mock_agents = { } -def get_agent_config(agent_id: int) -> AgentConfig: +def get_agent_config(agent_id: int, api_key: str = None, base_url: str = None) -> AgentConfig: """获取智能体配置""" agent_data = _mock_agents.get(agent_id) if not agent_data: @@ -88,6 +181,8 @@ def get_agent_config(agent_id: int) -> AgentConfig: role_description=agent_data["role_description"], model_provider=agent_data["model_provider"], model_name=agent_data["model_name"], + api_key=api_key, + base_url=base_url, skills=agent_data.get("skills", []) ) @@ -109,16 +204,42 @@ async def chat(request: ChatRequest): """ 单智能体对话 """ + chat_logger = logging.getLogger("agent.chat") + + # 打印请求参数(隐藏 api_key 敏感信息) + api_key_preview = f"{request.api_key[:10]}..." if request.api_key else "None" + chat_logger.info(f"========== 收到聊天请求 ==========") + chat_logger.info(f"agent_id: {request.agent_id}") + chat_logger.info(f"model_id: {request.model_id}") + chat_logger.info(f"model_provider: {request.model_provider}") + chat_logger.info(f"model_name: {request.model_name}") + chat_logger.info(f"api_key: {api_key_preview}") + chat_logger.info(f"base_url: {request.base_url}") + chat_logger.info(f"message: {request.message[:50]}...") + start_time = time.time() # 获取智能体配置 try: - config = get_agent_config(request.agent_id) - except HTTPException: + config = get_agent_config(request.agent_id, request.api_key, request.base_url) + chat_logger.info(f"Agent config loaded: provider={config.model_provider}, model={config.model_name}") + except HTTPException as e: + FailureLogger.log(f"Agent not found: agent_id={request.agent_id}", str(e)) + chat_logger.error(f"Agent not found: {e}") raise except Exception as e: + FailureLogger.log(f"Error loading config: agent_id={request.agent_id}", str(e)) + chat_logger.error(f"Error loading config: {e}") raise HTTPException(status_code=400, detail=str(e)) + # 如果请求中指定了模型,覆盖智能体的默认配置 + if request.model_provider: + config.model_provider = request.model_provider + if request.model_name: + config.model_name = request.model_name + + chat_logger.info(f"Final LLM config: provider={config.model_provider}, model={config.model_name}, api_key={config.api_key[:10] if config.api_key else 'None'}..., base_url={config.base_url}") + # 创建智能体实例 agent = AgentCore(config) @@ -129,10 +250,15 @@ async def chat(request: ChatRequest): try: result = await agent.run(request.message, request.user_id, session_id) except Exception as e: + FailureLogger.log(f"Agent execution failed: agent_id={request.agent_id}, message={request.message[:30]}", str(e)) + chat_logger.error(f"Agent execution error: {e}") raise HTTPException(status_code=500, detail=str(e)) duration_ms = int((time.time() - start_time) * 1000) + # 记录成功日志 + SuccessLogger.log(f"Chat success: agent_id={request.agent_id}, duration={duration_ms}ms, message={request.message[:30]}...") + return ChatResponse( agent_id=request.agent_id, response=result.content, diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index 2919e23..1c83916 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -296,7 +296,7 @@ func main() { toolService := service.NewToolService(toolRepo) mcpService := service.NewMCPService(mcpRepo) skillService := service.NewSkillService(skillRepo) - agentService := service.NewAgentService(cfg.PythonServiceURL) + agentService := service.NewAgentService(cfg.PythonServiceURL, modelRepo) memoryService := service.NewMemoryService(agentRepo) // 4.2 初始化默认工具 diff --git a/server/internal/handler/agent_handler.go b/server/internal/handler/agent_handler.go index d645de1..e8c41d5 100644 --- a/server/internal/handler/agent_handler.go +++ b/server/internal/handler/agent_handler.go @@ -25,6 +25,7 @@ type ChatRequest struct { AgentID int `json:"agent_id" binding:"required"` Message string `json:"message" binding:"required"` SessionID string `json:"session_id"` + ModelID string `json:"model_id"` } // ChatResponse 对话响应 @@ -54,6 +55,7 @@ func (h *AgentHandler) Chat(c *gin.Context) { Message: req.Message, UserID: userID, SessionID: req.SessionID, + ModelID: req.ModelID, } result, err := h.agentService.Chat(pythonReq) diff --git a/server/internal/model/agent.go b/server/internal/model/agent.go index bf908bc..389b473 100644 --- a/server/internal/model/agent.go +++ b/server/internal/model/agent.go @@ -105,6 +105,7 @@ type AgentRequest struct { AgentID string `json:"agent_id" binding:"required"` Message string `json:"message" binding:"required"` SessionID string `json:"session_id"` + ModelID string `json:"model_id"` Context map[string]interface{} `json:"context"` } diff --git a/server/internal/service/agent_service.go b/server/internal/service/agent_service.go index 67633fd..b189de1 100644 --- a/server/internal/service/agent_service.go +++ b/server/internal/service/agent_service.go @@ -5,16 +5,24 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "time" + + "x-agents/server/internal/repository" ) // AgentChatRequest Python Agent 对话请求 type AgentChatRequest struct { - AgentID int `json:"agent_id"` - Message string `json:"message"` - UserID int `json:"user_id"` - SessionID string `json:"session_id,omitempty"` + AgentID int `json:"agent_id"` + Message string `json:"message"` + UserID int `json:"user_id"` + SessionID string `json:"session_id,omitempty"` + ModelID string `json:"model_id,omitempty"` + ModelName string `json:"model_name,omitempty"` + ModelProvider string `json:"model_provider,omitempty"` + APIKey string `json:"api_key,omitempty"` + BaseURL string `json:"base_url,omitempty"` } // AgentChatResponse Python Agent 对话响应 @@ -51,20 +59,55 @@ type TeamChatResponse struct { type AgentService struct { pythonURL string client *http.Client + modelRepo *repository.ModelRepository } // NewAgentService 创建 Agent 服务 -func NewAgentService(pythonURL string) *AgentService { +func NewAgentService(pythonURL string, modelRepo *repository.ModelRepository) *AgentService { return &AgentService{ pythonURL: pythonURL, client: &http.Client{ Timeout: 120 * time.Second, // Agent 可能需要较长时间 }, + modelRepo: modelRepo, } } // Chat 单智能体对话 func (s *AgentService) Chat(req AgentChatRequest) (*AgentChatResponse, error) { + // 如果传入了 model_id,查询模型配置获取 api_key 和 base_url + log.Printf("[AgentService] Chat called, model_id: %s, modelRepo: %v", req.ModelID, s.modelRepo != nil) + + if req.ModelID != "" && s.modelRepo != nil { + model, err := s.modelRepo.FindByID(req.ModelID) + if err != nil { + log.Printf("[AgentService] Error finding model: %v", err) + } else if model != nil { + log.Printf("[AgentService] Found model: id=%s, provider=%s, model=%s, base_url=%s, api_key_len=%d", + model.ID, model.Provider, model.Model, model.BaseURL, len(model.APIKey)) + req.APIKey = model.APIKey + req.BaseURL = model.BaseURL + req.ModelProvider = model.Provider + req.ModelName = model.Model + log.Printf("[AgentService] Set req.APIKey=%s, req.BaseURL=%s", req.APIKey[:10]+"...", req.BaseURL) + } else { + log.Printf("[AgentService] Model not found for id: %s", req.ModelID) + } + } else if s.modelRepo == nil { + log.Printf("[AgentService] WARNING: modelRepo is nil!") + } + + // 打印传给 Python 的请求内容 + apiKeyPreview := "" + if req.APIKey != "" { + apiKeyPreview = req.APIKey + if len(apiKeyPreview) > 10 { + apiKeyPreview = apiKeyPreview[:10] + "..." + } + } + log.Printf("[AgentService] Sending to Python: model_id=%s, api_key=%s, base_url=%s, provider=%s, model=%s", + req.ModelID, apiKeyPreview, req.BaseURL, req.ModelProvider, req.ModelName) + url := fmt.Sprintf("%s/agent/chat", s.pythonURL) jsonData, err := json.Marshal(req) diff --git a/server/internal/service/chat_service.go b/server/internal/service/chat_service.go index ca11f68..3cc3559 100644 --- a/server/internal/service/chat_service.go +++ b/server/internal/service/chat_service.go @@ -16,13 +16,15 @@ import ( type ChatService struct { pythonURL string - agentRepo *repository.AgentRepository + agentRepo *repository.AgentRepository + modelRepo *repository.ModelRepository } -func NewChatService(pythonURL string, agentRepo *repository.AgentRepository) *ChatService { +func NewChatService(pythonURL string, agentRepo *repository.AgentRepository, modelRepo *repository.ModelRepository) *ChatService { return &ChatService{ pythonURL: pythonURL, - agentRepo: agentRepo, + agentRepo: agentRepo, + modelRepo: modelRepo, } } @@ -30,9 +32,19 @@ type ChatRequest struct { AgentID string `json:"agent_id"` Message string `json:"message"` SessionID string `json:"session_id"` + ModelID string `json:"model_id"` Context map[string]interface{} `json:"context"` } +// ModelConfig 模型配置,用于传递给 Python 服务 +type ModelConfig struct { + Provider string `json:"provider"` + Model string `json:"model"` + APIKey string `json:"api_key"` + BaseURL string `json:"base_url"` + APIEndpoint string `json:"api_endpoint"` +} + type ChatResponse struct { Reply string `json:"reply"` SessionID string `json:"session_id"` @@ -59,14 +71,40 @@ func (s *ChatService) Chat(ctx context.Context, userID string, req model.AgentRe sessionID = uuid.New().String() } - // 4. 调用 Python 服务 + // 4. 如果提供了 ModelID,获取模型配置 + var modelConfig *ModelConfig + if req.ModelID != "" { + modelInfo, err := s.modelRepo.FindByID(req.ModelID) + if err != nil { + return nil, fmt.Errorf("model not found: %w", err) + } + modelConfig = &ModelConfig{ + Provider: modelInfo.Provider, + Model: modelInfo.Model, + APIKey: modelInfo.APIKey, + BaseURL: modelInfo.BaseURL, + APIEndpoint: modelInfo.APIEndpoint, + } + } + + // 5. 调用 Python 服务 pythonReq := ChatRequest{ AgentID: req.AgentID, Message: req.Message, SessionID: sessionID, + ModelID: req.ModelID, Context: req.Context, } + // 将模型配置放入 Context 中传递给 Python 服务 + if modelConfig != nil { + pythonReq.Context = make(map[string]interface{}) + for k, v := range req.Context { + pythonReq.Context[k] = v + } + pythonReq.Context["model_config"] = modelConfig + } + pythonResp, err := s.callPythonChat(ctx, pythonReq) if err != nil { return nil, fmt.Errorf("failed to call python service: %w", err)