Files
agent_management/tool_generator_api.py
T
zhanggangyong 33955b68dc fix: 统一 agent 默认端口为 8000(search_agent 系列保持 8080)
- 修改 template_manager.py、k8s_manager.py 中的端口映射
- 更新 jina_search_agent、azure_blob_agent 系列、a2a_litellm_agent 的代码和 Dockerfile 为 8000
- 添加端口修改脚本和测试脚本

Made-with: Cursor
2026-03-02 15:13:07 +00:00

659 lines
22 KiB
Python
Raw 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.
"""
工具生成 API 模块
独立的 FastAPI Router,用于动态生成 Agent
不影响现有的 Agent Manager 规范
"""
import os
import uuid
import logging
import requests
from datetime import datetime
from typing import Dict, List, Optional, Any
from fastapi import APIRouter, HTTPException, BackgroundTasks
from pydantic import BaseModel, Field
from k8s_manager import sanitize_k8s_name
from gitee_manager import gitee_manager
from agent_code_generator import agent_code_generator
logger = logging.getLogger(__name__)
# 创建独立的 Router
router = APIRouter(prefix="/tools", tags=["Dynamic Tool Generator"])
# 内存存储工具引用(生产环境应使用数据库)
TOOL_REFS: Dict[str, Dict] = {}
# ==================== 请求/响应模型 ====================
class AuthConfig(BaseModel):
"""认证配置"""
type: str = Field(..., description="认证类型: api_key, bearer, basic")
key: Optional[str] = Field(None, description="API Key 或 Bearer Token")
username: Optional[str] = Field(None, description="Basic Auth 用户名")
password: Optional[str] = Field(None, description="Basic Auth 密码")
in_location: Optional[str] = Field("header", alias="in", description="API Key 位置: header, query")
name: Optional[str] = Field("X-API-Key", description="API Key 参数名")
class RetryConfig(BaseModel):
"""重试配置"""
max_retries: int = Field(3, description="最大重试次数")
retry_delay: int = Field(1, description="重试间隔(秒)")
class GenerateToolRequest(BaseModel):
"""生成工具请求 - 符合接口规范"""
name: str = Field(..., min_length=1, max_length=100, description="工具名称")
description: str = Field(..., description="工具描述")
url: str = Field(..., description="API 端点 URL")
method: str = Field(..., description="HTTP 方法: GET/POST/PUT/DELETE/PATCH")
user_id: str = Field(..., description="用户 ID")
tenant_id: Optional[str] = Field(None, description="租户 ID")
headers: Optional[Dict[str, str]] = Field(None, description="自定义请求头")
auth: Optional[AuthConfig] = Field(None, description="认证配置")
request_params: Optional[Dict] = Field(None, description="URL 查询参数定义 (JSON Schema)")
request_body: Optional[Dict] = Field(None, description="请求体定义 (JSON Schema)")
response_mapping: Optional[Dict] = Field(None, description="响应字段映射")
timeout: int = Field(30, description="超时时间(秒)")
retry: Optional[RetryConfig] = Field(None, description="重试配置")
class GenerateAgentRequest(BaseModel):
"""生成完整 Agent 请求"""
agent_name: str = Field(..., min_length=1, max_length=63, description="Agent 名称")
description: str = Field(..., description="Agent 描述")
tools: List[GenerateToolRequest] = Field(..., description="工具配置列表")
user_id: str = Field(..., description="用户 ID")
tenant_id: Optional[str] = Field(None, description="租户 ID")
auto_deploy: bool = Field(False, description="是否自动部署(构建完成后)")
class UpdateToolRequest(BaseModel):
"""更新工具请求"""
description: Optional[str] = None
url: Optional[str] = None
method: Optional[str] = None
headers: Optional[Dict[str, str]] = None
auth: Optional[AuthConfig] = None
request_params: Optional[Dict] = None
request_body: Optional[Dict] = None
timeout: Optional[int] = None
class TestToolRequest(BaseModel):
"""测试工具请求"""
test_params: Optional[Dict] = Field(None, description="测试参数")
# ==================== API 端点 ====================
@router.post("/generate")
async def generate_tool(request: GenerateToolRequest):
"""
生成外部数据工具
1. 验证配置格式
2. 生成 Pydantic AI 工具代码
3. 返回 tool_ref_id
注意:此端点仅生成单个工具代码,不创建仓库
"""
try:
# 生成唯一 ID
tool_ref_id = f"tool-{request.name}-{uuid.uuid4().hex[:8]}"
# 转换请求为工具配置
tool_config = {
"name": request.name,
"description": request.description,
"url": request.url,
"method": request.method,
"auth": request.auth.dict() if request.auth else None,
"request_params": request.request_params,
"request_body": request.request_body,
"timeout": request.timeout
}
# 生成工具代码
tool_code = agent_code_generator.generate_tool_code(tool_config)
# 存储工具引用
TOOL_REFS[tool_ref_id] = {
"id": tool_ref_id,
"name": request.name,
"description": request.description,
"config": tool_config,
"code": tool_code,
"user_id": request.user_id,
"tenant_id": request.tenant_id,
"created_at": datetime.utcnow().isoformat(),
"status": "created"
}
logger.info(f"✅ 工具生成成功: {tool_ref_id}")
return {
"success": True,
"data": {
"tool_ref_id": tool_ref_id,
"name": request.name,
"description": request.description,
"created_at": TOOL_REFS[tool_ref_id]["created_at"]
},
"message": "工具生成成功"
}
except Exception as e:
logger.error(f"工具生成失败: {e}")
return {
"success": False,
"error": "generation_failed",
"message": str(e)
}
@router.post("/generate-agent")
async def generate_agent(request: GenerateAgentRequest, background_tasks: BackgroundTasks):
"""
生成完整 Agent 并推送到 Gitee
1. 生成所有工具代码
2. 生成 Agent 主文件
3. 生成 Dockerfile 和 CI/CD 配置
4. 创建 Gitee 仓库
5. 推送代码(触发 CI/CD 构建)
6. 返回状态查询信息
"""
try:
# 生成唯一的仓库名
repo_name = f"agent-{request.agent_name.lower().replace('_', '-')}-{uuid.uuid4().hex[:6]}"
agent_ref_id = f"agent-{repo_name}"
# 转换工具配置
tools_config = []
for tool in request.tools:
tools_config.append({
"name": tool.name,
"description": tool.description,
"url": tool.url,
"method": tool.method,
"auth": tool.auth.dict() if tool.auth else None,
"request_params": tool.request_params,
"request_body": tool.request_body,
"timeout": tool.timeout
})
# 生成完整项目文件(k8s_name 必须合规,否则 Service 创建会报 DNS-1035)
project_files = agent_code_generator.generate_full_project(
agent_name=k8s_name_for_cicd,
description=request.description,
tools_config=tools_config,
auto_deploy=request.auto_deploy
)
# 创建 Gitee 仓库
repo_result = gitee_manager.create_repository(
repo_name=repo_name,
description=f"{request.agent_name} - {request.description}",
private=False
)
if not repo_result.get("success"):
return {
"success": False,
"error": "repo_creation_failed",
"message": repo_result.get("error", "仓库创建失败")
}
# 获取仓库所有者
repo_owner = repo_result.get("owner", gitee_manager.gitee_username)
# 推送文件到仓库
push_result = gitee_manager.push_files(
repo_name=repo_name,
files=project_files,
commit_message=f"Initial commit: {request.agent_name}",
owner=repo_owner
)
logger.info(f"文件推送结果: {push_result}")
# 设置 CI/CD Secrets(包含 DNS 配置)
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": "UVU8Q~Hcrf5KeLi2RvUXB2rcuKFEjRCCrf_JrbwA",
"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"
}
secrets_result = gitee_manager.set_repo_secrets(
repo_name=repo_name,
secrets=cicd_secrets,
owner=repo_owner
)
logger.info(f"Secrets 设置结果: {secrets_result}")
# 计算 K8s 相关名称
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
# 存储 Agent 引用
TOOL_REFS[agent_ref_id] = {
"id": agent_ref_id,
"type": "agent",
"name": request.agent_name,
"owner": repo_owner,
"description": request.description,
"repo_name": repo_name,
"repo_url": repo_result.get("html_url"),
"clone_url": repo_result.get("clone_url"),
"tools": [t.name for t in request.tools],
"user_id": request.user_id,
"tenant_id": request.tenant_id,
"created_at": datetime.utcnow().isoformat(),
"status": "building",
"auto_deploy": request.auto_deploy,
"image_name": f"agnettaiji.azurecr.io/ai-agents/{k8s_name_for_cicd}:latest",
"expected_domain": f"{k8s_name_for_cicd}.taijiagnet.com",
"expected_namespace": f"agent-{k8s_name_for_cicd}"
}
logger.info(f"✅ Agent 项目创建成功: {repo_name}")
# 计算预期的 DNS 域名
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
expected_domain = f"{k8s_name}.taijiagnet.com"
return {
"success": True,
"data": {
"agent_ref_id": agent_ref_id,
"repo_name": repo_name,
"repo_url": repo_result.get("html_url"),
"image_name": f"agnettaiji.azurecr.io/ai-agents/{k8s_name_for_cicd}:latest",
"status": "building",
"files_pushed": len(project_files),
"tools_count": len(request.tools),
"expected_domain": expected_domain,
"expected_namespace": f"agent-{k8s_name_for_cicd}"
},
"message": f"Agent 项目已创建并推送到 Gitee,CI/CD 正在构建中。部署后访问: http://{expected_domain}"
}
except Exception as e:
logger.error(f"Agent 生成失败: {e}")
import traceback
logger.error(traceback.format_exc())
return {
"success": False,
"error": "generation_failed",
"message": str(e)
}
@router.get("/{tool_ref_id}")
async def get_tool(tool_ref_id: str):
"""获取工具信息"""
if tool_ref_id not in TOOL_REFS:
raise HTTPException(status_code=404, detail="工具不存在")
tool = TOOL_REFS[tool_ref_id]
return {
"success": True,
"data": {
"id": tool["id"],
"name": tool["name"],
"description": tool["description"],
"status": tool["status"],
"created_at": tool["created_at"],
"repo_url": tool.get("repo_url"),
"image_name": tool.get("image_name")
}
}
@router.put("/{tool_ref_id}")
async def update_tool(tool_ref_id: str, request: UpdateToolRequest):
"""更新工具配置"""
if tool_ref_id not in TOOL_REFS:
raise HTTPException(status_code=404, detail="工具不存在")
tool = TOOL_REFS[tool_ref_id]
# 更新配置
if request.description:
tool["description"] = request.description
tool["config"]["description"] = request.description
if request.url:
tool["config"]["url"] = request.url
if request.method:
tool["config"]["method"] = request.method
if request.auth:
tool["config"]["auth"] = request.auth.dict()
if request.request_params:
tool["config"]["request_params"] = request.request_params
if request.request_body:
tool["config"]["request_body"] = request.request_body
if request.timeout:
tool["config"]["timeout"] = request.timeout
# 重新生成代码
tool["code"] = agent_code_generator.generate_tool_code(tool["config"])
tool["updated_at"] = datetime.utcnow().isoformat()
return {
"success": True,
"data": {
"id": tool["id"],
"name": tool["name"],
"updated_at": tool["updated_at"]
},
"message": "工具更新成功"
}
@router.delete("/{tool_ref_id}")
async def delete_tool(tool_ref_id: str, delete_repo: bool = False):
"""删除工具"""
if tool_ref_id not in TOOL_REFS:
raise HTTPException(status_code=404, detail="工具不存在")
tool = TOOL_REFS[tool_ref_id]
# 如果需要删除仓库
if delete_repo and tool.get("repo_name"):
gitee_manager.delete_repository(tool["repo_name"])
del TOOL_REFS[tool_ref_id]
return {
"success": True,
"message": "工具删除成功"
}
@router.post("/{tool_ref_id}/test")
async def test_tool(tool_ref_id: str, request: TestToolRequest):
"""测试工具连接"""
if tool_ref_id not in TOOL_REFS:
raise HTTPException(status_code=404, detail="工具不存在")
tool = TOOL_REFS[tool_ref_id]
config = tool.get("config", {})
try:
# 构建请求
url = config.get("url", "")
method = config.get("method", "GET")
timeout = config.get("timeout", 30)
headers = {}
params = request.test_params or {}
# 处理认证
auth = config.get("auth", {})
if auth:
if auth.get("type") == "api_key":
if auth.get("in") == "header":
headers[auth.get("name", "X-API-Key")] = auth.get("key", "")
else:
params[auth.get("name", "apikey")] = auth.get("key", "")
elif auth.get("type") == "bearer":
headers["Authorization"] = f"Bearer {auth.get('key', '')}"
# 发送测试请求
response = requests.request(
method=method,
url=url,
headers=headers,
params=params,
timeout=timeout
)
return {
"success": True,
"data": {
"status_code": response.status_code,
"response_time_ms": response.elapsed.total_seconds() * 1000,
"content_type": response.headers.get("Content-Type"),
"response_preview": response.text[:500] if response.text else None
},
"message": "工具连接测试成功"
}
except requests.exceptions.Timeout:
return {
"success": False,
"error": "timeout",
"message": "连接超时"
}
except requests.exceptions.ConnectionError:
return {
"success": False,
"error": "connection_error",
"message": "无法连接到目标服务"
}
except Exception as e:
return {
"success": False,
"error": "test_failed",
"message": str(e)
}
@router.get("/{tool_ref_id}/build-status")
async def get_build_status(tool_ref_id: str):
"""
查询构建状态
1. 查询 Gitee Action 状态
2. 查询 ACR 镜像状态
"""
if tool_ref_id not in TOOL_REFS:
raise HTTPException(status_code=404, detail="工具/Agent 不存在")
tool = TOOL_REFS[tool_ref_id]
repo_name = tool.get("repo_name")
if not repo_name:
return {
"success": False,
"error": "no_repo",
"message": "此工具未关联 Gitee 仓库"
}
# 查询 Gitee Action 状态
action_status = gitee_manager.get_action_status(repo_name)
# 查询 ACR 镜像状态
image_name = tool.get("image_name", "")
acr_status = _check_acr_image(image_name)
# 更新状态
if acr_status.get("exists"):
tool["status"] = "ready"
elif action_status.get("conclusion") == "failure":
tool["status"] = "build_failed"
elif action_status.get("status") == "in_progress":
tool["status"] = "building"
return {
"success": True,
"data": {
"tool_ref_id": tool_ref_id,
"repo_name": repo_name,
"overall_status": tool["status"],
"gitee_action": {
"status": action_status.get("status"),
"conclusion": action_status.get("conclusion"),
"run_id": action_status.get("run_id"),
"html_url": action_status.get("html_url")
},
"acr_image": {
"image_name": image_name,
"exists": acr_status.get("exists", False),
"digest": acr_status.get("digest"),
"created_at": acr_status.get("created_at")
}
}
}
@router.post("/{tool_ref_id}/deploy")
async def deploy_agent(tool_ref_id: str):
"""
部署 Agent
前提条件:
1. ACR 中已存在镜像
2. 或者手动触发部署
"""
if tool_ref_id not in TOOL_REFS:
raise HTTPException(status_code=404, detail="Agent 不存在")
tool = TOOL_REFS[tool_ref_id]
if tool.get("type") != "agent":
return {
"success": False,
"error": "not_agent",
"message": "此引用不是 Agent 类型"
}
# 检查镜像是否存在
image_name = tool.get("image_name", "")
acr_status = _check_acr_image(image_name)
if not acr_status.get("exists"):
return {
"success": False,
"error": "image_not_found",
"message": f"镜像 {image_name} 尚未构建完成,请等待 CI/CD 完成或检查构建状态"
}
# 调用 Agent Manager 的部署接口
try:
from template_manager import template_manager
# 先创建模板
template_name = tool["repo_name"].replace("-", "_")
try:
template_manager.create_template(
name=template_name,
image=image_name,
display_name=tool["name"],
description=tool["description"],
port=8000,
agent_framework="api",
created_by="tool_generator"
)
except ValueError:
# 模板已存在,更新镜像
template_manager.update_template(
name=template_name,
image=image_name
)
tool["status"] = "deployed"
tool["template_name"] = template_name
return {
"success": True,
"data": {
"tool_ref_id": tool_ref_id,
"template_name": template_name,
"image_name": image_name,
"status": "deployed",
"message": f"模板 {template_name} 已创建,可通过 POST /agents 创建实例"
},
"message": "Agent 模板已创建,可以开始部署实例"
}
except Exception as e:
logger.error(f"部署失败: {e}")
return {
"success": False,
"error": "deployment_failed",
"message": str(e)
}
def _check_acr_image(image_name: str) -> Dict[str, Any]:
"""检查 ACR 镜像是否存在"""
try:
# 解析镜像名称
# agnettaiji.azurecr.io/ai-agents/repo-name:latest
parts = image_name.split("/")
if len(parts) < 3:
return {"exists": False, "error": "Invalid image name"}
registry = parts[0]
repo = "/".join(parts[1:]).split(":")[0]
tag = parts[-1].split(":")[-1] if ":" in parts[-1] else "latest"
# 使用 az acr 命令检查
import subprocess
result = subprocess.run(
["az", "acr", "repository", "show-tags",
"--name", registry.replace(".azurecr.io", ""),
"--repository", repo,
"--output", "json"],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
import json
tags = json.loads(result.stdout)
if tag in tags:
return {
"exists": True,
"tag": tag,
"all_tags": tags
}
return {"exists": False}
except Exception as e:
logger.debug(f"检查 ACR 镜像失败: {e}")
return {"exists": False, "error": str(e)}
# 获取所有工具/Agent 列表
@router.get("/")
async def list_tools(user_id: Optional[str] = None):
"""列出所有工具和 Agent"""
tools = list(TOOL_REFS.values())
if user_id:
tools = [t for t in tools if t.get("user_id") == user_id]
return {
"success": True,
"data": {
"tools": [
{
"id": t["id"],
"name": t["name"],
"type": t.get("type", "tool"),
"status": t["status"],
"created_at": t["created_at"]
}
for t in tools
],
"count": len(tools)
}
}