1034 lines
36 KiB
Python
1034 lines
36 KiB
Python
"""
|
|
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 sqlalchemy import func
|
|
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管理服务 - 支持平台Agent和自定义Agent",
|
|
version="2.0.0"
|
|
)
|
|
|
|
# 初始化K8s管理器
|
|
NAMESPACE = os.getenv("NAMESPACE", "ai-agents")
|
|
KUBECONFIG_PATH = os.getenv("KUBECONFIG_PATH", None)
|
|
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
|
|
|
|
|
|
# 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
|
|
|
|
|
|
class MessageResponse(BaseModel):
|
|
"""通用消息响应"""
|
|
status: str
|
|
message: str
|
|
|
|
|
|
# Quota Models
|
|
class QuotaResponse(BaseModel):
|
|
"""配额响应"""
|
|
owner_id: str
|
|
owner_type: str
|
|
platform_pod_quota: int
|
|
platform_pod_used: int
|
|
custom_cpu_quota: float
|
|
custom_cpu_used: float
|
|
custom_memory_quota: float
|
|
custom_memory_used: float
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# Statistics Models
|
|
class StatsOverviewResponse(BaseModel):
|
|
"""统计概览响应"""
|
|
total_agents: int
|
|
platform_agents: int
|
|
custom_agents: int
|
|
running_agents: int
|
|
total_cpu_used: float
|
|
total_memory_used: float
|
|
|
|
|
|
class TemplateStatsResponse(BaseModel):
|
|
"""模板统计响应"""
|
|
template_name: str
|
|
agent_count: int
|
|
total_replicas: int
|
|
|
|
|
|
class OwnerStatsResponse(BaseModel):
|
|
"""所有者统计响应"""
|
|
owner_id: str
|
|
agent_count: int
|
|
platform_agents: int
|
|
custom_agents: int
|
|
total_cpu_used: float
|
|
total_memory_used: float
|
|
|
|
|
|
# ==================== 辅助函数 ====================
|
|
|
|
def check_quota(db: Session, owner_id: str, agent_type: AgentType,
|
|
cpu_request: str = None, memory_request: str = None) -> bool:
|
|
"""检查配额是否足够"""
|
|
quota = db.query(Quota).filter(Quota.owner_id == owner_id).first()
|
|
|
|
if not quota:
|
|
# 如果没有配额记录,返回True(暂时允许,生产环境应该拒绝)
|
|
logger.warning(f"Owner {owner_id} 没有配额记录")
|
|
return True
|
|
|
|
if agent_type == AgentType.PLATFORM:
|
|
# 平台Agent检查Pod数量配额
|
|
return quota.platform_pod_used < quota.platform_pod_quota
|
|
else:
|
|
# 自定义Agent检查CPU和内存配额
|
|
cpu_needed = parse_resource_string(cpu_request) if cpu_request else 0
|
|
memory_needed = parse_resource_string(memory_request) if memory_request else 0
|
|
|
|
cpu_available = quota.custom_cpu_quota - quota.custom_cpu_used
|
|
memory_available = quota.custom_memory_quota - quota.custom_memory_used
|
|
|
|
return cpu_needed <= cpu_available and memory_needed <= memory_available
|
|
|
|
|
|
def update_quota_usage(db: Session, owner_id: str, agent_type: AgentType,
|
|
delta_pods: int = 0, delta_cpu: float = 0, delta_memory: float = 0):
|
|
"""更新配额使用量"""
|
|
quota = db.query(Quota).filter(Quota.owner_id == owner_id).first()
|
|
|
|
if not quota:
|
|
# 创建新的配额记录(使用默认值)
|
|
quota = Quota(
|
|
owner_id=owner_id,
|
|
owner_type="tenant",
|
|
platform_pod_quota=10, # 默认值
|
|
custom_cpu_quota=10.0,
|
|
custom_memory_quota=20480.0 # 20GB
|
|
)
|
|
db.add(quota)
|
|
|
|
if agent_type == AgentType.PLATFORM:
|
|
quota.platform_pod_used += delta_pods
|
|
else:
|
|
quota.custom_cpu_used += delta_cpu
|
|
quota.custom_memory_used += delta_memory
|
|
|
|
db.commit()
|
|
|
|
|
|
# ==================== API端点 ====================
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
"""健康检查"""
|
|
return {
|
|
"service": "AI Agent Manager",
|
|
"version": "2.0.0",
|
|
"status": "running",
|
|
"namespace": NAMESPACE
|
|
}
|
|
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
"""健康检查端点"""
|
|
return {"status": "healthy"}
|
|
|
|
|
|
@app.get("/ready")
|
|
async def readiness_check(db: Session = Depends(get_db)):
|
|
"""就绪检查端点"""
|
|
try:
|
|
# 检查数据库连接
|
|
db.execute("SELECT 1")
|
|
return {"status": "ready"}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=503, detail=f"Service not ready: {str(e)}")
|
|
|
|
|
|
# ==================== 模板管理 API ====================
|
|
|
|
@app.post("/templates", response_model=TemplateResponse, status_code=201)
|
|
async def create_template(request: CreateTemplateRequest, db: Session = Depends(get_db)):
|
|
"""创建Agent模板"""
|
|
try:
|
|
# 检查模板是否已存在
|
|
existing = db.query(Template).filter(Template.name == request.name).first()
|
|
if existing:
|
|
raise HTTPException(status_code=409, detail=f"Template {request.name} already exists")
|
|
|
|
# 验证agent_type
|
|
if request.agent_type not in ["platform", "custom"]:
|
|
raise HTTPException(status_code=400, detail="agent_type must be 'platform' or 'custom'")
|
|
|
|
# 创建模板
|
|
template = Template(
|
|
name=request.name,
|
|
display_name=request.display_name,
|
|
description=request.description,
|
|
agent_type=AgentType(request.agent_type),
|
|
image=request.image,
|
|
port=request.port,
|
|
env_requirements=request.env_requirements,
|
|
cpu_request=request.cpu_request,
|
|
cpu_limit=request.cpu_limit,
|
|
memory_request=request.memory_request,
|
|
memory_limit=request.memory_limit,
|
|
min_replicas=request.min_replicas,
|
|
max_replicas=request.max_replicas,
|
|
target_cpu_utilization=request.target_cpu_utilization
|
|
)
|
|
|
|
db.add(template)
|
|
db.commit()
|
|
db.refresh(template)
|
|
|
|
logger.info(f"Created template: {template.name}")
|
|
return template
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to create template: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.get("/templates", response_model=List[TemplateResponse])
|
|
async def list_templates(agent_type: Optional[str] = None, db: Session = Depends(get_db)):
|
|
"""列出所有模板"""
|
|
try:
|
|
query = db.query(Template).filter(Template.is_active == True)
|
|
|
|
if agent_type:
|
|
if agent_type not in ["platform", "custom"]:
|
|
raise HTTPException(status_code=400, detail="agent_type must be 'platform' or 'custom'")
|
|
query = query.filter(Template.agent_type == AgentType(agent_type))
|
|
|
|
templates = query.all()
|
|
return templates
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to list templates: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.get("/templates/{template_name}", response_model=TemplateResponse)
|
|
async def get_template(template_name: str, db: Session = Depends(get_db)):
|
|
"""获取模板详情"""
|
|
template = db.query(Template).filter(Template.name == template_name).first()
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail=f"Template {template_name} not found")
|
|
return template
|
|
|
|
|
|
@app.put("/templates/{template_name}", response_model=TemplateResponse)
|
|
async def update_template(template_name: str, request: UpdateTemplateRequest, db: Session = Depends(get_db)):
|
|
"""更新模板"""
|
|
try:
|
|
template = db.query(Template).filter(Template.name == template_name).first()
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail=f"Template {template_name} not found")
|
|
|
|
# 更新字段
|
|
update_data = request.dict(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(template, field, value)
|
|
|
|
template.updated_at = datetime.utcnow()
|
|
db.commit()
|
|
db.refresh(template)
|
|
|
|
logger.info(f"Updated template: {template_name}")
|
|
return template
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to update template: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.delete("/templates/{template_name}", response_model=MessageResponse)
|
|
async def delete_template(template_name: str, db: Session = Depends(get_db)):
|
|
"""删除模板(软删除)"""
|
|
try:
|
|
template = db.query(Template).filter(Template.name == template_name).first()
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail=f"Template {template_name} not found")
|
|
|
|
# 检查是否有Agent使用此模板
|
|
agent_count = db.query(Agent).filter(Agent.template_id == template.id).count()
|
|
if agent_count > 0:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail=f"Cannot delete template: {agent_count} agents are using it"
|
|
)
|
|
|
|
# 软删除
|
|
template.is_active = False
|
|
db.commit()
|
|
|
|
logger.info(f"Deleted template: {template_name}")
|
|
return MessageResponse(status="success", message=f"Template {template_name} deleted")
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to delete template: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# ==================== 平台Agent API ====================
|
|
|
|
@app.post("/platform-agents", response_model=AgentResponseNew, status_code=201)
|
|
async def create_platform_agent(request: CreatePlatformAgentRequest, db: Session = Depends(get_db)):
|
|
"""创建平台Agent"""
|
|
try:
|
|
# 查找模板
|
|
template = db.query(Template).filter(
|
|
Template.name == request.template_name,
|
|
Template.agent_type == AgentType.PLATFORM,
|
|
Template.is_active == True
|
|
).first()
|
|
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail=f"Platform template {request.template_name} not found")
|
|
|
|
# 检查Agent名称是否已存在
|
|
existing = db.query(Agent).filter(Agent.name == request.name).first()
|
|
if existing:
|
|
raise HTTPException(status_code=409, detail=f"Agent {request.name} already exists")
|
|
|
|
# 检查配额
|
|
if not check_quota(db, request.owner_id, AgentType.PLATFORM):
|
|
raise HTTPException(status_code=429, detail="Platform agent quota exceeded")
|
|
|
|
# 创建Agent记录
|
|
agent = Agent(
|
|
name=request.name,
|
|
template_id=template.id,
|
|
owner_id=request.owner_id,
|
|
channel_id=request.channel_id,
|
|
tenant_id=request.tenant_id,
|
|
agent_type=AgentType.PLATFORM,
|
|
status=AgentStatus.PENDING,
|
|
deployment_name=f"{request.name}-deployment",
|
|
service_name=f"{request.name}-service",
|
|
namespace=NAMESPACE,
|
|
min_replicas=template.min_replicas,
|
|
max_replicas=template.max_replicas,
|
|
target_cpu_utilization=template.target_cpu_utilization
|
|
)
|
|
|
|
db.add(agent)
|
|
db.commit()
|
|
db.refresh(agent)
|
|
|
|
# 创建Kubernetes资源
|
|
try:
|
|
k8s_result = k8s_manager.create_deployment_and_service(
|
|
name=request.name,
|
|
template=template,
|
|
agent=agent,
|
|
env_vars={}
|
|
)
|
|
|
|
# 更新Agent状态和服务URL
|
|
agent.service_url = k8s_result.get("service_url")
|
|
agent.status = AgentStatus.RUNNING
|
|
db.commit()
|
|
|
|
except Exception as k8s_error:
|
|
logger.error(f"K8s deployment failed: {str(k8s_error)}")
|
|
agent.status = AgentStatus.FAILED
|
|
db.commit()
|
|
raise HTTPException(status_code=500, detail=f"Kubernetes deployment failed: {str(k8s_error)}")
|
|
|
|
# 更新配额使用
|
|
update_quota_usage(db, request.owner_id, AgentType.PLATFORM, delta_pods=1)
|
|
|
|
logger.info(f"Created platform agent: {agent.name}")
|
|
|
|
return AgentResponseNew(
|
|
id=agent.id,
|
|
name=agent.name,
|
|
display_name=agent.display_name,
|
|
template_name=template.name,
|
|
agent_type=agent.agent_type.value,
|
|
status=agent.status.value,
|
|
owner_id=agent.owner_id,
|
|
channel_id=agent.channel_id,
|
|
tenant_id=agent.tenant_id,
|
|
service_url=agent.service_url,
|
|
current_replicas=agent.current_replicas,
|
|
min_replicas=agent.min_replicas,
|
|
max_replicas=agent.max_replicas,
|
|
created_at=agent.created_at,
|
|
last_accessed_at=agent.last_accessed_at
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to create platform agent: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.get("/platform-agents", response_model=List[AgentResponseNew])
|
|
async def list_platform_agents(owner_id: Optional[str] = None, db: Session = Depends(get_db)):
|
|
"""列出平台Agent"""
|
|
try:
|
|
query = db.query(Agent).filter(Agent.agent_type == AgentType.PLATFORM)
|
|
|
|
if owner_id:
|
|
query = query.filter(Agent.owner_id == owner_id)
|
|
|
|
agents = query.all()
|
|
|
|
result = []
|
|
for agent in agents:
|
|
result.append(AgentResponseNew(
|
|
id=agent.id,
|
|
name=agent.name,
|
|
display_name=agent.display_name,
|
|
template_name=agent.template.name,
|
|
agent_type=agent.agent_type.value,
|
|
status=agent.status.value,
|
|
owner_id=agent.owner_id,
|
|
channel_id=agent.channel_id,
|
|
tenant_id=agent.tenant_id,
|
|
service_url=agent.service_url,
|
|
current_replicas=agent.current_replicas,
|
|
min_replicas=agent.min_replicas,
|
|
max_replicas=agent.max_replicas,
|
|
created_at=agent.created_at,
|
|
last_accessed_at=agent.last_accessed_at
|
|
))
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to list platform agents: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.delete("/platform-agents/{agent_name}", response_model=MessageResponse)
|
|
async def delete_platform_agent(agent_name: str, db: Session = Depends(get_db)):
|
|
"""删除平台Agent"""
|
|
try:
|
|
agent = db.query(Agent).filter(
|
|
Agent.name == agent_name,
|
|
Agent.agent_type == AgentType.PLATFORM
|
|
).first()
|
|
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail=f"Platform agent {agent_name} not found")
|
|
|
|
# 删除Kubernetes资源
|
|
try:
|
|
k8s_manager.delete_deployment_and_service(agent.deployment_name, agent.service_name)
|
|
except Exception as k8s_error:
|
|
logger.error(f"K8s deletion failed: {str(k8s_error)}")
|
|
|
|
# 更新配额
|
|
update_quota_usage(db, agent.owner_id, AgentType.PLATFORM, delta_pods=-1)
|
|
|
|
# 删除数据库记录
|
|
db.delete(agent)
|
|
db.commit()
|
|
|
|
logger.info(f"Deleted platform agent: {agent_name}")
|
|
return MessageResponse(status="success", message=f"Platform agent {agent_name} deleted")
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to delete platform agent: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# ==================== 自定义Agent API ====================
|
|
|
|
@app.post("/custom-agents", response_model=AgentResponseNew, status_code=201)
|
|
async def create_custom_agent(request: CreateCustomAgentRequest, db: Session = Depends(get_db)):
|
|
"""创建自定义Agent"""
|
|
try:
|
|
# 查找模板
|
|
template = db.query(Template).filter(
|
|
Template.name == request.template_name,
|
|
Template.agent_type == AgentType.CUSTOM,
|
|
Template.is_active == True
|
|
).first()
|
|
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail=f"Custom template {request.template_name} not found")
|
|
|
|
# 检查Agent名称是否已存在
|
|
existing = db.query(Agent).filter(Agent.name == request.name).first()
|
|
if existing:
|
|
raise HTTPException(status_code=409, detail=f"Agent {request.name} already exists")
|
|
|
|
# 验证必需的环境变量
|
|
required_env = template.env_requirements.get("required", {})
|
|
for key in required_env.keys():
|
|
if key not in request.environment_vars:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Missing required environment variable: {key}"
|
|
)
|
|
|
|
# 确定资源配置(使用请求值或模板默认值)
|
|
cpu_request = request.cpu_request or template.cpu_request
|
|
memory_request = request.memory_request or template.memory_request
|
|
|
|
# 检查配额
|
|
if not check_quota(db, request.owner_id, AgentType.CUSTOM, cpu_request, memory_request):
|
|
raise HTTPException(status_code=429, detail="Custom agent resource quota exceeded")
|
|
|
|
# 创建Agent记录
|
|
scaling = request.scaling_config or ScalingConfig()
|
|
|
|
agent = Agent(
|
|
name=request.name,
|
|
template_id=template.id,
|
|
owner_id=request.owner_id,
|
|
channel_id=request.channel_id,
|
|
tenant_id=request.tenant_id,
|
|
agent_type=AgentType.CUSTOM,
|
|
status=AgentStatus.PENDING,
|
|
environment_vars=request.environment_vars,
|
|
cpu_request=cpu_request,
|
|
cpu_limit=request.cpu_limit or template.cpu_limit,
|
|
memory_request=memory_request,
|
|
memory_limit=request.memory_limit or template.memory_limit,
|
|
deployment_name=f"{request.name}-deployment",
|
|
service_name=f"{request.name}-service",
|
|
namespace=NAMESPACE,
|
|
min_replicas=scaling.min_replicas,
|
|
max_replicas=scaling.max_replicas,
|
|
target_cpu_utilization=scaling.target_cpu_utilization
|
|
)
|
|
|
|
db.add(agent)
|
|
db.commit()
|
|
db.refresh(agent)
|
|
|
|
# 创建Kubernetes资源
|
|
try:
|
|
k8s_result = k8s_manager.create_deployment_and_service(
|
|
name=request.name,
|
|
template=template,
|
|
agent=agent,
|
|
env_vars=request.environment_vars
|
|
)
|
|
|
|
agent.service_url = k8s_result.get("service_url")
|
|
agent.status = AgentStatus.RUNNING
|
|
db.commit()
|
|
|
|
except Exception as k8s_error:
|
|
logger.error(f"K8s deployment failed: {str(k8s_error)}")
|
|
agent.status = AgentStatus.FAILED
|
|
db.commit()
|
|
raise HTTPException(status_code=500, detail=f"Kubernetes deployment failed: {str(k8s_error)}")
|
|
|
|
# 更新配额使用
|
|
cpu_used = parse_resource_string(cpu_request)
|
|
memory_used = parse_resource_string(memory_request)
|
|
update_quota_usage(db, request.owner_id, AgentType.CUSTOM,
|
|
delta_cpu=cpu_used, delta_memory=memory_used)
|
|
|
|
logger.info(f"Created custom agent: {agent.name}")
|
|
|
|
return AgentResponseNew(
|
|
id=agent.id,
|
|
name=agent.name,
|
|
display_name=agent.display_name,
|
|
template_name=template.name,
|
|
agent_type=agent.agent_type.value,
|
|
status=agent.status.value,
|
|
owner_id=agent.owner_id,
|
|
channel_id=agent.channel_id,
|
|
tenant_id=agent.tenant_id,
|
|
service_url=agent.service_url,
|
|
current_replicas=agent.current_replicas,
|
|
min_replicas=agent.min_replicas,
|
|
max_replicas=agent.max_replicas,
|
|
created_at=agent.created_at,
|
|
last_accessed_at=agent.last_accessed_at
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to create custom agent: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.get("/custom-agents", response_model=List[AgentResponseNew])
|
|
async def list_custom_agents(owner_id: Optional[str] = None, db: Session = Depends(get_db)):
|
|
"""列出自定义Agent"""
|
|
try:
|
|
query = db.query(Agent).filter(Agent.agent_type == AgentType.CUSTOM)
|
|
|
|
if owner_id:
|
|
query = query.filter(Agent.owner_id == owner_id)
|
|
|
|
agents = query.all()
|
|
|
|
result = []
|
|
for agent in agents:
|
|
result.append(AgentResponseNew(
|
|
id=agent.id,
|
|
name=agent.name,
|
|
display_name=agent.display_name,
|
|
template_name=agent.template.name,
|
|
agent_type=agent.agent_type.value,
|
|
status=agent.status.value,
|
|
owner_id=agent.owner_id,
|
|
channel_id=agent.channel_id,
|
|
tenant_id=agent.tenant_id,
|
|
service_url=agent.service_url,
|
|
current_replicas=agent.current_replicas,
|
|
min_replicas=agent.min_replicas,
|
|
max_replicas=agent.max_replicas,
|
|
created_at=agent.created_at,
|
|
last_accessed_at=agent.last_accessed_at
|
|
))
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to list custom agents: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.put("/custom-agents/{agent_name}/env", response_model=AgentResponseNew)
|
|
async def update_custom_agent_env(agent_name: str, request: UpdateAgentEnvRequest, db: Session = Depends(get_db)):
|
|
"""更新自定义Agent环境变量"""
|
|
try:
|
|
agent = db.query(Agent).filter(
|
|
Agent.name == agent_name,
|
|
Agent.agent_type == AgentType.CUSTOM
|
|
).first()
|
|
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail=f"Custom agent {agent_name} not found")
|
|
|
|
# 验证必需的环境变量
|
|
required_env = agent.template.env_requirements.get("required", {})
|
|
for key in required_env.keys():
|
|
if key not in request.environment_vars:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Missing required environment variable: {key}"
|
|
)
|
|
|
|
# 更新环境变量
|
|
agent.environment_vars = request.environment_vars
|
|
agent.updated_at = datetime.utcnow()
|
|
|
|
# 更新Kubernetes Deployment
|
|
try:
|
|
k8s_manager.update_deployment_env(agent.deployment_name, request.environment_vars)
|
|
except Exception as k8s_error:
|
|
logger.error(f"K8s update failed: {str(k8s_error)}")
|
|
raise HTTPException(status_code=500, detail=f"Kubernetes update failed: {str(k8s_error)}")
|
|
|
|
db.commit()
|
|
db.refresh(agent)
|
|
|
|
logger.info(f"Updated custom agent env: {agent_name}")
|
|
|
|
return AgentResponseNew(
|
|
id=agent.id,
|
|
name=agent.name,
|
|
display_name=agent.display_name,
|
|
template_name=agent.template.name,
|
|
agent_type=agent.agent_type.value,
|
|
status=agent.status.value,
|
|
owner_id=agent.owner_id,
|
|
channel_id=agent.channel_id,
|
|
tenant_id=agent.tenant_id,
|
|
service_url=agent.service_url,
|
|
current_replicas=agent.current_replicas,
|
|
min_replicas=agent.min_replicas,
|
|
max_replicas=agent.max_replicas,
|
|
created_at=agent.created_at,
|
|
last_accessed_at=agent.last_accessed_at
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to update custom agent env: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.delete("/custom-agents/{agent_name}", response_model=MessageResponse)
|
|
async def delete_custom_agent(agent_name: str, db: Session = Depends(get_db)):
|
|
"""删除自定义Agent"""
|
|
try:
|
|
agent = db.query(Agent).filter(
|
|
Agent.name == agent_name,
|
|
Agent.agent_type == AgentType.CUSTOM
|
|
).first()
|
|
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail=f"Custom agent {agent_name} not found")
|
|
|
|
# 删除Kubernetes资源
|
|
try:
|
|
k8s_manager.delete_deployment_and_service(agent.deployment_name, agent.service_name)
|
|
except Exception as k8s_error:
|
|
logger.error(f"K8s deletion failed: {str(k8s_error)}")
|
|
|
|
# 更新配额
|
|
cpu_used = parse_resource_string(agent.cpu_request)
|
|
memory_used = parse_resource_string(agent.memory_request)
|
|
update_quota_usage(db, agent.owner_id, AgentType.CUSTOM,
|
|
delta_cpu=-cpu_used, delta_memory=-memory_used)
|
|
|
|
# 删除数据库记录
|
|
db.delete(agent)
|
|
db.commit()
|
|
|
|
logger.info(f"Deleted custom agent: {agent_name}")
|
|
return MessageResponse(status="success", message=f"Custom agent {agent_name} deleted")
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to delete custom agent: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# ==================== 统计API ====================
|
|
|
|
@app.get("/stats/overview", response_model=StatsOverviewResponse)
|
|
async def get_stats_overview(db: Session = Depends(get_db)):
|
|
"""获取统计概览"""
|
|
try:
|
|
total_agents = db.query(Agent).count()
|
|
platform_agents = db.query(Agent).filter(Agent.agent_type == AgentType.PLATFORM).count()
|
|
custom_agents = db.query(Agent).filter(Agent.agent_type == AgentType.CUSTOM).count()
|
|
running_agents = db.query(Agent).filter(Agent.status == AgentStatus.RUNNING).count()
|
|
|
|
# 计算总CPU和内存使用(仅自定义Agent)
|
|
custom_agent_list = db.query(Agent).filter(Agent.agent_type == AgentType.CUSTOM).all()
|
|
total_cpu = sum(parse_resource_string(a.cpu_request or "0") for a in custom_agent_list)
|
|
total_memory = sum(parse_resource_string(a.memory_request or "0") for a in custom_agent_list)
|
|
|
|
return StatsOverviewResponse(
|
|
total_agents=total_agents,
|
|
platform_agents=platform_agents,
|
|
custom_agents=custom_agents,
|
|
running_agents=running_agents,
|
|
total_cpu_used=total_cpu,
|
|
total_memory_used=total_memory
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get stats overview: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.get("/stats/by-template", response_model=List[TemplateStatsResponse])
|
|
async def get_stats_by_template(db: Session = Depends(get_db)):
|
|
"""按模板统计"""
|
|
try:
|
|
results = db.query(
|
|
Template.name,
|
|
func.count(Agent.id).label("agent_count"),
|
|
func.sum(Agent.current_replicas).label("total_replicas")
|
|
).join(Agent, Template.id == Agent.template_id, isouter=True)\
|
|
.group_by(Template.name).all()
|
|
|
|
stats = []
|
|
for name, count, replicas in results:
|
|
stats.append(TemplateStatsResponse(
|
|
template_name=name,
|
|
agent_count=count or 0,
|
|
total_replicas=replicas or 0
|
|
))
|
|
|
|
return stats
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get stats by template: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.get("/stats/by-owner", response_model=List[OwnerStatsResponse])
|
|
async def get_stats_by_owner(db: Session = Depends(get_db)):
|
|
"""按所有者统计"""
|
|
try:
|
|
# 按owner_id分组统计
|
|
owners = db.query(Agent.owner_id).distinct().all()
|
|
|
|
stats = []
|
|
for (owner_id,) in owners:
|
|
agents = db.query(Agent).filter(Agent.owner_id == owner_id).all()
|
|
|
|
agent_count = len(agents)
|
|
platform_count = sum(1 for a in agents if a.agent_type == AgentType.PLATFORM)
|
|
custom_count = sum(1 for a in agents if a.agent_type == AgentType.CUSTOM)
|
|
|
|
total_cpu = sum(parse_resource_string(a.cpu_request or "0") for a in agents if a.agent_type == AgentType.CUSTOM)
|
|
total_memory = sum(parse_resource_string(a.memory_request or "0") for a in agents if a.agent_type == AgentType.CUSTOM)
|
|
|
|
stats.append(OwnerStatsResponse(
|
|
owner_id=owner_id,
|
|
agent_count=agent_count,
|
|
platform_agents=platform_count,
|
|
custom_agents=custom_count,
|
|
total_cpu_used=total_cpu,
|
|
total_memory_used=total_memory
|
|
))
|
|
|
|
return stats
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get stats by owner: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
# ==================== 配额管理 API ====================
|
|
|
|
@app.get("/quotas/{owner_id}", response_model=QuotaResponse)
|
|
async def get_quota(owner_id: str, db: Session = Depends(get_db)):
|
|
"""获取配额信息"""
|
|
quota = db.query(Quota).filter(Quota.owner_id == owner_id).first()
|
|
|
|
if not quota:
|
|
raise HTTPException(status_code=404, detail=f"Quota for owner {owner_id} not found")
|
|
|
|
return quota
|
|
|
|
|
|
# ==================== 日志管理 API ====================
|
|
|
|
@app.get("/platform-agents/{agent_name}/logs")
|
|
async def get_platform_agent_logs(agent_name: str, lines: int = 100, db: Session = Depends(get_db)):
|
|
"""获取平台Agent日志"""
|
|
try:
|
|
agent = db.query(Agent).filter(
|
|
Agent.name == agent_name,
|
|
Agent.agent_type == AgentType.PLATFORM
|
|
).first()
|
|
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail=f"Platform agent {agent_name} not found")
|
|
|
|
logs = k8s_manager.get_pod_logs(agent.deployment_name, lines)
|
|
|
|
return {
|
|
"agent_name": agent_name,
|
|
"deployment_name": agent.deployment_name,
|
|
"logs": logs,
|
|
"lines": lines
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to get platform agent logs: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.get("/custom-agents/{agent_name}/logs")
|
|
async def get_custom_agent_logs(agent_name: str, lines: int = 100, db: Session = Depends(get_db)):
|
|
"""获取自定义Agent日志"""
|
|
try:
|
|
agent = db.query(Agent).filter(
|
|
Agent.name == agent_name,
|
|
Agent.agent_type == AgentType.CUSTOM
|
|
).first()
|
|
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail=f"Custom agent {agent_name} not found")
|
|
|
|
logs = k8s_manager.get_pod_logs(agent.deployment_name, lines)
|
|
|
|
return {
|
|
"agent_name": agent_name,
|
|
"deployment_name": agent.deployment_name,
|
|
"logs": logs,
|
|
"lines": lines
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to get custom agent logs: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
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服务 v2.0: {host}:{port}")
|
|
uvicorn.run(app, host=host, port=port)
|