feat: 新增外部工具 API 及回调功能(计费)

主要更新:
- 新增 external_tool_api.py: 外部工具管理 API
- 新增 tool_storage.py: 工具存储管理器
- 新增回调功能用于计费 (agent_callback_utils)
- 支持多工具创建 Agent
- 新增 CI/CD 构建状态查询 API
- 新增部署信息查询 API
- 更新文档 (EXTERNAL_TOOL_API.md v2.0)
- 更新 Dockerfile 添加新模块
- 更新 app.py 集成外部工具路由
This commit is contained in:
zhanggangyong
2026-01-29 12:00:16 +00:00
parent 4b09912806
commit 8cb166fb58
12 changed files with 3777 additions and 442 deletions
+5
View File
@@ -16,8 +16,13 @@ COPY template_manager.py .
COPY gitee_manager.py . COPY gitee_manager.py .
COPY agent_code_generator.py . COPY agent_code_generator.py .
COPY tool_generator_api.py . COPY tool_generator_api.py .
COPY external_tool_api.py .
COPY tool_storage.py .
COPY agent_manager/ ./agent_manager/ COPY agent_manager/ ./agent_manager/
# 创建工具存储目录
RUN mkdir -p /app/tool_storage
# 安装依赖 # 安装依赖
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
+535 -17
View File
@@ -101,6 +101,135 @@ class AgentCodeGenerator:
} }
return type_map.get(json_type, "Any") return type_map.get(json_type, "Any")
def generate_tool_code(self, tool_config: dict) -> str:
"""
根据工具配置生成 Pydantic 工具代码
Args:
tool_config: 包含 name, url, method, auth, request_params 等
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 = tool_config.get("request_params", {})
request_body = tool_config.get("request_body", {})
timeout = tool_config.get("timeout", 30)
# 构建参数
params = []
params_doc = []
# 支持两种格式:
# 1. {"properties": {"symbol": {...}}}
# 2. {"symbol": {...}} (直接参数格式)
param_props = request_params
if request_params and request_params.get("properties"):
param_props = request_params["properties"]
elif request_params and not any(k in request_params for k in ["type", "required", "description"]):
param_props = request_params
else:
param_props = {}
if param_props:
for p_name, p_info in param_props.items():
if not isinstance(p_info, dict):
continue
p_type = self._json_type_to_python(p_info.get("type", "string"))
p_desc = p_info.get("description", "")
is_required = p_info.get("required", False)
default = p_info.get("default")
if is_required:
params.append(f"{p_name}: {p_type}")
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", "")'
# 生成函数代码
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 = "{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:
return json.dumps({{
"success": False,
"error": str(e)
}}, ensure_ascii=False)
'''
return code
def generate_mcp_server( def generate_mcp_server(
self, self,
agent_name: str, agent_name: str,
@@ -377,23 +506,40 @@ if __name__ == '__main__':
- 对于操作请求:返回操作状态和结果 - 对于操作请求:返回操作状态和结果
- 始终使用中文与用户交流(除非用户使用其他语言)''' - 始终使用中文与用户交流(除非用户使用其他语言)'''
def generate_api_server(self, agent_name: str, description: str) -> str: def generate_api_server(self, agent_name: str, description: str, tools_config: List[Dict] = None) -> str:
""" """
生成 API 服务器代码 (api_server.py) 生成 API 服务器代码 (api_server.py)
基于 _template/src/server/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'''""" return f'''"""
HTTP API 服务器 - {agent_name} HTTP API 服务器 - {agent_name}
{description} {description}
提供 REST API 和 MCP HTTP/SSE 端点。 提供 REST API 和 MCP HTTP/SSE 端点。
集成回调功能用于计费。
自动生成时间: {datetime.now().isoformat()} 自动生成时间: {datetime.now().isoformat()}
""" """
import json import json
import uuid import uuid
import os import os
from typing import Optional, Dict, Any, AsyncGenerator import logging
from typing import Optional, Dict, Any, AsyncGenerator, List
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime
from fastapi import FastAPI, HTTPException, Request, Header, Depends from fastapi import FastAPI, HTTPException, Request, Header, Depends
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@@ -401,19 +547,47 @@ from fastapi.responses import StreamingResponse, JSONResponse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from .mcp_server import TOOL_MAP, TOOL_LIST from .mcp_server import TOOL_MAP, TOOL_LIST
from .agent_callback_utils import AgentCallbackHandler, CallbackContextManager
# ==================== 配置 ==================== # ==================== 配置 ====================
SERVER_NAME = "{agent_name}" 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 应用 ==================== # ==================== FastAPI 应用 ====================
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
print(f"🚀 {{SERVER_NAME}} 启动") logger.info(f"🚀 {{SERVER_NAME}} 启动")
logger.info(f" 包含工具: {{', '.join(AGENT_TOOLS)}}")
logger.info(f" Pod 名称: {{POD_NAME}}")
logger.info(f" 用户 ID: {{USER_ID or '未设置'}}")
yield yield
print(f"🛑 {{SERVER_NAME}} 关闭") logger.info(f"🛑 {{SERVER_NAME}} 关闭")
app = FastAPI( app = FastAPI(
title=SERVER_NAME, title=SERVER_NAME,
@@ -467,13 +641,15 @@ async def root():
return {{ return {{
"service": SERVER_NAME, "service": SERVER_NAME,
"status": "running", "status": "running",
"tools": list(TOOL_MAP.keys()) "tools": list(TOOL_MAP.keys()),
"tools_count": len(TOOL_MAP),
"pod_name": POD_NAME
}} }}
@app.get("/health") @app.get("/health")
async def health(): async def health():
return {{"status": "healthy", "service": SERVER_NAME}} return {{"status": "healthy", "service": SERVER_NAME, "tools_count": len(TOOL_MAP)}}
# ==================== MCP 端点 ==================== # ==================== MCP 端点 ====================
@@ -481,8 +657,8 @@ async def health():
sessions: Dict[str, Dict] = {{}} sessions: Dict[str, Dict] = {{}}
async def handle_mcp_request(data: Dict, session_id: str = None, api_key: str = None) -> Dict: async def handle_mcp_request(data: Dict, session_id: str = None, api_key: str = None, user_id: str = None) -> Dict:
"""处理 MCP JSON-RPC 请求""" """处理 MCP JSON-RPC 请求(带回调)"""
method = data.get("method") method = data.get("method")
params = data.get("params", {{}}) params = data.get("params", {{}})
req_id = data.get("id") req_id = data.get("id")
@@ -515,7 +691,20 @@ async def handle_mcp_request(data: Dict, session_id: str = None, api_key: str =
if api_key: if api_key:
os.environ['OPENAI_API_KEY'] = api_key os.environ['OPENAI_API_KEY'] = api_key
# 使用回调上下文管理器(如果有 user_id)
effective_user_id = user_id or USER_ID
try: 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: finally:
if old_key: if old_key:
@@ -543,7 +732,8 @@ async def mcp_endpoint(request: Request):
body = await request.json() body = await request.json()
session_id = request.headers.get("x-mcp-session-id") session_id = request.headers.get("x-mcp-session-id")
api_key = get_api_key_from_request(request) api_key = get_api_key_from_request(request)
response = await handle_mcp_request(body, session_id, api_key) user_id = request.headers.get("x-user-id") or USER_ID
response = await handle_mcp_request(body, session_id, api_key, user_id)
return JSONResponse(content=response, headers={{"x-mcp-session-id": session_id or ""}}) return JSONResponse(content=response, headers={{"x-mcp-session-id": session_id or ""}})
except Exception as e: except Exception as e:
return JSONResponse(status_code=400, content={{"jsonrpc": "2.0", "error": {{"code": -32700, "message": str(e)}}}}) return JSONResponse(status_code=400, content={{"jsonrpc": "2.0", "error": {{"code": -32700, "message": str(e)}}}})
@@ -572,9 +762,10 @@ async def mcp_sse_post(request: Request):
body = await request.json() body = await request.json()
session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4()) session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4())
api_key = get_api_key_from_request(request) 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]: async def stream() -> AsyncGenerator[str, None]:
response = await handle_mcp_request(body, session_id, api_key) response = await handle_mcp_request(body, session_id, api_key, user_id)
yield f"data: {{json.dumps(response)}}\\n\\n" yield f"data: {{json.dumps(response)}}\\n\\n"
return StreamingResponse(stream(), media_type="text/event-stream", return StreamingResponse(stream(), media_type="text/event-stream",
@@ -589,6 +780,13 @@ class ToolCallRequest(BaseModel):
"""工具调用请求""" """工具调用请求"""
tool_name: str = Field(..., description="工具名称") tool_name: str = Field(..., description="工具名称")
parameters: Dict[str, Any] = Field(default={{}}, 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): class ToolCallResponse(BaseModel):
@@ -596,6 +794,15 @@ class ToolCallResponse(BaseModel):
success: bool success: bool
result: Optional[Any] = None result: Optional[Any] = None
error: Optional[str] = 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") @app.get("/tools")
@@ -605,28 +812,117 @@ async def list_tools():
"tools": [ "tools": [
{{"name": t["name"], "description": t["description"]}} {{"name": t["name"], "description": t["description"]}}
for t in TOOL_LIST for t in TOOL_LIST
] ],
"count": len(TOOL_LIST)
}} }}
@app.post("/tools/call", response_model=ToolCallResponse) @app.post("/tools/call", response_model=ToolCallResponse)
async def call_tool(request: ToolCallRequest, api_key: str = Depends(verify_api_key)): async def call_tool(request: ToolCallRequest, api_key: str = Depends(verify_api_key)):
"""调用工具""" """调用单个工具(带计费回调)"""
if request.tool_name not in TOOL_MAP: if request.tool_name not in TOOL_MAP:
raise HTTPException(status_code=404, detail=f"工具 {{request.tool_name}} 不存在") 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: try:
old_key = os.environ.get('OPENAI_API_KEY') old_key = os.environ.get('OPENAI_API_KEY')
os.environ['OPENAI_API_KEY'] = api_key os.environ['OPENAI_API_KEY'] = api_key
try: 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) result = await TOOL_MAP[request.tool_name](**request.parameters)
return ToolCallResponse(success=True, result=json.loads(result) if isinstance(result, str) else result) 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
)
finally: finally:
if old_key: if old_key:
os.environ['OPENAI_API_KEY'] = old_key os.environ['OPENAI_API_KEY'] = old_key
except Exception as e: except Exception as e:
return ToolCallResponse(success=False, error=str(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)
)
if __name__ == '__main__': if __name__ == '__main__':
@@ -701,11 +997,221 @@ uvicorn[standard]>=0.27.0
# HTTP Client # HTTP Client
httpx>=0.25.0 httpx>=0.25.0
aiohttp>=3.9.0 aiohttp>=3.9.0
requests>=2.31.0
# 其他 # 其他
python-dotenv>=1.0.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) -> str: def generate_gitea_action(self, agent_name: str, auto_deploy: bool = True) -> str:
""" """
生成 Gitea Actions CI/CD 配置 生成 Gitea Actions CI/CD 配置
@@ -1068,11 +1574,12 @@ MIT License
""" """
生成完整的 Agent 项目文件 生成完整的 Agent 项目文件
基于 agent_templates/agents/_template 框架 基于 agent_templates/agents/_template 框架
包含回调功能用于计费
Args: Args:
agent_name: Agent 名称 agent_name: Agent 名称
description: Agent 描述 description: Agent 描述
tools_config: 工具配置列表 tools_config: 工具配置列表(支持多个工具)
auto_deploy: 是否自动部署到 AKS auto_deploy: 是否自动部署到 AKS
Returns: Returns:
@@ -1087,8 +1594,13 @@ MIT License
agent_name, description, tools_config agent_name, description, tools_config
) )
# 生成 src/server/api_server.py # 生成 src/server/api_server.py(传递 tools_config 用于回调)
files["src/server/api_server.py"] = self.generate_api_server(agent_name, description) 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/__init__.py # 生成 src/server/__init__.py
files["src/server/__init__.py"] = '"""服务器模块"""\n' files["src/server/__init__.py"] = '"""服务器模块"""\n'
@@ -1111,6 +1623,12 @@ MIT License
# 生成 README # 生成 README
files["README.md"] = self.generate_readme(agent_name, description, tools_config, auto_deploy) 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 return files
def get_cicd_secrets_info(self) -> Dict[str, str]: def get_cicd_secrets_info(self) -> Dict[str, str]:
+41
View File
@@ -16,6 +16,8 @@ from database import (
) )
from template_manager import template_manager from template_manager import template_manager
from tool_generator_api import router as tool_generator_router 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 import os
# 配置日志 # 配置日志
@@ -32,6 +34,9 @@ app = FastAPI(
# 注册动态工具生成 Router # 注册动态工具生成 Router
app.include_router(tool_generator_router) app.include_router(tool_generator_router)
# 注册外部工具 API Router(符合 MCP-Server 规范)
app.include_router(external_tool_router)
# 初始化K8s管理器 # 初始化K8s管理器
NAMESPACE = os.getenv("NAMESPACE", "ai-agents") NAMESPACE = os.getenv("NAMESPACE", "ai-agents")
KUBECONFIG_PATH = os.getenv("KUBECONFIG_PATH", None) # 可选:指定kubeconfig路径 KUBECONFIG_PATH = os.getenv("KUBECONFIG_PATH", None) # 可选:指定kubeconfig路径
@@ -207,6 +212,7 @@ class CreateAgentRequest(BaseModel):
config: Dict = Field(default_factory=dict, description="配置信息") config: Dict = Field(default_factory=dict, description="配置信息")
env: Optional[Dict[str, str]] = Field(default_factory=dict, description="环境变量") env: Optional[Dict[str, str]] = Field(default_factory=dict, description="环境变量")
namespace: Optional[str] = Field(default=None, description="Kubernetes命名空间,默认使用环境变量NAMESPACE的值") namespace: Optional[str] = Field(default=None, description="Kubernetes命名空间,默认使用环境变量NAMESPACE的值")
tool_refs: Optional[List[str]] = Field(default=None, description="外部数据工具标识列表(符合MCP-Server规范)")
class AgentResponse(BaseModel): class AgentResponse(BaseModel):
@@ -224,6 +230,7 @@ class AgentResponse(BaseModel):
host_ip: Optional[str] = None host_ip: Optional[str] = None
node_name: Optional[str] = None node_name: Optional[str] = None
owner_info: Optional[Dict] = None owner_info: Optional[Dict] = None
tools_attached: Optional[int] = Field(default=0, description="附加的外部工具数量")
class ResourceUsage(BaseModel): class ResourceUsage(BaseModel):
@@ -328,6 +335,36 @@ async def create_agent(request: CreateAgentRequest, db: Session = Depends(get_db
config_data["env"] = request.env config_data["env"] = request.env
logger.info(f"环境变量: {list(request.env.keys())}") 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 标签
user_id = config_data.get("user_id", "default") user_id = config_data.get("user_id", "default")
if "labels" not in config_data: if "labels" not in config_data:
@@ -516,6 +553,10 @@ async def create_agent(request: CreateAgentRequest, db: Session = Depends(get_db
if "status" not in result: if "status" not in result:
result["status"] = "Pending" result["status"] = "Pending"
# 添加外部工具信息
if attached_tools:
result["tools_attached"] = len(attached_tools)
try: try:
return AgentResponse(**result) return AgentResponse(**result)
except Exception as validation_error: except Exception as validation_error:
+223
View File
@@ -0,0 +1,223 @@
# Agent Manager DNS问题修复报告
## 问题描述
在K8s上部署的Agent Manager服务,创建新agent后没有按照预期返回域名和外网地址。
## 问题诊断
### 1. 症状
- 创建agent时返回外网IP地址(`external_ip`)
- 但没有返回DNS域名信息(`domain`, `domain_url`)
- 推荐访问地址使用的是IP地址而非域名
### 2. 根本原因
通过查看agent-manager日志发现:
```
ERROR:k8s_manager:创建 DNS 记录失败: 403 Client Error: Forbidden
ERROR:k8s_manager:错误详情: {"error":{"code":"AuthorizationFailed",
"message":"The client 'c5ba26db-f180-425f-bac3-93708d853988' with object id
'9dd70c60-bca4-478f-8cac-9faf9f171c93' does not have authorization to perform
action 'Microsoft.Network/dnsZones/A/write' over scope
'/subscriptions/45d7a360-af09-40fc-9afc-56dc475245ec/resourceGroups/taiji-ai-v0/
providers/Microsoft.Network/dnsZones/taijiagnet.com/A/...' or the scope is invalid."}}
```
**核心问题:** Azure Service Principal缺少DNS Zone的写权限。
## 解决方案
### 步骤1:为Service Principal分配DNS Zone Contributor角色
```bash
# Azure配置
AZURE_CLIENT_ID="c5ba26db-f180-425f-bac3-93708d853988"
AZURE_SUBSCRIPTION_ID="45d7a360-af09-40fc-9afc-56dc475245ec"
AZURE_RESOURCE_GROUP="taiji-ai-v0"
AZURE_DNS_ZONE="taijiagnet.com"
# 构建DNS Zone资源ID
DNS_ZONE_ID="/subscriptions/$AZURE_SUBSCRIPTION_ID/resourceGroups/$AZURE_RESOURCE_GROUP/providers/Microsoft.Network/dnsZones/$AZURE_DNS_ZONE"
# 分配DNS Zone Contributor角色
az role assignment create \
--assignee $AZURE_CLIENT_ID \
--role "DNS Zone Contributor" \
--scope $DNS_ZONE_ID
```
### 步骤2:验证权限分配
```bash
# 验证角色分配
az role assignment list \
--assignee $AZURE_CLIENT_ID \
--scope $DNS_ZONE_ID \
--output table
```
输出应显示:
```
Principal Role Scope
------------------------------------ -------------------- --------
c5ba26db-f180-425f-bac3-93708d853988 DNS Zone Contributor /subscriptions/.../dnsZones/taijiagnet.com
```
## 修复验证
### 测试1:创建新agent
```bash
curl -X POST http://20.212.121.126/agents \
-H "Content-Type: application/json" \
-d '{
"name": "test-dns-fixed",
"template": "echo_agent",
"framework": "API",
"config": {"user_id": "test-user"}
}'
```
### 结果(修复前)
```json
{
"access_info": {
"external_ip": "20.212.128.51",
"ip_url": "http://20.212.128.51:80",
"recommended": "http://20.212.128.51:80"
}
}
```
### 结果(修复后)✅
```json
{
"access_info": {
"external_ip": "20.198.137.237",
"ip_url": "http://20.198.137.237:80",
"service_url": "http://10.0.48.107:80",
"pod_url": "http://10.224.0.67:8000",
"domain": "test-dns-fixed.taijiagnet.com",
"domain_url": "http://test-dns-fixed.taijiagnet.com",
"recommended": "http://test-dns-fixed.taijiagnet.com"
}
}
```
### 测试2:验证DNS记录
```bash
# 查看DNS记录
az network dns record-set a show \
--resource-group taiji-ai-v0 \
--zone-name taijiagnet.com \
--name test-dns-fixed
# DNS解析测试
nslookup test-dns-fixed.taijiagnet.com
# Output: Address: 20.198.137.237
# 域名访问测试
curl http://test-dns-fixed.taijiagnet.com/
# Output: {"status":"healthy",...}
```
### 测试3:查询agent状态
```bash
curl http://20.212.121.126/agents/test-dns-fixed/status | jq .access_info
```
结果:
```json
{
"external_ip": "20.198.137.237",
"ip_url": "http://20.198.137.237:80",
"domain": "test-dns-fixed.taijiagnet.com",
"domain_url": "http://test-dns-fixed.taijiagnet.com",
"recommended_url": "http://test-dns-fixed.taijiagnet.com",
"service_name": "test-dns-fixed-service"
}
```
## Agent Manager日志(修复后)
```
INFO:app:收到创建Agent请求: test-dns-fixed, 模板: echo_agent
INFO:k8s_manager:✅ 创建命名空间 agent-test-dns-fixed
INFO:k8s_manager:✅ LoadBalancer Service test-dns-fixed-service 创建成功
INFO:k8s_manager:✅ LoadBalancer IP 已分配: 20.198.137.237
INFO:app:✅ LoadBalancer 外网 IP: 20.198.137.237
INFO:app:创建 DNS 记录: test-dns-fixed.taijiagnet.com
INFO:k8s_manager:✅ DNS 记录创建成功: test-dns-fixed.taijiagnet.com -> 20.198.137.237
INFO:app:✅ DNS 记录: test-dns-fixed.taijiagnet.com -> 20.198.137.237
INFO:app: - 推荐访问: http://test-dns-fixed.taijiagnet.com
INFO:app:✅ Agent创建成功!
INFO:app:✅ Agent信息已保存到数据库
```
## 涉及的文件和组件
1. **Azure Service Principal** (c5ba26db-f180-425f-bac3-93708d853988)
- 需要DNS Zone Contributor权限
2. **k8s_manager.py** - DNS记录创建逻辑
- `create_dns_record()` - 使用Azure DNS API创建A记录
- `wait_for_loadbalancer_ip()` - 等待LoadBalancer IP分配
3. **app.py** - Agent创建流程
- 在创建LoadBalancer Service后自动创建DNS记录
- 将域名信息保存到数据库
4. **database.py** - Agent模型
- 存储`domain`, `domain_url`, `recommended_url`等字段
## 修复完成清单 ✅
- [x] 诊断DNS创建失败原因(Azure权限不足)
- [x] 为Service Principal分配DNS Zone Contributor角色
- [x] 测试DNS记录创建功能
- [x] 验证agent创建返回完整的域名信息
- [x] 验证agent状态查询返回完整的访问信息
- [x] 验证DNS解析和域名访问正常
- [x] 清理测试资源
## 后续建议
### 1. 文档更新
更新部署文档,明确说明Service Principal需要的Azure权限:
- AKS Cluster的Contributor角色
- DNS Zone的DNS Zone Contributor角色
### 2. 自动化部署脚本
在`k8s/deploy.sh`中添加权限检查和自动分配逻辑:
```bash
# 检查并分配DNS Zone权限
check_dns_permissions() {
echo "检查DNS Zone权限..."
# 实现权限检查逻辑
}
```
### 3. 监控和告警
添加DNS创建失败的监控和告警机制:
- 记录DNS创建失败次数
- 当失败超过阈值时发送告警
### 4. 重试机制
在`k8s_manager.py`中为DNS记录创建添加重试逻辑:
```python
def create_dns_record_with_retry(self, subdomain, ip_address, max_retries=3):
for attempt in range(max_retries):
try:
return self.create_dns_record(subdomain, ip_address)
except Exception as e:
if attempt < max_retries - 1:
logger.warning(f"DNS创建失败,重试 {attempt + 1}/{max_retries}")
time.sleep(5)
else:
raise
```
## 修复日期
2026-01-28
## 修复人员
AI Assistant (Claude Sonnet 4.5)
+202 -419
View File
@@ -1,7 +1,7 @@
# 动态 Agent 生成器 API 文档 # 动态 Agent 生成器 API 文档
> **版本**: 2026-01-26 v1.0 > **版本**: 2026-01-29 v1.1
> **服务地址**: http://57.158.136.164 > **服务地址**: http://20.212.121.126
> **模板参考**: [cicd-AKS](http://gitee.ath.cx:3000/xiaohei/cicd-AKS) > **模板参考**: [cicd-AKS](http://gitee.ath.cx:3000/xiaohei/cicd-AKS)
--- ---
@@ -29,6 +29,12 @@
│ │ AKS │ │ │ │ AKS │ │
│ │ (部署) │ │ │ │ (部署) │ │
│ └──────────┘ │ │ └──────────┘ │
│ │ │
│ ↓ │
│ ┌──────────┐ │
│ │Azure DNS │ │
│ │ (域名) │ │
│ └──────────┘ │
│ │ │ │
└─────────────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────────────┘
``` ```
@@ -41,33 +47,40 @@
| 配置项 | 值 | | 配置项 | 值 |
|--------|-----| |--------|-----|
| 终结点 | `https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1` | | 终结点 | \`https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1\` |
| 模型 | `taiji/claude-sonnet-4-5` | | 模型 | \`taiji/claude-sonnet-4-5\` |
| API Key | `sk-mPV5MVVVVvfGSkXA-ASQXQ` | | API Key | \`sk-mPV5MVVVVvfGSkXA-ASQXQ\` |
### Gitee 配置 ### Gitee 配置
| 配置项 | 值 | | 配置项 | 值 |
|--------|-----| |--------|-----|
| API URL | `http://gitee.ath.cx:3000/api/v1` | | API URL | \`http://gitee.ath.cx:3000/api/v1\` |
| 用户名 | `zhanggangyong` | | 用户名 | \`zhanggangyong\` |
| Owner | `xiaohei` | | Owner | \`xiaohei\` |
| 模板仓库 | [cicd-AKS](http://gitee.ath.cx:3000/xiaohei/cicd-AKS) | | 模板仓库 | [cicd-AKS](http://gitee.ath.cx:3000/xiaohei/cicd-AKS) |
### ACR 配置 ### ACR 配置
| 配置项 | 值 | | 配置项 | 值 |
|--------|-----| |--------|-----|
| Registry | `agnettaiji.azurecr.io` | | Registry | \`agnettaiji.azurecr.io\` |
| Namespace | `ai-agents` | | Namespace | \`ai-agents\` |
### Azure DNS 配置
| 配置项 | 值 |
|--------|-----|
| DNS Zone | \`taijiagnet.com\` |
| 资源组 | \`taiji-ai-v0\` |
### WAF 白名单 (重要!) ### WAF 白名单 (重要!)
Gitee 服务器使用雷池 WAF 保护,需要将以下 IP 添加到白名单: Gitee 服务器使用雷池 WAF 保护,需要将以下 IP 添加到白名单:
``` \`\`\`
4.144.251.116 (AKS 集群出口 IP) 4.144.251.116 (AKS 集群出口 IP)
``` \`\`\`
--- ---
@@ -75,15 +88,15 @@ Gitee 服务器使用雷池 WAF 保护,需要将以下 IP 添加到白名单
| 序号 | 接口 | 方法 | 说明 | | 序号 | 接口 | 方法 | 说明 |
|------|------|------|------| |------|------|------|------|
| 1 | `/tools/generate` | POST | 生成单个外部数据工具 | | 1 | \`/tools/generate\` | POST | 生成单个外部数据工具 |
| 2 | `/tools/generate-agent` | POST | 生成完整 Agent 并推送到 Gitee | | 2 | \`/tools/generate-agent\` | POST | 生成完整 Agent 并推送到 Gitee |
| 3 | `/tools/{tool_ref_id}` | GET | 获取工具/Agent 信息 | | 3 | \`/tools/{tool_ref_id}\` | GET | 获取工具/Agent 信息 |
| 4 | `/tools/{tool_ref_id}` | PUT | 更新工具配置 | | 4 | \`/tools/{tool_ref_id}\` | PUT | 更新工具配置 |
| 5 | `/tools/{tool_ref_id}` | DELETE | 删除工具 | | 5 | \`/tools/{tool_ref_id}\` | DELETE | 删除工具 |
| 6 | `/tools/{tool_ref_id}/test` | POST | 测试工具连接 | | 6 | \`/tools/{tool_ref_id}/test\` | POST | 测试工具连接 |
| 7 | `/tools/{tool_ref_id}/build-status` | GET | 查询构建状态 | | 7 | \`/tools/{tool_ref_id}/build-status\` | GET | 查询构建状态 |
| 8 | `/tools/{tool_ref_id}/deploy` | POST | 部署 Agent | | 8 | \`/tools/{tool_ref_id}/deploy\` | POST | 部署 Agent |
| 9 | `/tools/` | GET | 列出所有工具/Agent | | 9 | \`/tools/\` | GET | 列出所有工具/Agent |
--- ---
@@ -91,9 +104,9 @@ Gitee 服务器使用雷池 WAF 保护,需要将以下 IP 添加到白名单
### 接口 ### 接口
``` \`\`\`
POST /tools/generate POST /tools/generate
``` \`\`\`
### 请求参数 ### 请求参数
@@ -114,39 +127,39 @@ POST /tools/generate
#### API Key 认证 #### API Key 认证
```json \`\`\`json
{ {
"type": "api_key", "type": "api_key",
"key": "sk-xxxxxxxxxxxx", "key": "sk-xxxxxxxxxxxx",
"in": "header", "in": "header",
"name": "X-API-Key" "name": "X-API-Key"
} }
``` \`\`\`
#### Bearer Token 认证 #### Bearer Token 认证
```json \`\`\`json
{ {
"type": "bearer", "type": "bearer",
"key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." "key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
} }
``` \`\`\`
#### Basic Auth 认证 #### Basic Auth 认证
```json \`\`\`json
{ {
"type": "basic", "type": "basic",
"username": "admin", "username": "admin",
"password": "password123" "password": "password123"
} }
``` \`\`\`
### 请求示例 ### 请求示例
```bash \`\`\`bash
curl -X POST http://57.158.136.164/tools/generate \ curl -X POST http://20.212.121.126/tools/generate \\
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \\
-d '{ -d '{
"name": "weather-query", "name": "weather-query",
"description": "查询城市天气", "description": "查询城市天气",
@@ -170,22 +183,78 @@ curl -X POST http://57.158.136.164/tools/generate \
} }
} }
}' }'
``` \`\`\`
### 响应示例 ### 响应示例
```json \`\`\`json
{ {
"success": true, "success": true,
"data": { "data": {
"tool_ref_id": "tool-weather-query-a49e00fe", "tool_ref_id": "tool-weather-query-a49e00fe",
"name": "weather-query", "name": "weather-query",
"description": "查询城市天气", "description": "查询城市天气",
"created_at": "2026-01-26T10:00:00.000000" "created_at": "2026-01-29T10:00:00.000000"
}, },
"message": "工具生成成功" "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)
\`\`\`
--- ---
@@ -193,9 +262,9 @@ curl -X POST http://57.158.136.164/tools/generate \
### 接口 ### 接口
``` \`\`\`
POST /tools/generate-agent POST /tools/generate-agent
``` \`\`\`
### 功能描述 ### 功能描述
@@ -203,6 +272,7 @@ POST /tools/generate-agent
2. 创建 Gitee 仓库并推送代码 2. 创建 Gitee 仓库并推送代码
3. 包含完整 CI/CD 配置(参考 [cicd-AKS](http://gitee.ath.cx:3000/xiaohei/cicd-AKS)) 3. 包含完整 CI/CD 配置(参考 [cicd-AKS](http://gitee.ath.cx:3000/xiaohei/cicd-AKS))
4. 自动触发构建和部署 4. 自动触发构建和部署
5. 自动创建 Azure DNS 记录
### 请求参数 ### 请求参数
@@ -213,13 +283,13 @@ POST /tools/generate-agent
| tools | array | ✅ | 工具配置列表 | | tools | array | ✅ | 工具配置列表 |
| user_id | string | ✅ | 用户 ID | | user_id | string | ✅ | 用户 ID |
| tenant_id | string | ❌ | 租户 ID | | tenant_id | string | ❌ | 租户 ID |
| auto_deploy | boolean | ❌ | 是否自动部署到 AKS(默认 true) | | auto_deploy | boolean | ❌ | 是否自动部署到 AKS(默认 false) |
### 请求示例 ### 请求示例
```bash \`\`\`bash
curl -X POST http://57.158.136.164/tools/generate-agent \ curl -X POST http://20.212.121.126/tools/generate-agent \\
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \\
-d '{ -d '{
"agent_name": "weather-agent", "agent_name": "weather-agent",
"description": "天气查询智能 Agent,支持获取全球城市天气信息", "description": "天气查询智能 Agent,支持获取全球城市天气信息",
@@ -254,31 +324,14 @@ curl -X POST http://57.158.136.164/tools/generate-agent \
} }
}, },
"timeout": 30 "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, "success": true,
"data": { "data": {
@@ -287,256 +340,78 @@ curl -X POST http://57.158.136.164/tools/generate-agent \
"repo_url": "http://gitee.ath.cx:3000/zhanggangyong/agent-weather-agent-ac3c11", "repo_url": "http://gitee.ath.cx:3000/zhanggangyong/agent-weather-agent-ac3c11",
"image_name": "agnettaiji.azurecr.io/ai-agents/agent-weather-agent-ac3c11:latest", "image_name": "agnettaiji.azurecr.io/ai-agents/agent-weather-agent-ac3c11:latest",
"status": "building", "status": "building",
"files_pushed": 7, "files_pushed": 9,
"tools_count": 2 "tools_count": 2,
"expected_domain": "agent-weather-agent-ac3c11.taijiagnet.com",
"expected_namespace": "agent-agent-weather-agent-ac3c11"
}, },
"message": "Agent 项目已创建并推送到 Gitee,CI/CD 正在构建中" "message": "Agent 项目已创建并推送到 Gitee,CI/CD 正在构建中。部署后访问: http://agent-weather-agent-ac3c11.taijiagnet.com"
} }
``` \`\`\`
### 生成的项目结构 ### 生成的项目结构
``` \`\`\`
agent-weather-agent-{id}/ agent-weather-agent-{id}/
├── .gitea/workflows/ ├── .gitea/workflows/
│ └── ci-cd.yaml # CI/CD 配置(构建+部署) │ └── ci-cd.yaml # CI/CD 配置(构建+部署+DNS)
├── tools/ ├── src/
│ ├── __init__.py │ ├── __init__.py
│ ├── get_weather.py # 工具1: 天气查询 │ └── server/
│ └── get_forecast.py # 工具2: 天气预报 │ ├── __init__.py
├── main.py # FastAPI 主程序 │ ├── mcp_server.py # MCP 服务器 + 工具定义
│ └── api_server.py # FastAPI HTTP 服务器
├── run_api_server.py # 入口文件
├── Dockerfile # Docker 构建文件 ├── Dockerfile # Docker 构建文件
├── requirements.txt # Python 依赖 ├── requirements.txt # Python 依赖
└── README.md # 说明文档 └── README.md # 说明文档
``` \`\`\`
--- ---
## 3. 获取工具/Agent 信息 ## 3-9. 其他接口
### 接口 ### 3. 获取工具/Agent 信息
``` \`\`\`
GET /tools/{tool_ref_id} 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} 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 DELETE /tools/{tool_ref_id}?delete_repo=false
``` \`\`\`
### 参数 ### 6. 测试工具连接
| 参数 | 类型 | 说明 | \`\`\`
|------|------|------|
| delete_repo | boolean | 是否同时删除 Gitee 仓库 |
---
## 6. 测试工具连接
### 接口
```
POST /tools/{tool_ref_id}/test 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 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 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 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
}
}
```
--- ---
@@ -557,10 +432,9 @@ GET /tools/?user_id=user-001
### 步骤 1: 创建 Agent ### 步骤 1: 创建 Agent
```bash \`\`\`bash
# 发送工具定义,创建 Agent 项目 curl -X POST http://20.212.121.126/tools/generate-agent \\
curl -X POST http://57.158.136.164/tools/generate-agent \ -H "Content-Type: application/json" \\
-H "Content-Type: application/json" \
-d '{ -d '{
"agent_name": "my-custom-agent", "agent_name": "my-custom-agent",
"description": "我的自定义 Agent", "description": "我的自定义 Agent",
@@ -568,165 +442,73 @@ curl -X POST http://57.158.136.164/tools/generate-agent \
"auto_deploy": true, "auto_deploy": true,
"tools": [...] "tools": [...]
}' }'
\`\`\`
# 返回 agent_ref_id
```
### 步骤 2: 等待构建完成 ### 步骤 2: 等待构建完成
```bash \`\`\`bash
# 轮询查询构建状态 curl http://20.212.121.126/tools/{agent_ref_id}/build-status
curl http://57.158.136.164/tools/{agent_ref_id}/build-status \`\`\`
# 等待 overall_status 变为 "ready" ### 步骤 3: 访问 Agent
```
### 步骤 3: 部署 Agent(如果 auto_deploy=false) \`\`\`bash
# 通过域名访问
curl http://{agent-name}.taijiagnet.com/
```bash # 健康检查
# 手动部署 curl http://{agent-name}.taijiagnet.com/health
curl -X POST http://57.158.136.164/tools/{agent_ref_id}/deploy
```
### 步骤 4: 创建 Agent 实例 # 调用工具
curl -X POST http://{agent-name}.taijiagnet.com/tools/call \\
-H "Content-Type: application/json" \\
-d '{"tool_name": "get_weather", "parameters": {"q": "Beijing"}}'
```bash # MCP 调用
# 使用生成的模板创建实例 curl -X POST http://{agent-name}.taijiagnet.com/mcp \\
curl -X POST http://57.158.136.164/agents \ -H "Content-Type: application/json" \\
-H "Content-Type: application/json" \ -d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'
-d '{ \`\`\`
"name": "my-instance",
"template": "{template_name}",
"config": {}
}'
```
--- ---
## CI/CD 配置说明 ## Agent 端点说明
生成的 CI/CD 配置参考 [cicd-AKS](http://gitee.ath.cx:3000/xiaohei/cicd-AKS),包含: 生成的 Agent 提供以下端点:
1. **构建 Docker 镜像** (ARM64 架构) ### REST API
2. **推送到 ACR**
3. **部署到 AKS**(如果 `auto_deploy=true`)
### 生成的 CI/CD 配置示例 | 端点 | 方法 | 说明 |
|------|------|------|
| \`/\` | GET | 服务信息 |
| \`/health\` | GET | 健康检查 |
| \`/tools\` | GET | 列出可用工具 |
| \`/tools/call\` | POST | 调用工具 |
```yaml ### MCP 端点
name: CI-CD-AKS
on: | 端点 | 方法 | 说明 |
push: |------|------|------|
branches: [ main ] | \`/mcp\` | POST | MCP HTTP 端点 |
| \`/mcp/sse\` | GET | MCP SSE 端点 |
jobs: | \`/mcp/sse\` | POST | MCP SSE POST 端点 |
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 配置
Agent Manager 需要以下环境变量: | Secret 名称 | 说明 |
|-------------|------|
### ConfigMap (agent-manager-config) | \`ACR_LOGIN_SERVER\` | ACR 登录服务器 |
| \`ACR_USERNAME\` | ACR 用户名 |
```yaml | \`ACR_PASSWORD\` | ACR 密码 |
GITEE_API_URL: "http://gitee.ath.cx:3000/api/v1" | \`AZ_CLIENT_ID\` | Azure 服务主体 Client ID |
GITEE_BASE_URL: "http://gitee.ath.cx:3000" | \`AZ_CLIENT_SECRET\` | Azure 服务主体密钥 |
GITEE_OWNER: "xiaohei" | \`AZ_TENANT_ID\` | Azure 租户 ID |
GITEE_TEMPLATE_REPO: "cicd-AKS" | \`AZ_SUBSCRIPTION_ID\` | Azure 订阅 ID |
ACR_REGISTRY: "agnettaiji.azurecr.io" | \`AZ_RG\` | AKS 资源组名称 |
ACR_NAMESPACE: "ai-agents" | \`AZ_AKS\` | AKS 集群名称 |
LLM_BASE_URL: "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1" | \`AZURE_DNS_ZONE\` | DNS Zone 名称 |
LLM_MODEL: "taiji/claude-sonnet-4-5"
```
### Secret (agent-manager-secret)
```yaml
GITEE_TOKEN: <base64 encoded token>
GITEE_USERNAME: <base64 encoded: zhanggangyong>
GITEE_PASSWORD: <base64 encoded password>
LLM_API_KEY: <base64 encoded: sk-mPV5MVVVVvfGSkXA-ASQXQ>
```
--- ---
@@ -735,3 +517,4 @@ LLM_API_KEY: <base64 encoded: sk-mPV5MVVVVvfGSkXA-ASQXQ>
| 日期 | 版本 | 更新内容 | | 日期 | 版本 | 更新内容 |
|------|------|----------| |------|------|----------|
| 2026-01-26 | v1.0 | 初始版本 | | 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 配置表格 |
+741
View File
@@ -0,0 +1,741 @@
# 外部工具 API 文档
> **版本**: 2026-01-29 v2.0
> **服务地址**: http://20.212.121.126
> **规范参考**: [Agent-Manager外部工具接口规范](http://gitee.ath.cx:3000/xiaohei/taiji-AI-PAD/src/branch/feature/chenchen/Docs/Agent-Manager%E5%A4%96%E9%83%A8%E5%B7%A5%E5%85%B7%E6%8E%A5%E5%8F%A3%E8%A7%84%E8%8C%83.md)
---
## 📊 系统架构
```
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 系统交互流程 │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 前端 │ ───→ │ MCP-Server │ ───→ │ Agent Manager │ │
│ │ 用户界面 │ │ (调用方) │ │ (本API规范) │ │
│ └─────────────┘ └─────────────────┘ └─────────────────┘ │
│ │ │ │
│ ↓ ↓ │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ PostgreSQL │ │ 工具文件存储 │ │
│ │ (基本信息) │ │ AKS 部署 │ │
│ └─────────────┘ └─────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ 计费回调系统 │ │
│ │ (运行时长记录) │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘
```
### 职责划分
| 组件 | 职责 |
|------|------|
| **MCP-Server** | 接收前端请求、存储工具基本信息和 tool_ref_id、调用 Agent Manager 接口 |
| **Agent Manager** | 生成 Pydantic 工具代码文件、存储完整配置(含敏感信息)、部署 Agent 到 AKS、集成计费回调 |
| **计费系统** | 接收 Agent 运行时长回调、记录使用的工具、计算费用 |
---
## 🆕 v2.0 新增功能
### 1. 回调功能(计费)
每个生成的 Agent 自动包含回调模块,用于向计费系统发送使用记录:
- 记录 Agent 运行时长
- 记录使用的工具列表
- 支持请求级别的计费追踪
### 2. 多工具支持
每个 Agent 可以包含**多个工具**,在创建时通过 `tool_refs` 数组指定。
### 3. 新增状态查询 API
- `GET /agents/{agent_ref_id}/build-status` - 查询 CI/CD 构建状态
- `GET /agents/{agent_ref_id}/deployment-info` - 查询部署详情
### 4. 批量工具调用
- `POST /tools/batch-call` - 批量调用多个工具(统一计费)
---
## 🔐 通用规范
### 基础路径
```
http://20.212.121.126/external-tools
```
### 请求头
```http
Content-Type: application/json
X-User-ID: <user_id> # 可选,用于计费
```
### 响应格式
#### 成功响应
```json
{
"success": true,
"data": { ... },
"message": "操作成功"
}
```
#### 错误响应
```json
{
"success": false,
"error": "error_code",
"message": "错误描述"
}
```
---
## 📑 接口列表
| 序号 | 接口 | 方法 | 说明 |
|------|------|------|------|
| 1 | `/external-tools/generate` | POST | 生成外部数据工具 |
| 2 | `/external-tools/{tool_ref_id}` | GET | 获取工具详情 |
| 3 | `/external-tools/{tool_ref_id}` | PUT | 更新外部数据工具 |
| 4 | `/external-tools/{tool_ref_id}` | DELETE | 删除外部数据工具 |
| 5 | `/external-tools/{tool_ref_id}/test` | POST | 测试工具连接 |
| 6 | `/external-tools/{tool_ref_id}/code` | GET | 获取生成的代码 |
| 7 | `/external-tools/` | GET | 列出所有工具 |
| 8 | `/external-tools/agents/create-with-tools` | POST | 创建带工具的 Agent |
| 9 | `/external-tools/agents/{agent_ref_id}/build-status` | GET | 🆕 查询构建状态 |
| 10 | `/external-tools/agents/{agent_ref_id}/deployment-info` | GET | 🆕 查询部署信息 |
| 11 | `/agents` | POST | 创建 Agent(支持 tool_refs 字段) |
---
## 1️⃣ 生成外部数据工具
### 接口
```
POST /external-tools/generate
```
### 功能描述
MCP-Server 将用户配置的工具信息发送给 Agent Manager,Agent Manager 需要:
1. 验证配置格式
2. 根据配置生成 Pydantic AI 工具代码文件
3. 存储工具代码文件和完整配置(包含敏感信息如 API Key)
4. 返回唯一的 `tool_ref_id` 供后续引用
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | ✅ | 工具名称(1-100 字符,将用于生成 Python 函数名) |
| description | string | ✅ | 工具描述(将作为工具的 docstring) |
| url | string | ✅ | API 端点 URL |
| method | string | ✅ | HTTP 方法:GET/POST/PUT/DELETE/PATCH |
| user_id | string | ✅ | 用户 ID(UUID 格式) |
| tenant_id | string | ❌ | 租户 ID(UUID 格式) |
| headers | object | ❌ | 自定义请求头 |
| auth | object | ❌ | 认证配置(详见下方) |
| request_params | object | ❌ | URL 查询参数定义(JSON Schema 格式) |
| request_body | object | ❌ | 请求体定义(JSON Schema 格式) |
| response_mapping | object | ❌ | 响应字段映射 |
| timeout | integer | ❌ | 超时时间(秒),默认 30 |
| retry | object | ❌ | 重试配置 |
### 认证配置 (auth) 结构
#### API Key 认证
```json
{
"type": "api_key",
"key": "sk-xxxxxxxxxxxx",
"in": "header", // 位置: header / query
"name": "X-API-Key" // 参数名
}
```
#### Bearer Token 认证
```json
{
"type": "bearer",
"key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
#### Basic Auth 认证
```json
{
"type": "basic",
"username": "admin",
"password": "password123"
}
```
### 请求示例
```bash
curl -X POST http://20.212.121.126/external-tools/generate \
-H "Content-Type: application/json" \
-d '{
"name": "weather-query",
"description": "查询指定城市的天气信息",
"url": "https://api.weather.com/v1/current",
"method": "GET",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"auth": {
"type": "api_key",
"key": "sk-xxx",
"in": "query",
"name": "apikey"
},
"request_params": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称"
}
}
}
}'
```
### 响应示例
```json
{
"success": true,
"data": {
"tool_ref_id": "tool-weather-query-7ff71b5e",
"name": "weather-query",
"description": "查询指定城市的天气信息",
"created_at": "2026-01-29T11:07:32.538533"
},
"message": "工具生成成功"
}
```
---
## 2️⃣ 更新外部数据工具
### 接口
```
PUT /external-tools/{tool_ref_id}
```
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| description | string | ❌ | 工具描述 |
| url | string | ❌ | API URL |
| method | string | ❌ | HTTP 方法 |
| headers | object | ❌ | 自定义请求头 |
| auth | object | ❌ | 认证配置 |
| request_params | object | ❌ | 请求参数定义 |
| request_body | object | ❌ | 请求体定义 |
| timeout | integer | ❌ | 超时时间 |
### 请求示例
```bash
curl -X PUT http://20.212.121.126/external-tools/tool-weather-query-7ff71b5e \
-H "Content-Type: application/json" \
-d '{
"description": "更新后的天气查询工具描述",
"timeout": 60
}'
```
### 响应示例
```json
{
"success": true,
"data": {
"tool_ref_id": "tool-weather-query-7ff71b5e",
"name": "weather-query",
"updated_at": "2026-01-29T11:09:12.322545"
},
"message": "工具更新成功"
}
```
---
## 3️⃣ 删除外部数据工具
### 接口
```
DELETE /external-tools/{tool_ref_id}
```
### 请求示例
```bash
curl -X DELETE http://20.212.121.126/external-tools/tool-weather-query-7ff71b5e
```
### 响应示例
```json
{
"success": true,
"data": {
"tool_ref_id": "tool-weather-query-7ff71b5e"
},
"message": "工具删除成功"
}
```
### 注意事项
- 如果工具正在被 Agent 使用,删除将失败并返回 `tool_in_use` 错误
---
## 4️⃣ 测试工具连接
### 接口
```
POST /external-tools/{tool_ref_id}/test
```
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| test_params | object | ❌ | 测试参数(URL 查询参数) |
| test_body | object | ❌ | 测试请求体 |
### 请求示例
```bash
curl -X POST http://20.212.121.126/external-tools/tool-weather-query-7ff71b5e/test \
-H "Content-Type: application/json" \
-d '{"test_params": {"city": "Beijing"}}'
```
### 响应示例
```json
{
"success": true,
"data": {
"status_code": 200,
"response_time_ms": 1058.84,
"content_type": "application/json",
"response_preview": "{...}"
},
"message": "连接测试成功"
}
```
---
## 5️⃣ 创建带有外部工具的 Agent(支持多工具)
### 接口
```
POST /external-tools/agents/create-with-tools
```
### 功能描述
创建带有外部工具的 Agent,自动:
1. 加载对应的工具代码文件(**支持多个工具**)
2. 将工具集成到 Agent 中
3. 生成完整项目并推送到 Gitee
4. 触发 CI/CD 构建和部署
5. **集成计费回调功能**
### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | ✅ | Agent 名称(1-63 字符,符合 K8s 命名规范) |
| template | string | ✅ | Agent 模板名称 |
| tool_refs | string[] | ✅ | 外部数据工具标识列表(**支持多个**) |
| config | object | ❌ | 资源配置 |
| env | object | ❌ | 环境变量 |
### 请求示例(多工具)
```bash
curl -X POST http://20.212.121.126/external-tools/agents/create-with-tools \
-H "Content-Type: application/json" \
-d '{
"name": "multi-tool-agent",
"template": "custom_agent",
"tool_refs": [
"tool-search-api-60e5c2a1",
"tool-weather-api-8e08be59",
"tool-translate-api-82e0f9bd"
],
"config": {
"user_id": "550e8400-e29b-41d4-a716-446655440000"
}
}'
```
### 响应示例
```json
{
"success": true,
"name": "multi-tool-agent",
"agent_ref_id": "agent-agent-multi-tool-agent-ad2b92",
"namespace": "agent-agent-multi-tool-agent-ad2b92",
"status": "Building",
"created_at": "2026-01-29T11:45:30.123456",
"template": "custom_agent",
"service_port": 8000,
"access_info": {
"domain": "agent-multi-tool-agent-ad2b92.taijiagnet.com",
"domain_url": "http://agent-multi-tool-agent-ad2b92.taijiagnet.com",
"ip_url": "pending"
},
"tools_attached": 3,
"repo_url": "http://gitee.ath.cx:3000/zhanggangyong/agent-multi-tool-agent-ad2b92",
"image_name": "agnettaiji.azurecr.io/ai-agents/agent-multi-tool-agent-ad2b92:latest",
"status_query_url": "/external-tools/agents/agent-agent-multi-tool-agent-ad2b92/build-status",
"deployment_info_url": "/external-tools/agents/agent-agent-multi-tool-agent-ad2b92/deployment-info"
}
```
---
## 6️⃣ 🆕 查询 CI/CD 构建状态
### 接口
```
GET /external-tools/agents/{agent_ref_id}/build-status
```
### 功能描述
查询 Agent 的 CI/CD 构建状态,包括:
- Gitee Action 运行状态
- ACR 镜像是否已构建
- 整体构建状态
### 响应示例
```json
{
"success": true,
"data": {
"agent_ref_id": "agent-agent-multi-tool-agent-ad2b92",
"repo_name": "agent-multi-tool-agent-ad2b92",
"overall_status": "ready",
"gitee_action": {
"status": "completed",
"conclusion": "success",
"run_id": 123,
"html_url": "http://gitee.ath.cx:3000/.../actions/runs/123"
},
"acr_image": {
"image_name": "agnettaiji.azurecr.io/ai-agents/agent-multi-tool-agent-ad2b92:latest",
"exists": true,
"tags": ["latest", "abc123"]
},
"access_info": {
"expected_domain": "agent-multi-tool-agent-ad2b92.taijiagnet.com",
"expected_url": "http://agent-multi-tool-agent-ad2b92.taijiagnet.com",
"expected_namespace": "agent-agent-multi-tool-agent-ad2b92"
}
}
}
```
### 状态说明
| overall_status | 含义 |
|----------------|------|
| `pending` | 等待 CI/CD 开始 |
| `building` | CI/CD 正在构建 |
| `ready` | 镜像已构建,等待部署 |
| `deployed` | 已部署到 K8s |
| `build_failed` | 构建失败 |
| `unknown` | 状态未知 |
---
## 7️⃣ 🆕 查询部署详情
### 接口
```
GET /external-tools/agents/{agent_ref_id}/deployment-info
```
### 功能描述
查询 Agent 部署后的详细信息,包括:
- K8s 部署状态
- Pod 运行状态
- 服务端点信息
- DNS 域名信息
### 响应示例
```json
{
"success": true,
"data": {
"agent_ref_id": "agent-agent-multi-tool-agent-ad2b92",
"repo_name": "agent-multi-tool-agent-ad2b92",
"namespace": "agent-agent-multi-tool-agent-ad2b92",
"k8s_status": {
"namespace_exists": true,
"namespace_status": "Active",
"pod_name": "agent-multi-tool-agent-ad2b92-xxx",
"pod_status": "Running",
"pod_ip": "10.224.0.50",
"node_name": "aks-nodepool1-xxx",
"status": "running"
},
"access_info": {
"service_name": "agent-multi-tool-agent-ad2b92-service",
"cluster_ip": "10.0.100.50",
"external_ip": "20.x.x.x",
"ip_url": "http://20.x.x.x",
"domain": "agent-multi-tool-agent-ad2b92.taijiagnet.com",
"domain_url": "http://agent-multi-tool-agent-ad2b92.taijiagnet.com",
"recommended_url": "http://agent-multi-tool-agent-ad2b92.taijiagnet.com"
},
"image_info": {
"image_name": "agnettaiji.azurecr.io/ai-agents/agent-multi-tool-agent-ad2b92:latest",
"registry": "agnettaiji.azurecr.io"
}
}
}
```
---
## 8️⃣ 原有 /agents 接口 - 支持 tool_refs
### 接口
```
POST /agents
```
### 新增参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| tool_refs | string[] | ❌ | 外部数据工具标识列表(支持多个) |
### 请求示例
```bash
curl -X POST http://20.212.121.126/agents \
-H "Content-Type: application/json" \
-d '{
"name": "test-tools-agent",
"template": "echo_agent",
"framework": "API",
"config": {
"user_id": "test-user"
},
"tool_refs": ["tool-weather-query-7ff71b5e", "tool-stock-api-abc123"]
}'
```
### 响应新增字段
| 字段 | 类型 | 说明 |
|------|------|------|
| tools_attached | integer | 附加的外部工具数量 |
---
## 🔔 回调功能(计费)
### 概述
每个生成的 Agent 自动集成回调功能,用于向计费系统发送使用记录。
### 生成的文件结构
```
src/server/
├── mcp_server.py # MCP 服务器(工具定义)
├── api_server.py # HTTP API(集成回调)
└── agent_callback_utils.py # 回调工具模块
```
### 回调数据格式
Agent 在处理请求时会自动发送以下数据到计费系统:
```json
{
"agentName": "multi-tool-agent",
"userId": "550e8400-e29b-41d4-a716-446655440000",
"podRunningTimeSeconds": 5,
"toolsUsed": ["search-api", "weather-api"],
"startTime": "2026-01-29T11:45:30.000000+00:00",
"endTime": "2026-01-29T11:45:35.000000+00:00",
"requestId": "api-1738151130"
}
```
### 回调触发时机
- 每次 `/tools/call` 调用
- 每次 `/tools/batch-call` 调用
- 每次 MCP `tools/call` 请求
### 传递 User ID
通过以下方式传递 `user_id` 用于计费:
1. **请求参数**:在请求体中添加 `user_id` 字段
2. **请求头**:设置 `X-User-ID` 头
3. **环境变量**:Agent 启动时设置 `USER_ID` 环境变量
---
## ❌ 错误码定义
| HTTP 状态码 | 错误代码 | 说明 |
|------------|----------|------|
| 400 | invalid_config | 配置格式无效 |
| 400 | invalid_url | URL 格式无效 |
| 400 | invalid_method | HTTP 方法无效 |
| 400 | invalid_auth | 认证配置无效 |
| 400 | invalid_schema | JSON Schema 格式无效 |
| 404 | tool_not_found | 工具不存在 |
| 404 | agent_not_found | Agent 不存在 |
| 409 | tool_name_exists | 工具名称已存在(同一用户下) |
| 409 | tool_in_use | 工具正在被 Agent 使用 |
| 500 | generation_failed | 工具代码生成失败 |
| 500 | deployment_failed | Agent 部署失败 |
| 500 | repo_creation_failed | Gitee 仓库创建失败 |
---
## 📋 完整使用流程示例
### 1. 创建多个工具
```bash
# 工具1: 搜索 API
TOOL1=$(curl -s -X POST http://20.212.121.126/external-tools/generate \
-H "Content-Type: application/json" \
-d '{
"name": "search-api",
"description": "搜索API工具",
"url": "https://api.search.com/v1/search",
"method": "GET",
"user_id": "user-123"
}' | jq -r '.data.tool_ref_id')
# 工具2: 天气 API
TOOL2=$(curl -s -X POST http://20.212.121.126/external-tools/generate \
-H "Content-Type: application/json" \
-d '{
"name": "weather-api",
"description": "获取天气信息",
"url": "https://api.weather.com/v1/current",
"method": "GET",
"user_id": "user-123"
}' | jq -r '.data.tool_ref_id')
echo "工具1: $TOOL1"
echo "工具2: $TOOL2"
```
### 2. 创建包含多工具的 Agent
```bash
AGENT_RESULT=$(curl -s -X POST http://20.212.121.126/external-tools/agents/create-with-tools \
-H "Content-Type: application/json" \
-d "{
\"name\": \"my-multi-tool-agent\",
\"template\": \"custom_agent\",
\"tool_refs\": [\"$TOOL1\", \"$TOOL2\"],
\"config\": {\"user_id\": \"user-123\"}
}")
AGENT_REF_ID=$(echo $AGENT_RESULT | jq -r '.agent_ref_id')
echo "Agent Ref ID: $AGENT_REF_ID"
```
### 3. 轮询构建状态
```bash
while true; do
STATUS=$(curl -s "http://20.212.121.126/external-tools/agents/${AGENT_REF_ID}/build-status")
OVERALL=$(echo $STATUS | jq -r '.data.overall_status')
echo "当前状态: $OVERALL"
if [ "$OVERALL" = "ready" ] || [ "$OVERALL" = "deployed" ]; then
echo "✅ 构建完成!"
break
elif [ "$OVERALL" = "build_failed" ]; then
echo "❌ 构建失败!"
break
fi
sleep 30
done
```
### 4. 获取部署信息
```bash
curl -s "http://20.212.121.126/external-tools/agents/${AGENT_REF_ID}/deployment-info" | jq .
```
### 5. 访问 Agent
```bash
DOMAIN=$(curl -s "http://20.212.121.126/external-tools/agents/${AGENT_REF_ID}/deployment-info" \
| jq -r '.data.access_info.domain')
curl "http://${DOMAIN}/"
```
---
## 📞 联系方式
如有疑问,请联系 Agent Manager 开发团队。
---
**文档更新记录**
| 日期 | 版本 | 更新内容 |
|------|------|----------|
| 2026-01-29 | v1.0 | 初始版本,实现 MCP-Server 外部工具接口规范 |
| 2026-01-29 | v2.0 | 新增回调功能(计费)、多工具支持、构建状态查询、部署信息查询 |
+1025
View File
File diff suppressed because it is too large Load Diff
+414
View File
@@ -0,0 +1,414 @@
# Agent Manager Kubernetes部署文档
## 概述
本文档说明如何在Azure Kubernetes Service (AKS)上部署Agent Manager服务。
## 前提条件
1. **Azure资源**
- Azure订阅
- AKS集群
- Azure Container Registry (ACR)
- Azure DNS Zone(用于自动配置域名)
2. **本地工具**
- `kubectl` (Kubernetes命令行工具)
- `az` (Azure CLI)
- Docker (用于构建镜像)
3. **权限要求**
- AKS集群的管理员权限
- ACR的推送权限
- DNS Zone的管理权限
## 部署步骤
### 1. 配置Azure凭据
#### 1.1 创建Service Principal(如果还没有)
```bash
# 创建Service Principal
az ad sp create-for-rbac \
--name "agent-manager-sp" \
--role contributor \
--scopes /subscriptions/{subscription-id}
# 输出示例:
# {
# "appId": "c5ba26db-f180-425f-bac3-93708d853988",
# "displayName": "agent-manager-sp",
# "password": "ydt8Q~...",
# "tenant": "263c3ff6-1be5-4141-8308-b188464fb297"
# }
```
#### 1.2 配置DNS权限(重要!⚠️)
**Agent Manager需要DNS Zone Contributor权限才能为创建的agent自动配置域名。**
使用提供的脚本配置DNS权限:
```bash
# 方法1:使用自动化脚本(推荐)
bash scripts/setup_dns_permissions.sh
# 方法2:手动配置
AZURE_CLIENT_ID="your-service-principal-app-id"
AZURE_SUBSCRIPTION_ID="your-subscription-id"
AZURE_RESOURCE_GROUP="your-resource-group"
AZURE_DNS_ZONE="your-dns-zone.com"
DNS_ZONE_ID="/subscriptions/$AZURE_SUBSCRIPTION_ID/resourceGroups/$AZURE_RESOURCE_GROUP/providers/Microsoft.Network/dnsZones/$AZURE_DNS_ZONE"
az role assignment create \
--assignee $AZURE_CLIENT_ID \
--role "DNS Zone Contributor" \
--scope $DNS_ZONE_ID
```
验证权限:
```bash
az role assignment list \
--assignee $AZURE_CLIENT_ID \
--scope $DNS_ZONE_ID \
--output table
```
#### 1.3 更新Kubernetes Secret
编辑 `k8s/agent-manager-secret.yaml`:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: agent-manager-secret
namespace: agent-manager
type: Opaque
stringData:
AZURE_TENANT_ID: "263c3ff6-1be5-4141-8308-b188464fb297"
AZURE_CLIENT_ID: "c5ba26db-f180-425f-bac3-93708d853988"
AZURE_CLIENT_SECRET: "your-client-secret"
AZURE_SUBSCRIPTION_ID: "45d7a360-af09-40fc-9afc-56dc475245ec"
AZURE_RESOURCE_GROUP: "taiji-ai-v0"
AZURE_DNS_ZONE: "taijiagnet.com"
```
### 2. 配置ACR访问
创建ACR secret:
```bash
# 获取ACR登录服务器
ACR_NAME="your-acr-name"
ACR_LOGIN_SERVER="${ACR_NAME}.azurecr.io"
# 创建Docker registry secret
kubectl create secret docker-registry acr-secret \
--namespace agent-manager \
--docker-server=$ACR_LOGIN_SERVER \
--docker-username=$AZURE_CLIENT_ID \
--docker-password=$AZURE_CLIENT_SECRET
```
或使用脚本:
```bash
bash k8s/create-acr-secret.sh
```
### 3. 构建和推送镜像
```bash
# 构建镜像
docker build -t $ACR_LOGIN_SERVER/agent-manager:latest .
# 登录ACR
az acr login --name $ACR_NAME
# 推送镜像
docker push $ACR_LOGIN_SERVER/agent-manager:latest
```
### 4. 部署到Kubernetes
```bash
# 创建命名空间
kubectl apply -f k8s/agent-manager-namespace.yaml
# 创建RBAC(ServiceAccount、Role、RoleBinding)
kubectl apply -f k8s/agent-manager-rbac.yaml
# 创建Secret(Azure凭据)
kubectl apply -f k8s/agent-manager-secret.yaml
# 创建ACR Secret
kubectl apply -f k8s/acr-secret.yaml
# 创建ConfigMap(可选)
kubectl apply -f k8s/agent-manager-configmap.yaml
# 创建Deployment
kubectl apply -f k8s/agent-manager-deployment.yaml
# 创建Service(LoadBalancer)
kubectl apply -f k8s/agent-manager-service.yaml
```
或使用一键部署脚本:
```bash
bash k8s/deploy.sh
```
### 5. 验证部署
```bash
# 检查Pod状态
kubectl get pods -n agent-manager
# 检查Service和外网IP
kubectl get svc -n agent-manager
# 查看日志
kubectl logs -n agent-manager deployment/agent-manager
# 测试健康检查
AGENT_MANAGER_IP=$(kubectl get svc agent-manager -n agent-manager -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl http://$AGENT_MANAGER_IP/
```
### 6. 测试Agent创建
```bash
# 创建测试agent
curl -X POST http://$AGENT_MANAGER_IP/agents \
-H "Content-Type: application/json" \
-d '{
"name": "test-agent",
"template": "echo_agent",
"framework": "API",
"config": {
"user_id": "test-user"
}
}'
# 检查返回结果应包含:
# - external_ip: 外网IP地址
# - domain: 自动配置的域名 (test-agent.taijiagnet.com)
# - domain_url: 域名访问地址
# - recommended: 推荐访问地址(域名)
# 查看agent状态
curl http://$AGENT_MANAGER_IP/agents/test-agent/status
# 测试域名访问
curl http://test-agent.taijiagnet.com/
# 清理测试agent
curl -X DELETE http://$AGENT_MANAGER_IP/agents/test-agent
```
## 目录结构
```
k8s/
├── README.md # 本文档
├── agent-manager-namespace.yaml # Namespace定义
├── agent-manager-rbac.yaml # RBAC配置(ServiceAccount、Role等)
├── agent-manager-secret.yaml # Azure凭据Secret
├── agent-manager-configmap.yaml # 配置文件ConfigMap
├── agent-manager-deployment.yaml # Deployment定义
├── agent-manager-service.yaml # LoadBalancer Service定义
├── acr-secret.yaml # ACR访问Secret
├── create-acr-secret.sh # 创建ACR Secret脚本
└── deploy.sh # 一键部署脚本
```
## 常见问题
### Q1: Agent创建后没有返回域名信息
**症状:** 创建agent时返回外网IP但没有`domain`字段。
**原因:** Service Principal缺少DNS Zone的写权限。
**解决方案:**
```bash
# 运行DNS权限配置脚本
bash scripts/setup_dns_permissions.sh
# 或手动分配权限(见上文"配置DNS权限"部分)
```
详细信息见:`docs/DNS_ISSUE_FIX_REPORT.md`
### Q2: Pod启动失败,提示ImagePullBackOff
**原因:** 无法从ACR拉取镜像。
**解决方案:**
1. 检查ACR secret是否正确创建
2. 验证Service Principal有ACR的pull权限
3. 确认镜像名称和标签正确
```bash
# 检查ACR secret
kubectl get secret acr-secret -n agent-manager
# 重新创建ACR secret
bash k8s/create-acr-secret.sh
```
### Q3: LoadBalancer IP一直处于Pending状态
**原因:** AKS集群配置或云提供商问题。
**解决方案:**
1. 检查AKS集群是否支持LoadBalancer
2. 查看Service事件:`kubectl describe svc agent-manager -n agent-manager`
3. 确认Azure订阅有足够的配额
### Q4: 如何更新部署
```bash
# 方法1:修改YAML文件后重新应用
kubectl apply -f k8s/agent-manager-deployment.yaml
# 方法2:更新镜像
kubectl set image deployment/agent-manager \
agent-manager=your-acr.azurecr.io/agent-manager:new-tag \
-n agent-manager
# 方法3:编辑Deployment
kubectl edit deployment agent-manager -n agent-manager
# 查看滚动更新状态
kubectl rollout status deployment/agent-manager -n agent-manager
```
### Q5: 如何查看日志
```bash
# 查看所有Pod日志
kubectl logs -n agent-manager -l app=agent-manager
# 查看特定Pod日志
kubectl logs -n agent-manager <pod-name>
# 实时跟踪日志
kubectl logs -n agent-manager -l app=agent-manager -f
# 查看前一个容器的日志(如果Pod重启过)
kubectl logs -n agent-manager <pod-name> --previous
```
## 监控和维护
### 资源使用
```bash
# 查看Pod资源使用
kubectl top pods -n agent-manager
# 查看Node资源使用
kubectl top nodes
```
### 扩缩容
```bash
# 手动扩容
kubectl scale deployment agent-manager \
--replicas=3 \
-n agent-manager
# 自动扩缩容(HPA)
kubectl autoscale deployment agent-manager \
--cpu-percent=80 \
--min=2 \
--max=10 \
-n agent-manager
```
### 健康检查
Agent Manager提供以下健康检查端点:
- `GET /` - 基本健康检查
- `GET /templates` - 模板列表(验证数据库连接)
- `GET /agents` - Agent列表(验证K8s连接)
## 安全最佳实践
1. **Secret管理**
- 不要将Secret提交到版本控制
- 使用Azure Key Vault或Kubernetes Secrets加密
- 定期轮换凭据
2. **RBAC**
- 使用最小权限原则
- 为不同环境使用不同的Service Principal
- 定期审计权限分配
3. **网络安全**
- 考虑使用Private LoadBalancer
- 配置Network Policy限制Pod间通信
- 使用Ingress Controller配置TLS
4. **镜像安全**
- 定期扫描镜像漏洞
- 使用最新的基础镜像
- 不要在镜像中包含敏感信息
## 故障排查
### 诊断命令
```bash
# 检查所有资源
kubectl get all -n agent-manager
# 查看Pod详情
kubectl describe pod <pod-name> -n agent-manager
# 查看事件
kubectl get events -n agent-manager --sort-by='.lastTimestamp'
# 检查ServiceAccount
kubectl get sa -n agent-manager
kubectl describe sa agent-manager-sa -n agent-manager
# 检查RoleBinding
kubectl get rolebinding -n agent-manager
kubectl describe rolebinding agent-manager-role-binding -n agent-manager
# 进入Pod调试
kubectl exec -it <pod-name> -n agent-manager -- /bin/bash
```
### 日志级别
在Deployment中设置环境变量调整日志级别:
```yaml
env:
- name: LOG_LEVEL
value: "DEBUG" # DEBUG, INFO, WARNING, ERROR
```
## 参考资料
- [Kubernetes官方文档](https://kubernetes.io/docs/)
- [Azure Kubernetes Service文档](https://docs.microsoft.com/azure/aks/)
- [Azure DNS文档](https://docs.microsoft.com/azure/dns/)
- [Agent Manager API文档](../docs/API_DOCUMENTATION.md)
- [DNS问题修复报告](../docs/DNS_ISSUE_FIX_REPORT.md)
## 联系支持
如有问题,请:
1. 查看本文档的常见问题部分
2. 查看`docs/DNS_ISSUE_FIX_REPORT.md`
3. 查看agent-manager日志
4. 联系开发团队
+1 -1
View File
@@ -31,7 +31,7 @@ spec:
containers: containers:
- name: agent-manager - name: agent-manager
image: agnettaiji.azurecr.io/agent-manager:latest-arm64 image: agnettaiji.azurecr.io/ai-agents/agent-manager:latest
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail
# Non-destructive redeploy for agent-manager to a target cluster via kubeconfig/context.
# - Applies: namespace, RBAC, deployment, service
# - Does NOT apply configmap/secret by default (to avoid overwriting real credentials)
#
# Usage:
# ./scripts/redeploy-agent-manager-with-kubeconfig.sh /path/to/kubeconfig [context]
#
# Optional env:
# APPLY_CONFIG=1 -> also apply k8s/agent-manager-configmap.yaml
# APPLY_SECRET=1 -> also apply k8s/agent-manager-secret.yaml (DANGEROUS unless you edited it)
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
K8S_DIR="${ROOT_DIR}/k8s"
KUBECONFIG_FILE="${1:-}"
CONTEXT="${2:-}"
if [[ -z "${KUBECONFIG_FILE}" ]]; then
echo "ERROR: kubeconfig path required."
echo "Usage: $0 /path/to/kubeconfig [context]"
exit 1
fi
if [[ ! -f "${KUBECONFIG_FILE}" ]]; then
echo "ERROR: kubeconfig not found: ${KUBECONFIG_FILE}"
exit 1
fi
export KUBECONFIG="${KUBECONFIG_FILE}"
echo "Using kubeconfig: ${KUBECONFIG}"
if [[ -n "${CONTEXT}" ]]; then
echo "Switching context: ${CONTEXT}"
kubectl config use-context "${CONTEXT}" >/dev/null
fi
echo "Target cluster:"
kubectl cluster-info
echo "Applying namespace..."
kubectl apply -f "${K8S_DIR}/agent-manager-namespace.yaml"
if [[ "${APPLY_CONFIG:-0}" == "1" ]]; then
echo "Applying configmap (APPLY_CONFIG=1)..."
kubectl apply -f "${K8S_DIR}/agent-manager-configmap.yaml"
else
echo "Skipping configmap (set APPLY_CONFIG=1 to apply)."
fi
if [[ "${APPLY_SECRET:-0}" == "1" ]]; then
echo "Applying secret (APPLY_SECRET=1)..."
kubectl apply -f "${K8S_DIR}/agent-manager-secret.yaml"
else
echo "Skipping secret (set APPLY_SECRET=1 to apply)."
fi
echo "Applying RBAC..."
kubectl apply -f "${K8S_DIR}/agent-manager-rbac.yaml"
echo "Applying deployment + service..."
kubectl apply -f "${K8S_DIR}/agent-manager-deployment.yaml"
kubectl apply -f "${K8S_DIR}/agent-manager-service.yaml"
echo "Waiting for rollout..."
kubectl rollout status deployment/agent-manager -n agent-manager --timeout=300s
echo ""
echo "Pods:"
kubectl get pods -n agent-manager -o wide
echo ""
echo "Service:"
kubectl get svc agent-manager -n agent-manager -o wide
echo ""
echo "Done."
+192
View File
@@ -0,0 +1,192 @@
#!/bin/bash
# =============================================================================
# Agent Manager DNS权限配置脚本
# =============================================================================
#
# 用途:为Azure Service Principal分配DNS Zone管理权限
#
# 前提条件:
# 1. 已安装Azure CLI (az命令)
# 2. 已登录到正确的Azure账户 (az login)
# 3. 已选择正确的subscription (az account set)
# 4. 有足够权限分配角色(Owner或User Access Administrator)
#
# 使用方法:
# bash scripts/setup_dns_permissions.sh
#
# =============================================================================
set -e # 遇到错误立即退出
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# =============================================================================
# 配置部分 - 根据实际环境修改
# =============================================================================
# 从k8s secret读取配置(如果可用)
if command -v kubectl &> /dev/null && kubectl get secret agent-manager-secret -n agent-manager &> /dev/null 2>&1; then
log_info "从Kubernetes Secret读取配置..."
AZURE_CLIENT_ID=$(kubectl get secret agent-manager-secret -n agent-manager -o jsonpath='{.data.AZURE_CLIENT_ID}' 2>/dev/null | base64 -d)
AZURE_SUBSCRIPTION_ID=$(kubectl get secret agent-manager-secret -n agent-manager -o jsonpath='{.data.AZURE_SUBSCRIPTION_ID}' 2>/dev/null | base64 -d)
AZURE_RESOURCE_GROUP=$(kubectl get secret agent-manager-secret -n agent-manager -o jsonpath='{.data.AZURE_RESOURCE_GROUP}' 2>/dev/null | base64 -d)
AZURE_DNS_ZONE=$(kubectl get secret agent-manager-secret -n agent-manager -o jsonpath='{.data.AZURE_DNS_ZONE}' 2>/dev/null | base64 -d)
else
log_warning "无法从Kubernetes读取配置,使用默认值"
# 默认配置
AZURE_CLIENT_ID="${AZURE_CLIENT_ID:-c5ba26db-f180-425f-bac3-93708d853988}"
AZURE_SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID:-45d7a360-af09-40fc-9afc-56dc475245ec}"
AZURE_RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-taiji-ai-v0}"
AZURE_DNS_ZONE="${AZURE_DNS_ZONE:-taijiagnet.com}"
fi
# =============================================================================
# 主逻辑
# =============================================================================
echo ""
echo "========================================="
echo " Agent Manager DNS权限配置"
echo "========================================="
echo ""
log_info "配置信息:"
echo " Service Principal ID: $AZURE_CLIENT_ID"
echo " Subscription ID: $AZURE_SUBSCRIPTION_ID"
echo " Resource Group: $AZURE_RESOURCE_GROUP"
echo " DNS Zone: $AZURE_DNS_ZONE"
echo ""
# 1. 检查Azure CLI
log_info "检查Azure CLI..."
if ! command -v az &> /dev/null; then
log_error "Azure CLI未安装,请先安装: https://docs.microsoft.com/cli/azure/install-azure-cli"
exit 1
fi
log_success "Azure CLI已安装"
# 2. 检查Azure登录状态
log_info "检查Azure登录状态..."
if ! az account show &> /dev/null; then
log_error "未登录Azure,请先运行: az login"
exit 1
fi
log_success "已登录Azure"
# 3. 切换到正确的subscription
log_info "切换到目标subscription..."
CURRENT_SUB=$(az account show --query id -o tsv)
if [ "$CURRENT_SUB" != "$AZURE_SUBSCRIPTION_ID" ]; then
log_warning "当前subscription ($CURRENT_SUB) 与目标不同,正在切换..."
az account set --subscription "$AZURE_SUBSCRIPTION_ID"
log_success "已切换到subscription: $AZURE_SUBSCRIPTION_ID"
else
log_success "已在正确的subscription"
fi
# 4. 验证DNS Zone存在
log_info "验证DNS Zone是否存在..."
if ! az network dns zone show \
--name "$AZURE_DNS_ZONE" \
--resource-group "$AZURE_RESOURCE_GROUP" \
--output none 2>/dev/null; then
log_error "DNS Zone '$AZURE_DNS_ZONE' 不存在于资源组 '$AZURE_RESOURCE_GROUP'"
exit 1
fi
log_success "DNS Zone存在"
# 构建DNS Zone资源ID
DNS_ZONE_ID="/subscriptions/$AZURE_SUBSCRIPTION_ID/resourceGroups/$AZURE_RESOURCE_GROUP/providers/Microsoft.Network/dnsZones/$AZURE_DNS_ZONE"
# 5. 检查当前权限
log_info "检查Service Principal当前权限..."
EXISTING_ROLES=$(az role assignment list \
--assignee "$AZURE_CLIENT_ID" \
--scope "$DNS_ZONE_ID" \
--query "[].roleDefinitionName" \
--output tsv)
if echo "$EXISTING_ROLES" | grep -q "DNS Zone Contributor"; then
log_success "Service Principal已拥有DNS Zone Contributor角色"
echo ""
echo "当前角色分配:"
az role assignment list \
--assignee "$AZURE_CLIENT_ID" \
--scope "$DNS_ZONE_ID" \
--output table
echo ""
log_info "无需重复分配权限"
exit 0
fi
# 6. 分配DNS Zone Contributor角色
log_info "分配DNS Zone Contributor角色..."
if az role assignment create \
--assignee "$AZURE_CLIENT_ID" \
--role "DNS Zone Contributor" \
--scope "$DNS_ZONE_ID" \
--output none; then
log_success "DNS Zone Contributor角色分配成功"
else
log_error "角色分配失败,请检查是否有足够的权限"
exit 1
fi
# 7. 验证权限分配
log_info "验证权限分配..."
sleep 3 # 等待权限生效
echo ""
echo "当前角色分配:"
az role assignment list \
--assignee "$AZURE_CLIENT_ID" \
--scope "$DNS_ZONE_ID" \
--output table
echo ""
log_success "✅ DNS权限配置完成!"
echo ""
log_info "Service Principal现在可以管理DNS Zone '$AZURE_DNS_ZONE' 的DNS记录"
log_info "Agent Manager可以为创建的agent自动配置DNS域名"
echo ""
# 8. 测试DNS权限(可选)
read -p "是否要测试DNS记录创建权限?(y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
TEST_RECORD="test-dns-permission-check"
TEST_IP="1.2.3.4"
log_info "创建测试DNS记录: $TEST_RECORD.$AZURE_DNS_ZONE -> $TEST_IP"
# 使用Azure REST API测试(模拟agent-manager的行为)
log_warning "注意:此测试需要AZURE_CLIENT_SECRET,将跳过"
log_info "请在agent-manager中创建一个测试agent来验证DNS功能"
fi
echo ""
log_success "配置完成!"
+314
View File
@@ -0,0 +1,314 @@
"""
工具存储管理器
负责存储和管理外部数据工具的配置和代码
符合 MCP-Server 调用规范
"""
import os
import json
import logging
from typing import Dict, List, Optional, Any
from datetime import datetime
from pathlib import Path
logger = logging.getLogger(__name__)
class ToolStorage:
"""工具存储管理器 - 管理外部数据工具的配置和代码文件"""
def __init__(self, storage_path: str = None):
"""
初始化工具存储
Args:
storage_path: 工具存储路径,默认使用 ./tool_storage
"""
self.storage_path = Path(storage_path or os.getenv("TOOL_STORAGE_PATH", "./tool_storage"))
self.storage_path.mkdir(parents=True, exist_ok=True)
# 内存缓存
self._cache: Dict[str, Dict] = {}
# 加载已有工具
self._load_existing_tools()
def _load_existing_tools(self):
"""从文件系统加载已有的工具配置"""
try:
for tool_dir in self.storage_path.iterdir():
if tool_dir.is_dir():
config_file = tool_dir / "config.json"
if config_file.exists():
with open(config_file, "r", encoding="utf-8") as f:
config = json.load(f)
self._cache[config["tool_ref_id"]] = config
logger.info(f"✅ 已加载 {len(self._cache)} 个工具配置")
except Exception as e:
logger.error(f"加载工具配置失败: {e}")
def _get_tool_dir(self, tool_ref_id: str) -> Path:
"""获取工具的存储目录"""
return self.storage_path / tool_ref_id
def save_tool(
self,
tool_ref_id: str,
name: str,
description: str,
config: Dict,
code: str,
user_id: str,
tenant_id: Optional[str] = None
) -> Dict[str, Any]:
"""
保存工具配置和代码
Args:
tool_ref_id: 工具唯一标识
name: 工具名称
description: 工具描述
config: 工具配置(含认证、参数等)
code: 生成的 Python 代码
user_id: 用户 ID
tenant_id: 租户 ID
Returns:
保存结果
"""
tool_dir = self._get_tool_dir(tool_ref_id)
tool_dir.mkdir(parents=True, exist_ok=True)
# 保存配置
tool_data = {
"tool_ref_id": tool_ref_id,
"name": name,
"description": description,
"config": config,
"user_id": user_id,
"tenant_id": tenant_id,
"created_at": datetime.utcnow().isoformat(),
"updated_at": datetime.utcnow().isoformat(),
"status": "created"
}
config_file = tool_dir / "config.json"
with open(config_file, "w", encoding="utf-8") as f:
json.dump(tool_data, f, ensure_ascii=False, indent=2)
# 保存代码文件
code_file = tool_dir / f"{self._to_python_name(name)}.py"
with open(code_file, "w", encoding="utf-8") as f:
f.write(code)
# 更新缓存
tool_data["code_file"] = str(code_file)
self._cache[tool_ref_id] = tool_data
logger.info(f"✅ 工具已保存: {tool_ref_id}")
return {"success": True, "tool_ref_id": tool_ref_id, "code_file": str(code_file)}
def get_tool(self, tool_ref_id: str) -> Optional[Dict]:
"""
获取工具配置
Args:
tool_ref_id: 工具唯一标识
Returns:
工具配置,不存在返回 None
"""
# 先从缓存获取
if tool_ref_id in self._cache:
return self._cache[tool_ref_id]
# 从文件系统加载
tool_dir = self._get_tool_dir(tool_ref_id)
config_file = tool_dir / "config.json"
if config_file.exists():
with open(config_file, "r", encoding="utf-8") as f:
tool_data = json.load(f)
self._cache[tool_ref_id] = tool_data
return tool_data
return None
def get_tool_code(self, tool_ref_id: str) -> Optional[str]:
"""
获取工具代码
Args:
tool_ref_id: 工具唯一标识
Returns:
工具代码,不存在返回 None
"""
tool = self.get_tool(tool_ref_id)
if not tool:
return None
name = tool.get("name", "tool")
tool_dir = self._get_tool_dir(tool_ref_id)
code_file = tool_dir / f"{self._to_python_name(name)}.py"
if code_file.exists():
with open(code_file, "r", encoding="utf-8") as f:
return f.read()
return None
def update_tool(self, tool_ref_id: str, updates: Dict) -> Optional[Dict]:
"""
更新工具配置
Args:
tool_ref_id: 工具唯一标识
updates: 更新内容
Returns:
更新后的配置
"""
tool = self.get_tool(tool_ref_id)
if not tool:
return None
# 更新配置
if "description" in updates:
tool["description"] = updates["description"]
tool["config"]["description"] = updates["description"]
for key in ["url", "method", "headers", "auth", "request_params", "request_body", "timeout"]:
if key in updates and updates[key] is not None:
tool["config"][key] = updates[key]
tool["updated_at"] = datetime.utcnow().isoformat()
# 保存到文件
tool_dir = self._get_tool_dir(tool_ref_id)
config_file = tool_dir / "config.json"
save_data = {k: v for k, v in tool.items() if k != "code_file"}
with open(config_file, "w", encoding="utf-8") as f:
json.dump(save_data, f, ensure_ascii=False, indent=2)
# 更新缓存
self._cache[tool_ref_id] = tool
return tool
def delete_tool(self, tool_ref_id: str) -> bool:
"""
删除工具
Args:
tool_ref_id: 工具唯一标识
Returns:
是否删除成功
"""
tool_dir = self._get_tool_dir(tool_ref_id)
if tool_dir.exists():
import shutil
shutil.rmtree(tool_dir)
if tool_ref_id in self._cache:
del self._cache[tool_ref_id]
logger.info(f"✅ 工具已删除: {tool_ref_id}")
return True
def list_tools(self, user_id: Optional[str] = None, tenant_id: Optional[str] = None) -> List[Dict]:
"""
列出工具
Args:
user_id: 用户 ID 过滤
tenant_id: 租户 ID 过滤
Returns:
工具列表
"""
tools = list(self._cache.values())
if user_id:
tools = [t for t in tools if t.get("user_id") == user_id]
if tenant_id:
tools = [t for t in tools if t.get("tenant_id") == tenant_id]
return tools
def get_tools_by_refs(self, tool_refs: List[str]) -> List[Dict]:
"""
根据 tool_ref_id 列表获取工具
Args:
tool_refs: tool_ref_id 列表
Returns:
工具列表(仅返回存在的工具)
"""
tools = []
for ref in tool_refs:
tool = self.get_tool(ref)
if tool:
tools.append(tool)
return tools
def check_tool_in_use(self, tool_ref_id: str) -> bool:
"""
检查工具是否被 Agent 使用
Args:
tool_ref_id: 工具唯一标识
Returns:
是否被使用
"""
tool = self.get_tool(tool_ref_id)
if not tool:
return False
return tool.get("status") == "in_use" or bool(tool.get("used_by_agents"))
def mark_tool_in_use(self, tool_ref_id: str, agent_name: str) -> bool:
"""
标记工具被 Agent 使用
Args:
tool_ref_id: 工具唯一标识
agent_name: Agent 名称
Returns:
是否成功
"""
tool = self.get_tool(tool_ref_id)
if not tool:
return False
if "used_by_agents" not in tool:
tool["used_by_agents"] = []
if agent_name not in tool["used_by_agents"]:
tool["used_by_agents"].append(agent_name)
tool["status"] = "in_use"
# 保存更新
self.update_tool(tool_ref_id, {"status": "in_use"})
return True
def _to_python_name(self, name: str) -> str:
"""将名称转换为 Python 函数名格式"""
import re
name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
if name and name[0].isdigit():
name = '_' + name
return name.lower()
# 全局实例
tool_storage = ToolStorage()