forked from xiaohei/taiji-AI-PAD
1703 lines
56 KiB
Python
1703 lines
56 KiB
Python
"""
|
||
AI Agent Manager API Client
|
||
Encapsulates HTTP calls to the AI Agent Manager service
|
||
|
||
Adapts to the actual Agent Manager interface (based on agent-manager-interface-doc.md)
|
||
"""
|
||
|
||
import os
|
||
import httpx
|
||
import structlog
|
||
from typing import Dict, List, Optional, Any
|
||
from dataclasses import dataclass, field
|
||
from enum import Enum
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
# Get Agent Manager URL from environment variables
|
||
AGENT_MANAGER_URL = os.getenv("AGENT_MANAGER_URL", "http://localhost:8000")
|
||
|
||
# LLM_BASE_URL - 所有 Agent(平台和自定义)都必须传递的固定参数
|
||
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io")
|
||
|
||
|
||
class AgentStatus(str, Enum):
|
||
"""Agent Pod Status"""
|
||
PENDING = "Pending"
|
||
RUNNING = "Running"
|
||
SUCCEEDED = "Succeeded"
|
||
FAILED = "Failed"
|
||
UNKNOWN = "Unknown"
|
||
|
||
|
||
@dataclass
|
||
class AgentConfig:
|
||
"""
|
||
Agent Resource Configuration
|
||
|
||
Adapts to the config parameter of the Agent Manager POST /agents interface
|
||
"""
|
||
user_id: Optional[str] = None # User identifier for multi-tenancy management
|
||
cpu_request: Optional[str] = "100m"
|
||
cpu_limit: Optional[str] = "500m"
|
||
memory_request: Optional[str] = "128Mi"
|
||
memory_limit: Optional[str] = "512Mi"
|
||
replicas: Optional[int] = 1 # Number of replicas (instances)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""Converts to API request format"""
|
||
result = {}
|
||
if self.user_id:
|
||
result["user_id"] = self.user_id
|
||
if self.cpu_request:
|
||
result["cpu_request"] = self.cpu_request
|
||
if self.cpu_limit:
|
||
result["cpu_limit"] = self.cpu_limit
|
||
if self.memory_request:
|
||
result["memory_request"] = self.memory_request
|
||
if self.memory_limit:
|
||
result["memory_limit"] = self.memory_limit
|
||
if self.replicas is not None:
|
||
result["replicas"] = self.replicas
|
||
return result
|
||
|
||
|
||
@dataclass
|
||
class ContainerStatus:
|
||
"""
|
||
Container Status
|
||
|
||
Adapts to the containers field in the Agent Manager GET /agents/{name}/status interface
|
||
"""
|
||
name: str
|
||
ready: bool = False
|
||
restart_count: int = 0
|
||
state: str = "unknown" # running/waiting/terminated
|
||
reason: Optional[str] = None
|
||
message: Optional[str] = None
|
||
exit_code: Optional[int] = None
|
||
started_at: Optional[str] = None
|
||
finished_at: Optional[str] = None
|
||
|
||
|
||
@dataclass
|
||
class TemplateInfo:
|
||
"""
|
||
Template Information
|
||
|
||
Adapts to the Agent Manager GET /templates interface response
|
||
|
||
Agent Manager now returns:
|
||
- template: technical name (e.g., echo_agent)
|
||
- displayName: human-readable name (e.g., "Echo 测试服务")
|
||
- description: template description
|
||
- category: template category (e.g., testing, assistant)
|
||
- port: service port
|
||
- env_info: environment variable configuration
|
||
"""
|
||
template: str
|
||
display_name: Optional[str] = None
|
||
description: Optional[str] = None
|
||
category: Optional[str] = None
|
||
port: Optional[int] = None
|
||
env_info: Dict[str, Any] = field(default_factory=dict)
|
||
template_type: Optional[str] = None # platform or custom
|
||
|
||
|
||
@dataclass
|
||
class AgentCreateResult:
|
||
"""
|
||
Agent Creation Result
|
||
|
||
Adapts to the Agent Manager POST /agents interface response
|
||
"""
|
||
name: str
|
||
namespace: str
|
||
status: str
|
||
created_at: str
|
||
template: str
|
||
service_port: Optional[int] = None
|
||
access_info: Optional[Dict[str, Any]] = None
|
||
# New fields (adapted to actual interface response)
|
||
pod_id: Optional[str] = None
|
||
pod_ip: Optional[str] = None
|
||
host_ip: Optional[str] = None
|
||
node_name: Optional[str] = None
|
||
owner_info: Optional[Dict[str, Any]] = None
|
||
|
||
|
||
@dataclass
|
||
class AgentStatusResult:
|
||
"""
|
||
Agent Status Result
|
||
|
||
Adapts to the Agent Manager GET /agents/{name}/status interface response
|
||
|
||
Actual Agent Manager return format (after v2 update):
|
||
{
|
||
"name": "alice-echo",
|
||
"namespace": "ai-agents",
|
||
"status": "Running",
|
||
"health_status": "healthy",
|
||
"created_at": "2026-01-06T09:00:00Z",
|
||
"pod_ip": "10.244.1.100",
|
||
"host_ip": "192.168.1.10",
|
||
"node_name": "node-1",
|
||
"labels": {"template": "echo_agent", "user_id": "alice"},
|
||
"service_port": 8080,
|
||
"access_url": "http://alice-echo.ai-agents.svc.cluster.local:8080",
|
||
"containers": [
|
||
{
|
||
"name": "agent",
|
||
"ready": true,
|
||
"restart_count": 0,
|
||
"state": "running",
|
||
"started_at": "2026-01-06T09:00:05Z"
|
||
}
|
||
],
|
||
"resources": {
|
||
"requests": {"cpu": "100m", "memory": "128Mi"},
|
||
"limits": {"cpu": "500m", "memory": "512Mi"}
|
||
},
|
||
"endpoints": ["http://10.244.1.100:8080"],
|
||
"conditions": [
|
||
{"type": "Ready", "status": "True"},
|
||
{"type": "ContainersReady", "status": "True"}
|
||
]
|
||
}
|
||
|
||
Health status description:
|
||
- healthy: Pod is running normally, all containers are ready
|
||
- unhealthy: Pod is running abnormally, some containers are not ready or have crashed
|
||
- degraded: Pod is running but with warnings (e.g., too many restarts)
|
||
"""
|
||
name: str
|
||
namespace: str
|
||
status: str
|
||
# New: Health status (healthy/unhealthy/degraded)
|
||
health_status: str = "unknown"
|
||
created_at: Optional[str] = None
|
||
pod_ip: Optional[str] = None
|
||
host_ip: Optional[str] = None
|
||
node_name: Optional[str] = None
|
||
labels: Optional[Dict[str, str]] = None
|
||
# New: Service port and access URL
|
||
service_port: Optional[int] = None
|
||
access_url: Optional[str] = None
|
||
# New: Container status list
|
||
containers: List[ContainerStatus] = field(default_factory=list)
|
||
# New: Resource configuration
|
||
resources: Optional[Dict[str, Any]] = None
|
||
# New: Endpoint list
|
||
endpoints: Optional[List[str]] = None
|
||
# New: Pod conditions
|
||
conditions: Optional[List[Dict[str, Any]]] = None
|
||
# Compatible with old fields
|
||
template: Optional[str] = None
|
||
# ========== 访问信息字段(从 access_info 解析) ==========
|
||
external_ip: Optional[str] = None # 外网 IP 地址
|
||
domain: Optional[str] = None # 域名
|
||
domain_url: Optional[str] = None # 域名访问地址
|
||
ip_url: Optional[str] = None # IP 访问地址
|
||
# ======================================================
|
||
|
||
@property
|
||
def is_healthy(self) -> bool:
|
||
"""Is it healthy"""
|
||
return self.health_status == "healthy"
|
||
|
||
@property
|
||
def is_running(self) -> bool:
|
||
"""Is it running"""
|
||
return self.status.lower() == "running"
|
||
|
||
@property
|
||
def is_ready(self) -> bool:
|
||
"""Is it ready (all containers are ready)"""
|
||
if not self.containers:
|
||
return self.is_running
|
||
return all(c.ready for c in self.containers)
|
||
|
||
@property
|
||
def total_restart_count(self) -> int:
|
||
"""Total restart count of all containers"""
|
||
return sum(c.restart_count for c in self.containers)
|
||
|
||
@property
|
||
def has_crashed_container(self) -> bool:
|
||
"""Is there a crashed container"""
|
||
for c in self.containers:
|
||
if c.state == "terminated" and c.exit_code != 0:
|
||
return True
|
||
if c.reason in ("CrashLoopBackOff", "Error", "OOMKilled"):
|
||
return True
|
||
return False
|
||
|
||
@property
|
||
def cpu_request(self) -> Optional[str]:
|
||
"""Get CPU request"""
|
||
if self.resources and "requests" in self.resources:
|
||
return self.resources["requests"].get("cpu")
|
||
return None
|
||
|
||
@property
|
||
def cpu_limit(self) -> Optional[str]:
|
||
"""Get CPU limit"""
|
||
if self.resources and "limits" in self.resources:
|
||
return self.resources["limits"].get("cpu")
|
||
return None
|
||
|
||
@property
|
||
def memory_request(self) -> Optional[str]:
|
||
"""Get memory request"""
|
||
if self.resources and "requests" in self.resources:
|
||
return self.resources["requests"].get("memory")
|
||
return None
|
||
|
||
@property
|
||
def memory_limit(self) -> Optional[str]:
|
||
"""Get memory limit"""
|
||
if self.resources and "limits" in self.resources:
|
||
return self.resources["limits"].get("memory")
|
||
return None
|
||
|
||
|
||
@dataclass
|
||
class AgentMetricsResult:
|
||
"""
|
||
Agent Resource Usage Result
|
||
|
||
Adapts to the Agent Manager GET /agents/{name}/metrics interface response
|
||
|
||
Actual Agent Manager return format:
|
||
{
|
||
"name": "alice-echo",
|
||
"namespace": "ai-agents",
|
||
"requests": {
|
||
"cpu": "100m",
|
||
"memory": "128Mi"
|
||
},
|
||
"limits": {
|
||
"cpu": "500m",
|
||
"memory": "512Mi"
|
||
},
|
||
"usage": {
|
||
"cpu": "14502n",
|
||
"memory": "8704Ki"
|
||
},
|
||
"timestamp": "2026-01-06T05:01:04Z",
|
||
"metrics_available": null
|
||
}
|
||
|
||
Field description:
|
||
- requests/limits: Resource quotas (obtained from Pod spec)
|
||
- usage: Real-time resource usage (obtained from metrics-server, requires metrics-server to be installed in the cluster)
|
||
- timestamp: Timestamp of the metrics data (ISO 8601 format)
|
||
- metrics_available: Whether the metrics-server is available
|
||
|
||
CPU unit description:
|
||
- n (nanocores): 1 core = 1,000,000,000n
|
||
- m (millicores): 1 core = 1,000m
|
||
- e.g.: 14502n = 0.014502m ≈ 0.000014 cores
|
||
|
||
Memory unit description:
|
||
- Ki (Kibibytes): 1024 bytes
|
||
- Mi (Mebibytes): 1024 KiB
|
||
- e.g.: 8704Ki ≈ 8.5 MiB
|
||
"""
|
||
name: str
|
||
namespace: str = "ai-agents"
|
||
requests: Dict[str, str] = field(default_factory=dict)
|
||
limits: Dict[str, str] = field(default_factory=dict)
|
||
# New: Real-time resource usage (from metrics-server)
|
||
usage: Optional[Dict[str, str]] = None
|
||
# New: metrics data timestamp
|
||
timestamp: Optional[str] = None
|
||
# New: whether metrics-server is available
|
||
metrics_available: Optional[bool] = None
|
||
# Compatible with old format
|
||
resources: Dict[str, Any] = field(default_factory=dict)
|
||
|
||
@staticmethod
|
||
def _parse_cpu_to_millicores(cpu_str: str) -> float:
|
||
"""
|
||
Converts CPU string to millicores
|
||
|
||
Supported units:
|
||
- n (nanocores): 1m = 1,000,000n
|
||
- m (millicores): direct use
|
||
- no unit: treated as cores, multiplied by 1000
|
||
|
||
Args:
|
||
cpu_str: CPU string, e.g. "100m", "14502n", "0.5"
|
||
|
||
Returns:
|
||
Number of millicores (float)
|
||
"""
|
||
if not cpu_str or cpu_str == "0":
|
||
return 0.0
|
||
|
||
cpu_str = str(cpu_str).strip()
|
||
|
||
try:
|
||
if cpu_str.endswith("n"):
|
||
# nanocores -> millicores: 1m = 1,000,000n
|
||
return float(cpu_str[:-1]) / 1_000_000
|
||
elif cpu_str.endswith("m"):
|
||
# millicores
|
||
return float(cpu_str[:-1])
|
||
else:
|
||
# cores -> millicores
|
||
return float(cpu_str) * 1000
|
||
except ValueError:
|
||
return 0.0
|
||
|
||
@staticmethod
|
||
def _parse_memory_to_mb(mem_str: str) -> float:
|
||
"""
|
||
Converts memory string to MB
|
||
|
||
Supported units:
|
||
- Ki (Kibibytes): 1 MiB = 1024 KiB
|
||
- Mi (Mebibytes): direct use
|
||
- Gi (Gibibytes): 1 GiB = 1024 MiB
|
||
- K (Kilobytes): 1 MB = 1000 KB
|
||
- M (Megabytes): direct use
|
||
- G (Gigabytes): 1 GB = 1000 MB
|
||
- no unit: treated as bytes
|
||
|
||
Args:
|
||
mem_str: Memory string, e.g. "128Mi", "8704Ki", "1Gi"
|
||
|
||
Returns:
|
||
Number of MB (float)
|
||
"""
|
||
if not mem_str or mem_str == "0":
|
||
return 0.0
|
||
|
||
mem_str = str(mem_str).strip()
|
||
|
||
try:
|
||
# Binary units (IEC)
|
||
if mem_str.endswith("Ki"):
|
||
return float(mem_str[:-2]) / 1024
|
||
elif mem_str.endswith("Mi"):
|
||
return float(mem_str[:-2])
|
||
elif mem_str.endswith("Gi"):
|
||
return float(mem_str[:-2]) * 1024
|
||
elif mem_str.endswith("Ti"):
|
||
return float(mem_str[:-2]) * 1024 * 1024
|
||
# Decimal units (SI)
|
||
elif mem_str.endswith("K"):
|
||
return float(mem_str[:-1]) / 1000
|
||
elif mem_str.endswith("M"):
|
||
return float(mem_str[:-1])
|
||
elif mem_str.endswith("G"):
|
||
return float(mem_str[:-1]) * 1000
|
||
elif mem_str.endswith("T"):
|
||
return float(mem_str[:-1]) * 1000 * 1000
|
||
else:
|
||
# bytes -> MB
|
||
return float(mem_str) / (1024 * 1024)
|
||
except ValueError:
|
||
return 0.0
|
||
|
||
@property
|
||
def cpu_request(self) -> str:
|
||
"""Get CPU request (e.g. '100m')"""
|
||
return self.requests.get("cpu", "0")
|
||
|
||
@property
|
||
def memory_request(self) -> str:
|
||
"""Get memory request (e.g. '128Mi')"""
|
||
return self.requests.get("memory", "0")
|
||
|
||
@property
|
||
def cpu_limit(self) -> str:
|
||
"""Get CPU limit (e.g. '500m')"""
|
||
return self.limits.get("cpu", "0")
|
||
|
||
@property
|
||
def memory_limit(self) -> str:
|
||
"""Get memory limit (e.g. '512Mi')"""
|
||
return self.limits.get("memory", "0")
|
||
|
||
@property
|
||
def cpu_usage_current(self) -> str:
|
||
"""Get current CPU usage (e.g. '14502n')"""
|
||
if self.usage:
|
||
return self.usage.get("cpu", "0")
|
||
return "0"
|
||
|
||
@property
|
||
def memory_usage_current(self) -> str:
|
||
"""Get current memory usage (e.g. '8704Ki')"""
|
||
if self.usage:
|
||
return self.usage.get("memory", "0")
|
||
return "0"
|
||
|
||
# Compatible with old cpu_usage/memory_usage attribute names (use limits as display value)
|
||
@property
|
||
def cpu_usage(self) -> str:
|
||
"""Get CPU limit (compatible with old attribute name)"""
|
||
return self.cpu_limit
|
||
|
||
@property
|
||
def memory_usage(self) -> str:
|
||
"""Get memory limit (compatible with old attribute name)"""
|
||
return self.memory_limit
|
||
|
||
@property
|
||
def available(self) -> bool:
|
||
"""Is it available (based on whether there are limits configured)"""
|
||
return bool(self.limits.get("cpu") or self.limits.get("memory"))
|
||
|
||
@property
|
||
def has_realtime_metrics(self) -> bool:
|
||
"""Whether there is real-time metrics data"""
|
||
return self.usage is not None and bool(self.usage)
|
||
|
||
@property
|
||
def cpu_limit_millicores(self) -> float:
|
||
"""Get CPU limit (millicores, e.g. 500m -> 500.0)"""
|
||
return self._parse_cpu_to_millicores(self.cpu_limit)
|
||
|
||
@property
|
||
def cpu_request_millicores(self) -> float:
|
||
"""Get CPU request (millicores)"""
|
||
return self._parse_cpu_to_millicores(self.cpu_request)
|
||
|
||
@property
|
||
def cpu_usage_current_millicores(self) -> float:
|
||
"""Get current CPU usage (millicores)"""
|
||
return self._parse_cpu_to_millicores(self.cpu_usage_current)
|
||
|
||
@property
|
||
def memory_limit_mb(self) -> float:
|
||
"""Get memory limit (MB)"""
|
||
return self._parse_memory_to_mb(self.memory_limit)
|
||
|
||
@property
|
||
def memory_request_mb(self) -> float:
|
||
"""Get memory request (MB)"""
|
||
return self._parse_memory_to_mb(self.memory_request)
|
||
|
||
@property
|
||
def memory_usage_current_mb(self) -> float:
|
||
"""Get current memory usage (MB)"""
|
||
return self._parse_memory_to_mb(self.memory_usage_current)
|
||
|
||
@property
|
||
def cpu_utilization_percent(self) -> Optional[float]:
|
||
"""
|
||
Get CPU utilization (percentage relative to limit)
|
||
|
||
Returns:
|
||
Utilization percentage, or None if there is no real-time data
|
||
"""
|
||
if not self.has_realtime_metrics:
|
||
return None
|
||
|
||
limit = self.cpu_limit_millicores
|
||
if limit <= 0:
|
||
return None
|
||
|
||
usage = self.cpu_usage_current_millicores
|
||
return (usage / limit) * 100
|
||
|
||
@property
|
||
def memory_utilization_percent(self) -> Optional[float]:
|
||
"""
|
||
Get memory utilization (percentage relative to limit)
|
||
|
||
Returns:
|
||
Utilization percentage, or None if there is no real-time data
|
||
"""
|
||
if not self.has_realtime_metrics:
|
||
return None
|
||
|
||
limit = self.memory_limit_mb
|
||
if limit <= 0:
|
||
return None
|
||
|
||
usage = self.memory_usage_current_mb
|
||
return (usage / limit) * 100
|
||
|
||
# Compatible with old attribute names
|
||
@property
|
||
def cpu_usage_millicores(self) -> float:
|
||
"""Get CPU limit (millicores, compatible with old attribute name)"""
|
||
return self.cpu_limit_millicores
|
||
|
||
@property
|
||
def memory_usage_mb(self) -> float:
|
||
"""Get memory limit (MB, compatible with old attribute name)"""
|
||
return self.memory_limit_mb
|
||
|
||
|
||
@dataclass
|
||
class AgentListResult:
|
||
"""
|
||
Agent List Result
|
||
|
||
Adapts to the Agent Manager GET /agents interface response
|
||
"""
|
||
agents: List[Dict[str, Any]]
|
||
count: int
|
||
|
||
|
||
class AgentManagerError(Exception):
|
||
"""Agent Manager API Error"""
|
||
def __init__(self, message: str, status_code: int = 500, detail: Any = None):
|
||
self.message = message
|
||
self.status_code = status_code
|
||
self.detail = detail
|
||
super().__init__(message)
|
||
|
||
|
||
class AgentManagerClient:
|
||
"""
|
||
AI Agent Manager API Client
|
||
|
||
Adapts to the actual Agent Manager interface:
|
||
- POST /agents - Create Agent
|
||
- GET /agents - Get all Agents
|
||
- GET /agents/{name}/status - Get Agent status
|
||
- GET /agents/{name}/metrics - Get resource usage
|
||
- DELETE /agents/{name} - Delete Agent
|
||
- GET /templates - Get all templates
|
||
- GET /templates/platform - Get platform templates
|
||
- GET /templates/custom - Get custom templates
|
||
- GET /templates/{name} - Get template details
|
||
- GET / - Health check
|
||
"""
|
||
|
||
def __init__(self, base_url: Optional[str] = None, timeout: float = 30.0):
|
||
"""
|
||
Initializes the client
|
||
|
||
Args:
|
||
base_url: API base URL, defaults to environment variable
|
||
timeout: Request timeout in seconds
|
||
"""
|
||
self.base_url = (base_url or AGENT_MANAGER_URL).rstrip("/")
|
||
self.timeout = timeout
|
||
self._client: Optional[httpx.AsyncClient] = None
|
||
|
||
async def _get_client(self) -> httpx.AsyncClient:
|
||
"""Get or create HTTP client"""
|
||
if self._client is None or self._client.is_closed:
|
||
self._client = httpx.AsyncClient(
|
||
base_url=self.base_url,
|
||
timeout=self.timeout,
|
||
headers={"Content-Type": "application/json"}
|
||
)
|
||
return self._client
|
||
|
||
async def close(self):
|
||
"""Close the client"""
|
||
if self._client and not self._client.is_closed:
|
||
await self._client.aclose()
|
||
self._client = None
|
||
|
||
async def _request(
|
||
self,
|
||
method: str,
|
||
path: str,
|
||
json: Optional[Dict] = None,
|
||
params: Optional[Dict] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
Send HTTP request
|
||
|
||
Args:
|
||
method: HTTP method
|
||
path: API path
|
||
json: Request body
|
||
params: Query parameters
|
||
|
||
Returns:
|
||
Response JSON
|
||
|
||
Raises:
|
||
AgentManagerError: API call failed
|
||
"""
|
||
client = await self._get_client()
|
||
|
||
try:
|
||
response = await client.request(
|
||
method=method,
|
||
url=path,
|
||
json=json,
|
||
params=params
|
||
)
|
||
|
||
if response.status_code >= 400:
|
||
try:
|
||
detail = response.json()
|
||
except Exception:
|
||
detail = response.text
|
||
|
||
logger.error(
|
||
"agent_manager_api_error",
|
||
method=method,
|
||
path=path,
|
||
status_code=response.status_code,
|
||
detail=detail
|
||
)
|
||
|
||
raise AgentManagerError(
|
||
message=f"API call failed: {response.status_code}",
|
||
status_code=response.status_code,
|
||
detail=detail
|
||
)
|
||
|
||
return response.json()
|
||
|
||
except httpx.RequestError as e:
|
||
logger.error(
|
||
"agent_manager_connection_error",
|
||
method=method,
|
||
path=path,
|
||
error=str(e)
|
||
)
|
||
raise AgentManagerError(
|
||
message=f"Failed to connect to Agent Manager: {str(e)}",
|
||
status_code=503,
|
||
detail={"error": "connection_error", "message": str(e)}
|
||
)
|
||
|
||
# ==================== 健康检查 ====================
|
||
|
||
async def health_check(self) -> Dict[str, Any]:
|
||
"""
|
||
Check Agent Manager service status
|
||
|
||
Call: GET /
|
||
|
||
Returns:
|
||
Service status information, including:
|
||
- service: service name
|
||
- version: version
|
||
- status: status
|
||
"""
|
||
return await self._request("GET", "/")
|
||
|
||
# ==================== Template Management ====================
|
||
|
||
async def list_templates(self) -> List[TemplateInfo]:
|
||
"""
|
||
Get all available templates
|
||
|
||
Call: GET /templates
|
||
|
||
Returns:
|
||
Template list
|
||
"""
|
||
data = await self._request("GET", "/templates")
|
||
templates = []
|
||
for t in data.get("templates", []):
|
||
templates.append(TemplateInfo(
|
||
template=t["template"],
|
||
display_name=t.get("displayName"),
|
||
description=t.get("description"),
|
||
category=t.get("category"),
|
||
port=t.get("port"),
|
||
env_info=t.get("env_info", {})
|
||
))
|
||
return templates
|
||
|
||
async def list_platform_templates(self) -> List[TemplateInfo]:
|
||
"""
|
||
Get all platform Agent templates
|
||
|
||
Call: GET /templates/platform
|
||
|
||
Platform Agent templates are predefined Agent types managed by the platform.
|
||
|
||
Returns:
|
||
Platform template list
|
||
"""
|
||
data = await self._request("GET", "/templates/platform")
|
||
templates = []
|
||
for t in data.get("templates", []):
|
||
templates.append(TemplateInfo(
|
||
template=t["template"],
|
||
display_name=t.get("display_name"),
|
||
description=t.get("description"),
|
||
category=t.get("category"),
|
||
port=t.get("port"),
|
||
env_info=t.get("env_info", {}),
|
||
template_type="platform"
|
||
))
|
||
return templates
|
||
|
||
async def list_custom_templates(self) -> List[TemplateInfo]:
|
||
"""
|
||
Get all custom Agent templates
|
||
|
||
Call: GET /templates/custom
|
||
|
||
Custom Agent templates require users to configure environment variables (e.g., API Key).
|
||
|
||
Returns:
|
||
Custom template list, each template's env_info includes:
|
||
- required: required environment variables
|
||
- optional: optional environment variables
|
||
"""
|
||
data = await self._request("GET", "/templates/custom")
|
||
templates = []
|
||
for t in data.get("templates", []):
|
||
templates.append(TemplateInfo(
|
||
template=t["template"],
|
||
display_name=t.get("display_name"),
|
||
description=t.get("description"),
|
||
category=t.get("category"),
|
||
port=t.get("port"),
|
||
env_info=t.get("env_info", {}),
|
||
template_type="custom"
|
||
))
|
||
return templates
|
||
|
||
async def get_template(self, template_name: str) -> TemplateInfo:
|
||
"""
|
||
Get template details
|
||
|
||
Call: GET /templates/{template_name}
|
||
|
||
Args:
|
||
template_name: Template name
|
||
|
||
Returns:
|
||
Template information
|
||
"""
|
||
data = await self._request("GET", f"/templates/{template_name}")
|
||
return TemplateInfo(
|
||
template=data["template"],
|
||
port=data.get("port"),
|
||
env_info=data.get("env_info", {})
|
||
)
|
||
|
||
# ==================== Agent Management ====================
|
||
|
||
async def create_agent(
|
||
self,
|
||
name: str,
|
||
template: str,
|
||
config: Optional[AgentConfig] = None,
|
||
env: Optional[Dict[str, str]] = None
|
||
) -> AgentCreateResult:
|
||
"""
|
||
Create Agent Pod
|
||
|
||
Call: POST /agents
|
||
|
||
This is the unified Agent creation interface, applicable to both platform and custom Agents.
|
||
|
||
Args:
|
||
name: Agent name (1-63 characters, lowercase letters, numbers, hyphens)
|
||
template: Template type (e.g., echo_agent, mysql_agent)
|
||
config: Resource configuration (including user_id, cpu, memory, etc.)
|
||
env: Environment variables (required for custom Agents, e.g., API Key)
|
||
|
||
Returns:
|
||
Creation result
|
||
|
||
Example:
|
||
# Create a platform Agent (single replica)
|
||
result = await client.create_agent(
|
||
name="alice-echo",
|
||
template="echo_agent",
|
||
config=AgentConfig(user_id="alice", replicas=1)
|
||
)
|
||
|
||
# Create a custom Agent (multiple replicas)
|
||
result = await client.create_agent(
|
||
name="my-mysql-agent",
|
||
template="mysql_agent",
|
||
config=AgentConfig(user_id="alice", replicas=2),
|
||
env={
|
||
"MYSQL_HOST": "mysql.example.com",
|
||
"MYSQL_USER": "root",
|
||
"MYSQL_PASSWORD": "password",
|
||
"MYSQL_DATABASE": "mydb",
|
||
"OPENAI_API_KEY": "sk-..."
|
||
}
|
||
)
|
||
"""
|
||
payload: Dict[str, Any] = {
|
||
"name": name,
|
||
"template": template
|
||
}
|
||
|
||
if config:
|
||
payload["config"] = config.to_dict()
|
||
|
||
# 构建环境变量,确保 LLM_BASE_URL 始终被传递(平台 Agent 和自定义 Agent 都需要)
|
||
final_env = {"LLM_BASE_URL": LLM_BASE_URL}
|
||
if env:
|
||
# 用户传入的环境变量会覆盖默认值(但通常不应覆盖 LLM_BASE_URL)
|
||
final_env.update(env)
|
||
payload["env"] = final_env
|
||
|
||
logger.info(
|
||
"creating_agent",
|
||
name=name,
|
||
template=template,
|
||
config=config.to_dict() if config else None,
|
||
has_env=True,
|
||
llm_base_url=LLM_BASE_URL
|
||
)
|
||
|
||
data = await self._request("POST", "/agents", json=payload)
|
||
|
||
return AgentCreateResult(
|
||
name=data["name"],
|
||
namespace=data["namespace"],
|
||
status=data["status"],
|
||
created_at=data["created_at"],
|
||
template=data["template"],
|
||
service_port=data.get("service_port"),
|
||
access_info=data.get("access_info"),
|
||
pod_id=data.get("pod_id"),
|
||
pod_ip=data.get("pod_ip"),
|
||
host_ip=data.get("host_ip"),
|
||
node_name=data.get("node_name"),
|
||
owner_info=data.get("owner_info")
|
||
)
|
||
|
||
async def create_platform_agent(
|
||
self,
|
||
name: str,
|
||
template: str,
|
||
user_id: str,
|
||
config: Optional[AgentConfig] = None
|
||
) -> AgentCreateResult:
|
||
"""
|
||
Create a platform Agent instance
|
||
|
||
This is a convenience method for create_agent, used to create platform Agents.
|
||
Platform Agents use predefined images from the platform, so users do not need to configure environment variables.
|
||
|
||
Args:
|
||
name: Agent instance name
|
||
template: Platform template name (e.g., echo_agent, jina_search_agent)
|
||
user_id: User ID (for resource isolation and billing)
|
||
config: Resource configuration (optional, uses template defaults)
|
||
|
||
Returns:
|
||
Creation result
|
||
"""
|
||
if config is None:
|
||
config = AgentConfig(user_id=user_id)
|
||
else:
|
||
config.user_id = user_id
|
||
|
||
logger.info(
|
||
"creating_platform_agent",
|
||
name=name,
|
||
template=template,
|
||
user_id=user_id
|
||
)
|
||
|
||
return await self.create_agent(
|
||
name=name,
|
||
template=template,
|
||
config=config
|
||
)
|
||
|
||
async def create_custom_agent(
|
||
self,
|
||
name: str,
|
||
template: str,
|
||
user_id: str,
|
||
env_vars: Dict[str, str],
|
||
config: Optional[AgentConfig] = None
|
||
) -> AgentCreateResult:
|
||
"""
|
||
Create a custom Agent instance
|
||
|
||
This is a convenience method for create_agent, used to create custom Agents.
|
||
Custom Agents require the user to provide environment variables (e.g., API keys, database connection info).
|
||
|
||
Args:
|
||
name: Agent instance name
|
||
template: Custom template name (e.g., mysql_agent, postgresql_agent)
|
||
user_id: User ID
|
||
env_vars: Environment variables (required, contains sensitive info like API keys)
|
||
config: Resource configuration (optional)
|
||
|
||
Returns:
|
||
Creation result
|
||
|
||
Example:
|
||
result = await client.create_custom_agent(
|
||
name="my-mysql-agent",
|
||
template="mysql_agent",
|
||
user_id="alice",
|
||
env_vars={
|
||
"MYSQL_HOST": "mysql.example.com",
|
||
"MYSQL_USER": "root",
|
||
"MYSQL_PASSWORD": "password",
|
||
"MYSQL_DATABASE": "mydb",
|
||
"OPENAI_API_KEY": "sk-..."
|
||
}
|
||
)
|
||
"""
|
||
if config is None:
|
||
config = AgentConfig(user_id=user_id)
|
||
else:
|
||
config.user_id = user_id
|
||
|
||
logger.info(
|
||
"creating_custom_agent",
|
||
name=name,
|
||
template=template,
|
||
user_id=user_id,
|
||
env_keys=list(env_vars.keys()) if env_vars else []
|
||
)
|
||
|
||
return await self.create_agent(
|
||
name=name,
|
||
template=template,
|
||
config=config,
|
||
env=env_vars
|
||
)
|
||
|
||
async def generate_agent_from_tools(
|
||
self,
|
||
agent_name: str,
|
||
description: str,
|
||
tools: List[Dict[str, Any]],
|
||
user_id: str,
|
||
tenant_id: Optional[str] = None,
|
||
auto_deploy: bool = True
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
使用外部数据工具生成 Agent
|
||
|
||
调用 Agent Manager 的 POST /external-tools/agents/create-with-tools 接口
|
||
不需要预定义模板,根据工具配置动态生成 Agent
|
||
|
||
Args:
|
||
agent_name: Agent 名称
|
||
description: Agent 描述
|
||
tools: 工具配置列表,每个工具包含:
|
||
- name: 工具名称
|
||
- description: 工具描述
|
||
- url: API 端点 URL
|
||
- method: HTTP 方法
|
||
- user_id: 用户 ID
|
||
- headers: (可选) 自定义请求头
|
||
- auth: (可选) 认证配置
|
||
- request_params: (可选) URL 查询参数定义
|
||
- request_body: (可选) 请求体定义
|
||
- response_mapping: (可选) 响应字段映射
|
||
- timeout: (可选) 超时时间
|
||
- retry: (可选) 重试配置
|
||
user_id: 用户 ID
|
||
tenant_id: (可选) 租户 ID
|
||
auto_deploy: 是否自动部署,默认 True
|
||
|
||
Returns:
|
||
生成结果,包含 agent_id、status 等信息
|
||
|
||
Example:
|
||
result = await client.generate_agent_from_tools(
|
||
agent_name="my-api-agent",
|
||
description="调用外部 API 的 Agent",
|
||
tools=[
|
||
{
|
||
"name": "天气查询",
|
||
"description": "查询城市天气",
|
||
"url": "https://api.weather.com/forecast",
|
||
"method": "GET",
|
||
"user_id": "user-001",
|
||
"auth": {"type": "api_key", "key": "sk-xxx", "in": "header", "name": "X-API-Key"},
|
||
"request_params": {"type": "object", "properties": {"city": {"type": "string"}}}
|
||
}
|
||
],
|
||
user_id="user-001",
|
||
auto_deploy=True
|
||
)
|
||
"""
|
||
logger.info(
|
||
"generating_agent_from_tools",
|
||
agent_name=agent_name,
|
||
tool_count=len(tools),
|
||
user_id=user_id,
|
||
auto_deploy=auto_deploy
|
||
)
|
||
|
||
payload = {
|
||
"agent_name": agent_name,
|
||
"description": description,
|
||
"tools": tools,
|
||
"user_id": user_id,
|
||
"auto_deploy": auto_deploy
|
||
}
|
||
|
||
if tenant_id:
|
||
payload["tenant_id"] = tenant_id
|
||
|
||
return await self._request("POST", "/tools/generate-agent", json=payload)
|
||
|
||
async def delete_agent(self, agent_name: str) -> Dict[str, Any]:
|
||
"""
|
||
Delete Agent Pod
|
||
|
||
Call: DELETE /agents/{agent_name}
|
||
|
||
Args:
|
||
agent_name: Agent name
|
||
|
||
Returns:
|
||
Deletion result, including a message field
|
||
"""
|
||
logger.info("deleting_agent", name=agent_name)
|
||
return await self._request("DELETE", f"/agents/{agent_name}")
|
||
|
||
async def get_agent_status(self, agent_name: str) -> AgentStatusResult:
|
||
"""
|
||
Get Agent status
|
||
|
||
Call: GET /agents/{agent_name}/status
|
||
|
||
Agent Manager return format (after v2 update):
|
||
{
|
||
"name": "alice-echo",
|
||
"namespace": "ai-agents",
|
||
"status": "Running",
|
||
"health_status": "healthy",
|
||
"created_at": "2026-01-06T09:00:00Z",
|
||
"pod_ip": "10.244.1.100",
|
||
"host_ip": "192.168.1.10",
|
||
"node_name": "node-1",
|
||
"labels": {"template": "echo_agent", "user_id": "alice"},
|
||
"service_port": 8080,
|
||
"access_url": "http://alice-echo.ai-agents.svc.cluster.local:8080",
|
||
"containers": [...],
|
||
"resources": {...},
|
||
"endpoints": [...],
|
||
"conditions": [...]
|
||
}
|
||
|
||
Args:
|
||
agent_name: Agent name
|
||
|
||
Returns:
|
||
Agent status information, including:
|
||
- Basic info: name, namespace, status, health_status
|
||
- Network info: pod_ip, host_ip, service_port, access_url, endpoints
|
||
- Container status: containers (including ready, restart_count, state, etc.)
|
||
- Resource configuration: resources (including requests and limits)
|
||
- Pod conditions: conditions
|
||
|
||
Accessible via properties:
|
||
- status.is_healthy: Is it healthy
|
||
- status.is_running: Is it running
|
||
- status.is_ready: Is it ready
|
||
- status.total_restart_count: Total restart count
|
||
- status.has_crashed_container: Is there a crashed container
|
||
- status.cpu_request/cpu_limit: CPU configuration
|
||
- status.memory_request/memory_limit: Memory configuration
|
||
"""
|
||
response = await self._request("GET", f"/agents/{agent_name}/status")
|
||
|
||
# Agent Manager 可能返回 {"success": true, "data": {...}} 或直接返回数据
|
||
if isinstance(response, dict) and "data" in response:
|
||
data = response["data"]
|
||
else:
|
||
data = response
|
||
|
||
# 记录返回的原始数据,便于调试
|
||
logger.debug(
|
||
"agent_status_response",
|
||
agent_name=agent_name,
|
||
response_keys=list(data.keys()) if isinstance(data, dict) else None,
|
||
status=data.get("status") if isinstance(data, dict) else None
|
||
)
|
||
|
||
# Parse container status list
|
||
containers = []
|
||
for c in data.get("containers", []):
|
||
containers.append(ContainerStatus(
|
||
name=c.get("name", "unknown"),
|
||
ready=c.get("ready", False),
|
||
restart_count=c.get("restart_count", 0),
|
||
state=c.get("state", "unknown"),
|
||
reason=c.get("reason"),
|
||
message=c.get("message"),
|
||
exit_code=c.get("exit_code"),
|
||
started_at=c.get("started_at"),
|
||
finished_at=c.get("finished_at")
|
||
))
|
||
|
||
# Parse endpoints (can be list or dict)
|
||
endpoints_data = data.get("endpoints")
|
||
if isinstance(endpoints_data, dict):
|
||
# Old format: dictionary
|
||
endpoints = list(endpoints_data.values()) if endpoints_data else None
|
||
else:
|
||
# New format: list
|
||
endpoints = endpoints_data
|
||
|
||
# ========== 解析 access_info(域名和外网IP) ==========
|
||
access_info = data.get("access_info") or {}
|
||
external_ip = access_info.get("external_ip")
|
||
domain = access_info.get("domain")
|
||
domain_url = access_info.get("domain_url")
|
||
ip_url = access_info.get("ip_url")
|
||
# 优先使用 access_info 中的推荐地址,否则使用旧的 access_url
|
||
access_url = access_info.get("recommended") or data.get("access_url")
|
||
# ======================================================
|
||
|
||
# 确保状态有有效值
|
||
agent_status = data.get("status", "Pending")
|
||
if not agent_status or agent_status.lower() == "unknown":
|
||
agent_status = "Pending"
|
||
|
||
return AgentStatusResult(
|
||
name=data.get("name", agent_name),
|
||
namespace=data.get("namespace", "ai-agents"),
|
||
status=agent_status,
|
||
health_status=data.get("health_status", "unknown"),
|
||
created_at=data.get("created_at"),
|
||
pod_ip=data.get("pod_ip"),
|
||
host_ip=data.get("host_ip"),
|
||
node_name=data.get("node_name"),
|
||
labels=data.get("labels"),
|
||
service_port=data.get("service_port"),
|
||
access_url=access_url,
|
||
containers=containers,
|
||
resources=data.get("resources"),
|
||
endpoints=endpoints,
|
||
conditions=data.get("conditions"),
|
||
# Extract template from labels (for compatibility)
|
||
template=data.get("labels", {}).get("template") if data.get("labels") else None,
|
||
# ========== 新增:访问信息字段 ==========
|
||
external_ip=external_ip,
|
||
domain=domain,
|
||
domain_url=domain_url,
|
||
ip_url=ip_url,
|
||
)
|
||
|
||
async def get_agent_metrics(self, agent_name: str) -> AgentMetricsResult:
|
||
"""
|
||
Get Agent resource usage
|
||
|
||
Call: GET /agents/{agent_name}/metrics
|
||
|
||
Agent Manager return format:
|
||
{
|
||
"name": "alice-echo",
|
||
"namespace": "ai-agents",
|
||
"requests": {
|
||
"cpu": "100m",
|
||
"memory": "128Mi"
|
||
},
|
||
"limits": {
|
||
"cpu": "500m",
|
||
"memory": "512Mi"
|
||
},
|
||
"usage": {
|
||
"cpu": "14502n",
|
||
"memory": "8704Ki"
|
||
},
|
||
"timestamp": "2026-01-06T05:01:04Z",
|
||
"metrics_available": null
|
||
}
|
||
|
||
Args:
|
||
agent_name: Agent name
|
||
|
||
Returns:
|
||
Resource usage information, accessible via properties:
|
||
|
||
Quota information:
|
||
- metrics.cpu_limit: CPU limit string (e.g., "500m")
|
||
- metrics.memory_limit: Memory limit string (e.g., "512Mi")
|
||
- metrics.cpu_request: CPU request string (e.g., "100m")
|
||
- metrics.memory_request: Memory request string (e.g., "128Mi")
|
||
- metrics.cpu_limit_millicores: CPU limit (millicores)
|
||
- metrics.memory_limit_mb: Memory limit (MB)
|
||
|
||
Real-time usage (requires metrics-server):
|
||
- metrics.usage: Real-time resource usage dictionary
|
||
- metrics.cpu_usage_current: Current CPU usage string (e.g., "14502n")
|
||
- metrics.memory_usage_current: Current memory usage string (e.g., "8704Ki")
|
||
- metrics.cpu_usage_current_millicores: Current CPU usage (millicores)
|
||
- metrics.memory_usage_current_mb: Current memory usage (MB)
|
||
- metrics.cpu_utilization_percent: CPU utilization percentage
|
||
- metrics.memory_utilization_percent: Memory utilization percentage
|
||
- metrics.has_realtime_metrics: Whether there is real-time data
|
||
- metrics.timestamp: Timestamp of metrics data
|
||
- metrics.metrics_available: Whether metrics-server is available
|
||
|
||
Compatibility properties:
|
||
- metrics.available: Is it available (based on limits)
|
||
"""
|
||
data = await self._request("GET", f"/agents/{agent_name}/metrics")
|
||
|
||
return AgentMetricsResult(
|
||
name=data.get("name", agent_name),
|
||
namespace=data.get("namespace", "ai-agents"),
|
||
requests=data.get("requests", {}),
|
||
limits=data.get("limits", {}),
|
||
usage=data.get("usage"), # New: Real-time resource usage
|
||
timestamp=data.get("timestamp"), # New: metrics timestamp
|
||
metrics_available=data.get("metrics_available"), # New: metrics-server availability
|
||
resources=data.get("resources", {}) # Compatible with old format
|
||
)
|
||
|
||
async def list_agents(self, template: Optional[str] = None) -> AgentListResult:
|
||
"""
|
||
List all Agents
|
||
|
||
Call: GET /agents
|
||
|
||
Args:
|
||
template: Filter by template type (optional)
|
||
|
||
Returns:
|
||
Agent list result
|
||
"""
|
||
params = {}
|
||
if template:
|
||
params["template"] = template
|
||
|
||
data = await self._request("GET", "/agents", params=params)
|
||
|
||
return AgentListResult(
|
||
agents=data.get("agents", []),
|
||
count=data.get("count", 0)
|
||
)
|
||
|
||
# ==================== External Data Tool Management ====================
|
||
# 外部数据工具管理接口
|
||
# Agent Manager 负责生成和存储 Pydantic 工具文件,返回 tool_ref_id 给 MCP-Server
|
||
|
||
async def generate_external_tool(
|
||
self,
|
||
name: str,
|
||
description: str,
|
||
url: str,
|
||
method: str,
|
||
user_id: str,
|
||
tenant_id: Optional[str] = None,
|
||
headers: Optional[Dict[str, str]] = None,
|
||
auth: Optional[Dict[str, Any]] = None,
|
||
request_params: Optional[Dict[str, Any]] = None,
|
||
request_body: Optional[Dict[str, Any]] = None,
|
||
response_mapping: Optional[Dict[str, Any]] = None,
|
||
timeout: int = 30,
|
||
retry: Optional[Dict[str, Any]] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
生成外部数据工具
|
||
|
||
调用: POST /tools/generate
|
||
|
||
将工具配置发送给 Agent Manager,AM 会:
|
||
1. 根据配置生成 Pydantic 工具代码文件
|
||
2. 存储工具文件和配置
|
||
3. 返回 tool_ref_id(工具标识)
|
||
|
||
Args:
|
||
name: 工具名称
|
||
description: 工具描述
|
||
url: API 端点 URL
|
||
method: HTTP 方法(GET/POST/PUT/DELETE/PATCH)
|
||
user_id: 用户 ID
|
||
tenant_id: 租户 ID(可选)
|
||
headers: 自定义请求头
|
||
auth: 认证配置(type, key, in, name 等)
|
||
request_params: URL 查询参数定义(JSON Schema 格式)
|
||
request_body: 请求体定义(JSON Schema 格式)
|
||
response_mapping: 响应字段映射
|
||
timeout: 超时时间(秒)
|
||
retry: 重试配置
|
||
|
||
Returns:
|
||
{
|
||
"success": true,
|
||
"tool_ref_id": "tool-xxx-123",
|
||
"tool_name": "weather_query_tool",
|
||
"status": "active",
|
||
"message": "工具生成成功"
|
||
}
|
||
"""
|
||
payload: Dict[str, Any] = {
|
||
"name": name,
|
||
"description": description,
|
||
"url": url,
|
||
"method": method,
|
||
"user_id": user_id,
|
||
}
|
||
|
||
if tenant_id:
|
||
payload["tenant_id"] = tenant_id
|
||
if headers:
|
||
payload["headers"] = headers
|
||
if auth:
|
||
payload["auth"] = auth
|
||
|
||
# Agent Manager 期望的是 input_schema,而不是 request_params/request_body
|
||
# input_schema 统一使用 JSON Schema 格式定义工具的输入参数
|
||
input_schema = request_params or request_body
|
||
if input_schema:
|
||
payload["input_schema"] = input_schema
|
||
|
||
if response_mapping:
|
||
payload["response_mapping"] = response_mapping
|
||
if timeout:
|
||
payload["timeout"] = timeout
|
||
if retry:
|
||
payload["retry"] = retry
|
||
|
||
logger.info(
|
||
"generating_external_tool",
|
||
name=name,
|
||
url=url,
|
||
method=method,
|
||
user_id=user_id,
|
||
has_input_schema=input_schema is not None
|
||
)
|
||
|
||
return await self._request("POST", "/external-tools/generate", json=payload)
|
||
|
||
async def update_external_tool(
|
||
self,
|
||
tool_ref_id: str,
|
||
name: str,
|
||
description: str,
|
||
url: str,
|
||
method: str,
|
||
user_id: str,
|
||
tenant_id: Optional[str] = None,
|
||
headers: Optional[Dict[str, str]] = None,
|
||
auth: Optional[Dict[str, Any]] = None,
|
||
request_params: Optional[Dict[str, Any]] = None,
|
||
request_body: Optional[Dict[str, Any]] = None,
|
||
response_mapping: Optional[Dict[str, Any]] = None,
|
||
timeout: int = 30,
|
||
retry: Optional[Dict[str, Any]] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
更新外部数据工具
|
||
|
||
调用: PUT /external-tools/{tool_ref_id}
|
||
|
||
Agent Manager 会重新生成工具文件,可能返回新的 tool_ref_id。
|
||
|
||
Args:
|
||
tool_ref_id: 原工具标识
|
||
其他参数同 generate_external_tool
|
||
|
||
Returns:
|
||
{
|
||
"success": true,
|
||
"tool_ref_id": "tool-xxx-123-v2",
|
||
"status": "active",
|
||
"message": "工具更新成功"
|
||
}
|
||
"""
|
||
payload: Dict[str, Any] = {
|
||
"name": name,
|
||
"description": description,
|
||
"url": url,
|
||
"method": method,
|
||
"user_id": user_id,
|
||
}
|
||
|
||
if tenant_id:
|
||
payload["tenant_id"] = tenant_id
|
||
if headers:
|
||
payload["headers"] = headers
|
||
if auth:
|
||
payload["auth"] = auth
|
||
|
||
# Agent Manager 期望的是 input_schema,而不是 request_params/request_body
|
||
input_schema = request_params or request_body
|
||
if input_schema:
|
||
payload["input_schema"] = input_schema
|
||
|
||
if response_mapping:
|
||
payload["response_mapping"] = response_mapping
|
||
if timeout:
|
||
payload["timeout"] = timeout
|
||
if retry:
|
||
payload["retry"] = retry
|
||
|
||
logger.info(
|
||
"updating_external_tool",
|
||
tool_ref_id=tool_ref_id,
|
||
name=name,
|
||
user_id=user_id,
|
||
has_input_schema=input_schema is not None
|
||
)
|
||
|
||
return await self._request("PUT", f"/external-tools/{tool_ref_id}", json=payload)
|
||
|
||
async def delete_external_tool(self, tool_ref_id: str) -> Dict[str, Any]:
|
||
"""
|
||
删除外部数据工具
|
||
|
||
调用: DELETE /external-tools/{tool_ref_id}
|
||
|
||
Agent Manager 会删除对应的工具文件和配置。
|
||
|
||
Args:
|
||
tool_ref_id: 工具标识
|
||
|
||
Returns:
|
||
{
|
||
"success": true,
|
||
"message": "工具删除成功"
|
||
}
|
||
"""
|
||
logger.info("deleting_external_tool", tool_ref_id=tool_ref_id)
|
||
return await self._request("DELETE", f"/external-tools/{tool_ref_id}")
|
||
|
||
async def test_external_tool(
|
||
self,
|
||
tool_ref_id: str,
|
||
test_params: Optional[Dict[str, Any]] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
测试外部数据工具连接
|
||
|
||
调用: POST /external-tools/{tool_ref_id}/test
|
||
|
||
Agent Manager 会尝试调用工具的 API 并返回测试结果。
|
||
|
||
Args:
|
||
tool_ref_id: 工具标识
|
||
test_params: 测试参数(可选)
|
||
|
||
Returns:
|
||
{
|
||
"success": true,
|
||
"connected": true,
|
||
"response_time_ms": 156,
|
||
"status_code": 200,
|
||
"sample_response": {...}
|
||
}
|
||
"""
|
||
payload = {}
|
||
if test_params:
|
||
payload["test_params"] = test_params
|
||
|
||
logger.info("testing_external_tool", tool_ref_id=tool_ref_id)
|
||
return await self._request("POST", f"/external-tools/{tool_ref_id}/test", json=payload)
|
||
|
||
async def create_agent_with_tools(
|
||
self,
|
||
name: str,
|
||
template: str,
|
||
tool_refs: List[str],
|
||
config: Optional[AgentConfig] = None,
|
||
env: Optional[Dict[str, str]] = None
|
||
) -> AgentCreateResult:
|
||
"""
|
||
创建带有外部数据工具的 Agent
|
||
|
||
调用: POST /external-tools/agents/create-with-tools (Agent Manager v2.0)
|
||
|
||
Agent Manager 会根据 tool_refs 加载对应的工具文件,部署到 AKS。
|
||
|
||
Args:
|
||
name: Agent 名称
|
||
template: Agent 模板(如 echo_agent)
|
||
tool_refs: 外部数据工具标识列表
|
||
config: 资源配置
|
||
env: 环境变量
|
||
|
||
Returns:
|
||
创建结果
|
||
"""
|
||
payload: Dict[str, Any] = {
|
||
"name": name,
|
||
"template": template,
|
||
"tool_refs": tool_refs
|
||
}
|
||
|
||
if config:
|
||
payload["config"] = config.to_dict()
|
||
|
||
# 构建环境变量
|
||
final_env = {"LLM_BASE_URL": LLM_BASE_URL}
|
||
if env:
|
||
final_env.update(env)
|
||
payload["env"] = final_env
|
||
|
||
logger.info(
|
||
"creating_agent_with_tools",
|
||
name=name,
|
||
template=template,
|
||
tool_refs=tool_refs,
|
||
config=config.to_dict() if config else None
|
||
)
|
||
|
||
response = await self._request("POST", "/external-tools/agents/create-with-tools", json=payload)
|
||
|
||
# 检查业务逻辑是否成功(agent-manager 可能返回 HTTP 200 但 success=false)
|
||
if isinstance(response, dict) and response.get("success") is False:
|
||
error_code = response.get("error", "unknown_error")
|
||
error_message = response.get("message", "创建 Agent 失败")
|
||
logger.error(
|
||
"create_agent_with_tools_failed",
|
||
name=name,
|
||
error_code=error_code,
|
||
error_message=error_message
|
||
)
|
||
raise AgentManagerError(
|
||
message=error_message,
|
||
status_code=400,
|
||
detail={"error": error_code, "message": error_message}
|
||
)
|
||
|
||
# Agent Manager 返回格式: {"success": true, "data": {...}} 或直接返回数据
|
||
# 需要兼容两种格式
|
||
if isinstance(response, dict) and "data" in response:
|
||
data = response["data"]
|
||
else:
|
||
data = response
|
||
|
||
# 确保状态字段有默认值,避免 unknown
|
||
agent_status = data.get("status", "Pending")
|
||
if not agent_status or agent_status.lower() == "unknown":
|
||
# 如果 Agent 已经创建成功,但状态未知,默认设为 Pending
|
||
agent_status = "Pending"
|
||
|
||
logger.info(
|
||
"agent_with_tools_created",
|
||
name=name,
|
||
status=agent_status,
|
||
response_keys=list(data.keys()) if isinstance(data, dict) else None
|
||
)
|
||
|
||
return AgentCreateResult(
|
||
name=data.get("name", name),
|
||
namespace=data.get("namespace", "ai-agents"),
|
||
status=agent_status,
|
||
created_at=data.get("created_at", ""),
|
||
template=data.get("template", template),
|
||
service_port=data.get("service_port"),
|
||
access_info=data.get("access_info"),
|
||
pod_id=data.get("pod_id"),
|
||
pod_ip=data.get("pod_ip"),
|
||
host_ip=data.get("host_ip"),
|
||
node_name=data.get("node_name"),
|
||
owner_info=data.get("owner_info")
|
||
)
|
||
|
||
# ==================== Unimplemented Interfaces (Not yet provided by Agent Manager) ====================
|
||
# The interfaces called by the following methods are not yet implemented in Agent Manager
|
||
# Method signatures are retained for future extension, but will raise NotImplementedError when called
|
||
|
||
async def scale_agent(
|
||
self,
|
||
agent_name: str,
|
||
replicas: Optional[int] = None,
|
||
cpu_request: Optional[str] = None,
|
||
cpu_limit: Optional[str] = None,
|
||
memory_request: Optional[str] = None,
|
||
memory_limit: Optional[str] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
Scale Agent (to be implemented)
|
||
|
||
Expected call: PATCH /agents/{agent_name}/scale
|
||
|
||
Note: Agent Manager has not yet implemented this interface
|
||
"""
|
||
raise NotImplementedError(
|
||
"Agent Manager has not yet implemented the PATCH /agents/{name}/scale interface"
|
||
)
|
||
|
||
async def get_agent_logs(
|
||
self,
|
||
agent_name: str,
|
||
tail_lines: int = 100,
|
||
since_seconds: Optional[int] = None
|
||
) -> str:
|
||
"""
|
||
Get Agent logs (to be implemented)
|
||
|
||
Expected call: GET /agents/{agent_name}/logs
|
||
|
||
Note: Agent Manager has not yet implemented this interface
|
||
"""
|
||
raise NotImplementedError(
|
||
"Agent Manager has not yet implemented the GET /agents/{name}/logs interface"
|
||
)
|
||
|
||
async def restart_agent(self, agent_name: str) -> Dict[str, Any]:
|
||
"""
|
||
Restart Agent (to be implemented)
|
||
|
||
Expected call: POST /agents/{agent_name}/restart
|
||
|
||
Note: Agent Manager has not yet implemented this interface
|
||
"""
|
||
raise NotImplementedError(
|
||
"Agent Manager has not yet implemented the POST /agents/{name}/restart interface"
|
||
)
|
||
|
||
async def get_resource_stats(self) -> Dict[str, Any]:
|
||
"""
|
||
Get resource statistics (to be implemented)
|
||
|
||
Expected call: GET /resources/stats
|
||
|
||
Note: Agent Manager has not yet implemented this interface
|
||
"""
|
||
raise NotImplementedError(
|
||
"Agent Manager has not yet implemented the GET /resources/stats interface"
|
||
)
|
||
|
||
async def get_user_resources(self, user_id: str) -> Dict[str, Any]:
|
||
"""
|
||
Get user resource usage statistics (to be implemented)
|
||
|
||
Expected call: GET /resources/stats/by-user/{user_id}
|
||
|
||
Note: Agent Manager has not yet implemented this interface
|
||
"""
|
||
raise NotImplementedError(
|
||
"Agent Manager has not yet implemented the GET /resources/stats/by-user/{user_id} interface"
|
||
)
|
||
|
||
async def get_channel_resources(self, channel_id: str) -> Dict[str, Any]:
|
||
"""
|
||
Get channel resource usage statistics (to be implemented)
|
||
|
||
Expected call: GET /resources/stats/by-channel/{channel_id}
|
||
|
||
Note: Agent Manager has not yet implemented this interface
|
||
"""
|
||
raise NotImplementedError(
|
||
"Agent Manager has not yet implemented the GET /resources/stats/by-channel/{channel_id} interface"
|
||
)
|
||
|
||
|
||
# Global client instance
|
||
_agent_manager_client: Optional[AgentManagerClient] = None
|
||
|
||
|
||
def get_agent_manager_client() -> AgentManagerClient:
|
||
"""Get global Agent Manager client instance"""
|
||
global _agent_manager_client
|
||
if _agent_manager_client is None:
|
||
_agent_manager_client = AgentManagerClient()
|
||
return _agent_manager_client
|
||
|
||
|
||
async def close_agent_manager_client():
|
||
"""Close global client"""
|
||
global _agent_manager_client
|
||
if _agent_manager_client:
|
||
await _agent_manager_client.close()
|
||
_agent_manager_client = None
|