Files
agent_management/external_tool_api.py
T
zhanggangyong 5a2f9a7ffc feat: 新增 Agent 资源配置参数支持
- 新增 cpu_request, cpu_limit, memory_request, memory_limit, replicas 参数
- 创建 Agent 时可自定义 K8s 资源限制
- 更新 API 文档到 v2.1
2026-01-30 11:56:39 +00:00

1053 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
外部工具 API 模块
符合 MCP-Server 调用规范
参考: http://gitee.ath.cx:3000/xiaohei/taiji-AI-PAD/src/branch/feature/chenchen/Docs/Agent-Manager外部工具接口规范.md
职责:
- 接收 MCP-Server 的工具配置请求
- 生成 Pydantic AI 工具代码
- 存储工具配置和代码文件
- 支持工具的 CRUD 操作
- 支持带工具的 Agent 创建
"""
import os
import uuid
import logging
import requests
from datetime import datetime
from typing import Dict, List, Optional, Any
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from agent_code_generator import agent_code_generator
from tool_storage import tool_storage
from gitee_manager import gitee_manager
logger = logging.getLogger(__name__)
# 创建路由 - 按照规范使用 /tools 前缀
router = APIRouter(prefix="/external-tools", tags=["External Tool API (MCP-Server 规范)"])
# ==================== 请求/响应模型 ====================
class AuthConfig(BaseModel):
"""认证配置"""
type: str = Field(..., description="认证类型: api_key, bearer, basic")
key: Optional[str] = Field(None, description="API Key 或 Bearer Token")
username: Optional[str] = Field(None, description="Basic Auth 用户名")
password: Optional[str] = Field(None, description="Basic Auth 密码")
in_location: Optional[str] = Field("header", alias="in", description="API Key 位置: header, query")
name: Optional[str] = Field("X-API-Key", description="API Key 参数名")
class Config:
populate_by_name = True
class RetryConfig(BaseModel):
"""重试配置"""
max_retries: int = Field(3, description="最大重试次数")
retry_delay: int = Field(1, description="重试间隔(秒)")
class GenerateToolRequest(BaseModel):
"""
生成外部数据工具请求
符合接口规范文档
"""
name: str = Field(..., min_length=1, max_length=100, description="工具名称(将用于生成 Python 函数名)")
description: str = Field(..., description="工具描述(将作为工具的 docstring)")
url: str = Field(..., description="API 端点 URL")
method: str = Field(..., description="HTTP 方法:GET/POST/PUT/DELETE/PATCH")
user_id: str = Field(..., description="用户 ID(UUID 格式)")
tenant_id: Optional[str] = Field(None, description="租户 ID(UUID 格式)")
headers: Optional[Dict[str, str]] = Field(None, description="自定义请求头")
auth: Optional[AuthConfig] = Field(None, description="认证配置")
request_params: Optional[Dict] = Field(None, description="URL 查询参数定义(JSON Schema 格式)")
request_body: Optional[Dict] = Field(None, description="请求体定义(JSON Schema 格式)")
response_mapping: Optional[Dict] = Field(None, description="响应字段映射")
timeout: int = Field(30, description="超时时间(秒),默认 30")
retry: Optional[RetryConfig] = Field(None, description="重试配置")
class UpdateToolRequest(BaseModel):
"""更新外部数据工具请求"""
description: Optional[str] = Field(None, description="工具描述")
url: Optional[str] = Field(None, description="API URL")
method: Optional[str] = Field(None, description="HTTP 方法")
headers: Optional[Dict[str, str]] = Field(None, description="自定义请求头")
auth: Optional[AuthConfig] = Field(None, description="认证配置")
request_params: Optional[Dict] = Field(None, description="请求参数定义")
request_body: Optional[Dict] = Field(None, description="请求体定义")
response_mapping: Optional[Dict] = Field(None, description="响应字段映射")
timeout: Optional[int] = Field(None, description="超时时间")
class TestToolRequest(BaseModel):
"""测试工具连接请求"""
test_params: Optional[Dict] = Field(None, description="测试参数")
test_body: Optional[Dict] = Field(None, description="测试请求体")
class ResourceConfig(BaseModel):
"""资源配置"""
cpu_request: str = Field("100m", description="CPU 请求(如 100m, 500m)")
cpu_limit: str = Field("500m", description="CPU 限制(如 500m, 1000m)")
memory_request: str = Field("128Mi", description="内存请求(如 128Mi, 256Mi)")
memory_limit: str = Field("512Mi", description="内存限制(如 512Mi, 1Gi)")
class CreateAgentWithToolsRequest(BaseModel):
"""创建带有外部工具的 Agent 请求"""
name: str = Field(..., min_length=1, max_length=63, description="Agent 名称(符合 K8s 命名规范)")
template: str = Field(..., description="Agent 模板名称")
tool_refs: List[str] = Field(default=[], description="外部数据工具标识列表")
config: Optional[Dict] = Field(default_factory=dict, description="其他配置")
env: Optional[Dict[str, str]] = Field(default_factory=dict, description="环境变量")
# 资源配置 - 参考原有模板创建方式
cpu_request: Optional[str] = Field("100m", description="CPU 请求(如 100m, 500m)")
cpu_limit: Optional[str] = Field("500m", description="CPU 限制(如 500m, 1000m)")
memory_request: Optional[str] = Field("128Mi", description="内存请求(如 128Mi, 256Mi)")
memory_limit: Optional[str] = Field("512Mi", description="内存限制(如 512Mi, 1Gi)")
replicas: Optional[int] = Field(1, ge=1, le=10, description="副本数量")
# ==================== 工具 API 接口 ====================
@router.post("/generate")
async def generate_tool(request: GenerateToolRequest):
"""
1️⃣ 生成外部数据工具
MCP-Server 调用此接口,Agent Manager 需要:
1. 验证配置格式
2. 根据配置生成 Pydantic AI 工具代码文件
3. 存储工具代码文件和完整配置(包含敏感信息如 API Key)
4. 返回唯一的 tool_ref_id 供后续引用
"""
try:
# 验证 HTTP 方法
valid_methods = ["GET", "POST", "PUT", "DELETE", "PATCH"]
if request.method.upper() not in valid_methods:
return {
"success": False,
"error": "invalid_method",
"message": f"无效的 HTTP 方法。支持: {', '.join(valid_methods)}"
}
# 验证 URL 格式
if not request.url.startswith(("http://", "https://")):
return {
"success": False,
"error": "invalid_url",
"message": "URL 必须以 http:// 或 https:// 开头"
}
# 验证认证配置
if request.auth:
valid_auth_types = ["api_key", "bearer", "basic"]
if request.auth.type not in valid_auth_types:
return {
"success": False,
"error": "invalid_auth",
"message": f"无效的认证类型。支持: {', '.join(valid_auth_types)}"
}
# 生成唯一 tool_ref_id
tool_ref_id = f"tool-{request.name.lower().replace(' ', '-')}-{uuid.uuid4().hex[:8]}"
# 构建工具配置
tool_config = {
"name": request.name,
"description": request.description,
"url": request.url,
"method": request.method.upper(),
"headers": request.headers,
"auth": request.auth.model_dump(by_alias=True) if request.auth else None,
"request_params": request.request_params,
"request_body": request.request_body,
"response_mapping": request.response_mapping,
"timeout": request.timeout,
"retry": request.retry.model_dump() if request.retry else None
}
# 生成 Pydantic AI 工具代码
tool_code = agent_code_generator.generate_tool_code(tool_config)
# 存储工具配置和代码
save_result = tool_storage.save_tool(
tool_ref_id=tool_ref_id,
name=request.name,
description=request.description,
config=tool_config,
code=tool_code,
user_id=request.user_id,
tenant_id=request.tenant_id
)
if not save_result.get("success"):
return {
"success": False,
"error": "generation_failed",
"message": save_result.get("error", "工具保存失败")
}
logger.info(f"✅ 工具生成成功: {tool_ref_id} (用户: {request.user_id})")
return {
"success": True,
"data": {
"tool_ref_id": tool_ref_id,
"name": request.name,
"description": request.description,
"created_at": datetime.utcnow().isoformat()
},
"message": "工具生成成功"
}
except Exception as e:
logger.error(f"工具生成失败: {e}")
import traceback
logger.error(traceback.format_exc())
return {
"success": False,
"error": "generation_failed",
"message": str(e)
}
@router.put("/{tool_ref_id}")
async def update_tool(tool_ref_id: str, request: UpdateToolRequest):
"""
2️⃣ 更新外部数据工具
更新已存在的工具配置,重新生成代码
"""
try:
# 检查工具是否存在
existing_tool = tool_storage.get_tool(tool_ref_id)
if not existing_tool:
raise HTTPException(
status_code=404,
detail={"success": False, "error": "tool_not_found", "message": "工具不存在"}
)
# 构建更新内容
updates = {}
if request.description is not None:
updates["description"] = request.description
if request.url is not None:
updates["url"] = request.url
if request.method is not None:
# 验证 HTTP 方法
valid_methods = ["GET", "POST", "PUT", "DELETE", "PATCH"]
if request.method.upper() not in valid_methods:
return {
"success": False,
"error": "invalid_method",
"message": f"无效的 HTTP 方法。支持: {', '.join(valid_methods)}"
}
updates["method"] = request.method.upper()
if request.headers is not None:
updates["headers"] = request.headers
if request.auth is not None:
updates["auth"] = request.auth.model_dump(by_alias=True)
if request.request_params is not None:
updates["request_params"] = request.request_params
if request.request_body is not None:
updates["request_body"] = request.request_body
if request.response_mapping is not None:
updates["response_mapping"] = request.response_mapping
if request.timeout is not None:
updates["timeout"] = request.timeout
# 更新工具配置
updated_tool = tool_storage.update_tool(tool_ref_id, updates)
if not updated_tool:
return {
"success": False,
"error": "update_failed",
"message": "工具更新失败"
}
# 重新生成代码
tool_code = agent_code_generator.generate_tool_code(updated_tool["config"])
# 更新代码文件
tool_storage.save_tool(
tool_ref_id=tool_ref_id,
name=updated_tool["name"],
description=updated_tool.get("description", ""),
config=updated_tool["config"],
code=tool_code,
user_id=updated_tool["user_id"],
tenant_id=updated_tool.get("tenant_id")
)
logger.info(f"✅ 工具更新成功: {tool_ref_id}")
return {
"success": True,
"data": {
"tool_ref_id": tool_ref_id,
"name": updated_tool["name"],
"updated_at": updated_tool.get("updated_at", datetime.utcnow().isoformat())
},
"message": "工具更新成功"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"工具更新失败: {e}")
return {
"success": False,
"error": "update_failed",
"message": str(e)
}
@router.delete("/{tool_ref_id}")
async def delete_tool(tool_ref_id: str):
"""
3️⃣ 删除外部数据工具
删除工具配置和代码文件
"""
try:
# 检查工具是否存在
existing_tool = tool_storage.get_tool(tool_ref_id)
if not existing_tool:
raise HTTPException(
status_code=404,
detail={"success": False, "error": "tool_not_found", "message": "工具不存在"}
)
# 检查工具是否正在被使用
if tool_storage.check_tool_in_use(tool_ref_id):
return {
"success": False,
"error": "tool_in_use",
"message": "工具正在被 Agent 使用,无法删除"
}
# 删除工具
tool_storage.delete_tool(tool_ref_id)
logger.info(f"✅ 工具删除成功: {tool_ref_id}")
return {
"success": True,
"data": {
"tool_ref_id": tool_ref_id
},
"message": "工具删除成功"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"工具删除失败: {e}")
return {
"success": False,
"error": "delete_failed",
"message": str(e)
}
@router.post("/{tool_ref_id}/test")
async def test_tool(tool_ref_id: str, request: TestToolRequest = None):
"""
4️⃣ 测试工具连接
使用存储的配置发送测试请求,验证 API 连通性
"""
try:
# 获取工具配置
tool = tool_storage.get_tool(tool_ref_id)
if not tool:
raise HTTPException(
status_code=404,
detail={"success": False, "error": "tool_not_found", "message": "工具不存在"}
)
config = tool.get("config", {})
# 构建请求
url = config.get("url", "")
method = config.get("method", "GET")
timeout = config.get("timeout", 30)
headers = config.get("headers", {}) or {}
params = (request.test_params if request else None) or {}
body = (request.test_body if request else None) or None
# 处理认证
auth = config.get("auth")
if auth:
auth_type = auth.get("type", "")
if auth_type == "api_key":
location = auth.get("in", "header")
key_name = auth.get("name", "X-API-Key")
key_value = auth.get("key", "")
if location == "header":
headers[key_name] = key_value
elif location == "query":
params[key_name] = key_value
elif auth_type == "bearer":
headers["Authorization"] = f"Bearer {auth.get('key', '')}"
elif auth_type == "basic":
import base64
credentials = base64.b64encode(
f"{auth.get('username', '')}:{auth.get('password', '')}".encode()
).decode()
headers["Authorization"] = f"Basic {credentials}"
# 发送测试请求
start_time = datetime.utcnow()
response = requests.request(
method=method,
url=url,
headers=headers,
params=params if method.upper() == "GET" else None,
json=body if method.upper() != "GET" else None,
timeout=timeout
)
elapsed_ms = (datetime.utcnow() - start_time).total_seconds() * 1000
# 构建响应
result = {
"status_code": response.status_code,
"response_time_ms": round(elapsed_ms, 2),
"content_type": response.headers.get("Content-Type"),
"headers": dict(response.headers)
}
# 响应预览(截断)
if response.text:
result["response_preview"] = response.text[:500]
if len(response.text) > 500:
result["response_preview"] += "... (truncated)"
# 判断成功
is_success = 200 <= response.status_code < 300
return {
"success": is_success,
"data": result,
"message": "连接测试成功" if is_success else f"连接测试失败 (HTTP {response.status_code})"
}
except requests.exceptions.Timeout:
return {
"success": False,
"error": "timeout",
"message": "连接超时"
}
except requests.exceptions.ConnectionError:
return {
"success": False,
"error": "connection_error",
"message": "无法连接到目标服务"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"工具测试失败: {e}")
return {
"success": False,
"error": "test_failed",
"message": str(e)
}
@router.get("/{tool_ref_id}")
async def get_tool(tool_ref_id: str):
"""
获取工具详情
返回工具的完整配置信息(不含敏感信息)
"""
try:
tool = tool_storage.get_tool(tool_ref_id)
if not tool:
raise HTTPException(
status_code=404,
detail={"success": False, "error": "tool_not_found", "message": "工具不存在"}
)
# 脱敏处理
safe_tool = {
"tool_ref_id": tool["tool_ref_id"],
"name": tool["name"],
"description": tool.get("description"),
"url": tool["config"].get("url"),
"method": tool["config"].get("method"),
"status": tool.get("status", "created"),
"created_at": tool.get("created_at"),
"updated_at": tool.get("updated_at"),
"user_id": tool.get("user_id"),
"tenant_id": tool.get("tenant_id"),
"has_auth": bool(tool["config"].get("auth")),
"used_by_agents": tool.get("used_by_agents", [])
}
return {
"success": True,
"data": safe_tool
}
except HTTPException:
raise
except Exception as e:
logger.error(f"获取工具失败: {e}")
return {
"success": False,
"error": "get_failed",
"message": str(e)
}
@router.get("/")
async def list_tools(
user_id: Optional[str] = Query(None, description="用户 ID 过滤"),
tenant_id: Optional[str] = Query(None, description="租户 ID 过滤")
):
"""
列出所有工具
支持按用户和租户过滤
"""
try:
tools = tool_storage.list_tools(user_id=user_id, tenant_id=tenant_id)
# 简化输出
tool_list = [
{
"tool_ref_id": t["tool_ref_id"],
"name": t["name"],
"description": t.get("description"),
"status": t.get("status", "created"),
"created_at": t.get("created_at")
}
for t in tools
]
return {
"success": True,
"data": {
"tools": tool_list,
"count": len(tool_list)
}
}
except Exception as e:
logger.error(f"列出工具失败: {e}")
return {
"success": False,
"error": "list_failed",
"message": str(e)
}
@router.get("/{tool_ref_id}/code")
async def get_tool_code(tool_ref_id: str):
"""
获取工具生成的代码
返回 Pydantic AI 工具代码
"""
try:
code = tool_storage.get_tool_code(tool_ref_id)
if not code:
raise HTTPException(
status_code=404,
detail={"success": False, "error": "tool_not_found", "message": "工具不存在"}
)
return {
"success": True,
"data": {
"tool_ref_id": tool_ref_id,
"code": code
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"获取工具代码失败: {e}")
return {
"success": False,
"error": "get_code_failed",
"message": str(e)
}
# ==================== 工具集成到 Agent ====================
@router.post("/agents/create-with-tools")
async def create_agent_with_tools(request: CreateAgentWithToolsRequest):
"""
5️⃣ 创建带有外部工具的 Agent
这是扩展的 Agent 创建接口,支持 tool_refs 字段。
当传递 tool_refs 时,Agent Manager 需要:
1. 加载对应的工具代码文件
2. 将工具集成到 Agent 中
3. 生成完整项目并部署到 AKS
"""
try:
# 验证工具引用
tools = []
missing_tools = []
for ref in request.tool_refs:
tool = tool_storage.get_tool(ref)
if tool:
tools.append(tool)
else:
missing_tools.append(ref)
if missing_tools:
return {
"success": False,
"error": "tool_not_found",
"message": f"以下工具不存在: {', '.join(missing_tools)}"
}
# 转换工具配置为代码生成器需要的格式
tools_config = []
for tool in tools:
config = tool.get("config", {})
tools_config.append({
"name": tool["name"],
"description": tool.get("description", ""),
"url": config.get("url"),
"method": config.get("method", "GET"),
"auth": config.get("auth"),
"request_params": config.get("request_params"),
"request_body": config.get("request_body"),
"timeout": config.get("timeout", 30)
})
# 生成仓库名
repo_name = f"agent-{request.name.lower().replace('_', '-')}-{uuid.uuid4().hex[:6]}"
agent_ref_id = f"agent-{repo_name}"
# 生成完整项目文件(传递资源配置)
project_files = agent_code_generator.generate_full_project(
agent_name=request.name,
description=f"Agent with {len(tools)} external tools",
tools_config=tools_config,
auto_deploy=True,
cpu_request=request.cpu_request or "100m",
cpu_limit=request.cpu_limit or "500m",
memory_request=request.memory_request or "128Mi",
memory_limit=request.memory_limit or "512Mi",
replicas=request.replicas or 1
)
# 创建 Gitee 仓库
repo_result = gitee_manager.create_repository(
repo_name=repo_name,
description=f"{request.name} - Agent with external tools",
private=False
)
if not repo_result.get("success"):
return {
"success": False,
"error": "repo_creation_failed",
"message": repo_result.get("error", "仓库创建失败")
}
# 获取仓库所有者
repo_owner = repo_result.get("owner", gitee_manager.gitee_username)
# 推送文件
push_result = gitee_manager.push_files(
repo_name=repo_name,
files=project_files,
commit_message=f"Initial commit: {request.name} with {len(tools)} tools",
owner=repo_owner
)
# 设置 CI/CD Secrets - 使用新生成的 Azure 凭证
cicd_secrets = {
"ACR_LOGIN_SERVER": "agnettaiji.azurecr.io",
"ACR_USERNAME": "agnettaiji",
"ACR_PASSWORD": "hDpX5t34N5ZmnKdtqyjYL5co/SnXJrmD20CRpGpWaG+ACRCw2wGM",
"AZ_CLIENT_ID": "f2dd1cb2-02f6-4efb-bc72-d148f6e01545",
"AZ_CLIENT_SECRET": "S0J8Q~DE.DEu29nreaBn2EbeuGOg7GEIkonRMbYj",
"AZ_TENANT_ID": "263c3ff6-1be5-4141-8308-b188464fb297",
"AZ_SUBSCRIPTION_ID": "45d7a360-af09-40fc-9afc-56dc475245ec",
"AZ_RG": "taiji-ai-pda",
"AZ_AKS": "taiji-ai-pda",
"AZURE_DNS_ZONE": "taijiagnet.com",
"AZURE_DNS_RG": "taiji-Ai-v0"
}
gitee_manager.set_repo_secrets(
repo_name=repo_name,
secrets=cicd_secrets,
owner=repo_owner
)
# 标记工具被使用
for ref in request.tool_refs:
tool_storage.mark_tool_in_use(ref, request.name)
# 计算 K8s 相关名称
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
expected_domain = f"{k8s_name}.taijiagnet.com"
agent_ref_id = f"agent-{repo_name}"
# 存储 Agent 信息以便后续查询
AGENT_REFS[agent_ref_id] = {
"agent_ref_id": agent_ref_id,
"name": request.name,
"repo_name": repo_name,
"repo_url": repo_result.get("html_url"),
"repo_owner": repo_owner,
"namespace": f"agent-{k8s_name}",
"domain": expected_domain,
"image_name": f"agnettaiji.azurecr.io/ai-agents/{repo_name}:latest",
"tools": [t["name"] for t in tools],
"tool_refs": request.tool_refs,
"user_id": request.config.get("user_id") if request.config else None,
"created_at": datetime.utcnow().isoformat(),
"status": "building"
}
logger.info(f"✅ Agent 创建成功: {request.name} (工具: {len(tools)} 个)")
logger.info(f" Agent Ref ID: {agent_ref_id}")
logger.info(f" 查询构建状态: GET /external-tools/agents/{agent_ref_id}/build-status")
return {
"success": True,
"name": request.name,
"agent_ref_id": agent_ref_id,
"namespace": f"agent-{k8s_name}",
"status": "Building",
"created_at": datetime.utcnow().isoformat(),
"template": request.template,
"service_port": 8000,
"access_info": {
"domain": expected_domain,
"domain_url": f"http://{expected_domain}",
"ip_url": "pending"
},
"resources": {
"cpu_request": request.cpu_request or "100m",
"cpu_limit": request.cpu_limit or "500m",
"memory_request": request.memory_request or "128Mi",
"memory_limit": request.memory_limit or "512Mi",
"replicas": request.replicas or 1
},
"tools_attached": len(tools),
"repo_url": repo_result.get("html_url"),
"image_name": f"agnettaiji.azurecr.io/ai-agents/{repo_name}:latest",
"status_query_url": f"/external-tools/agents/{agent_ref_id}/build-status",
"deployment_info_url": f"/external-tools/agents/{agent_ref_id}/deployment-info"
}
except Exception as e:
logger.error(f"创建带工具的 Agent 失败: {e}")
import traceback
logger.error(traceback.format_exc())
return {
"success": False,
"error": "deployment_failed",
"message": str(e)
}
@router.get("/{tool_ref_id}/preview-code")
async def preview_generated_code(tool_ref_id: str):
"""
预览工具生成的 Pydantic AI 代码
用于调试和验证代码生成是否正确
"""
try:
tool = tool_storage.get_tool(tool_ref_id)
if not tool:
raise HTTPException(
status_code=404,
detail={"success": False, "error": "tool_not_found", "message": "工具不存在"}
)
code = tool_storage.get_tool_code(tool_ref_id)
return {
"success": True,
"data": {
"tool_ref_id": tool_ref_id,
"name": tool["name"],
"generated_code": code,
"code_preview": code[:1000] if code else None
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"预览代码失败: {e}")
return {
"success": False,
"error": "preview_failed",
"message": str(e)
}
# ==================== Agent 状态查询 ====================
# 内存存储 Agent 引用(用于查询构建状态)
AGENT_REFS: Dict[str, Dict] = {}
@router.get("/agents/{agent_ref_id}/build-status")
async def get_agent_build_status(agent_ref_id: str):
"""
查询 Agent CI/CD 构建状态
返回:
- Gitee Action 运行状态
- ACR 镜像是否已构建
- 整体构建状态
"""
try:
# 检查是否有存储的 agent 信息
agent_info = AGENT_REFS.get(agent_ref_id)
if not agent_info:
# 尝试从 repo_name 推断
if agent_ref_id.startswith("agent-"):
repo_name = agent_ref_id.replace("agent-", "", 1)
else:
return {
"success": False,
"error": "agent_not_found",
"message": f"Agent {agent_ref_id} 不存在或信息已丢失"
}
else:
repo_name = agent_info.get("repo_name", agent_ref_id.replace("agent-", "", 1))
# 查询 Gitee Action 状态
action_status = gitee_manager.get_action_status(repo_name)
# 查询 ACR 镜像状态
image_name = f"agnettaiji.azurecr.io/ai-agents/{repo_name}:latest"
acr_status = _check_acr_image(image_name)
# 判断整体状态
overall_status = "unknown"
if acr_status.get("exists"):
overall_status = "ready"
elif action_status.get("conclusion") == "failure":
overall_status = "build_failed"
elif action_status.get("status") == "in_progress":
overall_status = "building"
elif action_status.get("status") == "completed" and action_status.get("conclusion") == "success":
overall_status = "deployed"
elif action_status.get("status") == "no_runs":
overall_status = "pending"
# 计算域名
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
expected_domain = f"{k8s_name}.taijiagnet.com"
return {
"success": True,
"data": {
"agent_ref_id": agent_ref_id,
"repo_name": repo_name,
"overall_status": overall_status,
"gitee_action": {
"status": action_status.get("status"),
"conclusion": action_status.get("conclusion"),
"run_id": action_status.get("run_id"),
"html_url": action_status.get("html_url"),
"message": action_status.get("message")
},
"acr_image": {
"image_name": image_name,
"exists": acr_status.get("exists", False),
"tags": acr_status.get("all_tags", [])
},
"access_info": {
"expected_domain": expected_domain,
"expected_url": f"http://{expected_domain}",
"expected_namespace": f"agent-{k8s_name}"
}
}
}
except Exception as e:
logger.error(f"查询构建状态失败: {e}")
return {
"success": False,
"error": "query_failed",
"message": str(e)
}
@router.get("/agents/{agent_ref_id}/deployment-info")
async def get_agent_deployment_info(agent_ref_id: str):
"""
获取 Agent 部署后的详细信息
返回:
- K8s 部署状态
- 服务端点信息
- DNS 域名信息
- 访问 URL
"""
try:
# 从 agent_ref_id 推断 repo_name
if agent_ref_id.startswith("agent-"):
repo_name = agent_ref_id.replace("agent-", "", 1)
else:
repo_name = agent_ref_id
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
namespace = f"agent-{k8s_name}"
# 导入 K8sManager 查询实际状态
from k8s_manager import K8sManager
deployment_info = {
"agent_ref_id": agent_ref_id,
"repo_name": repo_name,
"namespace": namespace,
"k8s_status": {},
"access_info": {},
"image_info": {}
}
try:
k8s_mgr = K8sManager(namespace=namespace)
# 查询 namespace 是否存在
try:
ns = k8s_mgr.v1.read_namespace(name=namespace)
deployment_info["k8s_status"]["namespace_exists"] = True
deployment_info["k8s_status"]["namespace_status"] = ns.status.phase
except Exception:
deployment_info["k8s_status"]["namespace_exists"] = False
deployment_info["k8s_status"]["status"] = "not_deployed"
return {
"success": True,
"data": deployment_info,
"message": "Agent 尚未部署到 K8s"
}
# 查询 Pod 状态
try:
pods = k8s_mgr.v1.list_namespaced_pod(namespace=namespace)
if pods.items:
pod = pods.items[0]
deployment_info["k8s_status"]["pod_name"] = pod.metadata.name
deployment_info["k8s_status"]["pod_status"] = pod.status.phase
deployment_info["k8s_status"]["pod_ip"] = pod.status.pod_ip
deployment_info["k8s_status"]["node_name"] = pod.spec.node_name
deployment_info["k8s_status"]["status"] = "running" if pod.status.phase == "Running" else pod.status.phase.lower()
except Exception as e:
deployment_info["k8s_status"]["pod_error"] = str(e)
# 查询 Service 状态
try:
services = k8s_mgr.v1.list_namespaced_service(namespace=namespace)
for svc in services.items:
if svc.spec.type == "LoadBalancer":
deployment_info["access_info"]["service_name"] = svc.metadata.name
deployment_info["access_info"]["cluster_ip"] = svc.spec.cluster_ip
# 获取外网 IP
if svc.status.load_balancer.ingress:
external_ip = svc.status.load_balancer.ingress[0].ip
deployment_info["access_info"]["external_ip"] = external_ip
deployment_info["access_info"]["ip_url"] = f"http://{external_ip}"
except Exception as e:
deployment_info["access_info"]["service_error"] = str(e)
# DNS 域名信息
expected_domain = f"{k8s_name}.taijiagnet.com"
deployment_info["access_info"]["domain"] = expected_domain
deployment_info["access_info"]["domain_url"] = f"http://{expected_domain}"
deployment_info["access_info"]["recommended_url"] = f"http://{expected_domain}"
# 镜像信息
deployment_info["image_info"] = {
"image_name": f"agnettaiji.azurecr.io/ai-agents/{repo_name}:latest",
"registry": "agnettaiji.azurecr.io"
}
except Exception as e:
logger.error(f"查询 K8s 状态失败: {e}")
deployment_info["k8s_status"]["error"] = str(e)
return {
"success": True,
"data": deployment_info
}
except Exception as e:
logger.error(f"获取部署信息失败: {e}")
return {
"success": False,
"error": "query_failed",
"message": str(e)
}
def _check_acr_image(image_name: str) -> Dict[str, Any]:
"""检查 ACR 镜像是否存在"""
try:
import subprocess
# 解析镜像名称
parts = image_name.split("/")
if len(parts) < 3:
return {"exists": False, "error": "Invalid image name"}
registry = parts[0].replace(".azurecr.io", "")
repo = "/".join(parts[1:]).split(":")[0]
tag = parts[-1].split(":")[-1] if ":" in parts[-1] else "latest"
# 使用 az acr 命令检查
result = subprocess.run(
["az", "acr", "repository", "show-tags",
"--name", registry,
"--repository", repo,
"--output", "json"],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
import json
tags = json.loads(result.stdout)
if tag in tags:
return {
"exists": True,
"tag": tag,
"all_tags": tags
}
return {"exists": False}
except Exception as e:
logger.debug(f"检查 ACR 镜像失败: {e}")
return {"exists": False, "error": str(e)}