forked from zhanggangyong/agent_management
- 更新 AZ_CLIENT_ID 为新的 SP - 更新 AZ_CLIENT_SECRET - 更新 AZ_SUBSCRIPTION_ID 为新订阅
1123 lines
36 KiB
Python
1123 lines
36 KiB
Python
"""
|
||
Agent 代码生成模块
|
||
使用 AI 根据工具定义生成 Pydantic Agent 代码
|
||
基于 agent_templates/agents/_template 框架
|
||
包含完整 CI/CD 配置
|
||
"""
|
||
|
||
import os
|
||
import json
|
||
import logging
|
||
import re
|
||
import requests
|
||
from typing import Dict, List, Optional, Any
|
||
from datetime import datetime
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class AgentCodeGenerator:
|
||
"""Agent 代码生成器 - 基于 _template 框架"""
|
||
|
||
def __init__(self):
|
||
# LLM 配置 (用于 AI 生成代码)
|
||
self.llm_base_url = os.getenv(
|
||
"LLM_BASE_URL",
|
||
"https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1"
|
||
)
|
||
self.llm_api_key = os.getenv("LLM_API_KEY", "sk-mPV5MVVVVvfGSkXA-ASQXQ")
|
||
self.llm_model = os.getenv("LLM_MODEL", "taiji/claude-sonnet-4-5")
|
||
|
||
# ACR 配置
|
||
self.acr_registry = os.getenv("ACR_REGISTRY", "agnettaiji.azurecr.io")
|
||
self.acr_namespace = os.getenv("ACR_NAMESPACE", "ai-agents")
|
||
|
||
# CI/CD Secrets 默认值(用于文档说明)
|
||
self.cicd_secrets = {
|
||
"ACR_LOGIN_SERVER": "agnettaiji.azurecr.io",
|
||
"ACR_USERNAME": "agnettaiji",
|
||
"ACR_PASSWORD": "hDpX5t34N5ZmnKdtqyjYL5co/SnXJrmD20CRpGpWaG+ACRCw2wGM",
|
||
"AZ_CLIENT_ID": "f2dd1cb2-02f6-4efb-bc72-d148f6e01545",
|
||
"AZ_CLIENT_SECRET": "iym8Q~n5iHfqTTXofnKN8QSomhrUXmXXOBH9ubgq",
|
||
"AZ_TENANT_ID": "263c3ff6-1be5-4141-8308-b188464fb297",
|
||
"AZ_SUBSCRIPTION_ID": "45d7a360-af09-40fc-9afc-56dc475245ec",
|
||
"AZ_RG": "taiji-ai-pda",
|
||
"AZ_AKS": "taiji-ai-pda",
|
||
"AZURE_DNS_ZONE": "taijiagnet.com"
|
||
}
|
||
|
||
def _call_llm(self, prompt: str, system_prompt: str = None) -> str:
|
||
"""调用 LLM 生成代码"""
|
||
messages = []
|
||
|
||
if system_prompt:
|
||
messages.append({"role": "system", "content": system_prompt})
|
||
|
||
messages.append({"role": "user", "content": prompt})
|
||
|
||
try:
|
||
response = requests.post(
|
||
f"{self.llm_base_url}/chat/completions",
|
||
headers={
|
||
"Authorization": f"Bearer {self.llm_api_key}",
|
||
"Content-Type": "application/json"
|
||
},
|
||
json={
|
||
"model": self.llm_model,
|
||
"messages": messages,
|
||
"temperature": 0.2,
|
||
"max_tokens": 8000
|
||
},
|
||
timeout=120
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
result = response.json()
|
||
return result["choices"][0]["message"]["content"]
|
||
else:
|
||
logger.error(f"LLM 调用失败: {response.text}")
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"LLM 调用异常: {e}")
|
||
return None
|
||
|
||
def _convert_name_to_python(self, name: str) -> str:
|
||
"""将工具名称转换为 Python 函数名"""
|
||
name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
|
||
if name and name[0].isdigit():
|
||
name = '_' + name
|
||
return name.lower()
|
||
|
||
def _json_type_to_python(self, json_type: str) -> str:
|
||
"""JSON Schema 类型转 Python 类型"""
|
||
type_map = {
|
||
"string": "str",
|
||
"integer": "int",
|
||
"number": "float",
|
||
"boolean": "bool",
|
||
"array": "list",
|
||
"object": "dict"
|
||
}
|
||
return type_map.get(json_type, "Any")
|
||
|
||
def generate_mcp_server(
|
||
self,
|
||
agent_name: str,
|
||
description: str,
|
||
tools_config: List[Dict]
|
||
) -> str:
|
||
"""
|
||
生成 MCP 服务器代码 (mcp_server.py)
|
||
基于 _template/src/server/mcp_server.py 框架
|
||
"""
|
||
# 生成工具函数
|
||
tool_functions = []
|
||
tool_map_entries = []
|
||
tool_list_entries = []
|
||
|
||
for tool in tools_config:
|
||
name = tool.get("name", "custom_tool")
|
||
func_name = self._convert_name_to_python(name)
|
||
desc = tool.get("description", "")
|
||
url = tool.get("url", "")
|
||
method = tool.get("method", "GET").upper()
|
||
auth = tool.get("auth", {})
|
||
request_params = tool.get("request_params", {})
|
||
timeout = tool.get("timeout", 30)
|
||
|
||
# 构建参数
|
||
params = []
|
||
params_doc = []
|
||
properties = {}
|
||
required_params = []
|
||
|
||
# 支持两种格式:
|
||
# 1. {"properties": {"symbol": {...}}}
|
||
# 2. {"symbol": {...}} (直接参数格式)
|
||
param_props = request_params
|
||
if request_params and request_params.get("properties"):
|
||
param_props = request_params["properties"]
|
||
elif request_params and not any(k in request_params for k in ["type", "required", "description"]):
|
||
# 直接参数格式
|
||
param_props = request_params
|
||
else:
|
||
param_props = {}
|
||
|
||
if param_props:
|
||
for p_name, p_info in param_props.items():
|
||
if not isinstance(p_info, dict):
|
||
continue
|
||
p_type = self._json_type_to_python(p_info.get("type", "string"))
|
||
p_desc = p_info.get("description", "")
|
||
is_required = p_info.get("required", False)
|
||
default = p_info.get("default")
|
||
|
||
if is_required:
|
||
params.append(f"{p_name}: {p_type}")
|
||
required_params.append(p_name)
|
||
else:
|
||
default_val = f'"{default}"' if isinstance(default, str) else (default if default is not None else "None")
|
||
params.append(f"{p_name}: Optional[{p_type}] = {default_val}")
|
||
|
||
params_doc.append(f" {p_name}: {p_desc}")
|
||
properties[p_name] = {"type": p_info.get("type", "string"), "description": p_desc}
|
||
|
||
params_str = ", ".join(params) if params else ""
|
||
params_doc_str = "\n".join(params_doc) if params_doc else " 无参数"
|
||
|
||
# 生成认证代码
|
||
auth_headers = self._get_auth_headers_code(auth)
|
||
|
||
# 构建参数字典代码
|
||
params_dict_code = ""
|
||
if param_props:
|
||
params_dict_code = "params = {"
|
||
for p_name in param_props.keys():
|
||
if isinstance(param_props[p_name], dict):
|
||
params_dict_code += f'"{p_name}": {p_name}, '
|
||
params_dict_code = params_dict_code.rstrip(", ") + "}"
|
||
else:
|
||
params_dict_code = "params = {}"
|
||
|
||
# API Key in query
|
||
if auth and auth.get("type") == "api_key" and auth.get("in") == "query":
|
||
key_name = auth.get("name", "apikey")
|
||
params_dict_code += f'\n params["{key_name}"] = os.getenv("TOOL_API_KEY", "")'
|
||
|
||
# 生成函数代码
|
||
func_code = f'''
|
||
@server.tool()
|
||
async def {func_name}({params_str}) -> str:
|
||
"""
|
||
{desc}
|
||
|
||
Args:
|
||
{params_doc_str}
|
||
|
||
Returns:
|
||
API 响应结果 (JSON 格式)
|
||
"""
|
||
import httpx
|
||
|
||
url = "{url}"
|
||
{auth_headers}
|
||
{params_dict_code}
|
||
|
||
try:
|
||
async with httpx.AsyncClient(timeout={timeout}) as client:
|
||
response = await client.request(
|
||
method="{method}",
|
||
url=url,
|
||
headers=headers,
|
||
params={{k: v for k, v in params.items() if v is not None}}
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
return json.dumps({{
|
||
"success": True,
|
||
"data": response.json() if response.headers.get("content-type", "").startswith("application/json") else response.text
|
||
}}, ensure_ascii=False, indent=2)
|
||
else:
|
||
return json.dumps({{
|
||
"success": False,
|
||
"status_code": response.status_code,
|
||
"error": response.text[:500]
|
||
}}, ensure_ascii=False)
|
||
|
||
except Exception as e:
|
||
# 使用 AI Agent 作为后备
|
||
result = await get_agent().run(f"请帮我处理这个请求: {params}")
|
||
return json.dumps({{
|
||
"success": True,
|
||
"source": "ai_agent",
|
||
"result": result.output
|
||
}}, ensure_ascii=False, indent=2)
|
||
'''
|
||
tool_functions.append(func_code)
|
||
tool_map_entries.append(f" '{func_name}': {func_name},")
|
||
|
||
tool_list_entries.append(f''' {{
|
||
"name": "{func_name}",
|
||
"description": "{desc}",
|
||
"inputSchema": {{
|
||
"type": "object",
|
||
"properties": {json.dumps(properties, ensure_ascii=False)},
|
||
"required": {json.dumps(required_params)}
|
||
}}
|
||
}},''')
|
||
|
||
tool_functions_str = "\n".join(tool_functions)
|
||
tool_map_str = "\n".join(tool_map_entries)
|
||
tool_list_str = "\n".join(tool_list_entries)
|
||
|
||
# 生成系统提示词
|
||
system_prompt = self._generate_system_prompt(agent_name, description, tools_config)
|
||
|
||
return f'''"""
|
||
MCP 服务器 - {agent_name}
|
||
|
||
{description}
|
||
使用 Pydantic AI 和 FastMCP 框架。
|
||
自动生成时间: {datetime.now().isoformat()}
|
||
"""
|
||
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('{agent_name}')
|
||
|
||
# 系统提示词
|
||
SYSTEM_PROMPT = """{system_prompt}"""
|
||
|
||
|
||
def get_agent() -> Agent:
|
||
"""创建 Agent 实例(每次调用使用最新的 API Key)"""
|
||
return Agent(MODEL_NAME, system_prompt=SYSTEM_PROMPT)
|
||
|
||
|
||
# ==================== MCP 工具定义 ====================
|
||
{tool_functions_str}
|
||
|
||
# ==================== 工具映射(供 API 使用)====================
|
||
|
||
TOOL_MAP = {{
|
||
{tool_map_str}
|
||
}}
|
||
|
||
TOOL_LIST = [
|
||
{tool_list_str}
|
||
]
|
||
|
||
|
||
if __name__ == '__main__':
|
||
server.run()
|
||
'''
|
||
|
||
def _get_auth_headers_code(self, auth: Dict) -> str:
|
||
"""生成认证头代码"""
|
||
if not auth:
|
||
return "headers = {}"
|
||
|
||
auth_type = auth.get("type", "")
|
||
|
||
if auth_type == "api_key":
|
||
key = auth.get("key", "")
|
||
location = auth.get("in", "header")
|
||
name = auth.get("name", "X-API-Key")
|
||
|
||
if location == "header":
|
||
return f'headers = {{"{name}": os.getenv("TOOL_API_KEY", "{key}")}}'
|
||
else:
|
||
return "headers = {} # API Key 在查询参数中"
|
||
|
||
elif auth_type == "bearer":
|
||
key = auth.get("key", "")
|
||
return f'headers = {{"Authorization": f"Bearer {{os.getenv(\'TOOL_API_KEY\', \'{key}\')}}" }}'
|
||
|
||
elif auth_type == "basic":
|
||
username = auth.get("username", "")
|
||
password = auth.get("password", "")
|
||
return f'''import base64
|
||
_creds = base64.b64encode(f"{{os.getenv('TOOL_USERNAME', '{username}')}}:{{os.getenv('TOOL_PASSWORD', '{password}')}}".encode()).decode()
|
||
headers = {{"Authorization": f"Basic {{_creds}}"}}'''
|
||
|
||
return "headers = {}"
|
||
|
||
def _generate_system_prompt(
|
||
self,
|
||
agent_name: str,
|
||
description: str,
|
||
tools_config: List[Dict]
|
||
) -> str:
|
||
"""生成智能系统提示词"""
|
||
tools_desc = "\n".join([
|
||
f"- {t.get('name')}: {t.get('description', '')}"
|
||
for t in tools_config
|
||
])
|
||
|
||
return f'''你是 {agent_name},一个专业的 AI 智能助手。
|
||
|
||
## 角色定位
|
||
{description}
|
||
|
||
## 可用工具
|
||
{tools_desc}
|
||
|
||
## 工作原则
|
||
1. 理解用户意图:仔细分析用户的请求,确保准确理解需求
|
||
2. 选择合适工具:根据需求选择最合适的工具来完成任务
|
||
3. 清晰反馈:以用户友好的方式呈现结果
|
||
4. 错误处理:遇到问题时提供有用的错误信息和建议
|
||
|
||
## 响应格式
|
||
- 对于数据查询:返回结构化的 JSON 数据
|
||
- 对于操作请求:返回操作状态和结果
|
||
- 始终使用中文与用户交流(除非用户使用其他语言)'''
|
||
|
||
def generate_api_server(self, agent_name: str, description: str) -> str:
|
||
"""
|
||
生成 API 服务器代码 (api_server.py)
|
||
基于 _template/src/server/api_server.py 框架
|
||
"""
|
||
return f'''"""
|
||
HTTP API 服务器 - {agent_name}
|
||
|
||
{description}
|
||
提供 REST API 和 MCP HTTP/SSE 端点。
|
||
自动生成时间: {datetime.now().isoformat()}
|
||
"""
|
||
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 = "{agent_name}"
|
||
|
||
|
||
# ==================== FastAPI 应用 ====================
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
print(f"🚀 {{SERVER_NAME}} 启动")
|
||
yield
|
||
print(f"🛑 {{SERVER_NAME}} 关闭")
|
||
|
||
app = FastAPI(
|
||
title=SERVER_NAME,
|
||
description="{description}",
|
||
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())
|
||
}}
|
||
|
||
|
||
@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")
|
||
|
||
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 ToolCallRequest(BaseModel):
|
||
"""工具调用请求"""
|
||
tool_name: str = Field(..., description="工具名称")
|
||
parameters: Dict[str, Any] = Field(default={{}}, description="工具参数")
|
||
|
||
|
||
class ToolCallResponse(BaseModel):
|
||
"""工具调用响应"""
|
||
success: bool
|
||
result: Optional[Any] = None
|
||
error: Optional[str] = None
|
||
|
||
|
||
@app.get("/tools")
|
||
async def list_tools():
|
||
"""列出可用工具"""
|
||
return {{
|
||
"tools": [
|
||
{{"name": t["name"], "description": t["description"]}}
|
||
for t in 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}} 不存在")
|
||
|
||
try:
|
||
old_key = os.environ.get('OPENAI_API_KEY')
|
||
os.environ['OPENAI_API_KEY'] = api_key
|
||
|
||
try:
|
||
result = await TOOL_MAP[request.tool_name](**request.parameters)
|
||
return ToolCallResponse(success=True, result=json.loads(result) if isinstance(result, str) else result)
|
||
finally:
|
||
if old_key:
|
||
os.environ['OPENAI_API_KEY'] = old_key
|
||
except Exception as e:
|
||
return ToolCallResponse(success=False, error=str(e))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
import uvicorn
|
||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||
'''
|
||
|
||
def generate_run_api_server(self) -> str:
|
||
"""生成入口文件 run_api_server.py"""
|
||
return '''#!/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")
|
||
'''
|
||
|
||
def generate_dockerfile(self, agent_name: str) -> str:
|
||
"""生成 Dockerfile (基于 _template)"""
|
||
return f'''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"]
|
||
'''
|
||
|
||
def generate_requirements(self) -> str:
|
||
"""生成 requirements.txt"""
|
||
return '''# 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
|
||
httpx>=0.25.0
|
||
aiohttp>=3.9.0
|
||
|
||
# 其他
|
||
python-dotenv>=1.0.0
|
||
'''
|
||
|
||
def generate_gitea_action(self, agent_name: str, auto_deploy: bool = True) -> str:
|
||
"""
|
||
生成 Gitea Actions CI/CD 配置
|
||
参考: http://gitee.ath.cx:3000/xiaohei/cicd-AKS
|
||
使用预配置的密钥
|
||
"""
|
||
k8s_name = agent_name.lower().replace("_", "-").replace(" ", "-")
|
||
image_repo = f"{self.acr_namespace}/{k8s_name}"
|
||
|
||
deploy_step = ""
|
||
if auto_deploy:
|
||
deploy_step = f'''
|
||
- name: Login Azure
|
||
run: |
|
||
az login --service-principal \\
|
||
-u "$AZ_CLIENT_ID" \\
|
||
-p "$AZ_CLIENT_SECRET" \\
|
||
--tenant "$AZ_TENANT_ID"
|
||
az account set --subscription "$AZ_SUBSCRIPTION_ID"
|
||
|
||
- name: Get AKS Credentials
|
||
run: |
|
||
az aks get-credentials \\
|
||
--resource-group "$AZ_RG" \\
|
||
--name "$AZ_AKS" \\
|
||
--overwrite-existing
|
||
|
||
- name: Setup ACR Pull Secret
|
||
run: |
|
||
# 创建 Agent 专用命名空间
|
||
kubectl create namespace agent-{k8s_name} --dry-run=client -o yaml | kubectl apply -f -
|
||
|
||
# 创建或更新 ACR 拉取 Secret
|
||
kubectl create secret docker-registry acr-secret \\
|
||
--docker-server=$ACR_LOGIN_SERVER \\
|
||
--docker-username=$ACR_USERNAME \\
|
||
--docker-password=$ACR_PASSWORD \\
|
||
-n agent-{k8s_name} --dry-run=client -o yaml | kubectl apply -f -
|
||
|
||
- name: Deploy to AKS
|
||
run: |
|
||
# 删除旧的 deployment(如果存在)
|
||
kubectl delete deployment {k8s_name} -n agent-{k8s_name} --ignore-not-found=true
|
||
|
||
# 创建 deployment YAML
|
||
cat <<EOF | kubectl apply -f -
|
||
apiVersion: apps/v1
|
||
kind: Deployment
|
||
metadata:
|
||
name: {k8s_name}
|
||
namespace: agent-{k8s_name}
|
||
labels:
|
||
app: {k8s_name}
|
||
managed-by: gitea-cicd
|
||
spec:
|
||
replicas: 1
|
||
selector:
|
||
matchLabels:
|
||
app: {k8s_name}
|
||
template:
|
||
metadata:
|
||
labels:
|
||
app: {k8s_name}
|
||
spec:
|
||
imagePullSecrets:
|
||
- name: acr-secret
|
||
nodeSelector:
|
||
kubernetes.io/arch: arm64
|
||
tolerations:
|
||
- key: "kubernetes.io/arch"
|
||
operator: "Equal"
|
||
value: "arm64"
|
||
effect: "NoSchedule"
|
||
containers:
|
||
- name: {k8s_name}
|
||
image: $ACR_LOGIN_SERVER/{image_repo}:${{{{ github.sha }}}}
|
||
ports:
|
||
- containerPort: 8000
|
||
env:
|
||
- name: OPENAI_BASE_URL
|
||
value: "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1"
|
||
- name: MODEL_NAME
|
||
value: "taiji/gpt-4o-mini"
|
||
resources:
|
||
requests:
|
||
cpu: "100m"
|
||
memory: "128Mi"
|
||
limits:
|
||
cpu: "500m"
|
||
memory: "512Mi"
|
||
livenessProbe:
|
||
httpGet:
|
||
path: /health
|
||
port: 8000
|
||
initialDelaySeconds: 30
|
||
periodSeconds: 30
|
||
readinessProbe:
|
||
httpGet:
|
||
path: /health
|
||
port: 8000
|
||
initialDelaySeconds: 10
|
||
periodSeconds: 10
|
||
EOF
|
||
|
||
# 创建 Service
|
||
cat <<EOF | kubectl apply -f -
|
||
apiVersion: v1
|
||
kind: Service
|
||
metadata:
|
||
name: {k8s_name}-service
|
||
namespace: agent-{k8s_name}
|
||
spec:
|
||
type: LoadBalancer
|
||
ports:
|
||
- port: 80
|
||
targetPort: 8000
|
||
selector:
|
||
app: {k8s_name}
|
||
EOF
|
||
|
||
- name: Wait for Deployment
|
||
run: |
|
||
kubectl rollout status deployment/{k8s_name} -n agent-{k8s_name} --timeout=180s || {{
|
||
echo "=== Deployment 状态 ==="
|
||
kubectl get deployment {k8s_name} -n agent-{k8s_name} -o wide
|
||
echo "=== Pod 状态 ==="
|
||
kubectl get pods -l app={k8s_name} -n agent-{k8s_name} -o wide
|
||
echo "=== Pod 事件 ==="
|
||
kubectl describe pods -l app={k8s_name} -n agent-{k8s_name} | tail -30
|
||
exit 1
|
||
}}
|
||
|
||
echo "=== 部署成功 ==="
|
||
kubectl get svc {k8s_name}-service -n agent-{k8s_name}
|
||
|
||
# 等待 LoadBalancer IP
|
||
echo "等待 LoadBalancer IP..."
|
||
for i in {{1..30}}; do
|
||
EXTERNAL_IP=$(kubectl get svc {k8s_name}-service -n agent-{k8s_name} -o jsonpath='{{.status.loadBalancer.ingress[0].ip}}' 2>/dev/null)
|
||
if [ -n "$EXTERNAL_IP" ]; then
|
||
echo "✅ Agent LoadBalancer IP: $EXTERNAL_IP"
|
||
echo "EXTERNAL_IP=$EXTERNAL_IP" >> $GITHUB_ENV
|
||
break
|
||
fi
|
||
sleep 10
|
||
done
|
||
|
||
- name: Create DNS Record
|
||
if: env.EXTERNAL_IP != ''
|
||
run: |
|
||
echo "创建 DNS 记录: {k8s_name}.$AZURE_DNS_ZONE -> $EXTERNAL_IP"
|
||
|
||
# 获取 Azure 访问令牌
|
||
ACCESS_TOKEN=$(az account get-access-token --query accessToken -o tsv)
|
||
|
||
# 创建 DNS A 记录
|
||
DNS_URL="https://management.azure.com/subscriptions/$AZ_SUBSCRIPTION_ID/resourceGroups/$AZ_RG/providers/Microsoft.Network/dnsZones/$AZURE_DNS_ZONE/A/{k8s_name}?api-version=2018-05-01"
|
||
|
||
curl -X PUT "$DNS_URL" \\
|
||
-H "Authorization: Bearer $ACCESS_TOKEN" \\
|
||
-H "Content-Type: application/json" \\
|
||
-d '{{
|
||
"properties": {{
|
||
"TTL": 300,
|
||
"ARecords": [{{"ipv4Address": "'"$EXTERNAL_IP"'"}}]
|
||
}}
|
||
}}'
|
||
|
||
echo ""
|
||
echo "✅ DNS 记录创建成功!"
|
||
echo "🌐 Agent 访问地址: http://{k8s_name}.$AZURE_DNS_ZONE"
|
||
echo "🌐 Agent IP 地址: http://$EXTERNAL_IP"
|
||
'''
|
||
|
||
return f'''name: CI-CD-AKS-Agent
|
||
|
||
on:
|
||
push:
|
||
branches: ["main"]
|
||
workflow_dispatch:
|
||
|
||
jobs:
|
||
build_and_deploy:
|
||
runs-on: self-hosted
|
||
|
||
env:
|
||
ACR_LOGIN_SERVER: ${{{{ secrets.ACR_LOGIN_SERVER }}}}
|
||
ACR_USERNAME: ${{{{ secrets.ACR_USERNAME }}}}
|
||
ACR_PASSWORD: ${{{{ secrets.ACR_PASSWORD }}}}
|
||
AZ_CLIENT_ID: ${{{{ secrets.AZ_CLIENT_ID }}}}
|
||
AZ_CLIENT_SECRET: ${{{{ secrets.AZ_CLIENT_SECRET }}}}
|
||
AZ_TENANT_ID: ${{{{ secrets.AZ_TENANT_ID }}}}
|
||
AZ_SUBSCRIPTION_ID: ${{{{ secrets.AZ_SUBSCRIPTION_ID }}}}
|
||
AZ_RG: ${{{{ secrets.AZ_RG }}}}
|
||
AZ_AKS: ${{{{ secrets.AZ_AKS }}}}
|
||
AZURE_DNS_ZONE: ${{{{ secrets.AZURE_DNS_ZONE }}}}
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Install Azure CLI
|
||
run: |
|
||
if ! command -v az &> /dev/null; then
|
||
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
|
||
fi
|
||
|
||
- name: Install kubectl
|
||
run: |
|
||
if ! command -v kubectl &> /dev/null; then
|
||
ARCH=$(uname -m)
|
||
case $ARCH in
|
||
x86_64) ARCH="amd64" ;;
|
||
aarch64|arm64) ARCH="arm64" ;;
|
||
esac
|
||
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/${{ARCH}}/kubectl"
|
||
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
|
||
rm kubectl
|
||
fi
|
||
|
||
- name: Build Docker image (ARM64)
|
||
run: |
|
||
docker build -t $ACR_LOGIN_SERVER/{image_repo}:${{{{ github.sha }}}} .
|
||
docker tag $ACR_LOGIN_SERVER/{image_repo}:${{{{ github.sha }}}} $ACR_LOGIN_SERVER/{image_repo}:latest
|
||
|
||
- name: Login to ACR
|
||
run: |
|
||
echo "$ACR_PASSWORD" | docker login $ACR_LOGIN_SERVER -u "$ACR_USERNAME" --password-stdin
|
||
|
||
- name: Push Docker image
|
||
run: |
|
||
docker push $ACR_LOGIN_SERVER/{image_repo}:${{{{ github.sha }}}}
|
||
docker push $ACR_LOGIN_SERVER/{image_repo}:latest
|
||
|
||
echo "✅ 镜像已推送: $ACR_LOGIN_SERVER/{image_repo}:latest"
|
||
{deploy_step}
|
||
'''
|
||
|
||
def generate_readme(
|
||
self,
|
||
agent_name: str,
|
||
description: str,
|
||
tools: List[Dict],
|
||
auto_deploy: bool = True
|
||
) -> str:
|
||
"""生成 README.md"""
|
||
tools_doc = "\n".join([f"- **{t.get('name')}**: {t.get('description', '')}" for t in tools])
|
||
k8s_name = agent_name.lower().replace("_", "-").replace(" ", "-")
|
||
|
||
deploy_doc = ""
|
||
if auto_deploy:
|
||
deploy_doc = f'''
|
||
## 自动部署
|
||
|
||
CI/CD 会自动:
|
||
1. 构建 Docker 镜像 (ARM64)
|
||
2. 推送到 Azure Container Registry
|
||
3. 部署到 AKS 集群
|
||
4. 创建 DNS 记录
|
||
|
||
部署后可通过以下方式访问:
|
||
- 命名空间: `agent-{k8s_name}`
|
||
- 服务: `{k8s_name}-service`
|
||
- **公网域名**: `http://{k8s_name}.taijiagnet.com`
|
||
'''
|
||
|
||
return f'''# {agent_name}
|
||
|
||
{description}
|
||
|
||
## 自动生成信息
|
||
|
||
- 生成时间: {datetime.now().isoformat()}
|
||
- 生成方式: Agent Manager 动态生成
|
||
- 框架: agent_templates/agents/_template
|
||
- 参考: [cicd-AKS](http://gitee.ath.cx:3000/xiaohei/cicd-AKS)
|
||
|
||
## 包含工具
|
||
|
||
{tools_doc}
|
||
|
||
## API 端点
|
||
|
||
### REST API
|
||
- `GET /` - 服务信息
|
||
- `GET /health` - 健康检查
|
||
- `GET /tools` - 列出可用工具
|
||
- `POST /tools/call` - 调用工具
|
||
|
||
### MCP 端点
|
||
- `POST /mcp` - MCP HTTP 端点
|
||
- `GET /mcp/sse` - MCP SSE 端点
|
||
- `POST /mcp/sse` - MCP SSE POST 端点
|
||
|
||
## 调用示例
|
||
|
||
```bash
|
||
# 健康检查
|
||
curl http://localhost:8000/health
|
||
|
||
# 列出工具
|
||
curl http://localhost:8000/tools
|
||
|
||
# 调用工具
|
||
curl -X POST http://localhost:8000/tools/call \\
|
||
-H "Content-Type: application/json" \\
|
||
-H "api-key: your-api-key" \\
|
||
-d '{{"tool_name": "example_tool", "parameters": {{}}}}'
|
||
|
||
# MCP 调用
|
||
curl -X POST http://localhost:8000/mcp \\
|
||
-H "Content-Type: application/json" \\
|
||
-d '{{"jsonrpc": "2.0", "method": "tools/list", "id": 1}}'
|
||
```
|
||
|
||
## 环境变量
|
||
|
||
- `API_PORT`: 服务端口 (默认: 8000)
|
||
- `API_HOST`: 服务地址 (默认: 0.0.0.0)
|
||
- `OPENAI_API_KEY`: AI 模型 API Key
|
||
- `OPENAI_BASE_URL`: AI 模型 API 端点
|
||
- `MODEL_NAME`: AI 模型名称
|
||
- `TOOL_API_KEY`: 工具 API 密钥 (如需要)
|
||
|
||
## CI/CD 配置
|
||
|
||
本仓库配置了 Gitea Actions (self-hosted runner),推送到 main 分支后会自动:
|
||
1. 构建 Docker 镜像 (ARM64)
|
||
2. 推送到 Azure Container Registry
|
||
3. {"部署到 AKS 集群" if auto_deploy else "等待手动部署"}
|
||
|
||
### 必需的 Secrets
|
||
|
||
在 Gitee 仓库设置中配置以下 Secrets:
|
||
|
||
| Secret 名称 | 说明 | 值 |
|
||
|-------------|------|-----|
|
||
| `ACR_LOGIN_SERVER` | ACR 登录服务器 | `agnettaiji.azurecr.io` |
|
||
| `ACR_USERNAME` | ACR 用户名 | `agnettaiji` |
|
||
| `ACR_PASSWORD` | ACR 密码 | *(配置为仓库Secret)* |
|
||
| `AZ_CLIENT_ID` | Azure 服务主体 Client ID | `fb306798-2cfe-4ac9-ba48-eab7bc71bcfe` |
|
||
| `AZ_CLIENT_SECRET` | Azure 服务主体密钥 | *(配置为仓库Secret)* |
|
||
| `AZ_TENANT_ID` | Azure 租户 ID | `263c3ff6-1be5-4141-8308-b188464fb297` |
|
||
| `AZ_SUBSCRIPTION_ID` | Azure 订阅 ID | `c6c47e4c-f5f4-49f8-b26f-7728862c17d6` |
|
||
| `AZ_RG` | AKS 资源组名称 | `taiji-ai-pda` |
|
||
| `AZ_AKS` | AKS 集群名称 | `taiji-ai-pda` |
|
||
{deploy_doc}
|
||
|
||
## 许可证
|
||
|
||
MIT License
|
||
'''
|
||
|
||
def generate_full_project(
|
||
self,
|
||
agent_name: str,
|
||
description: str,
|
||
tools_config: List[Dict],
|
||
auto_deploy: bool = True
|
||
) -> Dict[str, str]:
|
||
"""
|
||
生成完整的 Agent 项目文件
|
||
基于 agent_templates/agents/_template 框架
|
||
|
||
Args:
|
||
agent_name: Agent 名称
|
||
description: Agent 描述
|
||
tools_config: 工具配置列表
|
||
auto_deploy: 是否自动部署到 AKS
|
||
|
||
Returns:
|
||
文件字典 {path: content}
|
||
"""
|
||
files = {}
|
||
|
||
k8s_name = agent_name.lower().replace("_", "-").replace(" ", "-")
|
||
|
||
# 生成 src/server/mcp_server.py
|
||
files["src/server/mcp_server.py"] = self.generate_mcp_server(
|
||
agent_name, description, tools_config
|
||
)
|
||
|
||
# 生成 src/server/api_server.py
|
||
files["src/server/api_server.py"] = self.generate_api_server(agent_name, description)
|
||
|
||
# 生成 src/server/__init__.py
|
||
files["src/server/__init__.py"] = '"""服务器模块"""\n'
|
||
|
||
# 生成 src/__init__.py
|
||
files["src/__init__.py"] = '"""源码模块"""\n'
|
||
|
||
# 生成入口文件
|
||
files["run_api_server.py"] = self.generate_run_api_server()
|
||
|
||
# 生成 Dockerfile
|
||
files["Dockerfile"] = self.generate_dockerfile(agent_name)
|
||
|
||
# 生成 requirements.txt
|
||
files["requirements.txt"] = self.generate_requirements()
|
||
|
||
# 生成 CI/CD 配置
|
||
files[".gitea/workflows/ci-cd.yaml"] = self.generate_gitea_action(k8s_name, auto_deploy)
|
||
|
||
# 生成 README
|
||
files["README.md"] = self.generate_readme(agent_name, description, tools_config, auto_deploy)
|
||
|
||
return files
|
||
|
||
def get_cicd_secrets_info(self) -> Dict[str, str]:
|
||
"""获取 CI/CD 密钥信息(用于配置仓库 Secrets)"""
|
||
return self.cicd_secrets.copy()
|
||
|
||
|
||
# 全局实例
|
||
agent_code_generator = AgentCodeGenerator()
|