Initial commit: lasttest with 1 tools - src/server/api_server.py
This commit is contained in:
@@ -0,0 +1,645 @@
|
|||||||
|
"""
|
||||||
|
HTTP API 服务器 - lasttest-0db7a0
|
||||||
|
|
||||||
|
Agent with 1 external tools
|
||||||
|
提供 REST API 和 MCP HTTP/SSE 端点。
|
||||||
|
集成回调功能用于计费。
|
||||||
|
自动生成时间: 2026-02-13T15:50:34.879610
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any, AsyncGenerator, List
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
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
|
||||||
|
from .agent_callback_utils import AgentCallbackHandler, CallbackContextManager
|
||||||
|
|
||||||
|
# ==================== 配置 ====================
|
||||||
|
|
||||||
|
SERVER_NAME = "lasttest-0db7a0"
|
||||||
|
POD_NAME = os.getenv("POD_NAME", "lasttest-0db7a0")
|
||||||
|
USER_ID = os.getenv("USER_ID", "")
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 本 Agent 包含的工具列表
|
||||||
|
AGENT_TOOLS = ["jinaaitool"]
|
||||||
|
|
||||||
|
# ==================== 回调处理器 ====================
|
||||||
|
|
||||||
|
callback_handler: Optional[AgentCallbackHandler] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_callback_handler() -> AgentCallbackHandler:
|
||||||
|
"""获取或创建回调处理器(单例)"""
|
||||||
|
global callback_handler
|
||||||
|
if callback_handler is None:
|
||||||
|
callback_handler = AgentCallbackHandler(
|
||||||
|
agent_name=POD_NAME,
|
||||||
|
user_id=USER_ID
|
||||||
|
)
|
||||||
|
return callback_handler
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== FastAPI 应用 ====================
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
logger.info(f"🚀 {SERVER_NAME} 启动")
|
||||||
|
logger.info(f" 包含工具: {', '.join(AGENT_TOOLS)}")
|
||||||
|
logger.info(f" Pod 名称: {POD_NAME}")
|
||||||
|
logger.info(f" 用户 ID: {USER_ID or '未设置'}")
|
||||||
|
yield
|
||||||
|
logger.info(f"🛑 {SERVER_NAME} 关闭")
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title=SERVER_NAME,
|
||||||
|
description="Agent with 1 external tools",
|
||||||
|
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
|
||||||
|
|
||||||
|
# 允许无 API Key 访问(使用默认配置)
|
||||||
|
return os.getenv("OPENAI_API_KEY", "sk")
|
||||||
|
|
||||||
|
|
||||||
|
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 or os.getenv("OPENAI_API_KEY", "sk")
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 健康检查 ====================
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {
|
||||||
|
"service": SERVER_NAME,
|
||||||
|
"status": "running",
|
||||||
|
"tools": list(TOOL_MAP.keys()),
|
||||||
|
"tools_count": len(TOOL_MAP),
|
||||||
|
"pod_name": POD_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {"status": "healthy", "service": SERVER_NAME, "tools_count": len(TOOL_MAP)}
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== MCP 端点 ====================
|
||||||
|
|
||||||
|
sessions: Dict[str, Dict] = {}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_mcp_request(data: Dict, session_id: str = None, api_key: str = None, user_id: str = None) -> Dict:
|
||||||
|
"""处理 MCP JSON-RPC 请求(带回调)"""
|
||||||
|
method = data.get("method")
|
||||||
|
params = data.get("params", {})
|
||||||
|
req_id = data.get("id")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 使用回调上下文管理器(如果有 user_id)
|
||||||
|
effective_user_id = user_id or USER_ID
|
||||||
|
|
||||||
|
try:
|
||||||
|
if effective_user_id:
|
||||||
|
handler = get_callback_handler()
|
||||||
|
with CallbackContextManager(
|
||||||
|
handler=handler,
|
||||||
|
user_id=effective_user_id,
|
||||||
|
request_id=f"mcp-{req_id}-{int(datetime.utcnow().timestamp())}"
|
||||||
|
) as ctx:
|
||||||
|
ctx.add_tool(tool_name)
|
||||||
|
result = await TOOL_MAP[tool_name](**args)
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
user_id = request.headers.get("x-user-id") or USER_ID
|
||||||
|
response = await handle_mcp_request(body, session_id, api_key, user_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": 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)
|
||||||
|
user_id = request.headers.get("x-user-id") or USER_ID
|
||||||
|
|
||||||
|
async def stream() -> AsyncGenerator[str, None]:
|
||||||
|
response = await handle_mcp_request(body, session_id, api_key, user_id)
|
||||||
|
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 ToolCallRequest(BaseModel):
|
||||||
|
"""工具调用请求"""
|
||||||
|
tool_name: str = Field(..., description="工具名称")
|
||||||
|
parameters: Dict[str, Any] = Field(default={}, description="工具参数")
|
||||||
|
user_id: Optional[str] = Field(None, description="用户ID(用于计费回调)")
|
||||||
|
|
||||||
|
|
||||||
|
class MultiToolCallRequest(BaseModel):
|
||||||
|
"""批量工具调用请求"""
|
||||||
|
calls: List[ToolCallRequest] = Field(..., description="工具调用列表")
|
||||||
|
user_id: Optional[str] = Field(None, description="用户ID(用于计费回调)")
|
||||||
|
|
||||||
|
|
||||||
|
class ToolCallResponse(BaseModel):
|
||||||
|
"""工具调用响应"""
|
||||||
|
success: bool
|
||||||
|
result: Optional[Any] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
tools_used: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MultiToolCallResponse(BaseModel):
|
||||||
|
"""批量工具调用响应"""
|
||||||
|
success: bool
|
||||||
|
results: List[ToolCallResponse]
|
||||||
|
tools_used: List[str]
|
||||||
|
total_calls: int
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/tools")
|
||||||
|
async def list_tools():
|
||||||
|
"""列出可用工具"""
|
||||||
|
return {
|
||||||
|
"tools": [
|
||||||
|
{"name": t["name"], "description": t["description"]}
|
||||||
|
for t in TOOL_LIST
|
||||||
|
],
|
||||||
|
"count": len(TOOL_LIST)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/tools/call", response_model=ToolCallResponse)
|
||||||
|
async def call_tool(request: ToolCallRequest, api_key: str = Depends(verify_api_key)):
|
||||||
|
"""调用单个工具(带计费回调)"""
|
||||||
|
if request.tool_name not in TOOL_MAP:
|
||||||
|
raise HTTPException(status_code=404, detail=f"工具 {request.tool_name} 不存在")
|
||||||
|
|
||||||
|
effective_user_id = request.user_id or USER_ID
|
||||||
|
tools_used = [request.tool_name]
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_key = os.environ.get('OPENAI_API_KEY')
|
||||||
|
os.environ['OPENAI_API_KEY'] = api_key
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 使用回调上下文管理器
|
||||||
|
if effective_user_id:
|
||||||
|
handler = get_callback_handler()
|
||||||
|
with CallbackContextManager(
|
||||||
|
handler=handler,
|
||||||
|
user_id=effective_user_id,
|
||||||
|
request_id=f"api-{int(datetime.utcnow().timestamp())}"
|
||||||
|
) as ctx:
|
||||||
|
ctx.add_tool(request.tool_name)
|
||||||
|
result = await TOOL_MAP[request.tool_name](**request.parameters)
|
||||||
|
else:
|
||||||
|
result = await TOOL_MAP[request.tool_name](**request.parameters)
|
||||||
|
|
||||||
|
return ToolCallResponse(
|
||||||
|
success=True,
|
||||||
|
result=json.loads(result) if isinstance(result, str) else result,
|
||||||
|
tools_used=tools_used
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if old_key:
|
||||||
|
os.environ['OPENAI_API_KEY'] = old_key
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"工具调用失败: {e}")
|
||||||
|
return ToolCallResponse(success=False, error=str(e), tools_used=tools_used)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/tools/batch-call", response_model=MultiToolCallResponse)
|
||||||
|
async def batch_call_tools(request: MultiToolCallRequest, api_key: str = Depends(verify_api_key)):
|
||||||
|
"""批量调用多个工具(带计费回调)"""
|
||||||
|
effective_user_id = request.user_id or USER_ID
|
||||||
|
results = []
|
||||||
|
tools_used = []
|
||||||
|
|
||||||
|
# 设置 API Key
|
||||||
|
old_key = os.environ.get('OPENAI_API_KEY')
|
||||||
|
os.environ['OPENAI_API_KEY'] = api_key
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 使用回调上下文管理器
|
||||||
|
if effective_user_id:
|
||||||
|
handler = get_callback_handler()
|
||||||
|
with CallbackContextManager(
|
||||||
|
handler=handler,
|
||||||
|
user_id=effective_user_id,
|
||||||
|
request_id=f"batch-{int(datetime.utcnow().timestamp())}"
|
||||||
|
) as ctx:
|
||||||
|
for call in request.calls:
|
||||||
|
if call.tool_name not in TOOL_MAP:
|
||||||
|
results.append(ToolCallResponse(
|
||||||
|
success=False,
|
||||||
|
error=f"工具 {call.tool_name} 不存在"
|
||||||
|
))
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
ctx.add_tool(call.tool_name)
|
||||||
|
tools_used.append(call.tool_name)
|
||||||
|
result = await TOOL_MAP[call.tool_name](**call.parameters)
|
||||||
|
results.append(ToolCallResponse(
|
||||||
|
success=True,
|
||||||
|
result=json.loads(result) if isinstance(result, str) else result
|
||||||
|
))
|
||||||
|
except Exception as e:
|
||||||
|
results.append(ToolCallResponse(success=False, error=str(e)))
|
||||||
|
else:
|
||||||
|
for call in request.calls:
|
||||||
|
if call.tool_name not in TOOL_MAP:
|
||||||
|
results.append(ToolCallResponse(
|
||||||
|
success=False,
|
||||||
|
error=f"工具 {call.tool_name} 不存在"
|
||||||
|
))
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
tools_used.append(call.tool_name)
|
||||||
|
result = await TOOL_MAP[call.tool_name](**call.parameters)
|
||||||
|
results.append(ToolCallResponse(
|
||||||
|
success=True,
|
||||||
|
result=json.loads(result) if isinstance(result, str) else result
|
||||||
|
))
|
||||||
|
except Exception as e:
|
||||||
|
results.append(ToolCallResponse(success=False, error=str(e)))
|
||||||
|
finally:
|
||||||
|
if old_key:
|
||||||
|
os.environ['OPENAI_API_KEY'] = old_key
|
||||||
|
|
||||||
|
return MultiToolCallResponse(
|
||||||
|
success=all(r.success for r in results),
|
||||||
|
results=results,
|
||||||
|
tools_used=list(set(tools_used)),
|
||||||
|
total_calls=len(request.calls)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 智能对话(Agent Chat)====================
|
||||||
|
|
||||||
|
# LLM 配置
|
||||||
|
LLM_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1")
|
||||||
|
LLM_MODEL = os.getenv("MODEL_NAME", "taiji/gpt-4o-mini")
|
||||||
|
|
||||||
|
class ChatRequest(BaseModel):
|
||||||
|
"""聊天请求"""
|
||||||
|
message: str
|
||||||
|
conversation_id: Optional[str] = None
|
||||||
|
user_id: Optional[str] = None
|
||||||
|
stream: bool = False
|
||||||
|
|
||||||
|
class ChatResponse(BaseModel):
|
||||||
|
"""聊天响应"""
|
||||||
|
success: bool
|
||||||
|
message: str
|
||||||
|
tools_used: List[str] = []
|
||||||
|
conversation_id: Optional[str] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
# 对话历史存储
|
||||||
|
conversations: Dict[str, List[Dict]] = {}
|
||||||
|
|
||||||
|
def get_tools_description() -> str:
|
||||||
|
"""生成工具描述供 LLM 使用"""
|
||||||
|
tools_desc = []
|
||||||
|
for t in TOOL_LIST:
|
||||||
|
# 支持 inputSchema.properties 或 parameters 格式
|
||||||
|
schema = t.get("inputSchema", {})
|
||||||
|
params = schema.get("properties", t.get("parameters", {}))
|
||||||
|
required = schema.get("required", [])
|
||||||
|
param_parts = []
|
||||||
|
for k, v in params.items():
|
||||||
|
req_mark = "*" if k in required else ""
|
||||||
|
param_parts.append(f"{k}{req_mark}: {v.get('type', 'string')} ({v.get('description', '')})")
|
||||||
|
param_desc = ", ".join(param_parts)
|
||||||
|
tools_desc.append(f"- {t['name']}: {t['description']}\n 参数: {param_desc or '无'}")
|
||||||
|
return "\n".join(tools_desc)
|
||||||
|
|
||||||
|
def build_system_prompt() -> str:
|
||||||
|
"""构建系统提示"""
|
||||||
|
tools_desc = get_tools_description()
|
||||||
|
json_example = '{"action": "tool_call", "tool": "工具名称", "parameters": {"参数名": "参数值"}}'
|
||||||
|
return f"""你是一个智能助手 {SERVER_NAME},可以使用以下工具来帮助用户:
|
||||||
|
|
||||||
|
{tools_desc}
|
||||||
|
|
||||||
|
当用户的问题需要使用工具时,请按以下 JSON 格式回复:
|
||||||
|
{json_example}
|
||||||
|
|
||||||
|
当不需要工具时,直接回复用户的问题。
|
||||||
|
|
||||||
|
重要规则:
|
||||||
|
1. 如果问题可以用工具解决,优先使用工具
|
||||||
|
2. 工具调用必须严格使用上述 JSON 格式
|
||||||
|
3. 参数名必须与工具定义匹配
|
||||||
|
4. 一次只调用一个工具"""
|
||||||
|
|
||||||
|
async def call_llm(messages: List[Dict], api_key: str) -> str:
|
||||||
|
"""调用 LLM - 使用请求传入的 API Key(用于计费)"""
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
if not api_key or api_key in ("sk", "sk-test", "test"):
|
||||||
|
raise ValueError("请提供有效的 API Key(用于计费)")
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{LLM_BASE_URL}/chat/completions",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"model": LLM_MODEL,
|
||||||
|
"messages": messages,
|
||||||
|
"temperature": 0.3,
|
||||||
|
"max_tokens": 2000
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
def parse_tool_call(response: str) -> Optional[Dict]:
|
||||||
|
"""解析 LLM 响应中的工具调用"""
|
||||||
|
# 方法1:尝试直接解析整个响应
|
||||||
|
try:
|
||||||
|
data = json.loads(response.strip())
|
||||||
|
if isinstance(data, dict) and data.get("action") == "tool_call":
|
||||||
|
return data
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 方法2:提取 JSON 块(处理 markdown 代码块)
|
||||||
|
import re
|
||||||
|
# 匹配 ```json ... ``` 或 ``` ... ```
|
||||||
|
code_block = re.search(r'```(?:json)?\s*([\s\S]*?)```', response)
|
||||||
|
if code_block:
|
||||||
|
try:
|
||||||
|
data = json.loads(code_block.group(1).strip())
|
||||||
|
if isinstance(data, dict) and data.get("action") == "tool_call":
|
||||||
|
return data
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 方法3:查找 JSON 对象(从 { 到匹配的 })
|
||||||
|
start = response.find('{')
|
||||||
|
if start == -1:
|
||||||
|
start = response.find('{"{"') # 处理转义
|
||||||
|
if start == -1:
|
||||||
|
start = response.find('{"action"')
|
||||||
|
|
||||||
|
if start != -1:
|
||||||
|
# 找到平衡的 }
|
||||||
|
depth = 0
|
||||||
|
end = start
|
||||||
|
for i, c in enumerate(response[start:]):
|
||||||
|
if c == '{':
|
||||||
|
depth += 1
|
||||||
|
elif c == '}':
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
end = start + i + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(response[start:end])
|
||||||
|
if isinstance(data, dict) and data.get("action") == "tool_call":
|
||||||
|
return data
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.post("/chat", response_model=ChatResponse)
|
||||||
|
async def chat(request: ChatRequest, api_key: str = Depends(verify_api_key)):
|
||||||
|
"""
|
||||||
|
智能对话端点 - Agent 自动选择并调用工具
|
||||||
|
|
||||||
|
输入自然语言,Agent 会:
|
||||||
|
1. 理解用户意图
|
||||||
|
2. 自动选择合适的工具
|
||||||
|
3. 执行工具并返回结果
|
||||||
|
"""
|
||||||
|
effective_user_id = request.user_id or USER_ID
|
||||||
|
tools_used = []
|
||||||
|
|
||||||
|
# 获取或创建对话历史
|
||||||
|
conv_id = request.conversation_id or str(uuid.uuid4())
|
||||||
|
if conv_id not in conversations:
|
||||||
|
conversations[conv_id] = []
|
||||||
|
|
||||||
|
# 构建消息
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": build_system_prompt()}
|
||||||
|
]
|
||||||
|
messages.extend(conversations[conv_id])
|
||||||
|
messages.append({"role": "user", "content": request.message})
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用 LLM
|
||||||
|
llm_response = await call_llm(messages, api_key)
|
||||||
|
|
||||||
|
# 检查是否需要调用工具
|
||||||
|
tool_call = parse_tool_call(llm_response)
|
||||||
|
|
||||||
|
if tool_call and tool_call.get("tool") in TOOL_MAP:
|
||||||
|
tool_name = tool_call["tool"]
|
||||||
|
tool_params = tool_call.get("parameters", {})
|
||||||
|
tools_used.append(tool_name)
|
||||||
|
|
||||||
|
logger.info(f"🔧 调用工具: {tool_name}, 参数: {tool_params}")
|
||||||
|
|
||||||
|
# 执行工具调用
|
||||||
|
if effective_user_id:
|
||||||
|
handler = get_callback_handler()
|
||||||
|
with CallbackContextManager(
|
||||||
|
handler=handler,
|
||||||
|
user_id=effective_user_id,
|
||||||
|
request_id=f"chat-{int(datetime.utcnow().timestamp())}"
|
||||||
|
) as ctx:
|
||||||
|
ctx.add_tool(tool_name)
|
||||||
|
tool_result = await TOOL_MAP[tool_name](**tool_params)
|
||||||
|
else:
|
||||||
|
tool_result = await TOOL_MAP[tool_name](**tool_params)
|
||||||
|
|
||||||
|
# 将工具结果发送给 LLM 生成最终回复
|
||||||
|
messages.append({"role": "assistant", "content": llm_response})
|
||||||
|
messages.append({"role": "user", "content": f"工具 {tool_name} 返回结果:{tool_result}\n\n请根据这个结果回答用户的问题。"})
|
||||||
|
|
||||||
|
final_response = await call_llm(messages, api_key)
|
||||||
|
|
||||||
|
# 保存对话历史
|
||||||
|
conversations[conv_id].append({"role": "user", "content": request.message})
|
||||||
|
conversations[conv_id].append({"role": "assistant", "content": final_response})
|
||||||
|
|
||||||
|
return ChatResponse(
|
||||||
|
success=True,
|
||||||
|
message=final_response,
|
||||||
|
tools_used=tools_used,
|
||||||
|
conversation_id=conv_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 不需要工具,直接返回 LLM 回复
|
||||||
|
conversations[conv_id].append({"role": "user", "content": request.message})
|
||||||
|
conversations[conv_id].append({"role": "assistant", "content": llm_response})
|
||||||
|
|
||||||
|
return ChatResponse(
|
||||||
|
success=True,
|
||||||
|
message=llm_response,
|
||||||
|
tools_used=[],
|
||||||
|
conversation_id=conv_id
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"聊天失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return ChatResponse(
|
||||||
|
success=False,
|
||||||
|
message="",
|
||||||
|
error=str(e),
|
||||||
|
conversation_id=conv_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/chat/history/{conversation_id}")
|
||||||
|
async def get_chat_history(conversation_id: str):
|
||||||
|
"""获取对话历史"""
|
||||||
|
if conversation_id not in conversations:
|
||||||
|
raise HTTPException(status_code=404, detail="对话不存在")
|
||||||
|
return {"conversation_id": conversation_id, "messages": conversations[conversation_id]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/chat/history/{conversation_id}")
|
||||||
|
async def clear_chat_history(conversation_id: str):
|
||||||
|
"""清除对话历史"""
|
||||||
|
if conversation_id in conversations:
|
||||||
|
del conversations[conversation_id]
|
||||||
|
return {"success": True, "message": "对话历史已清除"}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
Reference in New Issue
Block a user