feat: Agent 服务日志功能和后端更新

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 17:22:40 +08:00
parent f9660a3d7b
commit 25eb277a2a
10 changed files with 260 additions and 25 deletions

View File

@@ -1,6 +1,7 @@
""" """
Agent Core - 单智能体核心 Agent Core - 单智能体核心
""" """
import logging
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from pydantic import BaseModel from pydantic import BaseModel
from app.agent.memory.manager import MemoryManager 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.skills.executor import SkillExecutor
from app.agent.llm.factory import LLMFactory from app.agent.llm.factory import LLMFactory
logger = logging.getLogger("agent.core")
class AgentConfig(BaseModel): class AgentConfig(BaseModel):
"""智能体配置""" """智能体配置"""
@@ -16,6 +19,8 @@ class AgentConfig(BaseModel):
role_description: str role_description: str
model_provider: str = "openai" model_provider: str = "openai"
model_name: str = "gpt-4" model_name: str = "gpt-4"
api_key: Optional[str] = None # API Key可选用于覆盖默认配置
base_url: Optional[str] = None # Base URL可选用于覆盖默认配置
skills: List[int] = [] # 技能 ID 列表 skills: List[int] = [] # 技能 ID 列表
knowledge_base_ids: List[int] = [] knowledge_base_ids: List[int] = []
timeout: int = 60 timeout: int = 60
@@ -36,7 +41,12 @@ class AgentCore:
def __init__(self, config: AgentConfig): def __init__(self, config: AgentConfig):
self.config = config 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.memory = MemoryManager(config.id)
self.skill_router = SkillRouter(config.skills) self.skill_router = SkillRouter(config.skills)
self.skill_executor = SkillExecutor() self.skill_executor = SkillExecutor()

View File

@@ -2,17 +2,17 @@
Anthropic LLM 实现 Anthropic LLM 实现
""" """
import os import os
from typing import Dict, Any, List from typing import Dict, Any, List, Optional
from anthropic import AsyncAnthropic from anthropic import AsyncAnthropic
class AnthropicLLM: class AnthropicLLM:
"""Anthropic Claude LLM""" """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.model_name = model_name
self.client = AsyncAnthropic( 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]: async def decide(self, prompt: str) -> Dict[str, Any]:

View File

@@ -1,7 +1,7 @@
""" """
LLM Factory - LLM 工厂类 LLM Factory - LLM 工厂类
""" """
from typing import Dict, Any from typing import Optional
from app.agent.llm.openai import OpenAILLM from app.agent.llm.openai import OpenAILLM
from app.agent.llm.anthropic import AnthropicLLM from app.agent.llm.anthropic import AnthropicLLM
@@ -10,21 +10,23 @@ class LLMFactory:
"""LLM 工厂类""" """LLM 工厂类"""
@staticmethod @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 实例 创建 LLM 实例
Args: Args:
provider: 模型提供商 (openai/anthropic) provider: 模型提供商 (openai/anthropic)
model_name: 模型名称 model_name: 模型名称
api_key: API Key可选
base_url: Base URL可选
Returns: Returns:
LLM 实例 LLM 实例
""" """
if provider.lower() == "openai": if provider.lower() == "openai":
return OpenAILLM(model_name) return OpenAILLM(model_name, api_key, base_url)
elif provider.lower() == "anthropic": elif provider.lower() == "anthropic":
return AnthropicLLM(model_name) return AnthropicLLM(model_name, api_key)
else: else:
# 默认使用 OpenAI # 默认使用 OpenAI
return OpenAILLM(model_name) return OpenAILLM(model_name, api_key, base_url)

View File

@@ -2,18 +2,31 @@
OpenAI LLM 实现 OpenAI LLM 实现
""" """
import os import os
import logging
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from openai import AsyncOpenAI from openai import AsyncOpenAI
logger = logging.getLogger("llm.openai")
class OpenAILLM: class OpenAILLM:
"""OpenAI LLM""" """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.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( self.client = AsyncOpenAI(
api_key=os.getenv("OPENAI_API_KEY", ""), api_key=self.api_key,
base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") base_url=self.base_url
) )
async def decide(self, prompt: str) -> Dict[str, Any]: async def decide(self, prompt: str) -> Dict[str, Any]:

View File

