forked from zhanggangyong/agent_management
Update: 更新代码和添加新功能
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
# Cursor MCP 连接配置指南
|
||||
|
||||
本文档说明如何配置 Cursor 以连接到 `code_ai_agent` 和 `facebook_agent` 的 MCP 服务器。
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. 确保两个 agent 已成功部署并运行
|
||||
2. 获取 agent 的访问域名(例如:`test-code-ai-agent-4.taijiagnet.com`)
|
||||
|
||||
## 配置步骤
|
||||
|
||||
### 1. 找到 Cursor MCP 配置文件
|
||||
|
||||
Cursor 的 MCP 配置文件通常位于:
|
||||
- **macOS**: `~/Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
|
||||
- **Windows**: `%APPDATA%\Cursor\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json`
|
||||
- **Linux**: `~/.config/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
|
||||
|
||||
### 2. 编辑配置文件
|
||||
|
||||
将以下内容添加到 Cursor 的 MCP 配置文件中:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"code-ai-agent": {
|
||||
"url": "http://test-code-ai-agent-4.taijiagnet.com/mcp",
|
||||
"type": "http",
|
||||
"description": "代码助手 Agent - 支持代码生成、重构、审查和组织功能"
|
||||
},
|
||||
"facebook-agent": {
|
||||
"url": "http://test-facebook-agent-6.taijiagnet.com/mcp",
|
||||
"type": "http",
|
||||
"description": "Facebook 搜索 Agent - 支持 Facebook 内容搜索"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 重启 Cursor
|
||||
|
||||
保存配置文件后,重启 Cursor 以使配置生效。
|
||||
|
||||
## 可用的工具
|
||||
|
||||
### code-ai-agent 工具列表
|
||||
|
||||
1. **generate_code** - 根据需求生成代码
|
||||
2. **refactor_code** - 重构代码,改进代码质量
|
||||
3. **review_code** - 审查代码,发现潜在问题
|
||||
4. **organize_code** - 智能分析代码并自动组织到合适的文件夹
|
||||
5. **classify_code** - 分析代码内容,确定其应该属于哪个类别
|
||||
6. **analyze_project** - 分析项目结构,提供项目概览和建议
|
||||
7. **suggest_folder_structure** - 根据项目描述,建议合理的文件夹结构
|
||||
8. **create_code_file** - 在指定文件夹中创建代码文件
|
||||
|
||||
### facebook-agent 工具列表
|
||||
|
||||
1. **search_facebook** - 搜索 Facebook 内容,返回相关帖子和 AI 生成的总结
|
||||
|
||||
## 测试连接
|
||||
|
||||
### 测试 code-ai-agent
|
||||
|
||||
```bash
|
||||
curl -X POST http://test-code-ai-agent-4.taijiagnet.com/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/list",
|
||||
"id": 1
|
||||
}'
|
||||
```
|
||||
|
||||
### 测试 facebook-agent
|
||||
|
||||
```bash
|
||||
curl -X POST http://test-facebook-agent-6.taijiagnet.com/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/list",
|
||||
"id": 1
|
||||
}'
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 在 Cursor 中使用 code-ai-agent
|
||||
|
||||
配置完成后,你可以在 Cursor 的聊天界面中直接使用这些工具。例如:
|
||||
|
||||
- "帮我生成一个 FastAPI 的 hello world 应用"
|
||||
- "审查这段代码:`[粘贴代码]`"
|
||||
- "重构这个函数以提高性能"
|
||||
- "分析当前项目的结构"
|
||||
|
||||
### 在 Cursor 中使用 facebook-agent
|
||||
|
||||
- "搜索 Facebook 上关于 AI 的最新内容"
|
||||
- "查找 Facebook 上关于旅游的帖子"
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 连接失败
|
||||
|
||||
1. 检查 agent 是否正在运行:
|
||||
```bash
|
||||
kubectl get pods -n agent-test-code-ai-agent-4
|
||||
kubectl get pods -n agent-test-facebook-agent-6
|
||||
```
|
||||
|
||||
2. 检查域名解析是否正常:
|
||||
```bash
|
||||
curl -I http://test-code-ai-agent-4.taijiagnet.com/health
|
||||
curl -I http://test-facebook-agent-6.taijiagnet.com/health
|
||||
```
|
||||
|
||||
3. 检查 MCP 端点是否可访问:
|
||||
```bash
|
||||
curl -X POST http://test-code-ai-agent-4.taijiagnet.com/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
|
||||
```
|
||||
|
||||
### 工具调用失败
|
||||
|
||||
1. 检查 agent 日志:
|
||||
```bash
|
||||
kubectl logs -n agent-test-code-ai-agent-4 test-code-ai-agent-4 --tail=50
|
||||
kubectl logs -n agent-test-facebook-agent-6 test-facebook-agent-6 --tail=50
|
||||
```
|
||||
|
||||
2. 确认环境变量配置正确(特别是 API keys 和模型名称)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **域名更新**:如果 agent 被重新部署,域名可能会改变,需要更新配置文件
|
||||
2. **网络访问**:确保 Cursor 可以访问 agent 的域名
|
||||
3. **HTTPS**:如果 agent 使用 HTTPS,需要将 URL 中的 `http://` 改为 `https://`
|
||||
|
||||
## 参考
|
||||
|
||||
- [MCP 协议文档](https://modelcontextprotocol.io/)
|
||||
- [Cursor MCP 配置文档](https://docs.cursor.com/)
|
||||
@@ -0,0 +1,20 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
RUN apt-get update && apt-get install -y gcc curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
CMD ["python", "run_api_server.py"]
|
||||
@@ -0,0 +1,67 @@
|
||||
# Agent 模板
|
||||
|
||||
基于 **Pydantic AI** 的轻量级 Agent 模板。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 复制模板
|
||||
|
||||
```bash
|
||||
cp -r _template your_agent_name
|
||||
cd your_agent_name
|
||||
# 全局替换 "your_agent" 为你的 agent 名称
|
||||
```
|
||||
|
||||
### 2. 修改核心文件
|
||||
|
||||
- `src/server/mcp_server.py` - 添加你的 MCP 工具
|
||||
- `src/server/api_server.py` - 添加你的 API 端点(可选)
|
||||
|
||||
### 3. 本地测试
|
||||
|
||||
```bash
|
||||
python run_api_server.py
|
||||
```
|
||||
|
||||
### 4. 构建镜像
|
||||
|
||||
```bash
|
||||
docker build -t your-agent:latest .
|
||||
```
|
||||
|
||||
### 5. 注册到 Agent Manager
|
||||
|
||||
在 `k8s_manager.py` 中添加:
|
||||
|
||||
```python
|
||||
# TEMPLATE_PORTS
|
||||
"your_agent": 8000,
|
||||
|
||||
# image_map
|
||||
"your_agent": "agnettaiji.azurecr.io/ai-agents/your-agent:latest",
|
||||
```
|
||||
|
||||
在 `app.py` 的 `valid_templates` 中添加 `"your_agent"`。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
your_agent/
|
||||
├── Dockerfile
|
||||
├── requirements.txt
|
||||
├── run_api_server.py # 启动脚本
|
||||
└── src/
|
||||
├── __init__.py
|
||||
└── server/
|
||||
├── __init__.py
|
||||
├── api_server.py # FastAPI + MCP HTTP
|
||||
└── mcp_server.py # MCP 工具定义
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 必需 | 说明 |
|
||||
|------|------|------|
|
||||
| LITELLM_GATEWAY_URL | 是 | LiteLLM Gateway URL |
|
||||
| LITELLM_MODEL | 否 | 模型名称,默认 taiji/gpt-4o-mini |
|
||||
| API_PORT | 否 | 端口,默认 8000 |
|
||||
@@ -0,0 +1,13 @@
|
||||
# Pydantic AI
|
||||
pydantic-ai>=0.0.14
|
||||
|
||||
# MCP
|
||||
mcp>=0.9.0
|
||||
fastmcp>=0.1.0
|
||||
|
||||
# FastAPI
|
||||
fastapi>=0.109.0
|
||||
uvicorn[standard]>=0.27.0
|
||||
|
||||
# HTTP Client
|
||||
aiohttp>=3.9.0
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
"""启动 API 服务器"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
if __name__ == '__main__':
|
||||
from src.server.api_server import app
|
||||
import uvicorn
|
||||
import os
|
||||
|
||||
host = os.getenv('API_HOST', '0.0.0.0')
|
||||
port = int(os.getenv('API_PORT', '8000'))
|
||||
|
||||
print(f"🚀 启动 Agent API: http://{host}:{port}")
|
||||
uvicorn.run(app, host=host, port=port, log_level="info")
|
||||
@@ -0,0 +1 @@
|
||||
"""Agent 源代码包"""
|
||||
@@ -0,0 +1 @@
|
||||
"""服务器模块"""
|
||||
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
HTTP API 服务器
|
||||
|
||||
提供 REST API 和 MCP HTTP/SSE 端点。
|
||||
"""
|
||||
import json
|
||||
import uuid
|
||||
import os
|
||||
from typing import Optional, Dict, Any, AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request, Header, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .mcp_server import TOOL_MAP, TOOL_LIST
|
||||
|
||||
# ==================== 配置 ====================
|
||||
|
||||
SERVER_NAME = "Your Agent API" # 修改为你的 Agent 名称
|
||||
|
||||
|
||||
# ==================== FastAPI 应用 ====================
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
print(f"🚀 {SERVER_NAME} 启动")
|
||||
yield
|
||||
print(f"🛑 {SERVER_NAME} 关闭")
|
||||
|
||||
app = FastAPI(
|
||||
title=SERVER_NAME,
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# ==================== API Key 验证 ====================
|
||||
|
||||
async def verify_api_key(
|
||||
api_key: Optional[str] = Header(None, alias="api-key"),
|
||||
authorization: Optional[str] = Header(None)
|
||||
) -> str:
|
||||
"""验证 API Key"""
|
||||
if api_key and api_key.strip() and api_key.strip() != "sk":
|
||||
return api_key.strip()
|
||||
|
||||
if authorization:
|
||||
key = authorization[7:].strip() if authorization.startswith("Bearer ") else authorization.strip()
|
||||
if key and key != "sk":
|
||||
return key
|
||||
|
||||
raise HTTPException(status_code=401, detail="缺少 API Key")
|
||||
|
||||
|
||||
def get_api_key_from_request(request: Request) -> Optional[str]:
|
||||
"""从请求头提取 API Key(不验证)"""
|
||||
api_key = request.headers.get("api-key") or request.headers.get("api_key")
|
||||
if not api_key:
|
||||
auth = request.headers.get("Authorization")
|
||||
if auth:
|
||||
api_key = auth[7:] if auth.startswith("Bearer ") else auth
|
||||
return api_key
|
||||
|
||||
|
||||
# ==================== 健康检查 ====================
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"service": SERVER_NAME,
|
||||
"status": "running",
|
||||
"tools": list(TOOL_MAP.keys())
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy", "service": SERVER_NAME}
|
||||
|
||||
|
||||
# ==================== MCP 端点 ====================
|
||||
|
||||
sessions: Dict[str, Dict] = {}
|
||||
|
||||
|
||||
async def handle_mcp_request(data: Dict, session_id: str = None, api_key: str = None) -> Dict:
|
||||
"""处理 MCP JSON-RPC 请求"""
|
||||
method = data.get("method")
|
||||
params = data.get("params", {})
|
||||
req_id = data.get("id")
|
||||
|
||||
# tools/call 需要验证 API Key
|
||||
if method == "tools/call" and (not api_key or api_key == "sk"):
|
||||
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32001, "message": "缺少 API Key"}}
|
||||
|
||||
try:
|
||||
if method == "initialize":
|
||||
session_id = session_id or str(uuid.uuid4())
|
||||
sessions[session_id] = {"initialized": True}
|
||||
return {
|
||||
"jsonrpc": "2.0", "id": req_id,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {"tools": {}},
|
||||
"serverInfo": {"name": SERVER_NAME, "version": "1.0.0"}
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "tools/list":
|
||||
return {"jsonrpc": "2.0", "id": req_id, "result": {"tools": TOOL_LIST}}
|
||||
|
||||
elif method == "tools/call":
|
||||
tool_name = params.get("name")
|
||||
args = params.get("arguments", {})
|
||||
|
||||
if tool_name not in TOOL_MAP:
|
||||
raise ValueError(f"Unknown tool: {tool_name}")
|
||||
|
||||
# 设置 API Key 到环境变量
|
||||
old_key = os.environ.get('OPENAI_API_KEY')
|
||||
if api_key:
|
||||
os.environ['OPENAI_API_KEY'] = api_key
|
||||
|
||||
try:
|
||||
result = await TOOL_MAP[tool_name](**args)
|
||||
finally:
|
||||
if old_key:
|
||||
os.environ['OPENAI_API_KEY'] = old_key
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0", "id": req_id,
|
||||
"result": {"content": [{"type": "text", "text": str(result)}]}
|
||||
}
|
||||
|
||||
elif method == "ping":
|
||||
return {"jsonrpc": "2.0", "id": req_id, "result": {}}
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown method: {method}")
|
||||
|
||||
except Exception as e:
|
||||
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32603, "message": str(e)}}
|
||||
|
||||
|
||||
@app.post("/mcp")
|
||||
async def mcp_endpoint(request: Request):
|
||||
"""MCP HTTP 端点"""
|
||||
try:
|
||||
body = await request.json()
|
||||
session_id = request.headers.get("x-mcp-session-id")
|
||||
api_key = get_api_key_from_request(request)
|
||||
response = await handle_mcp_request(body, session_id, api_key)
|
||||
return JSONResponse(content=response, headers={"x-mcp-session-id": session_id or ""})
|
||||
except Exception as e:
|
||||
return JSONResponse(status_code=400, content={"jsonrpc": "2.0", "error": {"code": -32700, "message": str(e)}})
|
||||
|
||||
|
||||
@app.get("/mcp/sse")
|
||||
async def mcp_sse(request: Request):
|
||||
"""MCP SSE 端点"""
|
||||
session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4())
|
||||
|
||||
async def stream() -> AsyncGenerator[str, None]:
|
||||
yield f"data: {json.dumps({'type': 'connection', 'sessionId': session_id})}\n\n"
|
||||
import asyncio
|
||||
while True:
|
||||
await asyncio.sleep(30)
|
||||
yield f"data: {json.dumps({'type': 'ping'})}\n\n"
|
||||
|
||||
return StreamingResponse(stream(), media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "x-mcp-session-id": session_id})
|
||||
|
||||
|
||||
@app.post("/mcp/sse")
|
||||
async def mcp_sse_post(request: Request):
|
||||
"""MCP SSE POST 端点"""
|
||||
try:
|
||||
body = await request.json()
|
||||
session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4())
|
||||
api_key = get_api_key_from_request(request)
|
||||
|
||||
async def stream() -> AsyncGenerator[str, None]:
|
||||
response = await handle_mcp_request(body, session_id, api_key)
|
||||
yield f"data: {json.dumps(response)}\n\n"
|
||||
|
||||
return StreamingResponse(stream(), media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "x-mcp-session-id": session_id})
|
||||
except Exception as e:
|
||||
return JSONResponse(status_code=400, content={"jsonrpc": "2.0", "error": {"code": -32700, "message": str(e)}})
|
||||
|
||||
|
||||
# ==================== 业务 API(可选)====================
|
||||
|
||||
class QueryRequest(BaseModel):
|
||||
"""请求模型"""
|
||||
query: str = Field(..., description="查询内容")
|
||||
option: Optional[str] = Field(None, description="可选参数")
|
||||
|
||||
|
||||
class QueryResponse(BaseModel):
|
||||
"""响应模型"""
|
||||
success: bool
|
||||
result: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@app.post("/api/v1/query", response_model=QueryResponse)
|
||||
async def api_query(request: QueryRequest, api_key: str = Depends(verify_api_key)):
|
||||
"""业务 API 端点(示例)"""
|
||||
try:
|
||||
# 设置 API Key
|
||||
old_key = os.environ.get('OPENAI_API_KEY')
|
||||
os.environ['OPENAI_API_KEY'] = api_key
|
||||
|
||||
try:
|
||||
result = await TOOL_MAP['your_tool'](query=request.query, option=request.option)
|
||||
return QueryResponse(success=True, result=result)
|
||||
finally:
|
||||
if old_key:
|
||||
os.environ['OPENAI_API_KEY'] = old_key
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
MCP 服务器 - 定义 Agent 工具
|
||||
|
||||
使用 Pydantic AI 和 FastMCP 框架。
|
||||
在此文件中添加你的 MCP 工具。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from pydantic_ai import Agent
|
||||
|
||||
# ==================== 配置 ====================
|
||||
|
||||
# LiteLLM Gateway 配置
|
||||
_BASE_URL = os.getenv('OPENAI_BASE_URL',
|
||||
os.getenv('LLM_BASE_URL', 'https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1'))
|
||||
_API_KEY = os.getenv('OPENAI_API_KEY', 'sk')
|
||||
|
||||
os.environ.setdefault('OPENAI_API_KEY', _API_KEY)
|
||||
os.environ.setdefault('OPENAI_BASE_URL', _BASE_URL)
|
||||
|
||||
# 模型名称(pydantic_ai 需要 openai: 前缀)
|
||||
def _get_model_name() -> str:
|
||||
model = os.getenv('MODEL_NAME', os.getenv('LITELLM_MODEL', 'taiji/gpt-4o-mini'))
|
||||
return model if ':' in model else f'openai:{model}'
|
||||
|
||||
MODEL_NAME = _get_model_name()
|
||||
|
||||
# ==================== MCP 服务器 ====================
|
||||
|
||||
server = FastMCP('Your Agent') # 修改为你的 Agent 名称
|
||||
|
||||
# 系统提示词
|
||||
SYSTEM_PROMPT = '''你是一个专业的 AI 助手。
|
||||
请根据用户的需求提供帮助。'''
|
||||
|
||||
|
||||
def get_agent() -> Agent:
|
||||
"""创建 Agent 实例(每次调用使用最新的 API Key)"""
|
||||
return Agent(MODEL_NAME, system_prompt=SYSTEM_PROMPT)
|
||||
|
||||
|
||||
# ==================== MCP 工具定义 ====================
|
||||
|
||||
@server.tool()
|
||||
async def your_tool(
|
||||
query: str,
|
||||
option: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
你的工具描述
|
||||
|
||||
Args:
|
||||
query: 查询内容
|
||||
option: 可选参数
|
||||
|
||||
Returns:
|
||||
处理结果(JSON 格式)
|
||||
"""
|
||||
try:
|
||||
# 1. 调用 AI Agent 处理
|
||||
result = await get_agent().run(f"请处理: {query}")
|
||||
|
||||
# 2. 返回结果
|
||||
return json.dumps({
|
||||
"success": True,
|
||||
"query": query,
|
||||
"result": result.output
|
||||
}, ensure_ascii=False, indent=2)
|
||||
|
||||
except Exception as e:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
# 添加更多工具...
|
||||
# @server.tool()
|
||||
# async def another_tool(...) -> str:
|
||||
# pass
|
||||
|
||||
|
||||
# ==================== 工具映射(供 API 使用)====================
|
||||
|
||||
TOOL_MAP = {
|
||||
'your_tool': your_tool,
|
||||
}
|
||||
|
||||
TOOL_LIST = [
|
||||
{
|
||||
"name": "your_tool",
|
||||
"description": "你的工具描述",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "查询内容"},
|
||||
"option": {"type": "string", "description": "可选参数"}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
server.run()
|
||||
@@ -0,0 +1,38 @@
|
||||
# Cursor MCP 服务器配置
|
||||
# 将此配置添加到 Cursor 的 MCP 设置中
|
||||
|
||||
# 方式1: 本地 stdio 调用(推荐用于本地开发)
|
||||
# {
|
||||
# "mcpServers": {
|
||||
# "code-assistant": {
|
||||
# "command": "python",
|
||||
# "args": ["/home/taiji/tools/Pydantic-ai/run_mcp_server.py", "--transport", "stdio"],
|
||||
# "env": {
|
||||
# "OPENAI_API_KEY": "your_api_key",
|
||||
# "OPENAI_BASE_URL": "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1",
|
||||
# "LITELLM_MODEL": "openai:taiji/gpt-4o-mini"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
# 方式2: 远程 HTTP 调用(部署到云虚拟机后使用)
|
||||
# {
|
||||
# "mcpServers": {
|
||||
# "code-assistant": {
|
||||
# "url": "http://your-vm-ip:8001/mcp",
|
||||
# "type": "http"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
# 方式3: 远程 SSE 调用(部署到云虚拟机后使用)
|
||||
# {
|
||||
# "mcpServers": {
|
||||
# "code-assistant": {
|
||||
# "url": "http://your-vm-ip:8001/mcp/sse",
|
||||
# "type": "sse"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
.venv
|
||||
venv/
|
||||
env/
|
||||
*.egg-info
|
||||
dist/
|
||||
build/
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
.DS_Store
|
||||
test_*.py
|
||||
*.log
|
||||
@@ -0,0 +1,39 @@
|
||||
# Dockerfile for 代码助手 Agent 服务
|
||||
FROM python:3.12-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 设置环境变量
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
# 创建项目存储目录
|
||||
RUN mkdir -p /tmp/projects
|
||||
|
||||
# 安装curl用于健康检查
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000 8001
|
||||
|
||||
# 健康检查(默认检查8000端口,MCP服务会覆盖)
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
# 启动命令(默认启动API服务器,可通过command覆盖)
|
||||
CMD ["python", "run_api_server.py"]
|
||||
@@ -0,0 +1,167 @@
|
||||
# 项目结构说明
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
Pydantic-ai/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── __init__.py # 包初始化文件
|
||||
│ ├── server/ # 服务器模块
|
||||
│ │ ├── __init__.py # 导出服务器相关功能
|
||||
│ │ ├── mcp_server.py # MCP 服务器(MCP 协议)
|
||||
│ │ └── api_server.py # HTTP API 服务器(FastAPI)
|
||||
│ └── client/ # 客户端模块
|
||||
│ ├── __init__.py
|
||||
│ └── mcp_client.py # MCP 客户端示例
|
||||
│
|
||||
├── tests/ # 测试目录
|
||||
│ ├── __init__.py
|
||||
│ ├── test_agent.py # Agent 功能测试
|
||||
│ └── test_mcp_server.py # MCP 服务器测试
|
||||
│
|
||||
├── docs/ # 文档目录
|
||||
│ ├── API_DOCUMENTATION.md # API 接口文档
|
||||
│ ├── DEPLOYMENT.md # 部署指南
|
||||
│ └── README_API.md # API 快速开始
|
||||
│
|
||||
├── run_api_server.py # HTTP API 服务器启动脚本
|
||||
├── run_mcp_server.py # MCP 服务器启动脚本
|
||||
│
|
||||
├── requirements.txt # Python 依赖
|
||||
├── Dockerfile # Docker 镜像配置
|
||||
├── docker-compose.yml # Docker Compose 配置
|
||||
├── .dockerignore # Docker 构建忽略文件
|
||||
├── README.md # 项目说明
|
||||
└── PROJECT_STRUCTURE.md # 本文件
|
||||
```
|
||||
|
||||
## 📝 文件说明
|
||||
|
||||
### 源代码 (`src/`)
|
||||
|
||||
#### `src/server/`
|
||||
- **`mcp_server.py`**: MCP 服务器实现,包含所有工具函数和 Agent 配置
|
||||
- **`api_server.py`**: HTTP API 服务器,将 MCP 工具包装为 REST API
|
||||
|
||||
#### `src/client/`
|
||||
- **`mcp_client.py`**: MCP 客户端示例,演示如何通过 MCP 协议调用服务
|
||||
|
||||
### 测试 (`tests/`)
|
||||
|
||||
- **`test_agent.py`**: 直接测试 Agent 功能
|
||||
- **`test_mcp_server.py`**: 测试 MCP 服务器工具函数
|
||||
|
||||
### 文档 (`docs/`)
|
||||
|
||||
- **`API_DOCUMENTATION.md`**: 完整的 API 接口文档
|
||||
- **`DEPLOYMENT.md`**: 部署指南和配置说明
|
||||
- **`README_API.md`**: API 快速开始指南
|
||||
|
||||
### 启动脚本
|
||||
|
||||
- **`run_api_server.py`**: 启动 HTTP API 服务器的入口脚本
|
||||
- **`run_mcp_server.py`**: 启动 MCP 服务器的入口脚本
|
||||
|
||||
## 🔧 导入路径
|
||||
|
||||
### 在项目内部导入
|
||||
|
||||
```python
|
||||
# 从服务器模块导入工具函数
|
||||
from src.server.mcp_server import (
|
||||
generate_code,
|
||||
refactor_code,
|
||||
review_code,
|
||||
# ...
|
||||
)
|
||||
|
||||
# 或者使用 __init__.py 中的导出
|
||||
from src.server import (
|
||||
generate_code,
|
||||
refactor_code,
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
### 在测试中导入
|
||||
|
||||
```python
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.server.mcp_server import code_assistant_agent
|
||||
```
|
||||
|
||||
## 🚀 运行方式
|
||||
|
||||
### 本地运行
|
||||
|
||||
```bash
|
||||
# 启动 HTTP API 服务器
|
||||
python run_api_server.py
|
||||
|
||||
# 启动 MCP 服务器
|
||||
python run_mcp_server.py
|
||||
|
||||
# 运行测试
|
||||
python -m pytest tests/
|
||||
# 或
|
||||
python tests/test_agent.py
|
||||
```
|
||||
|
||||
### Docker 运行
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t code-assistant-agent .
|
||||
|
||||
# 运行容器
|
||||
docker run -p 8000:8000 code-assistant-agent
|
||||
|
||||
# 或使用 Docker Compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 📦 模块组织原则
|
||||
|
||||
1. **按功能分类**: 服务器、客户端、测试分别放在不同目录
|
||||
2. **清晰的导入路径**: 使用 `src.server`、`src.client` 等清晰的模块路径
|
||||
3. **统一的入口**: 使用根目录的启动脚本,方便运行
|
||||
4. **文档集中**: 所有文档放在 `docs/` 目录
|
||||
|
||||
## 🔄 导入关系
|
||||
|
||||
```
|
||||
run_api_server.py
|
||||
└─> src.server.api_server
|
||||
└─> src.server.mcp_server (工具函数)
|
||||
|
||||
run_mcp_server.py
|
||||
└─> src.server.mcp_server
|
||||
|
||||
src.client.mcp_client
|
||||
└─> (通过命令行调用) src.server.mcp_server
|
||||
|
||||
tests/test_*.py
|
||||
└─> src.server.mcp_server
|
||||
```
|
||||
|
||||
## ✅ 重构完成检查清单
|
||||
|
||||
- [x] 创建目录结构
|
||||
- [x] 移动文件到对应目录
|
||||
- [x] 更新所有导入语句
|
||||
- [x] 创建 `__init__.py` 文件
|
||||
- [x] 更新 Dockerfile 路径
|
||||
- [x] 创建启动脚本
|
||||
- [x] 更新测试文件导入
|
||||
- [x] 更新客户端文件路径引用
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2025-01-08
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Pydantic AI Agents
|
||||
|
||||
使用 Pydantic AI 创建的两个示例 Agent:天气查询 Agent 和搜索 Agent。
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
# 激活虚拟环境
|
||||
source .venv/bin/activate
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 1. 天气查询 Agent
|
||||
|
||||
### 配置 API Key(可选)
|
||||
|
||||
如果要使用真实的天气 API(OpenWeatherMap),需要设置环境变量:
|
||||
|
||||
```bash
|
||||
export WEATHER_API_KEY=your_api_key_here
|
||||
```
|
||||
|
||||
获取免费 API Key:https://openweathermap.org/api
|
||||
|
||||
如果不设置 API Key,Agent 会返回模拟数据用于演示。
|
||||
|
||||
### 运行
|
||||
|
||||
```bash
|
||||
python weather_agent.py
|
||||
```
|
||||
|
||||
### 使用方式
|
||||
|
||||
Agent 支持以下类型的查询:
|
||||
- "北京今天的天气怎么样?"
|
||||
- "请查询上海的天气"
|
||||
- "What's the weather in Tokyo?"
|
||||
|
||||
Agent 会自动调用天气查询工具,获取并返回天气信息。
|
||||
|
||||
## 2. 搜索 Agent(A2A 模式)
|
||||
|
||||
### 配置
|
||||
|
||||
搜索 Agent 使用以下服务:
|
||||
|
||||
1. **LiteLLM 服务器**(已配置在代码中):
|
||||
- 服务器地址:`https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1`
|
||||
- API Key:已内置在代码中
|
||||
- 模型:`taiji/gpt-4o-mini`
|
||||
|
||||
也可以通过环境变量自定义:
|
||||
```bash
|
||||
export OPENAI_BASE_URL=https://your-litellm-server.com/v1
|
||||
export OPENAI_API_KEY=your_litellm_api_key
|
||||
export LITELLM_MODEL=your_model_name
|
||||
```
|
||||
|
||||
2. **Serper.dev 搜索 API**(已内置在代码中):
|
||||
```bash
|
||||
export SERPER_API_KEY=your_api_key_here # 可选,代码中已有默认值
|
||||
```
|
||||
|
||||
### 运行
|
||||
|
||||
```bash
|
||||
python search_agent.py
|
||||
```
|
||||
|
||||
### 功能特性
|
||||
|
||||
- **搜索 Agent**:直接执行网络搜索,返回结构化搜索结果
|
||||
- **协调 Agent(A2A 模式)**:接收用户查询,委托搜索 Agent 执行搜索,并整理结果返回给用户
|
||||
|
||||
### 使用方式
|
||||
|
||||
Agent 支持以下类型的查询:
|
||||
- "苹果公司最新新闻"
|
||||
- "Python 异步编程最佳实践"
|
||||
- "What is machine learning?"
|
||||
- "请帮我搜索人工智能的最新发展"
|
||||
|
||||
### A2A(Agent-to-Agent)通信
|
||||
|
||||
搜索 Agent 演示了 A2A 通信模式:
|
||||
1. **协调 Agent** 接收用户查询
|
||||
2. **协调 Agent** 调用搜索工具(内部使用搜索 Agent)
|
||||
3. **搜索工具** 返回结果给协调 Agent
|
||||
4. **协调 Agent** 整理并返回给用户
|
||||
|
||||
这种模式允许多个 Agent 协作完成复杂任务。
|
||||
|
||||
### 测试结果
|
||||
|
||||
运行 `python search_agent.py` 后,Agent 会:
|
||||
1. 连接到 LiteLLM 服务器
|
||||
2. 使用 serper.dev API 执行网络搜索
|
||||
3. 整合搜索结果并返回给用户
|
||||
4. 展示 A2A 通信流程
|
||||
|
||||
测试输出显示 Agent 成功调用了搜索工具,并能够处理中英文查询。
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# 代码重构总结
|
||||
|
||||
## ✅ 重构完成
|
||||
|
||||
项目已成功重构,按照功能将 Python 文件分类到不同文件夹,保持目录整洁。
|
||||
|
||||
## 📁 新的目录结构
|
||||
|
||||
```
|
||||
Pydantic-ai/
|
||||
├── src/ # 源代码
|
||||
│ ├── server/ # 服务器模块
|
||||
│ │ ├── mcp_server.py # MCP 服务器
|
||||
│ │ └── api_server.py # HTTP API 服务器
|
||||
│ └── client/ # 客户端模块
|
||||
│ └── mcp_client.py # MCP 客户端示例
|
||||
│
|
||||
├── tests/ # 测试
|
||||
│ ├── test_agent.py
|
||||
│ └── test_mcp_server.py
|
||||
│
|
||||
├── docs/ # 文档
|
||||
│ ├── API_DOCUMENTATION.md
|
||||
│ ├── DEPLOYMENT.md
|
||||
│ └── README_API.md
|
||||
│
|
||||
├── run_api_server.py # HTTP API 启动脚本
|
||||
├── run_mcp_server.py # MCP 服务器启动脚本
|
||||
├── requirements.txt
|
||||
├── Dockerfile
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
||||
## 🔄 导入路径更新
|
||||
|
||||
### 已更新的文件
|
||||
|
||||
1. **`src/server/api_server.py`**
|
||||
- ✅ 更新:从 `import mcp_server` 改为 `from .mcp_server import ...`
|
||||
- ✅ 使用相对导入,更符合 Python 包结构
|
||||
|
||||
2. **`src/client/mcp_client.py`**
|
||||
- ✅ 更新:使用 `Path` 动态获取 `mcp_server.py` 路径
|
||||
- ✅ 支持从任何位置运行
|
||||
|
||||
3. **`tests/test_mcp_server.py`**
|
||||
- ✅ 更新:从 `from mcp_server import` 改为 `from src.server.mcp_server import`
|
||||
- ✅ 添加项目根目录到 Python 路径
|
||||
|
||||
4. **`Dockerfile`**
|
||||
- ✅ 更新:启动命令改为 `python run_api_server.py`
|
||||
- ✅ 使用统一的启动脚本
|
||||
|
||||
## 📝 新增文件
|
||||
|
||||
1. **`run_api_server.py`** - HTTP API 服务器启动脚本
|
||||
2. **`run_mcp_server.py`** - MCP 服务器启动脚本
|
||||
3. **`src/__init__.py`** - 源代码包初始化
|
||||
4. **`src/server/__init__.py`** - 服务器模块导出
|
||||
5. **`src/client/__init__.py`** - 客户端模块初始化
|
||||
6. **`tests/__init__.py`** - 测试模块初始化
|
||||
7. **`PROJECT_STRUCTURE.md`** - 项目结构说明文档
|
||||
|
||||
## 🚀 使用方式
|
||||
|
||||
### 启动 HTTP API 服务器
|
||||
|
||||
```bash
|
||||
# 方式 1: 使用启动脚本(推荐)
|
||||
python run_api_server.py
|
||||
|
||||
# 方式 2: 直接运行模块
|
||||
python -m src.server.api_server
|
||||
```
|
||||
|
||||
### 启动 MCP 服务器
|
||||
|
||||
```bash
|
||||
# 方式 1: 使用启动脚本(推荐)
|
||||
python run_mcp_server.py
|
||||
|
||||
# 方式 2: 直接运行模块
|
||||
python -m src.server.mcp_server
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
python tests/test_agent.py
|
||||
python tests/test_mcp_server.py
|
||||
|
||||
# 或使用 pytest
|
||||
pytest tests/
|
||||
```
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```bash
|
||||
# 构建和运行(已更新路径)
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## ✅ 验证清单
|
||||
|
||||
- [x] 所有文件已移动到正确目录
|
||||
- [x] 所有导入语句已更新
|
||||
- [x] `__init__.py` 文件已创建
|
||||
- [x] 启动脚本已创建
|
||||
- [x] Dockerfile 路径已更新
|
||||
- [x] 测试文件导入已更新
|
||||
- [x] 客户端文件路径引用已更新
|
||||
- [x] 文档已移动到 docs/ 目录
|
||||
|
||||
## 📋 导入示例
|
||||
|
||||
### 在项目内部导入
|
||||
|
||||
```python
|
||||
# 从服务器模块导入工具函数
|
||||
from src.server.mcp_server import generate_code, refactor_code
|
||||
|
||||
# 或使用 __init__.py 中的导出
|
||||
from src.server import generate_code, refactor_code
|
||||
```
|
||||
|
||||
### 在测试中导入
|
||||
|
||||
```python
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.server.mcp_server import code_assistant_agent
|
||||
```
|
||||
|
||||
## 🎯 重构优势
|
||||
|
||||
1. **清晰的目录结构**: 按功能分类,易于维护
|
||||
2. **标准的 Python 包结构**: 符合 Python 最佳实践
|
||||
3. **易于扩展**: 新增功能可以轻松添加到对应目录
|
||||
4. **导入路径清晰**: 使用 `src.server`、`src.client` 等明确路径
|
||||
5. **统一的启动方式**: 使用根目录启动脚本,方便运行
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md) - 详细的项目结构说明
|
||||
- [docs/API_DOCUMENTATION.md](./docs/API_DOCUMENTATION.md) - API 接口文档
|
||||
- [docs/DEPLOYMENT.md](./docs/DEPLOYMENT.md) - 部署指南
|
||||
|
||||
---
|
||||
|
||||
**重构完成时间**: 2025-01-08
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"code-assistant-stdio": {
|
||||
"command": "python",
|
||||
"args": ["/home/taiji/tools/Pydantic-ai/run_mcp_server.py", "--transport", "stdio"],
|
||||
"env": {
|
||||
"OPENAI_API_KEY": "sk-",
|
||||
"OPENAI_BASE_URL": "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1",
|
||||
"LITELLM_MODEL": "openai:taiji/gpt-4o-mini"
|
||||
}
|
||||
},
|
||||
"code-assistant-http": {
|
||||
"url": "http://your-vm-ip:8001/mcp",
|
||||
"type": "http",
|
||||
"headers": {
|
||||
"Authorization": "Bearer your-token-if-needed"
|
||||
}
|
||||
},
|
||||
"code-assistant-sse": {
|
||||
"url": "http://your-vm-ip:8001/mcp/sse",
|
||||
"type": "sse"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
code-assistant-api:
|
||||
build: .
|
||||
container_name: code-assistant-agent
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-rxegkFOciNmQLhOHr3qP3A}
|
||||
- OPENAI_BASE_URL=${OPENAI_BASE_URL:-https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1}
|
||||
- LITELLM_MODEL=${LITELLM_MODEL:-openai:taiji/gpt-4o-mini}
|
||||
- API_HOST=0.0.0.0
|
||||
- API_PORT=8000
|
||||
volumes:
|
||||
- ./projects:/tmp/projects
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
code-assistant-mcp:
|
||||
build: .
|
||||
container_name: code-assistant-mcp
|
||||
ports:
|
||||
- "8001:8001"
|
||||
environment:
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-rxegkFOciNmQLhOHr3qP3A}
|
||||
- OPENAI_BASE_URL=${OPENAI_BASE_URL:-https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1}
|
||||
- LITELLM_MODEL=${LITELLM_MODEL:-openai:taiji/gpt-4o-mini}
|
||||
- MCP_HOST=0.0.0.0
|
||||
- MCP_PORT=8001
|
||||
volumes:
|
||||
- ./projects:/tmp/projects
|
||||
restart: unless-stopped
|
||||
command: python run_mcp_server.py --transport http --host 0.0.0.0 --port 8001
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8001/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
@@ -0,0 +1,638 @@
|
||||
# 代码助手 Agent API 接口文档
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [概述](#概述)
|
||||
- [快速开始](#快速开始)
|
||||
- [部署方式](#部署方式)
|
||||
- [API 端点](#api-端点)
|
||||
- [健康检查](#健康检查)
|
||||
- [代码生成](#代码生成)
|
||||
- [代码重构](#代码重构)
|
||||
- [代码审查](#代码审查)
|
||||
- [代码组织](#代码组织)
|
||||
- [代码分类](#代码分类)
|
||||
- [项目分析](#项目分析)
|
||||
- [项目结构建议](#项目结构建议)
|
||||
- [创建文件](#创建文件)
|
||||
- [请求/响应格式](#请求响应格式)
|
||||
- [错误处理](#错误处理)
|
||||
- [使用示例](#使用示例)
|
||||
- [MCP 协议调用](#mcp-协议调用)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
代码助手 Agent 是一个基于 **MCP (Model Context Protocol)** 框架的代码助手服务,提供代码生成、重构、审查和组织功能。
|
||||
|
||||
### 服务架构
|
||||
|
||||
```
|
||||
用户请求 → HTTP API (FastAPI) → MCP 工具函数 → Pydantic AI Agent → LiteLLM Gateway
|
||||
```
|
||||
|
||||
### 主要特性
|
||||
|
||||
- ✨ **代码生成**:根据自然语言需求生成高质量代码
|
||||
- 🔧 **代码重构**:改进代码质量、性能和可维护性
|
||||
- 🔍 **代码审查**:发现潜在问题、bug 和安全漏洞
|
||||
- 📁 **智能组织**:根据代码功能自动分类到合适文件夹
|
||||
- 📊 **项目分析**:分析项目结构并提供改进建议
|
||||
|
||||
### 技术栈
|
||||
|
||||
- **框架**: FastAPI + MCP (Model Context Protocol)
|
||||
- **AI 引擎**: Pydantic AI
|
||||
- **模型**: taiji/gpt-4o-mini (通过 litellm gateway)
|
||||
- **协议**: HTTP REST API + MCP Protocol
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 本地运行
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 启动 HTTP API 服务
|
||||
python api_server.py
|
||||
|
||||
# 服务将在 http://localhost:8000 启动
|
||||
# API 文档: http://localhost:8000/docs
|
||||
```
|
||||
|
||||
### 2. Docker 部署
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t code-assistant-agent .
|
||||
|
||||
# 运行容器
|
||||
docker run -d \
|
||||
-p 8000:8000 \
|
||||
-e OPENAI_API_KEY=your_api_key \
|
||||
-e OPENAI_BASE_URL=your_gateway_url \
|
||||
code-assistant-agent
|
||||
```
|
||||
|
||||
### 3. Docker Compose 部署
|
||||
|
||||
```bash
|
||||
# 启动服务
|
||||
docker-compose up -d
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `OPENAI_API_KEY` | LiteLLM Gateway API Key | `sk-rxegkFOciNmQLhOHr3qP3A` |
|
||||
| `OPENAI_BASE_URL` | LiteLLM Gateway Base URL | `https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1` |
|
||||
| `LITELLM_MODEL` | 模型名称 | `openai:taiji/gpt-4o-mini` |
|
||||
| `API_HOST` | API 服务监听地址 | `0.0.0.0` |
|
||||
| `API_PORT` | API 服务端口 | `8000` |
|
||||
|
||||
### 云部署示例
|
||||
|
||||
#### Azure Container Apps
|
||||
|
||||
```bash
|
||||
az containerapp create \
|
||||
--name code-assistant-agent \
|
||||
--resource-group your-resource-group \
|
||||
--image your-registry/code-assistant-agent:latest \
|
||||
--target-port 8000 \
|
||||
--env-vars \
|
||||
OPENAI_API_KEY=your_key \
|
||||
OPENAI_BASE_URL=your_gateway_url
|
||||
```
|
||||
|
||||
#### AWS ECS / EKS
|
||||
|
||||
使用提供的 `Dockerfile` 构建镜像并部署到 ECS/EKS。
|
||||
|
||||
#### Google Cloud Run
|
||||
|
||||
```bash
|
||||
gcloud run deploy code-assistant-agent \
|
||||
--image gcr.io/your-project/code-assistant-agent \
|
||||
--platform managed \
|
||||
--set-env-vars OPENAI_API_KEY=your_key,OPENAI_BASE_URL=your_url
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 端点
|
||||
|
||||
### 基础信息
|
||||
|
||||
- **Base URL**: `http://your-domain:8000`
|
||||
- **API 版本**: `v1`
|
||||
- **API 前缀**: `/api/v1`
|
||||
- **文档地址**: `/docs` (Swagger UI)
|
||||
- **ReDoc 文档**: `/redoc`
|
||||
|
||||
### 统一响应格式
|
||||
|
||||
所有 API 响应使用统一格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"result": "响应内容"
|
||||
},
|
||||
"message": "操作成功",
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
错误响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": null,
|
||||
"message": null,
|
||||
"error": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 健康检查
|
||||
|
||||
#### GET `/health`
|
||||
|
||||
检查服务健康状态。
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"service": "代码助手 Agent API"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 代码生成
|
||||
|
||||
#### POST `/api/v1/generate-code`
|
||||
|
||||
根据自然语言需求生成代码。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"requirement": "创建一个用户认证服务类,包含登录、注册、密码验证功能",
|
||||
"language": "python",
|
||||
"style": "fastapi",
|
||||
"project_root": "/tmp/projects"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `requirement` | string | ✅ | 代码需求描述 |
|
||||
| `language` | string | ❌ | 编程语言,默认 `python` |
|
||||
| `style` | string | ❌ | 代码风格,如 `fastapi`, `django` |
|
||||
| `project_root` | string | ❌ | 项目根目录,默认 `/tmp/projects` |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"result": "✅ 代码生成成功!\n\n📁 文件夹: /tmp/projects/services\n📄 文件名: auth_service.py\n..."
|
||||
},
|
||||
"message": "代码生成成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 代码重构
|
||||
|
||||
#### POST `/api/v1/refactor-code`
|
||||
|
||||
重构代码,改进代码质量。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"code_content": "def get_user(id):\n users = {1: {'name': 'Alice'}}\n return users[id]",
|
||||
"refactoring_goal": "添加错误处理和类型提示"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `code_content` | string | ✅ | 要重构的代码 |
|
||||
| `refactoring_goal` | string | ❌ | 重构目标 |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"result": "✅ 代码重构完成!\n\n📝 重构后的代码:\n```python\n..."
|
||||
},
|
||||
"message": "代码重构成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 代码审查
|
||||
|
||||
#### POST `/api/v1/review-code`
|
||||
|
||||
审查代码,发现问题和改进建议。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"code_content": "def process_data(data):\n result = []\n for i in range(len(data)):\n if data[i] > 0:\n result.append(data[i] * 2)\n return result",
|
||||
"file_path": "utils/processors.py"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `code_content` | string | ✅ | 要审查的代码 |
|
||||
| `file_path` | string | ❌ | 文件路径(可选) |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"result": "### 1. 潜在的 Bug\n- **索引访问**: 如果 data 为空...\n\n### 2. 代码质量问题\n..."
|
||||
},
|
||||
"message": "代码审查完成"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 代码组织
|
||||
|
||||
#### POST `/api/v1/organize-code`
|
||||
|
||||
智能分析代码并自动组织到合适的文件夹。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"code_content": "from datetime import datetime\n\ndef format_timestamp(ts):\n return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')",
|
||||
"code_type": "utils",
|
||||
"project_root": "/tmp/projects"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `code_content` | string | ✅ | 要组织的代码 |
|
||||
| `code_type` | string | ❌ | 代码类型提示 |
|
||||
| `project_root` | string | ❌ | 项目根目录 |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"result": "✅ 代码已成功组织!\n\n📁 文件夹路径: /tmp/projects/utils\n..."
|
||||
},
|
||||
"message": "代码组织成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 代码分类
|
||||
|
||||
#### POST `/api/v1/classify-code`
|
||||
|
||||
分析代码内容,确定其应该属于哪个类别。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"code_content": "def calculate_total(items):\n total = sum(item['price'] * item['quantity'] for item in items)\n return total"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `code_content` | string | ✅ | 要分类的代码 |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"result": "代码分析结果:\n\n1. **主要功能**: 计算商品总价..."
|
||||
},
|
||||
"message": "代码分类完成"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 项目分析
|
||||
|
||||
#### POST `/api/v1/analyze-project`
|
||||
|
||||
分析项目结构,提供项目概览和改进建议。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"project_root": "/tmp/projects",
|
||||
"max_depth": 3
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `project_root` | string | ❌ | 项目根目录,默认 `/tmp/projects` |
|
||||
| `max_depth` | integer | ❌ | 最大扫描深度,默认 `3` |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"result": "📊 项目分析报告\n\n📁 项目路径: /tmp/projects\n..."
|
||||
},
|
||||
"message": "项目分析完成"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 项目结构建议
|
||||
|
||||
#### POST `/api/v1/suggest-structure`
|
||||
|
||||
根据项目描述,建议合理的文件夹结构。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"project_description": "一个微服务架构的电商系统,包含用户服务、商品服务、订单服务和支付服务"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `project_description` | string | ✅ | 项目描述 |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"result": "推荐的项目结构:\n\n```\necommerce-system/\n├── services/\n..."
|
||||
},
|
||||
"message": "结构建议生成成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 创建文件
|
||||
|
||||
#### POST `/api/v1/create-file`
|
||||
|
||||
在指定文件夹中创建代码文件。
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"code_content": "from pydantic import BaseModel\n\nclass User(BaseModel):\n id: int\n name: str",
|
||||
"folder_path": "models",
|
||||
"file_name": "user.py",
|
||||
"project_root": "/tmp/projects"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `code_content` | string | ✅ | 代码内容 |
|
||||
| `folder_path` | string | ✅ | 目标文件夹路径 |
|
||||
| `file_name` | string | ✅ | 文件名 |
|
||||
| `project_root` | string | ❌ | 项目根目录 |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"result": "✅ 文件创建成功!\n\n📁 文件夹: /tmp/projects/models\n..."
|
||||
},
|
||||
"message": "文件创建成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 请求/响应格式
|
||||
|
||||
### HTTP 状态码
|
||||
|
||||
- `200 OK`: 请求成功
|
||||
- `400 Bad Request`: 请求参数错误
|
||||
- `500 Internal Server Error`: 服务器内部错误
|
||||
|
||||
### Content-Type
|
||||
|
||||
- 请求: `application/json`
|
||||
- 响应: `application/json`
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "错误描述信息"
|
||||
}
|
||||
```
|
||||
|
||||
### 常见错误
|
||||
|
||||
1. **400 Bad Request**: 请求参数缺失或格式错误
|
||||
2. **500 Internal Server Error**: 服务器内部错误(AI 调用失败、文件操作失败等)
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### Python 示例
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:8000"
|
||||
|
||||
# 生成代码
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/v1/generate-code",
|
||||
json={
|
||||
"requirement": "创建一个用户认证服务类",
|
||||
"language": "python",
|
||||
"style": "fastapi"
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
print(result["data"]["result"])
|
||||
|
||||
# 审查代码
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/v1/review-code",
|
||||
json={
|
||||
"code_content": "def func(): pass"
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
print(result["data"]["result"])
|
||||
```
|
||||
|
||||
### cURL 示例
|
||||
|
||||
```bash
|
||||
# 生成代码
|
||||
curl -X POST "http://localhost:8000/api/v1/generate-code" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"requirement": "创建一个用户认证服务类",
|
||||
"language": "python",
|
||||
"style": "fastapi"
|
||||
}'
|
||||
|
||||
# 审查代码
|
||||
curl -X POST "http://localhost:8000/api/v1/review-code" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"code_content": "def func(): pass"
|
||||
}'
|
||||
```
|
||||
|
||||
### JavaScript 示例
|
||||
|
||||
```javascript
|
||||
const BASE_URL = 'http://localhost:8000';
|
||||
|
||||
// 生成代码
|
||||
async function generateCode() {
|
||||
const response = await fetch(`${BASE_URL}/api/v1/generate-code`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
requirement: '创建一个用户认证服务类',
|
||||
language: 'python',
|
||||
style: 'fastapi'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log(result.data.result);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MCP 协议调用
|
||||
|
||||
除了 HTTP API,服务还支持通过 **MCP (Model Context Protocol)** 协议直接调用。
|
||||
|
||||
### MCP 客户端连接
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
async def main():
|
||||
server_params = StdioServerParameters(
|
||||
command='python',
|
||||
args=['mcp_server.py'],
|
||||
env=os.environ
|
||||
)
|
||||
|
||||
async with stdio_client(server_params) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
await session.initialize()
|
||||
|
||||
# 调用工具
|
||||
result = await session.call_tool('generate_code', {
|
||||
'requirement': '创建一个用户服务类',
|
||||
'language': 'python'
|
||||
})
|
||||
print(result.content[0].text)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### MCP 工具列表
|
||||
|
||||
| 工具名称 | 功能 |
|
||||
|---------|------|
|
||||
| `generate_code` | 生成代码 |
|
||||
| `refactor_code` | 重构代码 |
|
||||
| `review_code` | 审查代码 |
|
||||
| `organize_code` | 组织代码 |
|
||||
| `classify_code` | 分类代码 |
|
||||
| `analyze_project` | 分析项目 |
|
||||
| `suggest_folder_structure` | 建议项目结构 |
|
||||
| `create_code_file` | 创建文件 |
|
||||
|
||||
---
|
||||
|
||||
## 部署检查清单
|
||||
|
||||
- [ ] 配置环境变量(API Key、Gateway URL)
|
||||
- [ ] 确保网络可以访问 LiteLLM Gateway
|
||||
- [ ] 配置项目存储目录权限
|
||||
- [ ] 设置适当的资源限制(CPU、内存)
|
||||
- [ ] 配置健康检查
|
||||
- [ ] 设置日志收集
|
||||
- [ ] 配置监控和告警
|
||||
- [ ] 设置 HTTPS(生产环境)
|
||||
|
||||
---
|
||||
|
||||
## 技术支持
|
||||
|
||||
- **API 文档**: `/docs` (Swagger UI)
|
||||
- **ReDoc 文档**: `/redoc`
|
||||
- **健康检查**: `/health`
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0.0
|
||||
**最后更新**: 2025-01-08
|
||||
@@ -0,0 +1,391 @@
|
||||
# 代码助手 Agent 部署指南
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [部署方式](#部署方式)
|
||||
- [环境配置](#环境配置)
|
||||
- [Docker 部署](#docker-部署)
|
||||
- [云平台部署](#云平台部署)
|
||||
- [监控和日志](#监控和日志)
|
||||
- [故障排查](#故障排查)
|
||||
|
||||
---
|
||||
|
||||
## 部署方式
|
||||
|
||||
代码助手 Agent 支持多种部署方式:
|
||||
|
||||
1. **本地部署** - 适合开发和测试
|
||||
2. **Docker 部署** - 适合单机部署
|
||||
3. **Docker Compose 部署** - 适合本地或小规模部署
|
||||
4. **云平台部署** - 适合生产环境
|
||||
|
||||
---
|
||||
|
||||
## 环境配置
|
||||
|
||||
### 必需环境变量
|
||||
|
||||
| 变量名 | 说明 | 示例 |
|
||||
|--------|------|------|
|
||||
| `OPENAI_API_KEY` | LiteLLM Gateway API Key | `sk-rxegkFOciNmQLhOHr3qP3A` |
|
||||
| `OPENAI_BASE_URL` | LiteLLM Gateway Base URL | `https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1` |
|
||||
|
||||
### 可选环境变量
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `LITELLM_MODEL` | 模型名称 | `openai:taiji/gpt-4o-mini` |
|
||||
| `API_HOST` | API 服务监听地址 | `0.0.0.0` |
|
||||
| `API_PORT` | API 服务端口 | `8000` |
|
||||
|
||||
---
|
||||
|
||||
## Docker 部署
|
||||
|
||||
### 1. 构建镜像
|
||||
|
||||
```bash
|
||||
docker build -t code-assistant-agent:latest .
|
||||
```
|
||||
|
||||
### 2. 运行容器
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name code-assistant-agent \
|
||||
-p 8000:8000 \
|
||||
-e OPENAI_API_KEY=your_api_key \
|
||||
-e OPENAI_BASE_URL=your_gateway_url \
|
||||
-v $(pwd)/projects:/tmp/projects \
|
||||
code-assistant-agent:latest
|
||||
```
|
||||
|
||||
### 3. 验证部署
|
||||
|
||||
```bash
|
||||
# 检查容器状态
|
||||
docker ps | grep code-assistant-agent
|
||||
|
||||
# 检查健康状态
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# 查看日志
|
||||
docker logs -f code-assistant-agent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker Compose 部署
|
||||
|
||||
### 1. 配置环境变量
|
||||
|
||||
创建 `.env` 文件:
|
||||
|
||||
```env
|
||||
OPENAI_API_KEY=your_api_key
|
||||
OPENAI_BASE_URL=your_gateway_url
|
||||
LITELLM_MODEL=openai:taiji/gpt-4o-mini
|
||||
API_HOST=0.0.0.0
|
||||
API_PORT=8000
|
||||
```
|
||||
|
||||
### 2. 启动服务
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 3. 查看状态
|
||||
|
||||
```bash
|
||||
# 查看服务状态
|
||||
docker-compose ps
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 云平台部署
|
||||
|
||||
### Azure Container Apps
|
||||
|
||||
#### 1. 准备镜像
|
||||
|
||||
```bash
|
||||
# 登录 Azure Container Registry
|
||||
az acr login --name your-registry
|
||||
|
||||
# 构建并推送镜像
|
||||
docker build -t your-registry.azurecr.io/code-assistant-agent:latest .
|
||||
docker push your-registry.azurecr.io/code-assistant-agent:latest
|
||||
```
|
||||
|
||||
#### 2. 创建 Container App
|
||||
|
||||
```bash
|
||||
az containerapp create \
|
||||
--name code-assistant-agent \
|
||||
--resource-group your-resource-group \
|
||||
--image your-registry.azurecr.io/code-assistant-agent:latest \
|
||||
--target-port 8000 \
|
||||
--ingress external \
|
||||
--env-vars \
|
||||
OPENAI_API_KEY=your_key \
|
||||
OPENAI_BASE_URL=your_gateway_url \
|
||||
LITELLM_MODEL=openai:taiji/gpt-4o-mini
|
||||
```
|
||||
|
||||
#### 3. 配置自动扩缩容
|
||||
|
||||
```bash
|
||||
az containerapp update \
|
||||
--name code-assistant-agent \
|
||||
--resource-group your-resource-group \
|
||||
--min-replicas 1 \
|
||||
--max-replicas 10 \
|
||||
--cpu 1.0 \
|
||||
--memory 2.0Gi
|
||||
```
|
||||
|
||||
### AWS ECS / EKS
|
||||
|
||||
#### 1. 构建并推送镜像到 ECR
|
||||
|
||||
```bash
|
||||
# 登录 ECR
|
||||
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin your-account.dkr.ecr.us-east-1.amazonaws.com
|
||||
|
||||
# 构建并推送
|
||||
docker build -t code-assistant-agent:latest .
|
||||
docker tag code-assistant-agent:latest your-account.dkr.ecr.us-east-1.amazonaws.com/code-assistant-agent:latest
|
||||
docker push your-account.dkr.ecr.us-east-1.amazonaws.com/code-assistant-agent:latest
|
||||
```
|
||||
|
||||
#### 2. 创建 ECS 任务定义
|
||||
|
||||
创建 `task-definition.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"family": "code-assistant-agent",
|
||||
"networkMode": "awsvpc",
|
||||
"requiresCompatibilities": ["FARGATE"],
|
||||
"cpu": "1024",
|
||||
"memory": "2048",
|
||||
"containerDefinitions": [
|
||||
{
|
||||
"name": "code-assistant-agent",
|
||||
"image": "your-account.dkr.ecr.us-east-1.amazonaws.com/code-assistant-agent:latest",
|
||||
"portMappings": [
|
||||
{
|
||||
"containerPort": 8000,
|
||||
"protocol": "tcp"
|
||||
}
|
||||
],
|
||||
"environment": [
|
||||
{
|
||||
"name": "OPENAI_API_KEY",
|
||||
"value": "your_api_key"
|
||||
},
|
||||
{
|
||||
"name": "OPENAI_BASE_URL",
|
||||
"value": "your_gateway_url"
|
||||
}
|
||||
],
|
||||
"logConfiguration": {
|
||||
"logDriver": "awslogs",
|
||||
"options": {
|
||||
"awslogs-group": "/ecs/code-assistant-agent",
|
||||
"awslogs-region": "us-east-1",
|
||||
"awslogs-stream-prefix": "ecs"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 注册任务定义并创建服务
|
||||
|
||||
```bash
|
||||
# 注册任务定义
|
||||
aws ecs register-task-definition --cli-input-json file://task-definition.json
|
||||
|
||||
# 创建服务
|
||||
aws ecs create-service \
|
||||
--cluster your-cluster \
|
||||
--service-name code-assistant-agent \
|
||||
--task-definition code-assistant-agent \
|
||||
--desired-count 2 \
|
||||
--launch-type FARGATE \
|
||||
--network-configuration "awsvpcConfiguration={subnets=[subnet-xxx],securityGroups=[sg-xxx],assignPublicIp=ENABLED}"
|
||||
```
|
||||
|
||||
### Google Cloud Run
|
||||
|
||||
#### 1. 构建并推送镜像
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
gcloud builds submit --tag gcr.io/your-project/code-assistant-agent
|
||||
|
||||
# 或者使用 Docker
|
||||
docker build -t gcr.io/your-project/code-assistant-agent .
|
||||
docker push gcr.io/your-project/code-assistant-agent
|
||||
```
|
||||
|
||||
#### 2. 部署服务
|
||||
|
||||
```bash
|
||||
gcloud run deploy code-assistant-agent \
|
||||
--image gcr.io/your-project/code-assistant-agent \
|
||||
--platform managed \
|
||||
--region us-central1 \
|
||||
--allow-unauthenticated \
|
||||
--set-env-vars \
|
||||
OPENAI_API_KEY=your_key,OPENAI_BASE_URL=your_url \
|
||||
--memory 2Gi \
|
||||
--cpu 1 \
|
||||
--min-instances 1 \
|
||||
--max-instances 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 监控和日志
|
||||
|
||||
### 健康检查
|
||||
|
||||
服务提供健康检查端点:
|
||||
|
||||
```bash
|
||||
curl http://your-domain:8000/health
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"service": "代码助手 Agent API"
|
||||
}
|
||||
```
|
||||
|
||||
### 日志收集
|
||||
|
||||
#### Docker 日志
|
||||
|
||||
```bash
|
||||
# 查看实时日志
|
||||
docker logs -f code-assistant-agent
|
||||
|
||||
# 查看最近 100 行
|
||||
docker logs --tail 100 code-assistant-agent
|
||||
```
|
||||
|
||||
#### 应用日志
|
||||
|
||||
服务使用标准输出,可以通过容器日志系统收集。
|
||||
|
||||
### 监控指标
|
||||
|
||||
建议监控以下指标:
|
||||
|
||||
- **请求速率**: 每秒请求数
|
||||
- **响应时间**: API 响应时间
|
||||
- **错误率**: 5xx 错误比例
|
||||
- **资源使用**: CPU、内存使用率
|
||||
- **AI 调用**: LiteLLM Gateway 调用成功率
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. 服务无法启动
|
||||
|
||||
**问题**: 容器启动失败
|
||||
|
||||
**排查步骤**:
|
||||
```bash
|
||||
# 查看容器日志
|
||||
docker logs code-assistant-agent
|
||||
|
||||
# 检查环境变量
|
||||
docker exec code-assistant-agent env | grep OPENAI
|
||||
|
||||
# 检查端口占用
|
||||
netstat -tulpn | grep 8000
|
||||
```
|
||||
|
||||
#### 2. API 调用失败
|
||||
|
||||
**问题**: 返回 500 错误
|
||||
|
||||
**排查步骤**:
|
||||
- 检查 LiteLLM Gateway 连接
|
||||
- 验证 API Key 是否正确
|
||||
- 查看服务日志
|
||||
|
||||
#### 3. AI 调用超时
|
||||
|
||||
**问题**: 请求超时
|
||||
|
||||
**解决方案**:
|
||||
- 增加超时时间配置
|
||||
- 检查网络连接
|
||||
- 验证 Gateway 服务状态
|
||||
|
||||
#### 4. 文件操作失败
|
||||
|
||||
**问题**: 无法创建文件
|
||||
|
||||
**排查步骤**:
|
||||
```bash
|
||||
# 检查目录权限
|
||||
ls -la /tmp/projects
|
||||
|
||||
# 检查磁盘空间
|
||||
df -h
|
||||
|
||||
# 检查容器挂载
|
||||
docker inspect code-assistant-agent | grep Mounts
|
||||
```
|
||||
|
||||
### 调试模式
|
||||
|
||||
启用详细日志:
|
||||
|
||||
```bash
|
||||
docker run -e LOG_LEVEL=debug ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **API Key 管理**: 使用密钥管理服务(如 Azure Key Vault、AWS Secrets Manager)
|
||||
2. **HTTPS**: 生产环境必须使用 HTTPS
|
||||
3. **访问控制**: 配置 API 网关或反向代理进行访问控制
|
||||
4. **资源限制**: 设置适当的 CPU 和内存限制
|
||||
5. **网络隔离**: 使用私有网络和防火墙规则
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **连接池**: 配置 HTTP 客户端连接池
|
||||
2. **缓存**: 对频繁请求的结果进行缓存
|
||||
3. **异步处理**: 使用异步任务队列处理长时间任务
|
||||
4. **负载均衡**: 部署多个实例并使用负载均衡器
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0.0
|
||||
**最后更新**: 2025-01-08
|
||||
@@ -0,0 +1,121 @@
|
||||
# 代码助手 Agent - API 服务
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 本地运行
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 2. 启动服务
|
||||
python api_server.py
|
||||
|
||||
# 3. 访问 API 文档
|
||||
# http://localhost:8000/docs
|
||||
```
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```bash
|
||||
# 构建并运行
|
||||
docker-compose up -d
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
## 📚 文档
|
||||
|
||||
- **[API 接口文档](./API_DOCUMENTATION.md)** - 完整的 API 接口说明
|
||||
- **[部署指南](./DEPLOYMENT.md)** - 云部署详细指南
|
||||
|
||||
## 🔧 配置
|
||||
|
||||
通过环境变量配置:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_api_key
|
||||
export OPENAI_BASE_URL=your_gateway_url
|
||||
export API_PORT=8000
|
||||
```
|
||||
|
||||
## 📡 API 端点
|
||||
|
||||
所有 API 端点前缀: `/api/v1`
|
||||
|
||||
- `POST /api/v1/generate-code` - 生成代码
|
||||
- `POST /api/v1/refactor-code` - 重构代码
|
||||
- `POST /api/v1/review-code` - 审查代码
|
||||
- `POST /api/v1/organize-code` - 组织代码
|
||||
- `POST /api/v1/classify-code` - 分类代码
|
||||
- `POST /api/v1/analyze-project` - 分析项目
|
||||
- `POST /api/v1/suggest-structure` - 建议项目结构
|
||||
- `POST /api/v1/create-file` - 创建文件
|
||||
|
||||
## 🔌 MCP 协议
|
||||
|
||||
服务基于 **MCP (Model Context Protocol)** 框架,也支持直接通过 MCP 协议调用。
|
||||
|
||||
查看 `mcp_server.py` 了解 MCP 工具定义。
|
||||
|
||||
## 📝 示例
|
||||
|
||||
### HTTP API 调用
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
"http://localhost:8000/api/v1/generate-code",
|
||||
json={
|
||||
"requirement": "创建一个用户认证服务类",
|
||||
"language": "python",
|
||||
"style": "fastapi"
|
||||
}
|
||||
)
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
### MCP 协议调用
|
||||
|
||||
```python
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
# 连接 MCP 服务器
|
||||
server_params = StdioServerParameters(
|
||||
command='python',
|
||||
args=['mcp_server.py']
|
||||
)
|
||||
|
||||
async with stdio_client(server_params) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
await session.initialize()
|
||||
result = await session.call_tool('generate_code', {
|
||||
'requirement': '创建一个用户服务类'
|
||||
})
|
||||
print(result.content[0].text)
|
||||
```
|
||||
|
||||
## 🐳 部署
|
||||
|
||||
支持多种部署方式:
|
||||
|
||||
- **Docker**: 使用提供的 Dockerfile
|
||||
- **Docker Compose**: 使用 docker-compose.yml
|
||||
- **云平台**: Azure Container Apps, AWS ECS, Google Cloud Run
|
||||
|
||||
详细部署说明请查看 [DEPLOYMENT.md](./DEPLOYMENT.md)
|
||||
|
||||
## 📊 健康检查
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
## 🔍 监控
|
||||
|
||||
- API 文档: `/docs` (Swagger UI)
|
||||
- ReDoc 文档: `/redoc`
|
||||
- 健康检查: `/health`
|
||||
@@ -0,0 +1,10 @@
|
||||
pydantic-ai
|
||||
httpx
|
||||
mcp
|
||||
fastmcp
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
python-multipart
|
||||
fastapi>=0.104.0
|
||||
uvicorn[standard]>=0.24.0
|
||||
python-multipart
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
启动 HTTP API 服务器的入口脚本
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
if __name__ == '__main__':
|
||||
from src.server.api_server import app
|
||||
import uvicorn
|
||||
import os
|
||||
|
||||
host = os.getenv('API_HOST', '0.0.0.0')
|
||||
port = int(os.getenv('API_PORT', '8000'))
|
||||
|
||||
print(f"🚀 启动代码助手 Agent API")
|
||||
print(f"📡 监听地址: http://{host}:{port}")
|
||||
print(f"📚 API 文档: http://{host}:{port}/docs")
|
||||
|
||||
uvicorn.run(app, host=host, port=port, log_level="info")
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
启动 MCP HTTP/SSE 服务器的入口脚本
|
||||
支持远程调用,供 Cursor 等客户端使用
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
if __name__ == '__main__':
|
||||
from src.server.mcp_http_server import app
|
||||
import uvicorn
|
||||
|
||||
host = os.getenv("MCP_HOST", "0.0.0.0")
|
||||
port = int(os.getenv("MCP_PORT", "8001"))
|
||||
|
||||
print(f"🚀 MCP HTTP/SSE Server 启动中...")
|
||||
print(f"📡 HTTP 端点: http://{host}:{port}/mcp")
|
||||
print(f"📡 SSE 端点: http://{host}:{port}/mcp/sse")
|
||||
print(f"📚 健康检查: http://{host}:{port}/health")
|
||||
print(f"📋 工具列表: http://{host}:{port}/")
|
||||
print()
|
||||
print("💡 Cursor 配置示例:")
|
||||
print(f' "url": "http://{host}:{port}/mcp"')
|
||||
print(f' "type": "http"')
|
||||
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
启动 MCP 服务器的入口脚本
|
||||
支持 stdio(本地)和 HTTP/SSE(远程)两种传输方式
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='启动 MCP 服务器')
|
||||
parser.add_argument(
|
||||
'--transport',
|
||||
choices=['stdio', 'http', 'sse'],
|
||||
default='stdio',
|
||||
help='传输方式: stdio (本地), http (HTTP), sse (SSE)'
|
||||
)
|
||||
parser.add_argument('--host', default='0.0.0.0', help='HTTP/SSE 服务器地址')
|
||||
parser.add_argument('--port', type=int, default=8001, help='HTTP/SSE 服务器端口')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.transport == 'stdio':
|
||||
# stdio 模式(本地)
|
||||
from src.server.mcp_server import server
|
||||
print("🚀 MCP Server (stdio) 启动中...")
|
||||
server.run()
|
||||
else:
|
||||
# HTTP/SSE 模式(远程)
|
||||
from src.server.mcp_http_server import app
|
||||
import uvicorn
|
||||
|
||||
host = args.host
|
||||
port = args.port
|
||||
|
||||
print(f"🚀 MCP HTTP/SSE Server 启动中...")
|
||||
print(f"📡 HTTP 端点: http://{host}:{port}/mcp")
|
||||
print(f"📡 SSE 端点: http://{host}:{port}/mcp/sse")
|
||||
print(f"📚 健康检查: http://{host}:{port}/health")
|
||||
print()
|
||||
print("💡 Cursor 配置示例:")
|
||||
print(f' "url": "http://{host}:{port}/mcp"')
|
||||
print(f' "type": "{args.transport}"')
|
||||
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
代码助手 Agent - 源代码包
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
客户端模块 - MCP 客户端示例
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
MCP 客户端示例 - 真实的代码助手 Agent
|
||||
演示如何使用代码助手 Agent 的各种功能
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数:连接到 MCP 服务器并调用工具"""
|
||||
# 配置服务器参数
|
||||
# 获取项目根目录
|
||||
import sys
|
||||
from pathlib import Path
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
mcp_server_path = project_root / 'src' / 'server' / 'mcp_server.py'
|
||||
|
||||
server_params = StdioServerParameters(
|
||||
command='python',
|
||||
args=[str(mcp_server_path)],
|
||||
env=os.environ
|
||||
)
|
||||
|
||||
async with stdio_client(server_params) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
# 初始化会话
|
||||
await session.initialize()
|
||||
|
||||
print("=" * 70)
|
||||
print("🤖 代码助手 Agent - 真实功能演示")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
# 示例 1: 生成代码
|
||||
print("✨ 示例 1: 根据需求生成代码")
|
||||
print("-" * 70)
|
||||
result = await session.call_tool(
|
||||
'generate_code',
|
||||
{
|
||||
'requirement': '创建一个用户认证服务类,包含登录、注册、密码验证功能',
|
||||
'language': 'python',
|
||||
'style': 'fastapi',
|
||||
'project_root': './demo_project'
|
||||
}
|
||||
)
|
||||
print(result.content[0].text)
|
||||
print()
|
||||
|
||||
# 示例 2: 代码审查
|
||||
print("🔍 示例 2: 代码审查")
|
||||
print("-" * 70)
|
||||
code_to_review = """
|
||||
def process_data(data):
|
||||
result = []
|
||||
for i in range(len(data)):
|
||||
if data[i] > 0:
|
||||
result.append(data[i] * 2)
|
||||
return result
|
||||
"""
|
||||
result = await session.call_tool(
|
||||
'review_code',
|
||||
{
|
||||
'code_content': code_to_review,
|
||||
'file_path': 'utils/processors.py'
|
||||
}
|
||||
)
|
||||
print(result.content[0].text)
|
||||
print()
|
||||
|
||||
# 示例 3: 代码重构
|
||||
print("🔧 示例 3: 代码重构")
|
||||
print("-" * 70)
|
||||
code_to_refactor = """
|
||||
def get_user(id):
|
||||
users = {
|
||||
1: {'name': 'Alice', 'age': 30},
|
||||
2: {'name': 'Bob', 'age': 25}
|
||||
}
|
||||
return users.get(id)
|
||||
"""
|
||||
result = await session.call_tool(
|
||||
'refactor_code',
|
||||
{
|
||||
'code_content': code_to_refactor,
|
||||
'refactoring_goal': '添加错误处理和类型提示'
|
||||
}
|
||||
)
|
||||
print(result.content[0].text)
|
||||
print()
|
||||
|
||||
# 示例 4: 智能组织代码
|
||||
print("📁 示例 4: 智能组织代码到合适文件夹")
|
||||
print("-" * 70)
|
||||
utility_code = """
|
||||
from datetime import datetime
|
||||
|
||||
def format_timestamp(ts):
|
||||
\"\"\"格式化时间戳为可读格式\"\"\"
|
||||
return datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')
|
||||
"""
|
||||
result = await session.call_tool(
|
||||
'organize_code',
|
||||
{
|
||||
'code_content': utility_code,
|
||||
'project_root': './demo_project'
|
||||
}
|
||||
)
|
||||
print(result.content[0].text)
|
||||
print()
|
||||
|
||||
# 示例 5: 项目分析
|
||||
print("📊 示例 5: 分析项目结构")
|
||||
print("-" * 70)
|
||||
result = await session.call_tool(
|
||||
'analyze_project',
|
||||
{
|
||||
'project_root': './demo_project',
|
||||
'max_depth': 2
|
||||
}
|
||||
)
|
||||
print(result.content[0].text)
|
||||
print()
|
||||
|
||||
# 示例 6: 建议项目结构
|
||||
print("🏗️ 示例 6: 建议项目文件夹结构")
|
||||
print("-" * 70)
|
||||
result = await session.call_tool(
|
||||
'suggest_folder_structure',
|
||||
{
|
||||
'project_description': '一个微服务架构的电商系统,包含用户服务、商品服务、订单服务和支付服务'
|
||||
}
|
||||
)
|
||||
print(result.content[0].text)
|
||||
print()
|
||||
|
||||
print("=" * 70)
|
||||
print("✅ 所有功能演示完成!")
|
||||
print("=" * 70)
|
||||
print("\n💡 提示:生成的代码和文件已保存在 ./demo_project 目录中")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
服务器模块 - MCP 服务器和 HTTP API 服务器
|
||||
"""
|
||||
|
||||
from .mcp_server import (
|
||||
server,
|
||||
code_assistant_agent,
|
||||
organize_code,
|
||||
suggest_folder_structure,
|
||||
classify_code,
|
||||
generate_code,
|
||||
refactor_code,
|
||||
review_code,
|
||||
analyze_project,
|
||||
create_code_file
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'server',
|
||||
'code_assistant_agent',
|
||||
'organize_code',
|
||||
'suggest_folder_structure',
|
||||
'classify_code',
|
||||
'generate_code',
|
||||
'refactor_code',
|
||||
'review_code',
|
||||
'analyze_project',
|
||||
'create_code_file',
|
||||
]
|
||||
|
||||
@@ -0,0 +1,724 @@
|
||||
"""
|
||||
HTTP API 服务器 - 代码助手 Agent 服务
|
||||
将 MCP 服务包装为 HTTP API,支持云部署
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional, Dict, Any, AsyncGenerator
|
||||
|
||||
from fastapi import FastAPI, HTTPException, BackgroundTasks, Request, Header, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
# 从同目录的 mcp_server 模块导入工具函数
|
||||
from .mcp_server import (
|
||||
organize_code,
|
||||
suggest_folder_structure,
|
||||
classify_code,
|
||||
generate_code,
|
||||
refactor_code,
|
||||
review_code,
|
||||
analyze_project,
|
||||
create_code_file
|
||||
)
|
||||
|
||||
# 配置
|
||||
API_VERSION = "v1"
|
||||
SERVER_NAME = "代码助手 Agent API"
|
||||
|
||||
# 创建 FastAPI 应用
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
# 启动时初始化
|
||||
print(f"🚀 {SERVER_NAME} 启动中...")
|
||||
yield
|
||||
# 关闭时清理
|
||||
print(f"🛑 {SERVER_NAME} 关闭中...")
|
||||
|
||||
app = FastAPI(
|
||||
title=SERVER_NAME,
|
||||
description="基于 MCP 框架的代码助手 Agent 服务",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# 配置 CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # 生产环境应限制具体域名
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# ==================== 请求模型 ====================
|
||||
|
||||
class GenerateCodeRequest(BaseModel):
|
||||
"""代码生成请求"""
|
||||
requirement: str = Field(..., description="代码需求描述")
|
||||
language: str = Field(default="python", description="编程语言")
|
||||
style: Optional[str] = Field(default=None, description="代码风格,如 'fastapi', 'django'")
|
||||
project_root: str = Field(default="/tmp/projects", description="项目根目录路径")
|
||||
|
||||
|
||||
class RefactorCodeRequest(BaseModel):
|
||||
"""代码重构请求"""
|
||||
code_content: str = Field(..., description="要重构的代码内容")
|
||||
refactoring_goal: Optional[str] = Field(default=None, description="重构目标")
|
||||
|
||||
|
||||
class ReviewCodeRequest(BaseModel):
|
||||
"""代码审查请求"""
|
||||
code_content: str = Field(..., description="要审查的代码内容")
|
||||
file_path: Optional[str] = Field(default=None, description="文件路径(可选)")
|
||||
|
||||
|
||||
class OrganizeCodeRequest(BaseModel):
|
||||
"""代码组织请求"""
|
||||
code_content: str = Field(..., description="要组织的代码内容")
|
||||
code_type: Optional[str] = Field(default=None, description="代码类型提示")
|
||||
project_root: str = Field(default="/tmp/projects", description="项目根目录路径")
|
||||
|
||||
|
||||
class ClassifyCodeRequest(BaseModel):
|
||||
"""代码分类请求"""
|
||||
code_content: str = Field(..., description="要分类的代码内容")
|
||||
|
||||
|
||||
class AnalyzeProjectRequest(BaseModel):
|
||||
"""项目分析请求"""
|
||||
project_root: str = Field(default="/tmp/projects", description="项目根目录路径")
|
||||
max_depth: int = Field(default=3, description="最大扫描深度")
|
||||
|
||||
|
||||
class SuggestStructureRequest(BaseModel):
|
||||
"""项目结构建议请求"""
|
||||
project_description: str = Field(..., description="项目描述")
|
||||
|
||||
|
||||
class CreateFileRequest(BaseModel):
|
||||
"""创建文件请求"""
|
||||
code_content: str = Field(..., description="代码内容")
|
||||
folder_path: str = Field(..., description="目标文件夹路径")
|
||||
file_name: str = Field(..., description="文件名")
|
||||
project_root: str = Field(default="/tmp/projects", description="项目根目录路径")
|
||||
|
||||
|
||||
# ==================== 响应模型 ====================
|
||||
|
||||
class APIResponse(BaseModel):
|
||||
"""统一 API 响应格式"""
|
||||
success: bool = Field(..., description="是否成功")
|
||||
data: Optional[Any] = Field(default=None, description="响应数据")
|
||||
message: Optional[str] = Field(default=None, description="响应消息")
|
||||
error: Optional[str] = Field(default=None, description="错误信息")
|
||||
|
||||
|
||||
# ==================== API Key 验证 ====================
|
||||
|
||||
async def verify_api_key(
|
||||
api_key: Optional[str] = Header(None, alias="api-key"),
|
||||
authorization: Optional[str] = Header(None)
|
||||
) -> str:
|
||||
"""
|
||||
验证 API Key
|
||||
|
||||
支持从以下位置获取 API key:
|
||||
1. api-key 请求头
|
||||
2. Authorization: Bearer <token> 请求头
|
||||
3. 环境变量 LLM_API_KEY 或 OPENAI_API_KEY(仅用于工具调用,不作为验证)
|
||||
|
||||
如果没有提供 API key,返回 401 错误
|
||||
"""
|
||||
# 从 api-key 请求头获取
|
||||
if api_key:
|
||||
if not api_key.strip() or api_key.strip() == "sk":
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="无效的 API key。请提供有效的 API key。"
|
||||
)
|
||||
return api_key.strip()
|
||||
|
||||
# 从 Authorization 请求头获取
|
||||
if authorization:
|
||||
if authorization.startswith("Bearer "):
|
||||
api_key = authorization[7:].strip()
|
||||
else:
|
||||
api_key = authorization.strip()
|
||||
|
||||
if not api_key or api_key == "sk":
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="无效的 API key。请提供有效的 API key。"
|
||||
)
|
||||
return api_key
|
||||
|
||||
# 如果没有提供 API key,返回错误
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="缺少 API key。请在请求头中提供 'api-key' 或 'Authorization: Bearer <token>'。"
|
||||
)
|
||||
|
||||
|
||||
# ==================== 工具函数 ====================
|
||||
|
||||
async def call_mcp_tool(tool_name: str, params: Dict[str, Any], api_key: Optional[str] = None) -> str:
|
||||
"""
|
||||
调用 MCP 工具
|
||||
|
||||
Args:
|
||||
tool_name: 工具名称
|
||||
params: 工具参数
|
||||
api_key: API key(如果提供,会临时设置到环境变量中)
|
||||
|
||||
Returns:
|
||||
工具返回结果
|
||||
"""
|
||||
# 如果提供了 API key,临时设置到环境变量中
|
||||
old_api_key = None
|
||||
if api_key:
|
||||
old_api_key = os.environ.get('OPENAI_API_KEY')
|
||||
os.environ['OPENAI_API_KEY'] = api_key
|
||||
|
||||
try:
|
||||
# 直接调用导入的工具函数
|
||||
tool_map = {
|
||||
'generate_code': generate_code,
|
||||
'refactor_code': refactor_code,
|
||||
'review_code': review_code,
|
||||
'organize_code': organize_code,
|
||||
'classify_code': classify_code,
|
||||
'analyze_project': analyze_project,
|
||||
'suggest_folder_structure': suggest_folder_structure,
|
||||
'create_code_file': create_code_file,
|
||||
}
|
||||
|
||||
if tool_name not in tool_map:
|
||||
raise ValueError(f"未知的工具: {tool_name}")
|
||||
|
||||
tool_func = tool_map[tool_name]
|
||||
# 过滤 None 值
|
||||
filtered_params = {k: v for k, v in params.items() if v is not None}
|
||||
return await tool_func(**filtered_params)
|
||||
finally:
|
||||
# 恢复原来的 API key
|
||||
if api_key and old_api_key is not None:
|
||||
os.environ['OPENAI_API_KEY'] = old_api_key
|
||||
elif api_key:
|
||||
# 如果原来没有设置,删除
|
||||
os.environ.pop('OPENAI_API_KEY', None)
|
||||
|
||||
|
||||
# ==================== API 端点 ====================
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路径,返回 API 信息"""
|
||||
return {
|
||||
"name": SERVER_NAME,
|
||||
"version": "1.0.0",
|
||||
"status": "running",
|
||||
"endpoints": {
|
||||
"health": "/health",
|
||||
"docs": "/docs",
|
||||
"api": f"/api/{API_VERSION}",
|
||||
"mcp": "/mcp",
|
||||
"mcp_sse": "/mcp/sse"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": SERVER_NAME
|
||||
}
|
||||
|
||||
|
||||
# ==================== MCP 端点 ====================
|
||||
|
||||
# Session 管理
|
||||
sessions: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
|
||||
async def handle_mcp_request(request_data: Dict[str, Any], session_id: Optional[str] = None, api_key: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""处理 MCP 请求"""
|
||||
method = request_data.get("method")
|
||||
params = request_data.get("params", {})
|
||||
request_id = request_data.get("id")
|
||||
|
||||
# 对于 tools/call 方法,需要验证 API key
|
||||
if method == "tools/call":
|
||||
if not api_key or api_key.strip() == "" or api_key.strip() == "sk":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {
|
||||
"code": -32001,
|
||||
"message": "缺少 API key。请在请求头中提供 'api-key' 或 'Authorization: Bearer <token>'。"
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
if method == "initialize":
|
||||
# 初始化会话
|
||||
if not session_id:
|
||||
session_id = str(uuid.uuid4())
|
||||
sessions[session_id] = {
|
||||
"initialized": True,
|
||||
"capabilities": {}
|
||||
}
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {},
|
||||
"resources": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "代码助手 Agent",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "tools/list":
|
||||
# 列出所有工具
|
||||
tools = [
|
||||
{
|
||||
"name": "organize_code",
|
||||
"description": "智能分析代码并自动组织到合适的文件夹中",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code_content": {"type": "string", "description": "要组织的代码内容"},
|
||||
"code_type": {"type": "string", "description": "代码类型(可选)"},
|
||||
"project_root": {"type": "string", "description": "项目根目录路径", "default": "."}
|
||||
},
|
||||
"required": ["code_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "suggest_folder_structure",
|
||||
"description": "根据项目描述,建议合理的文件夹结构",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_description": {"type": "string", "description": "项目描述"}
|
||||
},
|
||||
"required": ["project_description"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "classify_code",
|
||||
"description": "分析代码内容,确定其应该属于哪个类别/文件夹",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code_content": {"type": "string", "description": "代码内容"}
|
||||
},
|
||||
"required": ["code_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "generate_code",
|
||||
"description": "根据需求生成代码",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requirement": {"type": "string", "description": "代码需求描述"},
|
||||
"language": {"type": "string", "description": "编程语言", "default": "python"},
|
||||
"style": {"type": "string", "description": "代码风格(可选)"},
|
||||
"project_root": {"type": "string", "description": "项目根目录路径", "default": "."}
|
||||
},
|
||||
"required": ["requirement"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "refactor_code",
|
||||
"description": "重构代码,改进代码质量、性能和可维护性",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code_content": {"type": "string", "description": "要重构的代码"},
|
||||
"refactoring_goal": {"type": "string", "description": "重构目标(可选)"}
|
||||
},
|
||||
"required": ["code_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "review_code",
|
||||
"description": "审查代码,发现潜在问题、bug 和改进建议",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code_content": {"type": "string", "description": "要审查的代码"},
|
||||
"file_path": {"type": "string", "description": "文件路径(可选)"}
|
||||
},
|
||||
"required": ["code_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "analyze_project",
|
||||
"description": "分析项目结构,提供项目概览和建议",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_root": {"type": "string", "description": "项目根目录路径", "default": "."},
|
||||
"max_depth": {"type": "integer", "description": "最大扫描深度", "default": 3}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "create_code_file",
|
||||
"description": "在指定文件夹中创建代码文件",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code_content": {"type": "string", "description": "代码内容"},
|
||||
"folder_path": {"type": "string", "description": "目标文件夹路径"},
|
||||
"file_name": {"type": "string", "description": "文件名"},
|
||||
"project_root": {"type": "string", "description": "项目根目录路径", "default": "."}
|
||||
},
|
||||
"required": ["code_content", "folder_path", "file_name"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"tools": tools
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "tools/call":
|
||||
# 调用工具
|
||||
tool_name = params.get("name")
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
result = await call_mcp_tool(tool_name, arguments, api_key=api_key)
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": str(result)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "ping":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {}
|
||||
}
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown method: {method}")
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": str(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.post("/mcp")
|
||||
async def mcp_http_endpoint(request: Request):
|
||||
"""MCP HTTP 端点 - Streamable HTTP"""
|
||||
try:
|
||||
body = await request.json()
|
||||
session_id = request.headers.get("x-mcp-session-id")
|
||||
|
||||
# 从请求头获取 API key
|
||||
api_key = request.headers.get("api-key") or request.headers.get("api_key")
|
||||
if not api_key:
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if auth_header:
|
||||
if auth_header.startswith("Bearer "):
|
||||
api_key = auth_header[7:]
|
||||
else:
|
||||
api_key = auth_header
|
||||
|
||||
response = await handle_mcp_request(body, session_id, api_key=api_key)
|
||||
|
||||
# 如果创建了新会话,返回 session ID
|
||||
if "result" in response and isinstance(response["result"], dict):
|
||||
if "sessionId" not in response["result"] and session_id:
|
||||
response["result"]["sessionId"] = session_id
|
||||
|
||||
return JSONResponse(
|
||||
content=response,
|
||||
headers={"x-mcp-session-id": session_id or ""}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": f"Parse error: {str(e)}"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/mcp/sse")
|
||||
async def mcp_sse_endpoint(request: Request):
|
||||
"""MCP SSE 端点 - Server-Sent Events"""
|
||||
session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4())
|
||||
|
||||
async def event_stream() -> AsyncGenerator[str, None]:
|
||||
# 发送初始连接消息
|
||||
yield f"data: {json.dumps({'type': 'connection', 'sessionId': session_id})}\n\n"
|
||||
|
||||
# 保持连接
|
||||
while True:
|
||||
await asyncio.sleep(30) # 心跳
|
||||
yield f"data: {json.dumps({'type': 'ping'})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"x-mcp-session-id": session_id
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.post(f"/api/{API_VERSION}/generate-code", response_model=APIResponse)
|
||||
async def api_generate_code(request: GenerateCodeRequest, api_key: str = Depends(verify_api_key)):
|
||||
"""
|
||||
生成代码
|
||||
|
||||
根据自然语言需求生成高质量的代码
|
||||
"""
|
||||
try:
|
||||
result = await call_mcp_tool('generate_code', {
|
||||
'requirement': request.requirement,
|
||||
'language': request.language,
|
||||
'style': request.style,
|
||||
'project_root': request.project_root
|
||||
}, api_key=api_key)
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"result": result},
|
||||
message="代码生成成功"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"代码生成失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@app.post(f"/api/{API_VERSION}/refactor-code", response_model=APIResponse)
|
||||
async def api_refactor_code(request: RefactorCodeRequest, api_key: str = Depends(verify_api_key)):
|
||||
"""
|
||||
重构代码
|
||||
|
||||
改进代码质量、性能和可维护性
|
||||
"""
|
||||
try:
|
||||
result = await call_mcp_tool('refactor_code', {
|
||||
'code_content': request.code_content,
|
||||
'refactoring_goal': request.refactoring_goal
|
||||
}, api_key=api_key)
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"result": result},
|
||||
message="代码重构成功"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"代码重构失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@app.post(f"/api/{API_VERSION}/review-code", response_model=APIResponse)
|
||||
async def api_review_code(request: ReviewCodeRequest, api_key: str = Depends(verify_api_key)):
|
||||
"""
|
||||
审查代码
|
||||
|
||||
发现潜在问题、bug 和改进建议
|
||||
"""
|
||||
try:
|
||||
result = await call_mcp_tool('review_code', {
|
||||
'code_content': request.code_content,
|
||||
'file_path': request.file_path
|
||||
}, api_key=api_key)
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"result": result},
|
||||
message="代码审查完成"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"代码审查失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@app.post(f"/api/{API_VERSION}/organize-code", response_model=APIResponse)
|
||||
async def api_organize_code(request: OrganizeCodeRequest, api_key: str = Depends(verify_api_key)):
|
||||
"""
|
||||
组织代码
|
||||
|
||||
智能分析代码并自动组织到合适的文件夹中
|
||||
"""
|
||||
try:
|
||||
result = await call_mcp_tool('organize_code', {
|
||||
'code_content': request.code_content,
|
||||
'code_type': request.code_type,
|
||||
'project_root': request.project_root
|
||||
}, api_key=api_key)
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"result": result},
|
||||
message="代码组织成功"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"代码组织失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@app.post(f"/api/{API_VERSION}/classify-code", response_model=APIResponse)
|
||||
async def api_classify_code(request: ClassifyCodeRequest, api_key: str = Depends(verify_api_key)):
|
||||
"""
|
||||
分类代码
|
||||
|
||||
分析代码内容,确定其应该属于哪个类别/文件夹
|
||||
"""
|
||||
try:
|
||||
result = await call_mcp_tool('classify_code', {
|
||||
'code_content': request.code_content
|
||||
}, api_key=api_key)
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"result": result},
|
||||
message="代码分类完成"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"代码分类失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@app.post(f"/api/{API_VERSION}/analyze-project", response_model=APIResponse)
|
||||
async def api_analyze_project(request: AnalyzeProjectRequest, api_key: str = Depends(verify_api_key)):
|
||||
"""
|
||||
分析项目
|
||||
|
||||
分析项目结构,提供项目概览和改进建议
|
||||
"""
|
||||
try:
|
||||
result = await call_mcp_tool('analyze_project', {
|
||||
'project_root': request.project_root,
|
||||
'max_depth': request.max_depth
|
||||
}, api_key=api_key)
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"result": result},
|
||||
message="项目分析完成"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"项目分析失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@app.post(f"/api/{API_VERSION}/suggest-structure", response_model=APIResponse)
|
||||
async def api_suggest_structure(request: SuggestStructureRequest, api_key: str = Depends(verify_api_key)):
|
||||
"""
|
||||
建议项目结构
|
||||
|
||||
根据项目描述,建议合理的文件夹结构
|
||||
"""
|
||||
try:
|
||||
result = await call_mcp_tool('suggest_folder_structure', {
|
||||
'project_description': request.project_description
|
||||
}, api_key=api_key)
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"result": result},
|
||||
message="结构建议生成成功"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"结构建议生成失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@app.post(f"/api/{API_VERSION}/create-file", response_model=APIResponse)
|
||||
async def api_create_file(request: CreateFileRequest, api_key: str = Depends(verify_api_key)):
|
||||
"""
|
||||
创建代码文件
|
||||
|
||||
在指定文件夹中创建代码文件
|
||||
"""
|
||||
try:
|
||||
result = await call_mcp_tool('create_code_file', {
|
||||
'code_content': request.code_content,
|
||||
'folder_path': request.folder_path,
|
||||
'file_name': request.file_name,
|
||||
'project_root': request.project_root
|
||||
}, api_key=api_key)
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"result": result},
|
||||
message="文件创建成功"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"文件创建失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
|
||||
# 从环境变量读取配置
|
||||
host = os.getenv('API_HOST', '0.0.0.0')
|
||||
port = int(os.getenv('API_PORT', '8000'))
|
||||
|
||||
print(f"🚀 启动 {SERVER_NAME}")
|
||||
print(f"📡 监听地址: http://{host}:{port}")
|
||||
print(f"📚 API 文档: http://{host}:{port}/docs")
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level="info"
|
||||
)
|
||||
@@ -0,0 +1,402 @@
|
||||
"""
|
||||
MCP HTTP/SSE 服务器 - 支持远程调用
|
||||
实现 MCP 协议的 HTTP 和 SSE 传输方式,供 Cursor 等客户端远程调用
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from typing import Dict, Any, Optional, AsyncGenerator
|
||||
from fastapi import FastAPI, Request, HTTPException, Header
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
|
||||
# 导入 MCP 服务器和工具函数
|
||||
from .mcp_server import (
|
||||
server as mcp_server,
|
||||
organize_code,
|
||||
suggest_folder_structure,
|
||||
classify_code,
|
||||
generate_code,
|
||||
refactor_code,
|
||||
review_code,
|
||||
analyze_project,
|
||||
create_code_file
|
||||
)
|
||||
|
||||
# 工具映射
|
||||
TOOL_MAP = {
|
||||
'organize_code': organize_code,
|
||||
'suggest_folder_structure': suggest_folder_structure,
|
||||
'classify_code': classify_code,
|
||||
'generate_code': generate_code,
|
||||
'refactor_code': refactor_code,
|
||||
'review_code': review_code,
|
||||
'analyze_project': analyze_project,
|
||||
'create_code_file': create_code_file,
|
||||
}
|
||||
|
||||
# 创建 FastAPI 应用
|
||||
app = FastAPI(
|
||||
title="MCP HTTP/SSE Server",
|
||||
description="MCP 协议的 HTTP 和 SSE 传输实现",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 配置 CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Session 管理
|
||||
sessions: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
|
||||
class MCPRequest(BaseModel):
|
||||
"""MCP JSON-RPC 请求"""
|
||||
jsonrpc: str = "2.0"
|
||||
id: Optional[str] = None
|
||||
method: str
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class MCPResponse(BaseModel):
|
||||
"""MCP JSON-RPC 响应"""
|
||||
jsonrpc: str = "2.0"
|
||||
id: Optional[str] = None
|
||||
result: Optional[Any] = None
|
||||
error: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
async def handle_mcp_request(request_data: Dict[str, Any], session_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""处理 MCP 请求"""
|
||||
method = request_data.get("method")
|
||||
params = request_data.get("params", {})
|
||||
request_id = request_data.get("id")
|
||||
|
||||
try:
|
||||
if method == "initialize":
|
||||
# 初始化会话
|
||||
if not session_id:
|
||||
session_id = str(uuid.uuid4())
|
||||
sessions[session_id] = {
|
||||
"initialized": True,
|
||||
"capabilities": {}
|
||||
}
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {},
|
||||
"resources": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "代码助手 Agent",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "tools/list":
|
||||
# 列出所有工具
|
||||
tools = [
|
||||
{
|
||||
"name": "organize_code",
|
||||
"description": "智能分析代码并自动组织到合适的文件夹中",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code_content": {"type": "string", "description": "要组织的代码内容"},
|
||||
"code_type": {"type": "string", "description": "代码类型(可选)"},
|
||||
"project_root": {"type": "string", "description": "项目根目录路径", "default": "."}
|
||||
},
|
||||
"required": ["code_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "suggest_folder_structure",
|
||||
"description": "根据项目描述,建议合理的文件夹结构",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_description": {"type": "string", "description": "项目描述"}
|
||||
},
|
||||
"required": ["project_description"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "classify_code",
|
||||
"description": "分析代码内容,确定其应该属于哪个类别/文件夹",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code_content": {"type": "string", "description": "代码内容"}
|
||||
},
|
||||
"required": ["code_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "generate_code",
|
||||
"description": "根据需求生成代码",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requirement": {"type": "string", "description": "代码需求描述"},
|
||||
"language": {"type": "string", "description": "编程语言", "default": "python"},
|
||||
"style": {"type": "string", "description": "代码风格(可选)"},
|
||||
"project_root": {"type": "string", "description": "项目根目录路径", "default": "."}
|
||||
},
|
||||
"required": ["requirement"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "refactor_code",
|
||||
"description": "重构代码,改进代码质量、性能和可维护性",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code_content": {"type": "string", "description": "要重构的代码"},
|
||||
"refactoring_goal": {"type": "string", "description": "重构目标(可选)"}
|
||||
},
|
||||
"required": ["code_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "review_code",
|
||||
"description": "审查代码,发现潜在问题、bug 和改进建议",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code_content": {"type": "string", "description": "要审查的代码"},
|
||||
"file_path": {"type": "string", "description": "文件路径(可选)"}
|
||||
},
|
||||
"required": ["code_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "analyze_project",
|
||||
"description": "分析项目结构,提供项目概览和建议",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_root": {"type": "string", "description": "项目根目录路径", "default": "."},
|
||||
"max_depth": {"type": "integer", "description": "最大扫描深度", "default": 3}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "create_code_file",
|
||||
"description": "在指定文件夹中创建代码文件",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code_content": {"type": "string", "description": "代码内容"},
|
||||
"folder_path": {"type": "string", "description": "目标文件夹路径"},
|
||||
"file_name": {"type": "string", "description": "文件名"},
|
||||
"project_root": {"type": "string", "description": "项目根目录路径", "default": "."}
|
||||
},
|
||||
"required": ["code_content", "folder_path", "file_name"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"tools": tools
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "tools/call":
|
||||
# 调用工具
|
||||
tool_name = params.get("name")
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
if tool_name not in TOOL_MAP:
|
||||
raise ValueError(f"Tool '{tool_name}' not found")
|
||||
|
||||
# 获取工具函数
|
||||
tool_func = TOOL_MAP[tool_name]
|
||||
|
||||
# 调用工具(异步)
|
||||
result = await tool_func(**arguments)
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": str(result)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "ping":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {}
|
||||
}
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown method: {method}")
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": str(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.post("/mcp")
|
||||
async def mcp_http_endpoint(request: Request):
|
||||
"""MCP HTTP 端点 - Streamable HTTP"""
|
||||
try:
|
||||
body = await request.json()
|
||||
session_id = request.headers.get("x-mcp-session-id")
|
||||
|
||||
response = await handle_mcp_request(body, session_id)
|
||||
|
||||
# 如果创建了新会话,返回 session ID
|
||||
if "result" in response and isinstance(response["result"], dict):
|
||||
if "sessionId" not in response["result"] and session_id:
|
||||
response["result"]["sessionId"] = session_id
|
||||
|
||||
return JSONResponse(
|
||||
content=response,
|
||||
headers={"x-mcp-session-id": session_id or ""}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": f"Parse error: {str(e)}"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/mcp/sse")
|
||||
async def mcp_sse_endpoint(request: Request):
|
||||
"""MCP SSE 端点 - Server-Sent Events"""
|
||||
session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4())
|
||||
|
||||
async def event_stream() -> AsyncGenerator[str, None]:
|
||||
# 发送初始连接消息
|
||||
yield f"data: {json.dumps({'type': 'connection', 'sessionId': session_id})}\n\n"
|
||||
|
||||
# 保持连接,等待请求
|
||||
# 注意:SSE 通常需要客户端通过 POST 发送请求
|
||||
# 这里简化实现,实际应该使用 WebSocket 或轮询
|
||||
|
||||
# 发送工具列表
|
||||
tools = list(TOOL_MAP.keys())
|
||||
yield f"data: {json.dumps({'type': 'tools', 'tools': tools})}\n\n"
|
||||
|
||||
# 保持连接
|
||||
import asyncio
|
||||
while True:
|
||||
await asyncio.sleep(30) # 心跳
|
||||
yield f"data: {json.dumps({'type': 'ping'})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"x-mcp-session-id": session_id
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.post("/mcp/sse")
|
||||
async def mcp_sse_post(request: Request):
|
||||
"""MCP SSE POST 端点 - 处理 SSE 请求"""
|
||||
try:
|
||||
body = await request.json()
|
||||
session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4())
|
||||
|
||||
async def response_stream() -> AsyncGenerator[str, None]:
|
||||
response = await handle_mcp_request(body, session_id)
|
||||
yield f"data: {json.dumps(response)}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
response_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"x-mcp-session-id": session_id
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": f"Parse error: {str(e)}"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "MCP HTTP/SSE Server",
|
||||
"tools_count": len(TOOL_MAP)
|
||||
}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根端点"""
|
||||
return {
|
||||
"service": "MCP HTTP/SSE Server",
|
||||
"version": "1.0.0",
|
||||
"endpoints": {
|
||||
"mcp_http": "/mcp",
|
||||
"mcp_sse": "/mcp/sse",
|
||||
"health": "/health"
|
||||
},
|
||||
"tools": list(TOOL_MAP.keys())
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
host = os.getenv("MCP_HOST", "0.0.0.0")
|
||||
port = int(os.getenv("MCP_PORT", "8001"))
|
||||
|
||||
print(f"🚀 MCP HTTP/SSE Server 启动中...")
|
||||
print(f"📡 HTTP 端点: http://{host}:{port}/mcp")
|
||||
print(f"📡 SSE 端点: http://{host}:{port}/mcp/sse")
|
||||
print(f"📚 健康检查: http://{host}:{port}/health")
|
||||
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
|
||||
@@ -0,0 +1,654 @@
|
||||
"""
|
||||
MCP 服务器 - 代码助手 Agent
|
||||
使用 Pydantic AI 和 litellm gateway 提供专业的代码助手功能
|
||||
"""
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from pydantic_ai import Agent
|
||||
|
||||
# 配置 litellm gateway
|
||||
# 从环境变量读取配置,支持云部署
|
||||
_DEFAULT_API_KEY = os.getenv('OPENAI_API_KEY', 'sk')
|
||||
_DEFAULT_BASE_URL = os.getenv('OPENAI_BASE_URL', 'https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1')
|
||||
|
||||
# 设置默认值(仅用于 MCP 服务器初始化,实际调用时会使用动态传入的 API key)
|
||||
os.environ.setdefault('OPENAI_API_KEY', _DEFAULT_API_KEY)
|
||||
os.environ.setdefault('OPENAI_BASE_URL', _DEFAULT_BASE_URL)
|
||||
|
||||
# 创建 MCP 服务器
|
||||
server = FastMCP('代码助手 Agent')
|
||||
|
||||
# 创建 Pydantic AI Agent
|
||||
# 使用 litellm gateway,模型名称:taiji/gpt-4o-mini
|
||||
# 可以通过环境变量 LITELLM_MODEL 自定义模型名称
|
||||
# pydantic_ai 要求模型名称格式为 provider:model_name (例如 openai:taiji/gpt-4o-mini)
|
||||
def ensure_model_prefix(model_name: str) -> str:
|
||||
"""确保模型名称有 openai: 前缀"""
|
||||
if not model_name:
|
||||
return 'openai:taiji/gpt-4o-mini'
|
||||
# 如果已经有 provider: 前缀,直接返回
|
||||
if ':' in model_name:
|
||||
return model_name
|
||||
# 否则添加 openai: 前缀
|
||||
return f'openai:{model_name}'
|
||||
|
||||
model_name = ensure_model_prefix(os.getenv('LITELLM_MODEL', 'taiji/gpt-4o-mini'))
|
||||
|
||||
# 用于存储当前请求的 API key(线程安全)
|
||||
import contextvars
|
||||
_current_api_key: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar('current_api_key', default=None)
|
||||
|
||||
def get_current_api_key() -> str:
|
||||
"""获取当前请求的 API key,如果没有则使用默认值"""
|
||||
api_key = _current_api_key.get()
|
||||
return api_key if api_key else _DEFAULT_API_KEY
|
||||
|
||||
def set_current_api_key(api_key: str):
|
||||
"""设置当前请求的 API key"""
|
||||
_current_api_key.set(api_key)
|
||||
|
||||
def create_agent_with_api_key(api_key: Optional[str] = None) -> Agent:
|
||||
"""创建使用指定 API key 的 Agent 实例"""
|
||||
# 临时设置环境变量
|
||||
if api_key:
|
||||
os.environ['OPENAI_API_KEY'] = api_key
|
||||
|
||||
agent = Agent(
|
||||
model_name,
|
||||
system_prompt=CODE_ASSISTANT_SYSTEM_PROMPT
|
||||
)
|
||||
return agent
|
||||
|
||||
# 系统提示词(提取为常量)
|
||||
CODE_ASSISTANT_SYSTEM_PROMPT = '''你是一个专业的代码助手,具备以下能力:
|
||||
|
||||
1. **代码分析与理解**:深入分析代码的功能、结构和设计模式
|
||||
2. **代码组织**:根据代码职责和项目结构,智能分类和组织代码
|
||||
3. **代码生成**:根据需求生成高质量、符合最佳实践的代码
|
||||
4. **代码重构**:改进代码质量、性能和可维护性
|
||||
5. **代码审查**:发现潜在问题、bug 和改进建议
|
||||
6. **项目规划**:设计合理的项目结构和架构
|
||||
|
||||
**代码分类标准**:
|
||||
- utils/helpers: 工具函数和辅助函数
|
||||
- models/schemas: 数据模型和模式定义
|
||||
- services: 业务逻辑服务层
|
||||
- controllers/handlers: 请求处理层
|
||||
- middleware: 中间件
|
||||
- config: 配置文件
|
||||
- tests: 测试代码
|
||||
- api/routes: API 路由
|
||||
- database: 数据库相关代码
|
||||
- auth: 认证授权相关
|
||||
- validators: 验证器
|
||||
- exceptions: 异常处理
|
||||
- constants: 常量定义
|
||||
- types: 类型定义
|
||||
- decorators: 装饰器
|
||||
- factories: 工厂模式相关
|
||||
|
||||
**响应格式要求**:
|
||||
当需要返回结构化信息(如文件夹路径、文件名)时,请使用 JSON 格式:
|
||||
{
|
||||
"folder_path": "utils",
|
||||
"file_name": "helpers.py",
|
||||
"reason": "这是工具函数,应该放在 utils 文件夹"
|
||||
}
|
||||
|
||||
请始终提供专业、准确、实用的建议。'''
|
||||
|
||||
# 默认 Agent(使用默认 API key,仅用于 MCP 服务器)
|
||||
code_assistant_agent = Agent(
|
||||
model_name,
|
||||
system_prompt=CODE_ASSISTANT_SYSTEM_PROMPT
|
||||
)
|
||||
|
||||
|
||||
def get_agent() -> Agent:
|
||||
"""获取使用当前环境变量 API key 的 Agent 实例
|
||||
|
||||
每次调用都会创建新的 Agent 实例,以确保使用最新的 OPENAI_API_KEY 环境变量
|
||||
"""
|
||||
return Agent(
|
||||
model_name,
|
||||
system_prompt=CODE_ASSISTANT_SYSTEM_PROMPT
|
||||
)
|
||||
|
||||
|
||||
def _parse_ai_response(response: str, code_content: str = '') -> Dict[str, str]:
|
||||
"""解析 AI 响应,提取结构化信息"""
|
||||
# 尝试提取 JSON
|
||||
json_match = re.search(r'\{[^{}]*"folder_path"[^{}]*\}', response, re.DOTALL)
|
||||
if json_match:
|
||||
try:
|
||||
return json.loads(json_match.group())
|
||||
except:
|
||||
pass
|
||||
|
||||
# 尝试提取文件夹路径和文件名
|
||||
folder_path = None
|
||||
file_name = None
|
||||
|
||||
# 查找文件夹路径
|
||||
folder_patterns = [
|
||||
r'文件夹[路径]*[::]\s*([^\n]+)',
|
||||
r'folder[_\s]*path[::]\s*([^\n]+)',
|
||||
r'路径[::]\s*([^\n]+)',
|
||||
]
|
||||
for pattern in folder_patterns:
|
||||
match = re.search(pattern, response, re.IGNORECASE)
|
||||
if match:
|
||||
folder_path = match.group(1).strip().strip('"\'`')
|
||||
break
|
||||
|
||||
# 查找文件名
|
||||
file_patterns = [
|
||||
r'文件[名]*[::]\s*([^\n]+)',
|
||||
r'file[_\s]*name[::]\s*([^\n]+)',
|
||||
r'文件名[::]\s*([^\n]+)',
|
||||
]
|
||||
for pattern in file_patterns:
|
||||
match = re.search(pattern, response, re.IGNORECASE)
|
||||
if match:
|
||||
file_name = match.group(1).strip().strip('"\'`')
|
||||
break
|
||||
|
||||
# 如果没找到,尝试从响应中推断
|
||||
if not folder_path:
|
||||
response_lower = response.lower()
|
||||
if 'utils' in response_lower or 'helper' in response_lower:
|
||||
folder_path = 'utils'
|
||||
elif 'model' in response_lower or 'schema' in response_lower:
|
||||
folder_path = 'models'
|
||||
elif 'service' in response_lower:
|
||||
folder_path = 'services'
|
||||
elif 'controller' in response_lower or 'handler' in response_lower:
|
||||
folder_path = 'controllers'
|
||||
elif 'middleware' in response_lower:
|
||||
folder_path = 'middleware'
|
||||
elif 'config' in response_lower:
|
||||
folder_path = 'config'
|
||||
elif 'test' in response_lower:
|
||||
folder_path = 'tests'
|
||||
elif 'api' in response_lower or 'route' in response_lower:
|
||||
folder_path = 'api'
|
||||
elif 'database' in response_lower or 'db' in response_lower:
|
||||
folder_path = 'database'
|
||||
elif 'auth' in response_lower:
|
||||
folder_path = 'auth'
|
||||
elif 'validator' in response_lower:
|
||||
folder_path = 'validators'
|
||||
elif 'exception' in response_lower:
|
||||
folder_path = 'exceptions'
|
||||
elif 'constant' in response_lower:
|
||||
folder_path = 'constants'
|
||||
else:
|
||||
folder_path = 'utils' # 默认
|
||||
|
||||
if not file_name and code_content:
|
||||
# 尝试从代码中提取类名或函数名
|
||||
try:
|
||||
tree = ast.parse(code_content)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ClassDef):
|
||||
file_name = f"{node.name.lower()}.py"
|
||||
break
|
||||
elif isinstance(node, ast.FunctionDef) and not file_name:
|
||||
file_name = f"{node.name.lower()}.py"
|
||||
except:
|
||||
file_name = 'code.py'
|
||||
|
||||
if not file_name:
|
||||
file_name = 'code.py'
|
||||
|
||||
return {
|
||||
'folder_path': folder_path,
|
||||
'file_name': file_name,
|
||||
'reason': response
|
||||
}
|
||||
|
||||
|
||||
@server.tool()
|
||||
async def organize_code(
|
||||
code_content: str,
|
||||
code_type: Optional[str] = None,
|
||||
project_root: str = '.'
|
||||
) -> str:
|
||||
"""
|
||||
智能分析代码并自动组织到合适的文件夹中
|
||||
|
||||
Args:
|
||||
code_content: 要组织的代码内容
|
||||
code_type: 代码类型(可选),如 'utils', 'models', 'services' 等
|
||||
project_root: 项目根目录路径,默认为当前目录
|
||||
|
||||
Returns:
|
||||
组织结果和文件保存路径
|
||||
"""
|
||||
# 使用 Agent 分析代码并确定最佳文件夹
|
||||
if code_type:
|
||||
prompt = f'''请分析以下代码,确定它应该放在哪个文件夹中。
|
||||
建议的类型是:{code_type}
|
||||
|
||||
代码内容:
|
||||
```python
|
||||
{code_content}
|
||||
```
|
||||
|
||||
请以 JSON 格式返回:
|
||||
{{
|
||||
"folder_path": "文件夹路径(如 utils, models, services)",
|
||||
"file_name": "建议的文件名(如 helpers.py)",
|
||||
"reason": "简要说明为什么选择这个位置"
|
||||
}}'''
|
||||
else:
|
||||
prompt = f'''请分析以下代码,确定它应该放在哪个文件夹中。
|
||||
|
||||
代码内容:
|
||||
```python
|
||||
{code_content}
|
||||
```
|
||||
|
||||
请以 JSON 格式返回:
|
||||
{{
|
||||
"folder_path": "文件夹路径(如 utils, models, services)",
|
||||
"file_name": "建议的文件名(如 helpers.py)",
|
||||
"reason": "简要说明为什么选择这个位置"
|
||||
}}'''
|
||||
|
||||
result = await get_agent().run(prompt)
|
||||
response = result.output
|
||||
|
||||
# 智能解析 AI 响应
|
||||
parsed = _parse_ai_response(response, code_content)
|
||||
folder_path = parsed.get('folder_path', 'utils')
|
||||
file_name = parsed.get('file_name', 'code.py')
|
||||
reason = parsed.get('reason', response)
|
||||
|
||||
# 确保文件名有 .py 扩展名
|
||||
if not file_name.endswith('.py'):
|
||||
file_name += '.py'
|
||||
|
||||
# 创建完整路径
|
||||
full_folder_path = os.path.join(project_root, folder_path)
|
||||
Path(full_folder_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 保存代码到文件
|
||||
file_path = os.path.join(full_folder_path, file_name)
|
||||
|
||||
# 如果文件已存在,添加序号
|
||||
if os.path.exists(file_path):
|
||||
base_name = file_name[:-3]
|
||||
counter = 1
|
||||
while os.path.exists(file_path):
|
||||
file_name = f"{base_name}_{counter}.py"
|
||||
file_path = os.path.join(full_folder_path, file_name)
|
||||
counter += 1
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(code_content)
|
||||
|
||||
return f'''✅ 代码已成功组织!
|
||||
|
||||
📁 文件夹路径: {full_folder_path}
|
||||
📄 文件名: {file_name}
|
||||
💾 完整路径: {file_path}
|
||||
|
||||
🤖 AI 分析:
|
||||
{reason}'''
|
||||
|
||||
|
||||
@server.tool()
|
||||
async def suggest_folder_structure(project_description: str) -> str:
|
||||
"""
|
||||
根据项目描述,建议合理的文件夹结构
|
||||
|
||||
Args:
|
||||
project_description: 项目描述
|
||||
|
||||
Returns:
|
||||
建议的文件夹结构
|
||||
"""
|
||||
prompt = f'''根据以下项目描述,建议一个合理的文件夹结构:
|
||||
|
||||
项目描述:{project_description}
|
||||
|
||||
请提供:
|
||||
1. 推荐的文件夹结构(树状图)
|
||||
2. 每个文件夹的用途说明
|
||||
3. 文件夹之间的依赖关系'''
|
||||
|
||||
result = await get_agent().run(prompt)
|
||||
return result.output
|
||||
|
||||
|
||||
@server.tool()
|
||||
async def classify_code(code_content: str) -> str:
|
||||
"""
|
||||
分析代码内容,确定其应该属于哪个类别/文件夹
|
||||
|
||||
Args:
|
||||
code_content: 代码内容
|
||||
|
||||
Returns:
|
||||
代码分类建议
|
||||
"""
|
||||
prompt = f'''请分析以下代码,确定它的功能和应该属于的类别:
|
||||
|
||||
代码内容:
|
||||
```python
|
||||
{code_content}
|
||||
```
|
||||
|
||||
请提供:
|
||||
1. 代码的主要功能
|
||||
2. 建议的文件夹分类
|
||||
3. 推荐的文件名
|
||||
4. 是否需要其他相关文件'''
|
||||
|
||||
result = await get_agent().run(prompt)
|
||||
return result.output
|
||||
|
||||
|
||||
@server.tool()
|
||||
async def generate_code(
|
||||
requirement: str,
|
||||
language: str = 'python',
|
||||
style: Optional[str] = None,
|
||||
project_root: str = '.'
|
||||
) -> str:
|
||||
"""
|
||||
根据需求生成代码
|
||||
|
||||
Args:
|
||||
requirement: 代码需求描述
|
||||
language: 编程语言,默认为 'python'
|
||||
style: 代码风格(可选),如 'fastapi', 'django', 'flask' 等
|
||||
project_root: 项目根目录路径,默认为当前目录
|
||||
|
||||
Returns:
|
||||
生成的代码和保存路径
|
||||
"""
|
||||
prompt = f'''请根据以下需求生成高质量的 {language} 代码:
|
||||
|
||||
需求:{requirement}
|
||||
{f"代码风格:{style}" if style else ""}
|
||||
|
||||
要求:
|
||||
1. 代码要符合最佳实践
|
||||
2. 包含适当的注释和文档字符串
|
||||
3. 遵循 PEP 8(如果是 Python)
|
||||
4. 包含错误处理
|
||||
5. 代码要完整、可运行
|
||||
|
||||
请生成代码,并在代码后说明:
|
||||
1. 代码的主要功能
|
||||
2. 建议保存的文件夹路径
|
||||
3. 建议的文件名'''
|
||||
|
||||
result = await get_agent().run(prompt)
|
||||
response = result.output
|
||||
|
||||
# 提取代码块
|
||||
code_match = re.search(r'```(?:python|py)?\n(.*?)```', response, re.DOTALL)
|
||||
if code_match:
|
||||
generated_code = code_match.group(1).strip()
|
||||
else:
|
||||
# 如果没有代码块,尝试提取整个响应
|
||||
generated_code = response
|
||||
|
||||
# 解析建议的保存位置
|
||||
parsed = _parse_ai_response(response, generated_code)
|
||||
folder_path = parsed.get('folder_path', 'generated')
|
||||
file_name = parsed.get('file_name', 'generated_code.py')
|
||||
|
||||
# 确保文件名有正确的扩展名
|
||||
if language == 'python' and not file_name.endswith('.py'):
|
||||
file_name += '.py'
|
||||
|
||||
# 保存代码
|
||||
full_folder_path = os.path.join(project_root, folder_path)
|
||||
Path(full_folder_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_path = os.path.join(full_folder_path, file_name)
|
||||
if os.path.exists(file_path):
|
||||
base_name = file_name[:-3] if file_name.endswith('.py') else file_name
|
||||
counter = 1
|
||||
while os.path.exists(file_path):
|
||||
new_name = f"{base_name}_{counter}.py" if file_name.endswith('.py') else f"{base_name}_{counter}"
|
||||
file_path = os.path.join(full_folder_path, new_name)
|
||||
counter += 1
|
||||
file_name = os.path.basename(file_path)
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(generated_code)
|
||||
|
||||
return f'''✅ 代码生成成功!
|
||||
|
||||
📁 文件夹: {full_folder_path}
|
||||
📄 文件名: {file_name}
|
||||
💾 完整路径: {file_path}
|
||||
|
||||
📝 生成的代码:
|
||||
```{language}
|
||||
{generated_code}
|
||||
```
|
||||
|
||||
💡 AI 说明:
|
||||
{response}'''
|
||||
|
||||
|
||||
@server.tool()
|
||||
async def refactor_code(
|
||||
code_content: str,
|
||||
refactoring_goal: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
重构代码,改进代码质量、性能和可维护性
|
||||
|
||||
Args:
|
||||
code_content: 要重构的代码
|
||||
refactoring_goal: 重构目标(可选),如 'improve performance', 'add error handling' 等
|
||||
|
||||
Returns:
|
||||
重构后的代码和改进说明
|
||||
"""
|
||||
goal_text = f"\n重构目标:{refactoring_goal}" if refactoring_goal else ""
|
||||
|
||||
prompt = f'''请重构以下代码,改进代码质量、性能和可维护性:{goal_text}
|
||||
|
||||
原始代码:
|
||||
```python
|
||||
{code_content}
|
||||
```
|
||||
|
||||
请:
|
||||
1. 分析代码的问题和改进点
|
||||
2. 提供重构后的代码
|
||||
3. 说明做了哪些改进
|
||||
4. 如果可能,提供性能对比或改进建议'''
|
||||
|
||||
result = await get_agent().run(prompt)
|
||||
response = result.output
|
||||
|
||||
# 提取重构后的代码
|
||||
code_match = re.search(r'```(?:python|py)?\n(.*?)```', response, re.DOTALL)
|
||||
if code_match:
|
||||
refactored_code = code_match.group(1).strip()
|
||||
else:
|
||||
refactored_code = code_content # 如果没有找到,返回原代码
|
||||
|
||||
return f'''✅ 代码重构完成!
|
||||
|
||||
📝 重构后的代码:
|
||||
```python
|
||||
{refactored_code}
|
||||
```
|
||||
|
||||
💡 重构说明:
|
||||
{response}'''
|
||||
|
||||
|
||||
@server.tool()
|
||||
async def review_code(
|
||||
code_content: str,
|
||||
file_path: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
审查代码,发现潜在问题、bug 和改进建议
|
||||
|
||||
Args:
|
||||
code_content: 要审查的代码
|
||||
file_path: 文件路径(可选),用于上下文
|
||||
|
||||
Returns:
|
||||
代码审查报告
|
||||
"""
|
||||
file_context = f"\n文件路径:{file_path}" if file_path else ""
|
||||
|
||||
prompt = f'''请审查以下代码,提供详细的代码审查报告:{file_context}
|
||||
|
||||
代码内容:
|
||||
```python
|
||||
{code_content}
|
||||
```
|
||||
|
||||
请检查:
|
||||
1. **潜在 Bug**:逻辑错误、边界条件、异常处理
|
||||
2. **代码质量**:可读性、可维护性、代码风格
|
||||
3. **性能问题**:性能瓶颈、优化建议
|
||||
4. **安全性**:安全漏洞、输入验证
|
||||
5. **最佳实践**:是否符合语言和框架的最佳实践
|
||||
6. **改进建议**:具体的改进方案
|
||||
|
||||
请以结构化的方式提供审查报告。'''
|
||||
|
||||
result = await get_agent().run(prompt)
|
||||
return result.output
|
||||
|
||||
|
||||
@server.tool()
|
||||
async def analyze_project(
|
||||
project_root: str = '.',
|
||||
max_depth: int = 3
|
||||
) -> str:
|
||||
"""
|
||||
分析项目结构,提供项目概览和建议
|
||||
|
||||
Args:
|
||||
project_root: 项目根目录路径,默认为当前目录
|
||||
max_depth: 最大扫描深度,默认为 3
|
||||
|
||||
Returns:
|
||||
项目分析报告
|
||||
"""
|
||||
project_path = Path(project_root)
|
||||
if not project_path.exists():
|
||||
return f"❌ 项目路径不存在:{project_root}"
|
||||
|
||||
# 扫描项目结构
|
||||
structure = []
|
||||
python_files = []
|
||||
|
||||
for root, dirs, files in os.walk(project_path):
|
||||
# 跳过隐藏文件夹和常见的不需要扫描的文件夹
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ['__pycache__', 'node_modules', 'venv', '.venv']]
|
||||
|
||||
level = root.replace(str(project_path), '').count(os.sep)
|
||||
if level <= max_depth:
|
||||
indent = ' ' * level
|
||||
folder_name = os.path.basename(root) or project_path.name
|
||||
structure.append(f"{indent}{folder_name}/")
|
||||
|
||||
for file in files:
|
||||
if file.endswith('.py'):
|
||||
python_files.append(os.path.join(root, file))
|
||||
structure.append(f"{indent} {file}")
|
||||
|
||||
structure_text = '\n'.join(structure[:50]) # 限制输出长度
|
||||
|
||||
# 统计信息
|
||||
total_py_files = len(python_files)
|
||||
|
||||
prompt = f'''请分析以下项目结构,提供项目概览和改进建议:
|
||||
|
||||
项目结构:
|
||||
{structure_text}
|
||||
|
||||
统计信息:
|
||||
- Python 文件数量:{total_py_files}
|
||||
- 项目根目录:{project_root}
|
||||
|
||||
请提供:
|
||||
1. **项目类型识别**:这是什么类型的项目(Web应用、API服务、库等)
|
||||
2. **结构评估**:文件夹结构是否合理
|
||||
3. **改进建议**:如何优化项目结构
|
||||
4. **缺失组件**:可能缺失的重要组件或文件夹
|
||||
5. **依赖关系**:文件夹之间的依赖关系分析'''
|
||||
|
||||
result = await get_agent().run(prompt)
|
||||
|
||||
return f'''📊 项目分析报告
|
||||
|
||||
📁 项目路径: {project_root}
|
||||
📄 Python 文件数: {total_py_files}
|
||||
|
||||
📋 项目结构(前50项):
|
||||
{structure_text}
|
||||
|
||||
🤖 AI 分析:
|
||||
{result.output}'''
|
||||
|
||||
|
||||
@server.tool()
|
||||
async def create_code_file(
|
||||
code_content: str,
|
||||
folder_path: str,
|
||||
file_name: str,
|
||||
project_root: str = '.'
|
||||
) -> str:
|
||||
"""
|
||||
在指定文件夹中创建代码文件
|
||||
|
||||
Args:
|
||||
code_content: 代码内容
|
||||
folder_path: 目标文件夹路径(相对于项目根目录)
|
||||
file_name: 文件名
|
||||
project_root: 项目根目录路径,默认为当前目录
|
||||
|
||||
Returns:
|
||||
创建结果
|
||||
"""
|
||||
full_folder_path = os.path.join(project_root, folder_path)
|
||||
Path(full_folder_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_path = os.path.join(full_folder_path, file_name)
|
||||
|
||||
# 如果文件已存在,询问是否覆盖(这里直接覆盖,实际可以添加参数控制)
|
||||
if os.path.exists(file_path):
|
||||
backup_path = file_path + '.backup'
|
||||
with open(backup_path, 'w', encoding='utf-8') as f:
|
||||
with open(file_path, 'r', encoding='utf-8') as orig:
|
||||
f.write(orig.read())
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(code_content)
|
||||
|
||||
backup_msg = f"\n⚠️ 原文件已备份到: {backup_path}" if os.path.exists(file_path + '.backup') else ""
|
||||
|
||||
return f'''✅ 文件创建成功!
|
||||
|
||||
📁 文件夹: {full_folder_path}
|
||||
📄 文件名: {file_name}
|
||||
💾 完整路径: {file_path}{backup_msg}'''
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
server.run()
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
测试模块
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
测试代码助手 Agent 是否正常运行
|
||||
直接测试 Agent 功能,验证能否正常返回数据
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 确保使用虚拟环境
|
||||
venv_path = Path(__file__).parent / '.venv'
|
||||
if venv_path.exists():
|
||||
venv_python = venv_path / 'bin' / 'python'
|
||||
if venv_python.exists():
|
||||
print(f"✅ 检测到虚拟环境: {venv_path}")
|
||||
else:
|
||||
print(f"⚠️ 虚拟环境存在但 Python 可执行文件未找到")
|
||||
|
||||
# 设置环境变量 - 配置 litellm gateway
|
||||
os.environ['OPENAI_API_KEY'] = 'sk-'
|
||||
# 设置 litellm gateway 的 base URL
|
||||
os.environ['OPENAI_BASE_URL'] = 'https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1'
|
||||
|
||||
# 导入 Pydantic AI
|
||||
try:
|
||||
from pydantic_ai import Agent
|
||||
print("✅ Pydantic AI 导入成功")
|
||||
except ImportError as e:
|
||||
print(f"❌ Pydantic AI 导入失败: {e}")
|
||||
print("请运行: pip install -r requirements.txt")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
async def test_agent_basic():
|
||||
"""测试 Agent 基本功能"""
|
||||
print("\n" + "=" * 70)
|
||||
print("测试 1: Agent 基本功能测试")
|
||||
print("=" * 70)
|
||||
|
||||
try:
|
||||
# 创建 Agent
|
||||
model_name = os.getenv('LITELLM_MODEL', 'openai:taiji/gpt-4o-mini')
|
||||
print(f"📌 使用模型: {model_name}")
|
||||
|
||||
agent = Agent(
|
||||
model_name,
|
||||
system_prompt='你是一个专业的代码助手。'
|
||||
)
|
||||
|
||||
# 测试简单查询
|
||||
print("\n🤖 发送测试查询...")
|
||||
result = await agent.run("请用一句话介绍你自己,并确认你能正常工作")
|
||||
|
||||
print(f"\n✅ Agent 响应成功!")
|
||||
print(f"📝 响应内容:\n{result.output}")
|
||||
print(f"\n📊 响应统计:")
|
||||
print(f" - 响应长度: {len(result.output)} 字符")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Agent 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
async def test_code_classification():
|
||||
"""测试代码分类功能"""
|
||||
print("\n" + "=" * 70)
|
||||
print("测试 2: 代码分类功能测试")
|
||||
print("=" * 70)
|
||||
|
||||
try:
|
||||
model_name = os.getenv('LITELLM_MODEL', 'openai:taiji/gpt-4o-mini')
|
||||
agent = Agent(
|
||||
model_name,
|
||||
system_prompt='''你是一个专业的代码助手,擅长分析和分类代码。
|
||||
请根据代码功能,确定它应该属于哪个文件夹类别。'''
|
||||
)
|
||||
|
||||
test_code = """
|
||||
def calculate_total(items):
|
||||
\"\"\"计算商品总价\"\"\"
|
||||
total = sum(item['price'] * item['quantity'] for item in items)
|
||||
return total
|
||||
"""
|
||||
|
||||
print("\n📝 测试代码:")
|
||||
print(test_code)
|
||||
print("\n🤖 分析代码...")
|
||||
|
||||
prompt = f'''请分析以下代码,确定它应该放在哪个文件夹中:
|
||||
|
||||
代码内容:
|
||||
```python
|
||||
{test_code}
|
||||
```
|
||||
|
||||
请提供:
|
||||
1. 最适合的文件夹路径
|
||||
2. 建议的文件名
|
||||
3. 简要说明为什么选择这个位置'''
|
||||
|
||||
result = await agent.run(prompt)
|
||||
|
||||
print(f"\n✅ 代码分类成功!")
|
||||
print(f"📝 AI 分析结果:\n{result.output}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 代码分类测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
async def test_code_generation():
|
||||
"""测试代码生成功能"""
|
||||
print("\n" + "=" * 70)
|
||||
print("测试 3: 代码生成功能测试")
|
||||
print("=" * 70)
|
||||
|
||||
try:
|
||||
model_name = os.getenv('LITELLM_MODEL', 'openai:taiji/gpt-4o-mini')
|
||||
agent = Agent(
|
||||
model_name,
|
||||
system_prompt='''你是一个专业的代码助手,擅长生成高质量的代码。
|
||||
请根据需求生成符合最佳实践的代码。'''
|
||||
)
|
||||
|
||||
requirement = "创建一个简单的工具函数,用于格式化日期字符串"
|
||||
|
||||
print(f"\n📋 需求: {requirement}")
|
||||
print("\n🤖 生成代码...")
|
||||
|
||||
prompt = f'''请根据以下需求生成 Python 代码:
|
||||
|
||||
需求:{requirement}
|
||||
|
||||
要求:
|
||||
1. 代码要符合最佳实践
|
||||
2. 包含适当的注释和文档字符串
|
||||
3. 遵循 PEP 8
|
||||
4. 包含错误处理
|
||||
|
||||
请生成代码:'''
|
||||
|
||||
result = await agent.run(prompt)
|
||||
|
||||
print(f"\n✅ 代码生成成功!")
|
||||
print(f"📝 生成的代码:\n{result.output}")
|
||||
|
||||
# 检查是否包含代码
|
||||
if 'def ' in result.output or 'import ' in result.output:
|
||||
print("\n✅ 响应包含代码内容")
|
||||
else:
|
||||
print("\n⚠️ 响应可能不包含代码,请检查")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 代码生成测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
async def test_code_review():
|
||||
"""测试代码审查功能"""
|
||||
print("\n" + "=" * 70)
|
||||
print("测试 4: 代码审查功能测试")
|
||||
print("=" * 70)
|
||||
|
||||
try:
|
||||
model_name = os.getenv('LITELLM_MODEL', 'openai:taiji/gpt-4o-mini')
|
||||
agent = Agent(
|
||||
model_name,
|
||||
system_prompt='''你是一个专业的代码审查员,擅长发现代码问题。
|
||||
请仔细审查代码,发现潜在问题并提供改进建议。'''
|
||||
)
|
||||
|
||||
code_to_review = """
|
||||
def get_user(id):
|
||||
users = {
|
||||
1: {'name': 'Alice', 'age': 30},
|
||||
2: {'name': 'Bob', 'age': 25}
|
||||
}
|
||||
return users[id]
|
||||
"""
|
||||
|
||||
print("\n📝 待审查代码:")
|
||||
print(code_to_review)
|
||||
print("\n🤖 审查代码...")
|
||||
|
||||
prompt = f'''请审查以下代码,发现潜在问题:
|
||||
|
||||
代码:
|
||||
```python
|
||||
{code_to_review}
|
||||
```
|
||||
|
||||
请检查:
|
||||
1. 潜在的 Bug
|
||||
2. 代码质量问题
|
||||
3. 改进建议'''
|
||||
|
||||
result = await agent.run(prompt)
|
||||
|
||||
print(f"\n✅ 代码审查成功!")
|
||||
print(f"📝 审查结果:\n{result.output}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 代码审查测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
async def main():
|
||||
"""主测试函数"""
|
||||
print("=" * 70)
|
||||
print("🧪 代码助手 Agent 功能测试")
|
||||
print("=" * 70)
|
||||
|
||||
# 检查环境
|
||||
print(f"\n📋 环境信息:")
|
||||
print(f" - Python 版本: {sys.version}")
|
||||
print(f" - 工作目录: {os.getcwd()}")
|
||||
print(f" - API Key: {os.environ.get('OPENAI_API_KEY', '未设置')[:20]}...")
|
||||
print(f" - Base URL: {os.environ.get('OPENAI_BASE_URL', '未设置')}")
|
||||
|
||||
# 运行测试
|
||||
results = []
|
||||
|
||||
results.append(await test_agent_basic())
|
||||
results.append(await test_code_classification())
|
||||
results.append(await test_code_generation())
|
||||
results.append(await test_code_review())
|
||||
|
||||
# 总结
|
||||
print("\n" + "=" * 70)
|
||||
print("📊 测试结果总结")
|
||||
print("=" * 70)
|
||||
|
||||
passed = sum(results)
|
||||
total = len(results)
|
||||
|
||||
print(f"\n✅ 通过: {passed}/{total}")
|
||||
print(f"❌ 失败: {total - passed}/{total}")
|
||||
|
||||
if passed == total:
|
||||
print("\n🎉 所有测试通过!Agent 运行正常!")
|
||||
return 0
|
||||
else:
|
||||
print("\n⚠️ 部分测试失败,请检查配置和网络连接")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
快速测试 MCP 服务器配置
|
||||
验证 Agent 和工具是否正常工作
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.server.mcp_server import code_assistant_agent, organize_code, classify_code
|
||||
|
||||
|
||||
async def test_agent():
|
||||
"""测试 Agent 基本功能"""
|
||||
print("测试 Agent 基本功能...")
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
result = await code_assistant_agent.run("请用一句话介绍你自己")
|
||||
print("✅ Agent 响应成功:")
|
||||
print(result.output)
|
||||
print()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Agent 测试失败: {e}")
|
||||
print()
|
||||
return False
|
||||
|
||||
|
||||
async def test_classify_code():
|
||||
"""测试代码分类功能"""
|
||||
print("测试代码分类功能...")
|
||||
print("-" * 60)
|
||||
|
||||
test_code = """
|
||||
def calculate_sum(numbers):
|
||||
return sum(numbers)
|
||||
"""
|
||||
|
||||
try:
|
||||
result = await classify_code(test_code)
|
||||
print("✅ 代码分类成功:")
|
||||
print(result)
|
||||
print()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 代码分类测试失败: {e}")
|
||||
print()
|
||||
return False
|
||||
|
||||
|
||||
async def main():
|
||||
"""主测试函数"""
|
||||
print("=" * 60)
|
||||
print("MCP 服务器配置测试")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
print(f"API Key: {os.environ.get('OPENAI_API_KEY', '未设置')[:20]}...")
|
||||
print(f"Base URL: {os.environ.get('OPENAI_BASE_URL', '使用默认')}")
|
||||
print()
|
||||
|
||||
# 测试 Agent
|
||||
agent_ok = await test_agent()
|
||||
|
||||
# 测试工具
|
||||
tool_ok = await test_classify_code()
|
||||
|
||||
print("=" * 60)
|
||||
if agent_ok and tool_ok:
|
||||
print("✅ 所有测试通过!")
|
||||
else:
|
||||
print("⚠️ 部分测试失败,请检查配置")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Dockerfile for Facebook Agent 服务
|
||||
FROM python:3.12-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 设置环境变量
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt ./requirements.txt
|
||||
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
# 8000: API 服务端口
|
||||
# 8001: MCP HTTP 服务端口
|
||||
EXPOSE 8000 8001
|
||||
|
||||
# 健康检查(默认检查8000端口)
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
# 启动命令(默认启动API服务器)
|
||||
CMD ["python", "run_api.py"]
|
||||
@@ -0,0 +1,42 @@
|
||||
# Dockerfile for Facebook Agent 服务
|
||||
FROM python:3.12-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 设置环境变量
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制依赖文件(构建上下文是 ..,所以路径是 facebook_agent/requirements.txt)
|
||||
COPY facebook_agent/requirements.txt ./requirements.txt
|
||||
|
||||
# 安装 Python 依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制应用代码(构建上下文是 ..,所以从 facebook_agent/ 复制到 ./facebook_agent/)
|
||||
# 这会将 aks_agent/facebook_agent/ 的内容复制到 /app/facebook_agent/
|
||||
COPY facebook_agent/ ./facebook_agent/
|
||||
|
||||
# 安装curl用于健康检查
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 暴露端口
|
||||
# 8000: API 服务端口
|
||||
# 8001: MCP HTTP 服务端口
|
||||
EXPOSE 8000 8001
|
||||
|
||||
# 健康检查(默认检查8000端口,MCP服务会覆盖)
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
# 启动命令(默认启动API服务器,可通过command覆盖)
|
||||
CMD ["python", "-m", "uvicorn", "facebook_agent.api:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Facebook搜索智能Agent
|
||||
基于Pydantic AI框架,使用LiteLLM Gateway调用模型
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
# 导出主要接口
|
||||
from .config import Config
|
||||
from .agent import FacebookAgent
|
||||
from .models.schemas import SearchRequest, SearchResponse, SearchResultItem
|
||||
|
||||
__all__ = [
|
||||
"Config",
|
||||
"FacebookAgent",
|
||||
"SearchRequest",
|
||||
"SearchResponse",
|
||||
"SearchResultItem",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Agent模块
|
||||
包含Facebook搜索Agent的核心逻辑
|
||||
"""
|
||||
|
||||
from .facebook_agent import FacebookAgent, FacebookAgentDeps
|
||||
|
||||
__all__ = ["FacebookAgent", "FacebookAgentDeps"]
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Facebook搜索智能Agent
|
||||
基于Pydantic AI框架实现(简化版本,直接使用客户端)
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from loguru import logger
|
||||
|
||||
# 支持相对导入和绝对导入
|
||||
try:
|
||||
from ..config import Config
|
||||
from ..models.schemas import SearchRequest, SearchResponse, SearchResultItem
|
||||
from ..clients.facebook_client import FacebookClient
|
||||
from ..clients.litellm_client import LiteLLMClient
|
||||
except ImportError:
|
||||
# 如果相对导入失败,尝试绝对导入
|
||||
from config import Config
|
||||
from models.schemas import SearchRequest, SearchResponse, SearchResultItem
|
||||
from clients.facebook_client import FacebookClient
|
||||
from clients.litellm_client import LiteLLMClient
|
||||
|
||||
|
||||
# 定义Agent依赖类型
|
||||
class FacebookAgentDeps:
|
||||
"""Agent依赖项"""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.facebook_client = FacebookClient(config)
|
||||
self.llm_client = LiteLLMClient(config)
|
||||
|
||||
|
||||
class FacebookAgent:
|
||||
"""Facebook搜索Agent包装类"""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
"""
|
||||
初始化Agent
|
||||
|
||||
Args:
|
||||
config: 配置对象
|
||||
"""
|
||||
self.config = config
|
||||
self.deps = FacebookAgentDeps(config)
|
||||
logger.info("FacebookAgent 初始化完成")
|
||||
|
||||
async def search(self, request: SearchRequest) -> SearchResponse:
|
||||
"""
|
||||
执行搜索
|
||||
|
||||
Args:
|
||||
request: 搜索请求
|
||||
|
||||
Returns:
|
||||
搜索响应
|
||||
"""
|
||||
try:
|
||||
# 直接调用搜索工具获取结果
|
||||
search_results = await self.deps.facebook_client.search(
|
||||
request.query,
|
||||
request.limit
|
||||
)
|
||||
|
||||
# 生成总结
|
||||
summary = None
|
||||
if search_results:
|
||||
try:
|
||||
results_dict = [
|
||||
{
|
||||
"title": r.title,
|
||||
"snippet": r.snippet,
|
||||
"author": r.author,
|
||||
"likes": r.likes
|
||||
}
|
||||
for r in search_results
|
||||
]
|
||||
summary = await self.deps.llm_client.generate_summary(
|
||||
request.query,
|
||||
results_dict
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"生成总结失败: {e}")
|
||||
|
||||
return SearchResponse(
|
||||
success=True,
|
||||
query=request.query,
|
||||
results=search_results,
|
||||
total_count=len(search_results),
|
||||
summary=summary
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"搜索过程出错: {e}")
|
||||
return SearchResponse(
|
||||
success=False,
|
||||
query=request.query,
|
||||
results=[],
|
||||
message=f"搜索失败: {str(e)}"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
"""
|
||||
FastAPI服务 - Facebook搜索智能Agent
|
||||
提供搜索API接口和MCP协议支持
|
||||
"""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from typing import Dict, Any, Optional, AsyncGenerator
|
||||
from fastapi import FastAPI, HTTPException, Request, Header, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
import sys
|
||||
from typing import Optional
|
||||
from loguru import logger
|
||||
|
||||
# 支持相对导入和绝对导入
|
||||
try:
|
||||
from .config import Config
|
||||
from .agent import FacebookAgent
|
||||
from .models.schemas import SearchRequest, SearchResponse
|
||||
from .mcp_server import search_facebook, initialize_agent
|
||||
except ImportError:
|
||||
# 如果相对导入失败,尝试绝对导入
|
||||
from config import Config
|
||||
from agent import FacebookAgent
|
||||
from models.schemas import SearchRequest, SearchResponse
|
||||
from mcp_server import search_facebook, initialize_agent
|
||||
|
||||
|
||||
# ==================== FastAPI应用 ====================
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
title="Facebook搜索智能Agent API",
|
||||
description="提供Facebook内容搜索服务和MCP协议支持,基于Pydantic AI框架和LiteLLM Gateway",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 添加CORS中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 全局变量
|
||||
config: Optional[Config] = None
|
||||
agent: Optional[FacebookAgent] = None
|
||||
|
||||
# MCP 工具映射
|
||||
TOOL_MAP = {
|
||||
'search_facebook': search_facebook,
|
||||
}
|
||||
|
||||
# Session 管理
|
||||
sessions: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
|
||||
class MCPRequest(BaseModel):
|
||||
"""MCP JSON-RPC 请求"""
|
||||
jsonrpc: str = "2.0"
|
||||
id: Optional[str] = None
|
||||
method: str
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class MCPResponse(BaseModel):
|
||||
"""MCP JSON-RPC 响应"""
|
||||
jsonrpc: str = "2.0"
|
||||
id: Optional[str] = None
|
||||
result: Optional[Any] = None
|
||||
error: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
async def handle_mcp_request(request_data: Dict[str, Any], session_id: Optional[str] = None, api_key: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""处理 MCP 请求"""
|
||||
method = request_data.get("method")
|
||||
params = request_data.get("params", {})
|
||||
request_id = request_data.get("id")
|
||||
|
||||
# 对于 tools/call 方法,需要验证 API key
|
||||
if method == "tools/call":
|
||||
if not api_key or api_key.strip() == "" or api_key.strip() == "sk":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {
|
||||
"code": -32001,
|
||||
"message": "缺少 API key。请在请求头中提供 'api-key' 或 'Authorization: Bearer <token>'。"
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
if method == "initialize":
|
||||
# 初始化会话
|
||||
if not session_id:
|
||||
session_id = str(uuid.uuid4())
|
||||
sessions[session_id] = {
|
||||
"initialized": True,
|
||||
"capabilities": {}
|
||||
}
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {},
|
||||
"resources": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "Facebook搜索Agent",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "tools/list":
|
||||
# 列出所有工具
|
||||
tools = [
|
||||
{
|
||||
"name": "search_facebook",
|
||||
"description": "搜索Facebook内容,返回相关帖子和AI生成的总结。支持搜索关键词,返回帖子标题、链接、摘要、作者、点赞数等信息,并提供AI生成的总结。",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "搜索关键词,例如:technology news、travel tips、food recipes等"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "返回结果数量,默认5,最大20",
|
||||
"default": 5,
|
||||
"minimum": 1,
|
||||
"maximum": 20
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"tools": tools
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "tools/call":
|
||||
# 调用工具
|
||||
tool_name = params.get("name")
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
if tool_name not in TOOL_MAP:
|
||||
raise ValueError(f"Tool '{tool_name}' not found")
|
||||
|
||||
# 如果提供了 API key,临时更新配置
|
||||
if api_key and agent is not None:
|
||||
old_api_key = agent.config.litellm_api_key
|
||||
agent.config.litellm_api_key = api_key
|
||||
# 重新创建 LLM 客户端以使用新的 API key
|
||||
try:
|
||||
from .clients.litellm_client import LiteLLMClient
|
||||
except ImportError:
|
||||
from clients.litellm_client import LiteLLMClient
|
||||
agent.deps.llm_client = LiteLLMClient(agent.config)
|
||||
|
||||
try:
|
||||
# 获取工具函数
|
||||
tool_func = TOOL_MAP[tool_name]
|
||||
|
||||
# 调用工具(异步)
|
||||
result = await tool_func(**arguments)
|
||||
finally:
|
||||
# 恢复原来的 API key
|
||||
if api_key and agent is not None and 'old_api_key' in locals():
|
||||
agent.config.litellm_api_key = old_api_key
|
||||
try:
|
||||
from .clients.litellm_client import LiteLLMClient
|
||||
except ImportError:
|
||||
from clients.litellm_client import LiteLLMClient
|
||||
agent.deps.llm_client = LiteLLMClient(agent.config)
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": str(result)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "ping":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {}
|
||||
}
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown method: {method}")
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": str(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def setup_logger():
|
||||
"""配置日志"""
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
level=config.log_level if config else "INFO",
|
||||
format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||
)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""应用启动时初始化"""
|
||||
global config, agent
|
||||
|
||||
try:
|
||||
# 加载配置
|
||||
config = Config.from_env()
|
||||
config.validate()
|
||||
|
||||
# 配置日志
|
||||
setup_logger()
|
||||
|
||||
# 创建Agent
|
||||
agent = FacebookAgent(config)
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info("Facebook搜索智能Agent API 启动成功")
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"LiteLLM Gateway: {config.litellm_gateway_url}")
|
||||
logger.info(f"模型: {config.litellm_model}")
|
||||
logger.info(f"Facebook API Host: {config.facebook_api_host}")
|
||||
logger.info("MCP服务器已就绪,支持HTTP/SSE传输")
|
||||
|
||||
# 初始化MCP Agent(如果还没有初始化)
|
||||
try:
|
||||
initialize_agent()
|
||||
except Exception:
|
||||
pass # 如果已经初始化,忽略错误
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@app.get("/", tags=["健康检查"])
|
||||
async def root():
|
||||
"""根路径 - 服务信息"""
|
||||
return {
|
||||
"service": "Facebook搜索智能Agent API",
|
||||
"status": "running",
|
||||
"version": "1.0.0",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"endpoints": {
|
||||
"api": {
|
||||
"search": "/search",
|
||||
"config": "/config",
|
||||
"health": "/health"
|
||||
},
|
||||
"mcp": {
|
||||
"http": "/mcp",
|
||||
"sse": "/mcp/sse"
|
||||
}
|
||||
},
|
||||
"tools": list(TOOL_MAP.keys())
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health", tags=["健康检查"])
|
||||
async def health_check():
|
||||
"""健康检查接口"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "Facebook搜索智能Agent API + MCP Server",
|
||||
"agent_initialized": agent is not None,
|
||||
"tools_count": len(TOOL_MAP),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
# ==================== API Key 验证 ====================
|
||||
|
||||
async def verify_api_key(
|
||||
api_key: Optional[str] = Header(None, alias="api-key"),
|
||||
authorization: Optional[str] = Header(None)
|
||||
) -> str:
|
||||
"""
|
||||
验证 API Key
|
||||
|
||||
支持从以下位置获取 API key:
|
||||
1. api-key 请求头
|
||||
2. Authorization: Bearer <token> 请求头
|
||||
|
||||
如果没有提供 API key,返回 401 错误
|
||||
"""
|
||||
# 从 api-key 请求头获取
|
||||
if api_key:
|
||||
if not api_key.strip() or api_key.strip() == "sk":
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="无效的 API key。请提供有效的 API key。"
|
||||
)
|
||||
return api_key.strip()
|
||||
|
||||
# 从 Authorization 请求头获取
|
||||
if authorization:
|
||||
if authorization.startswith("Bearer "):
|
||||
api_key = authorization[7:].strip()
|
||||
else:
|
||||
api_key = authorization.strip()
|
||||
|
||||
if not api_key or api_key == "sk":
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="无效的 API key。请提供有效的 API key。"
|
||||
)
|
||||
return api_key
|
||||
|
||||
# 如果没有提供 API key,返回错误
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="缺少 API key。请在请求头中提供 'api-key' 或 'Authorization: Bearer <token>'。"
|
||||
)
|
||||
|
||||
|
||||
@app.post("/search", response_model=SearchResponse, tags=["搜索"])
|
||||
async def search(request: SearchRequest, api_key: str = Depends(verify_api_key)):
|
||||
"""
|
||||
执行Facebook搜索
|
||||
|
||||
- **query**: 搜索关键词(必填)
|
||||
- **limit**: 返回结果数量(可选,默认5,最大20)
|
||||
|
||||
返回:
|
||||
- 搜索结果列表
|
||||
- AI生成的总结
|
||||
- 统计信息
|
||||
"""
|
||||
if agent is None:
|
||||
raise HTTPException(status_code=503, detail="服务未初始化")
|
||||
|
||||
try:
|
||||
# 如果提供了 API key,临时更新配置
|
||||
if api_key:
|
||||
old_api_key = agent.config.litellm_api_key
|
||||
agent.config.litellm_api_key = api_key
|
||||
# 重新创建 LLM 客户端以使用新的 API key
|
||||
try:
|
||||
from .clients.litellm_client import LiteLLMClient
|
||||
except ImportError:
|
||||
from clients.litellm_client import LiteLLMClient
|
||||
agent.deps.llm_client = LiteLLMClient(agent.config)
|
||||
|
||||
try:
|
||||
# 执行搜索
|
||||
response = await agent.search(request)
|
||||
finally:
|
||||
# 恢复原来的 API key
|
||||
if api_key and 'old_api_key' in locals():
|
||||
agent.config.litellm_api_key = old_api_key
|
||||
try:
|
||||
from .clients.litellm_client import LiteLLMClient
|
||||
except ImportError:
|
||||
from clients.litellm_client import LiteLLMClient
|
||||
agent.deps.llm_client = LiteLLMClient(agent.config)
|
||||
|
||||
if not response.success:
|
||||
raise HTTPException(status_code=500, detail=response.message or "搜索失败")
|
||||
|
||||
return response
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"搜索失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"搜索失败: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/config", tags=["配置"])
|
||||
async def get_config():
|
||||
"""获取当前配置信息(隐藏敏感信息)"""
|
||||
if config is None:
|
||||
raise HTTPException(status_code=503, detail="服务未初始化")
|
||||
|
||||
return {
|
||||
"litellm_model": config.litellm_model,
|
||||
"facebook_api_host": config.facebook_api_host,
|
||||
"max_results": config.max_results,
|
||||
"log_level": config.log_level,
|
||||
"timeout": config.timeout
|
||||
}
|
||||
|
||||
|
||||
# ==================== MCP 协议端点 ====================
|
||||
|
||||
@app.post("/mcp", tags=["MCP"])
|
||||
async def mcp_http_endpoint(request: Request):
|
||||
"""MCP HTTP 端点 - Streamable HTTP"""
|
||||
try:
|
||||
body = await request.json()
|
||||
session_id = request.headers.get("x-mcp-session-id")
|
||||
|
||||
# 从请求头获取 API key
|
||||
api_key = request.headers.get("api-key") or request.headers.get("api_key")
|
||||
if not api_key:
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if auth_header:
|
||||
if auth_header.startswith("Bearer "):
|
||||
api_key = auth_header[7:]
|
||||
else:
|
||||
api_key = auth_header
|
||||
|
||||
response = await handle_mcp_request(body, session_id, api_key=api_key)
|
||||
|
||||
# 如果创建了新会话,返回 session ID
|
||||
if "result" in response and isinstance(response["result"], dict):
|
||||
if "sessionId" not in response["result"] and session_id:
|
||||
response["result"]["sessionId"] = session_id
|
||||
|
||||
return JSONResponse(
|
||||
content=response,
|
||||
headers={"x-mcp-session-id": session_id or ""}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": f"Parse error: {str(e)}"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/mcp/sse", tags=["MCP"])
|
||||
async def mcp_sse_endpoint(request: Request):
|
||||
"""MCP SSE 端点 - Server-Sent Events"""
|
||||
session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4())
|
||||
|
||||
async def event_stream() -> AsyncGenerator[str, None]:
|
||||
# 发送初始连接消息
|
||||
yield f"data: {json.dumps({'type': 'connection', 'sessionId': session_id})}\n\n"
|
||||
|
||||
# 发送工具列表
|
||||
tools = list(TOOL_MAP.keys())
|
||||
yield f"data: {json.dumps({'type': 'tools', 'tools': tools})}\n\n"
|
||||
|
||||
# 保持连接
|
||||
import asyncio
|
||||
while True:
|
||||
await asyncio.sleep(30) # 心跳
|
||||
yield f"data: {json.dumps({'type': 'ping'})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"x-mcp-session-id": session_id
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.post("/mcp/sse", tags=["MCP"])
|
||||
async def mcp_sse_post(request: Request):
|
||||
"""MCP SSE POST 端点 - 处理 SSE 请求"""
|
||||
try:
|
||||
body = await request.json()
|
||||
session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4())
|
||||
|
||||
# 从请求头获取 API key
|
||||
api_key = request.headers.get("api-key") or request.headers.get("api_key")
|
||||
if not api_key:
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if auth_header:
|
||||
if auth_header.startswith("Bearer "):
|
||||
api_key = auth_header[7:]
|
||||
else:
|
||||
api_key = auth_header
|
||||
|
||||
async def response_stream() -> AsyncGenerator[str, None]:
|
||||
response = await handle_mcp_request(body, session_id, api_key=api_key)
|
||||
yield f"data: {json.dumps(response)}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
response_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"x-mcp-session-id": session_id
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": f"Parse error: {str(e)}"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
# 运行服务
|
||||
uvicorn.run(
|
||||
"facebook_agent.api:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
log_level="info"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
客户端模块
|
||||
包含各种API客户端的封装
|
||||
"""
|
||||
|
||||
from .facebook_client import FacebookClient
|
||||
from .litellm_client import LiteLLMClient
|
||||
|
||||
__all__ = ["FacebookClient", "LiteLLMClient"]
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
Facebook API客户端
|
||||
封装与RapidAPI Facebook接口的交互
|
||||
通过 RapidAPI MCP 服务器调用
|
||||
"""
|
||||
|
||||
import aiohttp
|
||||
import json
|
||||
from typing import List, Optional
|
||||
from loguru import logger
|
||||
|
||||
# 支持相对导入和绝对导入
|
||||
try:
|
||||
from ..config import Config
|
||||
from ..models.schemas import SearchResultItem
|
||||
except ImportError:
|
||||
from config import Config
|
||||
from models.schemas import SearchResultItem
|
||||
|
||||
|
||||
class FacebookClient:
|
||||
"""Facebook API客户端 - 通过 RapidAPI MCP 服务器调用"""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
"""
|
||||
初始化客户端
|
||||
|
||||
Args:
|
||||
config: 配置对象
|
||||
"""
|
||||
self.config = config
|
||||
# 使用 RapidAPI MCP 服务器
|
||||
self.mcp_url = config.facebook_mcp_url
|
||||
self.headers = {
|
||||
"x-api-host": config.facebook_api_host,
|
||||
"x-api-key": config.facebook_api_key,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
self.timeout = aiohttp.ClientTimeout(total=config.timeout)
|
||||
|
||||
async def search(self, keyword: str, limit: int = 10) -> List[SearchResultItem]:
|
||||
"""
|
||||
搜索Facebook内容 - 通过 RapidAPI MCP 服务器
|
||||
|
||||
Args:
|
||||
keyword: 搜索关键词
|
||||
limit: 返回结果数量限制
|
||||
|
||||
Returns:
|
||||
搜索结果列表
|
||||
"""
|
||||
try:
|
||||
# 通过 RapidAPI MCP 服务器调用 Search_post 工具
|
||||
mcp_request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "Search_post",
|
||||
"arguments": {
|
||||
"query": keyword,
|
||||
"recent_posts": True # 获取最近的帖子
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
async with session.post(
|
||||
self.mcp_url,
|
||||
headers=self.headers,
|
||||
json=mcp_request
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
logger.error(f"RapidAPI MCP错误: {response.status} - {error_text}")
|
||||
raise Exception(f"RapidAPI MCP请求失败: {response.status}")
|
||||
|
||||
mcp_response = await response.json()
|
||||
|
||||
# 检查 MCP 响应是否有错误
|
||||
if "error" in mcp_response:
|
||||
error_msg = mcp_response["error"].get("message", "Unknown error")
|
||||
logger.error(f"RapidAPI MCP工具调用错误: {error_msg}")
|
||||
raise Exception(f"RapidAPI MCP工具调用失败: {error_msg}")
|
||||
|
||||
# 解析 MCP 响应
|
||||
result = mcp_response.get("result", {})
|
||||
content = result.get("content", [])
|
||||
|
||||
if not content:
|
||||
logger.warning(f"搜索关键词 '{keyword}' 未获得结果")
|
||||
return []
|
||||
|
||||
# 第一个 content 包含 JSON 字符串
|
||||
content_text = content[0].get("text", "{}")
|
||||
data = json.loads(content_text)
|
||||
|
||||
# 解析搜索结果
|
||||
results = []
|
||||
items = data.get("results", [])
|
||||
|
||||
for item in items[:limit]:
|
||||
author_info = item.get("author", {})
|
||||
reactions = item.get("reactions", {})
|
||||
|
||||
results.append(SearchResultItem(
|
||||
title=item.get("message", "")[:100] or f"Post {item.get('post_id', '')}",
|
||||
url=item.get("url", ""),
|
||||
snippet=item.get("message", "")[:500],
|
||||
author=author_info.get("name", ""),
|
||||
likes=reactions.get("like", 0) + reactions.get("love", 0),
|
||||
cover_image=item.get("image", {}).get("uri", "") if item.get("image") else None
|
||||
))
|
||||
|
||||
logger.info(f"搜索关键词 '{keyword}' 获得 {len(results)} 条结果")
|
||||
return results
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析错误: {e}")
|
||||
raise Exception(f"响应解析失败: {str(e)}")
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f"RapidAPI MCP网络错误: {e}")
|
||||
raise Exception(f"网络请求失败: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Facebook搜索异常: {e}")
|
||||
raise
|
||||
|
||||
async def search_by_mcp(self, keyword: str, limit: int = 10) -> List[SearchResultItem]:
|
||||
"""
|
||||
通过MCP服务器搜索Facebook内容(已弃用,search方法已使用MCP)
|
||||
|
||||
Args:
|
||||
keyword: 搜索关键词
|
||||
limit: 返回结果数量限制
|
||||
|
||||
Returns:
|
||||
搜索结果列表
|
||||
"""
|
||||
# search 方法已经通过 MCP 调用,此方法保留用于兼容性
|
||||
return await self.search(keyword, limit)
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
LiteLLM Gateway客户端
|
||||
封装与LiteLLM Gateway的交互
|
||||
"""
|
||||
|
||||
import aiohttp
|
||||
from typing import List, Dict, Any, Optional
|
||||
from loguru import logger
|
||||
|
||||
# 支持相对导入和绝对导入
|
||||
try:
|
||||
from ..config import Config
|
||||
except ImportError:
|
||||
from config import Config
|
||||
|
||||
|
||||
class LiteLLMClient:
|
||||
"""LiteLLM Gateway客户端"""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
"""
|
||||
初始化客户端
|
||||
|
||||
Args:
|
||||
config: 配置对象
|
||||
"""
|
||||
self.config = config
|
||||
# 确保 base_url 不包含尾部的斜杠,并且正确处理 /v1 路径
|
||||
base_url = config.litellm_gateway_url.rstrip("/")
|
||||
# 如果 base_url 已经包含 /v1,则直接使用;否则添加 /v1
|
||||
if base_url.endswith("/v1"):
|
||||
self.base_url = base_url
|
||||
else:
|
||||
self.base_url = f"{base_url}/v1"
|
||||
self.api_key = config.litellm_api_key
|
||||
# 移除 openai: 前缀(LiteLLM Gateway 不需要)
|
||||
model_name = config.litellm_model
|
||||
if model_name.startswith("openai:"):
|
||||
model_name = model_name[7:] # 移除 "openai:" 前缀
|
||||
self.model = model_name
|
||||
self.timeout = aiohttp.ClientTimeout(total=config.timeout)
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 2000,
|
||||
response_format: Optional[Dict[str, str]] = None
|
||||
) -> str:
|
||||
"""
|
||||
发送聊天请求到LiteLLM Gateway
|
||||
|
||||
Args:
|
||||
messages: 消息列表,格式 [{"role": "user", "content": "..."}]
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大token数
|
||||
response_format: 响应格式(如 {"type": "json_object"})
|
||||
|
||||
Returns:
|
||||
LLM的响应文本
|
||||
"""
|
||||
# LiteLLM Gateway通常兼容OpenAI API格式
|
||||
# base_url 已经包含 /v1,所以直接添加 /chat/completions
|
||||
url = f"{self.base_url}/chat/completions"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens
|
||||
}
|
||||
|
||||
if response_format:
|
||||
payload["response_format"] = response_format
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
async with session.post(url, headers=headers, json=payload) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
logger.error(f"LiteLLM API错误: {response.status} - {error_text}")
|
||||
raise Exception(f"LiteLLM API请求失败: {response.status}")
|
||||
|
||||
result = await response.json()
|
||||
return result["choices"][0]["message"]["content"]
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f"LiteLLM请求网络错误: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"LiteLLM请求异常: {e}")
|
||||
raise
|
||||
|
||||
async def chat_with_system(
|
||||
self,
|
||||
system_prompt: str,
|
||||
user_message: str,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 2000,
|
||||
response_format: Optional[Dict[str, str]] = None
|
||||
) -> str:
|
||||
"""
|
||||
使用系统提示和用户消息进行对话
|
||||
|
||||
Args:
|
||||
system_prompt: 系统提示
|
||||
user_message: 用户消息
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大token数
|
||||
response_format: 响应格式
|
||||
|
||||
Returns:
|
||||
LLM的响应文本
|
||||
"""
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_message}
|
||||
]
|
||||
|
||||
return await self.chat(
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
response_format=response_format
|
||||
)
|
||||
|
||||
async def generate_summary(
|
||||
self,
|
||||
query: str,
|
||||
results: List[Dict[str, Any]],
|
||||
temperature: float = 0.7
|
||||
) -> str:
|
||||
"""
|
||||
生成搜索结果总结
|
||||
|
||||
Args:
|
||||
query: 搜索关键词
|
||||
results: 搜索结果列表
|
||||
temperature: 温度参数
|
||||
|
||||
Returns:
|
||||
AI生成的总结文本
|
||||
"""
|
||||
system_prompt = """你是一个Facebook内容分析助手。用户给你搜索关键词和搜索结果,你需要:
|
||||
1. 总结这些内容的主要特点和亮点
|
||||
2. 提取关键信息(如热门话题、用户关注点等)
|
||||
3. 用简洁、友好的语言呈现
|
||||
4. 如果结果较少,说明可能的原因或建议"""
|
||||
|
||||
results_text = "\n\n".join([
|
||||
f"标题: {r.get('title', '')}\n"
|
||||
f"摘要: {r.get('snippet', '')}\n"
|
||||
f"作者: {r.get('author', '')}\n"
|
||||
f"点赞: {r.get('likes', 0)}"
|
||||
for r in results
|
||||
])
|
||||
|
||||
user_message = f"""用户搜索关键词:{query}
|
||||
|
||||
搜索结果:
|
||||
{results_text}
|
||||
|
||||
请为这些搜索结果生成一个简洁的总结。"""
|
||||
|
||||
return await self.chat_with_system(
|
||||
system_prompt=system_prompt,
|
||||
user_message=user_message,
|
||||
temperature=temperature,
|
||||
max_tokens=1000
|
||||
)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
配置管理模块
|
||||
负责加载和管理所有配置项
|
||||
"""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""Agent配置类"""
|
||||
|
||||
# LiteLLM Gateway配置
|
||||
litellm_gateway_url: str
|
||||
litellm_api_key: str
|
||||
litellm_model: str = "taiji/gpt-4o-mini"
|
||||
|
||||
# Facebook RapidAPI配置
|
||||
facebook_api_host: str = "facebook-scraper3.p.rapidapi.com"
|
||||
facebook_api_key: str = "34225c5924msh453fa7aff7a52a9p1d7adfjsn260ba0796c00"
|
||||
facebook_mcp_url: str = "https://mcp.rapidapi.com"
|
||||
|
||||
# Agent配置
|
||||
max_results: int = 10
|
||||
timeout: int = 30
|
||||
|
||||
# 可选配置
|
||||
log_level: str = "INFO"
|
||||
|
||||
@staticmethod
|
||||
def ensure_model_prefix(model_name: str) -> str:
|
||||
"""确保模型名称有 openai: 前缀(pydantic_ai 要求格式为 provider:model_name)"""
|
||||
if not model_name:
|
||||
return 'openai:taiji/gpt-4o-mini'
|
||||
# 如果已经有 provider: 前缀,直接返回
|
||||
if ':' in model_name:
|
||||
return model_name
|
||||
# 否则添加 openai: 前缀
|
||||
return f'openai:{model_name}'
|
||||
|
||||
@classmethod
|
||||
def from_env(cls, env_path: Optional[str] = None) -> "Config":
|
||||
"""从环境变量加载配置"""
|
||||
if env_path:
|
||||
load_dotenv(env_path)
|
||||
else:
|
||||
load_dotenv()
|
||||
|
||||
# 获取模型名称并确保有 openai: 前缀
|
||||
raw_model = os.getenv("LITELLM_MODEL", "taiji/gpt-4o-mini")
|
||||
model_name = cls.ensure_model_prefix(raw_model)
|
||||
|
||||
return cls(
|
||||
# LiteLLM Gateway配置
|
||||
litellm_gateway_url=os.getenv("LITELLM_GATEWAY_URL", ""),
|
||||
litellm_api_key=os.getenv("LITELLM_API_KEY", "sk"),
|
||||
litellm_model=model_name,
|
||||
|
||||
# Facebook RapidAPI配置
|
||||
facebook_api_host=os.getenv("FACEBOOK_API_HOST", "facebook-scraper3.p.rapidapi.com"),
|
||||
facebook_api_key=os.getenv("FACEBOOK_API_KEY", "34225c5924msh453fa7aff7a52a9p1d7adfjsn260ba0796c00"),
|
||||
facebook_mcp_url=os.getenv("FACEBOOK_MCP_URL", "https://mcp.rapidapi.com"),
|
||||
|
||||
# Agent配置
|
||||
max_results=int(os.getenv("MAX_RESULTS", "10")),
|
||||
timeout=int(os.getenv("TIMEOUT", "30")),
|
||||
|
||||
# 可选配置
|
||||
log_level=os.getenv("LOG_LEVEL", "INFO")
|
||||
)
|
||||
|
||||
def validate(self) -> bool:
|
||||
"""验证配置是否完整"""
|
||||
required_fields = [
|
||||
("litellm_gateway_url", self.litellm_gateway_url),
|
||||
("litellm_api_key", self.litellm_api_key),
|
||||
("facebook_api_key", self.facebook_api_key),
|
||||
]
|
||||
|
||||
missing = [name for name, value in required_fields if not value]
|
||||
|
||||
if missing:
|
||||
raise ValueError(f"缺少必要的配置项: {', '.join(missing)}")
|
||||
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"facebook-search-agent-stdio": {
|
||||
"command": "python",
|
||||
"args": ["/home/taiji/tools/aks_agent/facebook_agent/run_mcp_server.py", "--transport", "stdio"],
|
||||
"env": {
|
||||
"LITELLM_GATEWAY_URL": "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1",
|
||||
"LITELLM_API_KEY": "sk-rxegkFOciNmQLhOHr3qP3A",
|
||||
"LITELLM_MODEL": "taiji/gpt-4o-mini",
|
||||
"FACEBOOK_API_HOST": "facebook-scraper3.p.rapidapi.com",
|
||||
"FACEBOOK_API_KEY": "34225c5924msh453fa7aff7a52a9p1d7adfjsn260ba0796c00",
|
||||
"FACEBOOK_MCP_URL": "https://mcp.rapidapi.com",
|
||||
"MAX_RESULTS": "10",
|
||||
"TIMEOUT": "30",
|
||||
"LOG_LEVEL": "INFO"
|
||||
}
|
||||
},
|
||||
"facebook-search-agent-http": {
|
||||
"url": "http://20.6.9.191:18000/mcp",
|
||||
"type": "http"
|
||||
},
|
||||
"facebook-search-agent-sse": {
|
||||
"url": "http://20.6.9.191:18000/mcp/sse",
|
||||
"type": "sse"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
services:
|
||||
facebook-agent:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: facebook_agent/Dockerfile.mcp
|
||||
container_name: facebook-agent
|
||||
ports:
|
||||
- "18000:8000" # 统一端口:API + MCP 服务
|
||||
environment:
|
||||
- LITELLM_GATEWAY_URL=${LITELLM_GATEWAY_URL:-https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1}
|
||||
- LITELLM_API_KEY=${LITELLM_API_KEY:-sk-rxegkFOciNmQLhOHr3qP3A}
|
||||
- LITELLM_MODEL=${LITELLM_MODEL:-taiji/gpt-4o-mini}
|
||||
- FACEBOOK_API_HOST=${FACEBOOK_API_HOST:-facebook-scraper3.p.rapidapi.com}
|
||||
- FACEBOOK_API_KEY=${FACEBOOK_API_KEY:-34225c5924msh453fa7aff7a52a9p1d7adfjsn260ba0796c00}
|
||||
- FACEBOOK_MCP_URL=${FACEBOOK_MCP_URL:-https://mcp.rapidapi.com}
|
||||
- MAX_RESULTS=${MAX_RESULTS:-10}
|
||||
- TIMEOUT=${TIMEOUT:-30}
|
||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||
- API_HOST=0.0.0.0
|
||||
- API_PORT=8000
|
||||
- PYTHONPATH=/app
|
||||
# volumes:
|
||||
# - ./facebook_agent:/app/facebook_agent
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Docker测试脚本
|
||||
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "Facebook搜索Agent Docker测试"
|
||||
echo "=========================================="
|
||||
|
||||
# 颜色定义
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 检查Docker是否安装
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo -e "${RED}错误: Docker未安装${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查docker-compose是否安装
|
||||
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
||||
echo -e "${RED}错误: docker-compose未安装${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 使用docker compose或docker-compose
|
||||
if docker compose version &> /dev/null; then
|
||||
DOCKER_COMPOSE="docker compose"
|
||||
else
|
||||
DOCKER_COMPOSE="docker-compose"
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}1. 构建Docker镜像...${NC}"
|
||||
cd /home/taiji/tools/aks_agent/facebook_agent
|
||||
$DOCKER_COMPOSE build
|
||||
|
||||
echo -e "${YELLOW}2. 启动服务...${NC}"
|
||||
$DOCKER_COMPOSE up -d
|
||||
|
||||
echo -e "${YELLOW}3. 等待服务启动...${NC}"
|
||||
sleep 5
|
||||
|
||||
echo -e "${YELLOW}4. 检查服务状态...${NC}"
|
||||
$DOCKER_COMPOSE ps
|
||||
|
||||
echo -e "${YELLOW}5. 测试API服务 (端口18000)...${NC}"
|
||||
echo "测试根路径:"
|
||||
curl -s http://localhost:18000/ | python3 -m json.tool || echo -e "${RED}API服务未响应${NC}"
|
||||
|
||||
echo -e "\n测试健康检查:"
|
||||
curl -s http://localhost:18000/health | python3 -m json.tool || echo -e "${RED}健康检查失败${NC}"
|
||||
|
||||
echo -e "\n${YELLOW}6. 测试搜索功能...${NC}"
|
||||
SEARCH_RESULT=$(curl -s -X POST http://localhost:18000/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "technology news", "limit": 3}')
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "$SEARCH_RESULT" | python3 -m json.tool | head -50
|
||||
echo -e "${GREEN}搜索测试完成${NC}"
|
||||
else
|
||||
echo -e "${RED}搜索测试失败${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${YELLOW}7. 测试MCP服务 (端口18001)...${NC}"
|
||||
echo "测试MCP健康检查:"
|
||||
curl -s http://localhost:18001/health | python3 -m json.tool || echo -e "${RED}MCP服务未响应${NC}"
|
||||
|
||||
echo -e "\n测试MCP工具列表:"
|
||||
curl -s -X POST http://localhost:18001/mcp/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"method": "tools/list", "params": {"id": "test"}}' | python3 -m json.tool || echo -e "${RED}MCP工具列表获取失败${NC}"
|
||||
|
||||
echo -e "\n${YELLOW}8. 查看服务日志...${NC}"
|
||||
echo "API服务日志 (最后10行):"
|
||||
docker logs facebook-api --tail 10 2>&1 || echo "无法获取日志"
|
||||
|
||||
echo -e "\nMCP服务日志 (最后10行):"
|
||||
docker logs facebook-mcp --tail 10 2>&1 || echo "无法获取日志"
|
||||
|
||||
echo -e "\n${GREEN}=========================================="
|
||||
echo "测试完成!"
|
||||
echo "==========================================${NC}"
|
||||
echo ""
|
||||
echo "服务地址:"
|
||||
echo " - API服务: http://20.6.9.191:18000"
|
||||
echo " - MCP服务: http://20.6.9.191:18001"
|
||||
echo ""
|
||||
echo "查看日志: docker logs facebook-api 或 docker logs facebook-mcp"
|
||||
echo "停止服务: docker-compose down"
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: facebook-agent
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: facebook-agent-secrets
|
||||
namespace: facebook-agent
|
||||
type: Opaque
|
||||
stringData:
|
||||
LITELLM_API_KEY: "sk-rxegkFOciNmQLhOHr3qP3A"
|
||||
FACEBOOK_API_KEY: "34225c5924msh453fa7aff7a52a9p1d7adfjsn260ba0796c00"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: facebook-agent-config
|
||||
namespace: facebook-agent
|
||||
data:
|
||||
LITELLM_MODEL: "taiji/gpt-4o-mini"
|
||||
FACEBOOK_API_HOST: "facebook-scraper3.p.rapidapi.com"
|
||||
FACEBOOK_MCP_URL: "https://mcp.rapidapi.com"
|
||||
MAX_RESULTS: "10"
|
||||
TIMEOUT: "30"
|
||||
LOG_LEVEL: "INFO"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: facebook-agent
|
||||
namespace: facebook-agent
|
||||
labels:
|
||||
app: facebook-agent
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: facebook-agent
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: facebook-agent
|
||||
spec:
|
||||
containers:
|
||||
- name: agent
|
||||
image: your-registry.azurecr.io/facebook-agent:latest
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
name: http
|
||||
env:
|
||||
- name: LITELLM_GATEWAY_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: facebook-agent-config
|
||||
key: LITELLM_GATEWAY_URL
|
||||
- name: LITELLM_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: facebook-agent-secrets
|
||||
key: LITELLM_API_KEY
|
||||
- name: LITELLM_MODEL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: facebook-agent-config
|
||||
key: LITELLM_MODEL
|
||||
- name: FACEBOOK_API_HOST
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: facebook-agent-config
|
||||
key: FACEBOOK_API_HOST
|
||||
- name: FACEBOOK_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: facebook-agent-secrets
|
||||
key: FACEBOOK_API_KEY
|
||||
- name: FACEBOOK_MCP_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: facebook-agent-config
|
||||
key: FACEBOOK_MCP_URL
|
||||
- name: MAX_RESULTS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: facebook-agent-config
|
||||
key: MAX_RESULTS
|
||||
- name: TIMEOUT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: facebook-agent-config
|
||||
key: TIMEOUT
|
||||
- name: LOG_LEVEL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: facebook-agent-config
|
||||
key: LOG_LEVEL
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: facebook-agent-service
|
||||
namespace: facebook-agent
|
||||
spec:
|
||||
selector:
|
||||
app: facebook-agent
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8000
|
||||
type: LoadBalancer
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
主程序入口
|
||||
支持命令行和API服务两种模式
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from loguru import logger
|
||||
|
||||
from .config import Config
|
||||
from .agent import FacebookAgent
|
||||
from .models.schemas import SearchRequest
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
# 加载配置
|
||||
try:
|
||||
config = Config.from_env()
|
||||
config.validate()
|
||||
except Exception as e:
|
||||
logger.error(f"配置加载失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 创建Agent
|
||||
agent = FacebookAgent(config)
|
||||
|
||||
# 检查命令行参数
|
||||
if len(sys.argv) > 1:
|
||||
# 命令行模式
|
||||
query = sys.argv[1]
|
||||
limit = int(sys.argv[2]) if len(sys.argv) > 2 else 5
|
||||
|
||||
logger.info(f"搜索关键词: {query}")
|
||||
|
||||
request = SearchRequest(query=query, limit=limit)
|
||||
response = await agent.search(request)
|
||||
|
||||
if response.success:
|
||||
print("\n" + "=" * 60)
|
||||
print("搜索结果")
|
||||
print("=" * 60)
|
||||
print(f"关键词: {response.query}")
|
||||
print(f"找到 {len(response.results)} 条结果\n")
|
||||
|
||||
for i, result in enumerate(response.results, 1):
|
||||
print(f"[{i}] {result.title}")
|
||||
if result.snippet:
|
||||
print(f" {result.snippet[:100]}...")
|
||||
if result.author:
|
||||
print(f" 作者: {result.author}")
|
||||
if result.likes:
|
||||
print(f" 点赞: {result.likes}")
|
||||
if result.url:
|
||||
print(f" 链接: {result.url}")
|
||||
print()
|
||||
|
||||
if response.summary:
|
||||
print("=" * 60)
|
||||
print("AI总结")
|
||||
print("=" * 60)
|
||||
print(response.summary)
|
||||
print()
|
||||
else:
|
||||
print(f"搜索失败: {response.message}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# 交互模式
|
||||
print("=" * 60)
|
||||
print("Facebook搜索智能Agent")
|
||||
print("=" * 60)
|
||||
print("输入搜索关键词,输入 'quit' 或 'exit' 退出")
|
||||
print("=" * 60)
|
||||
|
||||
while True:
|
||||
try:
|
||||
query = input("\n🔎 请输入关键词: ").strip()
|
||||
|
||||
if query.lower() in ['quit', 'exit', '退出']:
|
||||
print("再见!")
|
||||
break
|
||||
|
||||
if not query:
|
||||
continue
|
||||
|
||||
request = SearchRequest(query=query, limit=5)
|
||||
response = await agent.search(request)
|
||||
|
||||
if response.success:
|
||||
print("\n" + "=" * 60)
|
||||
print(f"找到 {len(response.results)} 条结果")
|
||||
print("=" * 60)
|
||||
|
||||
for i, result in enumerate(response.results, 1):
|
||||
print(f"\n[{i}] {result.title}")
|
||||
if result.snippet:
|
||||
print(f" {result.snippet[:150]}...")
|
||||
if result.author:
|
||||
print(f" 👤 {result.author}")
|
||||
if result.likes:
|
||||
print(f" ❤️ {result.likes}")
|
||||
|
||||
if response.summary:
|
||||
print("\n" + "=" * 60)
|
||||
print("📝 AI总结")
|
||||
print("=" * 60)
|
||||
print(response.summary)
|
||||
else:
|
||||
print(f"❌ 搜索失败: {response.message}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n再见!")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"处理失败: {e}")
|
||||
print(f"❌ 发生错误: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"Facebook Search Agent": {
|
||||
"command": "python",
|
||||
"args": [
|
||||
"-m",
|
||||
"facebook_agent.mcp_server"
|
||||
],
|
||||
"env": {
|
||||
"LITELLM_GATEWAY_URL": "https://your-litellm-gateway-url",
|
||||
"LITELLM_API_KEY": "sk-",
|
||||
"LITELLM_MODEL": "taiji/gpt-4o-mini",
|
||||
"FACEBOOK_API_HOST": "facebook-scraper3.p.rapidapi.com",
|
||||
"FACEBOOK_API_KEY": "34225c5924msh453fa7aff7a52a9p1d7adfjsn260ba0796c00",
|
||||
"FACEBOOK_MCP_URL": "https://mcp.rapidapi.com",
|
||||
"MAX_RESULTS": "10",
|
||||
"TIMEOUT": "30",
|
||||
"LOG_LEVEL": "INFO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"RapidAPI Hub - Facebook Scraper": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"mcp-remote",
|
||||
"https://mcp.rapidapi.com",
|
||||
"--header",
|
||||
"x-api-host: facebook-scraper3.p.rapidapi.com",
|
||||
"--header",
|
||||
"x-api-key: 34225c5924msh453fa7aff7a52a9p1d7adfjsn260ba0796c00"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
MCP HTTP/SSE 服务器 - 支持远程调用
|
||||
实现 MCP 协议的 HTTP 和 SSE 传输方式,供 Cursor 等客户端远程调用
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from typing import Dict, Any, Optional, AsyncGenerator
|
||||
from fastapi import FastAPI, Request, HTTPException, Header
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
from loguru import logger
|
||||
|
||||
# 导入 MCP 服务器和工具函数
|
||||
from .mcp_server import (
|
||||
server as mcp_server,
|
||||
search_facebook,
|
||||
initialize_agent
|
||||
)
|
||||
|
||||
# 工具映射
|
||||
TOOL_MAP = {
|
||||
'search_facebook': search_facebook,
|
||||
}
|
||||
|
||||
|
||||
# 创建 FastAPI 应用
|
||||
app = FastAPI(
|
||||
title="MCP HTTP/SSE Server - Facebook搜索Agent",
|
||||
description="MCP 协议的 HTTP 和 SSE 传输实现",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 配置 CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Session 管理
|
||||
sessions: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
|
||||
class MCPRequest(BaseModel):
|
||||
"""MCP JSON-RPC 请求"""
|
||||
jsonrpc: str = "2.0"
|
||||
id: Optional[str] = None
|
||||
method: str
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class MCPResponse(BaseModel):
|
||||
"""MCP JSON-RPC 响应"""
|
||||
jsonrpc: str = "2.0"
|
||||
id: Optional[str] = None
|
||||
result: Optional[Any] = None
|
||||
error: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
async def handle_mcp_request(request_data: Dict[str, Any], session_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""处理 MCP 请求"""
|
||||
method = request_data.get("method")
|
||||
params = request_data.get("params", {})
|
||||
request_id = request_data.get("id")
|
||||
|
||||
try:
|
||||
if method == "initialize":
|
||||
# 初始化会话
|
||||
if not session_id:
|
||||
session_id = str(uuid.uuid4())
|
||||
sessions[session_id] = {
|
||||
"initialized": True,
|
||||
"capabilities": {}
|
||||
}
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {},
|
||||
"resources": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "Facebook搜索Agent",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "tools/list":
|
||||
# 列出所有工具
|
||||
tools = [
|
||||
{
|
||||
"name": "search_facebook",
|
||||
"description": "搜索Facebook内容,返回相关帖子和AI生成的总结。支持搜索关键词,返回帖子标题、链接、摘要、作者、点赞数等信息,并提供AI生成的总结。",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "搜索关键词,例如:technology news、travel tips、food recipes等"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "返回结果数量,默认5,最大20",
|
||||
"default": 5,
|
||||
"minimum": 1,
|
||||
"maximum": 20
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"tools": tools
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "tools/call":
|
||||
# 调用工具
|
||||
tool_name = params.get("name")
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
if tool_name not in TOOL_MAP:
|
||||
raise ValueError(f"Tool '{tool_name}' not found")
|
||||
|
||||
# 获取工具函数
|
||||
tool_func = TOOL_MAP[tool_name]
|
||||
|
||||
# 调用工具(异步)
|
||||
result = await tool_func(**arguments)
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": str(result)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
elif method == "ping":
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": {}
|
||||
}
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown method: {method}")
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": str(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""应用启动时初始化"""
|
||||
try:
|
||||
# 初始化Agent
|
||||
initialize_agent()
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info("Facebook搜索Agent MCP HTTP服务器启动")
|
||||
logger.info("=" * 60)
|
||||
logger.info("MCP服务器已就绪,支持HTTP/SSE传输")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@app.post("/mcp")
|
||||
async def mcp_http_endpoint(request: Request):
|
||||
"""MCP HTTP 端点 - Streamable HTTP"""
|
||||
try:
|
||||
body = await request.json()
|
||||
session_id = request.headers.get("x-mcp-session-id")
|
||||
|
||||
response = await handle_mcp_request(body, session_id)
|
||||
|
||||
# 如果创建了新会话,返回 session ID
|
||||
if "result" in response and isinstance(response["result"], dict):
|
||||
if "sessionId" not in response["result"] and session_id:
|
||||
response["result"]["sessionId"] = session_id
|
||||
|
||||
return JSONResponse(
|
||||
content=response,
|
||||
headers={"x-mcp-session-id": session_id or ""}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": f"Parse error: {str(e)}"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/mcp/sse")
|
||||
async def mcp_sse_endpoint(request: Request):
|
||||
"""MCP SSE 端点 - Server-Sent Events"""
|
||||
session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4())
|
||||
|
||||
async def event_stream() -> AsyncGenerator[str, None]:
|
||||
# 发送初始连接消息
|
||||
yield f"data: {json.dumps({'type': 'connection', 'sessionId': session_id})}\n\n"
|
||||
|
||||
# 发送工具列表
|
||||
tools = list(TOOL_MAP.keys())
|
||||
yield f"data: {json.dumps({'type': 'tools', 'tools': tools})}\n\n"
|
||||
|
||||
# 保持连接
|
||||
import asyncio
|
||||
while True:
|
||||
await asyncio.sleep(30) # 心跳
|
||||
yield f"data: {json.dumps({'type': 'ping'})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"x-mcp-session-id": session_id
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.post("/mcp/sse")
|
||||
async def mcp_sse_post(request: Request):
|
||||
"""MCP SSE POST 端点 - 处理 SSE 请求"""
|
||||
try:
|
||||
body = await request.json()
|
||||
session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4())
|
||||
|
||||
async def response_stream() -> AsyncGenerator[str, None]:
|
||||
response = await handle_mcp_request(body, session_id)
|
||||
yield f"data: {json.dumps(response)}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
response_stream(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"x-mcp-session-id": session_id
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": f"Parse error: {str(e)}"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "MCP HTTP/SSE Server - Facebook搜索Agent",
|
||||
"tools_count": len(TOOL_MAP)
|
||||
}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根端点"""
|
||||
return {
|
||||
"service": "MCP HTTP/SSE Server - Facebook搜索Agent",
|
||||
"version": "1.0.0",
|
||||
"endpoints": {
|
||||
"mcp_http": "/mcp",
|
||||
"mcp_sse": "/mcp/sse",
|
||||
"health": "/health"
|
||||
},
|
||||
"tools": list(TOOL_MAP.keys())
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
host = os.getenv("MCP_HOST", "0.0.0.0")
|
||||
port = int(os.getenv("MCP_PORT", "8001"))
|
||||
|
||||
print(f"🚀 MCP HTTP/SSE Server 启动中...")
|
||||
print(f"📡 HTTP 端点: http://{host}:{port}/mcp")
|
||||
print(f"📡 SSE 端点: http://{host}:{port}/mcp/sse")
|
||||
print(f"📚 健康检查: http://{host}:{port}/health")
|
||||
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
MCP 服务器 - Facebook 搜索 Agent
|
||||
使用 FastMCP 提供 Facebook 搜索功能
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from typing import Optional
|
||||
from loguru import logger
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# 支持相对导入和绝对导入
|
||||
try:
|
||||
from .config import Config
|
||||
from .agent import FacebookAgent
|
||||
from .models.schemas import SearchRequest
|
||||
except ImportError:
|
||||
from config import Config
|
||||
from agent import FacebookAgent
|
||||
from models.schemas import SearchRequest
|
||||
|
||||
# 创建 MCP 服务器
|
||||
server = FastMCP('Facebook搜索Agent')
|
||||
|
||||
# 全局变量
|
||||
agent: Optional[FacebookAgent] = None
|
||||
|
||||
|
||||
def initialize_agent():
|
||||
"""初始化 Agent"""
|
||||
global agent
|
||||
if agent is None:
|
||||
try:
|
||||
# 加载配置
|
||||
config = Config.from_env()
|
||||
config.validate()
|
||||
|
||||
# 创建Agent
|
||||
agent = FacebookAgent(config)
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info("Facebook搜索Agent初始化完成")
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"LiteLLM Gateway: {config.litellm_gateway_url}")
|
||||
logger.info(f"模型: {config.litellm_model}")
|
||||
logger.info(f"Facebook API Host: {config.facebook_api_host}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Agent初始化失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@server.tool()
|
||||
async def search_facebook(
|
||||
query: str,
|
||||
limit: int = 5
|
||||
) -> str:
|
||||
"""
|
||||
搜索Facebook内容,返回相关帖子和AI生成的总结
|
||||
|
||||
Args:
|
||||
query: 搜索关键词,例如:technology news、travel tips、food recipes等
|
||||
limit: 返回结果数量,默认5,最大20
|
||||
|
||||
Returns:
|
||||
搜索结果和AI生成的总结(JSON格式)
|
||||
"""
|
||||
global agent
|
||||
|
||||
# 确保Agent已初始化
|
||||
if agent is None:
|
||||
initialize_agent()
|
||||
|
||||
try:
|
||||
if not query:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "搜索关键词不能为空"
|
||||
}, ensure_ascii=False)
|
||||
|
||||
# 限制结果数量
|
||||
limit = max(1, min(limit, 20))
|
||||
|
||||
# 执行搜索
|
||||
request = SearchRequest(query=query, limit=limit)
|
||||
response = await agent.search(request)
|
||||
|
||||
# 构造返回结果
|
||||
result = {
|
||||
"success": response.success,
|
||||
"query": response.query,
|
||||
"total_count": response.total_count,
|
||||
"results": [
|
||||
{
|
||||
"title": r.title,
|
||||
"url": r.url,
|
||||
"snippet": r.snippet,
|
||||
"author": r.author,
|
||||
"likes": r.likes
|
||||
}
|
||||
for r in response.results
|
||||
],
|
||||
"summary": response.summary,
|
||||
"message": response.message
|
||||
}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"搜索处理失败: {e}")
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 初始化Agent
|
||||
initialize_agent()
|
||||
|
||||
# 运行服务器
|
||||
server.run()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
数据模型模块
|
||||
包含所有Pydantic数据模型定义
|
||||
"""
|
||||
|
||||
from .schemas import SearchRequest, SearchResponse, SearchResultItem
|
||||
|
||||
__all__ = ["SearchRequest", "SearchResponse", "SearchResultItem"]
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
数据模型定义
|
||||
使用Pydantic定义输入输出结构
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
"""搜索请求模型"""
|
||||
query: str = Field(..., description="搜索关键词", min_length=1, max_length=200)
|
||||
limit: Optional[int] = Field(5, description="返回结果数量", ge=1, le=20)
|
||||
|
||||
|
||||
class SearchResultItem(BaseModel):
|
||||
"""Facebook搜索结果项"""
|
||||
title: str = Field(..., description="笔记标题")
|
||||
url: Optional[str] = Field(None, description="笔记链接")
|
||||
snippet: Optional[str] = Field(None, description="笔记摘要")
|
||||
author: Optional[str] = Field(None, description="作者")
|
||||
likes: Optional[int] = Field(None, description="点赞数")
|
||||
cover_image: Optional[str] = Field(None, description="封面图片URL")
|
||||
|
||||
|
||||
class SearchResponse(BaseModel):
|
||||
"""搜索响应模型"""
|
||||
success: bool = Field(..., description="是否成功")
|
||||
query: str = Field(..., description="搜索关键词")
|
||||
results: List[SearchResultItem] = Field(default_factory=list, description="搜索结果列表")
|
||||
total_count: Optional[int] = Field(None, description="总结果数")
|
||||
summary: Optional[str] = Field(None, description="AI生成的总结")
|
||||
message: Optional[str] = Field(None, description="错误消息或提示信息")
|
||||
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat(), description="时间戳")
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Pydantic AI框架
|
||||
pydantic-ai>=0.0.14
|
||||
|
||||
# HTTP客户端
|
||||
aiohttp>=3.9.0
|
||||
httpx>=0.25.0
|
||||
|
||||
# FastAPI相关
|
||||
fastapi>=0.109.0
|
||||
uvicorn[standard]>=0.27.0
|
||||
pydantic>=2.5.0
|
||||
|
||||
# MCP支持
|
||||
mcp>=0.9.0
|
||||
fastmcp>=0.1.0
|
||||
# 注意:如果mcp包不可用,可以使用以下替代方案:
|
||||
# - 使用FastAPI直接实现MCP协议
|
||||
# - 或等待官方MCP Python SDK更新
|
||||
|
||||
# 环境变量
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# 日志
|
||||
loguru>=0.7.0
|
||||
|
||||
# 类型提示
|
||||
typing-extensions>=4.9.0
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
启动 Facebook Agent API 服务的脚本
|
||||
解决相对导入问题
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# 将当前目录添加到 Python 路径
|
||||
current_dir = Path(__file__).parent
|
||||
sys.path.insert(0, str(current_dir))
|
||||
|
||||
# 切换到当前目录,使绝对导入能够工作
|
||||
os.chdir(current_dir)
|
||||
|
||||
# 直接导入 api 模块
|
||||
from api import app
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
host = os.getenv("API_HOST", "0.0.0.0")
|
||||
port = int(os.getenv("API_PORT", "8000"))
|
||||
|
||||
print(f"🚀 启动 Facebook 搜索智能 Agent API")
|
||||
print(f"📡 监听地址: http://{host}:{port}")
|
||||
print(f"📚 API 文档: http://{host}:{port}/docs")
|
||||
|
||||
uvicorn.run(app, host=host, port=port, log_level="info")
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
启动 MCP HTTP/SSE 服务器的入口脚本
|
||||
支持远程调用,供 Cursor 等客户端使用
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
# 脚本在 facebook_agent/ 目录下,需要将父目录添加到路径
|
||||
# 在 Docker 容器中,PYTHONPATH 应该设置为 /app
|
||||
project_root = Path(__file__).parent.parent
|
||||
if str(project_root) not in sys.path:
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
# 确保 PYTHONPATH 包含 /app(Docker 容器中的根目录)
|
||||
app_root = Path("/app")
|
||||
if app_root.exists() and str(app_root) not in sys.path:
|
||||
sys.path.insert(0, str(app_root))
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 尝试不同的导入方式
|
||||
try:
|
||||
from facebook_agent.mcp_http_server import app
|
||||
except ImportError:
|
||||
# 如果在容器内且工作目录是 /app/facebook_agent,尝试相对导入
|
||||
try:
|
||||
from mcp_http_server import app
|
||||
except ImportError:
|
||||
# 最后尝试直接导入
|
||||
import mcp_http_server
|
||||
app = mcp_http_server.app
|
||||
|
||||
import uvicorn
|
||||
|
||||
host = os.getenv("MCP_HOST", "0.0.0.0")
|
||||
port = int(os.getenv("MCP_PORT", "8001"))
|
||||
|
||||
print(f"🚀 MCP HTTP/SSE Server 启动中...")
|
||||
print(f"📡 HTTP 端点: http://{host}:{port}/mcp")
|
||||
print(f"📡 SSE 端点: http://{host}:{port}/mcp/sse")
|
||||
print(f"📚 健康检查: http://{host}:{port}/health")
|
||||
print(f"📋 工具列表: http://{host}:{port}/")
|
||||
print()
|
||||
print("💡 Cursor 配置示例:")
|
||||
print(f' "url": "http://{host}:{port}/mcp"')
|
||||
print(f' "type": "http"')
|
||||
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
启动 MCP 服务器的入口脚本
|
||||
支持 stdio(本地)和 HTTP/SSE(远程)两种传输方式
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
# 脚本在 facebook_agent/ 目录下,需要将父目录添加到路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='启动 MCP 服务器')
|
||||
parser.add_argument(
|
||||
'--transport',
|
||||
choices=['stdio', 'http', 'sse'],
|
||||
default='stdio',
|
||||
help='传输方式: stdio (本地), http (HTTP), sse (SSE)'
|
||||
)
|
||||
parser.add_argument('--host', default='0.0.0.0', help='HTTP/SSE 服务器地址')
|
||||
parser.add_argument('--port', type=int, default=8001, help='HTTP/SSE 服务器端口')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.transport == 'stdio':
|
||||
# stdio 模式(本地)
|
||||
from facebook_agent.mcp_server import server
|
||||
print("🚀 MCP Server (stdio) 启动中...")
|
||||
server.run()
|
||||
else:
|
||||
# HTTP/SSE 模式(远程)
|
||||
from facebook_agent.mcp_http_server import app
|
||||
import uvicorn
|
||||
|
||||
host = args.host
|
||||
port = args.port
|
||||
|
||||
print(f"🚀 MCP HTTP/SSE Server 启动中...")
|
||||
print(f"📡 HTTP 端点: http://{host}:{port}/mcp")
|
||||
print(f"📡 SSE 端点: http://{host}:{port}/mcp/sse")
|
||||
print(f"📚 健康检查: http://{host}:{port}/health")
|
||||
print()
|
||||
print("💡 Cursor 配置示例:")
|
||||
print(f' "url": "http://{host}:{port}/mcp"')
|
||||
print(f' "type": "{args.transport}"')
|
||||
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Facebook搜索智能Agent API启动脚本
|
||||
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "启动Facebook搜索智能Agent API服务"
|
||||
echo "=========================================="
|
||||
|
||||
# 检查Python环境
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "错误: 未找到Python3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查依赖
|
||||
if [ ! -f "requirements.txt" ]; then
|
||||
echo "错误: 未找到requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 安装依赖(如果需要)
|
||||
if [ "$1" == "--install-deps" ]; then
|
||||
echo "安装依赖..."
|
||||
pip install -r requirements.txt
|
||||
fi
|
||||
|
||||
# 检查.env文件
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "警告: 未找到.env文件,将使用默认配置"
|
||||
echo "请确保设置了必要的环境变量"
|
||||
fi
|
||||
|
||||
# 启动服务
|
||||
echo "启动API服务..."
|
||||
python -m uvicorn facebook_agent.api:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 同时启动 API 和 MCP 服务的脚本
|
||||
# 使用 supervisor 或简单的后台进程方式
|
||||
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "启动 Facebook Agent (API + MCP)"
|
||||
echo "=========================================="
|
||||
|
||||
# 检查 Python 环境
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "错误: 未找到 Python3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 启动 API 服务(后台)
|
||||
echo "启动 API 服务 (端口 8000)..."
|
||||
python -m uvicorn facebook_agent.api:app --host 0.0.0.0 --port 8000 &
|
||||
API_PID=$!
|
||||
|
||||
# 等待 API 服务启动
|
||||
sleep 2
|
||||
|
||||
# 启动 MCP HTTP 服务(后台)
|
||||
echo "启动 MCP HTTP 服务 (端口 8001)..."
|
||||
python -m uvicorn facebook_agent.mcp_http_server:app --host 0.0.0.0 --port 8001 &
|
||||
MCP_PID=$!
|
||||
|
||||
# 等待 MCP 服务启动
|
||||
sleep 2
|
||||
|
||||
echo "=========================================="
|
||||
echo "两个服务已启动"
|
||||
echo "API 服务 PID: $API_PID (端口 8000)"
|
||||
echo "MCP 服务 PID: $MCP_PID (端口 8001)"
|
||||
echo "=========================================="
|
||||
|
||||
# 等待进程
|
||||
wait $API_PID $MCP_PID
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Facebook搜索Agent MCP服务器启动脚本
|
||||
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "启动Facebook搜索Agent MCP服务器"
|
||||
echo "=========================================="
|
||||
|
||||
# 检查Python环境
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "错误: 未找到Python3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查依赖
|
||||
if [ ! -f "requirements.txt" ]; then
|
||||
echo "错误: 未找到requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查.env文件
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "警告: 未找到.env文件,将使用默认配置"
|
||||
echo "请确保设置了必要的环境变量"
|
||||
fi
|
||||
|
||||
# 选择运行模式
|
||||
MODE=${1:-http}
|
||||
|
||||
if [ "$MODE" == "stdio" ]; then
|
||||
echo "启动stdio模式(本地开发)..."
|
||||
python3 -m facebook_agent.mcp_server
|
||||
elif [ "$MODE" == "http" ]; then
|
||||
echo "启动HTTP模式(远程部署)..."
|
||||
python3 -m facebook_agent.mcp_http_server
|
||||
else
|
||||
echo "用法: $0 [stdio|http]"
|
||||
echo " stdio - 本地stdio模式(默认)"
|
||||
echo " http - HTTP模式(远程部署)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
from typing import List, Optional
|
||||
from loguru import logger
|
||||
|
||||
from config import Config
|
||||
from models.schemas import (
|
||||
from search_agent.config import Config
|
||||
from search_agent.models.schemas import (
|
||||
QueryAnalysis,
|
||||
SearchPlan,
|
||||
SearchResult,
|
||||
@@ -16,13 +16,13 @@ from models.schemas import (
|
||||
Answer,
|
||||
AgentResponse,
|
||||
)
|
||||
from modules.query_analyzer import QueryAnalyzer
|
||||
from modules.search_planner import SearchPlanner
|
||||
from modules.search_executor import SearchExecutor
|
||||
from modules.content_extractor import ContentExtractor
|
||||
from modules.result_processor import ResultProcessor
|
||||
from modules.answer_generator import AnswerGenerator
|
||||
from modules.reflector import Reflector
|
||||
from search_agent.modules.query_analyzer import QueryAnalyzer
|
||||
from search_agent.modules.search_planner import SearchPlanner
|
||||
from search_agent.modules.search_executor import SearchExecutor
|
||||
from search_agent.modules.content_extractor import ContentExtractor
|
||||
from search_agent.modules.result_processor import ResultProcessor
|
||||
from search_agent.modules.answer_generator import AnswerGenerator
|
||||
from search_agent.modules.reflector import Reflector
|
||||
|
||||
|
||||
class SearchAgent:
|
||||
|
||||
@@ -6,8 +6,8 @@ import asyncio
|
||||
import sys
|
||||
from loguru import logger
|
||||
|
||||
from config import Config
|
||||
from agent.search_agent import SearchAgent
|
||||
from search_agent.config import Config
|
||||
from search_agent.agent.search_agent import SearchAgent
|
||||
|
||||
|
||||
def setup_logging(level: str = "INFO"):
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
from typing import List
|
||||
from loguru import logger
|
||||
|
||||
from config import Config
|
||||
from models.schemas import RankedDocument, Answer, Source
|
||||
from utils.llm_client import LLMClient
|
||||
from utils.helpers import format_documents_for_prompt
|
||||
from search_agent.config import Config
|
||||
from search_agent.models.schemas import RankedDocument, Answer, Source
|
||||
from search_agent.utils.llm_client import LLMClient
|
||||
from search_agent.utils.helpers import format_documents_for_prompt
|
||||
|
||||
|
||||
# 答案生成Prompt
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
from typing import List
|
||||
from loguru import logger
|
||||
|
||||
from config import Config
|
||||
from models.schemas import Document, SearchResult, SearchSource
|
||||
from tools.jina_reader import JinaReaderClient
|
||||
from search_agent.config import Config
|
||||
from search_agent.models.schemas import Document, SearchResult, SearchSource
|
||||
from search_agent.tools.jina_reader import JinaReaderClient
|
||||
|
||||
|
||||
class ContentExtractor:
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
from typing import Optional
|
||||
from loguru import logger
|
||||
|
||||
from config import Config
|
||||
from models.schemas import QueryAnalysis, Intent
|
||||
from utils.llm_client import LLMClient
|
||||
from search_agent.config import Config
|
||||
from search_agent.models.schemas import QueryAnalysis, Intent
|
||||
from search_agent.utils.llm_client import LLMClient
|
||||
|
||||
|
||||
# 查询分析Prompt
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
from typing import List
|
||||
from loguru import logger
|
||||
|
||||
from config import Config
|
||||
from models.schemas import Answer, QualityAssessment
|
||||
from utils.llm_client import LLMClient
|
||||
from search_agent.config import Config
|
||||
from search_agent.models.schemas import Answer, QualityAssessment
|
||||
from search_agent.utils.llm_client import LLMClient
|
||||
|
||||
|
||||
# 反思评估Prompt
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
from typing import List
|
||||
from loguru import logger
|
||||
|
||||
from config import Config
|
||||
from models.schemas import Document, RankedDocument
|
||||
from tools.jina_reranker import JinaRerankerClient
|
||||
from utils.helpers import deduplicate_by_url
|
||||
from search_agent.config import Config
|
||||
from search_agent.models.schemas import Document, RankedDocument
|
||||
from search_agent.tools.jina_reranker import JinaRerankerClient
|
||||
from search_agent.utils.helpers import deduplicate_by_url
|
||||
|
||||
|
||||
class ResultProcessor:
|
||||
|
||||
@@ -7,9 +7,9 @@ import asyncio
|
||||
from typing import List
|
||||
from loguru import logger
|
||||
|
||||
from config import Config
|
||||
from models.schemas import SearchPlan, SearchTask, SearchResult
|
||||
from tools.serper import SerperClient
|
||||
from search_agent.config import Config
|
||||
from search_agent.models.schemas import SearchPlan, SearchTask, SearchResult
|
||||
from search_agent.tools.serper import SerperClient
|
||||
|
||||
|
||||
class SearchExecutor:
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
from typing import List
|
||||
from loguru import logger
|
||||
|
||||
from config import Config
|
||||
from models.schemas import (
|
||||
from search_agent.config import Config
|
||||
from search_agent.models.schemas import (
|
||||
QueryAnalysis,
|
||||
SearchPlan,
|
||||
SearchTask,
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import List, Optional
|
||||
import aiohttp
|
||||
from loguru import logger
|
||||
|
||||
from models.schemas import Document, SearchSource
|
||||
from search_agent.models.schemas import Document, SearchSource
|
||||
|
||||
|
||||
class JinaReaderClient:
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import List, Tuple
|
||||
import aiohttp
|
||||
from loguru import logger
|
||||
|
||||
from models.schemas import Document, RankedDocument
|
||||
from search_agent.models.schemas import Document, RankedDocument
|
||||
|
||||
|
||||
class JinaRerankerClient:
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import List, Optional, Dict, Any
|
||||
import aiohttp
|
||||
from loguru import logger
|
||||
|
||||
from models.schemas import SearchResult, SearchSource
|
||||
from search_agent.models.schemas import SearchResult, SearchSource
|
||||
|
||||
|
||||
class SerperClient:
|
||||
|
||||
@@ -9,7 +9,7 @@ RUN apt-get update && apt-get install -y \
|
||||
|
||||
# 复制requirements文件
|
||||
COPY agents/search_agent/search_agent_A2A/requirements.txt /app/requirements.txt
|
||||
COPY agents/search_agent/search_agent/search_agent/requirements.txt /app/search_agent_requirements.txt
|
||||
COPY agents/search_agent/search_agent/requirements.txt /app/search_agent_requirements.txt
|
||||
|
||||
# 安装Python依赖
|
||||
# 先安装基础依赖(a2a-sdk的依赖)
|
||||
@@ -38,7 +38,7 @@ RUN if [ -f /app/search_agent_requirements.txt ]; then \
|
||||
COPY agents/search_agent/search_agent_A2A/ /app/
|
||||
|
||||
# 复制search_agent核心代码
|
||||
COPY agents/search_agent/search_agent/search_agent/ /app/search_agent/
|
||||
COPY agents/search_agent/search_agent/ /app/search_agent/
|
||||
|
||||
# 设置环境变量
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
@@ -91,14 +91,12 @@ class MCPSearchAgentServer:
|
||||
api_key: LiteLLM API密钥(可选,优先使用,否则从环境变量获取)
|
||||
model: 模型名称(可选,优先使用,否则从环境变量获取)
|
||||
"""
|
||||
# 获取配置
|
||||
# 获取配置(不验证 API key,允许运行时通过 headers 传入)
|
||||
self.llm_config, self.agent_config, self.mcp_config = get_config(api_key, model)
|
||||
|
||||
# 创建默认Agent(使用默认配置)
|
||||
self.default_agent = SearchAgentWrapper(
|
||||
litellm_config=self.llm_config,
|
||||
agent_config=self.agent_config
|
||||
)
|
||||
# 不在启动时创建默认 Agent,而是每次请求时根据传入的 API key 创建
|
||||
# 这样支持不同用户使用不同的 API key
|
||||
self.default_agent = None
|
||||
|
||||
# 任务存储
|
||||
self.tasks: Dict[str, Dict[str, Any]] = {}
|
||||
@@ -106,6 +104,40 @@ class MCPSearchAgentServer:
|
||||
# 创建FastAPI应用
|
||||
self.app = self._create_app()
|
||||
|
||||
def _extract_api_key_from_headers(self, request: Request) -> Optional[str]:
|
||||
"""
|
||||
从请求 headers 中提取 API key
|
||||
|
||||
支持两种方式:
|
||||
1. api_key header: "api_key: your-api-key-here"
|
||||
2. Authorization header: "Authorization: Bearer your-api-key-here"
|
||||
|
||||
Args:
|
||||
request: FastAPI Request 对象
|
||||
|
||||
Returns:
|
||||
提取的 API key,如果未找到则返回 None
|
||||
"""
|
||||
# 方式1: 从 api_key header 获取
|
||||
api_key = request.headers.get("api_key") or request.headers.get("api-key")
|
||||
if api_key:
|
||||
logger.debug("从 api_key header 获取 API key")
|
||||
return api_key
|
||||
|
||||
# 方式2: 从 Authorization header 获取 (Bearer token)
|
||||
auth_header = request.headers.get("Authorization") or request.headers.get("authorization")
|
||||
if auth_header:
|
||||
# 支持 "Bearer xxx" 格式
|
||||
if auth_header.startswith("Bearer "):
|
||||
api_key = auth_header[7:] # 去掉 "Bearer " 前缀
|
||||
logger.debug("从 Authorization Bearer header 获取 API key")
|
||||
return api_key
|
||||
# 也支持直接传入 key(无 Bearer 前缀)
|
||||
logger.debug("从 Authorization header 获取 API key(无 Bearer 前缀)")
|
||||
return auth_header
|
||||
|
||||
return None
|
||||
|
||||
def _create_app(self) -> FastAPI:
|
||||
"""创建FastAPI应用"""
|
||||
|
||||
@@ -113,7 +145,7 @@ class MCPSearchAgentServer:
|
||||
async def lifespan(app: FastAPI):
|
||||
logger.info("MCP Search Agent服务启动", agent_name=self.agent_config.name)
|
||||
yield
|
||||
await self.default_agent.close()
|
||||
# 不需要关闭默认 agent,因为每个请求都创建自己的 agent
|
||||
logger.info("MCP Search Agent服务关闭")
|
||||
|
||||
app = FastAPI(
|
||||
@@ -137,14 +169,19 @@ class MCPSearchAgentServer:
|
||||
|
||||
return app
|
||||
|
||||
def _get_agent(self, api_key: Optional[str] = None, model: Optional[str] = None) -> SearchAgentWrapper:
|
||||
def _get_agent(self, api_key: str, model: Optional[str] = None) -> SearchAgentWrapper:
|
||||
"""
|
||||
获取Agent实例
|
||||
创建并返回 Agent 实例
|
||||
|
||||
如果提供了api_key或model,创建新的Agent实例
|
||||
否则使用默认Agent
|
||||
每个请求都创建一个新的 Agent 实例,使用请求中传入的 API key
|
||||
|
||||
Args:
|
||||
api_key: LiteLLM API 密钥(必需)
|
||||
model: 模型名称(可选,默认从环境变量获取)
|
||||
|
||||
Returns:
|
||||
SearchAgentWrapper 实例
|
||||
"""
|
||||
if api_key or model:
|
||||
# 创建新的配置和Agent
|
||||
llm_config, agent_config, _ = get_config(api_key, model)
|
||||
return SearchAgentWrapper(
|
||||
@@ -153,7 +190,6 @@ class MCPSearchAgentServer:
|
||||
api_key=api_key,
|
||||
model=model
|
||||
)
|
||||
return self.default_agent
|
||||
|
||||
def _register_routes(self, app: FastAPI):
|
||||
"""注册MCP协议路由"""
|
||||
@@ -203,7 +239,7 @@ class MCPSearchAgentServer:
|
||||
|
||||
# 处理 search 方法
|
||||
if rpc_request.method == "search":
|
||||
return await self._handle_search(rpc_request)
|
||||
return await self._handle_search(rpc_request, request)
|
||||
else:
|
||||
return JSONResponse({
|
||||
"jsonrpc": "2.0",
|
||||
@@ -231,7 +267,7 @@ class MCPSearchAgentServer:
|
||||
}
|
||||
})
|
||||
|
||||
return await self._handle_search_stream(rpc_request)
|
||||
return await self._handle_search_stream(rpc_request, request)
|
||||
|
||||
@app.post("/mcp/v1/call")
|
||||
async def mcp_call(request: Request):
|
||||
@@ -252,9 +288,9 @@ class MCPSearchAgentServer:
|
||||
|
||||
# 根据方法名路由
|
||||
if rpc_request.method == "search":
|
||||
return await self._handle_search(rpc_request)
|
||||
return await self._handle_search(rpc_request, request)
|
||||
elif rpc_request.method == "search/stream":
|
||||
return await self._handle_search_stream(rpc_request)
|
||||
return await self._handle_search_stream(rpc_request, request)
|
||||
else:
|
||||
return JSONResponse({
|
||||
"jsonrpc": "2.0",
|
||||
@@ -265,8 +301,13 @@ class MCPSearchAgentServer:
|
||||
}
|
||||
})
|
||||
|
||||
async def _handle_search(self, request: MCPRequest) -> JSONResponse:
|
||||
"""处理 search 请求"""
|
||||
async def _handle_search(self, request: MCPRequest, http_request: Request) -> JSONResponse:
|
||||
"""处理 search 请求
|
||||
|
||||
认证方式(优先级从高到低):
|
||||
1. HTTP headers: api_key 或 Authorization: Bearer xxx
|
||||
2. JSON body params: api_key 或 llm_api_key(向后兼容)
|
||||
"""
|
||||
params = request.params or {}
|
||||
|
||||
# 提取搜索查询
|
||||
@@ -281,15 +322,19 @@ class MCPSearchAgentServer:
|
||||
}
|
||||
})
|
||||
|
||||
# 提取API key(必须从请求参数中获取,等同于API格式版本的llm_api_key)
|
||||
# 提取API key(优先从 headers 获取,其次从 params 获取)
|
||||
api_key = self._extract_api_key_from_headers(http_request)
|
||||
if not api_key:
|
||||
# 向后兼容:从 params 中获取
|
||||
api_key = params.get("api_key") or params.get("llm_api_key")
|
||||
|
||||
if not api_key:
|
||||
return JSONResponse({
|
||||
"jsonrpc": "2.0",
|
||||
"id": request.id,
|
||||
"error": {
|
||||
"code": -32602,
|
||||
"message": "Invalid params: 'api_key' or 'llm_api_key' is required"
|
||||
"message": "Authentication required: provide 'api_key' header or 'Authorization: Bearer xxx' header"
|
||||
}
|
||||
})
|
||||
|
||||
@@ -318,8 +363,7 @@ class MCPSearchAgentServer:
|
||||
|
||||
response = await agent.search(query=query)
|
||||
|
||||
# 如果创建了新Agent,关闭它
|
||||
if api_key or model:
|
||||
# 关闭 Agent(每个请求都创建新的 Agent)
|
||||
await agent.close()
|
||||
|
||||
# 构建响应数据
|
||||
@@ -360,8 +404,13 @@ class MCPSearchAgentServer:
|
||||
}
|
||||
})
|
||||
|
||||
async def _handle_search_stream(self, request: MCPRequest) -> StreamingResponse:
|
||||
"""处理 search/stream 请求 (SSE)"""
|
||||
async def _handle_search_stream(self, request: MCPRequest, http_request: Request) -> StreamingResponse:
|
||||
"""处理 search/stream 请求 (SSE)
|
||||
|
||||
认证方式(优先级从高到低):
|
||||
1. HTTP headers: api_key 或 Authorization: Bearer xxx
|
||||
2. JSON body params: api_key 或 llm_api_key(向后兼容)
|
||||
"""
|
||||
params = request.params or {}
|
||||
|
||||
# 提取搜索查询
|
||||
@@ -376,15 +425,19 @@ class MCPSearchAgentServer:
|
||||
}
|
||||
})
|
||||
|
||||
# 提取API key(必须从请求参数中获取,等同于API格式版本的llm_api_key)
|
||||
# 提取API key(优先从 headers 获取,其次从 params 获取)
|
||||
api_key = self._extract_api_key_from_headers(http_request)
|
||||
if not api_key:
|
||||
# 向后兼容:从 params 中获取
|
||||
api_key = params.get("api_key") or params.get("llm_api_key")
|
||||
|
||||
if not api_key:
|
||||
return JSONResponse({
|
||||
"jsonrpc": "2.0",
|
||||
"id": request.id,
|
||||
"error": {
|
||||
"code": -32602,
|
||||
"message": "Invalid params: 'api_key' or 'llm_api_key' is required"
|
||||
"message": "Authentication required: provide 'api_key' header or 'Authorization: Bearer xxx' header"
|
||||
}
|
||||
})
|
||||
|
||||
@@ -492,8 +545,8 @@ class MCPSearchAgentServer:
|
||||
}
|
||||
yield f"data: {json.dumps(error_event)}\n\n"
|
||||
finally:
|
||||
# 如果创建了新Agent,关闭它
|
||||
if agent and (api_key or model):
|
||||
# 关闭 Agent(每个请求都创建新的 Agent)
|
||||
if agent:
|
||||
await agent.close()
|
||||
|
||||
return StreamingResponse(
|
||||
|
||||
@@ -9,7 +9,7 @@ RUN apt-get update && apt-get install -y \
|
||||
|
||||
# 复制requirements文件
|
||||
COPY agents/search_agent/search_agent_MCP/requirements.txt /app/requirements.txt
|
||||
COPY agents/search_agent/search_agent/search_agent/requirements.txt /app/search_agent_requirements.txt
|
||||
COPY agents/search_agent/search_agent/requirements.txt /app/search_agent_requirements.txt
|
||||
|
||||
# 安装Python依赖
|
||||
RUN pip install --no-cache-dir \
|
||||
@@ -23,7 +23,7 @@ RUN pip install --no-cache-dir \
|
||||
COPY agents/search_agent/search_agent_MCP/ /app/
|
||||
|
||||
# 复制search_agent核心代码
|
||||
COPY agents/search_agent/search_agent/search_agent/ /app/search_agent/
|
||||
COPY agents/search_agent/search_agent/ /app/search_agent/
|
||||
|
||||
# 设置环境变量
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
@@ -43,10 +43,16 @@ docker buildx use multiarch-builder
|
||||
# Agent 列表(更新路径)
|
||||
declare -A AGENTS=(
|
||||
["search-agent"]="agents/search_agent/search_agent.Dockerfile"
|
||||
["search-agent-a2a"]="agents/search_agent/search_agent_A2A/search_agent_A2A.Dockerfile"
|
||||
["search-agent-mcp"]="agents/search_agent/search_agent_MCP/search_agent_MCP.Dockerfile"
|
||||
["azure-blob-agent"]="agents/azure_blob_agent/azure_blob_agent.Dockerfile"
|
||||
["azure-blob-agent-a2a"]="agents/azure_blob_agent_a2a/azure_blob_agent_a2a.Dockerfile"
|
||||
["azure-blob-agent-mcp"]="agents/azure_blob_agent_mcp/azure_blob_agent_mcp.Dockerfile"
|
||||
["a2a-litellm-agent"]="agents/a2a_litellm_agent/a2a_litellm_agent.Dockerfile"
|
||||
["mysql-agent"]="agents/mysql_agent/mysql_agent.Dockerfile"
|
||||
["postgresql-agent"]="agents/postgresql_agent/postgresql_agent.Dockerfile"
|
||||
["jina-search-agent"]="agents/jina_search_agent/jina_search_agent.Dockerfile"
|
||||
["echo-agent"]="agents/echo_agent/echo_agent.Dockerfile"
|
||||
)
|
||||
|
||||
# 构建函数
|
||||
|
||||
@@ -300,7 +300,7 @@ async def create_agent(request: CreateAgentRequest, db: Session = Depends(get_db
|
||||
logger.info(f"收到创建Agent请求: {request.name}, 模板: {request.template}")
|
||||
|
||||
# 验证模板类型
|
||||
valid_templates = ["echo_agent", "search_agent", "mysql_agent", "postgresql_agent", "jina_search_agent", "azure_blob_agent", "azure_blob_agent_mcp", "azure_blob_agent_a2a", "a2a_litellm_agent"]
|
||||
valid_templates = ["echo_agent", "search_agent", "search_agent_a2a", "search_agent_mcp", "mysql_agent", "postgresql_agent", "jina_search_agent", "azure_blob_agent", "azure_blob_agent_mcp", "azure_blob_agent_a2a", "a2a_litellm_agent", "code_ai_agent", "facebook_agent"]
|
||||
if request.template not in valid_templates:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
@@ -577,16 +577,44 @@ async def get_agent_status(agent_name: str, db: Session = Depends(get_db)):
|
||||
try:
|
||||
logger.info(f"获取Agent状态: {agent_name}")
|
||||
|
||||
# 从Kubernetes获取Pod状态
|
||||
result = k8s_manager.get_pod_status(pod_name=agent_name)
|
||||
# 先从数据库获取Agent的namespace
|
||||
db_agent = db.query(Agent).filter(Agent.name == agent_name).first()
|
||||
|
||||
# 确定Agent所在的namespace
|
||||
agent_namespace = None
|
||||
if db_agent and db_agent.namespace:
|
||||
agent_namespace = db_agent.namespace
|
||||
else:
|
||||
# 如果数据库中没有,尝试查找以 agent-{agent_name} 开头的命名空间
|
||||
try:
|
||||
namespaces = k8s_manager.v1.list_namespace(
|
||||
label_selector=f"agent-name={agent_name}"
|
||||
)
|
||||
if namespaces.items:
|
||||
agent_namespace = namespaces.items[0].metadata.name
|
||||
else:
|
||||
# 尝试常见的命名空间格式
|
||||
for ns_pattern in [f"agent-{agent_name}", f"agent-test-{agent_name}"]:
|
||||
try:
|
||||
k8s_manager.v1.read_namespace(name=ns_pattern)
|
||||
agent_namespace = ns_pattern
|
||||
break
|
||||
except:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning(f"查找命名空间失败: {e}")
|
||||
|
||||
if not agent_namespace:
|
||||
raise HTTPException(status_code=404, detail=f"Agent {agent_name} 的命名空间未找到")
|
||||
|
||||
# 使用正确的namespace获取Pod状态
|
||||
temp_manager = K8sManager(namespace=agent_namespace, kubeconfig_path=KUBECONFIG_PATH)
|
||||
result = temp_manager.get_pod_status(pod_name=agent_name)
|
||||
|
||||
if result.get("status") == "not_found":
|
||||
raise HTTPException(status_code=404, detail=result.get("message"))
|
||||
|
||||
# 从数据库获取Agent记录(包含访问信息)
|
||||
try:
|
||||
db_agent = db.query(Agent).filter(Agent.name == agent_name).first()
|
||||
|
||||
# 添加数据库中的信息
|
||||
if db_agent:
|
||||
# 添加框架类型
|
||||
result["framework"] = db_agent.agent_framework.upper() if db_agent.agent_framework else "API"
|
||||
@@ -619,11 +647,7 @@ async def get_agent_status(agent_name: str, db: Session = Depends(get_db)):
|
||||
else:
|
||||
logger.warning(f"⚠️ Agent {agent_name} 在数据库中没有访问信息")
|
||||
else:
|
||||
logger.warning(f"⚠️ Agent {agent_name} 在数据库中未找到,可能是在数据库启用前创建的")
|
||||
|
||||
except Exception as db_error:
|
||||
logger.error(f"从数据库读取访问信息失败: {str(db_error)}")
|
||||
# 不抛出异常,继续返回Pod状态信息
|
||||
logger.warning(f"⚠️ Agent {agent_name} 在数据库中未找到")
|
||||
|
||||
return PodStatusResponse(**result)
|
||||
|
||||
@@ -635,33 +659,70 @@ async def get_agent_status(agent_name: str, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@app.get("/agents/{agent_name}/metrics", response_model=PodMetricsResponse)
|
||||
async def get_agent_metrics(agent_name: str):
|
||||
async def get_agent_metrics(agent_name: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
获取Agent资源使用情况
|
||||
|
||||
Args:
|
||||
agent_name: Agent名称
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Agent资源使用信息
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取Agent资源信息: {agent_name}")
|
||||
result = k8s_manager.get_pod_metrics(pod_name=agent_name)
|
||||
|
||||
# 先从数据库获取Agent的namespace
|
||||
db_agent = db.query(Agent).filter(Agent.name == agent_name).first()
|
||||
|
||||
# 确定Agent所在的namespace
|
||||
agent_namespace = None
|
||||
if db_agent and db_agent.namespace:
|
||||
agent_namespace = db_agent.namespace
|
||||
else:
|
||||
# 如果数据库中没有,尝试查找以 agent-{agent_name} 开头的命名空间
|
||||
try:
|
||||
namespaces = k8s_manager.v1.list_namespace(
|
||||
label_selector=f"agent-name={agent_name}"
|
||||
)
|
||||
if namespaces.items:
|
||||
agent_namespace = namespaces.items[0].metadata.name
|
||||
else:
|
||||
# 尝试常见的命名空间格式
|
||||
for ns_pattern in [f"agent-{agent_name}", f"agent-test-{agent_name}"]:
|
||||
try:
|
||||
k8s_manager.v1.read_namespace(name=ns_pattern)
|
||||
agent_namespace = ns_pattern
|
||||
break
|
||||
except:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning(f"查找命名空间失败: {e}")
|
||||
|
||||
if not agent_namespace:
|
||||
raise HTTPException(status_code=404, detail=f"Agent {agent_name} 的命名空间未找到")
|
||||
|
||||
# 使用正确的namespace获取Pod指标
|
||||
temp_manager = K8sManager(namespace=agent_namespace, kubeconfig_path=KUBECONFIG_PATH)
|
||||
result = temp_manager.get_pod_metrics(pod_name=agent_name)
|
||||
return PodMetricsResponse(**result)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取Agent资源信息失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/agents")
|
||||
async def list_agents(template: Optional[str] = None):
|
||||
async def list_agents(template: Optional[str] = None, db: Session = Depends(get_db)):
|
||||
"""
|
||||
列出所有Agent
|
||||
列出所有Agent(跨所有命名空间)
|
||||
|
||||
Args:
|
||||
template: 模板类型过滤(可选)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Agent列表
|
||||
@@ -669,12 +730,94 @@ async def list_agents(template: Optional[str] = None):
|
||||
try:
|
||||
logger.info(f"列出Agents, 模板过滤: {template}")
|
||||
|
||||
all_agents = []
|
||||
|
||||
# 方法1: 从数据库获取Agent列表(推荐)
|
||||
try:
|
||||
query = db.query(Agent)
|
||||
db_agents = query.all()
|
||||
|
||||
for db_agent in db_agents:
|
||||
# 尝试从K8s获取Pod状态
|
||||
pod_status = "Unknown"
|
||||
pod_ip = None
|
||||
try:
|
||||
if db_agent.namespace:
|
||||
temp_manager = K8sManager(namespace=db_agent.namespace, kubeconfig_path=KUBECONFIG_PATH)
|
||||
pod = temp_manager.v1.read_namespaced_pod(
|
||||
name=db_agent.name,
|
||||
namespace=db_agent.namespace
|
||||
)
|
||||
pod_status = pod.status.phase
|
||||
pod_ip = pod.status.pod_ip
|
||||
except Exception:
|
||||
pod_status = "NotFound"
|
||||
|
||||
agent_info = {
|
||||
"name": db_agent.name,
|
||||
"namespace": db_agent.namespace,
|
||||
"status": pod_status,
|
||||
"template": db_agent.agent_framework or "unknown",
|
||||
"created_at": db_agent.created_at.isoformat() if db_agent.created_at else None,
|
||||
"pod_ip": pod_ip,
|
||||
"external_ip": db_agent.external_ip,
|
||||
"domain": db_agent.domain,
|
||||
"service_url": db_agent.recommended_url
|
||||
}
|
||||
|
||||
# 应用模板过滤
|
||||
if template and agent_info.get("template") != template:
|
||||
continue
|
||||
|
||||
all_agents.append(agent_info)
|
||||
|
||||
logger.info(f"从数据库获取到 {len(all_agents)} 个Agent")
|
||||
|
||||
except Exception as db_error:
|
||||
logger.warning(f"从数据库获取Agent列表失败: {db_error}")
|
||||
|
||||
# 方法2: 遍历所有以 agent- 开头的命名空间(作为补充)
|
||||
try:
|
||||
namespaces = k8s_manager.v1.list_namespace()
|
||||
agent_namespaces = [
|
||||
ns.metadata.name for ns in namespaces.items
|
||||
if ns.metadata.name.startswith("agent-")
|
||||
]
|
||||
|
||||
# 已经从数据库获取的Agent名称
|
||||
known_agents = {a["name"] for a in all_agents}
|
||||
|
||||
for ns_name in agent_namespaces:
|
||||
try:
|
||||
temp_manager = K8sManager(namespace=ns_name, kubeconfig_path=KUBECONFIG_PATH)
|
||||
label_selector = "managed-by=agent-manager"
|
||||
if template:
|
||||
label_selector += f",template={template}"
|
||||
|
||||
result = k8s_manager.list_pods(label_selector=label_selector)
|
||||
return {"agents": result, "count": len(result)}
|
||||
pods = temp_manager.v1.list_namespaced_pod(
|
||||
namespace=ns_name,
|
||||
label_selector=label_selector
|
||||
)
|
||||
|
||||
for pod in pods.items:
|
||||
if pod.metadata.name not in known_agents:
|
||||
agent_info = {
|
||||
"name": pod.metadata.name,
|
||||
"namespace": ns_name,
|
||||
"status": pod.status.phase,
|
||||
"template": pod.metadata.labels.get("template", "unknown"),
|
||||
"created_at": pod.metadata.creation_timestamp.isoformat() if pod.metadata.creation_timestamp else None,
|
||||
"pod_ip": pod.status.pod_ip
|
||||
}
|
||||
all_agents.append(agent_info)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"命名空间 {ns_name} 查询失败: {e}")
|
||||
|
||||
except Exception as ns_error:
|
||||
logger.warning(f"遍历命名空间失败: {ns_error}")
|
||||
|
||||
return {"agents": all_agents, "count": len(all_agents)}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"列出Agents失败: {str(e)}")
|
||||
@@ -689,7 +832,7 @@ async def list_templates():
|
||||
Returns:
|
||||
模板列表及其配置信息
|
||||
"""
|
||||
valid_templates = ["echo_agent", "search_agent", "mysql_agent", "postgresql_agent", "jina_search_agent", "azure_blob_agent", "azure_blob_agent_mcp", "azure_blob_agent_a2a", "a2a_litellm_agent"]
|
||||
valid_templates = ["echo_agent", "search_agent", "search_agent_a2a", "search_agent_mcp", "mysql_agent", "postgresql_agent", "jina_search_agent", "azure_blob_agent", "azure_blob_agent_mcp", "azure_blob_agent_a2a", "a2a_litellm_agent", "code_ai_agent", "facebook_agent"]
|
||||
|
||||
templates_info = []
|
||||
for template in valid_templates:
|
||||
@@ -711,7 +854,7 @@ async def list_platform_templates():
|
||||
平台提供的Agent模板列表
|
||||
"""
|
||||
# 平台 Agent 是预定义的标准模板
|
||||
platform_templates = ["echo_agent", "search_agent", "jina_search_agent", "azure_blob_agent", "azure_blob_agent_mcp", "azure_blob_agent_a2a", "a2a_litellm_agent"]
|
||||
platform_templates = ["echo_agent", "search_agent", "search_agent_a2a", "search_agent_mcp", "jina_search_agent", "azure_blob_agent", "azure_blob_agent_mcp", "azure_blob_agent_a2a", "a2a_litellm_agent", "code_ai_agent", "facebook_agent"]
|
||||
|
||||
templates_info = []
|
||||
for template in platform_templates:
|
||||
@@ -761,7 +904,7 @@ async def get_template_info(template_name: str):
|
||||
Returns:
|
||||
模板详细信息(端口、所需环境变量等)
|
||||
"""
|
||||
valid_templates = ["echo_agent", "search_agent", "mysql_agent", "postgresql_agent", "jina_search_agent", "azure_blob_agent", "azure_blob_agent_mcp", "azure_blob_agent_a2a", "a2a_litellm_agent"]
|
||||
valid_templates = ["echo_agent", "search_agent", "search_agent_a2a", "search_agent_mcp", "mysql_agent", "postgresql_agent", "jina_search_agent", "azure_blob_agent", "azure_blob_agent_mcp", "azure_blob_agent_a2a", "a2a_litellm_agent", "code_ai_agent", "facebook_agent"]
|
||||
|
||||
if template_name not in valid_templates:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"code-ai-agent": {
|
||||
"url": "http://test-code-ai-agent-4.taijiagnet.com/mcp",
|
||||
"type": "http",
|
||||
"description": "代码助手 Agent - 支持代码生成、重构、审查和组织功能。提供以下工具:generate_code, refactor_code, review_code, organize_code, classify_code, analyze_project, suggest_folder_structure, create_code_file"
|
||||
},
|
||||
"facebook-agent": {
|
||||
"url": "http://test-facebook-agent-6.taijiagnet.com/mcp",
|
||||
"type": "http",
|
||||
"description": "Facebook 搜索 Agent - 支持 Facebook 内容搜索,返回相关帖子和AI生成的总结"
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
-3
@@ -314,6 +314,8 @@ class K8sManager:
|
||||
TEMPLATE_PORTS = {
|
||||
"echo_agent": 8000,
|
||||
"search_agent": 8080,
|
||||
"search_agent_a2a": 8080,
|
||||
"search_agent_mcp": 8080,
|
||||
"mysql_agent": 8000,
|
||||
"postgresql_agent": 8000,
|
||||
"jina_search_agent": 8080,
|
||||
@@ -321,6 +323,8 @@ class K8sManager:
|
||||
"azure_blob_agent_mcp": 8080,
|
||||
"azure_blob_agent_a2a": 8080,
|
||||
"a2a_litellm_agent": 8080,
|
||||
"code_ai_agent": 8000,
|
||||
"facebook_agent": 8000,
|
||||
}
|
||||
|
||||
# 模板所需环境变量说明
|
||||
@@ -417,7 +421,7 @@ class K8sManager:
|
||||
},
|
||||
"optional": {
|
||||
"LLM_API_KEY": "LLM API 密钥(可在搜索请求中传入)",
|
||||
"LLM_MODEL": "LLM 模型名称,默认 gpt-4o-mini",
|
||||
"MODEL_NAME": "LLM 模型名称,默认 gpt-4o-mini",
|
||||
"MAX_ITERATIONS": "最大搜索迭代次数,默认 3",
|
||||
"MAX_RESULTS_PER_QUERY": "每次搜索最大结果数,默认 10",
|
||||
"CONTENT_MAX_LENGTH": "内容最大长度,默认 5000",
|
||||
@@ -426,6 +430,40 @@ class K8sManager:
|
||||
"SERVICE_HOST": "HTTP服务监听地址,默认 0.0.0.0"
|
||||
}
|
||||
},
|
||||
"search_agent_a2a": {
|
||||
"required": {
|
||||
"LLM_BASE_URL": "LLM 服务地址,如 https://api.openai.com/v1",
|
||||
},
|
||||
"optional": {
|
||||
"LITELLM_API_KEY": "LiteLLM API 密钥(可在请求中传入)",
|
||||
"LLM_API_KEY": "LLM API 密钥(备选,可在请求中传入)",
|
||||
"MODEL_NAME": "LLM 模型名称(优先)",
|
||||
"LLM_MODEL": "LLM 模型名称(备选)",
|
||||
"LITELLM_MODEL": "LiteLLM 模型名称(备选)",
|
||||
"SERPER_API_KEY": "Serper 搜索 API 密钥(已内置默认值)",
|
||||
"JINA_API_KEY": "Jina Reader API 密钥(已内置默认值)",
|
||||
"SERVICE_PORT": "HTTP服务端口,默认 8080",
|
||||
"SERVICE_HOST": "HTTP服务监听地址,默认 0.0.0.0"
|
||||
},
|
||||
"description": "A2A 协议搜索 Agent,支持通过请求动态传入 API key"
|
||||
},
|
||||
"search_agent_mcp": {
|
||||
"required": {
|
||||
"LLM_BASE_URL": "LLM 服务地址,如 https://api.openai.com/v1",
|
||||
},
|
||||
"optional": {
|
||||
"LITELLM_API_KEY": "LiteLLM API 密钥(可在请求中传入)",
|
||||
"LLM_API_KEY": "LLM API 密钥(备选,可在请求中传入)",
|
||||
"MODEL_NAME": "LLM 模型名称(优先)",
|
||||
"LLM_MODEL": "LLM 模型名称(备选)",
|
||||
"LITELLM_MODEL": "LiteLLM 模型名称(备选)",
|
||||
"SERPER_API_KEY": "Serper 搜索 API 密钥(已内置默认值)",
|
||||
"JINA_API_KEY": "Jina Reader API 密钥(已内置默认值)",
|
||||
"SERVICE_PORT": "HTTP服务端口,默认 8080",
|
||||
"SERVICE_HOST": "HTTP服务监听地址,默认 0.0.0.0"
|
||||
},
|
||||
"description": "MCP 协议搜索 Agent,支持通过请求动态传入 API key"
|
||||
},
|
||||
"a2a_litellm_agent": {
|
||||
"required": {
|
||||
"LITELLM_API_BASE": "LiteLLM 服务地址",
|
||||
@@ -438,6 +476,35 @@ class K8sManager:
|
||||
"SERVICE_PORT": "HTTP服务端口,默认 8080",
|
||||
"SERVICE_HOST": "HTTP服务监听地址,默认 0.0.0.0"
|
||||
}
|
||||
},
|
||||
"code_ai_agent": {
|
||||
"required": {
|
||||
"LLM_BASE_URL": "LLM 服务地址,如 https://api.openai.com/v1",
|
||||
},
|
||||
"optional": {
|
||||
"LLM_API_KEY": "LLM API 密钥(可在请求中传入)",
|
||||
"MODEL_NAME": "LLM 模型名称",
|
||||
"API_HOST": "API 服务监听地址,默认 0.0.0.0",
|
||||
"API_PORT": "API 服务端口,默认 8000",
|
||||
"PROJECTS_DIR": "项目存储目录,默认 /tmp/projects"
|
||||
},
|
||||
"description": "代码助手 Agent,支持代码生成、分析和执行"
|
||||
},
|
||||
"facebook_agent": {
|
||||
"required": {
|
||||
"LITELLM_GATEWAY_URL": "LiteLLM Gateway 服务地址,如 https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/",
|
||||
"FACEBOOK_API_KEY": "Facebook RapidAPI 密钥"
|
||||
},
|
||||
"optional": {
|
||||
"LITELLM_API_KEY": "LiteLLM API 密钥(可在请求中传入)",
|
||||
"LITELLM_MODEL": "LiteLLM 模型名称,默认 taiji/gpt-4o-mini",
|
||||
"FACEBOOK_API_HOST": "Facebook API 主机,默认 facebook-scraper3.p.rapidapi.com",
|
||||
"FACEBOOK_MCP_URL": "Facebook MCP URL,默认 https://mcp.rapidapi.com",
|
||||
"MAX_RESULTS": "最大搜索结果数,默认 10",
|
||||
"TIMEOUT": "超时时间(秒),默认 30",
|
||||
"LOG_LEVEL": "日志级别,默认 INFO"
|
||||
},
|
||||
"description": "Facebook 搜索智能 Agent,支持 Facebook 内容搜索和 MCP 协议"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,6 +590,8 @@ class K8sManager:
|
||||
image_map = {
|
||||
"echo_agent": "agnettaiji.azurecr.io/echo-agent:latest",
|
||||
"search_agent": "agnettaiji.azurecr.io/ai-agents/search-agent:latest",
|
||||
"search_agent_a2a": "agnettaiji.azurecr.io/ai-agents/search-agent-a2a:latest",
|
||||
"search_agent_mcp": "agnettaiji.azurecr.io/ai-agents/search-agent-mcp:latest",
|
||||
"mysql_agent": "agnettaiji.azurecr.io/ai-agents/mysql-agent:latest",
|
||||
"postgresql_agent": "agnettaiji.azurecr.io/ai-agents/postgresql-agent:latest",
|
||||
"jina_search_agent": "agnettaiji.azurecr.io/ai-agents/jina-search-agent:latest",
|
||||
@@ -530,6 +599,8 @@ class K8sManager:
|
||||
"azure_blob_agent_mcp": "agnettaiji.azurecr.io/ai-agents/azure-blob-agent-mcp:latest",
|
||||
"azure_blob_agent_a2a": "agnettaiji.azurecr.io/ai-agents/azure-blob-agent-a2a:latest",
|
||||
"a2a_litellm_agent": "agnettaiji.azurecr.io/ai-agents/a2a-litellm-agent:latest",
|
||||
"code_ai_agent": "agnettaiji.azurecr.io/ai-agents/code-ai-agent:latest",
|
||||
"facebook_agent": "agnettaiji.azurecr.io/ai-agents/facebook-agent:latest",
|
||||
}
|
||||
image = image_map.get(template, image_map["search_agent"])
|
||||
|
||||
@@ -591,8 +662,32 @@ class K8sManager:
|
||||
namespace = config_data.get("namespace", self.namespace)
|
||||
env_vars.append(client.V1EnvVar(name="NAMESPACE", value=namespace))
|
||||
|
||||
# 添加用户自定义环境变量
|
||||
# 为特定模板添加默认环境变量
|
||||
template_defaults = {
|
||||
"facebook_agent": {
|
||||
"LITELLM_GATEWAY_URL": "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io",
|
||||
"LITELLM_MODEL": "taiji/gpt-4o-mini",
|
||||
"FACEBOOK_API_KEY": "34225c5924msh453fa7aff7a52a9p1d7adfjsn260ba0796c00"
|
||||
},
|
||||
"code_ai_agent": {
|
||||
"OPENAI_BASE_URL": "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1",
|
||||
"LITELLM_MODEL": "taiji/gpt-4o-mini"
|
||||
}
|
||||
}
|
||||
|
||||
# 获取用户自定义环境变量
|
||||
custom_env = config_data.get("env", {})
|
||||
|
||||
# 如果模板有默认值,先添加默认值(用户自定义值会覆盖)
|
||||
if template in template_defaults:
|
||||
defaults = template_defaults[template]
|
||||
for key, value in defaults.items():
|
||||
# 只有当用户没有提供该环境变量时才使用默认值
|
||||
if key not in custom_env:
|
||||
env_vars.append(client.V1EnvVar(name=key, value=str(value)))
|
||||
logger.info(f"使用默认环境变量: {key}")
|
||||
|
||||
# 添加用户自定义环境变量(会覆盖默认值)
|
||||
for key, value in custom_env.items():
|
||||
env_vars.append(client.V1EnvVar(name=key, value=str(value)))
|
||||
|
||||
@@ -600,8 +695,10 @@ class K8sManager:
|
||||
|
||||
# 设置容器端口(如果是HTTP服务类型的agent)
|
||||
container_ports = None
|
||||
if template in ["jina_search_agent", "azure_blob_agent", "azure_blob_agent_mcp", "azure_blob_agent_a2a"]:
|
||||
if template in ["jina_search_agent", "azure_blob_agent", "azure_blob_agent_mcp", "azure_blob_agent_a2a", "search_agent", "search_agent_a2a", "search_agent_mcp", "a2a_litellm_agent"]:
|
||||
container_ports = [client.V1ContainerPort(container_port=8080)]
|
||||
elif template in ["code_ai_agent", "facebook_agent", "echo_agent", "mysql_agent", "postgresql_agent"]:
|
||||
container_ports = [client.V1ContainerPort(container_port=8000)]
|
||||
|
||||
# 创建Pod规格
|
||||
container = client.V1Container(
|
||||
|
||||
Reference in New Issue
Block a user