From 090f4a04b3200f214a2074585c16db4fea33ac3d Mon Sep 17 00:00:00 2001 From: zhanggangyong Date: Thu, 5 Feb 2026 07:31:02 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=20Azure=20Service=20P?= =?UTF-8?q?rincipal=20=E5=87=AD=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 AZ_CLIENT_ID 为新的 SP - 更新 AZ_CLIENT_SECRET - 更新 AZ_SUBSCRIPTION_ID 为新订阅 --- Dockerfile | 5 - agent_code_generator.py | 1277 +++------------------------ app.py | 41 - docs/DYNAMIC_AGENT_GENERATOR_API.md | 627 ++++++++----- gitee_manager.py | 387 +------- k8s/agent-manager-configmap.yaml | 11 + k8s/agent-manager-deployment.yaml | 11 + k8s/agent-manager-secret.yaml | 4 + scripts/deploy-to-k8s-arm64.sh | 19 +- 9 files changed, 649 insertions(+), 1733 deletions(-) diff --git a/Dockerfile b/Dockerfile index 91c52ad..fc5c713 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,13 +16,8 @@ COPY template_manager.py . COPY gitee_manager.py . COPY agent_code_generator.py . COPY tool_generator_api.py . -COPY external_tool_api.py . -COPY tool_storage.py . COPY agent_manager/ ./agent_manager/ -# 创建工具存储目录 -RUN mkdir -p /app/tool_storage - # 安装依赖 RUN pip install --no-cache-dir -r requirements.txt diff --git a/agent_code_generator.py b/agent_code_generator.py index 8f9e03c..53f8440 100644 --- a/agent_code_generator.py +++ b/agent_code_generator.py @@ -37,10 +37,10 @@ class AgentCodeGenerator: "ACR_LOGIN_SERVER": "agnettaiji.azurecr.io", "ACR_USERNAME": "agnettaiji", "ACR_PASSWORD": "hDpX5t34N5ZmnKdtqyjYL5co/SnXJrmD20CRpGpWaG+ACRCw2wGM", - "AZ_CLIENT_ID": "fb306798-2cfe-4ac9-ba48-eab7bc71bcfe", - "AZ_CLIENT_SECRET": "cVK8Q~xlfBwm2_t2TC24yrTukWV4F3G~eIjBBa0D", + "AZ_CLIENT_ID": "f2dd1cb2-02f6-4efb-bc72-d148f6e01545", + "AZ_CLIENT_SECRET": "iym8Q~n5iHfqTTXofnKN8QSomhrUXmXXOBH9ubgq", "AZ_TENANT_ID": "263c3ff6-1be5-4141-8308-b188464fb297", - "AZ_SUBSCRIPTION_ID": "c6c47e4c-f5f4-49f8-b26f-7728862c17d6", + "AZ_SUBSCRIPTION_ID": "45d7a360-af09-40fc-9afc-56dc475245ec", "AZ_RG": "taiji-ai-pda", "AZ_AKS": "taiji-ai-pda", "AZURE_DNS_ZONE": "taijiagnet.com" @@ -101,263 +101,6 @@ class AgentCodeGenerator: } return type_map.get(json_type, "Any") - async def generate_tool_code_with_ai(self, tool_config: dict, api_key: str = None) -> str: - """ - 使用 AI 理解工具定义并生成代码(更智能,支持复杂场景如 URL 拼接) - - Args: - tool_config: 工具配置 - api_key: LLM API Key - - Returns: - AI 生成的 Python 代码 - """ - import httpx - - # 使用实例配置或环境变量(默认使用 claude-sonnet 生成高质量代码) - llm_url = self.llm_base_url - llm_model = self.llm_model # 默认: taiji/claude-sonnet-4-5 - llm_key = api_key or self.llm_api_key - - if not llm_key: - # 如果没有 API Key,回退到模板生成 - logger.warning("未提供 LLM API Key,使用模板生成") - return self.generate_tool_code(tool_config) - - name = tool_config.get("name", "custom_tool") - func_name = self._convert_name_to_python(name) - - # 构建 prompt - prompt = f"""你是一个 Python 代码生成专家。根据以下工具定义,生成一个异步 Python 函数。 - -## 工具定义 -- 名称: {tool_config.get("name")} -- 描述: {tool_config.get("description")} -- API URL: {tool_config.get("url")} -- HTTP 方法: {tool_config.get("method", "GET")} -- 认证方式: {json.dumps(tool_config.get("auth", {}), ensure_ascii=False)} -- 请求参数: {json.dumps(tool_config.get("request_params", tool_config.get("input_schema", {})), ensure_ascii=False)} -- 请求体: {json.dumps(tool_config.get("request_body", {}), ensure_ascii=False)} - -## 重要提示 -1. 如果 URL 像 `https://r.jina.ai/` 这样需要将参数拼接到路径中(如 `https://r.jina.ai/{{target_url}}`),请正确处理 URL 拼接 -2. **认证方式处理**: - - 如果认证类型是 `bearer`,必须使用 `Authorization: Bearer ` 格式的 header(不是 X-API-Key!) - - 从环境变量 `TOOL_API_KEY` 获取 token:`os.getenv("TOOL_API_KEY")` - - 示例:`headers = {{"Authorization": f"Bearer {{os.getenv('TOOL_API_KEY')}}"}}` -3. 函数必须是 async def,返回 JSON 字符串 -4. 使用 httpx 作为 HTTP 客户端 -5. 包含完整的错误处理 - -## 输出格式 -只输出 Python 代码,不要其他解释。代码格式如下: - -```python -\"\"\" -工具: {{name}} -描述: {{description}} -\"\"\" -import os -import json -from typing import Optional, Any -import httpx - -async def {func_name}(...) -> str: - ... -```""" - - try: - async with httpx.AsyncClient(timeout=60.0) as client: - response = await client.post( - f"{llm_url}/chat/completions", - headers={ - "Authorization": f"Bearer {llm_key}", - "Content-Type": "application/json" - }, - json={ - "model": llm_model, - "messages": [ - {"role": "system", "content": "你是一个专业的 Python 代码生成器,只输出代码,不要解释。"}, - {"role": "user", "content": prompt} - ], - "temperature": 0.2, - "max_tokens": 2000 - } - ) - response.raise_for_status() - - content = response.json()["choices"][0]["message"]["content"] - - # 提取代码块 - if "```python" in content: - code = content.split("```python")[1].split("```")[0].strip() - elif "```" in content: - code = content.split("```")[1].split("```")[0].strip() - else: - code = content.strip() - - logger.info(f"✅ AI 成功生成工具代码: {name}") - return code - - except Exception as e: - logger.error(f"AI 生成代码失败: {e},回退到模板生成") - return self.generate_tool_code(tool_config) - - def generate_tool_code(self, tool_config: dict) -> str: - """ - 根据工具配置生成 Pydantic 工具代码(模板方式) - - Args: - tool_config: 包含 name, url, method, auth, request_params/input_schema 等 - - Returns: - 生成的 Python 代码字符串 - """ - name = tool_config.get("name", "custom_tool") - func_name = self._convert_name_to_python(name) - desc = tool_config.get("description", "") - url = tool_config.get("url", "") - method = tool_config.get("method", "GET").upper() - auth = tool_config.get("auth", {}) - # 兼容 request_params 和 input_schema 两种字段名 - request_params = tool_config.get("request_params") or tool_config.get("input_schema") or {} - request_body = tool_config.get("request_body", {}) - timeout = tool_config.get("timeout", 30) - - # 构建参数 - params = [] - params_doc = [] - required_params = [] - - # 支持两种格式: - # 1. {"properties": {"symbol": {...}}, "required": ["symbol"]} - # 2. {"symbol": {...}} (直接参数格式) - param_props = request_params - if request_params and request_params.get("properties"): - param_props = request_params["properties"] - required_params = request_params.get("required", []) - 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", "") - # 检查是否在 required 列表中 - is_required = p_name in required_params or p_info.get("required", False) - default = p_info.get("default") - - if is_required: - params.append(f"{p_name}: {p_type}") - 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}") - - 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_items = [] - for p_name in param_props.keys(): - if isinstance(param_props[p_name], dict): - params_dict_items.append(f'"{p_name}": {p_name}') - if params_dict_items: - params_dict_code = "params = {" + ", ".join(params_dict_items) + "}" - else: - params_dict_code = "params = {}" - 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", "")' - - # 生成 URL 代码(使用 api_url 避免与参数名冲突) - url_code = f'api_url = "{url}"' - - # 根据 HTTP 方法决定参数传递方式 - # POST/PUT/PATCH: 参数放到请求体 (json) - # GET/DELETE: 参数放到查询参数 (params) - if method in ["POST", "PUT", "PATCH"]: - # POST 请求:参数作为 JSON 请求体 - request_code = f'''async with httpx.AsyncClient(timeout={timeout}) as client: - response = await client.request( - method="{method}", - url=api_url, - headers=headers, - json={{k: v for k, v in params.items() if v is not None}} - )''' - else: - # GET 请求:参数作为查询参数 - request_code = f'''async with httpx.AsyncClient(timeout={timeout}) as client: - response = await client.request( - method="{method}", - url=api_url, - headers=headers, - params={{k: v for k, v in params.items() if v is not None}} - )''' - - # 生成函数代码 - code = f'''""" -工具: {name} -描述: {desc} -自动生成时间: {datetime.now().isoformat()} -""" -import os -import json -from typing import Optional, Any -import httpx - - -async def {func_name}({params_str}) -> str: - """ - {desc} - - Args: -{params_doc_str} - - Returns: - API 响应结果 (JSON 格式) - """ - {url_code} - {auth_headers} - {params_dict_code} - - try: - {request_code} - - 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: - return json.dumps({{ - "success": False, - "error": str(e) - }}, ensure_ascii=False) -''' - return code - def generate_mcp_server( self, agent_name: str, @@ -382,15 +125,21 @@ async def {func_name}({params_str}) -> str: auth = tool.get("auth", {}) request_params = tool.get("request_params", {}) timeout = tool.get("timeout", 30) - generated_code = tool.get("generated_code", "") - # 构建参数信息(用于 TOOL_LIST) + # 构建参数 + 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 = {} @@ -399,48 +148,93 @@ async def {func_name}({params_str}) -> str: 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} - # 如果有 AI 生成的代码,使用它;否则使用模板 - if generated_code: - # 从 AI 生成的代码中提取函数并添加 @server.tool() 装饰器 - import re - # 移除开头的 docstring 和 import 语句 - code_lines = generated_code.split('\n') - func_start = -1 - for i, line in enumerate(code_lines): - if line.strip().startswith('async def '): - func_start = i - break - - if func_start >= 0: - # 提取函数代码 - func_code_lines = code_lines[func_start:] - func_body = '\n'.join(func_code_lines) - - # 添加 @server.tool() 装饰器 - func_code = f''' -@server.tool() -{func_body} -''' - tool_functions.append(func_code) - else: - # 无法解析,使用原始代码 - logger.warning(f"无法解析 AI 生成的代码: {name}") - func_code = self._generate_fallback_tool_code( - func_name, desc, url, method, auth, param_props, timeout - ) - tool_functions.append(func_code) + 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: - # 使用模板生成代码 - func_code = self._generate_fallback_tool_code( - func_name, desc, url, method, auth, param_props, timeout - ) - tool_functions.append(func_code) + 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''' {{ @@ -471,7 +265,6 @@ import json import os from typing import Optional -import httpx from mcp.server.fastmcp import FastMCP from pydantic_ai import Agent @@ -553,106 +346,6 @@ if __name__ == '__main__': return "headers = {}" - def _generate_fallback_tool_code( - self, - func_name: str, - desc: str, - url: str, - method: str, - auth: Dict, - param_props: Dict, - timeout: int - ) -> str: - """生成后备工具代码(当没有 AI 生成代码时使用)""" - # 构建参数 - params = [] - params_doc = [] - - 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}") - 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}") - - 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", "")' - - return f''' -@server.tool() -async def {func_name}({params_str}) -> str: - """ - {desc} - - Args: -{params_doc_str} - - Returns: - API 响应结果 (JSON 格式) - """ - import httpx - - api_url = "{url}" - {auth_headers} - {params_dict_code} - - try: - async with httpx.AsyncClient(timeout={timeout}) as client: - response = await client.request( - method="{method}", - url=api_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: - return json.dumps({{ - "success": False, - "error": str(e) - }}, ensure_ascii=False, indent=2) -''' - def _generate_system_prompt( self, agent_name: str, @@ -684,40 +377,23 @@ async def {func_name}({params_str}) -> str: - 对于操作请求:返回操作状态和结果 - 始终使用中文与用户交流(除非用户使用其他语言)''' - def generate_api_server(self, agent_name: str, description: str, tools_config: List[Dict] = None) -> str: + def generate_api_server(self, agent_name: str, description: str) -> str: """ 生成 API 服务器代码 (api_server.py) 基于 _template/src/server/api_server.py 框架 - 集成回调功能用于计费 - - Args: - agent_name: Agent 名称 - description: Agent 描述 - tools_config: 工具配置列表(用于生成工具名称列表) """ - # 生成工具名称列表(用于回调) - tool_names = [] - if tools_config: - for tool in tools_config: - tool_names.append(tool.get("name", "unknown_tool")) - - tool_names_str = json.dumps(tool_names, ensure_ascii=False) - return f'''""" HTTP API 服务器 - {agent_name} {description} 提供 REST API 和 MCP HTTP/SSE 端点。 -集成回调功能用于计费。 自动生成时间: {datetime.now().isoformat()} """ import json import uuid import os -import logging -from typing import Optional, Dict, Any, AsyncGenerator, List +from typing import Optional, Dict, Any, AsyncGenerator from contextlib import asynccontextmanager -from datetime import datetime from fastapi import FastAPI, HTTPException, Request, Header, Depends from fastapi.middleware.cors import CORSMiddleware @@ -725,47 +401,19 @@ 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 = "{agent_name}" -POD_NAME = os.getenv("POD_NAME", "{agent_name}") -USER_ID = os.getenv("USER_ID", "") - -# 配置日志 -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# 本 Agent 包含的工具列表 -AGENT_TOOLS = {tool_names_str} - -# ==================== 回调处理器 ==================== - -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 '未设置'}}") + print(f"🚀 {{SERVER_NAME}} 启动") yield - logger.info(f"🛑 {{SERVER_NAME}} 关闭") + print(f"🛑 {{SERVER_NAME}} 关闭") app = FastAPI( title=SERVER_NAME, @@ -819,15 +467,13 @@ async def root(): return {{ "service": SERVER_NAME, "status": "running", - "tools": list(TOOL_MAP.keys()), - "tools_count": len(TOOL_MAP), - "pod_name": POD_NAME + "tools": list(TOOL_MAP.keys()) }} @app.get("/health") async def health(): - return {{"status": "healthy", "service": SERVER_NAME, "tools_count": len(TOOL_MAP)}} + return {{"status": "healthy", "service": SERVER_NAME}} # ==================== MCP 端点 ==================== @@ -835,8 +481,8 @@ async def health(): 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 请求(带回调)""" +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") @@ -869,21 +515,8 @@ async def handle_mcp_request(data: Dict, session_id: str = None, api_key: str = 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) + result = await TOOL_MAP[tool_name](**args) finally: if old_key: os.environ['OPENAI_API_KEY'] = old_key @@ -910,8 +543,7 @@ async def mcp_endpoint(request: Request): 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) + 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)}}}}) @@ -940,10 +572,9 @@ async def mcp_sse_post(request: Request): 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) + 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", @@ -958,13 +589,6 @@ 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): @@ -972,15 +596,6 @@ 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") @@ -990,359 +605,28 @@ async def list_tools(): "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 - ) + 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: - 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": "对话历史已清除"}} + return ToolCallResponse(success=False, error=str(e)) if __name__ == '__main__': @@ -1372,20 +656,32 @@ if __name__ == '__main__': ''' def generate_dockerfile(self, agent_name: str) -> str: - """生成 Dockerfile (简化版,避免 WAF 拦截)""" - # 使用简化的 Dockerfile,避免触发 WAF - return '''FROM python:3.11-slim + """生成 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 . . -ENV PORT=8000 +# 暴露端口 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"] ''' @@ -1405,268 +701,20 @@ uvicorn[standard]>=0.27.0 # HTTP Client httpx>=0.25.0 aiohttp>=3.9.0 -requests>=2.31.0 # 其他 python-dotenv>=1.0.0 ''' - def generate_callback_utils(self) -> str: - """生成回调工具代码 (用于计费)""" - return '''""" -Agent回调工具 - 用于向Agent Manager回调运行时长记录(计费) -""" -import os -import time -import logging -import requests -from typing import Optional, List -from datetime import datetime, timezone - -logger = logging.getLogger(__name__) - - -class AgentCallbackHandler: - """Agent回调处理器""" - - def __init__( - self, - agent_name: Optional[str] = None, - user_id: Optional[str] = None, - callback_url: Optional[str] = None - ): - """ - 初始化回调处理器 - - Args: - agent_name: Agent名称,默认从环境变量 POD_NAME 获取 - user_id: 用户ID,默认从环境变量 USER_ID 获取 - callback_url: 回调URL,默认从环境变量 AGENT_CALLBACK_URL 获取 - """ - self.agent_name = agent_name or os.getenv("POD_NAME", "unknown-agent") - self.user_id = user_id or os.getenv("USER_ID", "") - self.callback_url = callback_url or os.getenv( - "AGENT_CALLBACK_URL", - "http://mcp-server.taiji-ai.svc.cluster.local:8000/api/v1/billing/agent-callback" - ) - - self.start_time: Optional[datetime] = None - self.tools_used: List[str] = [] - self.request_id: Optional[str] = None - - logger.info(f"AgentCallbackHandler initialized: agent={self.agent_name}, callback_url={self.callback_url}") - - def start_request(self, request_id: Optional[str] = None, user_id: Optional[str] = None): - """ - 开始一次请求处理 - - Args: - request_id: 请求ID - user_id: 用户ID(如果提供则覆盖默认值) - """ - self.start_time = datetime.now(timezone.utc) - self.tools_used = [] - self.request_id = request_id or f"req-{int(time.time())}" - - if user_id: - self.user_id = user_id - - logger.info(f"Request started: request_id={self.request_id}, user_id={self.user_id}") - - def add_tool_used(self, tool_name: str): - """ - 记录使用的工具 - - Args: - tool_name: 工具名称 - """ - if tool_name not in self.tools_used: - self.tools_used.append(tool_name) - logger.debug(f"Tool used: {tool_name}") - - def end_request(self, tools_used: Optional[List[str]] = None) -> bool: - """ - 结束请求并发送回调 - - Args: - tools_used: 使用的工具列表(可选,如果提供则覆盖内部记录) - - Returns: - 是否成功发送回调 - """ - if not self.start_time: - logger.warning("Cannot end request: no start time recorded") - return False - - if not self.user_id: - logger.warning("Cannot send callback: user_id not set") - return False - - end_time = datetime.now(timezone.utc) - running_time = (end_time - self.start_time).total_seconds() - - # 使用提供的工具列表或内部记录 - final_tools_used = tools_used if tools_used is not None else self.tools_used - - # 发送回调 - success = self._send_callback( - running_time_seconds=int(running_time), - start_time=self.start_time, - end_time=end_time, - tools_used=final_tools_used - ) - - # 重置状态 - self.start_time = None - self.tools_used = [] - self.request_id = None - - return success - - def _send_callback( - self, - running_time_seconds: int, - start_time: datetime, - end_time: datetime, - tools_used: List[str] - ) -> bool: - """ - 发送回调到Agent Manager - - Args: - running_time_seconds: 运行时长(秒) - start_time: 开始时间 - end_time: 结束时间 - tools_used: 使用的工具列表 - - Returns: - 是否成功发送 - """ - try: - payload = { - "agentName": self.agent_name, - "userId": self.user_id, - "podRunningTimeSeconds": running_time_seconds, - "toolsUsed": tools_used, - "startTime": start_time.isoformat(), - "endTime": end_time.isoformat(), - "requestId": self.request_id - } - - logger.info(f"Sending callback: {payload}") - - response = requests.post( - self.callback_url, - json=payload, - timeout=5 - ) - - if response.status_code == 200: - logger.info(f"Callback sent successfully: {response.json()}") - return True - else: - logger.error(f"Callback failed with status {response.status_code}: {response.text}") - return False - - except requests.exceptions.RequestException as e: - logger.error(f"Failed to send callback: {str(e)}") - return False - except Exception as e: - logger.error(f"Unexpected error sending callback: {str(e)}") - return False - - -class CallbackContextManager: - """回调上下文管理器 - 使用with语句自动处理开始和结束""" - - def __init__( - self, - handler: AgentCallbackHandler, - request_id: Optional[str] = None, - user_id: Optional[str] = None, - tools_used: Optional[List[str]] = None - ): - """ - 初始化上下文管理器 - - Args: - handler: AgentCallbackHandler实例 - request_id: 请求ID - user_id: 用户ID - tools_used: 使用的工具列表(可选) - """ - self.handler = handler - self.request_id = request_id - self.user_id = user_id - self.tools_used = tools_used or [] - - def __enter__(self): - """进入上下文时开始计时""" - self.handler.start_request( - request_id=self.request_id, - user_id=self.user_id - ) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """退出上下文时发送回调""" - self.handler.end_request(tools_used=self.tools_used) - return False # 不抑制异常 - - def add_tool(self, tool_name: str): - """添加使用的工具""" - self.handler.add_tool_used(tool_name) - if tool_name not in self.tools_used: - self.tools_used.append(tool_name) -''' - - def generate_gitea_action( - self, - agent_name: str, - auto_deploy: bool = True, - cpu_request: str = "100m", - cpu_limit: str = "500m", - memory_request: str = "128Mi", - memory_limit: str = "512Mi", - replicas: int = 1, - tool_api_keys: List[str] = None - ) -> str: + 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 使用预配置的密钥 - - Args: - agent_name: Agent 名称 - auto_deploy: 是否自动部署 - cpu_request: CPU 请求 (如 100m, 500m) - cpu_limit: CPU 限制 (如 500m, 1000m) - memory_request: 内存请求 (如 128Mi, 256Mi) - memory_limit: 内存限制 (如 512Mi, 1Gi) - replicas: 副本数量 - tool_api_keys: 工具 API 密钥列表(将注入到容器环境变量) """ k8s_name = agent_name.lower().replace("_", "-").replace(" ", "-") image_repo = f"{self.acr_namespace}/{k8s_name}" - # 生成工具 API Key 环境变量配置 - tool_api_key_env = "" - if tool_api_keys: - # 如果只有一个 key,使用 TOOL_API_KEY - if len(tool_api_keys) == 1: - tool_api_key_env = f''' - name: TOOL_API_KEY - value: "{tool_api_keys[0]}"''' - else: - # 多个 key 时,使用编号 - env_lines = [] - for i, key in enumerate(tool_api_keys): - env_lines.append(f''' - name: TOOL_API_KEY_{i} - value: "{key}"''') - # 第一个 key 也设置为默认的 TOOL_API_KEY - env_lines.insert(0, f''' - name: TOOL_API_KEY - value: "{tool_api_keys[0]}"''') - tool_api_key_env = "\n".join(env_lines) - deploy_step = "" if auto_deploy: deploy_step = f''' @@ -1713,7 +761,7 @@ class CallbackContextManager: app: {k8s_name} managed-by: gitea-cicd spec: - replicas: {replicas} + replicas: 1 selector: matchLabels: app: {k8s_name} @@ -1741,14 +789,13 @@ class CallbackContextManager: value: "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1" - name: MODEL_NAME value: "taiji/gpt-4o-mini" -{tool_api_key_env} resources: requests: - cpu: "{cpu_request}" - memory: "{memory_request}" + cpu: "100m" + memory: "128Mi" limits: - cpu: "{cpu_limit}" - memory: "{memory_limit}" + cpu: "500m" + memory: "512Mi" livenessProbe: httpGet: path: /health @@ -1814,9 +861,8 @@ class CallbackContextManager: # 获取 Azure 访问令牌 ACCESS_TOKEN=$(az account get-access-token --query accessToken -o tsv) - # 创建 DNS A 记录 (DNS Zone 在单独的资源组中) - DNS_RG="${{AZURE_DNS_RG:-taiji-Ai-v0}}" - DNS_URL="https://management.azure.com/subscriptions/$AZ_SUBSCRIPTION_ID/resourceGroups/$DNS_RG/providers/Microsoft.Network/dnsZones/$AZURE_DNS_ZONE/A/{k8s_name}?api-version=2018-05-01" + # 创建 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" \\ @@ -1856,7 +902,6 @@ jobs: AZ_RG: ${{{{ secrets.AZ_RG }}}} AZ_AKS: ${{{{ secrets.AZ_AKS }}}} AZURE_DNS_ZONE: ${{{{ secrets.AZURE_DNS_ZONE }}}} - AZURE_DNS_RG: ${{{{ secrets.AZURE_DNS_RG }}}} steps: - uses: actions/checkout@v4 @@ -1885,16 +930,9 @@ jobs: 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: Debug Secrets - run: | - echo "ACR_LOGIN_SERVER=$ACR_LOGIN_SERVER" - echo "ACR_USERNAME=$ACR_USERNAME" - echo "ACR_PASSWORD length=$(echo -n "$ACR_PASSWORD" | wc -c)" - if [ -z "$ACR_PASSWORD" ]; then echo "ERROR: ACR_PASSWORD is empty!"; fi - - name: Login to ACR run: | - echo "$ACR_PASSWORD" | docker login "$ACR_LOGIN_SERVER" -u "$ACR_USERNAME" --password-stdin + echo "$ACR_PASSWORD" | docker login $ACR_LOGIN_SERVER -u "$ACR_USERNAME" --password-stdin - name: Push Docker image run: | @@ -2025,28 +1063,17 @@ MIT License agent_name: str, description: str, tools_config: List[Dict], - auto_deploy: bool = True, - cpu_request: str = "100m", - cpu_limit: str = "500m", - memory_request: str = "128Mi", - memory_limit: str = "512Mi", - replicas: int = 1 + auto_deploy: bool = True ) -> Dict[str, str]: """ 生成完整的 Agent 项目文件 基于 agent_templates/agents/_template 框架 - 包含回调功能用于计费 Args: agent_name: Agent 名称 description: Agent 描述 - tools_config: 工具配置列表(支持多个工具) + tools_config: 工具配置列表 auto_deploy: 是否自动部署到 AKS - cpu_request: CPU 请求 (如 100m, 500m) - cpu_limit: CPU 限制 (如 500m, 1000m) - memory_request: 内存请求 (如 128Mi, 256Mi) - memory_limit: 内存限制 (如 512Mi, 1Gi) - replicas: 副本数量 Returns: 文件字典 {path: content} @@ -2060,13 +1087,8 @@ MIT License agent_name, description, tools_config ) - # 生成 src/server/api_server.py(传递 tools_config 用于回调) - files["src/server/api_server.py"] = self.generate_api_server( - agent_name, description, tools_config - ) - - # 生成 src/server/agent_callback_utils.py(回调工具) - files["src/server/agent_callback_utils.py"] = self.generate_callback_utils() + # 生成 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' @@ -2083,37 +1105,12 @@ MIT License # 生成 requirements.txt files["requirements.txt"] = self.generate_requirements() - # 从工具配置中提取 API Keys - tool_api_keys = [] - for tool in tools_config: - auth = tool.get("auth") - if auth: - # 兼容 token 和 key 字段 - api_key = auth.get("token") or auth.get("key") - if api_key: - tool_api_keys.append(api_key) - # 生成 CI/CD 配置 - files[".gitea/workflows/ci-cd.yaml"] = self.generate_gitea_action( - agent_name=k8s_name, - auto_deploy=auto_deploy, - cpu_request=cpu_request, - cpu_limit=cpu_limit, - memory_request=memory_request, - memory_limit=memory_limit, - replicas=replicas, - tool_api_keys=tool_api_keys if tool_api_keys else None - ) + 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) - logger.info(f"生成 Agent 项目: {agent_name}") - logger.info(f" 包含工具: {len(tools_config)} 个") - logger.info(f" 工具列表: {[t.get('name') for t in tools_config]}") - logger.info(f" 自动部署: {auto_deploy}") - logger.info(f" 包含回调功能: 是") - return files def get_cicd_secrets_info(self) -> Dict[str, str]: diff --git a/app.py b/app.py index 5860da6..a699668 100644 --- a/app.py +++ b/app.py @@ -16,8 +16,6 @@ from database import ( ) from template_manager import template_manager from tool_generator_api import router as tool_generator_router -from external_tool_api import router as external_tool_router -from tool_storage import tool_storage import os # 配置日志 @@ -34,9 +32,6 @@ app = FastAPI( # 注册动态工具生成 Router app.include_router(tool_generator_router) -# 注册外部工具 API Router(符合 MCP-Server 规范) -app.include_router(external_tool_router) - # 初始化K8s管理器 NAMESPACE = os.getenv("NAMESPACE", "ai-agents") KUBECONFIG_PATH = os.getenv("KUBECONFIG_PATH", None) # 可选:指定kubeconfig路径 @@ -212,7 +207,6 @@ class CreateAgentRequest(BaseModel): config: Dict = Field(default_factory=dict, description="配置信息") env: Optional[Dict[str, str]] = Field(default_factory=dict, description="环境变量") namespace: Optional[str] = Field(default=None, description="Kubernetes命名空间,默认使用环境变量NAMESPACE的值") - tool_refs: Optional[List[str]] = Field(default=None, description="外部数据工具标识列表(符合MCP-Server规范)") class AgentResponse(BaseModel): @@ -230,7 +224,6 @@ class AgentResponse(BaseModel): host_ip: Optional[str] = None node_name: Optional[str] = None owner_info: Optional[Dict] = None - tools_attached: Optional[int] = Field(default=0, description="附加的外部工具数量") class ResourceUsage(BaseModel): @@ -335,36 +328,6 @@ async def create_agent(request: CreateAgentRequest, db: Session = Depends(get_db config_data["env"] = request.env logger.info(f"环境变量: {list(request.env.keys())}") - # 处理 tool_refs(符合 MCP-Server 规范) - attached_tools = [] - if request.tool_refs: - logger.info(f"📦 处理外部工具引用: {request.tool_refs}") - - # 验证所有工具存在 - missing_tools = [] - for ref in request.tool_refs: - tool = tool_storage.get_tool(ref) - if tool: - attached_tools.append(tool) - else: - missing_tools.append(ref) - - if missing_tools: - raise HTTPException( - status_code=404, - detail=f"以下工具不存在: {', '.join(missing_tools)}" - ) - - # 将工具配置添加到 config - config_data["tool_refs"] = request.tool_refs - config_data["tools_count"] = len(attached_tools) - - # 标记工具被使用 - for ref in request.tool_refs: - tool_storage.mark_tool_in_use(ref, request.name) - - logger.info(f"✅ 已附加 {len(attached_tools)} 个外部工具") - # 添加 user_id 标签 user_id = config_data.get("user_id", "default") if "labels" not in config_data: @@ -553,10 +516,6 @@ async def create_agent(request: CreateAgentRequest, db: Session = Depends(get_db if "status" not in result: result["status"] = "Pending" - # 添加外部工具信息 - if attached_tools: - result["tools_attached"] = len(attached_tools) - try: return AgentResponse(**result) except Exception as validation_error: diff --git a/docs/DYNAMIC_AGENT_GENERATOR_API.md b/docs/DYNAMIC_AGENT_GENERATOR_API.md index 463dbe7..422ffbc 100644 --- a/docs/DYNAMIC_AGENT_GENERATOR_API.md +++ b/docs/DYNAMIC_AGENT_GENERATOR_API.md @@ -1,7 +1,7 @@ # 动态 Agent 生成器 API 文档 -> **版本**: 2026-01-29 v1.1 -> **服务地址**: http://20.212.121.126 +> **版本**: 2026-01-26 v1.0 +> **服务地址**: http://57.158.136.164 > **模板参考**: [cicd-AKS](http://gitee.ath.cx:3000/xiaohei/cicd-AKS) --- @@ -29,12 +29,6 @@ │ │ AKS │ │ │ │ (部署) │ │ │ └──────────┘ │ -│ │ │ -│ ↓ │ -│ ┌──────────┐ │ -│ │Azure DNS │ │ -│ │ (域名) │ │ -│ └──────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────────────┘ ``` @@ -47,40 +41,33 @@ | 配置项 | 值 | |--------|-----| -| 终结点 | \`https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1\` | -| 模型 | \`taiji/claude-sonnet-4-5\` | -| API Key | \`sk-mPV5MVVVVvfGSkXA-ASQXQ\` | +| 终结点 | `https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1` | +| 模型 | `taiji/claude-sonnet-4-5` | +| API Key | `sk-mPV5MVVVVvfGSkXA-ASQXQ` | ### Gitee 配置 | 配置项 | 值 | |--------|-----| -| API URL | \`http://gitee.ath.cx:3000/api/v1\` | -| 用户名 | \`zhanggangyong\` | -| Owner | \`xiaohei\` | +| API URL | `http://gitee.ath.cx:3000/api/v1` | +| 用户名 | `zhanggangyong` | +| Owner | `xiaohei` | | 模板仓库 | [cicd-AKS](http://gitee.ath.cx:3000/xiaohei/cicd-AKS) | ### ACR 配置 | 配置项 | 值 | |--------|-----| -| Registry | \`agnettaiji.azurecr.io\` | -| Namespace | \`ai-agents\` | - -### Azure DNS 配置 - -| 配置项 | 值 | -|--------|-----| -| DNS Zone | \`taijiagnet.com\` | -| 资源组 | \`taiji-ai-v0\` | +| Registry | `agnettaiji.azurecr.io` | +| Namespace | `ai-agents` | ### WAF 白名单 (重要!) Gitee 服务器使用雷池 WAF 保护,需要将以下 IP 添加到白名单: -\`\`\` +``` 4.144.251.116 (AKS 集群出口 IP) -\`\`\` +``` --- @@ -88,15 +75,15 @@ Gitee 服务器使用雷池 WAF 保护,需要将以下 IP 添加到白名单 | 序号 | 接口 | 方法 | 说明 | |------|------|------|------| -| 1 | \`/tools/generate\` | POST | 生成单个外部数据工具 | -| 2 | \`/tools/generate-agent\` | POST | 生成完整 Agent 并推送到 Gitee | -| 3 | \`/tools/{tool_ref_id}\` | GET | 获取工具/Agent 信息 | -| 4 | \`/tools/{tool_ref_id}\` | PUT | 更新工具配置 | -| 5 | \`/tools/{tool_ref_id}\` | DELETE | 删除工具 | -| 6 | \`/tools/{tool_ref_id}/test\` | POST | 测试工具连接 | -| 7 | \`/tools/{tool_ref_id}/build-status\` | GET | 查询构建状态 | -| 8 | \`/tools/{tool_ref_id}/deploy\` | POST | 部署 Agent | -| 9 | \`/tools/\` | GET | 列出所有工具/Agent | +| 1 | `/tools/generate` | POST | 生成单个外部数据工具 | +| 2 | `/tools/generate-agent` | POST | 生成完整 Agent 并推送到 Gitee | +| 3 | `/tools/{tool_ref_id}` | GET | 获取工具/Agent 信息 | +| 4 | `/tools/{tool_ref_id}` | PUT | 更新工具配置 | +| 5 | `/tools/{tool_ref_id}` | DELETE | 删除工具 | +| 6 | `/tools/{tool_ref_id}/test` | POST | 测试工具连接 | +| 7 | `/tools/{tool_ref_id}/build-status` | GET | 查询构建状态 | +| 8 | `/tools/{tool_ref_id}/deploy` | POST | 部署 Agent | +| 9 | `/tools/` | GET | 列出所有工具/Agent | --- @@ -104,9 +91,9 @@ Gitee 服务器使用雷池 WAF 保护,需要将以下 IP 添加到白名单 ### 接口 -\`\`\` +``` POST /tools/generate -\`\`\` +``` ### 请求参数 @@ -127,39 +114,39 @@ POST /tools/generate #### API Key 认证 -\`\`\`json +```json { "type": "api_key", "key": "sk-xxxxxxxxxxxx", "in": "header", "name": "X-API-Key" } -\`\`\` +``` #### Bearer Token 认证 -\`\`\`json +```json { "type": "bearer", "key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } -\`\`\` +``` #### Basic Auth 认证 -\`\`\`json +```json { "type": "basic", "username": "admin", "password": "password123" } -\`\`\` +``` ### 请求示例 -\`\`\`bash -curl -X POST http://20.212.121.126/tools/generate \\ - -H "Content-Type: application/json" \\ +```bash +curl -X POST http://57.158.136.164/tools/generate \ + -H "Content-Type: application/json" \ -d '{ "name": "weather-query", "description": "查询城市天气", @@ -183,78 +170,22 @@ curl -X POST http://20.212.121.126/tools/generate \\ } } }' -\`\`\` +``` ### 响应示例 -\`\`\`json +```json { "success": true, "data": { "tool_ref_id": "tool-weather-query-a49e00fe", "name": "weather-query", "description": "查询城市天气", - "created_at": "2026-01-29T10:00:00.000000" + "created_at": "2026-01-26T10:00:00.000000" }, "message": "工具生成成功" } -\`\`\` - -### 生成的工具代码示例 - -\`\`\`python -""" -工具: weather-query -描述: 查询城市天气 -""" -import os -import json -from typing import Optional, Any -import httpx - - -async def weather_query(city: str) -> str: - """ - 查询城市天气 - - Args: - city: 城市名称 - - Returns: - API 响应结果 (JSON 格式) - """ - url = "https://api.weather.com/v1/current" - headers = {} - params = {"city": city} - params["appid"] = os.getenv("TOOL_API_KEY", "") - - try: - async with httpx.AsyncClient(timeout=30) as client: - response = await client.request( - method="GET", - 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() - }, 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: - return json.dumps({ - "success": False, - "error": str(e) - }, ensure_ascii=False) -\`\`\` +``` --- @@ -262,9 +193,9 @@ async def weather_query(city: str) -> str: ### 接口 -\`\`\` +``` POST /tools/generate-agent -\`\`\` +``` ### 功能描述 @@ -272,7 +203,6 @@ POST /tools/generate-agent 2. 创建 Gitee 仓库并推送代码 3. 包含完整 CI/CD 配置(参考 [cicd-AKS](http://gitee.ath.cx:3000/xiaohei/cicd-AKS)) 4. 自动触发构建和部署 -5. 自动创建 Azure DNS 记录 ### 请求参数 @@ -283,13 +213,13 @@ POST /tools/generate-agent | tools | array | ✅ | 工具配置列表 | | user_id | string | ✅ | 用户 ID | | tenant_id | string | ❌ | 租户 ID | -| auto_deploy | boolean | ❌ | 是否自动部署到 AKS(默认 false) | +| auto_deploy | boolean | ❌ | 是否自动部署到 AKS(默认 true) | ### 请求示例 -\`\`\`bash -curl -X POST http://20.212.121.126/tools/generate-agent \\ - -H "Content-Type: application/json" \\ +```bash +curl -X POST http://57.158.136.164/tools/generate-agent \ + -H "Content-Type: application/json" \ -d '{ "agent_name": "weather-agent", "description": "天气查询智能 Agent,支持获取全球城市天气信息", @@ -324,14 +254,31 @@ curl -X POST http://20.212.121.126/tools/generate-agent \\ } }, "timeout": 30 + }, + { + "name": "get_forecast", + "description": "获取城市未来天气预报", + "url": "https://api.openweathermap.org/data/2.5/forecast", + "method": "GET", + "user_id": "user-001", + "request_params": { + "type": "object", + "properties": { + "q": { + "type": "string", + "description": "城市名称", + "required": true + } + } + } } ] }' -\`\`\` +``` ### 响应示例 -\`\`\`json +```json { "success": true, "data": { @@ -340,78 +287,256 @@ curl -X POST http://20.212.121.126/tools/generate-agent \\ "repo_url": "http://gitee.ath.cx:3000/zhanggangyong/agent-weather-agent-ac3c11", "image_name": "agnettaiji.azurecr.io/ai-agents/agent-weather-agent-ac3c11:latest", "status": "building", - "files_pushed": 9, - "tools_count": 2, - "expected_domain": "agent-weather-agent-ac3c11.taijiagnet.com", - "expected_namespace": "agent-agent-weather-agent-ac3c11" + "files_pushed": 7, + "tools_count": 2 }, - "message": "Agent 项目已创建并推送到 Gitee,CI/CD 正在构建中。部署后访问: http://agent-weather-agent-ac3c11.taijiagnet.com" + "message": "Agent 项目已创建并推送到 Gitee,CI/CD 正在构建中" } -\`\`\` +``` ### 生成的项目结构 -\`\`\` +``` agent-weather-agent-{id}/ ├── .gitea/workflows/ -│ └── ci-cd.yaml # CI/CD 配置(构建+部署+DNS) -├── src/ +│ └── ci-cd.yaml # CI/CD 配置(构建+部署) +├── tools/ │ ├── __init__.py -│ └── server/ -│ ├── __init__.py -│ ├── mcp_server.py # MCP 服务器 + 工具定义 -│ └── api_server.py # FastAPI HTTP 服务器 -├── run_api_server.py # 入口文件 -├── Dockerfile # Docker 构建文件 -├── requirements.txt # Python 依赖 -└── README.md # 说明文档 -\`\`\` +│ ├── get_weather.py # 工具1: 天气查询 +│ └── get_forecast.py # 工具2: 天气预报 +├── main.py # FastAPI 主程序 +├── Dockerfile # Docker 构建文件 +├── requirements.txt # Python 依赖 +└── README.md # 说明文档 +``` --- -## 3-9. 其他接口 +## 3. 获取工具/Agent 信息 -### 3. 获取工具/Agent 信息 +### 接口 -\`\`\` +``` GET /tools/{tool_ref_id} -\`\`\` +``` -### 4. 更新工具配置 +### 响应示例 -\`\`\` +```json +{ + "success": true, + "data": { + "id": "agent-agent-weather-agent-ac3c11", + "name": "weather-agent", + "description": "天气查询智能 Agent", + "status": "building", + "created_at": "2026-01-26T10:00:00.000000", + "repo_url": "http://gitee.ath.cx:3000/zhanggangyong/agent-weather-agent-ac3c11", + "image_name": "agnettaiji.azurecr.io/ai-agents/agent-weather-agent-ac3c11:latest" + } +} +``` + +--- + +## 4. 更新工具配置 + +### 接口 + +``` PUT /tools/{tool_ref_id} -\`\`\` +``` -### 5. 删除工具 +### 请求参数 -\`\`\` +| 参数 | 类型 | 说明 | +|------|------|------| +| description | string | 工具描述 | +| url | string | API URL | +| method | string | HTTP 方法 | +| auth | object | 认证配置 | +| request_params | object | 请求参数 | +| timeout | integer | 超时时间 | + +--- + +## 5. 删除工具 + +### 接口 + +``` DELETE /tools/{tool_ref_id}?delete_repo=false -\`\`\` +``` -### 6. 测试工具连接 +### 参数 -\`\`\` +| 参数 | 类型 | 说明 | +|------|------|------| +| delete_repo | boolean | 是否同时删除 Gitee 仓库 | + +--- + +## 6. 测试工具连接 + +### 接口 + +``` POST /tools/{tool_ref_id}/test -\`\`\` +``` -### 7. 查询构建状态 +### 请求参数 -\`\`\` +```json +{ + "test_params": { + "q": "Beijing" + } +} +``` + +### 响应示例 + +```json +{ + "success": true, + "data": { + "status_code": 200, + "response_time_ms": 156.5, + "content_type": "application/json", + "response_preview": "{\"coord\":{\"lon\":116.4074,\"lat\":39.9042}...}" + }, + "message": "工具连接测试成功" +} +``` + +--- + +## 7. 查询构建状态 + +### 接口 + +``` GET /tools/{tool_ref_id}/build-status -\`\`\` +``` -### 8. 部署 Agent +### 功能描述 -\`\`\` +1. 查询 Gitee Action 运行状态 +2. 查询 ACR 镜像是否已构建完成 + +### 响应示例 + +```json +{ + "success": true, + "data": { + "tool_ref_id": "agent-agent-weather-agent-ac3c11", + "repo_name": "agent-weather-agent-ac3c11", + "overall_status": "ready", + "gitee_action": { + "status": "completed", + "conclusion": "success", + "run_id": 12345, + "html_url": "http://gitee.ath.cx:3000/zhanggangyong/agent-weather-agent-ac3c11/actions/runs/12345" + }, + "acr_image": { + "image_name": "agnettaiji.azurecr.io/ai-agents/agent-weather-agent-ac3c11:latest", + "exists": true, + "digest": "sha256:abc123...", + "created_at": "2026-01-26T10:05:00Z" + } + } +} +``` + +### 状态说明 + +| 状态 | 说明 | +|------|------| +| `building` | 正在构建中 | +| `ready` | 镜像已就绪,可以部署 | +| `build_failed` | 构建失败 | + +--- + +## 8. 部署 Agent + +### 接口 + +``` POST /tools/{tool_ref_id}/deploy -\`\`\` +``` -### 9. 列出所有工具/Agent +### 前提条件 -\`\`\` +- ACR 中已存在镜像(构建状态为 `ready`) + +### 响应示例 + +```json +{ + "success": true, + "data": { + "tool_ref_id": "agent-agent-weather-agent-ac3c11", + "template_name": "agent_weather_agent_ac3c11", + "image_name": "agnettaiji.azurecr.io/ai-agents/agent-weather-agent-ac3c11:latest", + "status": "deployed", + "message": "模板 agent_weather_agent_ac3c11 已创建,可通过 POST /agents 创建实例" + }, + "message": "Agent 模板已创建,可以开始部署实例" +} +``` + +### 部署实例 + +模板创建后,使用标准的 Agent 创建接口部署实例: + +```bash +curl -X POST http://57.158.136.164/agents \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-weather-instance", + "template": "agent_weather_agent_ac3c11", + "config": {} + }' +``` + +--- + +## 9. 列出所有工具/Agent + +### 接口 + +``` GET /tools/?user_id=user-001 -\`\`\` +``` + +### 响应示例 + +```json +{ + "success": true, + "data": { + "tools": [ + { + "id": "agent-agent-weather-agent-ac3c11", + "name": "weather-agent", + "type": "agent", + "status": "ready", + "created_at": "2026-01-26T10:00:00.000000" + }, + { + "id": "tool-weather-query-a49e00fe", + "name": "weather-query", + "type": "tool", + "status": "created", + "created_at": "2026-01-26T09:00:00.000000" + } + ], + "count": 2 + } +} +``` --- @@ -432,9 +557,10 @@ GET /tools/?user_id=user-001 ### 步骤 1: 创建 Agent -\`\`\`bash -curl -X POST http://20.212.121.126/tools/generate-agent \\ - -H "Content-Type: application/json" \\ +```bash +# 发送工具定义,创建 Agent 项目 +curl -X POST http://57.158.136.164/tools/generate-agent \ + -H "Content-Type: application/json" \ -d '{ "agent_name": "my-custom-agent", "description": "我的自定义 Agent", @@ -442,73 +568,165 @@ curl -X POST http://20.212.121.126/tools/generate-agent \\ "auto_deploy": true, "tools": [...] }' -\`\`\` + +# 返回 agent_ref_id +``` ### 步骤 2: 等待构建完成 -\`\`\`bash -curl http://20.212.121.126/tools/{agent_ref_id}/build-status -\`\`\` +```bash +# 轮询查询构建状态 +curl http://57.158.136.164/tools/{agent_ref_id}/build-status -### 步骤 3: 访问 Agent +# 等待 overall_status 变为 "ready" +``` -\`\`\`bash -# 通过域名访问 -curl http://{agent-name}.taijiagnet.com/ +### 步骤 3: 部署 Agent(如果 auto_deploy=false) -# 健康检查 -curl http://{agent-name}.taijiagnet.com/health +```bash +# 手动部署 +curl -X POST http://57.158.136.164/tools/{agent_ref_id}/deploy +``` -# 调用工具 -curl -X POST http://{agent-name}.taijiagnet.com/tools/call \\ - -H "Content-Type: application/json" \\ - -d '{"tool_name": "get_weather", "parameters": {"q": "Beijing"}}' +### 步骤 4: 创建 Agent 实例 -# MCP 调用 -curl -X POST http://{agent-name}.taijiagnet.com/mcp \\ - -H "Content-Type: application/json" \\ - -d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}' -\`\`\` +```bash +# 使用生成的模板创建实例 +curl -X POST http://57.158.136.164/agents \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-instance", + "template": "{template_name}", + "config": {} + }' +``` --- -## Agent 端点说明 +## CI/CD 配置说明 -生成的 Agent 提供以下端点: +生成的 CI/CD 配置参考 [cicd-AKS](http://gitee.ath.cx:3000/xiaohei/cicd-AKS),包含: -### REST API +1. **构建 Docker 镜像** (ARM64 架构) +2. **推送到 ACR** +3. **部署到 AKS**(如果 `auto_deploy=true`) -| 端点 | 方法 | 说明 | -|------|------|------| -| \`/\` | GET | 服务信息 | -| \`/health\` | GET | 健康检查 | -| \`/tools\` | GET | 列出可用工具 | -| \`/tools/call\` | POST | 调用工具 | +### 生成的 CI/CD 配置示例 -### MCP 端点 +```yaml +name: CI-CD-AKS -| 端点 | 方法 | 说明 | -|------|------|------| -| \`/mcp\` | POST | MCP HTTP 端点 | -| \`/mcp/sse\` | GET | MCP SSE 端点 | -| \`/mcp/sse\` | POST | MCP SSE POST 端点 | +on: + push: + branches: [ main ] + +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to ACR + uses: docker/login-action@v3 + with: + registry: agnettaiji.azurecr.io + username: ${{ secrets.ACR_USERNAME }} + password: ${{ secrets.ACR_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/arm64 + push: true + tags: agnettaiji.azurecr.io/ai-agents/{agent-name}:latest + + - name: Azure Login + uses: azure/login@v2 + with: + creds: | + { + "clientId": "${{ secrets.AZ_CLIENT_ID }}", + "clientSecret": "${{ secrets.AZ_CLIENT_SECRET }}", + "subscriptionId": "${{ secrets.AZ_SUBSCRIPTION_ID }}", + "tenantId": "${{ secrets.AZ_TENANT_ID }}" + } + + - name: Get AKS credentials + run: | + az aks get-credentials --resource-group ${{ secrets.AZ_RG }} \ + --name ${{ secrets.AZ_AKS }} --overwrite-existing + + - name: Deploy to AKS + run: | + kubectl set image deployment/{agent-name} \ + {agent-name}=agnettaiji.azurecr.io/ai-agents/{agent-name}:latest \ + -n ai-agents || echo "Deployment will be created by agent-manager" +``` + +### 必需的 Gitee Secrets + +在 Gitee 仓库设置中配置以下 Secrets: + +| Secret 名称 | 说明 | 值 | +|-------------|------|-----| +| `ACR_LOGIN_SERVER` | ACR 登录服务器 | `agnettaiji.azurecr.io` | +| `ACR_USERNAME` | ACR 用户名 | `agnettaiji` | +| `ACR_PASSWORD` | ACR 密码 | `hDpX5t34N5ZmnKdtqyjYL5co/SnXJrmD20CRpGpWaG+ACRCw2wGM` | +| `AZ_CLIENT_ID` | Azure 服务主体 Client ID | `fb306798-2cfe-4ac9-ba48-eab7bc71bcfe` | +| `AZ_CLIENT_SECRET` | Azure 服务主体密钥 | `cVK8Q~xlfBwm2_t2TC24yrTukWV4F3G~eIjBBa0D` | +| `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` | + +### Gitee Runner 配置 + +Runner 注册 Token: `1A4jRxECdtycHAuzDHcICNeVmLaWrZ9FvwV2vYY2` + +注册命令: +```bash +act_runner register --no-interactive \ + --instance "http://gitee.ath.cx:3000" \ + --token "1A4jRxECdtycHAuzDHcICNeVmLaWrZ9FvwV2vYY2" \ + --name "your-runner-name" \ + --labels "self-hosted,linux,arm64" +``` --- -## CI/CD Secrets 配置 +## 环境变量配置 -| Secret 名称 | 说明 | -|-------------|------| -| \`ACR_LOGIN_SERVER\` | ACR 登录服务器 | -| \`ACR_USERNAME\` | ACR 用户名 | -| \`ACR_PASSWORD\` | ACR 密码 | -| \`AZ_CLIENT_ID\` | Azure 服务主体 Client ID | -| \`AZ_CLIENT_SECRET\` | Azure 服务主体密钥 | -| \`AZ_TENANT_ID\` | Azure 租户 ID | -| \`AZ_SUBSCRIPTION_ID\` | Azure 订阅 ID | -| \`AZ_RG\` | AKS 资源组名称 | -| \`AZ_AKS\` | AKS 集群名称 | -| \`AZURE_DNS_ZONE\` | DNS Zone 名称 | +Agent Manager 需要以下环境变量: + +### ConfigMap (agent-manager-config) + +```yaml +GITEE_API_URL: "http://gitee.ath.cx:3000/api/v1" +GITEE_BASE_URL: "http://gitee.ath.cx:3000" +GITEE_OWNER: "xiaohei" +GITEE_TEMPLATE_REPO: "cicd-AKS" +ACR_REGISTRY: "agnettaiji.azurecr.io" +ACR_NAMESPACE: "ai-agents" +LLM_BASE_URL: "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1" +LLM_MODEL: "taiji/claude-sonnet-4-5" +``` + +### Secret (agent-manager-secret) + +```yaml +GITEE_TOKEN: +GITEE_USERNAME: +GITEE_PASSWORD: +LLM_API_KEY: +``` --- @@ -517,4 +735,3 @@ curl -X POST http://{agent-name}.taijiagnet.com/mcp \\ | 日期 | 版本 | 更新内容 | |------|------|----------| | 2026-01-26 | v1.0 | 初始版本 | -| 2026-01-29 | v1.1 | 更新服务地址 (20.212.121.126);添加 DNS 配置说明;添加 generate_tool_code 生成的代码示例;更新项目结构为 src/server/ 目录;添加 expected_domain/expected_namespace 响应字段;添加 Agent 端点说明;简化 Secrets 配置表格 | diff --git a/gitee_manager.py b/gitee_manager.py index c0bfdd2..38dde97 100644 --- a/gitee_manager.py +++ b/gitee_manager.py @@ -55,8 +55,7 @@ class GiteeManager: self, repo_name: str, description: str = "", - private: bool = False, - auto_init: bool = False + private: bool = False ) -> Dict[str, Any]: """ 创建 Gitee 仓库 @@ -65,7 +64,6 @@ class GiteeManager: repo_name: 仓库名称 description: 仓库描述 private: 是否私有 - auto_init: 是否自动初始化(默认不初始化,这样可以直接推送文件) Returns: 仓库信息 @@ -76,68 +74,50 @@ class GiteeManager: "name": repo_name, "description": description, "private": private, - "auto_init": auto_init, # 不自动初始化,后续直接推送文件 + "auto_init": True, # 自动初始化 "default_branch": "main" } - import time - max_retries = 3 - - for attempt in range(max_retries): - try: - response = requests.post( - url, - json=data, - headers=self._get_headers(), - timeout=30 - ) + try: + response = requests.post( + url, + json=data, + headers=self._get_headers(), + timeout=30 + ) + + if response.status_code == 201: + repo_info = response.json() + owner = repo_info.get("owner", {}).get("login", self.gitee_username) + logger.info(f"✅ Gitee 仓库创建成功: {owner}/{repo_name}") + return { + "success": True, + "repo_name": repo_name, + "owner": owner, + "clone_url": repo_info.get("clone_url"), + "html_url": repo_info.get("html_url"), + "ssh_url": repo_info.get("ssh_url") + } + elif response.status_code == 409: + # 仓库已存在 + owner = self.gitee_username or self.gitee_owner + logger.warning(f"仓库已存在: {repo_name}") + return { + "success": True, + "repo_name": repo_name, + "owner": owner, + "clone_url": f"{self.gitee_base_url}/{owner}/{repo_name}.git", + "html_url": f"{self.gitee_base_url}/{owner}/{repo_name}", + "exists": True + } + else: + error_msg = response.json().get("message", response.text) + logger.error(f"创建仓库失败: {error_msg}") + return {"success": False, "error": error_msg} - if response.status_code == 201: - repo_info = response.json() - owner = repo_info.get("owner", {}).get("login", self.gitee_username) - logger.info(f"✅ Gitee 仓库创建成功: {owner}/{repo_name}") - return { - "success": True, - "repo_name": repo_name, - "owner": owner, - "clone_url": repo_info.get("clone_url"), - "html_url": repo_info.get("html_url"), - "ssh_url": repo_info.get("ssh_url") - } - elif response.status_code == 409: - # 仓库已存在 - owner = self.gitee_username or self.gitee_owner - logger.warning(f"仓库已存在: {repo_name}") - return { - "success": True, - "repo_name": repo_name, - "owner": owner, - "clone_url": f"{self.gitee_base_url}/{owner}/{repo_name}.git", - "html_url": f"{self.gitee_base_url}/{owner}/{repo_name}", - "exists": True - } - elif response.status_code == 429: - # API 限流,等待后重试 - wait_time = (attempt + 1) * 2 - logger.warning(f"API 限流,等待 {wait_time} 秒后重试 ({attempt + 1}/{max_retries})") - time.sleep(wait_time) - continue - else: - try: - error_msg = response.json().get("message", response.text) - except: - error_msg = response.text[:200] if response.text else f"HTTP {response.status_code}" - logger.error(f"创建仓库失败: {error_msg}") - return {"success": False, "error": error_msg} - - except Exception as e: - logger.error(f"创建仓库异常 (尝试 {attempt + 1}/{max_retries}): {e}") - if attempt < max_retries - 1: - time.sleep(2) - continue - return {"success": False, "error": str(e)} - - return {"success": False, "error": "API 限流,请稍后重试"} + except Exception as e: + logger.error(f"创建仓库异常: {e}") + return {"success": False, "error": str(e)} def push_files( self, @@ -148,7 +128,7 @@ class GiteeManager: owner: str = None ) -> Dict[str, Any]: """ - 推送文件到仓库(使用 Git Data API 批量提交) + 推送文件到仓库 Args: repo_name: 仓库名称 @@ -166,51 +146,19 @@ class GiteeManager: repo_owner = owner or self.gitee_username or self.gitee_owner results = {"success": True, "files": [], "owner": repo_owner} - logger.info(f"开始推送 {len(files)} 个文件到 {repo_owner}/{repo_name}") - - # 检查仓库是否为空 - is_empty = self._is_repo_empty(repo_owner, repo_name) - - if is_empty: - # 空仓库:使用初始提交方式 - logger.info("仓库为空,使用初始提交方式") - init_result = self._push_initial_commit(repo_owner, repo_name, files, commit_message, branch) - if init_result.get("success"): - logger.info(f"✅ 初始提交成功: {len(files)} 个文件") - results["files"] = [{"path": p, "status": "success"} for p in files.keys()] - return results - else: - logger.error(f"初始提交失败: {init_result.get('error')}") - results["success"] = False - results["error"] = init_result.get("error") - return results - - # 非空仓库:尝试使用批量提交 API - try: - batch_result = self._push_files_batch(repo_owner, repo_name, files, commit_message, branch) - if batch_result.get("success"): - logger.info(f"✅ 批量推送成功: {len(files)} 个文件") - results["files"] = [{"path": p, "status": "success"} for p in files.keys()] - return results - else: - logger.warning(f"批量推送失败,尝试逐个推送: {batch_result.get('error')}") - except Exception as e: - logger.warning(f"批量推送异常,尝试逐个推送: {e}") - - # 方法2:逐个推送文件 for file_path, content in files.items(): try: # 使用 Contents API 创建/更新文件 url = f"{self.gitee_api_url}/repos/{repo_owner}/{repo_name}/contents/{file_path}" - # 检查文件是否存在(获取 SHA) + # 检查文件是否存在 check_response = requests.get(url, headers=self._get_headers(), timeout=10) sha = None if check_response.status_code == 200: sha = check_response.json().get("sha") data = { - "message": f"{commit_message} - {file_path}", + "message": commit_message, "content": base64.b64encode(content.encode()).decode(), "branch": branch } @@ -226,7 +174,7 @@ class GiteeManager: ) # 添加短暂延迟避免 API 限流 - time.sleep(0.3) + time.sleep(0.5) if response.status_code in [200, 201]: results["files"].append({ @@ -235,17 +183,14 @@ class GiteeManager: }) logger.info(f"✅ 文件推送成功: {file_path}") else: - error_msg = response.text[:200] - logger.error(f"❌ 文件推送失败: {file_path}, 状态码: {response.status_code}, 错误: {error_msg}") results["files"].append({ "path": file_path, "status": "failed", - "error": error_msg + "error": response.text }) results["success"] = False except Exception as e: - logger.error(f"❌ 文件推送异常: {file_path}, 错误: {e}") results["files"].append({ "path": file_path, "status": "failed", @@ -253,248 +198,8 @@ class GiteeManager: }) results["success"] = False - logger.info(f"推送完成: 成功 {sum(1 for f in results['files'] if f['status'] == 'success')}/{len(files)} 个文件") return results - def _push_files_batch( - self, - owner: str, - repo_name: str, - files: Dict[str, str], - commit_message: str, - branch: str - ) -> Dict[str, Any]: - """ - 使用 Git Data API 批量推送文件 - - 流程: - 1. 获取当前分支的最新 commit SHA - 2. 获取该 commit 的 tree SHA - 3. 创建新的 blobs - 4. 创建新的 tree - 5. 创建新的 commit - 6. 更新分支引用 - """ - try: - headers = self._get_headers() - base_url = f"{self.gitee_api_url}/repos/{owner}/{repo_name}" - - # 1. 获取当前分支的最新 commit - # Gitea API 返回的是数组 - ref_url = f"{base_url}/git/refs/heads/{branch}" - ref_response = requests.get(ref_url, headers=headers, timeout=10) - - if ref_response.status_code != 200: - return {"success": False, "error": f"获取分支引用失败: {ref_response.text}"} - - ref_data = ref_response.json() - - # 处理 Gitea API 可能返回数组或对象的情况 - if isinstance(ref_data, list): - if not ref_data: - return {"success": False, "error": "无法获取分支引用"} - ref_data = ref_data[0] - - latest_commit_sha = ref_data.get("object", {}).get("sha") - - if not latest_commit_sha: - return {"success": False, "error": "无法获取最新 commit SHA"} - - logger.info(f"获取到最新 commit SHA: {latest_commit_sha[:8]}...") - - # 2. 获取该 commit 的 tree - commit_url = f"{base_url}/git/commits/{latest_commit_sha}" - commit_response = requests.get(commit_url, headers=headers, timeout=10) - - if commit_response.status_code != 200: - return {"success": False, "error": f"获取 commit 信息失败: {commit_response.text}"} - - commit_data = commit_response.json() - - # Gitea API: tree 在 commit.tree 中 - base_tree_sha = None - if commit_data.get("commit", {}).get("tree"): - base_tree_sha = commit_data["commit"]["tree"].get("sha") - elif commit_data.get("tree"): - base_tree_sha = commit_data["tree"].get("sha") - - if not base_tree_sha: - logger.error(f"无法获取 tree SHA,commit 数据: {commit_data}") - return {"success": False, "error": "无法获取 tree SHA"} - - logger.info(f"获取到 base tree SHA: {base_tree_sha[:8]}...") - - # 3. 创建 tree 项目 - tree_items = [] - for file_path, content in files.items(): - tree_items.append({ - "path": file_path, - "mode": "100644", - "type": "blob", - "content": content - }) - - # 4. 创建新 tree - tree_url = f"{base_url}/git/trees" - tree_data = { - "base_tree": base_tree_sha, - "tree": tree_items - } - - logger.info(f"创建新 tree,包含 {len(tree_items)} 个文件...") - tree_response = requests.post(tree_url, json=tree_data, headers=headers, timeout=120) - - if tree_response.status_code not in [200, 201]: - return {"success": False, "error": f"创建 tree 失败: {tree_response.text[:200]}"} - - new_tree_sha = tree_response.json().get("sha") - logger.info(f"新 tree SHA: {new_tree_sha[:8]}...") - - # 5. 创建新 commit - commit_create_url = f"{base_url}/git/commits" - commit_create_data = { - "message": commit_message, - "tree": new_tree_sha, - "parents": [latest_commit_sha] - } - - logger.info(f"创建新 commit...") - commit_create_response = requests.post(commit_create_url, json=commit_create_data, headers=headers, timeout=30) - - if commit_create_response.status_code not in [200, 201]: - return {"success": False, "error": f"创建 commit 失败: {commit_create_response.text[:200]}"} - - new_commit_sha = commit_create_response.json().get("sha") - logger.info(f"新 commit SHA: {new_commit_sha[:8]}...") - - # 6. 更新分支引用 - update_ref_data = { - "sha": new_commit_sha, - "force": False - } - - logger.info(f"更新分支引用到新 commit...") - update_ref_response = requests.patch(ref_url, json=update_ref_data, headers=headers, timeout=10) - - if update_ref_response.status_code not in [200, 201]: - return {"success": False, "error": f"更新分支引用失败: {update_ref_response.text[:200]}"} - - logger.info(f"✅ 批量推送成功,commit: {new_commit_sha[:8]}") - return {"success": True, "commit_sha": new_commit_sha} - - except Exception as e: - import traceback - logger.error(f"批量推送异常: {e}") - logger.error(traceback.format_exc()) - return {"success": False, "error": str(e)} - - def _is_repo_empty(self, owner: str, repo_name: str) -> bool: - """检查仓库是否为空""" - try: - url = f"{self.gitee_api_url}/repos/{owner}/{repo_name}" - response = requests.get(url, headers=self._get_headers(), timeout=10) - if response.status_code == 200: - repo_data = response.json() - return repo_data.get("empty", True) - return True - except Exception as e: - logger.warning(f"检查仓库是否为空失败: {e}") - return True - - def _push_initial_commit( - self, - owner: str, - repo_name: str, - files: Dict[str, str], - commit_message: str, - branch: str - ) -> Dict[str, Any]: - """ - 推送初始提交到空仓库 - - 空仓库不支持 Git Data API,需要先用 Contents API 创建第一个文件 - 然后再使用 Git Data API 批量推送其余文件 - """ - import time - - try: - headers = self._get_headers() - base_url = f"{self.gitee_api_url}/repos/{owner}/{repo_name}" - - logger.info(f"开始初始提交,共 {len(files)} 个文件...") - - # 找出 README.md 作为初始文件(或者使用第一个文件) - init_file_path = None - init_file_content = None - - if "README.md" in files: - init_file_path = "README.md" - init_file_content = files["README.md"] - else: - # 使用第一个文件 - init_file_path = list(files.keys())[0] - init_file_content = files[init_file_path] - - # 1. 使用 Contents API 创建第一个文件(初始化仓库) - init_url = f"{base_url}/contents/{init_file_path}" - init_data = { - "message": f"{commit_message} - {init_file_path}", - "content": base64.b64encode(init_file_content.encode()).decode(), - "branch": branch - } - - logger.info(f"创建初始文件: {init_file_path}") - init_response = requests.post(init_url, json=init_data, headers=headers, timeout=30) - - if init_response.status_code not in [200, 201]: - return {"success": False, "error": f"创建初始文件失败: {init_response.text[:300]}"} - - logger.info(f"✓ 初始文件创建成功: {init_file_path}") - time.sleep(0.5) - - # 2. 推送剩余文件 - remaining_files = {k: v for k, v in files.items() if k != init_file_path} - - if remaining_files: - logger.info(f"使用批量方式推送剩余 {len(remaining_files)} 个文件...") - - # 现在仓库已初始化,使用 Git Data API 批量推送 - batch_result = self._push_files_batch(owner, repo_name, remaining_files, commit_message, branch) - - if not batch_result.get("success"): - # 批量失败,逐个推送 - logger.warning(f"批量推送失败: {batch_result.get('error')}, 尝试逐个推送") - - for file_path, content in remaining_files.items(): - try: - file_url = f"{base_url}/contents/{file_path}" - file_data = { - "message": f"{commit_message} - {file_path}", - "content": base64.b64encode(content.encode()).decode(), - "branch": branch - } - - file_response = requests.post(file_url, json=file_data, headers=headers, timeout=30) - - if file_response.status_code in [200, 201]: - logger.info(f" ✓ {file_path}") - else: - logger.error(f" ✗ {file_path}: {file_response.text[:100]}") - - time.sleep(0.3) - - except Exception as e: - logger.error(f" ✗ {file_path}: {e}") - - logger.info(f"✅ 初始提交完成") - return {"success": True} - - except Exception as e: - import traceback - logger.error(f"初始提交异常: {e}") - logger.error(traceback.format_exc()) - return {"success": False, "error": str(e)} - def get_action_status(self, repo_name: str) -> Dict[str, Any]: """ 获取 Gitee Action 运行状态 diff --git a/k8s/agent-manager-configmap.yaml b/k8s/agent-manager-configmap.yaml index b039168..68199ab 100644 --- a/k8s/agent-manager-configmap.yaml +++ b/k8s/agent-manager-configmap.yaml @@ -10,3 +10,14 @@ data: AZURE_DNS_ZONE: "taijiagnet.com" AZURE_SUBSCRIPTION_ID: "your-subscription-id" AZURE_RESOURCE_GROUP: "your-resource-group" + + # Gitee 配置(非敏感信息) + GITEE_API_URL: "http://gitee.ath.cx:3000/api/v1" + GITEE_BASE_URL: "http://gitee.ath.cx:3000" + GITEE_OWNER: "xiaohei" + GITEE_USERNAME: "zhanggangyong" + GITEE_TEMPLATE_REPO: "cicd-AKS" + + # ACR 配置 + ACR_REGISTRY: "agnettaiji.azurecr.io" + ACR_NAMESPACE: "ai-agents" \ No newline at end of file diff --git a/k8s/agent-manager-deployment.yaml b/k8s/agent-manager-deployment.yaml index 13c48fe..3dd4ee2 100644 --- a/k8s/agent-manager-deployment.yaml +++ b/k8s/agent-manager-deployment.yaml @@ -61,6 +61,17 @@ spec: secretKeyRef: name: agent-manager-secret key: AZURE_CLIENT_SECRET + # Gitee 凭据 + - name: GITEE_TOKEN + valueFrom: + secretKeyRef: + name: agent-manager-secret + key: GITEE_TOKEN + - name: GITEE_PASSWORD + valueFrom: + secretKeyRef: + name: agent-manager-secret + key: GITEE_PASSWORD # 挂载 kubeconfig(用于管理其他 Agent) volumeMounts: diff --git a/k8s/agent-manager-secret.yaml b/k8s/agent-manager-secret.yaml index 789473f..1803980 100644 --- a/k8s/agent-manager-secret.yaml +++ b/k8s/agent-manager-secret.yaml @@ -10,5 +10,9 @@ stringData: AZURE_CLIENT_ID: "your-client-id" AZURE_CLIENT_SECRET: "your-client-secret" + # Gitee 凭据(敏感信息) + GITEE_TOKEN: "your-gitee-token" + GITEE_PASSWORD: "your-gitee-password" + # 数据库密码(如果需要单独管理) # DB_PASSWORD: "By@123456." diff --git a/scripts/deploy-to-k8s-arm64.sh b/scripts/deploy-to-k8s-arm64.sh index 4c4e797..d65c92a 100755 --- a/scripts/deploy-to-k8s-arm64.sh +++ b/scripts/deploy-to-k8s-arm64.sh @@ -30,6 +30,10 @@ AZURE_CLIENT_SECRET="${AZURE_CLIENT_SECRET:-your-client-secret}" AZURE_SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID:-your-subscription-id}" AZURE_RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-your-resource-group}" +# Gitee 配置(需要替换为实际值) +GITEE_TOKEN="${GITEE_TOKEN:-your-gitee-token}" +GITEE_PASSWORD="${GITEE_PASSWORD:-your-gitee-password}" + echo -e "${BLUE}========================================${NC}" echo -e "${BLUE} Agent Manager K8s 部署 (ARM64)${NC}" echo -e "${BLUE}========================================${NC}" @@ -154,7 +158,18 @@ update_config() { fi fi - # 创建临时 secret 文件 + # 检查 Gitee 凭据 + if [ "$GITEE_TOKEN" = "your-gitee-token" ]; then + print_warning "请设置 GITEE_TOKEN 环境变量(创建 Agent 仓库必需)" + print_warning " export GITEE_TOKEN=your-actual-token" + read -p "是否继续部署(不含 Gitee 仓库功能)?[y/N] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi + fi + + # 创建临时 secret 文件(包含 Azure 和 Gitee 凭据) cat > /tmp/agent-manager-secret.yaml <