@@ -2,7 +2,10 @@
FastAPI Agent Engine Server FastAPI Agent Engine Server
""" """
import os import os
import sys
import time import time
import logging
from datetime import datetime
from typing import Optional from typing import Optional
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
@@ -12,6 +15,90 @@ from app.agent.core import AgentCore, Supervisor, AgentConfig
from app.agent.llm import LLMFactory 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") app = FastAPI(title="X-Agents Python Engine", version="1.0.0")
# CORS # CORS
@@ -32,6 +119,12 @@ class ChatRequest(BaseModel):
message: str message: str
user_id: int = 1 user_id: int = 1
session_id: Optional[str] = None 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): 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) agent_data = _mock_agents.get(agent_id)
if not agent_data: if not agent_data:
@@ -88,6 +181,8 @@ def get_agent_config(agent_id: int) -> AgentConfig:
role_description=agent_data["role_description"], role_description=agent_data["role_description"],
model_provider=agent_data["model_provider"], model_provider=agent_data["model_provider"],
model_name=agent_data["model_name"], model_name=agent_data["model_name"],
api_key=api_key,
base_url=base_url,
skills=agent_data.get("skills", []) 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() start_time = time.time()
# 获取智能体配置 # 获取智能体配置
try: try:
config = get_agent_config(request.agent_id) config = get_agent_config(request.agent_id, request.api_key, request.base_url)
except HTTPException: 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 raise
except Exception as e: 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)) 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) agent = AgentCore(config)
@@ -129,10 +250,15 @@ async def chat(request: ChatRequest):
try: try:
result = await agent.run(request.message, request.user_id, session_id) result = await agent.run(request.message, request.user_id, session_id)
except Exception as e: 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)) raise HTTPException(status_code=500, detail=str(e))
duration_ms = int((time.time() - start_time) * 1000) 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( return ChatResponse(
agent_id=request.agent_id, agent_id=request.agent_id,
response=result.content, response=result.content,

View File

@@ -296,7 +296,7 @@ func main() {
toolService := service.NewToolService(toolRepo) toolService := service.NewToolService(toolRepo)
mcpService := service.NewMCPService(mcpRepo) mcpService := service.NewMCPService(mcpRepo)
skillService := service.NewSkillService(skillRepo) skillService := service.NewSkillService(skillRepo)
agentService := service.NewAgentService(cfg.PythonServiceURL) agentService := service.NewAgentService(cfg.PythonServiceURL, modelRepo)
memoryService := service.NewMemoryService(agentRepo) memoryService := service.NewMemoryService(agentRepo)
// 4.2 初始化默认工具 // 4.2 初始化默认工具

View File

@@ -25,6 +25,7 @@ type ChatRequest struct {
AgentID int `json:"agent_id" binding:"required"` AgentID int `json:"agent_id" binding:"required"`
Message string `json:"message" binding:"required"` Message string `json:"message" binding:"required"`
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
ModelID string `json:"model_id"`
} }
// ChatResponse 对话响应 // ChatResponse 对话响应
@@ -54,6 +55,7 @@ func (h *AgentHandler) Chat(c *gin.Context) {
Message: req.Message, Message: req.Message,
UserID: userID, UserID: userID,
SessionID: req.SessionID, SessionID: req.SessionID,
ModelID: req.ModelID,
} }
result, err := h.agentService.Chat(pythonReq) result, err := h.agentService.Chat(pythonReq)

View File

@@ -105,6 +105,7 @@ type AgentRequest struct {
AgentID string `json:"agent_id" binding:"required"` AgentID string `json:"agent_id" binding:"required"`
Message string `json:"message" binding:"required"` Message string `json:"message" binding:"required"`
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
ModelID string `json:"model_id"`
Context map[string]interface{} `json:"context"` Context map[string]interface{} `json:"context"`
} }

View File

@@ -5,16 +5,24 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"time" "time"
"x-agents/server/internal/repository"
) )
// AgentChatRequest Python Agent 对话请求 // AgentChatRequest Python Agent 对话请求
type AgentChatRequest struct { type AgentChatRequest struct {
AgentID int `json:"agent_id"` AgentID int `json:"agent_id"`
Message string `json:"message"` Message string `json:"message"`
UserID int `json:"user_id"` UserID int `json:"user_id"`
SessionID string `json:"session_id,omitempty"` 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 对话响应 // AgentChatResponse Python Agent 对话响应
@@ -51,20 +59,55 @@ type TeamChatResponse struct {
type AgentService struct { type AgentService struct {
pythonURL string pythonURL string
client *http.Client client *http.Client
modelRepo *repository.ModelRepository
} }
// NewAgentService 创建 Agent 服务 // NewAgentService 创建 Agent 服务
func NewAgentService(pythonURL string) *AgentService { func NewAgentService(pythonURL string, modelRepo *repository.ModelRepository) *AgentService {
return &AgentService{ return &AgentService{
pythonURL: pythonURL, pythonURL: pythonURL,
client: &http.Client{ client: &http.Client{
Timeout: 120 * time.Second, // Agent 可能需要较长时间 Timeout: 120 * time.Second, // Agent 可能需要较长时间
}, },
modelRepo: modelRepo,
} }
} }
// Chat 单智能体对话 // Chat 单智能体对话
func (s *AgentService) Chat(req AgentChatRequest) (*AgentChatResponse, error) { 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) url := fmt.Sprintf("%s/agent/chat", s.pythonURL)
jsonData, err := json.Marshal(req) jsonData, err := json.Marshal(req)

View File

@@ -16,13 +16,15 @@ import (
type ChatService struct { type ChatService struct {
pythonURL string 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{ return &ChatService{
pythonURL: pythonURL, pythonURL: pythonURL,
agentRepo: agentRepo, agentRepo: agentRepo,
modelRepo: modelRepo,
} }
} }
@@ -30,9 +32,19 @@ type ChatRequest struct {
AgentID string `json:"agent_id"` AgentID string `json:"agent_id"`
Message string `json:"message"` Message string `json:"message"`
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
ModelID string `json:"model_id"`
Context map[string]interface{} `json:"context"` 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 { type ChatResponse struct {
Reply string `json:"reply"` Reply string `json:"reply"`
SessionID string `json:"session_id"` 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() 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{ pythonReq := ChatRequest{
AgentID: req.AgentID, AgentID: req.AgentID,
Message: req.Message, Message: req.Message,
SessionID: sessionID, SessionID: sessionID,
ModelID: req.ModelID,
Context: req.Context, 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) pythonResp, err := s.callPythonChat(ctx, pythonReq)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to call python service: %w", err) return nil, fmt.Errorf("failed to call python service: %w", err)