init
This commit is contained in:
@@ -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
@@ -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
|
||||||
@@ -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
@@ -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"]
|
||||||
@@ -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 | 更新资源限制逻辑,明确按需创建和配额分配机制 |
|
||||||
Binary file not shown.
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+40
@@ -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"
|
||||||
Executable
+54
@@ -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"]
|
||||||
@@ -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()
|
||||||
@@ -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"]
|
||||||
@@ -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"]
|
||||||
@@ -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()
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+239
@@ -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()
|
||||||
Executable
+156
@@ -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}"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
.dockerconfigjson: eyJhdXRocyI6eyJhZ25ldHRhaWppLmF6dXJlY3IuaW8iOnsidXNlcm5hbWUiOiJhZ25ldHRhaWppIiwicGFzc3dvcmQiOiJoRHBYNXQzNE41Wm1uS2R0cXlqWUw1Y28vU25YSnJtRDIwQ1JwR3BXYUcrQUNSQ3cyd0dNIiwiYXV0aCI6IllXZHVaWFIwWVdscWFUcG9SSEJZTlhRek5FNDFXbTF1UzJSMGNYbHFXVXcxWTI4dlUyNVlTbkp0UkRJd1ExSndSM0JYWVVjclFVTlNRM2N5ZDBkTiJ9fX0=
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: acr-secret
|
||||||
|
namespace: default
|
||||||
|
type: kubernetes.io/dockerconfigjson
|
||||||
Executable
+39
@@ -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"
|
||||||
Executable
+22
@@ -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"
|
||||||
Executable
+48
@@ -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
@@ -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"
|
||||||
Executable
+82
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
|
# 然后将输出粘贴到这里
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: ai-agents
|
||||||
|
labels:
|
||||||
|
name: ai-agents
|
||||||
|
purpose: ai-agent-hosting
|
||||||
@@ -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
@@ -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}")
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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 "======================================================"
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Executable
+8
@@ -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())
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/usr/bin/python3
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
lib
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Web Service Package"""
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"))
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user