forked from xiaohei/taiji-AI-PAD
912 lines
32 KiB
Python
912 lines
32 KiB
Python
"""Agent CRUD and execution endpoints with Kubernetes integration."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import time
|
||
import uuid
|
||
import structlog
|
||
from datetime import datetime
|
||
from decimal import Decimal
|
||
from typing import List, Optional, Dict, Any
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from database import get_db
|
||
from models import Agent, User, Billing, Execution, Balance, Session, TenantCustomAgentQuota
|
||
from schemas import (
|
||
AgentCard, AgentCreateRequest, ExecutionResult, MCPRequest,
|
||
SessionCreate, SessionResponse,
|
||
AgentStatusResponse, AgentMetricsResponse, TemplateInfo, TemplateListResponse,
|
||
K8sResourceConfig
|
||
)
|
||
from ..metrics import (
|
||
agents_queries_total,
|
||
agents_registered_total,
|
||
mcp_request_duration,
|
||
mcp_requests_total,
|
||
)
|
||
from ..state import get_state
|
||
from ..utils import record_tool_metrics
|
||
from ..auth import get_current_user
|
||
from ..resource_control import enforce_resource_control, resource_controller
|
||
from ..agent_manager_client import (
|
||
get_agent_manager_client,
|
||
AgentConfig,
|
||
AgentManagerError,
|
||
)
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
router = APIRouter(prefix="/agents", tags=["agents"])
|
||
|
||
|
||
def _build_agent_card(agent: Agent) -> AgentCard:
|
||
"""构建 Agent 卡片信息"""
|
||
return AgentCard(
|
||
id=agent.id,
|
||
name=agent.name,
|
||
description=agent.description,
|
||
role=agent.role,
|
||
goal=agent.goal,
|
||
tools=agent.tools or [],
|
||
capabilities=agent.capabilities or [],
|
||
endpoints=agent.endpoints or {
|
||
"mcp": f"mcp://localhost:8002/agents/{agent.id}",
|
||
"http": f"http://localhost:8002/agents/{agent.id}",
|
||
"websocket": f"ws://localhost:8002/agents/{agent.id}/ws",
|
||
},
|
||
status=agent.status,
|
||
version=agent.version,
|
||
# K8s 相关信息
|
||
template=agent.template,
|
||
pod_name=agent.pod_name,
|
||
pod_ip=agent.pod_ip,
|
||
k8s_status=agent.k8s_status,
|
||
service_port=agent.service_port,
|
||
access_url=agent.access_url,
|
||
# 模型配置
|
||
model_name=agent.model_name,
|
||
# 资源配置
|
||
cpu_request=agent.cpu_request,
|
||
cpu_limit=agent.cpu_limit,
|
||
memory_request=agent.memory_request,
|
||
memory_limit=agent.memory_limit,
|
||
# 统计信息
|
||
total_executions=agent.total_executions,
|
||
success_rate=agent.success_rate,
|
||
avg_execution_time=agent.avg_execution_time,
|
||
created_at=agent.created_at,
|
||
updated_at=agent.updated_at,
|
||
pod_created_at=agent.pod_created_at,
|
||
)
|
||
|
||
|
||
async def _get_balance(db: AsyncSession, user_id: uuid.UUID, for_update: bool = False) -> Balance:
|
||
"""
|
||
获取或创建用户余额记录
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
user_id: 用户ID
|
||
for_update: 是否使用行锁(用于更新操作)
|
||
|
||
Returns:
|
||
Balance 对象
|
||
"""
|
||
query = select(Balance).where(Balance.user_id == user_id)
|
||
if for_update:
|
||
query = query.with_for_update() # 行锁保护并发更新
|
||
|
||
result = await db.execute(query)
|
||
balance = result.scalar_one_or_none()
|
||
if balance is None:
|
||
balance = Balance(user_id=user_id, eu_balance=0.0)
|
||
db.add(balance)
|
||
await db.flush()
|
||
return balance
|
||
|
||
|
||
# ==================== 模板管理 ====================
|
||
|
||
@router.get("/templates", response_model=TemplateListResponse)
|
||
async def list_templates() -> TemplateListResponse:
|
||
"""获取所有可用的 Agent 模板"""
|
||
try:
|
||
client = get_agent_manager_client()
|
||
templates = await client.list_templates()
|
||
|
||
return TemplateListResponse(
|
||
templates=[
|
||
TemplateInfo(
|
||
template=t.template,
|
||
displayName=t.display_name,
|
||
description=t.description,
|
||
category=t.category,
|
||
port=t.port,
|
||
env_info=t.env_info
|
||
)
|
||
for t in templates
|
||
],
|
||
count=len(templates)
|
||
)
|
||
except AgentManagerError as e:
|
||
logger.error("获取模板列表失败", error=str(e))
|
||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||
except Exception as e:
|
||
logger.error("获取模板列表失败", error=str(e))
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.get("/templates/platform", response_model=TemplateListResponse)
|
||
async def list_platform_templates() -> TemplateListResponse:
|
||
"""
|
||
获取所有平台 Agent 模板
|
||
|
||
平台 Agent 模板是预定义的、由平台管理的 Agent 类型,
|
||
用户无需配置环境变量即可使用。
|
||
"""
|
||
try:
|
||
client = get_agent_manager_client()
|
||
templates = await client.list_platform_templates()
|
||
|
||
return TemplateListResponse(
|
||
templates=[
|
||
TemplateInfo(
|
||
template=t.template,
|
||
displayName=t.display_name,
|
||
description=t.description,
|
||
category=t.category,
|
||
port=t.port,
|
||
env_info=t.env_info
|
||
)
|
||
for t in templates
|
||
],
|
||
count=len(templates),
|
||
type="platform"
|
||
)
|
||
except AgentManagerError as e:
|
||
logger.error("获取平台模板列表失败", error=str(e))
|
||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||
except Exception as e:
|
||
logger.error("获取平台模板列表失败", error=str(e))
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.get("/templates/custom", response_model=TemplateListResponse)
|
||
async def list_custom_templates() -> TemplateListResponse:
|
||
"""
|
||
获取所有自定义 Agent 模板
|
||
|
||
自定义 Agent 模板需要用户配置环境变量(如 API Key、数据库连接信息)。
|
||
env_info 字段包含 required(必需)和 optional(可选)环境变量说明。
|
||
"""
|
||
try:
|
||
client = get_agent_manager_client()
|
||
templates = await client.list_custom_templates()
|
||
|
||
return TemplateListResponse(
|
||
templates=[
|
||
TemplateInfo(
|
||
template=t.template,
|
||
displayName=t.display_name,
|
||
description=t.description,
|
||
category=t.category,
|
||
port=t.port,
|
||
env_info=t.env_info
|
||
)
|
||
for t in templates
|
||
],
|
||
count=len(templates),
|
||
type="custom"
|
||
)
|
||
except AgentManagerError as e:
|
||
logger.error("获取自定义模板列表失败", error=str(e))
|
||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||
except Exception as e:
|
||
logger.error("获取自定义模板列表失败", error=str(e))
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.get("/templates/{template_name}", response_model=TemplateInfo)
|
||
async def get_template(template_name: str) -> TemplateInfo:
|
||
"""获取模板详情"""
|
||
try:
|
||
client = get_agent_manager_client()
|
||
template = await client.get_template(template_name)
|
||
|
||
return TemplateInfo(
|
||
template=template.template,
|
||
port=template.port,
|
||
env_info=template.env_info
|
||
)
|
||
except AgentManagerError as e:
|
||
logger.error("获取模板详情失败", template=template_name, error=str(e))
|
||
raise HTTPException(status_code=e.status_code, detail=e.detail)
|
||
except Exception as e:
|
||
logger.error("获取模板详情失败", template=template_name, error=str(e))
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ==================== Agent CRUD ====================
|
||
|
||
@router.post("", response_model=AgentCard)
|
||
async def create_agent(
|
||
request: AgentCreateRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: dict = Depends(get_current_user),
|
||
) -> AgentCard:
|
||
"""
|
||
创建新的 Agent。
|
||
|
||
如果提供了 template,将在 Kubernetes 中创建对应的 Pod。
|
||
"""
|
||
state = get_state()
|
||
|
||
try:
|
||
user_id = uuid.UUID(current_user["user_id"])
|
||
|
||
# ========== 资源管控检查 ==========
|
||
await enforce_resource_control(
|
||
user_id=str(user_id),
|
||
resource_type="agent",
|
||
resource_id=None,
|
||
estimated_cost=Decimal("0.0"),
|
||
db=db
|
||
)
|
||
# ==================================
|
||
|
||
# 准备资源配置
|
||
resource_config = request.resource_config or K8sResourceConfig()
|
||
|
||
# 创建数据库记录
|
||
agent = Agent(
|
||
name=request.name,
|
||
description=request.description,
|
||
role=request.role,
|
||
goal=request.goal,
|
||
tools=request.tools,
|
||
config=request.config,
|
||
capabilities=request.capabilities,
|
||
owner_id=user_id,
|
||
# K8s 相关字段
|
||
template=request.template,
|
||
cpu_request=resource_config.cpu_request,
|
||
cpu_limit=resource_config.cpu_limit,
|
||
memory_request=resource_config.memory_request,
|
||
memory_limit=resource_config.memory_limit,
|
||
env_config=resource_config.env,
|
||
k8s_status="Unknown",
|
||
)
|
||
db.add(agent)
|
||
await db.flush() # 获取 agent.id
|
||
|
||
# 如果指定了模板,创建 K8s Pod
|
||
if request.template:
|
||
try:
|
||
client = get_agent_manager_client()
|
||
|
||
# 构建 Pod 名称(使用 agent ID 确保唯一性)
|
||
pod_name = f"{request.name}-{str(agent.id)[:8]}"
|
||
|
||
# 创建 Agent 配置(适配新的 AgentConfig 格式)
|
||
agent_config = AgentConfig(
|
||
user_id=str(user_id),
|
||
cpu_request=resource_config.cpu_request,
|
||
cpu_limit=resource_config.cpu_limit,
|
||
memory_request=resource_config.memory_request,
|
||
memory_limit=resource_config.memory_limit,
|
||
replicas=1, # 默认单副本
|
||
)
|
||
|
||
# 调用 Agent Manager API 创建 Pod
|
||
# 使用统一的 POST /agents 接口
|
||
result = await client.create_agent(
|
||
name=pod_name,
|
||
template=request.template,
|
||
config=agent_config,
|
||
env=resource_config.env # 环境变量单独传递
|
||
)
|
||
|
||
# 更新数据库记录
|
||
agent.pod_name = result.name
|
||
agent.k8s_namespace = result.namespace
|
||
agent.k8s_status = result.status
|
||
agent.service_port = result.service_port
|
||
# 解析时间并移除时区信息(数据库使用 TIMESTAMP WITHOUT TIME ZONE)
|
||
pod_created = datetime.fromisoformat(result.created_at.replace("Z", "+00:00"))
|
||
agent.pod_created_at = pod_created.replace(tzinfo=None)
|
||
|
||
if result.access_info:
|
||
agent.endpoints = result.access_info.get("endpoints", {})
|
||
|
||
logger.info(
|
||
"K8s Pod 创建成功",
|
||
agent_id=str(agent.id),
|
||
pod_name=result.name,
|
||
status=result.status
|
||
)
|
||
|
||
except AgentManagerError as e:
|
||
# K8s 创建失败,回滚数据库
|
||
await db.rollback()
|
||
# Ensure detail is serializable for logging
|
||
detail_info = e.detail
|
||
try:
|
||
import json as _json
|
||
detail_serialized = _json.dumps(detail_info, ensure_ascii=False)
|
||
except Exception:
|
||
detail_serialized = str(detail_info)
|
||
|
||
logger.error(
|
||
"创建 K8s Pod 失败",
|
||
error=str(e),
|
||
status_code=e.status_code,
|
||
detail=detail_serialized
|
||
)
|
||
|
||
raise HTTPException(
|
||
status_code=e.status_code,
|
||
detail={
|
||
"error": "k8s_creation_failed",
|
||
"message": f"创建 Kubernetes Pod 失败: {e.message}",
|
||
"detail": detail_info
|
||
}
|
||
)
|
||
|
||
await db.commit()
|
||
await db.refresh(agent)
|
||
|
||
card = _build_agent_card(agent)
|
||
|
||
# 缓存到 Redis
|
||
redis_client = state.redis_client
|
||
if redis_client:
|
||
await redis_client.setex(
|
||
f"agent:{agent.id}", 3600, json.dumps(card.model_dump(mode="json"), ensure_ascii=False)
|
||
)
|
||
|
||
# 注册Agent的工具权限到Redis
|
||
if agent.tools:
|
||
agent_tools_key = f"agent:{agent.id}:tools"
|
||
for tool_name in agent.tools:
|
||
await redis_client.sadd(agent_tools_key, tool_name)
|
||
await redis_client.expire(agent_tools_key, 86400) # 24小时过期
|
||
|
||
# 同时注册工具信息到Redis(如果是内置函数)
|
||
from function_registry import get_function_registry
|
||
func_registry = get_function_registry()
|
||
for tool_name in agent.tools:
|
||
func_info = func_registry.get(tool_name)
|
||
if func_info:
|
||
tool_data = {
|
||
"name": tool_name,
|
||
"description": func_info.get("description", ""),
|
||
"category": "function",
|
||
"schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
p["name"]: {"type": p.get("type", "string"), "description": p.get("description", "")}
|
||
for p in func_info.get("parameters", [])
|
||
},
|
||
"required": [p["name"] for p in func_info.get("parameters", []) if p.get("required", True)]
|
||
}
|
||
}
|
||
await redis_client.setex(
|
||
f"tool:{tool_name}", 86400, json.dumps(tool_data, ensure_ascii=False)
|
||
)
|
||
logger.info(f"已为Agent {agent.id} 注册 {len(agent.tools)} 个工具")
|
||
|
||
# 发布事件到 NATS
|
||
nats_client = state.nats_client
|
||
if nats_client:
|
||
await nats_client.publish(
|
||
"agent.created",
|
||
json.dumps(
|
||
{
|
||
"agent_id": str(agent.id),
|
||
"name": agent.name,
|
||
"template": agent.template,
|
||
"pod_name": agent.pod_name,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
).encode(),
|
||
)
|
||
|
||
agents_registered_total.labels(status="success").inc()
|
||
logger.info("Agent创建成功", agent_id=str(agent.id), template=agent.template)
|
||
return card
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as exc:
|
||
await db.rollback()
|
||
agents_registered_total.labels(status="error").inc()
|
||
logger.error("创建Agent失败", error=str(exc))
|
||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||
|
||
|
||
@router.get("", response_model=List[AgentCard])
|
||
async def list_agents(
|
||
skip: int = 0,
|
||
limit: int = 100,
|
||
template: Optional[str] = Query(None, description="按模板类型过滤"),
|
||
db: AsyncSession = Depends(get_db)
|
||
) -> List[AgentCard]:
|
||
"""返回分页的 Agent 列表"""
|
||
state = get_state()
|
||
redis_client = state.redis_client
|
||
|
||
try:
|
||
agents_queries_total.labels(operation="list").inc()
|
||
|
||
query = select(Agent).order_by(Agent.created_at.desc())
|
||
|
||
# 按模板过滤
|
||
if template:
|
||
query = query.where(Agent.template == template)
|
||
|
||
query = query.offset(skip).limit(limit)
|
||
result = await db.execute(query)
|
||
agent_rows = result.scalars().all()
|
||
|
||
cards: List[AgentCard] = []
|
||
for agent in agent_rows:
|
||
card = _build_agent_card(agent)
|
||
cards.append(card)
|
||
if redis_client:
|
||
await redis_client.setex(
|
||
f"agent:{agent.id}", 3600, json.dumps(card.model_dump(mode="json"), ensure_ascii=False)
|
||
)
|
||
return cards
|
||
except Exception as exc:
|
||
logger.error("获取Agent列表失败", error=str(exc))
|
||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||
|
||
|
||
@router.get("/{agent_id}", response_model=AgentCard)
|
||
async def get_agent(agent_id: str, db: AsyncSession = Depends(get_db)) -> AgentCard:
|
||
"""获取指定 Agent 的详细信息"""
|
||
state = get_state()
|
||
redis_client = state.redis_client
|
||
|
||
try:
|
||
agents_queries_total.labels(operation="get").inc()
|
||
|
||
if redis_client:
|
||
cached = await redis_client.get(f"agent:{agent_id}")
|
||
if cached:
|
||
return AgentCard.parse_raw(cached)
|
||
|
||
agent_uuid = uuid.UUID(agent_id)
|
||
agent = await db.get(Agent, agent_uuid)
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="Agent not found")
|
||
|
||
card = _build_agent_card(agent)
|
||
if redis_client:
|
||
await redis_client.setex(
|
||
f"agent:{agent.id}", 3600, json.dumps(card.model_dump(mode="json"), ensure_ascii=False)
|
||
)
|
||
return card
|
||
except ValueError as exc:
|
||
raise HTTPException(status_code=400, detail="Invalid agent ID") from exc
|
||
except HTTPException:
|
||
raise
|
||
except Exception as exc:
|
||
logger.error("获取Agent失败", error=str(exc))
|
||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||
|
||
|
||
@router.delete("/{agent_id}")
|
||
async def delete_agent(
|
||
agent_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: dict = Depends(get_current_user),
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
删除 Agent。
|
||
|
||
如果 Agent 有关联的 K8s Pod,也会一并删除。
|
||
如果是自定义 Agent,会释放用户的配额。
|
||
"""
|
||
try:
|
||
agent_uuid = uuid.UUID(agent_id)
|
||
user_id = uuid.UUID(current_user["user_id"])
|
||
except ValueError as exc:
|
||
raise HTTPException(status_code=400, detail="Invalid agent ID") from exc
|
||
|
||
agent = await db.get(Agent, agent_uuid)
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="Agent not found")
|
||
|
||
# 检查权限
|
||
if agent.owner_id != user_id and current_user.get("role") != "super_admin":
|
||
raise HTTPException(status_code=403, detail="Access denied")
|
||
|
||
# 保存 Agent 信息用于后续处理
|
||
agent_name = agent.name
|
||
agent_type = agent.type
|
||
agent_cpu = float(agent.cpu or 0)
|
||
agent_memory = float(agent.memory or 0)
|
||
agent_owner_id = agent.owner_id
|
||
|
||
# 如果有 Pod,先删除 K8s Pod
|
||
if agent.pod_name:
|
||
try:
|
||
client = get_agent_manager_client()
|
||
await client.delete_agent(agent.pod_name)
|
||
logger.info("K8s Pod 删除成功", pod_name=agent.pod_name)
|
||
except AgentManagerError as e:
|
||
# 如果 Pod 不存在(404),继续删除数据库记录
|
||
if e.status_code != 404:
|
||
logger.error("删除 K8s Pod 失败", pod_name=agent.pod_name, error=str(e))
|
||
raise HTTPException(
|
||
status_code=e.status_code,
|
||
detail={
|
||
"error": "k8s_deletion_failed",
|
||
"message": f"删除 Kubernetes Pod 失败: {e.message}",
|
||
"detail": e.detail
|
||
}
|
||
)
|
||
|
||
# 如果是自定义 Agent,释放用户配额
|
||
if agent_type == "custom" and agent_owner_id:
|
||
quota_result = await db.execute(
|
||
select(TenantCustomAgentQuota).where(
|
||
TenantCustomAgentQuota.tenant_id == agent_owner_id
|
||
)
|
||
)
|
||
quota = quota_result.scalar_one_or_none()
|
||
|
||
if quota:
|
||
# 释放配额
|
||
quota.cpu_used = max(0, float(quota.cpu_used or 0) - agent_cpu)
|
||
quota.memory_used = max(0, float(quota.memory_used or 0) - agent_memory)
|
||
quota.agent_count = max(0, (quota.agent_count or 0) - 1)
|
||
logger.info(
|
||
"释放自定义 Agent 配额",
|
||
agent_id=agent_id,
|
||
cpu_released=agent_cpu,
|
||
memory_released=agent_memory
|
||
)
|
||
|
||
# 删除数据库记录
|
||
await db.delete(agent)
|
||
await db.commit()
|
||
|
||
# 清除缓存
|
||
state = get_state()
|
||
redis_client = state.redis_client
|
||
if redis_client:
|
||
await redis_client.delete(f"agent:{agent_id}")
|
||
|
||
# 发布事件
|
||
nats_client = state.nats_client
|
||
if nats_client:
|
||
await nats_client.publish(
|
||
"agent.deleted",
|
||
json.dumps(
|
||
{
|
||
"agent_id": agent_id,
|
||
"name": agent_name,
|
||
"type": agent_type,
|
||
"pod_name": agent.pod_name,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
}
|
||
).encode(),
|
||
)
|
||
|
||
logger.info("Agent删除成功", agent_id=agent_id, type=agent_type)
|
||
return {"status": "success", "message": f"Agent {agent_name} 已删除"}
|
||
|
||
|
||
# ==================== Agent 状态和监控 ====================
|
||
|
||
@router.get("/{agent_id}/status", response_model=AgentStatusResponse)
|
||
async def get_agent_status(
|
||
agent_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> AgentStatusResponse:
|
||
"""
|
||
获取 Agent 的实时状态。
|
||
|
||
如果 Agent 有关联的 K8s Pod,会从 Agent Manager 获取最新状态。
|
||
"""
|
||
try:
|
||
agent_uuid = uuid.UUID(agent_id)
|
||
except ValueError as exc:
|
||
raise HTTPException(status_code=400, detail="Invalid agent ID") from exc
|
||
|
||
agent = await db.get(Agent, agent_uuid)
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="Agent not found")
|
||
|
||
# 如果有 Pod,获取实时状态
|
||
if agent.pod_name:
|
||
try:
|
||
client = get_agent_manager_client()
|
||
status = await client.get_agent_status(agent.pod_name)
|
||
|
||
# 更新数据库中的状态(适配新的 AgentStatusResult 格式)
|
||
agent.k8s_status = status.status
|
||
agent.pod_ip = status.pod_ip
|
||
if status.endpoints:
|
||
agent.endpoints = status.endpoints
|
||
|
||
await db.commit()
|
||
|
||
return AgentStatusResponse(
|
||
id=agent.id,
|
||
name=agent.name,
|
||
status=agent.status,
|
||
k8s_status=status.status,
|
||
pod_name=status.name,
|
||
pod_ip=status.pod_ip,
|
||
node=status.node_name, # 字段名从 node 改为 node_name
|
||
service_port=agent.service_port, # 从数据库获取,新接口不返回此字段
|
||
access_url=agent.access_url, # 从数据库获取,新接口不返回此字段
|
||
endpoints=status.endpoints or {},
|
||
cpu_request=agent.cpu_request,
|
||
cpu_limit=agent.cpu_limit,
|
||
memory_request=agent.memory_request,
|
||
memory_limit=agent.memory_limit,
|
||
created_at=agent.created_at,
|
||
pod_created_at=agent.pod_created_at,
|
||
conditions=None, # 新接口不返回 conditions
|
||
)
|
||
|
||
except AgentManagerError as e:
|
||
logger.warning("获取 Pod 状态失败", pod_name=agent.pod_name, error=str(e))
|
||
# 返回数据库中的状态
|
||
|
||
# 返回数据库中的状态
|
||
return AgentStatusResponse(
|
||
id=agent.id,
|
||
name=agent.name,
|
||
status=agent.status,
|
||
k8s_status=agent.k8s_status or "Unknown",
|
||
pod_name=agent.pod_name,
|
||
pod_ip=agent.pod_ip,
|
||
service_port=agent.service_port,
|
||
access_url=agent.access_url,
|
||
endpoints=agent.endpoints or {},
|
||
cpu_request=agent.cpu_request,
|
||
cpu_limit=agent.cpu_limit,
|
||
memory_request=agent.memory_request,
|
||
memory_limit=agent.memory_limit,
|
||
created_at=agent.created_at,
|
||
pod_created_at=agent.pod_created_at,
|
||
)
|
||
|
||
|
||
@router.get("/{agent_id}/metrics", response_model=AgentMetricsResponse)
|
||
async def get_agent_metrics(
|
||
agent_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
) -> AgentMetricsResponse:
|
||
"""
|
||
获取 Agent 的资源使用情况。
|
||
"""
|
||
try:
|
||
agent_uuid = uuid.UUID(agent_id)
|
||
except ValueError as exc:
|
||
raise HTTPException(status_code=400, detail="Invalid agent ID") from exc
|
||
|
||
agent = await db.get(Agent, agent_uuid)
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="Agent not found")
|
||
|
||
# 如果有 Pod,获取实时资源使用
|
||
if agent.pod_name:
|
||
try:
|
||
client = get_agent_manager_client()
|
||
metrics = await client.get_agent_metrics(agent.pod_name)
|
||
|
||
return AgentMetricsResponse(
|
||
id=agent.id,
|
||
name=agent.name,
|
||
requests=metrics.requests,
|
||
limits=metrics.limits,
|
||
)
|
||
|
||
except AgentManagerError as e:
|
||
logger.warning("获取 Pod 资源使用失败", pod_name=agent.pod_name, error=str(e))
|
||
|
||
# 返回数据库中的配置
|
||
return AgentMetricsResponse(
|
||
id=agent.id,
|
||
name=agent.name,
|
||
requests={
|
||
"cpu": agent.cpu_request or "100m",
|
||
"memory": agent.memory_request or "128Mi",
|
||
},
|
||
limits={
|
||
"cpu": agent.cpu_limit or "500m",
|
||
"memory": agent.memory_limit or "512Mi",
|
||
},
|
||
)
|
||
|
||
|
||
# ==================== Agent 执行 ====================
|
||
|
||
|
||
@router.post("/{agent_id}/execute", response_model=ExecutionResult)
|
||
async def execute_agent(
|
||
agent_id: str,
|
||
request: MCPRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: dict = Depends(get_current_user),
|
||
session_id: Optional[str] = None,
|
||
) -> ExecutionResult:
|
||
"""Execute an MCP request for a given agent with session support."""
|
||
state = get_state()
|
||
handler = state.mcp_handler
|
||
if not handler:
|
||
raise HTTPException(status_code=500, detail="MCP handler not initialized")
|
||
|
||
try:
|
||
agent_uuid = uuid.UUID(agent_id)
|
||
user_id = uuid.UUID(current_user["user_id"])
|
||
except ValueError as exc:
|
||
raise HTTPException(status_code=400, detail="Invalid agent ID or user ID") from exc
|
||
|
||
agent = await db.get(Agent, agent_uuid)
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="Agent not found")
|
||
|
||
# Check if user owns the agent
|
||
if agent.owner_id != user_id and current_user.get("role") != "super_admin":
|
||
raise HTTPException(status_code=403, detail="Access denied")
|
||
|
||
# ========== 资源管控检查 ==========
|
||
# 执行前检查用户配额和速率限制
|
||
await enforce_resource_control(
|
||
user_id=str(user_id),
|
||
resource_type="agent",
|
||
resource_id=agent_id,
|
||
estimated_cost=Decimal("0.01"), # 可根据agent类型动态计算
|
||
db=db
|
||
)
|
||
# ==================================
|
||
|
||
# Get or create session
|
||
session = None
|
||
if session_id:
|
||
result_query = await db.execute(
|
||
select(Session).where(
|
||
Session.session_id == session_id,
|
||
Session.user_id == user_id
|
||
)
|
||
)
|
||
session = result_query.scalar_one_or_none()
|
||
|
||
if not session:
|
||
# Create new session
|
||
session = Session(
|
||
session_id=session_id or str(uuid.uuid4()),
|
||
user_id=user_id,
|
||
context={"agent_id": agent_id, "request_history": []},
|
||
session_metadata={"created_via": "agent_execution"},
|
||
status="active",
|
||
)
|
||
db.add(session)
|
||
await db.flush()
|
||
|
||
start_time = time.time()
|
||
|
||
try:
|
||
mcp_requests_total.labels(method=request.method, status="processing").inc()
|
||
|
||
# Pass session context and resource control parameters to handler
|
||
result = await handler.execute_request(
|
||
agent_id,
|
||
request,
|
||
user_id=str(user_id),
|
||
db_session=db
|
||
)
|
||
|
||
duration = time.time() - start_time
|
||
mcp_requests_total.labels(method=request.method, status="success").inc()
|
||
mcp_request_duration.labels(method=request.method).observe(duration)
|
||
|
||
record_tool_metrics(request, duration, success=True)
|
||
|
||
nats_client = state.nats_client
|
||
if nats_client:
|
||
await nats_client.publish(
|
||
f"agent.execution.{agent_id}",
|
||
json.dumps(
|
||
{
|
||
"agent_id": agent_id,
|
||
"request_id": request.id,
|
||
"method": request.method,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"success": result.success,
|
||
}
|
||
).encode(),
|
||
)
|
||
|
||
# 记录执行与计费
|
||
execution = Execution(
|
||
execution_id=str(request.id),
|
||
method=request.method,
|
||
params=request.params or {},
|
||
result=result.result if result.success else {},
|
||
error=result.error,
|
||
started_at=datetime.utcfromtimestamp(start_time),
|
||
completed_at=datetime.utcnow(),
|
||
execution_time=duration,
|
||
status="completed" if result.success else "failed",
|
||
cpu_usage=result.cpu_usage if hasattr(result, "cpu_usage") else 0.0,
|
||
memory_usage=result.memory_usage if hasattr(result, "memory_usage") else 0.0,
|
||
network_io=result.network_io if hasattr(result, "network_io") else 0.0,
|
||
eu_consumed=max(duration / 10.0, 0.0),
|
||
agent_id=agent.id,
|
||
session_id=session.id if session else None,
|
||
)
|
||
|
||
# Update session context with execution history
|
||
if session:
|
||
context = session.context or {}
|
||
request_history = context.get("request_history", [])
|
||
request_history.append({
|
||
"execution_id": str(execution.id),
|
||
"method": request.method,
|
||
"timestamp": datetime.utcnow().isoformat(),
|
||
"success": result.success,
|
||
})
|
||
# Keep only last 50 requests in session
|
||
context["request_history"] = request_history[-50:]
|
||
session.context = context
|
||
session.updated_at = datetime.utcnow()
|
||
db.add(session)
|
||
db.add(execution)
|
||
await db.flush()
|
||
|
||
billing = Billing(
|
||
execution_id=execution.id,
|
||
eu_consumed=execution.eu_consumed,
|
||
cost=execution.eu_consumed,
|
||
currency="EU",
|
||
cpu_time=execution.cpu_usage,
|
||
memory_max=execution.memory_usage,
|
||
network_io=execution.network_io,
|
||
storage_io=0.0,
|
||
user_id=agent.owner_id,
|
||
)
|
||
db.add(billing)
|
||
|
||
# 使用行锁保护余额更新,防止并发扣款
|
||
balance = await _get_balance(db, agent.owner_id, for_update=True)
|
||
balance.eu_balance = (balance.eu_balance or 0) - execution.eu_consumed
|
||
|
||
# ========== 记录资源消耗 ==========
|
||
# 记录到资源监控系统
|
||
await resource_controller.record_resource_consumption(
|
||
user_id=str(agent.owner_id),
|
||
resource_type="agent",
|
||
resource_id=agent_id,
|
||
cost=Decimal(str(execution.eu_consumed)),
|
||
execution_time_ms=duration * 1000,
|
||
cpu_usage=execution.cpu_usage,
|
||
memory_usage=execution.memory_usage,
|
||
network_io=execution.network_io,
|
||
db=db
|
||
)
|
||
# ==================================
|
||
|
||
await db.commit()
|
||
|
||
return result
|
||
except Exception as exc:
|
||
duration = time.time() - start_time
|
||
mcp_requests_total.labels(method=request.method, status="error").inc()
|
||
mcp_request_duration.labels(method=request.method).observe(duration)
|
||
record_tool_metrics(request, duration, success=False)
|
||
logger.error("执行Agent任务失败", error=str(exc))
|
||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||
|
||
|