""" 工具生成 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 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 }) # 生成完整项目文件(参考 http://gitee.ath.cx:3000/xiaohei/cicd-AKS) project_files = agent_code_generator.generate_full_project( agent_name=request.agent_name, 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/{repo_name}:latest", "expected_domain": f"{k8s_name}.taijiagnet.com", "expected_namespace": f"agent-{k8s_name}" } 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/{repo_name}:latest", "status": "building", "files_pushed": len(project_files), "tools_count": len(request.tools), "expected_domain": expected_domain, "expected_namespace": f"agent-{k8s_name}" }, "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) } }