Files
agent_management/app.py
T
2026-01-06 09:26:11 +00:00

523 lines
15 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.
"""
FastAPI Web服务 - AI Agent管理服务
支持平台Agent和自定义Agent两种类型
"""
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel, Field
from typing import Dict, List, Optional
from sqlalchemy.orm import Session
from datetime import datetime
import logging
from k8s_manager import K8sManager
from database import (
get_db, Template, Agent, Quota, AgentMetric,
AgentType, AgentStatus, parse_resource_string
)
import os
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 创建FastAPI应用
app = FastAPI(
title="AI Agent Manager",
description="Kubernetes AI Agent管理服务",
version="1.0.0"
)
# 初始化K8s管理器
NAMESPACE = os.getenv("NAMESPACE", "ai-agents")
KUBECONFIG_PATH = os.getenv("KUBECONFIG_PATH", None) # 可选:指定kubeconfig路径
k8s_manager = K8sManager(namespace=NAMESPACE, kubeconfig_path=KUBECONFIG_PATH)
# ==================== 请求/响应模型 ====================
# Template Management Models
class CreateTemplateRequest(BaseModel):
"""创建模板请求"""
name: str = Field(..., min_length=1, max_length=100)
display_name: str
description: Optional[str] = None
agent_type: str = Field(..., description="platform or custom")
image: str
port: Optional[int] = None
env_requirements: Optional[Dict] = Field(default_factory=dict)
cpu_request: Optional[str] = None
cpu_limit: Optional[str] = None
memory_request: Optional[str] = None
memory_limit: Optional[str] = None
min_replicas: int = 1
max_replicas: int = 3
target_cpu_utilization: int = 80
class UpdateTemplateRequest(BaseModel):
"""更新模板请求"""
display_name: Optional[str] = None
description: Optional[str] = None
image: Optional[str] = None
port: Optional[int] = None
env_requirements: Optional[Dict] = None
cpu_request: Optional[str] = None
cpu_limit: Optional[str] = None
memory_request: Optional[str] = None
memory_limit: Optional[str] = None
min_replicas: Optional[int] = None
max_replicas: Optional[int] = None
target_cpu_utilization: Optional[int] = None
is_active: Optional[bool] = None
class TemplateResponse(BaseModel):
"""模板响应"""
id: int
name: str
display_name: str
description: Optional[str]
agent_type: str
image: str
port: Optional[int]
env_requirements: Dict
cpu_request: Optional[str]
cpu_limit: Optional[str]
memory_request: Optional[str]
memory_limit: Optional[str]
min_replicas: int
max_replicas: int
target_cpu_utilization: int
is_active: bool
created_at: datetime
class Config:
from_attributes = True
# Platform Agent Models
class CreatePlatformAgentRequest(BaseModel):
"""创建平台Agent请求"""
name: str = Field(..., min_length=1, max_length=63)
template_name: str
owner_id: str
channel_id: Optional[str] = None
tenant_id: Optional[str] = None
query_params: Optional[Dict] = Field(default_factory=dict)
# Custom Agent Models
class ScalingConfig(BaseModel):
"""弹性伸缩配置"""
min_replicas: int = Field(1, ge=0)
max_replicas: int = Field(3, ge=1)
target_cpu_utilization: int = Field(80, ge=1, le=100)
class CreateCustomAgentRequest(BaseModel):
"""创建自定义Agent请求"""
name: str = Field(..., min_length=1, max_length=63)
template_name: str
owner_id: str
channel_id: Optional[str] = None
tenant_id: Optional[str] = None
environment_vars: Dict[str, str]
cpu_request: Optional[str] = None
cpu_limit: Optional[str] = None
memory_request: Optional[str] = None
memory_limit: Optional[str] = None
scaling_config: Optional[ScalingConfig] = None
class UpdateAgentEnvRequest(BaseModel):
"""更新Agent环境变量请求"""
environment_vars: Dict[str, str]
class UpdateScalingRequest(BaseModel):
"""更新伸缩配置请求"""
min_replicas: Optional[int] = None
max_replicas: Optional[int] = None
target_cpu_utilization: Optional[int] = None
# Unified Agent Response
class AgentResponseNew(BaseModel):
"""Agent响应(新)"""
id: int
name: str
display_name: Optional[str]
template_name: str
agent_type: str
status: str
owner_id: str
channel_id: Optional[str]
tenant_id: Optional[str]
service_url: Optional[str]
current_replicas: int
min_replicas: int
max_replicas: int
created_at: datetime
last_accessed_at: Optional[datetime]
class Config:
from_attributes = True
# Legacy Models (for backward compatibility)
class CreateAgentRequest(BaseModel):
"""创建Agent请求(旧版)"""
name: str = Field(..., description="Agent名称", min_length=1, max_length=63)
template: str = Field(..., description="模板类型")
config: Dict = Field(default_factory=dict, description="配置信息")
env: Optional[Dict[str, str]] = Field(default_factory=dict, description="环境变量")
class AgentResponse(BaseModel):
"""Agent响应"""
name: str
namespace: str
status: str
created_at: Optional[str] = None
template: Optional[str] = None
service_port: Optional[int] = None
access_info: Optional[Dict] = None
pod_id: Optional[str] = None
pod_ip: Optional[str] = None
host_ip: Optional[str] = None
node_name: Optional[str] = None
owner_info: Optional[Dict] = None
class ResourceUsage(BaseModel):
"""资源使用情况"""
cpu: Optional[str] = None
memory: Optional[str] = None
available: Optional[bool] = None
reason: Optional[str] = None
class ResourceInfo(BaseModel):
"""资源信息(配额和使用情况)"""
requests: Optional[Dict] = None
limits: Optional[Dict] = None
usage: Optional[ResourceUsage] = None
class PodStatusResponse(BaseModel):
"""Pod状态响应"""
name: str
namespace: str
status: str
health_status: Optional[str] = None # 新增:健康状态 (healthy, unhealthy, degraded)
template: Optional[str] = None
created_at: Optional[str] = None
node: Optional[str] = None
pod_ip: Optional[str] = None
containers: Optional[List[Dict]] = None # 新增:容器详细信息
resources: Optional[ResourceInfo] = None
service_port: Optional[int] = None
access_url: Optional[str] = None
endpoints: Optional[Dict] = None
conditions: Optional[List[Dict]] = None
class PodMetricsResponse(BaseModel):
"""Pod资源使用响应"""
name: str
namespace: Optional[str] = None
requests: Dict
limits: Dict
usage: Optional[Dict] = None # 实时使用情况(需要 metrics-server)
timestamp: Optional[str] = None # metrics 时间戳
metrics_available: Optional[bool] = None # metrics-server 是否可用
class MessageResponse(BaseModel):
"""通用消息响应"""
status: str
message: str
@app.get("/")
async def root():
"""健康检查"""
return {
"service": "AI Agent Manager",
"status": "running",
"namespace": NAMESPACE
}
@app.post("/agents", response_model=AgentResponse)
async def create_agent(request: CreateAgentRequest):
"""
创建AI Agent Pod
Args:
request: 创建请求(name, template, config, user_id可选)
Returns:
创建的Agent信息包括pod_id
"""
try:
logger.info(f"收到创建Agent请求: {request.name}, 模板: {request.template}")
# 验证模板类型
valid_templates = ["echo_agent", "chat_agent", "code_agent", "search_agent", "mysql_agent", "postgresql_agent", "jina_search_agent"]
if request.template not in valid_templates:
raise HTTPException(
status_code=400,
detail=f"无效的模板类型。支持的模板: {', '.join(valid_templates)}"
)
# 合并环境变量到config
config_data = request.config.copy()
if request.env:
config_data["env"] = request.env
logger.info(f"环境变量: {list(request.env.keys())}")
# 添加 user_id 标签
user_id = config_data.get("user_id", "default")
if "labels" not in config_data:
config_data["labels"] = {}
config_data["labels"]["user-id"] = user_id
config_data["labels"]["managed-by"] = "agent-manager"
# 创建Pod
result = k8s_manager.create_pod(
pod_name=request.name,
template=request.template,
config_data=config_data
)
# 获取 Pod 详细信息(包括 pod_id)
try:
import time
time.sleep(1) # 等待 Pod 创建完成
pod = k8s_manager.v1.read_namespaced_pod(
name=request.name,
namespace=NAMESPACE
)
result["pod_id"] = pod.metadata.uid
result["pod_ip"] = pod.status.pod_ip
result["host_ip"] = pod.status.host_ip
result["node_name"] = pod.spec.node_name
result["owner_info"] = {
"user_id": user_id,
"agent_name": request.name,
"namespace": NAMESPACE,
"labels": pod.metadata.labels
}
logger.info(f"✅ Agent创建成功,Pod ID: {result['pod_id']}, 用户: {user_id}")
except Exception as e:
logger.warning(f"获取Pod详细信息失败: {str(e)}")
return AgentResponse(**result)
except Exception as e:
logger.error(f"创建Agent失败: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/agents/{agent_name}", response_model=MessageResponse)
async def delete_agent(agent_name: str):
"""
删除AI Agent Pod
Args:
agent_name: Agent名称
Returns:
删除结果
"""
try:
logger.info(f"收到删除Agent请求: {agent_name}")
result = k8s_manager.delete_pod(pod_name=agent_name)
if result.get("status") == "not_found":
raise HTTPException(status_code=404, detail=result.get("message"))
return MessageResponse(**result)
except HTTPException:
raise
except Exception as e:
logger.error(f"删除Agent失败: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/agents/{agent_name}/status", response_model=PodStatusResponse)
async def get_agent_status(agent_name: str):
"""
获取Agent状态
Args:
agent_name: Agent名称
Returns:
Agent状态信息
"""
try:
logger.info(f"获取Agent状态: {agent_name}")
result = k8s_manager.get_pod_status(pod_name=agent_name)
if result.get("status") == "not_found":
raise HTTPException(status_code=404, detail=result.get("message"))
return PodStatusResponse(**result)
except HTTPException:
raise
except Exception as e:
logger.error(f"获取Agent状态失败: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/agents/{agent_name}/metrics", response_model=PodMetricsResponse)
async def get_agent_metrics(agent_name: str):
"""
获取Agent资源使用情况
Args:
agent_name: Agent名称
Returns:
Agent资源使用信息
"""
try:
logger.info(f"获取Agent资源信息: {agent_name}")
result = k8s_manager.get_pod_metrics(pod_name=agent_name)
return PodMetricsResponse(**result)
except Exception as e:
logger.error(f"获取Agent资源信息失败: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/agents")
async def list_agents(template: Optional[str] = None):
"""
列出所有Agent
Args:
template: 模板类型过滤(可选)
Returns:
Agent列表
"""
try:
logger.info(f"列出Agents, 模板过滤: {template}")
label_selector = "managed-by=agent-manager"
if template:
label_selector += f",template={template}"
result = k8s_manager.list_pods(label_selector=label_selector)
return {"agents": result, "count": len(result)}
except Exception as e:
logger.error(f"列出Agents失败: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/templates")
async def list_templates():
"""
列出所有可用的Agent模板及其所需参数
Returns:
模板列表及其配置信息
"""
valid_templates = ["echo_agent", "chat_agent", "code_agent", "search_agent", "mysql_agent", "postgresql_agent", "jina_search_agent"]
templates_info = []
for template in valid_templates:
info = k8s_manager.get_template_info(template)
templates_info.append(info)
return {
"templates": templates_info,
"count": len(templates_info)
}
@app.get("/templates/platform")
async def list_platform_templates():
"""
获取平台 Agent 镜像列表
Returns:
平台提供的Agent模板列表
"""
# 平台 Agent 是预定义的标准模板
platform_templates = ["echo_agent", "chat_agent", "code_agent", "search_agent", "jina_search_agent"]
templates_info = []
for template in platform_templates:
info = k8s_manager.get_template_info(template)
info["type"] = "platform"
templates_info.append(info)
return {
"templates": templates_info,
"count": len(templates_info),
"type": "platform"
}
@app.get("/templates/custom")
async def list_custom_templates():
"""
获取自定义 Agent 镜像列表
Returns:
用户自定义的Agent模板列表
"""
# 自定义 Agent 是用户可以配置数据库连接的模板
custom_templates = ["mysql_agent", "postgresql_agent"]
templates_info = []
for template in custom_templates:
info = k8s_manager.get_template_info(template)
info["type"] = "custom"
templates_info.append(info)
return {
"templates": templates_info,
"count": len(templates_info),
"type": "custom"
}
@app.get("/templates/{template_name}")
async def get_template_info(template_name: str):
"""
获取指定模板的详细信息
Args:
template_name: 模板名称
Returns:
模板详细信息(端口、所需环境变量等)
"""
valid_templates = ["echo_agent", "chat_agent", "code_agent", "search_agent", "mysql_agent", "postgresql_agent", "jina_search_agent"]
if template_name not in valid_templates:
raise HTTPException(
status_code=404,
detail=f"模板 {template_name} 不存在。可用模板: {', '.join(valid_templates)}"
)
return k8s_manager.get_template_info(template_name)
if __name__ == "__main__":
import uvicorn
host = os.getenv("SERVICE_HOST", "0.0.0.0")
port = int(os.getenv("SERVICE_PORT", "8000"))
logger.info(f"启动AI Agent Manager服务: {host}:{port}")
uvicorn.run(app, host=host, port=port)