Files
agent_management/agent_code_generator.py
zhanggangyong 090f4a04b3 fix: 更新 Azure Service Principal 凭据
- 更新 AZ_CLIENT_ID 为新的 SP
- 更新 AZ_CLIENT_SECRET
- 更新 AZ_SUBSCRIPTION_ID 为新订阅
2026-02-05 07:31:02 +00:00

1123 lines
36 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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()