Update: 更新代码和添加新功能

This commit is contained in:
zhanggangyong
2026-01-23 07:30:24 +00:00
parent 647f9e6e31
commit 377c7f4b0e
81 changed files with 7634 additions and 137 deletions
+145
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
)
# 构建函数
+164 -21
View File
@@ -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(
+14
View File
@@ -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
View File
@@ -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(