This commit is contained in:
Ubuntu
2026-01-05 12:44:28 +00:00
commit 23116e9086
64 changed files with 9030 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# AKS配置
AKS_SUBSCRIPTION_ID=your-subscription-id-here
AKS_RESOURCE_GROUP=your-resource-group-here
AKS_CLUSTER_NAME=your-aks-cluster-name-here
AKS_DEFAULT_NAMESPACE=default
# 容器镜像仓库配置
REGISTRY_URL=your-registry.azurecr.io
REGISTRY_USERNAME=your-registry-username
REGISTRY_PASSWORD=your-registry-password
# 功能开关
ENABLE_QUOTA_MANAGEMENT=true
ENABLE_LIFECYCLE_MANAGEMENT=true
ENABLE_RETRY_MECHANISM=true
ENABLE_METERING=true
# 生命周期管理配置
CLEANUP_INTERVAL=60
# Web服务配置
SERVER_HOST=0.0.0.0
SERVER_PORT=8000
SERVER_WORKERS=1
+46
View File
@@ -0,0 +1,46 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
ENV/
env/
.venv
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# Testing
.pytest_cache/
.coverage
htmlcov/
# Environment variables
.env
.env.local
# Logs
*.log
+842
View File
@@ -0,0 +1,842 @@
# Agent Manager API 接口文档
## 基础信息
- **Base URL**: `http://localhost:8000`
- **版本**: v1.0.0
- **协议**: HTTP/HTTPS
- **数据格式**: JSON
---
## 目录
1. [Agent 管理](#agent-管理)
2. [模板查询](#模板查询)
3. [状态监控](#状态监控)
4. [资源管理](#资源管理)
---
## Agent 管理
### 1. 创建 Agent
创建一个新的 AI Agent 实例。
**请求**
```http
POST /agents
Content-Type: application/json
```
**请求参数**
```json
{
"name": "agent-name", // 必填,Agent名称,必须唯一
"template": "echo_agent", // 必填,模板类型
"config": { // 必填,配置信息
"user_id": "user-001", // 推荐,用户标识,用于多租户管理
"cpu_request": "100m", // 可选,CPU请求量
"cpu_limit": "500m", // 可选,CPU限制
"memory_request": "128Mi", // 可选,内存请求量
"memory_limit": "512Mi" // 可选,内存限制
},
"env": { // 可选,环境变量
"KEY": "value"
}
}
```
**支持的模板类型**
| 模板 | 说明 | 类型 |
|------|------|------|
| `echo_agent` | Echo 测试服务 | 平台 |
| `chat_agent` | 聊天服务 | 平台 |
| `code_agent` | 代码执行服务 | 平台 |
| `search_agent` | 搜索服务 | 平台 |
| `jina_search_agent` | Jina 搜索服务 | 平台 |
| `mysql_agent` | MySQL 客户端 | 自定义 |
| `postgresql_agent` | PostgreSQL 客户端 | 自定义 |
**响应**
```json
{
"name": "agent-name",
"namespace": "ai-agents",
"status": "Pending",
"created_at": "2026-01-05T07:35:00+00:00",
"template": "echo_agent",
"service_port": null,
"access_info": null,
"pod_id": "111175ce-8118-484d-9b3e-009733644acf",
"pod_ip": "10.244.2.24",
"host_ip": "10.224.0.5",
"node_name": "aks-node-123",
"owner_info": {
"user_id": "user-001",
"agent_name": "agent-name",
"namespace": "ai-agents",
"labels": {
"app": "ai-agent",
"managed-by": "agent-manager",
"template": "echo_agent",
"user-id": "user-001"
}
}
}
```
**状态码**
- `201` - 创建成功
- `400` - 请求参数错误
- `409` - Agent 已存在
- `500` - 服务器内部错误
**示例**
```bash
curl -X POST http://localhost:8000/agents \
-H "Content-Type: application/json" \
-d '{
"name": "alice-echo",
"template": "echo_agent",
"config": {
"user_id": "alice"
}
}'
```
---
### 2. 查询 Agent 列表
获取所有 Agent 的列表。
**请求**
```http
GET /agents
```
**查询参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| template | string | 否 | 按模板类型过滤 |
**响应**
```json
{
"agents": [
{
"name": "alice-echo",
"namespace": "ai-agents",
"status": "Running",
"template": "echo_agent",
"pod_ip": "10.244.2.24",
"labels": {
"user-id": "alice"
}
}
],
"count": 1
}
```
**示例**
```bash
# 获取所有 Agents
curl http://localhost:8000/agents
# 按模板过滤
curl http://localhost:8000/agents?template=echo_agent
```
---
### 3. 获取 Agent 状态
获取指定 Agent 的详细状态信息。
**请求**
```http
GET /agents/{agent_name}/status
```
**路径参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| agent_name | string | Agent 名称 |
**响应**
```json
{
"name": "alice-echo",
"namespace": "ai-agents",
"status": "Running",
"pod_ip": "10.244.2.24",
"node_name": "aks-node-123",
"created_at": "2026-01-05T07:35:00+00:00",
"labels": {
"user-id": "alice",
"template": "echo_agent"
}
}
```
**状态码**
- `200` - 成功
- `404` - Agent 不存在
- `500` - 服务器内部错误
**示例**
```bash
curl http://localhost:8000/agents/alice-echo/status
```
---
### 4. 获取 Agent 资源使用情况
获取 Agent 的 CPU 和内存使用情况。
**请求**
```http
GET /agents/{agent_name}/metrics
```
**响应**
```json
{
"name": "alice-echo",
"namespace": "ai-agents",
"resources": {
"cpu_usage": "50m",
"memory_usage": "128Mi",
"available": true
}
}
```
**示例**
```bash
curl http://localhost:8000/agents/alice-echo/metrics
```
---
### 5. 删除 Agent
删除指定的 Agent。
**请求**
```http
DELETE /agents/{agent_name}
```
**路径参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| agent_name | string | Agent 名称 |
**响应**
```json
{
"message": "Agent alice-echo 删除成功"
}
```
**状态码**
- `200` - 删除成功
- `404` - Agent 不存在
- `500` - 服务器内部错误
**示例**
```bash
curl -X DELETE http://localhost:8000/agents/alice-echo
```
---
## 模板查询
### 1. 获取所有模板
获取所有可用的 Agent 模板列表。
**请求**
```http
GET /templates
```
**响应**
```json
{
"templates": [
{
"template": "echo_agent",
"port": null,
"env_info": {}
},
{
"template": "jina_search_agent",
"port": 8080,
"env_info": {}
},
{
"template": "mysql_agent",
"port": null,
"env_info": {
"required": {
"MYSQL_HOST": "MySQL数据库主机地址",
"MYSQL_USER": "MySQL用户名",
"MYSQL_PASSWORD": "MySQL密码",
"MYSQL_DATABASE": "MySQL数据库名",
"OPENAI_API_KEY": "OpenAI API密钥"
},
"optional": {
"MYSQL_PORT": "MySQL端口,默认3306"
}
}
}
],
"count": 7
}
```
**示例**
```bash
curl http://localhost:8000/templates
```
---
### 2. 获取平台模板
获取平台提供的标准 Agent 模板列表。
**请求**
```http
GET /templates/platform
```
**响应**
```json
{
"templates": [
{
"template": "echo_agent",
"type": "platform",
"port": null,
"env_info": {}
},
{
"template": "chat_agent",
"type": "platform",
"port": null,
"env_info": {}
},
{
"template": "code_agent",
"type": "platform",
"port": null,
"env_info": {}
},
{
"template": "search_agent",
"type": "platform",
"port": null,
"env_info": {}
},
{
"template": "jina_search_agent",
"type": "platform",
"port": 8080,
"env_info": {}
}
],
"count": 5,
"type": "platform"
}
```
**平台模板说明**
- **echo_agent**: 简单的 Echo 服务,用于测试
- **chat_agent**: 聊天对话服务
- **code_agent**: 代码生成和执行服务
- **search_agent**: 通用搜索服务
- **jina_search_agent**: 基于 Jina 的向量搜索服务(端口: 8080)
**示例**
```bash
curl http://localhost:8000/templates/platform
```
---
### 3. 获取自定义模板
获取需要用户配置环境变量的自定义 Agent 模板列表。
**请求**
```http
GET /templates/custom
```
**响应**
```json
{
"templates": [
{
"template": "mysql_agent",
"type": "custom",
"port": null,
"env_info": {
"required": {
"MYSQL_HOST": "MySQL数据库主机地址",
"MYSQL_USER": "MySQL用户名",
"MYSQL_PASSWORD": "MySQL密码",
"MYSQL_DATABASE": "MySQL数据库名",
"OPENAI_API_KEY": "OpenAI API密钥"
},
"optional": {
"MYSQL_PORT": "MySQL端口,默认3306"
}
}
},
{
"template": "postgresql_agent",
"type": "custom",
"port": null,
"env_info": {
"required": {
"POSTGRES_HOST": "PostgreSQL数据库主机地址",
"POSTGRES_USER": "PostgreSQL用户名",
"POSTGRES_PASSWORD": "PostgreSQL密码",
"POSTGRES_DATABASE": "PostgreSQL数据库名",
"OPENAI_API_KEY": "OpenAI API密钥"
},
"optional": {
"POSTGRES_PORT": "PostgreSQL端口,默认5432"
}
}
}
],
"count": 2,
"type": "custom"
}
```
**自定义模板说明**
自定义模板需要用户在创建时通过 `env` 参数提供必需的环境变量。
**示例:创建 MySQL Agent**
```bash
curl -X POST http://localhost:8000/agents \
-H "Content-Type: application/json" \
-d '{
"name": "my-mysql-agent",
"template": "mysql_agent",
"config": {
"user_id": "alice"
},
"env": {
"MYSQL_HOST": "mysql.example.com",
"MYSQL_USER": "root",
"MYSQL_PASSWORD": "password",
"MYSQL_DATABASE": "mydb",
"OPENAI_API_KEY": "sk-..."
}
}'
```
**示例**
```bash
curl http://localhost:8000/templates/custom
```
---
### 4. 获取指定模板详情
获取单个模板的详细信息。
**请求**
```http
GET /templates/{template_name}
```
**路径参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| template_name | string | 模板名称 |
**响应**
```json
{
"template": "mysql_agent",
"port": null,
"env_info": {
"required": {
"MYSQL_HOST": "MySQL数据库主机地址",
"MYSQL_USER": "MySQL用户名",
"MYSQL_PASSWORD": "MySQL密码",
"MYSQL_DATABASE": "MySQL数据库名",
"OPENAI_API_KEY": "OpenAI API密钥"
},
"optional": {
"MYSQL_PORT": "MySQL端口,默认3306"
}
}
}
```
**状态码**
- `200` - 成功
- `404` - 模板不存在
**示例**
```bash
curl http://localhost:8000/templates/mysql_agent
```
---
## 状态监控
### 健康检查
检查服务是否正常运行。
**请求**
```http
GET /
```
**响应**
```json
{
"service": "Agent Manager API",
"version": "1.0.0",
"status": "running"
}
```
**示例**
```bash
curl http://localhost:8000/
```
---
## 多租户管理
### 按用户查询 Agents
使用 Kubernetes 标签选择器按用户 ID 查询 Agents。
**方法 1: 通过 kubectl**
```bash
# 查询特定用户的所有 Agents
kubectl get pods -n ai-agents -l user-id=alice
# 查看详细信息
kubectl get pods -n ai-agents -l user-id=alice \
-o custom-columns=NAME:.metadata.name,POD_ID:.metadata.uid,STATUS:.status.phase
```
**方法 2: 通过 API 查询后过滤**
```bash
curl http://localhost:8000/agents | \
jq '.agents[] | select(.labels["user-id"]=="alice")'
```
### 验证 Pod 归属
**通过 Pod ID 验证**
```bash
# 通过 Pod ID 查询
kubectl get pods -n ai-agents -o json | \
jq ".items[] | select(.metadata.uid==\"$POD_ID\")"
```
**通过 user-id 标签验证**
```bash
kubectl get pod <pod-name> -n ai-agents \
-o jsonpath='{.metadata.labels.user-id}'
```
---
## 错误码
### HTTP 状态码
| 状态码 | 说明 |
|--------|------|
| 200 | 请求成功 |
| 201 | 创建成功 |
| 400 | 请求参数错误 |
| 404 | 资源不存在 |
| 409 | 资源冲突(如 Agent 已存在) |
| 500 | 服务器内部错误 |
### 错误响应格式
```json
{
"detail": "错误详细信息"
}
```
---
## 使用示例
### Python SDK 示例
```python
import requests
class AgentManagerClient:
def __init__(self, base_url="http://localhost:8000"):
self.base_url = base_url
def create_agent(self, name, template, user_id, env=None):
"""创建 Agent"""
payload = {
"name": name,
"template": template,
"config": {"user_id": user_id},
"env": env or {}
}
response = requests.post(
f"{self.base_url}/agents",
json=payload
)
response.raise_for_status()
return response.json()
def get_agent_status(self, name):
"""获取 Agent 状态"""
response = requests.get(
f"{self.base_url}/agents/{name}/status"
)
response.raise_for_status()
return response.json()
def list_agents(self, template=None):
"""列出所有 Agents"""
params = {"template": template} if template else {}
response = requests.get(
f"{self.base_url}/agents",
params=params
)
response.raise_for_status()
return response.json()
def delete_agent(self, name):
"""删除 Agent"""
response = requests.delete(
f"{self.base_url}/agents/{name}"
)
response.raise_for_status()
return response.json()
def list_templates(self, type=None):
"""列出模板"""
if type == "platform":
url = f"{self.base_url}/templates/platform"
elif type == "custom":
url = f"{self.base_url}/templates/custom"
else:
url = f"{self.base_url}/templates"
response = requests.get(url)
response.raise_for_status()
return response.json()
# 使用示例
client = AgentManagerClient()
# 创建 Agent
result = client.create_agent(
name="alice-echo",
template="echo_agent",
user_id="alice"
)
print(f"✅ Agent 创建成功,Pod ID: {result['pod_id']}")
# 查询状态
status = client.get_agent_status("alice-echo")
print(f"Agent 状态: {status['status']}")
# 列出所有 Agents
agents = client.list_agents()
print(f"总共 {agents['count']} 个 Agents")
# 删除 Agent
client.delete_agent("alice-echo")
print("✅ Agent 删除成功")
```
### JavaScript/Node.js 示例
```javascript
const axios = require('axios');
class AgentManagerClient {
constructor(baseURL = 'http://localhost:8000') {
this.client = axios.create({ baseURL });
}
async createAgent(name, template, userId, env = {}) {
const response = await this.client.post('/agents', {
name,
template,
config: { user_id: userId },
env
});
return response.data;
}
async getAgentStatus(name) {
const response = await this.client.get(`/agents/${name}/status`);
return response.data;
}
async listAgents(template = null) {
const params = template ? { template } : {};
const response = await this.client.get('/agents', { params });
return response.data;
}
async deleteAgent(name) {
const response = await this.client.delete(`/agents/${name}`);
return response.data;
}
async listTemplates(type = null) {
let url = '/templates';
if (type === 'platform') url = '/templates/platform';
if (type === 'custom') url = '/templates/custom';
const response = await this.client.get(url);
return response.data;
}
}
// 使用示例
(async () => {
const client = new AgentManagerClient();
// 创建 Agent
const result = await client.createAgent('bob-chat', 'chat_agent', 'bob');
console.log(`✅ Agent 创建成功,Pod ID: ${result.pod_id}`);
// 查询状态
const status = await client.getAgentStatus('bob-chat');
console.log(`Agent 状态: ${status.status}`);
// 列出平台模板
const templates = await client.listTemplates('platform');
console.log(`平台模板: ${templates.count} 个`);
})();
```
---
## 附录
### A. 资源配置建议
| Agent 类型 | CPU Request | CPU Limit | Memory Request | Memory Limit |
|-----------|-------------|-----------|----------------|--------------|
| echo_agent | 100m | 500m | 128Mi | 512Mi |
| chat_agent | 200m | 1000m | 256Mi | 1Gi |
| code_agent | 500m | 2000m | 512Mi | 2Gi |
| search_agent | 200m | 1000m | 256Mi | 1Gi |
| mysql_agent | 100m | 500m | 128Mi | 512Mi |
| postgresql_agent | 100m | 500m | 128Mi | 512Mi |
| jina_search_agent | 500m | 2000m | 1Gi | 4Gi |
### B. 命名规范
- **Agent 名称**: 小写字母、数字、连字符,长度 1-63 字符
- **推荐格式**: `{user_id}-{type}` 或 `{user_id}-{type}-{number}`
- **示例**: `alice-echo`, `bob-chat-001`, `team-a-search`
### C. 标签说明
所有创建的 Agent 自动包含以下标签:
| 标签 | 说明 | 示例值 |
|------|------|--------|
| `app` | 应用类型 | `ai-agent` |
| `template` | 模板类型 | `echo_agent` |
| `managed-by` | 管理器标识 | `agent-manager` |
| `user-id` | 用户标识 | `alice`, `bob` |
---
## 更新日志
### v1.0.0 (2026-01-05)
- ✅ 实现 Agent 创建和管理
- ✅ 支持 7 种 Agent 模板
- ✅ 多租户支持(user-id 标签)
- ✅ Pod ID 返回和归属验证
- ✅ 模板分类查询(平台/自定义)
- ✅ 资源监控和状态查询
---
## 联系支持
如有问题或建议,请联系开发团队。
+23
View File
@@ -0,0 +1,23 @@
FROM python:3.11-slim
WORKDIR /app
# 复制应用代码
COPY requirements.txt .
COPY app.py .
COPY k8s_manager.py .
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt
# 设置环境变量
ENV PYTHONUNBUFFERED=1
ENV NAMESPACE=ai-agents
ENV SERVICE_PORT=8000
ENV SERVICE_HOST=0.0.0.0
# 暴露端口
EXPOSE 8000
# 运行应用
CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
+860
View File
@@ -0,0 +1,860 @@
# Agent Manager 服务需求文档
## 1. 概述
### 1.1 服务定位
Agent Manager 是一个独立的服务,负责 AKS/K8s 上所有 Agent 的部署、管理和查询操作。它是 Agent 生命周期管理的核心服务,不涉及权限验证、计费等业务逻辑。
### 1.2 系统架构
```mermaid
flowchart TB
subgraph Frontend[前端]
UI[用户界面]
end
subgraph MCPServer[MCP Server]
Auth[权限验证]
Billing[计费管理]
Quota[配额管理]
AgentAPI[Agent API]
end
subgraph AgentManager[Agent Manager]
TemplateManager[模板管理]
PodManager[Pod 管理]
ResourceManager[资源管理]
HealthChecker[健康检查]
end
subgraph AKS[Azure Kubernetes Service]
subgraph AgentNS[Agent 命名空间 - 统一]
PlatformPods[平台 Agent Pods]
CustomPods[自定义 Agent Pods]
end
end
subgraph ACR[Azure Container Registry]
PlatformImages[平台 Agent 镜像仓库]
CustomImages[自定义 Agent 镜像仓库]
end
UI --> MCPServer
MCPServer --> AgentManager
AgentManager --> AKS
AgentManager --> ACR
```
### 1.3 调用链路
```
前端 → MCP Server(权限验证、计费、配额检查)→ Agent Manager(K8s 部署操作)→ AKS
```
### 1.4 核心设计原则
1. **按需创建**:Agent Pod 在用户实际使用时才创建,不预先启动
2. **配额分配**:分配的是 Pod 数量配额,不是实际运行的 Pod
3. **镜像共享**:同一模板的镜像配置(CPU/内存)是平台级别固定的
4. **实例隔离**:每个用户使用时创建自己的 Pod 实例
---
## 2. Agent 类型定义
### 2.1 平台端 Agent (Platform Agent)
| 属性 | 说明 |
|------|------|
| **来源** | 平台管理员打镜像到 ACR 平台镜像仓库 |
| **部署方式** | K8s 部署,使用平台预设的镜像,**按需创建 Pod** |
| **资源配置** | 管理员固定设置每个 Pod 的 CPU/内存(平台级别统一) |
| **分配方式** | 管理员设置总 Pod 上限 → 分配 Pod 数量给渠道 → 渠道分配给租户 |
| **使用方式** | 用户只需传查询参数即可使用 |
| **Pod 创建时机** | 用户实际使用时才创建 Pod,不预先启动 |
| **弹性伸缩** | 用户可在分配的配额内启动多个 Pod |
### 2.2 自定义 Agent (Custom Agent)
| 属性 | 说明 |
|------|------|
| **来源** | 平台提供模板镜像到 ACR 自定义镜像仓库,用户配置自己的密钥和终结点 |
| **部署方式** | K8s 部署,使用模板镜像 + 用户环境变量,**按需创建 Pod** |
| **资源配置** | 用户在分配的资源总量(CPU/内存)内自由配置每个 Pod 的大小 |
| **分配方式** | 管理员 → 渠道(分配 CPU/内存总量)→ 租户 |
| **使用方式** | 需要传终结点、密钥、查询参数等 |
| **Pod 创建时机** | 用户创建 Agent 并配置完成后启动 Pod |
| **弹性伸缩** | 可设置预留 Pod 数和弹性 Pod 数(如固定 2 个 + 弹性 2 个) |
### 2.3 两种 Agent 的核心区别
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Agent 类型对比 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ 平台端 Agent │ │ 自定义 Agent │ │
│ ├─────────────────────────────────┤ ├─────────────────────────────────┤ │
│ │ 镜像: 平台预设,完整可用 │ │ 镜像: 模板,需要用户配置 │ │
│ │ 配置: 无需用户配置 │ │ 配置: 需要终结点、密钥等 │ │
│ │ 资源: 固定大小,限制 Pod 数量 │ │ 资源: 限制总量,自由分配 │ │
│ │ 弹性: 在配额内启动多个 Pod │ │ 弹性: 预留N个 + 弹性M个 │ │
│ │ 归属: 每个Pod属于一个用户 │ │ 归属: 每个Pod属于一个用户 │ │
│ │ 创建: 用户使用时按需创建 │ │ 创建: 配置完成后启动 │ │
│ └─────────────────────────────────┘ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### 2.4 资源分配流程
```mermaid
flowchart TB
subgraph Admin[管理员层]
A1[设置平台Agent模板]
A2[设置CPU/内存/最大Pod数]
A3[设置自定义Agent模板]
A4[设置自定义Agent资源池]
end
subgraph Channel[渠道层]
C1[获得平台Agent Pod配额]
C2[获得自定义Agent资源配额]
C3[分配给租户]
end
subgraph Tenant[租户层]
T1[获得平台Agent Pod配额]
T2[获得自定义Agent资源配额]
end
subgraph Usage[使用层]
U1[使用平台Agent - 按需创建Pod]
U2[创建自定义Agent - 配置后启动Pod]
end
A1 --> A2
A3 --> A4
A2 --> C1
A4 --> C2
C1 --> C3
C2 --> C3
C3 --> T1
C3 --> T2
T1 --> U1
T2 --> U2
```
### 2.5 ACR 镜像仓库规划
| 仓库 | 用途 | 示例路径 |
|------|------|----------|
| 平台 Agent 镜像仓库 | 存放平台预设的完整 Agent 镜像 | `your-acr.azurecr.io/platform-agents/` |
| 自定义 Agent 镜像仓库 | 存放需要用户配置的模板镜像 | `your-acr.azurecr.io/custom-agents/` |
### 2.6 K8s 命名空间规划
| 命名空间 | 用途 |
|----------|------|
| `ai-agents` | 统一的 Agent 命名空间,包含平台 Agent 和自定义 Agent 的所有 Pod |
---
## 3. 功能需求
### 3.1 模板管理
#### 3.1.1 平台 Agent 模板
| 功能 | 说明 |
|------|------|
| 注册模板 | 管理员注册新的平台 Agent 模板,包含镜像地址、默认资源配置等 |
| 更新模板 | 更新模板的镜像版本、资源配置等 |
| 删除模板 | 删除不再使用的模板 |
| 查询模板 | 获取模板列表和详情 |
**模板信息结构**:
```json
{
"name": "jina_search_agent",
"displayName": "Jina 搜索 Agent",
"description": "基于 Jina AI 的搜索 Agent",
"image": "your-acr.azurecr.io/platform-agents/jina-search:v1.0",
"category": "search",
"defaultConfig": {
"cpuRequest": "100m",
"cpuLimit": "500m",
"memoryRequest": "128Mi",
"memoryLimit": "512Mi",
"port": 8080
},
"healthCheck": {
"path": "/health",
"port": 8080,
"intervalSeconds": 30
},
"endpoints": {
"query": "/query",
"status": "/status"
}
}
```
#### 3.1.2 自定义 Agent 模板
| 功能 | 说明 |
|------|------|
| 注册模板 | 管理员注册自定义 Agent 模板,定义所需的环境变量 |
| 更新模板 | 更新模板配置 |
| 删除模板 | 删除模板 |
| 查询模板 | 获取模板列表和详情,包含所需环境变量定义 |
**模板信息结构**:
```json
{
"name": "openai_agent_template",
"displayName": "OpenAI Agent 模板",
"description": "需要配置 OpenAI API 密钥的 Agent 模板",
"image": "your-acr.azurecr.io/custom-agents/openai-template:v1.0",
"category": "llm",
"requiredEnvVars": [
{
"name": "OPENAI_API_KEY",
"displayName": "OpenAI API 密钥",
"description": "您的 OpenAI API 密钥",
"required": true,
"sensitive": true
},
{
"name": "OPENAI_API_BASE",
"displayName": "API 终结点",
"description": "OpenAI API 终结点地址",
"required": true,
"default": "https://api.openai.com/v1"
},
{
"name": "MODEL_NAME",
"displayName": "模型名称",
"description": "使用的模型名称",
"required": false,
"default": "gpt-4"
}
],
"defaultConfig": {
"cpuRequest": "100m",
"cpuLimit": "500m",
"memoryRequest": "128Mi",
"memoryLimit": "512Mi",
"port": 8080
}
}
```
### 3.2 平台 Agent 管理
#### 3.2.1 创建平台 Agent
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | 是 | Agent 名称,K8s 资源命名规范 |
| template | string | 是 | 模板名称 |
| namespace | string | 否 | K8s 命名空间,默认 platform-agents |
| replicas | int | 否 | 副本数,默认 1 |
| maxReplicas | int | 否 | 最大副本数,用于弹性伸缩 |
| resourceConfig | object | 否 | 资源配置,覆盖模板默认值 |
**响应**:
```json
{
"success": true,
"data": {
"name": "jina-search-agent-001",
"namespace": "platform-agents",
"template": "jina_search_agent",
"status": "Pending",
"replicas": 1,
"maxReplicas": 5,
"resourceConfig": {
"cpuRequest": "100m",
"cpuLimit": "500m",
"memoryRequest": "128Mi",
"memoryLimit": "512Mi"
},
"createdAt": "2026-01-04T12:00:00Z"
}
}
```
#### 3.2.2 扩缩容平台 Agent
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | 是 | Agent 名称 |
| replicas | int | 是 | 目标副本数 |
#### 3.2.3 删除平台 Agent
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | 是 | Agent 名称 |
#### 3.2.4 查询平台 Agent
- 获取单个 Agent 状态
- 获取 Agent 列表(支持分页、筛选)
- 获取 Agent 资源使用情况
- 获取 Agent 日志
### 3.3 自定义 Agent 管理
#### 3.3.1 创建自定义 Agent
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | 是 | Agent 名称 |
| template | string | 是 | 模板名称 |
| namespace | string | 否 | K8s 命名空间,默认 custom-agents |
| ownerId | string | 是 | 所属用户 ID |
| envVars | object | 是 | 环境变量配置(终结点、密钥等) |
| resourceConfig | object | 是 | 资源配置 |
| scalingConfig | object | 否 | 弹性伸缩配置 |
**资源配置结构**:
```json
{
"cpuRequest": "200m",
"cpuLimit": "1000m",
"memoryRequest": "256Mi",
"memoryLimit": "1Gi"
}
```
**弹性伸缩配置结构**:
```json
{
"minReplicas": 2,
"maxReplicas": 4,
"targetCPUUtilization": 80
}
```
**响应**:
```json
{
"success": true,
"data": {
"name": "my-openai-agent-001",
"namespace": "custom-agents",
"template": "openai_agent_template",
"ownerId": "user-uuid-123",
"status": "Pending",
"resourceConfig": {
"cpuRequest": "200m",
"cpuLimit": "1000m",
"memoryRequest": "256Mi",
"memoryLimit": "1Gi"
},
"scalingConfig": {
"minReplicas": 2,
"maxReplicas": 4
},
"createdAt": "2026-01-04T12:00:00Z"
}
}
```
#### 3.3.2 更新自定义 Agent 配置
**可更新内容**:
- 环境变量(终结点、密钥等)
- 资源配置(需要重启 Pod)
- 弹性伸缩配置
#### 3.3.3 扩缩容自定义 Agent
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | 是 | Agent 名称 |
| replicas | int | 是 | 目标副本数 |
#### 3.3.4 删除自定义 Agent
#### 3.3.5 查询自定义 Agent
- 获取单个 Agent 状态
- 获取 Agent 列表(支持按 ownerId 筛选)
- 获取 Agent 资源使用情况
- 获取 Agent 日志
### 3.4 资源统计
#### 3.4.1 平台 Agent 资源统计
```json
{
"totalPods": 15,
"runningPods": 12,
"pendingPods": 2,
"failedPods": 1,
"byTemplate": {
"jina_search_agent": {
"totalPods": 5,
"runningPods": 5
},
"mysql_agent": {
"totalPods": 10,
"runningPods": 7
}
}
}
```
#### 3.4.2 自定义 Agent 资源统计
```json
{
"totalPods": 20,
"totalCpuRequested": "4000m",
"totalMemoryRequested": "8Gi",
"byOwner": {
"user-uuid-123": {
"pods": 3,
"cpuRequested": "600m",
"memoryRequested": "1.5Gi"
}
}
}
```
### 3.5 健康检查
| 功能 | 说明 |
|------|------|
| Pod 健康检查 | 定期检查 Pod 的健康状态 |
| 服务健康检查 | 检查 Agent 服务的可用性 |
| 自动恢复 | 检测到不健康的 Pod 时触发重启 |
---
## 4. API 设计
### 4.1 模板管理 API
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /templates | 获取所有模板列表 |
| GET | /templates/platform | 获取平台 Agent 模板列表 |
| GET | /templates/custom | 获取自定义 Agent 模板列表 |
| GET | /templates/{name} | 获取模板详情 |
| POST | /templates | 注册新模板 |
| PUT | /templates/{name} | 更新模板 |
| DELETE | /templates/{name} | 删除模板 |
### 4.2 平台 Agent API
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /platform-agents | 获取平台 Agent 列表 |
| GET | /platform-agents/{name} | 获取平台 Agent 详情 |
| GET | /platform-agents/{name}/status | 获取 Agent 状态 |
| GET | /platform-agents/{name}/metrics | 获取资源使用情况 |
| GET | /platform-agents/{name}/logs | 获取 Agent 日志 |
| POST | /platform-agents | 创建平台 Agent |
| PUT | /platform-agents/{name}/scale | 扩缩容 |
| PUT | /platform-agents/{name}/config | 更新配置 |
| DELETE | /platform-agents/{name} | 删除 Agent |
| POST | /platform-agents/{name}/restart | 重启 Agent |
### 4.3 自定义 Agent API
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /custom-agents | 获取自定义 Agent 列表 |
| GET | /custom-agents/{name} | 获取自定义 Agent 详情 |
| GET | /custom-agents/{name}/status | 获取 Agent 状态 |
| GET | /custom-agents/{name}/metrics | 获取资源使用情况 |
| GET | /custom-agents/{name}/logs | 获取 Agent 日志 |
| POST | /custom-agents | 创建自定义 Agent |
| PUT | /custom-agents/{name}/scale | 扩缩容 |
| PUT | /custom-agents/{name}/config | 更新配置 |
| PUT | /custom-agents/{name}/env | 更新环境变量 |
| DELETE | /custom-agents/{name} | 删除 Agent |
| POST | /custom-agents/{name}/restart | 重启 Agent |
### 4.4 统计 API
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /stats/overview | 获取整体统计 |
| GET | /stats/platform-agents | 获取平台 Agent 统计 |
| GET | /stats/custom-agents | 获取自定义 Agent 统计 |
| GET | /stats/resources | 获取资源使用统计 |
### 4.5 健康检查 API
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /health | 服务健康检查 |
| GET | /ready | 服务就绪检查 |
---
## 5. 数据模型
### 5.1 Template 模板
```python
class Template:
name: str # 模板名称,唯一标识
display_name: str # 显示名称
description: str # 描述
type: str # 类型:platform / custom
image: str # 镜像地址
category: str # 分类:search, llm, database 等
default_config: dict # 默认资源配置
required_env_vars: list # 所需环境变量定义(自定义 Agent)
health_check: dict # 健康检查配置
endpoints: dict # 端点定义
created_at: datetime
updated_at: datetime
```
### 5.2 PlatformAgent 平台 Agent
```python
class PlatformAgent:
name: str # Agent 名称
namespace: str # K8s 命名空间
template: str # 使用的模板
status: str # 状态:Pending, Running, Failed 等
replicas: int # 当前副本数
max_replicas: int # 最大副本数
resource_config: dict # 资源配置
pod_ips: list # Pod IP 列表
service_name: str # Service 名称
service_port: int # Service 端口
access_url: str # 访问 URL
created_at: datetime
updated_at: datetime
```
### 5.3 CustomAgent 自定义 Agent
```python
class CustomAgent:
name: str # Agent 名称
namespace: str # K8s 命名空间
template: str # 使用的模板
owner_id: str # 所属用户 ID
status: str # 状态
env_vars: dict # 环境变量(加密存储)
resource_config: dict # 资源配置
scaling_config: dict # 弹性伸缩配置
min_replicas: int # 最小副本数(预留)
max_replicas: int # 最大副本数(弹性)
current_replicas: int # 当前副本数
pod_ips: list # Pod IP 列表
service_name: str # Service 名称
service_port: int # Service 端口
access_url: str # 访问 URL
created_at: datetime
updated_at: datetime
```
---
## 6. 资源限制逻辑
### 6.1 平台 Agent 资源限制
#### 6.1.1 分配流程
```mermaid
flowchart LR
A[管理员] -->|设置模板| B[平台Agent模板]
B -->|固定配置| C[CPU/内存/最大Pod数]
A -->|分配Pod配额| D[渠道]
D -->|分配Pod配额| E[租户]
E -->|使用时创建| F[Pod实例]
```
#### 6.1.2 配额检查流程
```mermaid
flowchart TD
A[用户请求使用平台Agent] --> B[MCP Server 权限验证]
B --> C{检查用户Pod配额}
C -->|配额充足| D[调用 Agent Manager]
C -->|配额不足| E[拒绝请求]
D --> F[创建Pod实例]
F --> G[更新已使用Pod数]
```
**限制规则**:
- 每个 Pod 的资源配置(CPU/内存)由管理员在模板级别固定
- 管理员设置该模板的最大 Pod 总数
- 分配给渠道时,分配的是 Pod 数量配额
- 渠道分配给租户时,分配的也是 Pod 数量配额
- 用户使用时才真正创建 Pod,按需启动
- 用户可在配额内启动多个 Pod 实例
**配额分配示例**:
```
平台 Agent: jina_search_agent
├── 模板配置: CPU=500m, Memory=512Mi, 最大Pod数=100
│
├── 渠道A 配额: 30 个 Pod
│ ├── 租户A1: 10 个 Pod 配额
│ ├── 租户A2: 15 个 Pod 配额
│ └── 租户A3: 5 个 Pod 配额
│
└── 渠道B 配额: 20 个 Pod
├── 租户B1: 12 个 Pod 配额
└── 租户B2: 8 个 Pod 配额
```
### 6.2 自定义 Agent 资源限制
#### 6.2.1 分配流程
```mermaid
flowchart LR
A[管理员] -->|设置模板| B[自定义Agent模板]
A -->|分配资源配额| C[渠道]
C -->|分配资源配额| D[租户]
D -->|在配额内创建| E[自定义Agent]
E -->|启动| F[Pod实例]
```
#### 6.2.2 配额检查流程
```mermaid
flowchart TD
A[用户创建自定义Agent] --> B[MCP Server 权限验证]
B --> C[计算请求资源总量]
C --> D{检查资源配额}
D -->|配额充足| E[调用 Agent Manager]
D -->|配额不足| F[拒绝请求]
E --> G[创建Pod实例]
G --> H[更新已使用资源]
```
**限制规则**:
- 分配给渠道/租户的是资源总量(CPU/内存)
- 用户在总量内自由配置每个 Pod 的资源大小
- 计算公式:`Σ(每个Pod的资源) ≤ 资源配额`
- 支持预留 Pod 数 + 弹性 Pod 数配置
**配额分配示例**:
```
自定义 Agent 资源池
│
├── 渠道A 配额: 8 CPU, 16GB 内存
│ ├── 租户A1: 4 CPU, 8GB 内存
│ │ └── 可创建: 4个(1CPU,2GB) 或 2个(2CPU,4GB) 或混合
│ └── 租户A2: 4 CPU, 8GB 内存
│
└── 渠道B 配额: 4 CPU, 8GB 内存
└── 租户B1: 4 CPU, 8GB 内存
└── 配置: 预留2个Pod + 弹性2个Pod
```
### 6.3 弹性伸缩配置
#### 6.3.1 平台 Agent 弹性配置
| 参数 | 说明 | 示例 |
|------|------|------|
| minReplicas | 最小 Pod 数(预留) | 1 |
| maxReplicas | 最大 Pod 数(配额上限) | 5 |
**说明**:用户在 `minReplicas` 到 `maxReplicas` 范围内按需创建 Pod
#### 6.3.2 自定义 Agent 弹性配置
| 参数 | 说明 | 示例 |
|------|------|------|
| minReplicas | 预留 Pod 数(始终运行) | 2 |
| maxReplicas | 最大 Pod 数(弹性上限) | 4 |
| targetCPUUtilization | CPU 使用率阈值 | 80% |
**说明**:
- `minReplicas` 个 Pod 始终运行(预留)
- 根据负载自动扩展到 `maxReplicas`
- 总资源消耗不能超过用户配额
---
## 7. 安全考虑
### 7.1 敏感信息处理
- 自定义 Agent 的环境变量(密钥、终结点等)需要加密存储
- 使用 K8s Secret 存储敏感信息
- API 响应中不返回敏感信息明文
- 日志中脱敏处理敏感字段
### 7.2 命名空间隔离
- 所有 Agent Pod 统一部署在 `ai-agents` 命名空间
- 通过 Label 区分平台 Agent 和自定义 Agent
- 通过 Label 标记 Pod 所属的用户/渠道
### 7.3 网络策略
- 配置 NetworkPolicy 限制 Pod 间通信
- 自定义 Agent 的 Pod 之间相互隔离
- 只允许 Agent Manager 和 MCP Server 访问 Agent Pod
---
## 8. 与 MCP Server 的集成
### 8.1 调用关系
```mermaid
sequenceDiagram
participant FE as 前端
participant MCP as MCP Server
participant AM as Agent Manager
participant K8s as Kubernetes
FE->>MCP: 创建 Agent 请求
MCP->>MCP: 权限验证
MCP->>MCP: 配额检查
MCP->>AM: 调用创建 API
AM->>K8s: 创建 Pod/Deployment
K8s-->>AM: 返回结果
AM-->>MCP: 返回创建结果
MCP->>MCP: 记录计费信息
MCP-->>FE: 返回结果
```
### 8.2 MCP Server 职责
| 职责 | 说明 |
|------|------|
| 权限验证 | 验证用户是否有权限操作 Agent |
| 配额检查 | 检查用户的资源配额是否足够 |
| 计费管理 | 记录 Agent 使用情况,计算费用 |
| 分配管理 | 管理 Agent 的分配关系(管理员→渠道→租户) |
### 8.3 Agent Manager 职责
| 职责 | 说明 |
|------|------|
| K8s 操作 | 创建、删除、更新 K8s 资源 |
| 状态查询 | 查询 Pod 状态、资源使用情况 |
| 健康检查 | 监控 Agent 健康状态 |
| 日志获取 | 获取 Pod 日志 |
---
## 9. 部署架构
### 9.1 服务部署
```yaml
# Agent Manager 部署配置示例
apiVersion: apps/v1
kind: Deployment
metadata:
name: agent-manager
namespace: taiji-system
spec:
replicas: 2
selector:
matchLabels:
app: agent-manager
template:
spec:
containers:
- name: agent-manager
image: your-acr.azurecr.io/agent-manager:latest
env:
- name: KUBERNETES_NAMESPACE
value: "ai-agents"
- name: ACR_PLATFORM_REGISTRY
value: "your-acr.azurecr.io/platform-agents"
- name: ACR_CUSTOM_REGISTRY
value: "your-acr.azurecr.io/custom-agents"
```
### 9.2 命名空间规划
| 命名空间 | 用途 |
|----------|------|
| taiji-system | 系统服务(MCP Server, Agent Manager 等) |
| ai-agents | 所有 Agent Pods(平台 Agent + 自定义 Agent) |
### 9.3 ACR 镜像仓库规划
| 仓库路径 | 用途 |
|----------|------|
| `your-acr.azurecr.io/platform-agents/` | 平台 Agent 镜像(完整可用) |
| `your-acr.azurecr.io/custom-agents/` | 自定义 Agent 模板镜像(需要用户配置) |
### 9.4 Pod Label 规划
```yaml
# 平台 Agent Pod Labels
labels:
app: agent
agent-type: platform
template: jina_search_agent
owner-id: user-uuid-123
channel-id: channel-uuid-456
# 自定义 Agent Pod Labels
labels:
app: agent
agent-type: custom
template: openai_agent_template
owner-id: user-uuid-123
channel-id: channel-uuid-456
```
---
## 10. 待确认事项
1. **镜像仓库**:是否使用 Azure Container Registry (ACR)?需要确认仓库地址和认证方式。
2. **弹性伸缩**:是否需要集成 Kubernetes HPA (Horizontal Pod Autoscaler)?
3. **日志收集**:是否需要集成日志收集系统(如 Azure Monitor, ELK 等)?
4. **监控告警**:是否需要集成 Prometheus/Grafana 进行监控?
5. **备份恢复**:Agent 配置是否需要备份?
6. **Pod 命名规则**:建议格式 `{template}-{owner-id-short}-{random}`,如 `jina-search-a1b2c3-xyz123`
---
## 11. 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| v1.0 | 2026-01-04 | 初始版本 |
| v1.1 | 2026-01-04 | 更新资源限制逻辑,明确按需创建和配额分配机制 |
BIN
View File
Binary file not shown.
+69
View File
@@ -0,0 +1,69 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ name }}
namespace: {{ namespace }}
labels:
app: {{ name }}
type: basic-agent
managed-by: agent-manager
user-id: {{ user_id }}
spec:
replicas: {{ replicas | default(1) }}
selector:
matchLabels:
app: {{ name }}
template:
metadata:
labels:
app: {{ name }}
type: basic-agent
managed-by: agent-manager
user-id: {{ user_id }}
spec:
imagePullSecrets:
- name: acr-secret
containers:
- name: agent
image: {{ image }}
ports:
- containerPort: 8000
name: http
env:
- name: AGENT_NAME
value: "{{ name }}"
- name: USER_ID
value: "{{ user_id }}"
{% if env_vars %}
{% for key, value in env_vars.items() %}
- name: {{ key }}
value: "{{ value }}"
{% endfor %}
{% endif %}
resources:
requests:
cpu: {{ resources.requests.cpu | default("100m") }}
memory: {{ resources.requests.memory | default("256Mi") }}
limits:
cpu: {{ resources.limits.cpu | default("1000m") }}
memory: {{ resources.limits.memory | default("1Gi") }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ name }}
namespace: {{ namespace }}
labels:
app: {{ name }}
type: basic-agent
managed-by: agent-manager
user-id: {{ user_id }}
spec:
selector:
app: {{ name }}
ports:
- port: 8000
targetPort: 8000
protocol: TCP
name: http
type: ClusterIP
+75
View File
@@ -0,0 +1,75 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ name }}
namespace: {{ namespace }}
labels:
app: {{ name }}
type: echo-agent
managed-by: agent-manager
user-id: {{ user_id }}
spec:
replicas: {{ replicas | default(1) }}
selector:
matchLabels:
app: {{ name }}
template:
metadata:
labels:
app: {{ name }}
type: echo-agent
managed-by: agent-manager
user-id: {{ user_id }}
spec:
imagePullSecrets:
- name: acr-secret
containers:
- name: echo-agent
image: {{ image }}
ports:
- containerPort: 8000
name: http
env:
- name: AGENT_NAME
value: "{{ name }}"
- name: USER_ID
value: "{{ user_id }}"
resources:
requests:
cpu: {{ resources.requests.cpu | default("100m") }}
memory: {{ resources.requests.memory | default("128Mi") }}
limits:
cpu: {{ resources.limits.cpu | default("500m") }}
memory: {{ resources.limits.memory | default("512Mi") }}
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: {{ name }}
namespace: {{ namespace }}
labels:
app: {{ name }}
type: echo-agent
managed-by: agent-manager
user-id: {{ user_id }}
spec:
selector:
app: {{ name }}
ports:
- port: 8000
targetPort: 8000
protocol: TCP
name: http
type: ClusterIP
+71
View File
@@ -0,0 +1,71 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ name }}
namespace: {{ namespace }}
labels:
app: {{ name }}
type: mcp-agent
managed-by: agent-manager
user-id: {{ user_id }}
spec:
replicas: {{ replicas | default(1) }}
selector:
matchLabels:
app: {{ name }}
template:
metadata:
labels:
app: {{ name }}
type: mcp-agent
managed-by: agent-manager
user-id: {{ user_id }}
spec:
imagePullSecrets:
- name: acr-secret
containers:
- name: mcp-agent
image: {{ image }}
ports:
- containerPort: 8000
name: http
env:
- name: AGENT_NAME
value: "{{ name }}"
- name: USER_ID
value: "{{ user_id }}"
- name: MCP_SERVER_URL
value: {{ mcp_server_url | default("http://localhost:3000") }}
{% if env_vars %}
{% for key, value in env_vars.items() %}
- name: {{ key }}
value: "{{ value }}"
{% endfor %}
{% endif %}
resources:
requests:
cpu: {{ resources.requests.cpu | default("200m") }}
memory: {{ resources.requests.memory | default("512Mi") }}
limits:
cpu: {{ resources.limits.cpu | default("2000m") }}
memory: {{ resources.limits.memory | default("2Gi") }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ name }}
namespace: {{ namespace }}
labels:
app: {{ name }}
type: mcp-agent
managed-by: agent-manager
user-id: {{ user_id }}
spec:
selector:
app: {{ name }}
ports:
- port: 8000
targetPort: 8000
protocol: TCP
name: http
type: ClusterIP
+75
View File
@@ -0,0 +1,75 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ name }}
namespace: {{ namespace }}
labels:
app: {{ name }}
type: task-worker
managed-by: agent-manager
user-id: {{ user_id }}
spec:
replicas: {{ replicas | default(1) }}
selector:
matchLabels:
app: {{ name }}
template:
metadata:
labels:
app: {{ name }}
type: task-worker
managed-by: agent-manager
user-id: {{ user_id }}
spec:
imagePullSecrets:
- name: acr-secret
containers:
- name: task-worker
image: {{ image }}
ports:
- containerPort: 8000
name: http
env:
- name: AGENT_NAME
value: "{{ name }}"
- name: USER_ID
value: "{{ user_id }}"
resources:
requests:
cpu: {{ resources.requests.cpu | default("100m") }}
memory: {{ resources.requests.memory | default("256Mi") }}
limits:
cpu: {{ resources.limits.cpu | default("1000m") }}
memory: {{ resources.limits.memory | default("1Gi") }}
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: {{ name }}
namespace: {{ namespace }}
labels:
app: {{ name }}
type: task-worker
managed-by: agent-manager
user-id: {{ user_id }}
spec:
selector:
app: {{ name }}
ports:
- port: 8000
targetPort: 8000
protocol: TCP
name: http
type: ClusterIP
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
# 构建并推送AI Agent镜像到ACR
set -e # 遇到错误立即退出
# 配置变量
ACR_NAME="agnettaiji" # 你的ACR名称
ACR_LOGIN_SERVER="${ACR_NAME}.azurecr.io"
# 登录到ACR
echo "登录到Azure Container Registry..."
az acr login --name ${ACR_NAME}
echo "当前目录: $(pwd)"
echo ""
# 构建并推送MySQL Agent
echo "构建MySQL Agent镜像..."
docker build -f mysql_agent.Dockerfile -t ${ACR_LOGIN_SERVER}/ai-agents/mysql-agent:latest .
echo "推送MySQL Agent镜像..."
docker push ${ACR_LOGIN_SERVER}/ai-agents/mysql-agent:latest
# 构建并推送PostgreSQL Agent
echo "构建PostgreSQL Agent镜像..."
docker build -f postgresql_agent.Dockerfile -t ${ACR_LOGIN_SERVER}/ai-agents/postgresql-agent:latest .
echo "推送PostgreSQL Agent镜像..."
docker push ${ACR_LOGIN_SERVER}/ai-agents/postgresql-agent:latest
# 构建并推送Jina Search Agent
echo "构建Jina Search Agent镜像..."
docker build -f jina_search_agent.Dockerfile -t ${ACR_LOGIN_SERVER}/ai-agents/jina-search-agent:latest .
echo "推送Jina Search Agent镜像..."
docker push ${ACR_LOGIN_SERVER}/ai-agents/jina-search-agent:latest
echo "✅ 所有镜像构建并推送完成!"
echo ""
echo "已推送的镜像:"
echo " - ${ACR_LOGIN_SERVER}/ai-agents/mysql-agent:latest"
echo " - ${ACR_LOGIN_SERVER}/ai-agents/postgresql-agent:latest"
echo " - ${ACR_LOGIN_SERVER}/ai-agents/jina-search-agent:latest"
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# 构建并推送Jina Search Agent镜像到ACR
set -e
# 配置变量
ACR_NAME="agnettaiji"
ACR_LOGIN_SERVER="${ACR_NAME}.azurecr.io"
IMAGE_NAME="ai-agents/jina-search-agent"
IMAGE_TAG="latest"
echo "=========================================="
echo "Jina Search Agent Docker镜像构建脚本"
echo "=========================================="
# 检查是否在正确的目录
if [ ! -f "jina_search_agent.py" ]; then
echo "错误: 请在agent_templates目录下运行此脚本"
exit 1
fi
# 登录到ACR
echo ""
echo "步骤1: 登录到Azure Container Registry..."
az acr login --name ${ACR_NAME}
# 构建镜像
echo ""
echo "步骤2: 构建Docker镜像..."
docker build -f jina_search_agent.Dockerfile -t ${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG} .
# 推送镜像
echo ""
echo "步骤3: 推送镜像到ACR..."
docker push ${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}
echo ""
echo "=========================================="
echo "构建完成!"
echo "镜像: ${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}"
echo "=========================================="
echo ""
echo "使用示例:"
echo "curl -X POST 'http://localhost:8000/agents' \\"
echo " -H 'Content-Type: application/json' \\"
echo " -d '{"
echo " \"name\": \"my-jina-agent\","
echo " \"template\": \"jina_search_agent\","
echo " \"config\": {"
echo " \"env\": {"
echo " \"JINA_API_KEY\": \"your-jina-api-key\""
echo " }"
echo " }"
echo " }'"
@@ -0,0 +1,24 @@
FROM python:3.11-slim
WORKDIR /app
# 安装Python依赖
RUN pip install --no-cache-dir \
fastapi==0.109.0 \
uvicorn==0.27.0 \
requests==2.31.0 \
pydantic==2.5.3
# 复制agent代码
COPY jina_search_agent.py .
# 设置环境变量
ENV PYTHONUNBUFFERED=1
ENV SERVICE_HOST=0.0.0.0
ENV SERVICE_PORT=8080
# 暴露端口
EXPOSE 8080
# 运行agent
CMD ["python", "jina_search_agent.py"]
+230
View File
@@ -0,0 +1,230 @@
"""
Jina Search Agent - 使用Jina Reader API获取网站内容的HTTP服务
需要设置环境变量: JINA_API_KEY
"""
import os
import time
import logging
import requests
from typing import Optional
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
import uvicorn
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 环境变量配置
POD_NAME = os.getenv("POD_NAME", "unknown")
TEMPLATE_TYPE = os.getenv("TEMPLATE_TYPE", "jina_search_agent")
JINA_API_KEY = os.getenv("JINA_API_KEY", "")
SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0")
SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080"))
# Jina Reader API基础URL
JINA_BASE_URL = "https://r.jina.ai"
# 创建FastAPI应用
app = FastAPI(
title="Jina Search Agent",
description="使用Jina Reader API获取网站内容的AI Agent",
version="1.0.0"
)
class SearchRequest(BaseModel):
"""搜索请求模型"""
url: str = Field(..., description="要搜索的网站URL")
timeout: int = Field(default=30, description="请求超时时间(秒)")
class SearchResponse(BaseModel):
"""搜索响应模型"""
url: str
content: str
status_code: int
content_type: Optional[str] = None
success: bool
class HealthResponse(BaseModel):
"""健康检查响应"""
status: str
pod_name: str
template_type: str
jina_api_configured: bool
@app.get("/health", response_model=HealthResponse)
async def health_check():
"""
健康检查端点
Returns:
服务状态信息
"""
return HealthResponse(
status="healthy",
pod_name=POD_NAME,
template_type=TEMPLATE_TYPE,
jina_api_configured=bool(JINA_API_KEY)
)
@app.get("/")
async def root():
"""根路径 - 返回服务信息和所需参数"""
return {
"service": "Jina Search Agent",
"version": "1.0.0",
"description": "使用Jina Reader API获取网站内容的AI Agent",
"pod_name": POD_NAME,
"template_type": TEMPLATE_TYPE,
"required_env": {
"JINA_API_KEY": {
"description": "Jina API密钥,从 https://jina.ai/ 获取",
"required": True,
"configured": bool(JINA_API_KEY)
}
},
"optional_env": {
"SERVICE_PORT": {
"description": "HTTP服务端口",
"default": "8080"
},
"SERVICE_HOST": {
"description": "HTTP服务监听地址",
"default": "0.0.0.0"
}
},
"endpoints": {
"health": {
"method": "GET",
"path": "/health",
"description": "健康检查"
},
"search": {
"method": "POST",
"path": "/search",
"description": "搜索网站内容",
"body": {
"url": "要搜索的网站URL (必填)",
"timeout": "请求超时时间,默认30秒 (可选)"
}
},
"fetch": {
"method": "GET",
"path": "/fetch",
"description": "快速获取网站内容",
"params": {
"url": "要获取的网站URL (必填)",
"timeout": "请求超时时间,默认30秒 (可选)"
}
}
},
"example_usage": {
"search": 'curl -X POST "http://<pod-ip>:8080/search" -H "Content-Type: application/json" -d \'{"url": "https://www.example.com"}\'',
"fetch": 'curl "http://<pod-ip>:8080/fetch?url=https://www.example.com"'
}
}
@app.post("/search", response_model=SearchResponse)
async def search(request: SearchRequest):
"""
搜索网站内容
使用Jina Reader API获取指定URL的网站内容
Args:
request: 包含URL和选项的搜索请求
Returns:
网站内容和元数据
"""
if not JINA_API_KEY:
raise HTTPException(
status_code=500,
detail="JINA_API_KEY未配置,请设置环境变量"
)
logger.info(f"[{POD_NAME}] 搜索请求: {request.url}")
try:
# 构建Jina Reader API请求
jina_url = f"{JINA_BASE_URL}/{request.url}"
headers = {
"Authorization": f"Bearer {JINA_API_KEY}"
}
# 发送请求
response = requests.get(
jina_url,
headers=headers,
timeout=request.timeout
)
logger.info(f"[{POD_NAME}] Jina API响应状态: {response.status_code}")
return SearchResponse(
url=request.url,
content=response.text,
status_code=response.status_code,
content_type=response.headers.get("Content-Type"),
success=response.status_code == 200
)
except requests.exceptions.Timeout:
logger.error(f"[{POD_NAME}] 请求超时: {request.url}")
raise HTTPException(
status_code=504,
detail=f"请求超时({request.timeout}秒)"
)
except requests.exceptions.RequestException as e:
logger.error(f"[{POD_NAME}] 请求失败: {str(e)}")
raise HTTPException(
status_code=502,
detail=f"请求失败: {str(e)}"
)
@app.get("/fetch", response_model=SearchResponse)
async def fetch(
url: str = Query(..., description="要获取的网站URL"),
timeout: int = Query(default=30, description="请求超时时间(秒)")
):
"""
快速获取网站内容(GET方式)
Args:
url: 要获取的网站URL
timeout: 请求超时时间
Returns:
网站内容和元数据
"""
request = SearchRequest(url=url, timeout=timeout)
return await search(request)
def main():
"""主函数 - 启动HTTP服务"""
logger.info(f"Jina Search Agent启动: {POD_NAME} (模板: {TEMPLATE_TYPE})")
logger.info(f"服务地址: {SERVICE_HOST}:{SERVICE_PORT}")
logger.info(f"JINA_API_KEY已配置: {bool(JINA_API_KEY)}")
if not JINA_API_KEY:
logger.warning("⚠️ JINA_API_KEY未设置,API调用将失败")
# 启动uvicorn服务
uvicorn.run(
app,
host=SERVICE_HOST,
port=SERVICE_PORT,
log_level="info"
)
if __name__ == "__main__":
main()
+28
View File
@@ -0,0 +1,28 @@
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
default-libmysqlclient-dev \
build-essential \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
# 安装Python依赖
RUN pip install --no-cache-dir \
langchain==0.1.0 \
langchain-community==0.0.10 \
langchain-openai==0.0.2 \
openai==1.7.2 \
pymysql==1.1.0 \
sqlalchemy==2.0.23
# 复制agent代码
COPY mysql_agent.py .
# 设置环境变量
ENV PYTHONUNBUFFERED=1
# 运行agent
CMD ["python", "mysql_agent.py"]
+129
View File
@@ -0,0 +1,129 @@
"""
MySQL AI Agent - 使用LangChain实现的MySQL数据库查询代理
需要设置环境变量: MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, OPENAI_API_KEY
"""
import os
import time
import logging
from langchain_community.utilities import SQLDatabase
from langchain.agents import create_sql_agent
from langchain.agents.agent_toolkits import SQLDatabaseToolkit
from langchain_openai import ChatOpenAI
from langchain.agents.agent_types import AgentType
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
POD_NAME = os.getenv("POD_NAME", "unknown")
TEMPLATE_TYPE = os.getenv("TEMPLATE_TYPE", "mysql_agent")
# MySQL数据库配置
MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost")
MYSQL_PORT = os.getenv("MYSQL_PORT", "3306")
MYSQL_USER = os.getenv("MYSQL_USER", "root")
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "test")
# OpenAI配置
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
def create_mysql_agent():
"""创建MySQL数据库Agent"""
# 构建数据库URI
db_uri = f"mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}"
try:
# 连接数据库
db = SQLDatabase.from_uri(db_uri)
logger.info(f"✅ 成功连接到MySQL数据库: {MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}")
# 显示可用的表
tables = db.get_usable_table_names()
logger.info(f"可用的表: {tables}")
except Exception as e:
logger.error(f"❌ 数据库连接失败: {str(e)}")
return None
# 初始化LLM
if not OPENAI_API_KEY:
logger.error("❌ 未设置OPENAI_API_KEY")
return None
llm = ChatOpenAI(
temperature=0,
model="gpt-3.5-turbo",
openai_api_key=OPENAI_API_KEY
)
# 创建SQL工具包
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
# 创建SQL Agent
agent_executor = create_sql_agent(
llm=llm,
toolkit=toolkit,
agent_type=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
handle_parsing_errors=True,
max_iterations=5
)
return agent_executor
def main():
"""主函数 - MySQL Agent主循环"""
logger.info(f"MySQL Agent启动: {POD_NAME} (模板: {TEMPLATE_TYPE})")
logger.info(f"数据库配置: {MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}")
# 创建Agent
agent = create_mysql_agent()
if agent is None:
logger.error("Agent创建失败,请检查配置")
# 保持容器运行
while True:
logger.info(f"[{POD_NAME}] 等待正确的配置...")
time.sleep(30)
return
logger.info("✅ MySQL Agent创建成功,开始运行...")
# 示例查询列表
sample_queries = [
"列出数据库中所有的表",
"描述第一个表的结构",
"统计每个表的记录数",
"显示最近的5条记录",
]
query_index = 0
while True:
try:
# 每2分钟执行一次示例查询
query = sample_queries[query_index % len(sample_queries)]
logger.info(f"\n{'='*60}")
logger.info(f"📊 执行查询: {query}")
logger.info(f"{'='*60}\n")
# 执行Agent
result = agent.invoke({"input": query})
logger.info(f"\n✅ 结果:\n{result['output']}\n")
query_index += 1
except Exception as e:
logger.error(f"❌ 查询执行失败: {str(e)}")
# 等待120秒后执行下一个查询
logger.info(f"[{POD_NAME}] 等待下一次查询...")
time.sleep(120)
if __name__ == "__main__":
main()
@@ -0,0 +1,27 @@
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
libpq-dev \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# 安装Python依赖
RUN pip install --no-cache-dir \
langchain==0.1.0 \
langchain-community==0.0.10 \
langchain-openai==0.0.2 \
openai==1.7.2 \
psycopg2-binary==2.9.9 \
sqlalchemy==2.0.23
# 复制agent代码
COPY postgresql_agent.py .
# 设置环境变量
ENV PYTHONUNBUFFERED=1
# 运行agent
CMD ["python", "postgresql_agent.py"]
+130
View File
@@ -0,0 +1,130 @@
"""
PostgreSQL AI Agent - 使用LangChain实现的PostgreSQL数据库查询代理
需要设置环境变量: POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DATABASE, OPENAI_API_KEY
"""
import os
import time
import logging
from langchain_community.utilities import SQLDatabase
from langchain.agents import create_sql_agent
from langchain.agents.agent_toolkits import SQLDatabaseToolkit
from langchain_openai import ChatOpenAI
from langchain.agents.agent_types import AgentType
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
POD_NAME = os.getenv("POD_NAME", "unknown")
TEMPLATE_TYPE = os.getenv("TEMPLATE_TYPE", "postgresql_agent")
# PostgreSQL数据库配置
POSTGRES_HOST = os.getenv("POSTGRES_HOST", "localhost")
POSTGRES_PORT = os.getenv("POSTGRES_PORT", "5432")
POSTGRES_USER = os.getenv("POSTGRES_USER", "postgres")
POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD", "")
POSTGRES_DATABASE = os.getenv("POSTGRES_DATABASE", "postgres")
# OpenAI配置
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
def create_postgresql_agent():
"""创建PostgreSQL数据库Agent"""
# 构建数据库URI
db_uri = f"postgresql+psycopg2://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DATABASE}"
try:
# 连接数据库
db = SQLDatabase.from_uri(db_uri)
logger.info(f"✅ 成功连接到PostgreSQL数据库: {POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DATABASE}")
# 显示可用的表
tables = db.get_usable_table_names()
logger.info(f"可用的表: {tables}")
except Exception as e:
logger.error(f"❌ 数据库连接失败: {str(e)}")
return None
# 初始化LLM
if not OPENAI_API_KEY:
logger.error("❌ 未设置OPENAI_API_KEY")
return None
llm = ChatOpenAI(
temperature=0,
model="gpt-3.5-turbo",
openai_api_key=OPENAI_API_KEY
)
# 创建SQL工具包
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
# 创建SQL Agent
agent_executor = create_sql_agent(
llm=llm,
toolkit=toolkit,
agent_type=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
handle_parsing_errors=True,
max_iterations=5
)
return agent_executor
def main():
"""主函数 - PostgreSQL Agent主循环"""
logger.info(f"PostgreSQL Agent启动: {POD_NAME} (模板: {TEMPLATE_TYPE})")
logger.info(f"数据库配置: {POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DATABASE}")
# 创建Agent
agent = create_postgresql_agent()
if agent is None:
logger.error("Agent创建失败,请检查配置")
# 保持容器运行
while True:
logger.info(f"[{POD_NAME}] 等待正确的配置...")
time.sleep(30)
return
logger.info("✅ PostgreSQL Agent创建成功,开始运行...")
# 示例查询列表
sample_queries = [
"列出数据库中所有的表和视图",
"描述每个表的结构和主键",
"统计每个表的记录数",
"查询数据库的版本信息",
"显示最大的3个表",
]
query_index = 0
while True:
try:
# 每2分钟执行一次示例查询
query = sample_queries[query_index % len(sample_queries)]
logger.info(f"\n{'='*60}")
logger.info(f"🐘 执行查询: {query}")
logger.info(f"{'='*60}\n")
# 执行Agent
result = agent.invoke({"input": query})
logger.info(f"\n✅ 结果:\n{result['output']}\n")
query_index += 1
except Exception as e:
logger.error(f"❌ 查询执行失败: {str(e)}")
# 等待120秒后执行下一个查询
logger.info(f"[{POD_NAME}] 等待下一次查询...")
time.sleep(120)
if __name__ == "__main__":
main()
+516
View File
@@ -0,0 +1,516 @@
"""
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
template: Optional[str] = None
created_at: Optional[str] = None
node: Optional[str] = None
pod_ip: Optional[str] = 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
requests: Dict
limits: Dict
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)
+1033
View File
File diff suppressed because it is too large Load Diff
+239
View File
@@ -0,0 +1,239 @@
"""
Database models and session management for Agent Manager.
Supports both SQLite (development) and PostgreSQL (production).
"""
from datetime import datetime
from typing import Optional, Dict, Any
from sqlalchemy import (
create_engine, Column, Integer, String, DateTime,
Boolean, JSON, Float, ForeignKey, Text, Enum as SQLEnum
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship, Session
import enum
import os
# Database URL from environment or default to SQLite
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./agent_manager.db")
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class AgentType(str, enum.Enum):
"""Agent type enumeration"""
PLATFORM = "platform"
CUSTOM = "custom"
class AgentStatus(str, enum.Enum):
"""Agent status enumeration"""
PENDING = "pending"
RUNNING = "running"
STOPPED = "stopped"
FAILED = "failed"
SCALING = "scaling"
class Template(Base):
"""Template model for agent templates"""
__tablename__ = "templates"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), unique=True, nullable=False, index=True)
display_name = Column(String(200), nullable=False)
description = Column(Text)
agent_type = Column(SQLEnum(AgentType), nullable=False, index=True)
# Image configuration
image = Column(String(500), nullable=False)
port = Column(Integer, nullable=True)
# Environment variable requirements (JSON format)
# {"required": {"KEY": "description"}, "optional": {"KEY": "description"}}
env_requirements = Column(JSON, default={})
# Resource configuration (for platform agents, fixed by admin)
cpu_request = Column(String(20)) # e.g., "100m"
cpu_limit = Column(String(20)) # e.g., "500m"
memory_request = Column(String(20)) # e.g., "128Mi"
memory_limit = Column(String(20)) # e.g., "512Mi"
# Scaling configuration defaults
min_replicas = Column(Integer, default=1)
max_replicas = Column(Integer, default=3)
target_cpu_utilization = Column(Integer, default=80)
# Metadata
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_by = Column(String(100))
# Relationships
agents = relationship("Agent", back_populates="template")
class Agent(Base):
"""Agent instance model"""
__tablename__ = "agents"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), unique=True, nullable=False, index=True)
display_name = Column(String(200))
# Template reference
template_id = Column(Integer, ForeignKey("templates.id"), nullable=False)
template = relationship("Template", back_populates="agents")
# Ownership and organization
owner_id = Column(String(100), nullable=False, index=True)
channel_id = Column(String(100), index=True)
tenant_id = Column(String(100), index=True)
# Agent configuration
agent_type = Column(SQLEnum(AgentType), nullable=False, index=True)
status = Column(SQLEnum(AgentStatus), default=AgentStatus.PENDING, index=True)
# Environment variables (encrypted in production)
environment_vars = Column(JSON, default={})
# Resource configuration (for custom agents)
cpu_request = Column(String(20))
cpu_limit = Column(String(20))
memory_request = Column(String(20))
memory_limit = Column(String(20))
# Scaling configuration
min_replicas = Column(Integer, default=1)
max_replicas = Column(Integer, default=3)
target_cpu_utilization = Column(Integer, default=80)
current_replicas = Column(Integer, default=0)
# Kubernetes resources
deployment_name = Column(String(100))
service_name = Column(String(100))
service_url = Column(String(500))
namespace = Column(String(100), default="ai-agents")
# Metadata
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
last_accessed_at = Column(DateTime, nullable=True)
# Relationships
metrics = relationship("AgentMetric", back_populates="agent", cascade="all, delete-orphan")
class Quota(Base):
"""Resource quota model"""
__tablename__ = "quotas"
id = Column(Integer, primary_key=True, index=True)
# Quota owner (hierarchical: admin -> channel -> tenant)
owner_type = Column(String(20), nullable=False) # "admin", "channel", "tenant"
owner_id = Column(String(100), nullable=False, index=True)
channel_id = Column(String(100), index=True)
# For platform agents: quota is Pod count
platform_pod_quota = Column(Integer, default=0)
platform_pod_used = Column(Integer, default=0)
# For custom agents: quota is CPU/Memory totals
custom_cpu_quota = Column(Float, default=0.0) # in cores
custom_cpu_used = Column(Float, default=0.0)
custom_memory_quota = Column(Float, default=0.0) # in GB
custom_memory_used = Column(Float, default=0.0)
# Metadata
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Unique constraint
__table_args__ = (
# Unique per owner
# UniqueConstraint('owner_type', 'owner_id', name='uq_quota_owner'),
)
class AgentMetric(Base):
"""Agent metrics tracking"""
__tablename__ = "agent_metrics"
id = Column(Integer, primary_key=True, index=True)
agent_id = Column(Integer, ForeignKey("agents.id"), nullable=False, index=True)
agent = relationship("Agent", back_populates="metrics")
# Timestamp
timestamp = Column(DateTime, default=datetime.utcnow, index=True)
# Resource metrics
cpu_usage = Column(Float) # in cores
memory_usage = Column(Float) # in MB
network_rx_bytes = Column(Integer, default=0)
network_tx_bytes = Column(Integer, default=0)
# Replica count
replica_count = Column(Integer, default=0)
# Request metrics
request_count = Column(Integer, default=0)
error_count = Column(Integer, default=0)
# Database initialization
def init_db():
"""Initialize database tables"""
Base.metadata.create_all(bind=engine)
def get_db() -> Session:
"""Get database session (FastAPI dependency)"""
db = SessionLocal()
try:
yield db
finally:
db.close()
def parse_resource_string(resource_str: str) -> float:
"""Parse Kubernetes resource string to float
Examples:
"100m" -> 0.1 (cores)
"2" -> 2.0 (cores)
"128Mi" -> 128.0 (MB)
"1Gi" -> 1024.0 (MB)
"""
if not resource_str:
return 0.0
resource_str = resource_str.strip()
# CPU resources
if resource_str.endswith('m'):
return float(resource_str[:-1]) / 1000.0
# Memory resources
if resource_str.endswith('Mi'):
return float(resource_str[:-2])
elif resource_str.endswith('Gi'):
return float(resource_str[:-2]) * 1024.0
elif resource_str.endswith('Ki'):
return float(resource_str[:-2]) / 1024.0
# Plain number
try:
return float(resource_str)
except ValueError:
return 0.0
# Initialize database on import
init_db()
+156
View File
@@ -0,0 +1,156 @@
#!/bin/bash
# 多租户 Agent 管理演示脚本
set -e
API_URL="http://localhost:8000"
NAMESPACE="ai-agents"
echo "========================================="
echo "多租户 Agent 管理系统演示"
echo "========================================="
echo
# 颜色定义
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 1. 创建不同用户的 Agents
echo -e "${BLUE}步骤 1: 创建不同用户的 Agents${NC}"
echo "-----------------------------------"
users=("user001" "user002" "user003")
templates=("echo_agent" "chat_agent" "code_agent")
for i in "${!users[@]}"; do
user="${users[$i]}"
template="${templates[$i]}"
agent_name="${user}-${template//_/-}"
echo -e "\n${YELLOW}创建 Agent: $agent_name (用户: $user)${NC}"
# 删除已存在的 agent
kubectl delete pod "$agent_name" -n "$NAMESPACE" 2>/dev/null || true
sleep 1
# 创建新的 agent
response=$(curl -s -X POST "$API_URL/agents" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"$agent_name\",
\"template\": \"$template\",
\"config\": {
\"user_id\": \"$user\"
}
}")
pod_id=$(echo "$response" | jq -r '.pod_id')
user_id=$(echo "$response" | jq -r '.owner_info.user_id')
if [ "$pod_id" != "null" ]; then
echo -e "${GREEN}✓ 创建成功${NC}"
echo " Pod ID: $pod_id"
echo " User ID: $user_id"
else
echo -e "${RED}✗ 创建失败${NC}"
echo "$response" | jq .
fi
done
echo
echo "等待 Pods 启动..."
sleep 3
# 2. 查看所有 Agents
echo
echo -e "${BLUE}步骤 2: 查看所有 Agents${NC}"
echo "-----------------------------------"
kubectl get pods -n "$NAMESPACE" -l managed-by=agent-manager \
-o custom-columns=NAME:.metadata.name,POD_ID:.metadata.uid,USER:.metadata.labels.user-id,STATUS:.status.phase
# 3. 按用户查询
echo
echo -e "${BLUE}步骤 3: 按用户查询 Agents${NC}"
echo "-----------------------------------"
for user in "${users[@]}"; do
echo
echo -e "${YELLOW}用户: $user${NC}"
pods=$(kubectl get pods -n "$NAMESPACE" -l user-id="$user" \
-o custom-columns=NAME:.metadata.name,POD_ID:.metadata.uid,STATUS:.status.phase --no-headers)
if [ -n "$pods" ]; then
echo "$pods"
pod_count=$(echo "$pods" | wc -l)
echo -e "${GREEN}共 $pod_count 个 Agent${NC}"
else
echo "无 Agents"
fi
done
# 4. 验证 Pod ID 查询
echo
echo -e "${BLUE}步骤 4: 通过 Pod ID 验证归属${NC}"
echo "-----------------------------------"
# 获取第一个 agent
first_agent="${users[0]}-${templates[0]//_/-}"
pod_info=$(kubectl get pod "$first_agent" -n "$NAMESPACE" -o json 2>/dev/null)
if [ -n "$pod_info" ]; then
pod_id=$(echo "$pod_info" | jq -r '.metadata.uid')
user_id=$(echo "$pod_info" | jq -r '.metadata.labels["user-id"]')
echo "Agent: $first_agent"
echo "Pod ID: $pod_id"
echo "User ID: $user_id"
# 验证通过 UID 查询
echo
echo "验证: 通过 Pod ID 查询..."
kubectl get pods -n "$NAMESPACE" --all-namespaces \
-o json | jq -r ".items[] | select(.metadata.uid==\"$pod_id\") | .metadata.name"
fi
# 5. API 状态查询
echo
echo -e "${BLUE}步骤 5: 通过 API 查询 Agent 状态${NC}"
echo "-----------------------------------"
first_agent="${users[0]}-${templates[0]//_/-}"
echo "查询 Agent: $first_agent"
curl -s "$API_URL/agents/$first_agent/status" | jq '{name, status, pod_ip, node_name}'
# 6. 汇总统计
echo
echo -e "${BLUE}步骤 6: 统计信息${NC}"
echo "-----------------------------------"
total_agents=$(kubectl get pods -n "$NAMESPACE" -l managed-by=agent-manager --no-headers | wc -l)
running_agents=$(kubectl get pods -n "$NAMESPACE" -l managed-by=agent-manager \
--field-selector status.phase=Running --no-headers | wc -l)
echo "总 Agents 数: $total_agents"
echo "运行中: $running_agents"
echo
# 按模板统计
echo "按模板统计:"
for template in "${templates[@]}"; do
count=$(kubectl get pods -n "$NAMESPACE" -l template="$template" --no-headers 2>/dev/null | wc -l)
echo " $template: $count"
done
echo
echo "按用户统计:"
for user in "${users[@]}"; do
count=$(kubectl get pods -n "$NAMESPACE" -l user-id="$user" --no-headers 2>/dev/null | wc -l)
echo " $user: $count"
done
echo
echo -e "${GREEN}========================================="
echo "演示完成!"
echo "=========================================${NC}"
+8
View File
@@ -0,0 +1,8 @@
apiVersion: v1
data:
.dockerconfigjson: eyJhdXRocyI6eyJhZ25ldHRhaWppLmF6dXJlY3IuaW8iOnsidXNlcm5hbWUiOiJhZ25ldHRhaWppIiwicGFzc3dvcmQiOiJoRHBYNXQzNE41Wm1uS2R0cXlqWUw1Y28vU25YSnJtRDIwQ1JwR3BXYUcrQUNSQ3cyd0dNIiwiYXV0aCI6IllXZHVaWFIwWVdscWFUcG9SSEJZTlhRek5FNDFXbTF1UzJSMGNYbHFXVXcxWTI4dlUyNVlTbkp0UkRJd1ExSndSM0JYWVVjclFVTlNRM2N5ZDBkTiJ9fX0=
kind: Secret
metadata:
name: acr-secret
namespace: default
type: kubernetes.io/dockerconfigjson
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# 创建ACR imagePullSecret的脚本
echo "创建ACR ImagePullSecret..."
# 从.env文件读取ACR配置
if [ -f ".env" ]; then
source .env
else
echo "错误: .env文件不存在"
exit 1
fi
# 检查必要的环境变量
if [ -z "$REGISTRY_URL" ] || [ -z "$REGISTRY_USERNAME" ] || [ -z "$REGISTRY_PASSWORD" ]; then
echo "错误: 请在.env文件中配置以下变量:"
echo " REGISTRY_URL"
echo " REGISTRY_USERNAME"
echo " REGISTRY_PASSWORD"
exit 1
fi
echo "ACR配置:"
echo " Registry: $REGISTRY_URL"
echo " Username: $REGISTRY_USERNAME"
# 创建imagePullSecret
kubectl create secret docker-registry acr-secret \
--docker-server=$REGISTRY_URL \
--docker-username=$REGISTRY_USERNAME \
--docker-password=$REGISTRY_PASSWORD \
--namespace=default \
--dry-run=client -o yaml > k8s/acr-secret.yaml
echo ""
echo "✅ ACR Secret配置已生成: k8s/acr-secret.yaml"
echo ""
echo "应用Secret到集群:"
echo "kubectl apply -f k8s/acr-secret.yaml"
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# 创建kubeconfig Secret的脚本
echo "创建Kubeconfig Secret..."
# 检查kubeconfig文件是否存在
KUBECONFIG_FILE="${HOME}/.kube/config"
if [ ! -f "$KUBECONFIG_FILE" ]; then
echo "错误: kubeconfig文件不存在: $KUBECONFIG_FILE"
exit 1
fi
# 创建Secret
kubectl create secret generic kubeconfig-secret \
--from-file=config=$KUBECONFIG_FILE \
--namespace=default \
--dry-run=client -o yaml > k8s/kubeconfig-secret.yaml
echo "✅ Secret配置已生成: k8s/kubeconfig-secret.yaml"
echo ""
echo "应用Secret到集群:"
echo "kubectl apply -f k8s/kubeconfig-secret.yaml"
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# 部署脚本 - 使用kubeconfig Secret方式
echo "开始部署Agent Manager (使用Kubeconfig Secret)..."
# 1. 创建命名空间
echo "1. 创建ai-agents命名空间..."
kubectl apply -f k8s/namespace.yaml
# 2. 创建ACR访问密钥
echo "2. 创建ACR访问密钥..."
if [ ! -f "k8s/acr-secret.yaml" ]; then
echo "生成ACR Secret..."
./k8s/create-acr-secret.sh
fi
kubectl apply -f k8s/acr-secret.yaml
# 3. 创建kubeconfig Secret
echo "3. 创建kubeconfig Secret..."
if [ ! -f "k8s/kubeconfig-secret.yaml" ]; then
echo "生成kubeconfig Secret..."
./k8s/create-kubeconfig-secret.sh
fi
kubectl apply -f k8s/kubeconfig-secret.yaml
# 4. 部署Agent Manager服务(使用kubeconfig)
echo "4. 部署Agent Manager..."
kubectl apply -f k8s/deployment-with-kubeconfig.yaml
# 5. 等待部署完成
echo "5. 等待Pod就绪..."
kubectl wait --for=condition=ready pod -l app=agent-manager -n default --timeout=120s
# 6. 显示服务状态
echo ""
echo "✅ 部署完成!"
echo ""
echo "服务状态:"
kubectl get pods -n default -l app=agent-manager
echo ""
echo "服务信息:"
kubectl get svc -n default -l app=agent-manager
echo ""
echo "查看日志:"
echo "kubectl logs -n default -l app=agent-manager -f"
echo ""
echo "访问服务:"
echo "kubectl port-forward -n default svc/agent-manager 8000:8000"
Executable
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
# 部署脚本 - 使用ServiceAccount + RBAC方式(推荐用于生产环境)
echo "开始部署Agent Manager (使用ServiceAccount + RBAC)..."
# 1. 创建命名空间
echo "1. 创建ai-agents命名空间..."
kubectl apply -f k8s/namespace.yaml
# 2. 创建ACR访问密钥
echo "2. 创建ACR访问密钥..."
if [ ! -f "k8s/acr-secret.yaml" ]; then
echo "生成ACR Secret..."
./k8s/create-acr-secret.sh
fi
kubectl apply -f k8s/acr-secret.yaml
# 3. 配置RBAC权限
echo "3. 配置RBAC权限..."
kubectl apply -f k8s/rbac.yaml
# 4. 部署Agent Manager服务
echo "4. 部署Agent Manager..."
kubectl apply -f k8s/deployment.yaml
# 5. 等待部署完成
echo "5. 等待Pod就绪..."
kubectl wait --for=condition=ready pod -l app=agent-manager -n default --timeout=120s
# 6. 显示服务状态
echo ""
echo "✅ 部署完成!"
echo ""
echo "服务状态:"
kubectl get pods -n default -l app=agent-manager
echo ""
echo "服务信息:"
kubectl get svc -n default -l app=agent-manager
echo ""
echo "查看日志:"
echo "kubectl logs -n default -l app=agent-manager -f"
echo ""
echo "访问服务:"
echo "kubectl port-forward -n default svc/agent-manager 8000:8000"
+82
View File
@@ -0,0 +1,82 @@
# 方案1:使用kubeconfig Secret部署(推荐用于开发/测试)
apiVersion: apps/v1
kind: Deployment
metadata:
name: agent-manager
namespace: default
labels:
app: agent-manager
spec:
replicas: 1
selector:
matchLabels:
app: agent-manager
template:
metadata:
labels:
app: agent-manager
spec:
# 使用我们创建的ServiceAccount(即使我们也挂载了kubeconfig作为备份)
serviceAccountName: agent-manager
imagePullSecrets:
- name: acr-secret
containers:
- name: agent-manager
image: agnettaiji.azurecr.io/agent-manager:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
name: http
env:
- name: NAMESPACE
value: "ai-agents"
- name: SERVICE_PORT
value: "8000"
- name: SERVICE_HOST
value: "0.0.0.0"
- name: KUBECONFIG_PATH
value: "/root/.kube/config"
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
volumeMounts:
- name: kubeconfig
mountPath: /root/.kube
readOnly: true
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: kubeconfig
secret:
secretName: kubeconfig-secret
---
apiVersion: v1
kind: Service
metadata:
name: agent-manager
namespace: default
labels:
app: agent-manager
spec:
type: ClusterIP
ports:
- port: 8000
targetPort: 8000
protocol: TCP
name: http
selector:
app: agent-manager
+83
View File
@@ -0,0 +1,83 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: agent-manager
namespace: default
labels:
app: agent-manager
spec:
replicas: 1
selector:
matchLabels:
app: agent-manager
template:
metadata:
labels:
app: agent-manager
spec:
serviceAccountName: agent-manager
imagePullSecrets:
- name: acr-secret
containers:
- name: agent-manager
image: agnettaiji.azurecr.io/agent-manager:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
name: http
env:
- name: NAMESPACE
value: "ai-agents"
- name: SERVICE_PORT
value: "8000"
- name: SERVICE_HOST
value: "0.0.0.0"
# 可选:如果使用挂载的kubeconfig文件
# - name: KUBECONFIG_PATH
# value: "/root/.kube/config"
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
# 可选:挂载kubeconfig Secret(如果不使用ServiceAccount)
# volumeMounts:
# - name: kubeconfig
# mountPath: /root/.kube
# readOnly: true
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
# 可选:定义kubeconfig volume(如果不使用ServiceAccount)
# volumes:
# - name: kubeconfig
# secret:
# secretName: kubeconfig-secret
---
apiVersion: v1
kind: Service
metadata:
name: agent-manager
namespace: default
labels:
app: agent-manager
spec:
type: ClusterIP
ports:
- port: 8000
targetPort: 8000
protocol: TCP
name: http
selector:
app: agent-manager
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: kubeconfig-secret
namespace: default
type: Opaque
data:
config: |
# 这里需要放置base64编码的kubeconfig内容
# 生成方式: cat ~/.kube/config | base64 -w 0
# 然后将输出粘贴到这里
+7
View File
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: ai-agents
labels:
name: ai-agents
purpose: ai-agent-hosting
+40
View File
@@ -0,0 +1,40 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: agent-manager
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: agent-manager-role
rules:
# 管理ai-agents命名空间的所有权限
- apiGroups: [""]
resources: ["pods", "pods/log", "pods/status"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list", "create"]
# 允许访问所有命名空间的命名空间资源(用于确保命名空间存在)
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get"]
resourceNames: ["ai-agents"]
# Metrics权限(如果安装了metrics-server)
- apiGroups: ["metrics.k8s.io"]
resources: ["pods"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: agent-manager-binding
subjects:
- kind: ServiceAccount
name: agent-manager
namespace: default
roleRef:
kind: ClusterRole
name: agent-manager-role
apiGroup: rbac.authorization.k8s.io
+473
View File
@@ -0,0 +1,473 @@
"""
Kubernetes管理模块 - 负责与Kubernetes集群交互
"""
from kubernetes import client, config
from kubernetes.client.rest import ApiException
from typing import Dict, List, Optional
import logging
import os
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class K8sManager:
"""Kubernetes资源管理器"""
def __init__(self, namespace: str = "ai-agents", kubeconfig_path: str = None):
"""
初始化Kubernetes管理器
Args:
namespace: AI Agent部署的命名空间
kubeconfig_path: kubeconfig文件路径(可选)
"""
self.namespace = namespace
self._load_kube_config(kubeconfig_path)
self.v1 = client.CoreV1Api()
self.apps_v1 = client.AppsV1Api()
# 确保命名空间存在
self._ensure_namespace()
def _load_kube_config(self, kubeconfig_path: str = None):
"""
加载Kubernetes配置
优先级:
1. 集群内ServiceAccount(推荐用于生产环境)
2. 指定的kubeconfig文件路径
3. 默认的kubeconfig路径 (~/.kube/config)
"""
try:
# 方式1: 尝试加载集群内配置(当服务运行在K8s中且有ServiceAccount时)
config.load_incluster_config()
logger.info("✅ 使用集群内ServiceAccount配置")
except config.ConfigException:
try:
if kubeconfig_path and os.path.exists(kubeconfig_path):
# 方式2: 使用指定的kubeconfig文件
config.load_kube_config(config_file=kubeconfig_path)
logger.info(f"✅ 使用指定的kubeconfig: {kubeconfig_path}")
else:
# 方式3: 使用默认kubeconfig(开发环境)
config.load_kube_config()
logger.info("✅ 使用默认kubeconfig (~/.kube/config)")
except Exception as e:
logger.error(f"❌ 无法加载Kubernetes配置: {e}")
raise Exception(f"Kubernetes配置加载失败: {e}")
def _ensure_namespace(self):
"""确保AI Agent命名空间存在"""
try:
self.v1.read_namespace(name=self.namespace)
logger.info(f"命名空间 {self.namespace} 已存在")
except ApiException as e:
if e.status == 404:
# 创建命名空间
namespace_manifest = client.V1Namespace(
metadata=client.V1ObjectMeta(name=self.namespace)
)
self.v1.create_namespace(body=namespace_manifest)
logger.info(f"创建命名空间 {self.namespace}")
else:
raise
# 模板端口映射
TEMPLATE_PORTS = {
"jina_search_agent": 8080,
}
# 模板所需环境变量说明
TEMPLATE_ENV_INFO = {
"jina_search_agent": {
"required": {
"JINA_API_KEY": "Jina API密钥,从 https://jina.ai/ 获取"
},
"optional": {
"SERVICE_PORT": "HTTP服务端口,默认8080",
"SERVICE_HOST": "HTTP服务监听地址,默认0.0.0.0"
}
},
"mysql_agent": {
"required": {
"MYSQL_HOST": "MySQL数据库主机地址",
"MYSQL_USER": "MySQL用户名",
"MYSQL_PASSWORD": "MySQL密码",
"MYSQL_DATABASE": "MySQL数据库名",
"OPENAI_API_KEY": "OpenAI API密钥"
},
"optional": {
"MYSQL_PORT": "MySQL端口,默认3306"
}
},
"postgresql_agent": {
"required": {
"POSTGRES_HOST": "PostgreSQL数据库主机地址",
"POSTGRES_USER": "PostgreSQL用户名",
"POSTGRES_PASSWORD": "PostgreSQL密码",
"POSTGRES_DATABASE": "PostgreSQL数据库名",
"OPENAI_API_KEY": "OpenAI API密钥"
},
"optional": {
"POSTGRES_PORT": "PostgreSQL端口,默认5432"
}
}
}
def get_template_info(self, template: str) -> Dict:
"""
获取模板信息
Args:
template: 模板类型
Returns:
模板信息(端口、所需环境变量等)
"""
return {
"template": template,
"port": self.TEMPLATE_PORTS.get(template),
"env_info": self.TEMPLATE_ENV_INFO.get(template, {})
}
def create_pod(self, pod_name: str, template: str, config_data: Dict) -> Dict:
"""
创建Pod
Args:
pod_name: Pod名称
template: 模板类型(echo_agent, chat_agent等)
config_data: 配置信息(replicas, resources等)
Returns:
创建的Pod信息,包含访问地址
"""
try:
# 生成Pod规格
pod_manifest = self._generate_pod_manifest(pod_name, template, config_data)
# 创建Pod
response = self.v1.create_namespaced_pod(
namespace=self.namespace,
body=pod_manifest
)
logger.info(f"Pod {pod_name} 创建成功")
# 获取服务端口
service_port = self.TEMPLATE_PORTS.get(template)
result = {
"name": response.metadata.name,
"namespace": response.metadata.namespace,
"status": response.status.phase,
"created_at": response.metadata.creation_timestamp.isoformat() if response.metadata.creation_timestamp else None,
"template": template
}
# 如果是HTTP服务类型的agent,添加访问信息
if service_port:
result["service_port"] = service_port
result["access_info"] = {
"note": "Pod IP将在Pod运行后可用,请通过 /agents/{name}/status 获取",
"port": service_port,
"endpoints": {
"root": f"http://<pod_ip>:{service_port}/",
"health": f"http://<pod_ip>:{service_port}/health"
}
}
return result
except ApiException as e:
logger.error(f"创建Pod失败: {e}")
raise Exception(f"创建Pod失败: {e.reason}")
def _generate_pod_manifest(self, pod_name: str, template: str, config_data: Dict) -> client.V1Pod:
"""生成Pod配置清单"""
# 默认资源配置
replicas = config_data.get("replicas", 1)
cpu_request = config_data.get("cpu_request", "100m")
cpu_limit = config_data.get("cpu_limit", "500m")
memory_request = config_data.get("memory_request", "128Mi")
memory_limit = config_data.get("memory_limit", "512Mi")
# 根据模板类型选择镜像
image_map = {
"echo_agent": "agnettaiji.azurecr.io/ai-agents/echo-agent:latest",
"chat_agent": "agnettaiji.azurecr.io/ai-agents/chat-agent:latest",
"code_agent": "agnettaiji.azurecr.io/ai-agents/code-agent:latest",
"search_agent": "agnettaiji.azurecr.io/ai-agents/search-agent:latest",
"mysql_agent": "agnettaiji.azurecr.io/ai-agents/mysql-agent:latest",
"postgresql_agent": "agnettaiji.azurecr.io/ai-agents/postgresql-agent:latest",
"jina_search_agent": "agnettaiji.azurecr.io/ai-agents/jina-search-agent:latest",
}
image = image_map.get(template, image_map["echo_agent"])
# 构建环境变量列表
env_vars = [
client.V1EnvVar(name="POD_NAME", value=pod_name),
client.V1EnvVar(name="TEMPLATE_TYPE", value=template)
]
# 添加用户自定义环境变量
custom_env = config_data.get("env", {})
for key, value in custom_env.items():
env_vars.append(client.V1EnvVar(name=key, value=str(value)))
logger.info(f"Pod {pod_name} 环境变量数量: {len(env_vars)}")
# 设置容器端口(如果是HTTP服务类型的agent)
container_ports = None
if template in ["jina_search_agent"]:
container_ports = [client.V1ContainerPort(container_port=8080)]
# 创建Pod规格
container = client.V1Container(
name=pod_name,
image=image,
resources=client.V1ResourceRequirements(
requests={"cpu": cpu_request, "memory": memory_request},
limits={"cpu": cpu_limit, "memory": memory_limit}
),
env=env_vars,
ports=container_ports
)
pod_spec = client.V1PodSpec(
containers=[container],
restart_policy="Always",
image_pull_secrets=[client.V1LocalObjectReference(name="acr-secret")]
)
# 构建标签(合并默认标签和用户自定义标签)
labels = {
"app": "ai-agent",
"template": template,
"managed-by": "agent-manager"
}
# 添加用户自定义标签
if "labels" in config_data:
labels.update(config_data["labels"])
pod_manifest = client.V1Pod(
api_version="v1",
kind="Pod",
metadata=client.V1ObjectMeta(
name=pod_name,
labels=labels
),
spec=pod_spec
)
return pod_manifest
def delete_pod(self, pod_name: str) -> Dict:
"""
删除Pod
Args:
pod_name: Pod名称
Returns:
删除结果
"""
try:
self.v1.delete_namespaced_pod(
name=pod_name,
namespace=self.namespace,
body=client.V1DeleteOptions()
)
logger.info(f"Pod {pod_name} 删除成功")
return {"status": "success", "message": f"Pod {pod_name} 已删除"}
except ApiException as e:
if e.status == 404:
return {"status": "not_found", "message": f"Pod {pod_name} 不存在"}
logger.error(f"删除Pod失败: {e}")
raise Exception(f"删除Pod失败: {e.reason}")
def get_pod_status(self, pod_name: str) -> Dict:
"""
获取Pod状态
Args:
pod_name: Pod名称
Returns:
Pod状态信息,包含访问URL和资源使用情况
"""
try:
pod = self.v1.read_namespaced_pod(
name=pod_name,
namespace=self.namespace
)
template = pod.metadata.labels.get("template", "unknown")
pod_ip = pod.status.pod_ip
service_port = self.TEMPLATE_PORTS.get(template)
# 获取资源配额信息
container = pod.spec.containers[0]
resources = container.resources
resource_requests = {
"cpu": resources.requests.get("cpu") if resources.requests else None,
"memory": resources.requests.get("memory") if resources.requests else None
}
resource_limits = {
"cpu": resources.limits.get("cpu") if resources.limits else None,
"memory": resources.limits.get("memory") if resources.limits else None
}
# 尝试获取实际资源使用情况(需要metrics-server)
resource_usage = self._get_pod_resource_usage(pod_name)
result = {
"name": pod.metadata.name,
"namespace": pod.metadata.namespace,
"status": pod.status.phase,
"template": template,
"created_at": pod.metadata.creation_timestamp.isoformat() if pod.metadata.creation_timestamp else None,
"node": pod.spec.node_name,
"pod_ip": pod_ip,
"resources": {
"requests": resource_requests,
"limits": resource_limits,
"usage": resource_usage
},
"conditions": [
{
"type": condition.type,
"status": condition.status,
"reason": condition.reason
}
for condition in (pod.status.conditions or [])
]
}
# 如果是HTTP服务类型的agent且Pod已有IP,添加访问URL
if service_port and pod_ip:
result["service_port"] = service_port
result["access_url"] = f"http://{pod_ip}:{service_port}"
result["endpoints"] = {
"root": f"http://{pod_ip}:{service_port}/",
"health": f"http://{pod_ip}:{service_port}/health"
}
return result
except ApiException as e:
if e.status == 404:
return {"status": "not_found", "message": f"Pod {pod_name} 不存在"}
logger.error(f"获取Pod状态失败: {e}")
raise Exception(f"获取Pod状态失败: {e.reason}")
def _get_pod_resource_usage(self, pod_name: str) -> Dict:
"""
获取Pod实际资源使用情况(需要metrics-server)
Args:
pod_name: Pod名称
Returns:
资源使用信息(cpu、memory)
"""
try:
# 使用CustomObjectsApi调用metrics API
custom_api = client.CustomObjectsApi()
metrics = custom_api.get_namespaced_custom_object(
group="metrics.k8s.io",
version="v1beta1",
namespace=self.namespace,
plural="pods",
name=pod_name
)
# 解析容器资源使用情况
containers = metrics.get("containers", [])
if containers:
container = containers[0]
usage = container.get("usage", {})
return {
"cpu": usage.get("cpu"),
"memory": usage.get("memory"),
"available": True
}
return {"cpu": None, "memory": None, "available": False, "reason": "无容器数据"}
except ApiException as e:
if e.status == 404:
return {"cpu": None, "memory": None, "available": False, "reason": "metrics-server未安装或Pod不存在"}
logger.warning(f"获取Pod资源使用情况失败: {e.reason}")
return {"cpu": None, "memory": None, "available": False, "reason": f"获取失败: {e.reason}"}
except Exception as e:
logger.warning(f"获取Pod资源使用情况异常: {str(e)}")
return {"cpu": None, "memory": None, "available": False, "reason": f"异常: {str(e)}"}
def get_pod_metrics(self, pod_name: str) -> Dict:
"""
获取Pod资源使用情况(CPU、内存)
Args:
pod_name: Pod名称
Returns:
Pod资源使用信息
"""
try:
# 注意: 需要集群安装metrics-server
# 这里提供基本的资源配额信息
pod = self.v1.read_namespaced_pod(
name=pod_name,
namespace=self.namespace
)
container = pod.spec.containers[0]
resources = container.resources
return {
"name": pod_name,
"requests": {
"cpu": resources.requests.get("cpu") if resources.requests else None,
"memory": resources.requests.get("memory") if resources.requests else None
},
"limits": {
"cpu": resources.limits.get("cpu") if resources.limits else None,
"memory": resources.limits.get("memory") if resources.limits else None
}
}
except ApiException as e:
logger.error(f"获取Pod资源信息失败: {e}")
raise Exception(f"获取Pod资源信息失败: {e.reason}")
def list_pods(self, label_selector: Optional[str] = None) -> List[Dict]:
"""
列出所有Pod
Args:
label_selector: 标签选择器(可选)
Returns:
Pod列表
"""
try:
if label_selector is None:
label_selector = "managed-by=agent-manager"
pods = self.v1.list_namespaced_pod(
namespace=self.namespace,
label_selector=label_selector
)
return [
{
"name": pod.metadata.name,
"status": pod.status.phase,
"template": pod.metadata.labels.get("template", "unknown"),
"created_at": pod.metadata.creation_timestamp.isoformat() if pod.metadata.creation_timestamp else None,
"pod_ip": pod.status.pod_ip
}
for pod in pods.items
]
except ApiException as e:
logger.error(f"列出Pod失败: {e}")
raise Exception(f"列出Pod失败: {e.reason}")
+567
View File
@@ -0,0 +1,567 @@
"""
Enhanced Kubernetes Manager - 支持Deployment、Service、HPA和Secrets
"""
from kubernetes import client, config
from kubernetes.client.rest import ApiException
from datetime import datetime
import logging
import os
import base64
logger = logging.getLogger(__name__)
class K8sManager:
"""Kubernetes资源管理器 - 增强版"""
def __init__(self, namespace="ai-agents", kubeconfig_path=None):
"""
初始化K8s管理器
Args:
namespace: 命名空间
kubeconfig_path: kubeconfig文件路径(可选,用于本地开发)
"""
self.namespace = namespace
try:
if kubeconfig_path and os.path.exists(kubeconfig_path):
config.load_kube_config(kubeconfig_path)
logger.info(f"使用kubeconfig: {kubeconfig_path}")
else:
config.load_incluster_config()
logger.info("使用集群内ServiceAccount")
except Exception as e:
logger.error(f"K8s配置加载失败: {str(e)}")
raise
self.core_v1 = client.CoreV1Api()
self.apps_v1 = client.AppsV1Api()
self.autoscaling_v2 = client.AutoscalingV2Api()
self._ensure_namespace()
def _ensure_namespace(self):
"""确保命名空间存在"""
try:
self.core_v1.read_namespace(self.namespace)
logger.info(f"命名空间 {self.namespace} 已存在")
except ApiException as e:
if e.status == 404:
namespace = client.V1Namespace(
metadata=client.V1ObjectMeta(name=self.namespace)
)
self.core_v1.create_namespace(namespace)
logger.info(f"创建命名空间: {self.namespace}")
else:
raise
def create_secret(self, name: str, data: dict) -> dict:
"""
创建Kubernetes Secret存储敏感数据
Args:
name: Secret名称
data: 敏感数据字典
Returns:
Secret信息
"""
try:
# 编码数据为base64
encoded_data = {}
for key, value in data.items():
if isinstance(value, str):
encoded_data[key] = base64.b64encode(value.encode()).decode()
else:
encoded_data[key] = base64.b64encode(str(value).encode()).decode()
secret = client.V1Secret(
metadata=client.V1ObjectMeta(
name=name,
namespace=self.namespace,
labels={
"managed-by": "agent-manager",
"type": "agent-secret"
}
),
type="Opaque",
data=encoded_data
)
result = self.core_v1.create_namespaced_secret(self.namespace, secret)
logger.info(f"Created secret: {name}")
return {"name": name, "namespace": self.namespace}
except ApiException as e:
if e.status == 409:
# Secret已存在,更新它
logger.info(f"Secret {name} exists, updating...")
result = self.core_v1.replace_namespaced_secret(name, self.namespace, secret)
return {"name": name, "namespace": self.namespace}
else:
logger.error(f"Failed to create secret: {e}")
raise
def delete_secret(self, name: str):
"""删除Secret"""
try:
self.core_v1.delete_namespaced_secret(name, self.namespace)
logger.info(f"Deleted secret: {name}")
except ApiException as e:
if e.status != 404:
logger.error(f"Failed to delete secret: {e}")
def create_deployment_and_service(self, name: str, template, agent, env_vars: dict) -> dict:
"""
创建Deployment和Service
Args:
name: Agent名称
template: Template数据库对象
agent: Agent数据库对象
env_vars: 环境变量字典
Returns:
部署结果信息
"""
try:
deployment_name = f"{name}-deployment"
service_name = f"{name}-service"
# 1. 如果有敏感环境变量,创建Secret
secret_name = None
if env_vars:
secret_name = f"{name}-secret"
self.create_secret(secret_name, env_vars)
# 2. 创建Deployment
deployment = self._build_deployment(
name=deployment_name,
image=template.image,
port=template.port,
secret_name=secret_name,
agent=agent,
labels={
"app": name,
"managed-by": "agent-manager",
"template": template.name,
"agent-type": agent.agent_type.value,
"owner": agent.owner_id
}
)
self.apps_v1.create_namespaced_deployment(self.namespace, deployment)
logger.info(f"Created deployment: {deployment_name}")
# 3. 创建Service(如果模板定义了端口)
service_url = None
if template.port:
service = self._build_service(
name=service_name,
port=template.port,
selector={"app": name}
)
self.core_v1.create_namespaced_service(self.namespace, service)
logger.info(f"Created service: {service_name}")
# 生成服务URL(集群内访问)
service_url = f"http://{service_name}.{self.namespace}.svc.cluster.local:{template.port}"
# 4. 创建HPA(如果配置了弹性伸缩)
if agent.max_replicas > agent.min_replicas:
self.create_hpa(
name=f"{name}-hpa",
deployment_name=deployment_name,
min_replicas=agent.min_replicas,
max_replicas=agent.max_replicas,
target_cpu_utilization=agent.target_cpu_utilization
)
return {
"deployment_name": deployment_name,
"service_name": service_name,
"service_url": service_url,
"secret_name": secret_name
}
except Exception as e:
logger.error(f"Failed to create deployment and service: {str(e)}")
# 清理已创建的资源
self._cleanup_resources(deployment_name, service_name, secret_name)
raise
def _build_deployment(self, name: str, image: str, port: int, secret_name: str,
agent, labels: dict) -> client.V1Deployment:
"""构建Deployment对象"""
# 环境变量配置
env_vars = []
if secret_name:
# 从Secret引用环境变量
for key in agent.environment_vars.keys():
env_vars.append(client.V1EnvVar(
name=key,
value_from=client.V1EnvVarSource(
secret_key_ref=client.V1SecretKeySelector(
name=secret_name,
key=key
)
)
))
# 容器配置
container = client.V1Container(
name="agent",
image=image,
image_pull_policy="Always",
env=env_vars if env_vars else None,
resources=client.V1ResourceRequirements(
requests={
"cpu": agent.cpu_request or "100m",
"memory": agent.memory_request or "128Mi"
},
limits={
"cpu": agent.cpu_limit or "500m",
"memory": agent.memory_limit or "512Mi"
}
)
)
# 如果有端口,添加端口配置
if port:
container.ports = [client.V1ContainerPort(container_port=port)]
# Pod模板
template = client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(
labels=labels
),
spec=client.V1PodSpec(
containers=[container],
image_pull_secrets=[client.V1LocalObjectReference(name="acr-secret")]
)
)
# Deployment规格
spec = client.V1DeploymentSpec(
replicas=agent.min_replicas,
selector=client.V1LabelSelector(
match_labels={"app": labels["app"]}
),
template=template
)
# Deployment对象
deployment = client.V1Deployment(
api_version="apps/v1",
kind="Deployment",
metadata=client.V1ObjectMeta(
name=name,
namespace=self.namespace,
labels=labels
),
spec=spec
)
return deployment
def _build_service(self, name: str, port: int, selector: dict) -> client.V1Service:
"""构建Service对象"""
service = client.V1Service(
api_version="v1",
kind="Service",
metadata=client.V1ObjectMeta(
name=name,
namespace=self.namespace,
labels={
"managed-by": "agent-manager"
}
),
spec=client.V1ServiceSpec(
selector=selector,
ports=[client.V1ServicePort(
port=port,
target_port=port,
protocol="TCP"
)],
type="ClusterIP"
)
)
return service
def create_hpa(self, name: str, deployment_name: str, min_replicas: int,
max_replicas: int, target_cpu_utilization: int) -> dict:
"""
创建HorizontalPodAutoscaler
Args:
name: HPA名称
deployment_name: 目标Deployment名称
min_replicas: 最小副本数
max_replicas: 最大副本数
target_cpu_utilization: 目标CPU利用率(百分比)
Returns:
HPA信息
"""
try:
hpa = client.V2HorizontalPodAutoscaler(
api_version="autoscaling/v2",
kind="HorizontalPodAutoscaler",
metadata=client.V1ObjectMeta(
name=name,
namespace=self.namespace
),
spec=client.V2HorizontalPodAutoscalerSpec(
scale_target_ref=client.V2CrossVersionObjectReference(
api_version="apps/v1",
kind="Deployment",
name=deployment_name
),
min_replicas=min_replicas,
max_replicas=max_replicas,
metrics=[
client.V2MetricSpec(
type="Resource",
resource=client.V2ResourceMetricSource(
name="cpu",
target=client.V2MetricTarget(
type="Utilization",
average_utilization=target_cpu_utilization
)
)
)
]
)
)
result = self.autoscaling_v2.create_namespaced_horizontal_pod_autoscaler(
self.namespace, hpa
)
logger.info(f"Created HPA: {name}")
return {"name": name, "namespace": self.namespace}
except ApiException as e:
logger.error(f"Failed to create HPA: {e}")
raise
def delete_hpa(self, name: str):
"""删除HPA"""
try:
self.autoscaling_v2.delete_namespaced_horizontal_pod_autoscaler(
name, self.namespace
)
logger.info(f"Deleted HPA: {name}")
except ApiException as e:
if e.status != 404:
logger.error(f"Failed to delete HPA: {e}")
def update_deployment_env(self, deployment_name: str, env_vars: dict):
"""
更新Deployment的环境变量(通过更新Secret)
Args:
deployment_name: Deployment名称
env_vars: 新的环境变量字典
"""
try:
# 获取Deployment
deployment = self.apps_v1.read_namespaced_deployment(
deployment_name, self.namespace
)
# 查找Secret名称
secret_name = None
for env in deployment.spec.template.spec.containers[0].env or []:
if env.value_from and env.value_from.secret_key_ref:
secret_name = env.value_from.secret_key_ref.name
break
if secret_name:
# 更新Secret
self.create_secret(secret_name, env_vars)
# 触发Pod重启(通过添加annotation)
if not deployment.spec.template.metadata.annotations:
deployment.spec.template.metadata.annotations = {}
deployment.spec.template.metadata.annotations["kubectl.kubernetes.io/restartedAt"] = \
datetime.utcnow().isoformat()
self.apps_v1.replace_namespaced_deployment(
deployment_name, self.namespace, deployment
)
logger.info(f"Updated deployment env: {deployment_name}")
else:
raise ValueError("No secret found in deployment")
except ApiException as e:
logger.error(f"Failed to update deployment env: {e}")
raise
def delete_deployment_and_service(self, deployment_name: str, service_name: str):
"""
删除Deployment、Service和相关资源
Args:
deployment_name: Deployment名称
service_name: Service名称
"""
try:
# 删除Deployment
try:
self.apps_v1.delete_namespaced_deployment(
deployment_name, self.namespace,
propagation_policy='Foreground'
)
logger.info(f"Deleted deployment: {deployment_name}")
except ApiException as e:
if e.status != 404:
logger.error(f"Failed to delete deployment: {e}")
# 删除Service
try:
self.core_v1.delete_namespaced_service(service_name, self.namespace)
logger.info(f"Deleted service: {service_name}")
except ApiException as e:
if e.status != 404:
logger.error(f"Failed to delete service: {e}")
# 删除HPA
hpa_name = deployment_name.replace("-deployment", "-hpa")
self.delete_hpa(hpa_name)
# 删除Secret
secret_name = deployment_name.replace("-deployment", "-secret")
self.delete_secret(secret_name)
except Exception as e:
logger.error(f"Failed to delete resources: {str(e)}")
raise
def _cleanup_resources(self, deployment_name: str, service_name: str, secret_name: str):
"""清理资源(用于错误恢复)"""
if deployment_name:
try:
self.apps_v1.delete_namespaced_deployment(deployment_name, self.namespace)
except:
pass
if service_name:
try:
self.core_v1.delete_namespaced_service(service_name, self.namespace)
except:
pass
if secret_name:
try:
self.delete_secret(secret_name)
except:
pass
def get_deployment_status(self, deployment_name: str) -> dict:
"""获取Deployment状态"""
try:
deployment = self.apps_v1.read_namespaced_deployment(
deployment_name, self.namespace
)
return {
"name": deployment_name,
"namespace": self.namespace,
"replicas": deployment.status.replicas or 0,
"ready_replicas": deployment.status.ready_replicas or 0,
"available_replicas": deployment.status.available_replicas or 0,
"conditions": [
{
"type": c.type,
"status": c.status,
"reason": c.reason,
"message": c.message
}
for c in (deployment.status.conditions or [])
]
}
except ApiException as e:
if e.status == 404:
return {"status": "not_found", "message": f"Deployment {deployment_name} not found"}
raise
def get_pod_logs(self, deployment_name: str, lines: int = 100) -> str:
"""获取Pod日志"""
try:
# 查找Deployment对应的Pods
label_selector = f"app={deployment_name.replace('-deployment', '')}"
pods = self.core_v1.list_namespaced_pod(
self.namespace,
label_selector=label_selector
)
if not pods.items:
return "No pods found"
# 获取第一个Pod的日志
pod_name = pods.items[0].metadata.name
logs = self.core_v1.read_namespaced_pod_log(
pod_name, self.namespace,
tail_lines=lines
)
return logs
except ApiException as e:
logger.error(f"Failed to get pod logs: {e}")
raise
# ==================== 向后兼容的方法 ====================
def create_pod(self, pod_name: str, template: str, config_data: dict) -> dict:
"""创建Pod(旧方法,保留向后兼容)"""
# 这个方法现在已被create_deployment_and_service替代
# 但为了兼容性保留
raise NotImplementedError("Use create_deployment_and_service instead")
def delete_pod(self, pod_name: str) -> dict:
"""删除Pod(旧方法)"""
raise NotImplementedError("Use delete_deployment_and_service instead")
def get_pod_status(self, pod_name: str) -> dict:
"""获取Pod状态(旧方法)"""
# 尝试查找对应的Deployment
deployment_name = f"{pod_name}-deployment"
return self.get_deployment_status(deployment_name)
def list_pods(self, label_selector: str = None) -> list:
"""列出Pods"""
try:
if label_selector:
deployments = self.apps_v1.list_namespaced_deployment(
self.namespace,
label_selector=label_selector
)
else:
deployments = self.apps_v1.list_namespaced_deployment(self.namespace)
result = []
for deployment in deployments.items:
result.append({
"name": deployment.metadata.name,
"namespace": self.namespace,
"replicas": deployment.status.replicas or 0,
"ready_replicas": deployment.status.ready_replicas or 0,
"labels": deployment.metadata.labels
})
return result
except ApiException as e:
logger.error(f"Failed to list deployments: {e}")
raise
+704
View File
@@ -0,0 +1,704 @@
# AI Agent Manager API 文档
## 概述
AI Agent Manager 是一个基于 FastAPI 构建的 Kubernetes AI Agent 管理服务。该服务提供 RESTful API 接口,用于在 Kubernetes 集群中创建、删除、查询和管理 AI Agent Pod。
### 基本信息
| 项目 | 值 |
|------|-----|
| 服务名称 | AI Agent Manager |
| 版本 | 1.0.0 |
| 基础URL | `http://<host>:<port>` |
| 默认端口 | 8000 |
| 内容类型 | `application/json` |
### 环境变量配置
| 变量名 | 默认值 | 说明 |
|--------|--------|------|
| `NAMESPACE` | `ai-agents` | AI Agent 部署的 Kubernetes 命名空间 |
| `KUBECONFIG_PATH` | `None` | kubeconfig 文件路径(可选) |
| `SERVICE_HOST` | `0.0.0.0` | 服务监听地址 |
| `SERVICE_PORT` | `8000` | 服务监听端口 |
---
## API 端点
### 1. 健康检查
检查服务运行状态。
**请求**
```
GET /
```
**响应**
```json
{
"service": "AI Agent Manager",
"status": "running",
"namespace": "ai-agents"
}
```
**cURL 示例**
```bash
curl -X GET "http://localhost:8000/"
```
---
### 2. 列出所有模板
获取所有可用的 Agent 模板及其所需参数。
**请求**
```
GET /templates
```
**成功响应 (200)**
```json
{
"templates": [
{
"template": "echo_agent",
"port": null,
"env_info": {}
},
{
"template": "jina_search_agent",
"port": 8080,
"env_info": {
"required": {
"JINA_API_KEY": "Jina API密钥,从 https://jina.ai/ 获取"
},
"optional": {
"SERVICE_PORT": "HTTP服务端口,默认8080",
"SERVICE_HOST": "HTTP服务监听地址,默认0.0.0.0"
}
}
},
{
"template": "mysql_agent",
"port": null,
"env_info": {
"required": {
"MYSQL_HOST": "MySQL数据库主机地址",
"MYSQL_USER": "MySQL用户名",
"MYSQL_PASSWORD": "MySQL密码",
"MYSQL_DATABASE": "MySQL数据库名",
"OPENAI_API_KEY": "OpenAI API密钥"
},
"optional": {
"MYSQL_PORT": "MySQL端口,默认3306"
}
}
}
],
"count": 7
}
```
**cURL 示例**
```bash
curl -X GET "http://localhost:8000/templates"
```
---
### 3. 获取模板详情
获取指定模板的详细信息,包括所需环境变量。
**请求**
```
GET /templates/{template_name}
```
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `template_name` | string | 是 | 模板名称 |
**成功响应 (200)**
```json
{
"template": "jina_search_agent",
"port": 8080,
"env_info": {
"required": {
"JINA_API_KEY": "Jina API密钥,从 https://jina.ai/ 获取"
},
"optional": {
"SERVICE_PORT": "HTTP服务端口,默认8080",
"SERVICE_HOST": "HTTP服务监听地址,默认0.0.0.0"
}
}
}
```
**错误响应**
| 状态码 | 说明 |
|--------|------|
| 404 | 模板不存在 |
**cURL 示例**
```bash
curl -X GET "http://localhost:8000/templates/jina_search_agent"
```
---
### 4. 创建 Agent
创建一个新的 AI Agent Pod。
**请求**
```
POST /agents
```
**请求头**
```
Content-Type: application/json
```
**请求体参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | string | 是 | Agent 名称,长度 1-63 字符 |
| `template` | string | 是 | 模板类型(见模板列表) |
| `config` | object | 否 | 配置信息(见下表) |
**config 配置参数**
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `replicas` | integer | 1 | 副本数量 |
| `cpu_request` | string | `100m` | CPU 请求量 |
| `cpu_limit` | string | `500m` | CPU 限制量 |
| `memory_request` | string | `128Mi` | 内存请求量 |
| `memory_limit` | string | `512Mi` | 内存限制量 |
| `env` | object | `{}` | 自定义环境变量 |
**请求体示例(带环境变量)**
```json
{
"name": "my-jina-agent",
"template": "jina_search_agent",
"config": {
"cpu_request": "100m",
"cpu_limit": "500m",
"memory_request": "128Mi",
"memory_limit": "512Mi",
"env": {
"JINA_API_KEY": "your-jina-api-key"
}
}
}
```
**成功响应 (200) - HTTP服务类型Agent**
```json
{
"name": "my-jina-agent",
"namespace": "ai-agents",
"status": "Pending",
"created_at": "2025-12-31T01:00:00.000000+00:00",
"template": "jina_search_agent",
"service_port": 8080,
"access_info": {
"note": "Pod IP将在Pod运行后可用,请通过 /agents/{name}/status 获取",
"port": 8080,
"endpoints": {
"root": "http://<pod_ip>:8080/",
"health": "http://<pod_ip>:8080/health"
}
}
}
```
**成功响应 (200) - 普通Agent**
```json
{
"name": "my-echo-agent",
"namespace": "ai-agents",
"status": "Pending",
"created_at": "2025-12-31T01:00:00.000000+00:00",
"template": "echo_agent"
}
```
**错误响应**
| 状态码 | 说明 |
|--------|------|
| 400 | 无效的模板类型 |
| 500 | 服务器内部错误 |
**cURL 示例**
```bash
# 创建 Jina Search Agent(带环境变量)
curl -X POST "http://localhost:8000/agents" \
-H "Content-Type: application/json" \
-d '{
"name": "my-jina-agent",
"template": "jina_search_agent",
"config": {
"env": {
"JINA_API_KEY": "your-jina-api-key"
}
}
}'
# 创建 MySQL Agent
curl -X POST "http://localhost:8000/agents" \
-H "Content-Type: application/json" \
-d '{
"name": "my-mysql-agent",
"template": "mysql_agent",
"config": {
"env": {
"MYSQL_HOST": "mysql.example.com",
"MYSQL_USER": "root",
"MYSQL_PASSWORD": "password",
"MYSQL_DATABASE": "mydb",
"OPENAI_API_KEY": "sk-xxx"
}
}
}'
```
---
### 5. 删除 Agent
删除指定的 AI Agent Pod。
**请求**
```
DELETE /agents/{agent_name}
```
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `agent_name` | string | 是 | 要删除的 Agent 名称 |
**成功响应 (200)**
```json
{
"status": "success",
"message": "Pod my-jina-agent 已删除"
}
```
**cURL 示例**
```bash
curl -X DELETE "http://localhost:8000/agents/my-jina-agent"
```
---
### 6. 获取 Agent 状态
获取指定 Agent 的详细状态信息,包括访问URL和资源使用情况(CPU、内存)。
**请求**
```
GET /agents/{agent_name}/status
```
**路径参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `agent_name` | string | 是 | Agent 名称 |
**成功响应 (200) - HTTP服务类型Agent(运行中)**
```json
{
"name": "my-jina-agent",
"namespace": "ai-agents",
"status": "Running",
"template": "jina_search_agent",
"created_at": "2025-12-31T01:00:00.000000+00:00",
"node": "aks-nodepool1-12345678-vmss000000",
"pod_ip": "10.244.0.15",
"resources": {
"requests": {
"cpu": "100m",
"memory": "128Mi"
},
"limits": {
"cpu": "500m",
"memory": "512Mi"
},
"usage": {
"cpu": "50m",
"memory": "64Mi",
"available": true
}
},
"service_port": 8080,
"access_url": "http://10.244.0.15:8080",
"endpoints": {
"root": "http://10.244.0.15:8080/",
"health": "http://10.244.0.15:8080/health"
},
"conditions": [
{
"type": "Ready",
"status": "True",
"reason": null
}
]
}
```
**resources 字段说明**
| 字段 | 说明 |
|------|------|
| `requests` | 资源请求配额(Pod 启动时保证的资源) |
| `limits` | 资源限制配额(Pod 可使用的最大资源) |
| `usage` | 实际资源使用情况(需要集群安装 metrics-server) |
**usage 字段说明**
| 字段 | 类型 | 说明 |
|------|------|------|
| `cpu` | string | 当前 CPU 使用量(如 "50m" 表示 50 毫核) |
| `memory` | string | 当前内存使用量(如 "64Mi" 表示 64 MiB) |
| `available` | boolean | 资源使用数据是否可用 |
| `reason` | string | 如果 `available` 为 false,说明原因 |
**注意**: 实际资源使用量(`usage`)需要 Kubernetes 集群安装 [metrics-server](https://github.com/kubernetes-sigs/metrics-server)。如果未安装,`usage.available` 为 `false`,并在 `usage.reason` 中说明原因。
**状态值说明**
| 状态 | 说明 |
|------|------|
| `Pending` | Pod 已被接受,但容器尚未创建 |
| `Running` | Pod 已绑定到节点,所有容器已创建 |
| `Succeeded` | Pod 中所有容器已成功终止 |
| `Failed` | Pod 中所有容器已终止,至少一个容器失败 |
| `Unknown` | 无法获取 Pod 状态 |
**cURL 示例**
```bash
curl -X GET "http://localhost:8000/agents/my-jina-agent/status"
```
---
### 7. 获取 Agent 资源使用情况
获取指定 Agent 的 CPU 和内存资源配置信息。
**请求**
```
GET /agents/{agent_name}/metrics
```
**成功响应 (200)**
```json
{
"name": "my-jina-agent",
"requests": {
"cpu": "100m",
"memory": "128Mi"
},
"limits": {
"cpu": "500m",
"memory": "512Mi"
}
}
```
**cURL 示例**
```bash
curl -X GET "http://localhost:8000/agents/my-jina-agent/metrics"
```
---
### 8. 列出所有 Agent
获取所有 AI Agent 的列表,支持按模板类型过滤。
**请求**
```
GET /agents
```
**查询参数**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `template` | string | 否 | 按模板类型过滤 |
**成功响应 (200)**
```json
{
"agents": [
{
"name": "my-jina-agent",
"status": "Running",
"template": "jina_search_agent",
"created_at": "2025-12-31T01:00:00.000000+00:00",
"pod_ip": "10.244.0.15"
}
],
"count": 1
}
```
**cURL 示例**
```bash
# 列出所有 Agent
curl -X GET "http://localhost:8000/agents"
# 按模板类型过滤
curl -X GET "http://localhost:8000/agents?template=jina_search_agent"
```
---
## 模板类型说明
### echo_agent
简单的回显 Agent,用于测试和演示。
- **镜像**: `agnettaiji.azurecr.io/ai-agents/echo-agent:latest`
- **用途**: 测试、演示、健康检查
- **所需环境变量**: 无
### chat_agent
聊天对话 Agent,支持对话交互。
- **镜像**: `agnettaiji.azurecr.io/ai-agents/chat-agent:latest`
- **用途**: 对话系统、客服机器人
- **所需环境变量**: 无
### code_agent
代码生成 Agent,支持代码相关任务。
- **镜像**: `agnettaiji.azurecr.io/ai-agents/code-agent:latest`
- **用途**: 代码生成、代码审查、代码解释
- **所需环境变量**: 无
### search_agent
搜索 Agent。
- **镜像**: `agnettaiji.azurecr.io/ai-agents/search-agent:latest`
- **用途**: 搜索功能
- **所需环境变量**: 无
### jina_search_agent
使用 Jina Reader API 获取网站内容的 HTTP 服务 Agent。
- **镜像**: `agnettaiji.azurecr.io/ai-agents/jina-search-agent:latest`
- **服务端口**: 8080
- **用途**: 网站内容抓取、网页转文本
- **所需环境变量**:
| 变量名 | 必填 | 说明 |
|--------|------|------|
| `JINA_API_KEY` | 是 | Jina API密钥 |
| `SERVICE_PORT` | 否 | HTTP服务端口,默认8080 |
| `SERVICE_HOST` | 否 | HTTP服务监听地址,默认0.0.0.0 |
- **Agent API端点**:
- `GET /` - 服务信息和所需参数
- `GET /health` - 健康检查
- `POST /search` - 搜索网站内容
- `GET /fetch?url=<url>` - 快速获取网站内容
### mysql_agent
MySQL 数据库查询 Agent,使用 LangChain 实现。
- **镜像**: `agnettaiji.azurecr.io/ai-agents/mysql-agent:latest`
- **用途**: MySQL 数据库自然语言查询
- **所需环境变量**:
| 变量名 | 必填 | 说明 |
|--------|------|------|
| `MYSQL_HOST` | 是 | MySQL数据库主机地址 |
| `MYSQL_USER` | 是 | MySQL用户名 |
| `MYSQL_PASSWORD` | 是 | MySQL密码 |
| `MYSQL_DATABASE` | 是 | MySQL数据库名 |
| `OPENAI_API_KEY` | 是 | OpenAI API密钥 |
| `MYSQL_PORT` | 否 | MySQL端口,默认3306 |
### postgresql_agent
PostgreSQL 数据库查询 Agent,使用 LangChain 实现。
- **镜像**: `agnettaiji.azurecr.io/ai-agents/postgresql-agent:latest`
- **用途**: PostgreSQL 数据库自然语言查询
- **所需环境变量**:
| 变量名 | 必填 | 说明 |
|--------|------|------|
| `POSTGRES_HOST` | 是 | PostgreSQL数据库主机地址 |
| `POSTGRES_USER` | 是 | PostgreSQL用户名 |
| `POSTGRES_PASSWORD` | 是 | PostgreSQL密码 |
| `POSTGRES_DATABASE` | 是 | PostgreSQL数据库名 |
| `OPENAI_API_KEY` | 是 | OpenAI API密钥 |
| `POSTGRES_PORT` | 否 | PostgreSQL端口,默认5432 |
---
## 完整使用流程示例
### 场景:创建并使用 Jina Search Agent
```python
import requests
import time
BASE_URL = "http://localhost:8000"
# 1. 查看模板所需参数
print("=== 查看模板信息 ===")
response = requests.get(f"{BASE_URL}/templates/jina_search_agent")
template_info = response.json()
print(f"所需环境变量: {template_info['env_info']}")
# 2. 创建 Agent
print("\n=== 创建 Agent ===")
response = requests.post(
f"{BASE_URL}/agents",
json={
"name": "my-jina-agent",
"template": "jina_search_agent",
"config": {
"env": {
"JINA_API_KEY": "your-jina-api-key"
}
}
}
)
create_result = response.json()
print(f"创建结果: {create_result}")
# 3. 等待 Agent 运行
print("\n=== 等待 Agent 启动 ===")
for i in range(30):
response = requests.get(f"{BASE_URL}/agents/my-jina-agent/status")
status = response.json()
if status.get("status") == "Running":
print(f"Agent 已启动!")
print(f"访问地址: {status.get('access_url')}")
break
print(f"当前状态: {status.get('status')}, 等待中...")
time.sleep(2)
# 4. 调用 Agent 服务
if status.get("access_url"):
agent_url = status["access_url"]
# 查看 Agent 信息
print("\n=== Agent 服务信息 ===")
response = requests.get(f"{agent_url}/")
print(response.json())
# 搜索网站内容
print("\n=== 搜索网站内容 ===")
response = requests.post(
f"{agent_url}/search",
json={"url": "https://www.example.com"}
)
print(response.json())
# 5. 删除 Agent
print("\n=== 删除 Agent ===")
response = requests.delete(f"{BASE_URL}/agents/my-jina-agent")
print(response.json())
```
---
## 错误处理
### 通用错误响应格式
```json
{
"detail": "错误描述信息"
}
```
### HTTP 状态码
| 状态码 | 说明 |
|--------|------|
| 200 | 请求成功 |
| 400 | 请求参数错误 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
---
## OpenAPI/Swagger 文档
FastAPI 自动生成交互式 API 文档:
- **Swagger UI**: `http://localhost:8000/docs`
- **ReDoc**: `http://localhost:8000/redoc`
- **OpenAPI JSON**: `http://localhost:8000/openapi.json`
---
## 注意事项
1. **Agent 名称规范**: 名称必须符合 Kubernetes 命名规范(小写字母、数字、连字符,长度 1-63)
2. **资源配额**: 请根据实际需求配置 CPU 和内存,避免资源浪费或不足
3. **命名空间**: 所有 Agent 默认部署在 `ai-agents` 命名空间
4. **镜像拉取**: 需要配置 ACR 密钥(`acr-secret`)才能拉取私有镜像
5. **网络访问**: 服务默认监听所有网络接口(0.0.0.0),生产环境请注意安全配置
6. **环境变量安全**: 敏感信息(如 API 密钥)应通过安全方式传递,避免在日志中暴露
7. **资源监控**: 获取 Agent 实际 CPU/内存使用量需要集群安装 [metrics-server](https://github.com/kubernetes-sigs/metrics-server)。安装命令:
```bash
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
```
+185
View File
@@ -0,0 +1,185 @@
# Jina Search Agent 实施计划
## 概述
创建一个基于Jina Reader API的网站内容搜索Agent,以HTTP服务模式运行,接收用户请求后调用Jina API搜索网站内容并返回结果。Agent将被打包成Docker镜像并部署到Azure Container Registry (ACR)。
## 架构设计
```mermaid
flowchart LR
subgraph User
A[用户请求]
end
subgraph K8s Cluster
B[Agent Manager]
C[Jina Search Agent Pod]
end
subgraph External
D[Jina Reader API]
E[目标网站]
end
A --> B
B -->|创建/管理| C
C -->|HTTP请求| D
D -->|抓取内容| E
E -->|返回内容| D
D -->|返回结果| C
C -->|响应| A
```
## 环境变量设计
| 变量名 | 必填 | 默认值 | 说明 |
|--------|------|--------|------|
| `JINA_API_KEY` | 是 | - | Jina API密钥 |
| `POD_NAME` | 否 | unknown | Pod名称,由K8s注入 |
| `TEMPLATE_TYPE` | 否 | jina_search_agent | 模板类型标识 |
| `SERVICE_PORT` | 否 | 8080 | HTTP服务端口 |
| `SERVICE_HOST` | 否 | 0.0.0.0 | HTTP服务监听地址 |
## API设计
### 1. 健康检查
```
GET /health
```
返回服务状态
### 2. 搜索网站内容
```
POST /search
Content-Type: application/json
{
"url": "https://www.example.com",
"options": {
"timeout": 30
}
}
```
### 3. 直接获取URL内容
```
GET /fetch?url=https://www.example.com
```
## 文件结构
```
agent_templates/
├── jina_search_agent.py # Agent主程序
├── jina_search_agent.Dockerfile # Docker构建文件
└── build_and_push.sh # 更新构建脚本
```
## 实施步骤
### 阶段1: 创建Jina Search Agent代码
- [x] 创建 `agent_templates/jina_search_agent.py`
- 实现FastAPI HTTP服务
- 实现 `/health` 健康检查端点
- 实现 `/search` POST端点,接收URL并调用Jina API
- 实现 `/fetch` GET端点,快速获取URL内容
- 使用环境变量配置JINA_API_KEY
- 添加错误处理和日志记录
### 阶段2: 创建Dockerfile
- [x] 创建 `agent_templates/jina_search_agent.Dockerfile`
- 基于 python:3.11-slim
- 安装必要依赖:fastapi, uvicorn, requests
- 设置环境变量
- 暴露服务端口
### 阶段3: 更新Agent Manager
- [x] 更新 `k8s_manager.py`
- 在 `image_map` 中添加 `jina_search_agent` 映射
- 添加环境变量注入支持(JINA_API_KEY等)
- [x] 更新 `app.py`
- 在 `valid_templates` 列表中添加 `jina_search_agent`
- 支持在创建Agent时传入自定义环境变量
### 阶段4: 更新构建脚本
- [x] 更新 `agent_templates/build_and_push.sh`
- 添加Jina Search Agent的构建和推送命令
### 阶段5: 构建并推送Docker镜像
- [x] 登录ACR: `az acr login --name agnettaiji`
- [x] 构建镜像: `docker build -f jina_search_agent.Dockerfile -t agnettaiji.azurecr.io/ai-agents/jina-search-agent:latest .`
- [x] 推送镜像: `docker push agnettaiji.azurecr.io/ai-agents/jina-search-agent:latest`
### 阶段6: 测试验证
- [ ] 本地测试Agent代码
- [ ] 通过Agent Manager API创建Jina Search Agent
- [ ] 验证Agent Pod正常运行
- [ ] 测试搜索功能
## 代码示例
### jina_search_agent.py 核心逻辑
```python
import os
import requests
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
JINA_API_KEY = os.getenv("JINA_API_KEY", "")
JINA_BASE_URL = "https://r.jina.ai"
app = FastAPI(title="Jina Search Agent")
class SearchRequest(BaseModel):
url: str
options: dict = {}
@app.post("/search")
async def search(request: SearchRequest):
headers = {"Authorization": f"Bearer {JINA_API_KEY}"}
response = requests.get(
f"{JINA_BASE_URL}/{request.url}",
headers=headers,
timeout=request.options.get("timeout", 30)
)
return {"content": response.text, "status_code": response.status_code}
```
### 创建Agent时传入环境变量
```bash
curl -X POST "http://localhost:8000/agents" \
-H "Content-Type: application/json" \
-d '{
"name": "my-jina-agent",
"template": "jina_search_agent",
"config": {
"env": {
"JINA_API_KEY": "your-jina-api-key"
}
}
}'
```
## 注意事项
1. **安全性**: JINA_API_KEY应通过Kubernetes Secret或环境变量安全传递
2. **超时处理**: Jina API调用可能需要较长时间,需要合理设置超时
3. **错误处理**: 需要处理网络错误、API限流等情况
4. **日志记录**: 记录所有请求和响应,便于调试
## 后续扩展
- 支持批量URL搜索
- 添加缓存机制
- 支持更多Jina API参数(如代理、自定义headers等)
- 集成LangChain实现更复杂的搜索Agent
+7
View File
@@ -0,0 +1,7 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
kubernetes==28.1.0
pydantic==2.5.0
python-dotenv==1.0.0
sqlalchemy==2.0.23
psycopg2-binary==2.9.9
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""
汇总测试平台中由 agent-manager 管理的 agents 数量及资源使用(请求/限制/实际使用)
用法示例:
python3 scripts/aggregate_agents_resources.py --namespace ai-agents
注意:
- 需要在运行环境中能访问 Kubernetes 集群(in-cluster 或提供 KUBECONFIG)
- 若想使用实际资源使用(usage),需要集群安装 metrics-server
"""
import argparse
import sys
import os
# ensure repo root is on sys.path so we can import k8s_manager when running from scripts/
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
from k8s_manager import K8sManager
from typing import Optional
def parse_cpu(s: Optional[str]) -> float:
if not s:
return 0.0
s = str(s).strip()
try:
if s.endswith('m'):
return float(s[:-1]) / 1000.0
return float(s)
except ValueError:
return 0.0
def parse_memory(s: Optional[str]) -> float:
"""解析内存字符串并返回Mi单位的浮点数值"""
if not s:
return 0.0
s = str(s).strip()
units = {
'Ki': 1024.0,
'Mi': 1024.0 ** 2,
'Gi': 1024.0 ** 3,
'Ti': 1024.0 ** 4,
'K': 1000.0,
'M': 1000.0 ** 2,
'G': 1000.0 ** 3,
}
# 直接 numeric
try:
return float(s) / (1024.0 ** 2)
except Exception:
pass
for u, factor in units.items():
if s.endswith(u):
try:
num = float(s[:-len(u)])
# 返回 Mi 为单位
return (num * factor) / (1024.0 ** 2)
except Exception:
return 0.0
# 未知单位,尝试移除非数字字符
num = ''.join(ch for ch in s if (ch.isdigit() or ch == '.' ))
try:
return float(num) / (1024.0 ** 2)
except Exception:
return 0.0
def human_mem(mib: float) -> str:
if mib >= 1024:
return f"{mib/1024:.2f} GiB"
return f"{mib:.1f} MiB"
def human_cpu(cores: float) -> str:
if cores < 1:
return f"{int(cores*1000)} m"
return f"{cores:.3f} cores"
def aggregate(namespace: str, kubeconfig: Optional[str], detailed: bool = False):
mgr = K8sManager(namespace=namespace, kubeconfig_path=kubeconfig)
pods = mgr.list_pods()
total = len(pods)
sum_req_cpu = 0.0
sum_lim_cpu = 0.0
sum_usage_cpu = 0.0
have_usage_cpu = False
sum_req_mem = 0.0
sum_lim_mem = 0.0
sum_usage_mem = 0.0
have_usage_mem = False
details = []
for p in pods:
name = p.get('name')
status = mgr.get_pod_status(name)
# requests/limits
resources = status.get('resources', {})
requests = resources.get('requests', {}) or {}
limits = resources.get('limits', {}) or {}
r_cpu = parse_cpu(requests.get('cpu'))
l_cpu = parse_cpu(limits.get('cpu'))
sum_req_cpu += r_cpu
sum_lim_cpu += l_cpu
r_mem = parse_memory(requests.get('memory'))
l_mem = parse_memory(limits.get('memory'))
sum_req_mem += r_mem
sum_lim_mem += l_mem
usage = resources.get('usage') or {}
u_cpu = parse_cpu(usage.get('cpu'))
u_mem = parse_memory(usage.get('memory'))
if u_cpu:
have_usage_cpu = True
sum_usage_cpu += u_cpu
if u_mem:
have_usage_mem = True
sum_usage_mem += u_mem
details.append({
'name': name,
'status': status.get('status'),
'template': status.get('template'),
'req_cpu': r_cpu,
'lim_cpu': l_cpu,
'use_cpu': u_cpu,
'req_mem_mi': r_mem,
'lim_mem_mi': l_mem,
'use_mem_mi': u_mem,
})
# 输出
print(f"Agents 总数: {total}")
print("")
print("CPU 总计:")
print(f" 请求 (requests): {human_cpu(sum_req_cpu)}")
print(f" 限制 (limits): {human_cpu(sum_lim_cpu)}")
if have_usage_cpu:
print(f" 实际使用 (usage): {human_cpu(sum_usage_cpu)}")
else:
print(" 实际使用 (usage): 未获取(需安装 metrics-server 或 无法访问 metrics API)")
print("")
print("内存 总计:")
print(f" 请求 (requests): {human_mem(sum_req_mem)}")
print(f" 限制 (limits): {human_mem(sum_lim_mem)}")
if have_usage_mem:
print(f" 实际使用 (usage): {human_mem(sum_usage_mem)}")
else:
print(" 实际使用 (usage): 未获取(需安装 metrics-server 或 无法访问 metrics API)")
if detailed:
print('\n每个 Pod 详情:')
for d in details:
print(f"- {d['name']}: status={d['status']}, template={d['template']}, req={human_cpu(d['req_cpu'])}/{human_mem(d['req_mem_mi'])}, lim={human_cpu(d['lim_cpu'])}/{human_mem(d['lim_mem_mi'])}, use={human_cpu(d['use_cpu'])}/{human_mem(d['use_mem_mi'])}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--namespace', '-n', default='ai-agents', help='Kubernetes namespace')
parser.add_argument('--kubeconfig', '-k', default=None, help='可选 kubeconfig 文件路径')
parser.add_argument('--detailed', '-d', action='store_true', help='输出每个 pod 的详细资源信息')
args = parser.parse_args()
aggregate(args.namespace, args.kubeconfig, args.detailed)
if __name__ == '__main__':
main()
+198
View File
@@ -0,0 +1,198 @@
#!/bin/bash
# 数据库初始化和数据迁移脚本
set -e
echo "Agent Manager - Database Initialization and Migration"
echo "======================================================"
# 1. 备份现有数据(如果存在)
if [ -f "agent_manager.db" ]; then
echo "Backing up existing database..."
cp agent_manager.db agent_manager.db.backup.$(date +%Y%m%d_%H%M%S)
fi
# 2. 初始化数据库(如果不存在)
echo "Initializing database..."
python3 << 'EOF'
from database import init_db, SessionLocal, Template, Quota, AgentType
from datetime import datetime
# 创建所有表
init_db()
print("✓ Database tables created")
# 创建示例模板数据
db = SessionLocal()
try:
# 检查是否已有模板
existing_templates = db.query(Template).count()
if existing_templates == 0:
print("\nCreating default templates...")
# MySQL Agent (Platform)
mysql_template = Template(
name="mysql_agent",
display_name="MySQL Query Agent",
description="Platform MySQL database query agent using LangChain",
agent_type=AgentType.PLATFORM,
image="agnettaiji.azurecr.io/mysql_agent:latest",
port=None,
env_requirements={
"required": {
"MYSQL_HOST": "MySQL server hostname",
"MYSQL_USER": "MySQL username",
"MYSQL_PASSWORD": "MySQL password",
"MYSQL_DATABASE": "MySQL database name",
"OPENAI_API_KEY": "OpenAI API key for LangChain"
},
"optional": {
"MYSQL_PORT": "MySQL port (default: 3306)"
}
},
cpu_request="100m",
cpu_limit="500m",
memory_request="256Mi",
memory_limit="512Mi",
min_replicas=1,
max_replicas=3,
target_cpu_utilization=80
)
db.add(mysql_template)
# PostgreSQL Agent (Custom)
pg_template = Template(
name="postgresql_agent",
display_name="PostgreSQL Query Agent",
description="Custom PostgreSQL database query agent using LangChain",
agent_type=AgentType.CUSTOM,
image="agnettaiji.azurecr.io/postgresql_agent:latest",
port=None,
env_requirements={
"required": {
"POSTGRES_HOST": "PostgreSQL server hostname",
"POSTGRES_USER": "PostgreSQL username",
"POSTGRES_PASSWORD": "PostgreSQL password",
"POSTGRES_DATABASE": "PostgreSQL database name",
"OPENAI_API_KEY": "OpenAI API key for LangChain"
},
"optional": {
"POSTGRES_PORT": "PostgreSQL port (default: 5432)"
}
},
cpu_request="100m",
cpu_limit="500m",
memory_request="256Mi",
memory_limit="512Mi",
min_replicas=1,
max_replicas=5,
target_cpu_utilization=80
)
db.add(pg_template)
# Jina Search Agent (Custom with HTTP service)
jina_template = Template(
name="jina_search_agent",
display_name="Jina Search Agent",
description="Custom web search agent using Jina Reader API",
agent_type=AgentType.CUSTOM,
image="agnettaiji.azurecr.io/jina_search_agent:latest",
port=8080,
env_requirements={
"required": {
"JINA_API_KEY": "Jina API key"
},
"optional": {
"SERVICE_PORT": "HTTP service port (default: 8080)",
"SERVICE_HOST": "HTTP service host (default: 0.0.0.0)"
}
},
cpu_request="100m",
cpu_limit="500m",
memory_request="128Mi",
memory_limit="256Mi",
min_replicas=1,
max_replicas=5,
target_cpu_utilization=80
)
db.add(jina_template)
# Echo Agent (Platform - simple example)
echo_template = Template(
name="echo_agent",
display_name="Echo Agent",
description="Simple platform echo agent for testing",
agent_type=AgentType.PLATFORM,
image="busybox:latest",
port=None,
env_requirements={},
cpu_request="50m",
cpu_limit="100m",
memory_request="64Mi",
memory_limit="128Mi",
min_replicas=1,
max_replicas=2,
target_cpu_utilization=80
)
db.add(echo_template)
db.commit()
print("✓ Created 4 default templates")
# 创建默认配额
print("\nCreating default quotas...")
# 默认管理员配额
admin_quota = Quota(
owner_type="admin",
owner_id="admin",
platform_pod_quota=100,
platform_pod_used=0,
custom_cpu_quota=50.0,
custom_cpu_used=0.0,
custom_memory_quota=102400.0, # 100GB
custom_memory_used=0.0
)
db.add(admin_quota)
# 默认租户配额
tenant_quota = Quota(
owner_type="tenant",
owner_id="default_tenant",
platform_pod_quota=10,
platform_pod_used=0,
custom_cpu_quota=5.0,
custom_cpu_used=0.0,
custom_memory_quota=10240.0, # 10GB
custom_memory_used=0.0
)
db.add(tenant_quota)
db.commit()
print("✓ Created default quotas")
else:
print(f"\n✓ Database already contains {existing_templates} templates")
print("\n✅ Database initialization completed successfully!")
except Exception as e:
print(f"\n❌ Error: {str(e)}")
db.rollback()
raise
finally:
db.close()
EOF
echo ""
echo "======================================================"
echo "Database initialized at: agent_manager.db"
echo ""
echo "Next steps:"
echo "1. Review database content: sqlite3 agent_manager.db"
echo "2. Start the service: python app_new.py"
echo "3. Test API: curl http://localhost:8000/templates"
echo "======================================================"
Executable
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# 快速启动脚本 - 本地开发环境
echo "🚀 启动AI Agent Manager服务..."
echo ""
# 检查Python虚拟环境
if [ ! -d "venv" ]; then
echo "创建Python虚拟环境..."
python3 -m venv venv
fi
# 激活虚拟环境
source venv/bin/activate
# 安装依赖
echo "安装依赖..."
pip install -q -r requirements.txt
echo ""
echo "✅ 环境准备完成"
echo ""
echo "启动服务在 http://localhost:8000"
echo "API文档: http://localhost:8000/docs"
echo ""
# 启动服务
python app.py
+247
View File
@@ -0,0 +1,247 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
+70
View File
@@ -0,0 +1,70 @@
# This file must be used with "source bin/activate" *from bash*
# You cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
# transform D:\path\to\venv to /d/path/to/venv on MSYS
# and to /cygdrive/d/path/to/venv on Cygwin
export VIRTUAL_ENV=$(cygpath /home/taiji/tools/agent-manager/test_venv)
else
# use the path as-is
export VIRTUAL_ENV=/home/taiji/tools/agent-manager/test_venv
fi
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"bin":$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1='(test_venv) '"${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT='(test_venv) '
export VIRTUAL_ENV_PROMPT
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
+27
View File
@@ -0,0 +1,27 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /home/taiji/tools/agent-manager/test_venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = '(test_venv) '"$prompt"
setenv VIRTUAL_ENV_PROMPT '(test_venv) '
endif
alias pydoc python -m pydoc
rehash
+69
View File
@@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /home/taiji/tools/agent-manager/test_venv
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) '(test_venv) ' (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT '(test_venv) '
end
+8
View File
@@ -0,0 +1,8 @@
#!/home/taiji/tools/agent-manager/test_venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from charset_normalizer.cli import cli_detect
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli_detect())
+8
View File
@@ -0,0 +1,8 @@
#!/home/taiji/tools/agent-manager/test_venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/home/taiji/tools/agent-manager/test_venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/home/taiji/tools/agent-manager/test_venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+1
View File
@@ -0,0 +1 @@
python3
+1
View File
@@ -0,0 +1 @@
/usr/bin/python3
+1
View File
@@ -0,0 +1 @@
python3
+1
View File
@@ -0,0 +1 @@
lib
+5
View File
@@ -0,0 +1,5 @@
home = /usr/bin
include-system-site-packages = false
version = 3.12.3
executable = /usr/bin/python3.12
command = /usr/bin/python3 -m venv /home/taiji/tools/agent-manager/test_venv
+90
View File
@@ -0,0 +1,90 @@
"""
测试脚本 - 创建AI Agent
"""
import requests
import json
import sys
# 配置
BASE_URL = "http://localhost:8000"
def test_create_agent():
"""测试创建Agent"""
print("=" * 50)
print("测试: 创建AI Agent")
print("=" * 50)
# 测试数据
test_cases = [
{
"name": "test-echo-1",
"template": "echo_agent",
"config": {
"replicas": 1,
"cpu_request": "100m",
"cpu_limit": "500m",
"memory_request": "128Mi",
"memory_limit": "512Mi"
}
},
{
"name": "test-chat-1",
"template": "chat_agent",
"config": {
"replicas": 1,
"cpu_request": "200m",
"cpu_limit": "1000m",
"memory_request": "256Mi",
"memory_limit": "1Gi"
}
},
{
"name": "test-code-1",
"template": "code_agent",
"config": {
"replicas": 1
}
}
]
for i, test_data in enumerate(test_cases, 1):
print(f"\n测试用例 {i}: 创建 {test_data['name']}")
print(f"模板: {test_data['template']}")
print(f"配置: {json.dumps(test_data['config'], indent=2)}")
try:
response = requests.post(
f"{BASE_URL}/agents",
json=test_data,
headers={"Content-Type": "application/json"}
)
print(f"\n状态码: {response.status_code}")
if response.status_code == 200:
result = response.json()
print("✅ 创建成功!")
print(f"响应: {json.dumps(result, indent=2, ensure_ascii=False)}")
else:
print(f"❌ 创建失败!")
print(f"错误: {response.text}")
except Exception as e:
print(f"❌ 请求失败: {str(e)}")
print("-" * 50)
if __name__ == "__main__":
try:
# 先检查服务是否可用
print("检查服务状态...")
response = requests.get(f"{BASE_URL}")
print(f"服务状态: {response.json()}\n")
test_create_agent()
except requests.exceptions.ConnectionError:
print("❌ 无法连接到服务。请确保服务正在运行: python app.py")
sys.exit(1)
+64
View File
@@ -0,0 +1,64 @@
"""
测试脚本 - 删除Agent
"""
import requests
import json
import sys
# 配置
BASE_URL = "http://localhost:8000"
def test_delete_agent():
"""测试删除Agent"""
print("=" * 50)
print("测试: 删除AI Agent")
print("=" * 50)
# 测试的Agent名称列表
agent_names = ["test-echo-1", "test-chat-1", "test-code-1"]
for agent_name in agent_names:
print(f"\n删除Agent: {agent_name}")
try:
response = requests.delete(f"{BASE_URL}/agents/{agent_name}")
print(f"状态码: {response.status_code}")
if response.status_code == 200:
result = response.json()
print("✅ 删除成功!")
print(f"响应: {json.dumps(result, indent=2, ensure_ascii=False)}")
elif response.status_code == 404:
print(f"⚠️ Agent不存在")
print(f"错误: {response.text}")
else:
print(f"❌ 删除失败!")
print(f"错误: {response.text}")
except Exception as e:
print(f"❌ 请求失败: {str(e)}")
print("-" * 50)
if __name__ == "__main__":
try:
# 先检查服务是否可用
print("检查服务状态...")
response = requests.get(f"{BASE_URL}/")
print(f"服务状态: {response.json()}\n")
# 警告
print("⚠️ 警告: 此脚本将删除测试Agents!")
confirm = input("确认继续? (yes/no): ")
if confirm.lower() == "yes":
test_delete_agent()
else:
print("已取消")
except requests.exceptions.ConnectionError:
print("❌ 无法连接到服务。请确保服务正在运行: python app.py")
sys.exit(1)
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""
Test script to verify environment variable passing to agent pods
"""
import requests
import json
import sys
API_URL = "http://localhost:8000"
def test_create_mysql_agent_with_env():
"""Test creating a MySQL agent with environment variables"""
payload = {
"name": "test-mysql-agent",
"template": "mysql_agent",
"env": {
"MYSQL_HOST": "test-mysql-server.mysql.database.azure.com",
"MYSQL_PORT": "3306",
"MYSQL_USER": "testuser",
"MYSQL_PASSWORD": "testpass",
"MYSQL_DATABASE": "testdb",
"OPENAI_API_KEY": "sk-test-key"
},
"config": {
"cpu_request": "100m",
"memory_request": "128Mi"
}
}
print("Creating MySQL agent with environment variables...")
print(f"Payload: {json.dumps(payload, indent=2)}")
try:
response = requests.post(f"{API_URL}/agents", json=payload)
print(f"\nStatus Code: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
if response.status_code == 200:
print("\n✅ Agent created successfully!")
return True
else:
print("\n❌ Agent creation failed!")
return False
except Exception as e:
print(f"\n❌ Error: {str(e)}")
return False
def test_get_agent_status():
"""Test getting agent status"""
agent_name = "test-mysql-agent"
print(f"\nGetting status for agent: {agent_name}")
try:
response = requests.get(f"{API_URL}/agents/{agent_name}/status")
print(f"Status Code: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
if response.status_code == 200:
print("\n✅ Status retrieved successfully!")
return True
else:
print("\n❌ Failed to get status!")
return False
except Exception as e:
print(f"\n❌ Error: {str(e)}")
return False
def test_get_template_info():
"""Test getting template information"""
template = "mysql_agent"
print(f"\nGetting template info for: {template}")
try:
response = requests.get(f"{API_URL}/templates/{template}")
print(f"Status Code: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
if response.status_code == 200:
print("\n✅ Template info retrieved successfully!")
return True
else:
print("\n❌ Failed to get template info!")
return False
except Exception as e:
print(f"\n❌ Error: {str(e)}")
return False
def cleanup_test_agent():
"""Cleanup test agent"""
agent_name = "test-mysql-agent"
print(f"\nCleaning up test agent: {agent_name}")
try:
response = requests.delete(f"{API_URL}/agents/{agent_name}")
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
print("✅ Test agent cleaned up!")
else:
print("⚠️ Cleanup may have failed (agent might not exist)")
except Exception as e:
print(f"⚠️ Cleanup error: {str(e)}")
if __name__ == "__main__":
print("=" * 60)
print("Testing Agent Manager API - Environment Variables")
print("=" * 60)
# Test 1: Get template info
test_get_template_info()
print("\n" + "=" * 60)
# Test 2: Create agent with env vars
test_create_mysql_agent_with_env()
print("\n" + "=" * 60)
# Wait a bit for pod to be created
import time
print("\nWaiting 5 seconds for pod creation...")
time.sleep(5)
# Test 3: Get agent status
test_get_agent_status()
print("\n" + "=" * 60)
# Cleanup
cleanup_test_agent()
print("\n" + "=" * 60)
print("Tests completed!")
print("=" * 60)
+57
View File
@@ -0,0 +1,57 @@
"""
测试脚本 - 获取Agent资源使用情况
"""
import requests
import json
import sys
# 配置
BASE_URL = "http://localhost:8000"
def test_get_agent_metrics():
"""测试获取Agent资源信息"""
print("=" * 50)
print("测试: 获取AI Agent资源使用情况")
print("=" * 50)
# 测试的Agent名称列表
agent_names = ["test-echo-1", "test-chat-1", "test-code-1"]
for agent_name in agent_names:
print(f"\n查询Agent资源: {agent_name}")
try:
response = requests.get(f"{BASE_URL}/agents/{agent_name}/metrics")
print(f"状态码: {response.status_code}")
if response.status_code == 200:
result = response.json()
print("✅ 查询成功!")
print(f"响应: {json.dumps(result, indent=2, ensure_ascii=False)}")
elif response.status_code == 404:
print(f"⚠️ Agent不存在")
print(f"错误: {response.text}")
else:
print(f"❌ 查询失败!")
print(f"错误: {response.text}")
except Exception as e:
print(f"❌ 请求失败: {str(e)}")
print("-" * 50)
if __name__ == "__main__":
try:
# 先检查服务是否可用
print("检查服务状态...")
response = requests.get(f"{BASE_URL}/")
print(f"服务状态: {response.json()}\n")
test_get_agent_metrics()
except requests.exceptions.ConnectionError:
print("❌ 无法连接到服务。请确保服务正在运行: python app.py")
sys.exit(1)
+57
View File
@@ -0,0 +1,57 @@
"""
测试脚本 - 获取Agent状态
"""
import requests
import json
import sys
# 配置
BASE_URL = "http://localhost:8000"
def test_get_agent_status():
"""测试获取Agent状态"""
print("=" * 50)
print("测试: 获取AI Agent状态")
print("=" * 50)
# 测试的Agent名称列表
agent_names = ["test-echo-1", "test-chat-1", "test-code-1"]
for agent_name in agent_names:
print(f"\n查询Agent: {agent_name}")
try:
response = requests.get(f"{BASE_URL}/agents/{agent_name}/status")
print(f"状态码: {response.status_code}")
if response.status_code == 200:
result = response.json()
print("✅ 查询成功!")
print(f"响应: {json.dumps(result, indent=2, ensure_ascii=False)}")
elif response.status_code == 404:
print(f"⚠️ Agent不存在")
print(f"错误: {response.text}")
else:
print(f"❌ 查询失败!")
print(f"错误: {response.text}")
except Exception as e:
print(f"❌ 请求失败: {str(e)}")
print("-" * 50)
if __name__ == "__main__":
try:
# 先检查服务是否可用
print("检查服务状态...")
response = requests.get(f"{BASE_URL}/")
print(f"服务状态: {response.json()}\n")
test_get_agent_status()
except requests.exceptions.ConnectionError:
print("❌ 无法连接到服务。请确保服务正在运行: python app.py")
sys.exit(1)
+72
View File
@@ -0,0 +1,72 @@
"""
测试脚本 - 列出所有Agents
"""
import requests
import json
import sys
# 配置
BASE_URL = "http://localhost:8000"
def test_list_agents():
"""测试列出所有Agent"""
print("=" * 50)
print("测试: 列出所有AI Agents")
print("=" * 50)
# 测试1: 列出所有agents
print("\n测试1: 列出所有Agents")
try:
response = requests.get(f"{BASE_URL}/agents")
print(f"状态码: {response.status_code}")
if response.status_code == 200:
result = response.json()
print("✅ 查询成功!")
print(f"找到 {result['count']} 个Agents")
print(f"响应: {json.dumps(result, indent=2, ensure_ascii=False)}")
else:
print(f"❌ 查询失败!")
print(f"错误: {response.text}")
except Exception as e:
print(f"❌ 请求失败: {str(e)}")
print("-" * 50)
# 测试2: 按模板类型过滤
templates = ["echo_agent", "chat_agent", "code_agent"]
for template in templates:
print(f"\n测试2: 列出模板为 {template} 的Agents")
try:
response = requests.get(f"{BASE_URL}/agents?template={template}")
print(f"状态码: {response.status_code}")
if response.status_code == 200:
result = response.json()
print("✅ 查询成功!")
print(f"找到 {result['count']} 个 {template} Agents")
print(f"响应: {json.dumps(result, indent=2, ensure_ascii=False)}")
else:
print(f"❌ 查询失败!")
print(f"错误: {response.text}")
except Exception as e:
print(f"❌ 请求失败: {str(e)}")
print("-" * 50)
if __name__ == "__main__":
try:
# 先检查服务是否可用
print("检查服务状态...")
response = requests.get(f"{BASE_URL}/")
print(f"服务状态: {response.json()}\n")
test_list_agents()
except requests.exceptions.ConnectionError:
print("❌ 无法连接到服务。请确保服务正在运行: python app.py")
sys.exit(1)
+328
View File
@@ -0,0 +1,328 @@
#!/usr/bin/env python3
"""
Agent Manager v2.0 - 综合测试脚本
测试新架构的所有主要功能
"""
import requests
import json
import time
import sys
BASE_URL = "http://localhost:8000"
def print_section(title):
"""打印测试章节标题"""
print(f"\n{'='*60}")
print(f" {title}")
print(f"{'='*60}\n")
def test_health_checks():
"""测试健康检查端点"""
print_section("1. 健康检查测试")
# 根端点
response = requests.get(f"{BASE_URL}/")
print(f"GET / : {response.status_code}")
print(json.dumps(response.json(), indent=2))
# 健康检查
response = requests.get(f"{BASE_URL}/health")
print(f"\nGET /health : {response.status_code}")
print(json.dumps(response.json(), indent=2))
# 就绪检查
response = requests.get(f"{BASE_URL}/ready")
print(f"\nGET /ready : {response.status_code}")
print(json.dumps(response.json(), indent=2))
def test_template_management():
"""测试模板管理"""
print_section("2. 模板管理测试")
# 列出所有模板
response = requests.get(f"{BASE_URL}/templates")
print(f"GET /templates : {response.status_code}")
templates = response.json()
print(f"找到 {len(templates)} 个模板")
for template in templates:
print(f" - {template['name']} ({template['agent_type']}): {template['display_name']}")
# 获取特定模板
if templates:
template_name = templates[0]['name']
response = requests.get(f"{BASE_URL}/templates/{template_name}")
print(f"\nGET /templates/{template_name} : {response.status_code}")
print(json.dumps(response.json(), indent=2))
# 创建自定义模板
print("\n创建自定义模板...")
new_template = {
"name": "test_custom_agent",
"display_name": "Test Custom Agent",
"description": "Test agent for v2.0 testing",
"agent_type": "custom",
"image": "nginx:latest",
"port": 80,
"env_requirements": {
"required": {
"API_KEY": "API key for testing"
},
"optional": {
"DEBUG": "Debug mode flag"
}
},
"cpu_request": "100m",
"cpu_limit": "200m",
"memory_request": "128Mi",
"memory_limit": "256Mi",
"min_replicas": 1,
"max_replicas": 3,
"target_cpu_utilization": 75
}
response = requests.post(f"{BASE_URL}/templates", json=new_template)
print(f"POST /templates : {response.status_code}")
if response.status_code == 201:
print("✓ 模板创建成功")
print(json.dumps(response.json(), indent=2))
elif response.status_code == 409:
print("✓ 模板已存在(预期行为)")
else:
print(f"✗ 创建失败: {response.text}")
def test_platform_agents():
"""测试平台Agent"""
print_section("3. 平台Agent测试")
# 创建平台Agent
print("创建平台Agent...")
platform_agent = {
"name": "test-platform-echo",
"template_name": "echo_agent",
"owner_id": "test_user_001",
"channel_id": "test_channel",
"tenant_id": "test_tenant"
}
response = requests.post(f"{BASE_URL}/platform-agents", json=platform_agent)
print(f"POST /platform-agents : {response.status_code}")
if response.status_code == 201:
print("✓ 平台Agent创建成功")
print(json.dumps(response.json(), indent=2))
agent_created = True
elif response.status_code == 409:
print("✓ Agent已存在(预期行为)")
agent_created = False
else:
print(f"✗ 创建失败: {response.text}")
agent_created = False
# 等待一下让Kubernetes创建资源
if agent_created:
print("\n等待5秒让Kubernetes创建资源...")
time.sleep(5)
# 列出平台Agents
print("\n列出所有平台Agents...")
response = requests.get(f"{BASE_URL}/platform-agents")
print(f"GET /platform-agents : {response.status_code}")
agents = response.json()
print(f"找到 {len(agents)} 个平台Agents")
for agent in agents:
print(f" - {agent['name']}: {agent['status']} (副本: {agent['current_replicas']})")
# 获取日志(如果Agent存在)
if agents:
agent_name = agents[0]['name']
print(f"\n获取Agent日志: {agent_name}")
response = requests.get(f"{BASE_URL}/platform-agents/{agent_name}/logs?lines=20")
print(f"GET /platform-agents/{agent_name}/logs : {response.status_code}")
if response.status_code == 200:
logs_data = response.json()
print(f"日志行数: {len(logs_data['logs'].split(chr(10)))}")
def test_custom_agents():
"""测试自定义Agent"""
print_section("4. 自定义Agent测试")
# 创建自定义Agent
print("创建自定义Agent...")
custom_agent = {
"name": "test-custom-nginx",
"template_name": "test_custom_agent",
"owner_id": "test_user_001",
"channel_id": "test_channel",
"environment_vars": {
"API_KEY": "test_api_key_12345",
"DEBUG": "true"
},
"cpu_request": "100m",
"cpu_limit": "200m",
"memory_request": "128Mi",
"memory_limit": "256Mi",
"scaling_config": {
"min_replicas": 1,
"max_replicas": 3,
"target_cpu_utilization": 75
}
}
response = requests.post(f"{BASE_URL}/custom-agents", json=custom_agent)
print(f"POST /custom-agents : {response.status_code}")
if response.status_code == 201:
print("✓ 自定义Agent创建成功")
print(json.dumps(response.json(), indent=2))
agent_created = True
elif response.status_code == 409:
print("✓ Agent已存在(预期行为)")
agent_created = False
else:
print(f"✗ 创建失败: {response.text}")
agent_created = False
# 列出自定义Agents
print("\n列出所有自定义Agents...")
response = requests.get(f"{BASE_URL}/custom-agents")
print(f"GET /custom-agents : {response.status_code}")
agents = response.json()
print(f"找到 {len(agents)} 个自定义Agents")
for agent in agents:
print(f" - {agent['name']}: {agent['status']}")
# 更新环境变量(如果Agent存在)
if agents and agent_created:
agent_name = agents[0]['name']
print(f"\n更新Agent环境变量: {agent_name}")
update_env = {
"environment_vars": {
"API_KEY": "updated_api_key_67890",
"DEBUG": "false"
}
}
response = requests.put(f"{BASE_URL}/custom-agents/{agent_name}/env", json=update_env)
print(f"PUT /custom-agents/{agent_name}/env : {response.status_code}")
if response.status_code == 200:
print("✓ 环境变量更新成功")
def test_statistics():
"""测试统计API"""
print_section("5. 统计API测试")
# 统计概览
response = requests.get(f"{BASE_URL}/stats/overview")
print(f"GET /stats/overview : {response.status_code}")
print(json.dumps(response.json(), indent=2))
# 按模板统计
print("\n按模板统计:")
response = requests.get(f"{BASE_URL}/stats/by-template")
print(f"GET /stats/by-template : {response.status_code}")
stats = response.json()
for stat in stats:
print(f" - {stat['template_name']}: {stat['agent_count']} agents, {stat['total_replicas']} replicas")
# 按所有者统计
print("\n按所有者统计:")
response = requests.get(f"{BASE_URL}/stats/by-owner")
print(f"GET /stats/by-owner : {response.status_code}")
stats = response.json()
for stat in stats:
print(f" - {stat['owner_id']}: {stat['agent_count']} agents "
f"(Platform: {stat['platform_agents']}, Custom: {stat['custom_agents']})")
def test_quotas():
"""测试配额API"""
print_section("6. 配额管理测试")
# 获取配额(测试用户)
owner_id = "test_user_001"
response = requests.get(f"{BASE_URL}/quotas/{owner_id}")
print(f"GET /quotas/{owner_id} : {response.status_code}")
if response.status_code == 200:
print(json.dumps(response.json(), indent=2))
else:
print(f"配额不存在(预期行为): {response.status_code}")
# 获取默认租户配额
response = requests.get(f"{BASE_URL}/quotas/default_tenant")
print(f"\nGET /quotas/default_tenant : {response.status_code}")
if response.status_code == 200:
quota = response.json()
print(f"平台Pod配额: {quota['platform_pod_used']}/{quota['platform_pod_quota']}")
print(f"自定义CPU配额: {quota['custom_cpu_used']:.2f}/{quota['custom_cpu_quota']:.2f} 核")
print(f"自定义内存配额: {quota['custom_memory_used']:.2f}/{quota['custom_memory_quota']:.2f} MB")
def cleanup_test_resources():
"""清理测试资源"""
print_section("7. 清理测试资源")
cleanup = input("\n是否删除测试创建的Agents? (y/N): ").strip().lower()
if cleanup != 'y':
print("跳过清理")
return
# 删除测试平台Agent
print("\n删除平台Agent...")
response = requests.delete(f"{BASE_URL}/platform-agents/test-platform-echo")
print(f"DELETE /platform-agents/test-platform-echo : {response.status_code}")
if response.status_code in [200, 404]:
print("✓ 平台Agent已删除或不存在")
# 删除测试自定义Agent
print("\n删除自定义Agent...")
response = requests.delete(f"{BASE_URL}/custom-agents/test-custom-nginx")
print(f"DELETE /custom-agents/test-custom-nginx : {response.status_code}")
if response.status_code in [200, 404]:
print("✓ 自定义Agent已删除或不存在")
# 删除测试模板
print("\n删除测试模板...")
response = requests.delete(f"{BASE_URL}/templates/test_custom_agent")
print(f"DELETE /templates/test_custom_agent : {response.status_code}")
if response.status_code in [200, 404]:
print("✓ 测试模板已删除或不存在")
def main():
"""主测试流程"""
print("\n" + "="*60)
print(" Agent Manager v2.0 - 综合功能测试")
print("="*60)
print(f"\n测试服务器: {BASE_URL}")
print("\n确保服务正在运行: python app.py")
try:
# 测试连接
response = requests.get(f"{BASE_URL}/", timeout=5)
response.raise_for_status()
except Exception as e:
print(f"\n❌ 无法连接到服务器: {e}")
print("\n请先启动服务: python app.py")
sys.exit(1)
try:
# 执行所有测试
test_health_checks()
test_template_management()
test_platform_agents()
test_custom_agents()
test_statistics()
test_quotas()
cleanup_test_resources()
print_section("测试完成")
print("✅ 所有测试已执行完毕")
print("\n下一步:")
print("1. 查看 Swagger UI: http://localhost:8000/docs")
print("2. 查看数据库: sqlite3 agent_manager.db")
print("3. 查看 Kubernetes 资源: kubectl get all -n ai-agents")
except KeyboardInterrupt:
print("\n\n测试被中断")
except Exception as e:
print(f"\n❌ 测试失败: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
"""Web Service Package"""
+271
View File
@@ -0,0 +1,271 @@
"""
Agent Manager Web Service
提供RESTful API来管理AKS上的AI Agent服务
"""
from fastapi import FastAPI, HTTPException, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any, List
import logging
import sys
import os
# 添加父目录到路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from agent_manager import AKSAgentManager
from agent_manager.exceptions import (
AgentNotFoundError,
AgentDeploymentError,
QuotaExceededError,
ConcurrencyLimitError,
CircuitBreakerOpenError
)
from web_service.config import config
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# 创建FastAPI应用
app = FastAPI(
title="Agent Manager API",
description="用于管理AKS上AI Agent服务的RESTful API",
version="1.0.0"
)
# 全局管理器实例
manager: Optional[AKSAgentManager] = None
# ==================== 请求/响应模型 ====================
class AgentCreateRequest(BaseModel):
"""创建Agent请求"""
name: str = Field(..., description="Agent名称")
template: str = Field(..., description="模板类型 (basic_agent, mcp_agent, echo_agent, task_worker)")
config: Dict[str, Any] = Field(..., description="Agent配置")
namespace: Optional[str] = Field(None, description="K8s命名空间")
user_id: Optional[str] = Field("default", description="用户ID")
timeout_seconds: Optional[int] = Field(3600, description="超时时间(秒)")
auto_cleanup: bool = Field(True, description="是否自动清理")
class HealthResponse(BaseModel):
"""健康检查响应"""
status: str
aks_connected: bool
cluster_name: str
# ==================== 启动/关闭事件 ====================
@app.on_event("startup")
async def startup_event():
"""应用启动事件"""
global manager
try:
logger.info("正在启动Agent Manager Web Service...")
# 验证配置
config.validate()
if config.aks_config.use_local_kubeconfig:
logger.info("使用本地 kubeconfig 连接到 Kubernetes 集群")
else:
logger.info(f"配置验证通过: AKS集群={config.aks_config.cluster_name}")
# 初始化管理器
manager = AKSAgentManager(
subscription_id=config.aks_config.subscription_id,
resource_group=config.aks_config.resource_group,
cluster_name=config.aks_config.cluster_name,
default_namespace=config.aks_config.default_namespace,
auto_connect=True,
enable_quota_management=config.enable_quota_management,
enable_lifecycle_management=config.enable_lifecycle_management,
enable_retry_mechanism=config.enable_retry_mechanism,
enable_metering=config.enable_metering,
cleanup_interval=config.cleanup_interval,
use_local_kubeconfig=config.aks_config.use_local_kubeconfig
)
logger.info("Agent Manager Web Service 启动成功")
except Exception as e:
logger.error(f"启动失败: {str(e)}")
raise
@app.on_event("shutdown")
async def shutdown_event():
"""应用关闭事件"""
global manager
logger.info("正在关闭Agent Manager Web Service...")
if manager and manager.lifecycle_manager:
manager.lifecycle_manager.stop_cleanup_worker()
logger.info("Agent Manager Web Service 已关闭")
# ==================== 健康检查 ====================
@app.get("/health", response_model=HealthResponse, tags=["健康检查"])
async def health_check():
"""健康检查接口"""
try:
cluster_info = manager.aks_client.get_cluster_info()
cluster_name = config.aks_config.cluster_name or cluster_info.get("cluster_name", "unknown")
return HealthResponse(
status="healthy",
aks_connected=True,
cluster_name=cluster_name
)
except Exception as e:
cluster_name = config.aks_config.cluster_name or "unknown"
return HealthResponse(
status="unhealthy",
aks_connected=False,
cluster_name=cluster_name
)
# ==================== Agent管理 ====================
@app.post("/agents", status_code=status.HTTP_201_CREATED, tags=["Agent管理"])
async def create_agent(request: AgentCreateRequest):
"""
创建新的AI Agent,返回详细信息包括Pod ID用于归属确认
"""
try:
result = manager.create_agent(
name=request.name,
template=request.template,
config=request.config,
namespace=request.namespace,
user_id=request.user_id,
timeout_seconds=request.timeout_seconds,
auto_cleanup=request.auto_cleanup
)
# 获取详细信息
namespace = request.namespace or config.aks_config.default_namespace
# 获取 Pod 信息
try:
pods = manager.aks_client.core_v1_api.list_namespaced_pod(
namespace=namespace,
label_selector=f"app={request.name}"
)
pod_info = [{
"pod_id": pod.metadata.uid,
"pod_name": pod.metadata.name,
"status": pod.status.phase,
"node_name": pod.spec.node_name,
"pod_ip": pod.status.pod_ip,
"host_ip": pod.status.host_ip,
"creation_timestamp": pod.metadata.creation_timestamp.isoformat() if pod.metadata.creation_timestamp else None,
"labels": pod.metadata.labels,
"owner": {
"user_id": pod.metadata.labels.get("user-id", request.user_id) if pod.metadata.labels else request.user_id,
"agent_name": request.name,
"namespace": namespace
}
} for pod in pods.items]
except Exception as e:
logger.warning(f"获取 Pod 信息失败: {str(e)}")
pod_info = []
# 获取 Deployment 信息
try:
deployment = manager.aks_client.apps_v1_api.read_namespaced_deployment(
name=request.name,
namespace=namespace
)
deployment_info = {
"deployment_id": deployment.metadata.uid,
"deployment_name": deployment.metadata.name,
"replicas": {
"desired": deployment.spec.replicas,
"ready": deployment.status.ready_replicas or 0,
"available": deployment.status.available_replicas or 0
},
"labels": deployment.metadata.labels,
"creation_timestamp": deployment.metadata.creation_timestamp.isoformat() if deployment.metadata.creation_timestamp else None
}
except Exception as e:
logger.warning(f"获取 Deployment 信息失败: {str(e)}")
deployment_info = None
return {
"message": f"Agent {request.name} 创建成功",
"agent": {
"name": request.name,
"namespace": namespace,
"template": request.template,
"user_id": request.user_id,
"timeout_seconds": request.timeout_seconds,
"auto_cleanup": request.auto_cleanup
},
"deployment": deployment_info,
"pods": pod_info,
"summary": {
"total_pods": len(pod_info),
"running_pods": len([p for p in pod_info if p["status"] == "Running"]),
"owner_user_id": request.user_id
}
}
except QuotaExceededError as e:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))
except ConcurrencyLimitError as e:
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail=str(e))
except CircuitBreakerOpenError as e:
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(e))
except AgentDeploymentError as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
except Exception as e:
logger.error(f"创建Agent失败: {str(e)}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@app.get("/agents", tags=["Agent管理"])
async def list_agents(namespace: Optional[str] = None, label_selector: Optional[str] = None):
"""列出所有AI Agent"""
try:
agents = manager.list_agents(namespace=namespace, label_selector=label_selector)
return {"count": len(agents), "agents": agents}
except Exception as e:
logger.error(f"列出Agents失败: {str(e)}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@app.delete("/agents/{agent_name}", tags=["Agent管理"])
async def delete_agent(agent_name: str, namespace: Optional[str] = None, user_id: Optional[str] = None):
"""删除指定的Agent"""
try:
manager.delete_agent(name=agent_name, namespace=namespace, user_id=user_id)
return {"message": f"Agent {agent_name} 删除成功"}
except AgentNotFoundError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except Exception as e:
logger.error(f"删除Agent失败: {str(e)}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app:app",
host=config.host,
port=config.port,
workers=config.workers,
log_level="info"
)
+67
View File
@@ -0,0 +1,67 @@
"""
Web Service Configuration
"""
import os
from typing import Optional
from pydantic import BaseModel, Field
class AKSConfig(BaseModel):
"""AKS配置"""
subscription_id: Optional[str] = Field(None, description="Azure订阅ID")
resource_group: Optional[str] = Field(None, description="资源组名称")
cluster_name: Optional[str] = Field(None, description="AKS集群名称")
default_namespace: str = Field("default", description="默认命名空间")
use_local_kubeconfig: bool = Field(False, description="使用本地kubeconfig")
class ServiceConfig(BaseModel):
"""服务配置"""
host: str = Field("0.0.0.0", description="服务主机")
port: int = Field(8000, description="服务端口")
workers: int = Field(1, description="工作进程数")
# AKS配置
aks_config: AKSConfig
# 功能开关
enable_quota_management: bool = Field(True, description="启用配额管理")
enable_lifecycle_management: bool = Field(True, description="启用生命周期管理")
enable_retry_mechanism: bool = Field(True, description="启用重试机制")
enable_metering: bool = Field(True, description="启用计量")
# 清理间隔
cleanup_interval: int = Field(60, description="清理间隔(秒)")
def validate(self):
"""验证配置"""
if not self.aks_config.use_local_kubeconfig:
if not all([
self.aks_config.subscription_id,
self.aks_config.resource_group,
self.aks_config.cluster_name
]):
raise ValueError(
"使用Azure API时,必须提供 subscription_id, resource_group, cluster_name"
)
# 从环境变量加载配置
config = ServiceConfig(
host=os.getenv("HOST", "0.0.0.0"),
port=int(os.getenv("PORT", "8000")),
workers=int(os.getenv("WORKERS", "1")),
aks_config=AKSConfig(
subscription_id=os.getenv("AZURE_SUBSCRIPTION_ID"),
resource_group=os.getenv("AZURE_RESOURCE_GROUP"),
cluster_name=os.getenv("AKS_CLUSTER_NAME"),
default_namespace=os.getenv("DEFAULT_NAMESPACE", "default"),
use_local_kubeconfig=os.getenv("USE_LOCAL_KUBECONFIG", "false").lower() == "true"
),
enable_quota_management=os.getenv("ENABLE_QUOTA_MANAGEMENT", "true").lower() == "true",
enable_lifecycle_management=os.getenv("ENABLE_LIFECYCLE_MANAGEMENT", "true").lower() == "true",
enable_retry_mechanism=os.getenv("ENABLE_RETRY_MECHANISM", "true").lower() == "true",
enable_metering=os.getenv("ENABLE_METERING", "true").lower() == "true",
cleanup_interval=int(os.getenv("CLEANUP_INTERVAL", "60"))
)