From 23116e90862602710d9997e03a74f3cafa2bf36a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 5 Jan 2026 12:44:28 +0000 Subject: [PATCH] init --- .env.example | 24 + .gitignore | 46 + API_DOCUMENTATION.md | 842 ++++++++++++++ Dockerfile | 23 + README.md | 860 +++++++++++++++ agent_manager.db | Bin 0 -> 86016 bytes agent_manager/templates/basic_agent.yaml | 69 ++ agent_manager/templates/echo_agent.yaml | 75 ++ agent_manager/templates/mcp_agent.yaml | 71 ++ agent_manager/templates/task_worker.yaml | 75 ++ agent_templates/build_and_push.sh | 40 + agent_templates/build_jina_agent.sh | 54 + agent_templates/jina_search_agent.Dockerfile | 24 + agent_templates/jina_search_agent.py | 230 ++++ agent_templates/mysql_agent.Dockerfile | 28 + agent_templates/mysql_agent.py | 129 +++ agent_templates/postgresql_agent.Dockerfile | 27 + agent_templates/postgresql_agent.py | 130 +++ app.py | 516 +++++++++ app_new.py | 1033 ++++++++++++++++++ database.py | 239 ++++ demo_multi_tenant.sh | 156 +++ k8s/acr-secret.yaml | 8 + k8s/create-acr-secret.sh | 39 + k8s/create-kubeconfig-secret.sh | 22 + k8s/deploy-with-kubeconfig.sh | 48 + k8s/deploy.sh | 44 + k8s/deployment-with-kubeconfig.yaml | 82 ++ k8s/deployment.yaml | 83 ++ k8s/kubeconfig-secret.yaml | 7 + k8s/kubeconfig-secret.yaml.template | 11 + k8s/namespace.yaml | 7 + k8s/rbac.yaml | 40 + k8s_manager.py | 473 ++++++++ k8s_manager_new.py | 567 ++++++++++ plans/API_DOCUMENTATION.md | 704 ++++++++++++ plans/jina_search_agent_plan.md | 185 ++++ requirements.txt | 7 + scripts/aggregate_agents_resources.py | 180 +++ scripts/init_database.sh | 198 ++++ start.sh | 28 + test_venv/bin/Activate.ps1 | 247 +++++ test_venv/bin/activate | 70 ++ test_venv/bin/activate.csh | 27 + test_venv/bin/activate.fish | 69 ++ test_venv/bin/normalizer | 8 + test_venv/bin/pip | 8 + test_venv/bin/pip3 | 8 + test_venv/bin/pip3.12 | 8 + test_venv/bin/python | 1 + test_venv/bin/python3 | 1 + test_venv/bin/python3.12 | 1 + test_venv/lib64 | 1 + test_venv/pyvenv.cfg | 5 + tests/test_create_agent.py | 90 ++ tests/test_delete_agent.py | 64 ++ tests/test_env_variables.py | 145 +++ tests/test_get_metrics.py | 57 + tests/test_get_status.py | 57 + tests/test_list_agents.py | 72 ++ tests/test_v2_comprehensive.py | 328 ++++++ web_service/__init__.py | 1 + web_service/app.py | 271 +++++ web_service/config.py | 67 ++ 64 files changed, 9030 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 API_DOCUMENTATION.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 agent_manager.db create mode 100644 agent_manager/templates/basic_agent.yaml create mode 100644 agent_manager/templates/echo_agent.yaml create mode 100644 agent_manager/templates/mcp_agent.yaml create mode 100644 agent_manager/templates/task_worker.yaml create mode 100755 agent_templates/build_and_push.sh create mode 100755 agent_templates/build_jina_agent.sh create mode 100644 agent_templates/jina_search_agent.Dockerfile create mode 100644 agent_templates/jina_search_agent.py create mode 100644 agent_templates/mysql_agent.Dockerfile create mode 100644 agent_templates/mysql_agent.py create mode 100644 agent_templates/postgresql_agent.Dockerfile create mode 100644 agent_templates/postgresql_agent.py create mode 100644 app.py create mode 100644 app_new.py create mode 100644 database.py create mode 100755 demo_multi_tenant.sh create mode 100644 k8s/acr-secret.yaml create mode 100755 k8s/create-acr-secret.sh create mode 100755 k8s/create-kubeconfig-secret.sh create mode 100755 k8s/deploy-with-kubeconfig.sh create mode 100755 k8s/deploy.sh create mode 100755 k8s/deployment-with-kubeconfig.yaml create mode 100644 k8s/deployment.yaml create mode 100644 k8s/kubeconfig-secret.yaml create mode 100644 k8s/kubeconfig-secret.yaml.template create mode 100644 k8s/namespace.yaml create mode 100644 k8s/rbac.yaml create mode 100644 k8s_manager.py create mode 100644 k8s_manager_new.py create mode 100644 plans/API_DOCUMENTATION.md create mode 100644 plans/jina_search_agent_plan.md create mode 100644 requirements.txt create mode 100644 scripts/aggregate_agents_resources.py create mode 100644 scripts/init_database.sh create mode 100755 start.sh create mode 100644 test_venv/bin/Activate.ps1 create mode 100644 test_venv/bin/activate create mode 100644 test_venv/bin/activate.csh create mode 100644 test_venv/bin/activate.fish create mode 100755 test_venv/bin/normalizer create mode 100755 test_venv/bin/pip create mode 100755 test_venv/bin/pip3 create mode 100755 test_venv/bin/pip3.12 create mode 120000 test_venv/bin/python create mode 120000 test_venv/bin/python3 create mode 120000 test_venv/bin/python3.12 create mode 120000 test_venv/lib64 create mode 100644 test_venv/pyvenv.cfg create mode 100644 tests/test_create_agent.py create mode 100644 tests/test_delete_agent.py create mode 100644 tests/test_env_variables.py create mode 100644 tests/test_get_metrics.py create mode 100644 tests/test_get_status.py create mode 100644 tests/test_list_agents.py create mode 100644 tests/test_v2_comprehensive.py create mode 100644 web_service/__init__.py create mode 100644 web_service/app.py create mode 100644 web_service/config.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..78cc497 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b1d0087 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..f672d29 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -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 -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 返回和归属验证 +- ✅ 模板分类查询(平台/自定义) +- ✅ 资源监控和状态查询 + +--- + +## 联系支持 + +如有问题或建议,请联系开发团队。 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0786558 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..88575ba --- /dev/null +++ b/README.md @@ -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 | 更新资源限制逻辑,明确按需创建和配额分配机制 | diff --git a/agent_manager.db b/agent_manager.db new file mode 100644 index 0000000000000000000000000000000000000000..99f88e4a44b85355a29160509a39762739e2ad9e GIT binary patch literal 86016 zcmeI(+i%-c9Ki9oYn?S)w{Ghc<*9xEwbr=oq6s7(Aluwg=u(!ps1st5o0wrOx7kkD zG65Pe1QOz%2j2M~_$QEf;qTxt;2d90Y$xe7i6`q@sat$5zvJ^c7yBsc&iySnaEvbo zUe6AU8>Quvrj{s{DRx>I7jYL z%YNkagI3Q8Jh$z~S8gYhdii$Ks_$CHX5)^vZ_KP?Y&VQdJ!3t(bUWtJrE;|{n!P-| z*}&~Neqi^8nffayx2q~mX;|f%2N(6~jWz9fdc(P%UOKr&w=<4rEc5r4C>Yu&Y)Mq_)|XzXomyy}Oc;3L9O#Y;~d9U7lV0>&nFxG{oYHzZ$h7ekGMd(g z)wCM7t({2D56f4~hs%2PW=;D#Gs1qW{iWUSJKa`1#Que;W1B9g23JHL*EXBy_3E27 zt(S{0=4%iV_XB@{Q}tVg|>ox83+)XD#cXmeH)Ow62!)Fz-*lW4P?zaM`Z;MSX33p~jI8`itmnzZx@ycSkTB~Wl zTpO=)xx~cySK^4NbuDga=7J85Kdm=!e^_s>U%PtM%q;hg>kqs3aWoQ1sq3PYTpEGX z6PbY{I+LurOyy)A7)iy6@Z*`CDH8>cohEX|W2Lg*&E9o9)str53GdeFAG+S4-;k9M{jvL(4c#XE%xcs=T&%<8&5H^^ti8`z9$aqje*ZeO$(ZdjQOu4f-jO%H7E zkt4P+xrqjD*Zs;4+(AF_4)1^Dd9qW}qIN_}gX6Hz69X;l`i^(#ww>(w%3(@pj=Zkw z|IIj0M)QYu+fg}5t+wZgInil}_pS6(U^MJZOYYiYf419g$M+}eylg*CO+YifB`4|0 zuDo$NyQH6t27&F1X&Zf5%&$R}FBe~Q5$^B%amrLam{$d9|qNtKpEuhNtET^Yt~ozkmsuWH{;wk4Ob zsxu$Al7AYj0_uURQjB#O!EDay)hnx-JK2ESQDslf?nLzPR3*d3_QNf$TwPt&e%cAA ztjbg$<}wpkC7&>NC$_-Mw4M2^%a(Hdw&xDRO?cPZ4_{|r{zP(nqKsNFaudx29S*$A zi>r86g^wiHbL8G#oM)9Qj&<$8J#>s)+uK`KJ$eAY(ppNMF9*lUGkNq(E;D&H7l(d+ z5I_I{1Q0*~0R#|0009ILK%l?^71=k={{fB*srAbfB*srAbfB*srAbfB*srAbfB*srAbwd400Iag zfB*srAb#e1vvi~e~jrN0tg_000IagfB*sr zAb:8080/search" -H "Content-Type: application/json" -d \'{"url": "https://www.example.com"}\'', + "fetch": 'curl "http://: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() diff --git a/agent_templates/mysql_agent.Dockerfile b/agent_templates/mysql_agent.Dockerfile new file mode 100644 index 0000000..8914b30 --- /dev/null +++ b/agent_templates/mysql_agent.Dockerfile @@ -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"] diff --git a/agent_templates/mysql_agent.py b/agent_templates/mysql_agent.py new file mode 100644 index 0000000..22d2572 --- /dev/null +++ b/agent_templates/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() diff --git a/agent_templates/postgresql_agent.Dockerfile b/agent_templates/postgresql_agent.Dockerfile new file mode 100644 index 0000000..e8cb71b --- /dev/null +++ b/agent_templates/postgresql_agent.Dockerfile @@ -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"] diff --git a/agent_templates/postgresql_agent.py b/agent_templates/postgresql_agent.py new file mode 100644 index 0000000..81847f7 --- /dev/null +++ b/agent_templates/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() diff --git a/app.py b/app.py new file mode 100644 index 0000000..9048880 --- /dev/null +++ b/app.py @@ -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) diff --git a/app_new.py b/app_new.py new file mode 100644 index 0000000..cfaa964 --- /dev/null +++ b/app_new.py @@ -0,0 +1,1033 @@ +""" +FastAPI Web服务 - AI Agent管理服务 +支持平台Agent和自定义Agent两种类型,集成数据库和配额管理 +""" +from fastapi import FastAPI, HTTPException, Depends +from pydantic import BaseModel, Field +from typing import Dict, List, Optional +from sqlalchemy.orm import Session +from sqlalchemy import func +from datetime import datetime +import logging + +from k8s_manager import K8sManager +from database import ( + get_db, Template, Agent, Quota, AgentMetric, + AgentType, AgentStatus, parse_resource_string +) +import os + +# 配置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# 创建FastAPI应用 +app = FastAPI( + title="AI Agent Manager", + description="Kubernetes AI Agent管理服务 - 支持平台Agent和自定义Agent", + version="2.0.0" +) + +# 初始化K8s管理器 +NAMESPACE = os.getenv("NAMESPACE", "ai-agents") +KUBECONFIG_PATH = os.getenv("KUBECONFIG_PATH", None) +k8s_manager = K8sManager(namespace=NAMESPACE, kubeconfig_path=KUBECONFIG_PATH) + + +# ==================== 请求/响应模型 ==================== + +# Template Management Models +class CreateTemplateRequest(BaseModel): + """创建模板请求""" + name: str = Field(..., min_length=1, max_length=100) + display_name: str + description: Optional[str] = None + agent_type: str = Field(..., description="platform or custom") + image: str + port: Optional[int] = None + env_requirements: Optional[Dict] = Field(default_factory=dict) + cpu_request: Optional[str] = None + cpu_limit: Optional[str] = None + memory_request: Optional[str] = None + memory_limit: Optional[str] = None + min_replicas: int = 1 + max_replicas: int = 3 + target_cpu_utilization: int = 80 + + +class UpdateTemplateRequest(BaseModel): + """更新模板请求""" + display_name: Optional[str] = None + description: Optional[str] = None + image: Optional[str] = None + port: Optional[int] = None + env_requirements: Optional[Dict] = None + cpu_request: Optional[str] = None + cpu_limit: Optional[str] = None + memory_request: Optional[str] = None + memory_limit: Optional[str] = None + min_replicas: Optional[int] = None + max_replicas: Optional[int] = None + target_cpu_utilization: Optional[int] = None + is_active: Optional[bool] = None + + +class TemplateResponse(BaseModel): + """模板响应""" + id: int + name: str + display_name: str + description: Optional[str] + agent_type: str + image: str + port: Optional[int] + env_requirements: Dict + cpu_request: Optional[str] + cpu_limit: Optional[str] + memory_request: Optional[str] + memory_limit: Optional[str] + min_replicas: int + max_replicas: int + target_cpu_utilization: int + is_active: bool + created_at: datetime + + class Config: + from_attributes = True + + +# Platform Agent Models +class CreatePlatformAgentRequest(BaseModel): + """创建平台Agent请求""" + name: str = Field(..., min_length=1, max_length=63) + template_name: str + owner_id: str + channel_id: Optional[str] = None + tenant_id: Optional[str] = None + + +# Custom Agent Models +class ScalingConfig(BaseModel): + """弹性伸缩配置""" + min_replicas: int = Field(1, ge=0) + max_replicas: int = Field(3, ge=1) + target_cpu_utilization: int = Field(80, ge=1, le=100) + + +class CreateCustomAgentRequest(BaseModel): + """创建自定义Agent请求""" + name: str = Field(..., min_length=1, max_length=63) + template_name: str + owner_id: str + channel_id: Optional[str] = None + tenant_id: Optional[str] = None + environment_vars: Dict[str, str] + cpu_request: Optional[str] = None + cpu_limit: Optional[str] = None + memory_request: Optional[str] = None + memory_limit: Optional[str] = None + scaling_config: Optional[ScalingConfig] = None + + +class UpdateAgentEnvRequest(BaseModel): + """更新Agent环境变量请求""" + environment_vars: Dict[str, str] + + +class UpdateScalingRequest(BaseModel): + """更新伸缩配置请求""" + min_replicas: Optional[int] = None + max_replicas: Optional[int] = None + target_cpu_utilization: Optional[int] = None + + +# Unified Agent Response +class AgentResponseNew(BaseModel): + """Agent响应""" + id: int + name: str + display_name: Optional[str] + template_name: str + agent_type: str + status: str + owner_id: str + channel_id: Optional[str] + tenant_id: Optional[str] + service_url: Optional[str] + current_replicas: int + min_replicas: int + max_replicas: int + created_at: datetime + last_accessed_at: Optional[datetime] + + class Config: + from_attributes = True + + +class MessageResponse(BaseModel): + """通用消息响应""" + status: str + message: str + + +# Quota Models +class QuotaResponse(BaseModel): + """配额响应""" + owner_id: str + owner_type: str + platform_pod_quota: int + platform_pod_used: int + custom_cpu_quota: float + custom_cpu_used: float + custom_memory_quota: float + custom_memory_used: float + + class Config: + from_attributes = True + + +# Statistics Models +class StatsOverviewResponse(BaseModel): + """统计概览响应""" + total_agents: int + platform_agents: int + custom_agents: int + running_agents: int + total_cpu_used: float + total_memory_used: float + + +class TemplateStatsResponse(BaseModel): + """模板统计响应""" + template_name: str + agent_count: int + total_replicas: int + + +class OwnerStatsResponse(BaseModel): + """所有者统计响应""" + owner_id: str + agent_count: int + platform_agents: int + custom_agents: int + total_cpu_used: float + total_memory_used: float + + +# ==================== 辅助函数 ==================== + +def check_quota(db: Session, owner_id: str, agent_type: AgentType, + cpu_request: str = None, memory_request: str = None) -> bool: + """检查配额是否足够""" + quota = db.query(Quota).filter(Quota.owner_id == owner_id).first() + + if not quota: + # 如果没有配额记录,返回True(暂时允许,生产环境应该拒绝) + logger.warning(f"Owner {owner_id} 没有配额记录") + return True + + if agent_type == AgentType.PLATFORM: + # 平台Agent检查Pod数量配额 + return quota.platform_pod_used < quota.platform_pod_quota + else: + # 自定义Agent检查CPU和内存配额 + cpu_needed = parse_resource_string(cpu_request) if cpu_request else 0 + memory_needed = parse_resource_string(memory_request) if memory_request else 0 + + cpu_available = quota.custom_cpu_quota - quota.custom_cpu_used + memory_available = quota.custom_memory_quota - quota.custom_memory_used + + return cpu_needed <= cpu_available and memory_needed <= memory_available + + +def update_quota_usage(db: Session, owner_id: str, agent_type: AgentType, + delta_pods: int = 0, delta_cpu: float = 0, delta_memory: float = 0): + """更新配额使用量""" + quota = db.query(Quota).filter(Quota.owner_id == owner_id).first() + + if not quota: + # 创建新的配额记录(使用默认值) + quota = Quota( + owner_id=owner_id, + owner_type="tenant", + platform_pod_quota=10, # 默认值 + custom_cpu_quota=10.0, + custom_memory_quota=20480.0 # 20GB + ) + db.add(quota) + + if agent_type == AgentType.PLATFORM: + quota.platform_pod_used += delta_pods + else: + quota.custom_cpu_used += delta_cpu + quota.custom_memory_used += delta_memory + + db.commit() + + +# ==================== API端点 ==================== + +@app.get("/") +async def root(): + """健康检查""" + return { + "service": "AI Agent Manager", + "version": "2.0.0", + "status": "running", + "namespace": NAMESPACE + } + + +@app.get("/health") +async def health_check(): + """健康检查端点""" + return {"status": "healthy"} + + +@app.get("/ready") +async def readiness_check(db: Session = Depends(get_db)): + """就绪检查端点""" + try: + # 检查数据库连接 + db.execute("SELECT 1") + return {"status": "ready"} + except Exception as e: + raise HTTPException(status_code=503, detail=f"Service not ready: {str(e)}") + + +# ==================== 模板管理 API ==================== + +@app.post("/templates", response_model=TemplateResponse, status_code=201) +async def create_template(request: CreateTemplateRequest, db: Session = Depends(get_db)): + """创建Agent模板""" + try: + # 检查模板是否已存在 + existing = db.query(Template).filter(Template.name == request.name).first() + if existing: + raise HTTPException(status_code=409, detail=f"Template {request.name} already exists") + + # 验证agent_type + if request.agent_type not in ["platform", "custom"]: + raise HTTPException(status_code=400, detail="agent_type must be 'platform' or 'custom'") + + # 创建模板 + template = Template( + name=request.name, + display_name=request.display_name, + description=request.description, + agent_type=AgentType(request.agent_type), + image=request.image, + port=request.port, + env_requirements=request.env_requirements, + cpu_request=request.cpu_request, + cpu_limit=request.cpu_limit, + memory_request=request.memory_request, + memory_limit=request.memory_limit, + min_replicas=request.min_replicas, + max_replicas=request.max_replicas, + target_cpu_utilization=request.target_cpu_utilization + ) + + db.add(template) + db.commit() + db.refresh(template) + + logger.info(f"Created template: {template.name}") + return template + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Failed to create template: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/templates", response_model=List[TemplateResponse]) +async def list_templates(agent_type: Optional[str] = None, db: Session = Depends(get_db)): + """列出所有模板""" + try: + query = db.query(Template).filter(Template.is_active == True) + + if agent_type: + if agent_type not in ["platform", "custom"]: + raise HTTPException(status_code=400, detail="agent_type must be 'platform' or 'custom'") + query = query.filter(Template.agent_type == AgentType(agent_type)) + + templates = query.all() + return templates + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to list templates: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/templates/{template_name}", response_model=TemplateResponse) +async def get_template(template_name: str, db: Session = Depends(get_db)): + """获取模板详情""" + template = db.query(Template).filter(Template.name == template_name).first() + if not template: + raise HTTPException(status_code=404, detail=f"Template {template_name} not found") + return template + + +@app.put("/templates/{template_name}", response_model=TemplateResponse) +async def update_template(template_name: str, request: UpdateTemplateRequest, db: Session = Depends(get_db)): + """更新模板""" + try: + template = db.query(Template).filter(Template.name == template_name).first() + if not template: + raise HTTPException(status_code=404, detail=f"Template {template_name} not found") + + # 更新字段 + update_data = request.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(template, field, value) + + template.updated_at = datetime.utcnow() + db.commit() + db.refresh(template) + + logger.info(f"Updated template: {template_name}") + return template + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Failed to update template: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.delete("/templates/{template_name}", response_model=MessageResponse) +async def delete_template(template_name: str, db: Session = Depends(get_db)): + """删除模板(软删除)""" + try: + template = db.query(Template).filter(Template.name == template_name).first() + if not template: + raise HTTPException(status_code=404, detail=f"Template {template_name} not found") + + # 检查是否有Agent使用此模板 + agent_count = db.query(Agent).filter(Agent.template_id == template.id).count() + if agent_count > 0: + raise HTTPException( + status_code=409, + detail=f"Cannot delete template: {agent_count} agents are using it" + ) + + # 软删除 + template.is_active = False + db.commit() + + logger.info(f"Deleted template: {template_name}") + return MessageResponse(status="success", message=f"Template {template_name} deleted") + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Failed to delete template: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== 平台Agent API ==================== + +@app.post("/platform-agents", response_model=AgentResponseNew, status_code=201) +async def create_platform_agent(request: CreatePlatformAgentRequest, db: Session = Depends(get_db)): + """创建平台Agent""" + try: + # 查找模板 + template = db.query(Template).filter( + Template.name == request.template_name, + Template.agent_type == AgentType.PLATFORM, + Template.is_active == True + ).first() + + if not template: + raise HTTPException(status_code=404, detail=f"Platform template {request.template_name} not found") + + # 检查Agent名称是否已存在 + existing = db.query(Agent).filter(Agent.name == request.name).first() + if existing: + raise HTTPException(status_code=409, detail=f"Agent {request.name} already exists") + + # 检查配额 + if not check_quota(db, request.owner_id, AgentType.PLATFORM): + raise HTTPException(status_code=429, detail="Platform agent quota exceeded") + + # 创建Agent记录 + agent = Agent( + name=request.name, + template_id=template.id, + owner_id=request.owner_id, + channel_id=request.channel_id, + tenant_id=request.tenant_id, + agent_type=AgentType.PLATFORM, + status=AgentStatus.PENDING, + deployment_name=f"{request.name}-deployment", + service_name=f"{request.name}-service", + namespace=NAMESPACE, + min_replicas=template.min_replicas, + max_replicas=template.max_replicas, + target_cpu_utilization=template.target_cpu_utilization + ) + + db.add(agent) + db.commit() + db.refresh(agent) + + # 创建Kubernetes资源 + try: + k8s_result = k8s_manager.create_deployment_and_service( + name=request.name, + template=template, + agent=agent, + env_vars={} + ) + + # 更新Agent状态和服务URL + agent.service_url = k8s_result.get("service_url") + agent.status = AgentStatus.RUNNING + db.commit() + + except Exception as k8s_error: + logger.error(f"K8s deployment failed: {str(k8s_error)}") + agent.status = AgentStatus.FAILED + db.commit() + raise HTTPException(status_code=500, detail=f"Kubernetes deployment failed: {str(k8s_error)}") + + # 更新配额使用 + update_quota_usage(db, request.owner_id, AgentType.PLATFORM, delta_pods=1) + + logger.info(f"Created platform agent: {agent.name}") + + return AgentResponseNew( + id=agent.id, + name=agent.name, + display_name=agent.display_name, + template_name=template.name, + agent_type=agent.agent_type.value, + status=agent.status.value, + owner_id=agent.owner_id, + channel_id=agent.channel_id, + tenant_id=agent.tenant_id, + service_url=agent.service_url, + current_replicas=agent.current_replicas, + min_replicas=agent.min_replicas, + max_replicas=agent.max_replicas, + created_at=agent.created_at, + last_accessed_at=agent.last_accessed_at + ) + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Failed to create platform agent: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/platform-agents", response_model=List[AgentResponseNew]) +async def list_platform_agents(owner_id: Optional[str] = None, db: Session = Depends(get_db)): + """列出平台Agent""" + try: + query = db.query(Agent).filter(Agent.agent_type == AgentType.PLATFORM) + + if owner_id: + query = query.filter(Agent.owner_id == owner_id) + + agents = query.all() + + result = [] + for agent in agents: + result.append(AgentResponseNew( + id=agent.id, + name=agent.name, + display_name=agent.display_name, + template_name=agent.template.name, + agent_type=agent.agent_type.value, + status=agent.status.value, + owner_id=agent.owner_id, + channel_id=agent.channel_id, + tenant_id=agent.tenant_id, + service_url=agent.service_url, + current_replicas=agent.current_replicas, + min_replicas=agent.min_replicas, + max_replicas=agent.max_replicas, + created_at=agent.created_at, + last_accessed_at=agent.last_accessed_at + )) + + return result + + except Exception as e: + logger.error(f"Failed to list platform agents: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.delete("/platform-agents/{agent_name}", response_model=MessageResponse) +async def delete_platform_agent(agent_name: str, db: Session = Depends(get_db)): + """删除平台Agent""" + try: + agent = db.query(Agent).filter( + Agent.name == agent_name, + Agent.agent_type == AgentType.PLATFORM + ).first() + + if not agent: + raise HTTPException(status_code=404, detail=f"Platform agent {agent_name} not found") + + # 删除Kubernetes资源 + try: + k8s_manager.delete_deployment_and_service(agent.deployment_name, agent.service_name) + except Exception as k8s_error: + logger.error(f"K8s deletion failed: {str(k8s_error)}") + + # 更新配额 + update_quota_usage(db, agent.owner_id, AgentType.PLATFORM, delta_pods=-1) + + # 删除数据库记录 + db.delete(agent) + db.commit() + + logger.info(f"Deleted platform agent: {agent_name}") + return MessageResponse(status="success", message=f"Platform agent {agent_name} deleted") + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Failed to delete platform agent: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== 自定义Agent API ==================== + +@app.post("/custom-agents", response_model=AgentResponseNew, status_code=201) +async def create_custom_agent(request: CreateCustomAgentRequest, db: Session = Depends(get_db)): + """创建自定义Agent""" + try: + # 查找模板 + template = db.query(Template).filter( + Template.name == request.template_name, + Template.agent_type == AgentType.CUSTOM, + Template.is_active == True + ).first() + + if not template: + raise HTTPException(status_code=404, detail=f"Custom template {request.template_name} not found") + + # 检查Agent名称是否已存在 + existing = db.query(Agent).filter(Agent.name == request.name).first() + if existing: + raise HTTPException(status_code=409, detail=f"Agent {request.name} already exists") + + # 验证必需的环境变量 + required_env = template.env_requirements.get("required", {}) + for key in required_env.keys(): + if key not in request.environment_vars: + raise HTTPException( + status_code=400, + detail=f"Missing required environment variable: {key}" + ) + + # 确定资源配置(使用请求值或模板默认值) + cpu_request = request.cpu_request or template.cpu_request + memory_request = request.memory_request or template.memory_request + + # 检查配额 + if not check_quota(db, request.owner_id, AgentType.CUSTOM, cpu_request, memory_request): + raise HTTPException(status_code=429, detail="Custom agent resource quota exceeded") + + # 创建Agent记录 + scaling = request.scaling_config or ScalingConfig() + + agent = Agent( + name=request.name, + template_id=template.id, + owner_id=request.owner_id, + channel_id=request.channel_id, + tenant_id=request.tenant_id, + agent_type=AgentType.CUSTOM, + status=AgentStatus.PENDING, + environment_vars=request.environment_vars, + cpu_request=cpu_request, + cpu_limit=request.cpu_limit or template.cpu_limit, + memory_request=memory_request, + memory_limit=request.memory_limit or template.memory_limit, + deployment_name=f"{request.name}-deployment", + service_name=f"{request.name}-service", + namespace=NAMESPACE, + min_replicas=scaling.min_replicas, + max_replicas=scaling.max_replicas, + target_cpu_utilization=scaling.target_cpu_utilization + ) + + db.add(agent) + db.commit() + db.refresh(agent) + + # 创建Kubernetes资源 + try: + k8s_result = k8s_manager.create_deployment_and_service( + name=request.name, + template=template, + agent=agent, + env_vars=request.environment_vars + ) + + agent.service_url = k8s_result.get("service_url") + agent.status = AgentStatus.RUNNING + db.commit() + + except Exception as k8s_error: + logger.error(f"K8s deployment failed: {str(k8s_error)}") + agent.status = AgentStatus.FAILED + db.commit() + raise HTTPException(status_code=500, detail=f"Kubernetes deployment failed: {str(k8s_error)}") + + # 更新配额使用 + cpu_used = parse_resource_string(cpu_request) + memory_used = parse_resource_string(memory_request) + update_quota_usage(db, request.owner_id, AgentType.CUSTOM, + delta_cpu=cpu_used, delta_memory=memory_used) + + logger.info(f"Created custom agent: {agent.name}") + + return AgentResponseNew( + id=agent.id, + name=agent.name, + display_name=agent.display_name, + template_name=template.name, + agent_type=agent.agent_type.value, + status=agent.status.value, + owner_id=agent.owner_id, + channel_id=agent.channel_id, + tenant_id=agent.tenant_id, + service_url=agent.service_url, + current_replicas=agent.current_replicas, + min_replicas=agent.min_replicas, + max_replicas=agent.max_replicas, + created_at=agent.created_at, + last_accessed_at=agent.last_accessed_at + ) + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Failed to create custom agent: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/custom-agents", response_model=List[AgentResponseNew]) +async def list_custom_agents(owner_id: Optional[str] = None, db: Session = Depends(get_db)): + """列出自定义Agent""" + try: + query = db.query(Agent).filter(Agent.agent_type == AgentType.CUSTOM) + + if owner_id: + query = query.filter(Agent.owner_id == owner_id) + + agents = query.all() + + result = [] + for agent in agents: + result.append(AgentResponseNew( + id=agent.id, + name=agent.name, + display_name=agent.display_name, + template_name=agent.template.name, + agent_type=agent.agent_type.value, + status=agent.status.value, + owner_id=agent.owner_id, + channel_id=agent.channel_id, + tenant_id=agent.tenant_id, + service_url=agent.service_url, + current_replicas=agent.current_replicas, + min_replicas=agent.min_replicas, + max_replicas=agent.max_replicas, + created_at=agent.created_at, + last_accessed_at=agent.last_accessed_at + )) + + return result + + except Exception as e: + logger.error(f"Failed to list custom agents: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.put("/custom-agents/{agent_name}/env", response_model=AgentResponseNew) +async def update_custom_agent_env(agent_name: str, request: UpdateAgentEnvRequest, db: Session = Depends(get_db)): + """更新自定义Agent环境变量""" + try: + agent = db.query(Agent).filter( + Agent.name == agent_name, + Agent.agent_type == AgentType.CUSTOM + ).first() + + if not agent: + raise HTTPException(status_code=404, detail=f"Custom agent {agent_name} not found") + + # 验证必需的环境变量 + required_env = agent.template.env_requirements.get("required", {}) + for key in required_env.keys(): + if key not in request.environment_vars: + raise HTTPException( + status_code=400, + detail=f"Missing required environment variable: {key}" + ) + + # 更新环境变量 + agent.environment_vars = request.environment_vars + agent.updated_at = datetime.utcnow() + + # 更新Kubernetes Deployment + try: + k8s_manager.update_deployment_env(agent.deployment_name, request.environment_vars) + except Exception as k8s_error: + logger.error(f"K8s update failed: {str(k8s_error)}") + raise HTTPException(status_code=500, detail=f"Kubernetes update failed: {str(k8s_error)}") + + db.commit() + db.refresh(agent) + + logger.info(f"Updated custom agent env: {agent_name}") + + return AgentResponseNew( + id=agent.id, + name=agent.name, + display_name=agent.display_name, + template_name=agent.template.name, + agent_type=agent.agent_type.value, + status=agent.status.value, + owner_id=agent.owner_id, + channel_id=agent.channel_id, + tenant_id=agent.tenant_id, + service_url=agent.service_url, + current_replicas=agent.current_replicas, + min_replicas=agent.min_replicas, + max_replicas=agent.max_replicas, + created_at=agent.created_at, + last_accessed_at=agent.last_accessed_at + ) + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Failed to update custom agent env: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.delete("/custom-agents/{agent_name}", response_model=MessageResponse) +async def delete_custom_agent(agent_name: str, db: Session = Depends(get_db)): + """删除自定义Agent""" + try: + agent = db.query(Agent).filter( + Agent.name == agent_name, + Agent.agent_type == AgentType.CUSTOM + ).first() + + if not agent: + raise HTTPException(status_code=404, detail=f"Custom agent {agent_name} not found") + + # 删除Kubernetes资源 + try: + k8s_manager.delete_deployment_and_service(agent.deployment_name, agent.service_name) + except Exception as k8s_error: + logger.error(f"K8s deletion failed: {str(k8s_error)}") + + # 更新配额 + cpu_used = parse_resource_string(agent.cpu_request) + memory_used = parse_resource_string(agent.memory_request) + update_quota_usage(db, agent.owner_id, AgentType.CUSTOM, + delta_cpu=-cpu_used, delta_memory=-memory_used) + + # 删除数据库记录 + db.delete(agent) + db.commit() + + logger.info(f"Deleted custom agent: {agent_name}") + return MessageResponse(status="success", message=f"Custom agent {agent_name} deleted") + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Failed to delete custom agent: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== 统计API ==================== + +@app.get("/stats/overview", response_model=StatsOverviewResponse) +async def get_stats_overview(db: Session = Depends(get_db)): + """获取统计概览""" + try: + total_agents = db.query(Agent).count() + platform_agents = db.query(Agent).filter(Agent.agent_type == AgentType.PLATFORM).count() + custom_agents = db.query(Agent).filter(Agent.agent_type == AgentType.CUSTOM).count() + running_agents = db.query(Agent).filter(Agent.status == AgentStatus.RUNNING).count() + + # 计算总CPU和内存使用(仅自定义Agent) + custom_agent_list = db.query(Agent).filter(Agent.agent_type == AgentType.CUSTOM).all() + total_cpu = sum(parse_resource_string(a.cpu_request or "0") for a in custom_agent_list) + total_memory = sum(parse_resource_string(a.memory_request or "0") for a in custom_agent_list) + + return StatsOverviewResponse( + total_agents=total_agents, + platform_agents=platform_agents, + custom_agents=custom_agents, + running_agents=running_agents, + total_cpu_used=total_cpu, + total_memory_used=total_memory + ) + + except Exception as e: + logger.error(f"Failed to get stats overview: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/stats/by-template", response_model=List[TemplateStatsResponse]) +async def get_stats_by_template(db: Session = Depends(get_db)): + """按模板统计""" + try: + results = db.query( + Template.name, + func.count(Agent.id).label("agent_count"), + func.sum(Agent.current_replicas).label("total_replicas") + ).join(Agent, Template.id == Agent.template_id, isouter=True)\ + .group_by(Template.name).all() + + stats = [] + for name, count, replicas in results: + stats.append(TemplateStatsResponse( + template_name=name, + agent_count=count or 0, + total_replicas=replicas or 0 + )) + + return stats + + except Exception as e: + logger.error(f"Failed to get stats by template: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/stats/by-owner", response_model=List[OwnerStatsResponse]) +async def get_stats_by_owner(db: Session = Depends(get_db)): + """按所有者统计""" + try: + # 按owner_id分组统计 + owners = db.query(Agent.owner_id).distinct().all() + + stats = [] + for (owner_id,) in owners: + agents = db.query(Agent).filter(Agent.owner_id == owner_id).all() + + agent_count = len(agents) + platform_count = sum(1 for a in agents if a.agent_type == AgentType.PLATFORM) + custom_count = sum(1 for a in agents if a.agent_type == AgentType.CUSTOM) + + total_cpu = sum(parse_resource_string(a.cpu_request or "0") for a in agents if a.agent_type == AgentType.CUSTOM) + total_memory = sum(parse_resource_string(a.memory_request or "0") for a in agents if a.agent_type == AgentType.CUSTOM) + + stats.append(OwnerStatsResponse( + owner_id=owner_id, + agent_count=agent_count, + platform_agents=platform_count, + custom_agents=custom_count, + total_cpu_used=total_cpu, + total_memory_used=total_memory + )) + + return stats + + except Exception as e: + logger.error(f"Failed to get stats by owner: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== 配额管理 API ==================== + +@app.get("/quotas/{owner_id}", response_model=QuotaResponse) +async def get_quota(owner_id: str, db: Session = Depends(get_db)): + """获取配额信息""" + quota = db.query(Quota).filter(Quota.owner_id == owner_id).first() + + if not quota: + raise HTTPException(status_code=404, detail=f"Quota for owner {owner_id} not found") + + return quota + + +# ==================== 日志管理 API ==================== + +@app.get("/platform-agents/{agent_name}/logs") +async def get_platform_agent_logs(agent_name: str, lines: int = 100, db: Session = Depends(get_db)): + """获取平台Agent日志""" + try: + agent = db.query(Agent).filter( + Agent.name == agent_name, + Agent.agent_type == AgentType.PLATFORM + ).first() + + if not agent: + raise HTTPException(status_code=404, detail=f"Platform agent {agent_name} not found") + + logs = k8s_manager.get_pod_logs(agent.deployment_name, lines) + + return { + "agent_name": agent_name, + "deployment_name": agent.deployment_name, + "logs": logs, + "lines": lines + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to get platform agent logs: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/custom-agents/{agent_name}/logs") +async def get_custom_agent_logs(agent_name: str, lines: int = 100, db: Session = Depends(get_db)): + """获取自定义Agent日志""" + try: + agent = db.query(Agent).filter( + Agent.name == agent_name, + Agent.agent_type == AgentType.CUSTOM + ).first() + + if not agent: + raise HTTPException(status_code=404, detail=f"Custom agent {agent_name} not found") + + logs = k8s_manager.get_pod_logs(agent.deployment_name, lines) + + return { + "agent_name": agent_name, + "deployment_name": agent.deployment_name, + "logs": logs, + "lines": lines + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to get custom agent logs: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + + +if __name__ == "__main__": + import uvicorn + + host = os.getenv("SERVICE_HOST", "0.0.0.0") + port = int(os.getenv("SERVICE_PORT", "8000")) + + logger.info(f"启动AI Agent Manager服务 v2.0: {host}:{port}") + uvicorn.run(app, host=host, port=port) diff --git a/database.py b/database.py new file mode 100644 index 0000000..bb3f90c --- /dev/null +++ b/database.py @@ -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() diff --git a/demo_multi_tenant.sh b/demo_multi_tenant.sh new file mode 100755 index 0000000..ffa2e39 --- /dev/null +++ b/demo_multi_tenant.sh @@ -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}" diff --git a/k8s/acr-secret.yaml b/k8s/acr-secret.yaml new file mode 100644 index 0000000..a45c2b0 --- /dev/null +++ b/k8s/acr-secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +data: + .dockerconfigjson: eyJhdXRocyI6eyJhZ25ldHRhaWppLmF6dXJlY3IuaW8iOnsidXNlcm5hbWUiOiJhZ25ldHRhaWppIiwicGFzc3dvcmQiOiJoRHBYNXQzNE41Wm1uS2R0cXlqWUw1Y28vU25YSnJtRDIwQ1JwR3BXYUcrQUNSQ3cyd0dNIiwiYXV0aCI6IllXZHVaWFIwWVdscWFUcG9SSEJZTlhRek5FNDFXbTF1UzJSMGNYbHFXVXcxWTI4dlUyNVlTbkp0UkRJd1ExSndSM0JYWVVjclFVTlNRM2N5ZDBkTiJ9fX0= +kind: Secret +metadata: + name: acr-secret + namespace: default +type: kubernetes.io/dockerconfigjson diff --git a/k8s/create-acr-secret.sh b/k8s/create-acr-secret.sh new file mode 100755 index 0000000..5eddd7e --- /dev/null +++ b/k8s/create-acr-secret.sh @@ -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" diff --git a/k8s/create-kubeconfig-secret.sh b/k8s/create-kubeconfig-secret.sh new file mode 100755 index 0000000..a0bef18 --- /dev/null +++ b/k8s/create-kubeconfig-secret.sh @@ -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" diff --git a/k8s/deploy-with-kubeconfig.sh b/k8s/deploy-with-kubeconfig.sh new file mode 100755 index 0000000..0561921 --- /dev/null +++ b/k8s/deploy-with-kubeconfig.sh @@ -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" diff --git a/k8s/deploy.sh b/k8s/deploy.sh new file mode 100755 index 0000000..de1d9d8 --- /dev/null +++ b/k8s/deploy.sh @@ -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" diff --git a/k8s/deployment-with-kubeconfig.yaml b/k8s/deployment-with-kubeconfig.yaml new file mode 100755 index 0000000..00a7642 --- /dev/null +++ b/k8s/deployment-with-kubeconfig.yaml @@ -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 diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..d38d205 --- /dev/null +++ b/k8s/deployment.yaml @@ -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 diff --git a/k8s/kubeconfig-secret.yaml b/k8s/kubeconfig-secret.yaml new file mode 100644 index 0000000..cbb9231 --- /dev/null +++ b/k8s/kubeconfig-secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +data: + config: Y2x1c3RlcnM6Ci0gY2x1c3RlcjoKICAgIGNlcnRpZmljYXRlLWF1dGhvcml0eS1kYXRhOiBMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VVMlJFTkRRWFJEWjBGM1NVSkJaMGxSVkhkSVQwVnFjbGM0Y0dOMGNVTklVWEJxVUZoU2VrRk9RbWRyY1docmFVYzVkekJDUVZGelJrRkVRVTRLVFZGemQwTlJXVVJXVVZGRVJYZEthbGxVUVdkR2R6QjVUbFJGZVUxcVVYaE9WRUUwVGxSb1lVZEJPSGxOUkZVeFRWUkplVTVFUlRGTlZHY3hUMFp2ZHdwRVZFVk1UVUZyUjBFeFZVVkJlRTFEV1RKRmQyZG5TV2xOUVRCSFExTnhSMU5KWWpORVVVVkNRVkZWUVVFMFNVTkVkMEYzWjJkSlMwRnZTVU5CVVVNNENreEZXSFZXYWpkeFUxaGhPVVJtVjAxWmJXeFFPVzFUUzI4d1NUVnNRMWd6YVVWeVlUSnhkbGQwUVZoVFIyeFVWbUZKTjFaVVZ6Z3pWbVpsSzNCRk9GSUtSR04zYkVRd2MzZDRUWFpvV0ZFMFZYRkJTbTFyY2t4VFdVMWFjWGxFVkdScmFpdG1LMHRSZUVSVVFqUnJhMmRIWjJjNEsxWjNWVmMzTVc5TGJ6RkZaQXBSUmxaNE9IVnlkVXhEVGtoRVoyVkpaaXRRTm5KUlVXUjJMMnRzYlZGRGJ6Z3JPRFJ2UWl0R1pIVlBlVlpEVGt4RFRYcElVMGhrZGxsQ1RGRnJWREZWQ2k5UWJFbEZkemRTTXpodFkybzBSMWhqV1dsQmJYa3hUSGhtTldzcmMwOWlURzVxVnpKaWNqUnNVWFo0VVZGSWFYcHJhVk5qVGt0cFkzTlpNRXhQWkhBS1JrVlljVzFsYm10T1IzZFJWSEJzYWpSQlRFZEdSMUZFS3pseldUVnhObkptYUU1MVpWZHBhR1E1YkVWbWFXNXhWR3RRZDJGYWREVnlkbVpoZW1KemR3bzNWVkZGUWpkUFkzaGpaVmRKU25KSVYyNVRRazFqYmpBeVZtd3pZa2RrS3pKVlNFd3lVbU5DTmtNM1pYWkdhMGRvTUcxWlJUZzNVSGN3VFVKSmJ5ODVDbFl5YTNSTWVGcENaMFZEYkRSTE5UTlNXVUpRZWxock1YaG5NakZqY1ZCaU9HVmlORm8yUmprNFVWRjVaalpxWlU0NVZrSjBjbVlyYjAxWFFYcGFjMklLWkdOR2RXcFhVM0pKWlhodFdrbGhZVlpwYVhaT1pIUnNSU3RFY25CU1VIVndXbEUwWVV4d2FFdFFTREptSzJKMFRuSldkbEYzYnpKWU9XVjFjVFZYYndwb1ltOW9OVmhHYzJ0YWVFOW5lbHBSY2xSSk5FNDNPV0k1YkhZMlkyVlZUSGhIVEVKc1RYaHdielpaUVhkNFFVRnRjbE5pTURWVU1UVXlSR1JWTW1wekNuZEJRVzVJWkN0TVNGTTNiak5NUkZjNGJVaGhaRlJZYTNKMk5EWXZiRnBMTkhac05tRlliamhqTUVveU5tMWpNV2hpSzBOSVZDOXFNV3hHZDNWc1NXUUtaVGh5YVhwWlJXUkhRbXRJU0hKbU9VMHhOa1U0TDFwbWVXYzRTMUo0Y0ZremEyZHlPRGxxU25WM1NVUkJVVUZDYnpCSmQxRkVRVTlDWjA1V1NGRTRRZ3BCWmpoRlFrRk5RMEZ4VVhkRWQxbEVWbEl3VkVGUlNDOUNRVlYzUVhkRlFpOTZRV1JDWjA1V1NGRTBSVVpuVVZVNEswbG5RMFpGZVhSVWVtSlFMM0pJQ21kSk5td3piR1puWjNsUmQwUlJXVXBMYjFwSmFIWmpUa0ZSUlV4Q1VVRkVaMmRKUWtGRVVqZzBXbTVuWVVkeWVFVkJiSFZqVDNKbmRGRkZVVlZrV2pVS09EbHRjRU5QTVd0WE9GUmFaRlZyU2psbWRHTnBMM04xYUhoMlVsbG5SM0JGVGxGUmVFOXJia0lyVm05RlJtcFpiRXREWmpKM1dWcG9la2czSzFwQmFncFBRM05RVTNoM1NYZG1kbWt6Y1ZWR1ZWVm5ObTlXTkZaVlRrUm1WazFxY1VWTlpteEpXRE5RYUVwTlRqQnlSMkpKYms5NmVIVjZjak5SYWxFNFdGRmtDbVZPU21wc1lYYzBiamhYUkRReFRtZ3pVamhyYldjNFlWZHpSekZ2T0RaVE5sRmpURmh6VUV0aksyOTBSVVJJYTNnNFJEQmlWREpSVXpFdmFqQllkR2tLTTAxemNISjViMk0zZGpadlFuazRlV2RDT0dKSVREWk9jSGczUldJMVpIWjZabmN6VlZCTGFrbFFVbmxYTmtwRFJGSkxhaXR0VXl0Sk4zVkxaM0ZJY1FwVmVtZEVaa2w1T0dveWNXSlVjbXhwTVV4eFkwcDRPVGR1YlVock5GWnhXa1ZPWjBVMkwxUlpkMjVKZFhoTlpWVk9WMHhOUmtwdlprTnphMHBwTkRnMENsSlhWRGx0UWpkTE1IWjRVaXRHVTBGQmVIWnZjMHBIY2pkalNsUjJNSEpSY1ZCRFpXbzFWV28zTURBdmNIVlRhREZ0UWxSM055OWFVelJtWVhwS2F6SUtaRkJKZW10ME5UTjNVRWN6WWsxWFUyaFlNakJMYTJaTVR6TllSSGx6ZEZkSFVsWm5abEZvYzNCNlowbFRZMFJGUjJaRlZrTkRlbEpTVjBSWWJHdGlOUW94Y1hWb2RXWTROVkUyYmtoVmNVMWhXa2xHUkVsS1UwTXZReTlqV205VlpsVkVZeXRoWWxKbFZsZERjM1pYTDJOQlpsRnRWeTlvTTFWMmRVMXNhM2h4Q2twRmJraEdRMDgwUzBWV1ZVeHpja2R1WVRSeFUxQlFVa1F2VVRGYVlUWmhUWEI2WWpCa1UwWlZkVUpJTmtOYVRrMUxhVGhZVkZneE1FUjNaRkl4UTBNS1MzQXJZVGhwUkZSa05sRkVVSFZ2YzFCYWQxVnZOR1psY0ROTWFGUXdjMWN4Y201Q1YxQlZlVXgwTW5wSk0wVjJSVGd5VVdwR2VtcEhjWGRzWm1oWmN3cEtOMDFhV0U5UGN5OU5PUzlZVkhkVENpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSwogICAgc2VydmVyOiBodHRwczovL3RhaWppLWFpLXBkYS1kbnMtdXAwOWpzYnYuaGNwLnNvdXRoZWFzdGFzaWEuYXptazhzLmlvOjQ0MwogIG5hbWU6IHRhaWppLWFpLXBkYQpjb250ZXh0czoKLSBjb250ZXh0OgogICAgY2x1c3RlcjogdGFpamktYWktcGRhCiAgICB1c2VyOiBjbHVzdGVyVXNlcl90YWlqaS1haS1wZGFfdGFpamktYWktcGRhCiAgbmFtZTogdGFpamktYWktcGRhCmN1cnJlbnQtY29udGV4dDogdGFpamktYWktcGRhCnVzZXJzOgotIG5hbWU6IGNsdXN0ZXJVc2VyX3RhaWppLWFpLXBkYV90YWlqaS1haS1wZGEKICB1c2VyOgogICAgY2xpZW50LWNlcnRpZmljYXRlLWRhdGE6IExTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVVpJVkVORFFYZFhaMEYzU1VKQlowbFJVVk5VUmxKTWQxZHFSbmRMZHpOTE4wMWtUR3RwYWtGT1FtZHJjV2hyYVVjNWR6QkNRVkZ6UmtGRVFVNEtUVkZ6ZDBOUldVUldVVkZFUlhkS2FsbFVRV1ZHZHpCNVRsUkZlVTFxVVhoT1ZFRTBUbFJvWVVaM01IbE9la1Y1VFdwUmVFNVVSVFJPVkdoaFRVUkJlQXBHZWtGV1FtZE9Wa0pCYjFSRWJrNDFZek5TYkdKVWNIUlpXRTR3V2xoS2VrMVNWWGRGZDFsRVZsRlJSRVYzZUhSWldFNHdXbGhLYW1KSGJHeGlibEYzQ21kblNXbE5RVEJIUTFOeFIxTkpZak5FVVVWQ1FWRlZRVUUwU1VORWQwRjNaMmRKUzBGdlNVTkJVVVJYYmk4cldYcFdTVXR0SzJSbVdXd3dUblJPVDB3S0t6VnRiSEZyYld0UldHdElRVUZzTDB0S2NWWTVlamhQYzJSTFMwRkNkbTVGVWxsdWNtWlVOV2swTldndloyVkNNa2hrWm1adlZrUTFVa1JQTkdKMmFncHRZblV5ZVhGNVkzQklWRGhoUm5adk5YZERRM3BDU2xGTGVrRXJNVXB4WldGRk0zUkhhamhpTDFGblVtNXFRVVEzVG5KdFV6Tk5UbWd2VkV4NFN6QlZDa04yV1ZkYU9HWXpXVkUzU25kWlF6RmplVmMzV1c4eE1WUkhVbTlGVjBkUU55dERVRXMwTURCcVFVcHJTRkVyYm1sV1JrcG1aRUZvV0hNNE1IQnRlVWtLUzAweVdpOUlkSFZ4TkVWdlYybE5kMk5RUzNCWFEzRkhiV0VyTkdOM1VuTjFZbTlZVDBSeVluTTJRWGhDV1haM1FUbDRTVU5EU3pOd1ZtZHFVbmwzVUFwMmJIcENkM0ZxWW5KcFNURTRZMnBxYTNBdlRFWjJTakY0YW5WNmRWZ3lkbUpFUkhRNVVtVjRZMk5NV21seFVuZHRhMUZZYVdodWRHRXJPV1pHYkhaMUNrMHhLek15VEhkWlFsQk5jM1ZYTUZsbk4xSlFSR3RsUm5aQlFqRTRNblpHV0dNNFdVTkNPRmxYTnpaNFEwMTBSa2h0ZG05cFRXMUhUVEZYT0ZFMllVSUthRnBDSzFWbE5FNDVVVFF2YldGeVJITndUSEUzVWpadGJYZzVZMEpUYjNOalNsaDNlRk5IV21Oek5WTXJObTFyTVZGRVEzSTBkbmRIUTJSWlNGazJkQW8wU1hneFlVaG1jSHBKYlRjNFkxRklTVUZpY1ZNMk1HOWpVRTR2Yld0UFVYbEZjM3A1U0VWUllsbDBWVWhYY0hKcVMxbzBkRE40V1VNNGEwOVRWbkZ3Q21NNWNrNWhUMDE1Y3lzelNEVXZibFoxYzI1b1NHMW1RWEJLUm0xelRGSk9aV28yVlVod1ZEZERSM2hWUWxkRVZscGhhM042UVV0cVNEVTNVMGQ2ZUUwS05FMHJZaXR6WjNaUFRIcEdaVTFGVUd3MlZ5OTJNRTFxZUVwTk4zUnJSMVZXUVM5TVozWkVibVIwWkhOclVUQktPRmxEWlhKTFNXZGpUVk5RTlRsdGVBcERVVFF6YUc0d09FaGFiVGRHYW5oeFdYSXdkVlZSU1VSQlVVRkNiekZaZDFaRVFVOUNaMDVXU0ZFNFFrRm1PRVZDUVUxRFFtRkJkMFYzV1VSV1VqQnNDa0pCZDNkRFoxbEpTM2RaUWtKUlZVaEJkMGwzUkVGWlJGWlNNRlJCVVVndlFrRkpkMEZFUVdaQ1owNVdTRk5OUlVkRVFWZG5RbFI2TkdsQlNWVlVTekVLVUU1ekx5dHpaVUZxY1ZobFZpdERSRXBFUVU1Q1oydHhhR3RwUnpsM01FSkJVWE5HUVVGUFEwRm5SVUZIZVZWRWFTOU9PV05LUm10QlVXVkZkREJZY1FwTFJsZDRlV2NyTjBWSlQxVlBMemR1V2xoRFFrNVplRnBaYWxjNFMxbHlhV3RrTjNGb2VFUnVXRmN4VnpCR1NEZFhUekpTSzBWMFkzcGxLeXQwTjI5WENrVTRjeXNyY25aSlNFUXZMMVJzWVdneVp6a3JLemRCWjNGemFEZ3pORkZGUTNWeWN6WjZjWE00WW5Gd1FsWXZMM1Z6THk5MmFUVkVPR2hCZVVwUFJVUUtVR3BSZDJGdk1HTnBlazluT1dFNWJXcGtTbFZvTHpsWlNVVnFNSGxRYWxwVVJqVlJSbGN2VEUxemRXcEpkR3B1YTJ3d05qSmlXVEZEWTBWSk0wUlNUZ3BEYkRsTWJtNDNkM1J3YmtKMFRTOXpkblZ6TXpodE5YbEtkMmcwT0dSUVlXMXNaVVphWjNKYWRXeFBTMjFTVVRZNWRYa3plbXhYVTBaRmJXSTRVWEZZQ21sUk4zaDZjMEZ6T1U1bmExTXhWRlpDVkU1RVNTdGFWM0o2TW5kS09VWkpURlV5UXpkaWR6ZDRWRFpVTURCUGRFeEpSR3hJT1dVeFRUQnBVVUl4TXpZS2FIZFZNRFZwYTNOQlVYZ3JNVUZRTUhSNVlXRXhSVFJKZGpKRE5tNXhOM2xhUkZGalVHVTNZVzQxZVZaSmIwMWpaWGRrWWswNFVtODRaRGszTW0xNlZBcEVaVEF3VmpSRmRXRjFNMVpDYnpKUWNGUkVRbXN3V1VWSlYyNURWbWxZY2twM1ZFWjBhbmR6Y0VRNGRHNDBUbGc0U0daR2FFeFlhQzlSZEZWNmRFNUZDbHBpZWtRNGJtOUhNME53WjJocVJXbHBaM2QyV2xSMmJUVk1Rako0UjNOMk5XMVliSEZPTUV0bmVsbHNabkJ5Y0hOc2JreG1ZMHA2VUhNd1IyeHlkR2NLUldsbldrcGxVbG81ZW10WGVVSkJOMmx0UVRFNGEzaFNkR1Z5UkRsUVFuaEhRbFF2UVVJMWQyRXJkbkJMUzBkRVZsZzNUMUp3Y2pSVVkwbE1RWGxaTVFwMGJTOTVUV2h4TlVOMlYwdDZhRTU1TkZRMU5scFJURVZWVjJOalRtbEdWa2hzV0VSWVdWZFRaM0ZWWkZsUWNUZEJSVEUyVTNwMlpqQm9VR0Z0VlVGTENsUmhWbkl2YldnNWIwRkxOV0pOYm1aUFFtOXpPVGx6UFFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PQogICAgY2xpZW50LWtleS1kYXRhOiBMUzB0TFMxQ1JVZEpUaUJTVTBFZ1VGSkpWa0ZVUlNCTFJWa3RMUzB0TFFwTlNVbEtTMEZKUWtGQlMwTkJaMFZCTVhBdkwyMU5NVk5EY0hadVdESktaRVJpVkZScEwzVmFjR0Z3U25CRlJqVkNkMEZLWm5scFlXeG1ZeTlFY2toVENtbG5RV0kxZUVWWFNqWXpNQ3RaZFU5WlpqUklaMlJvTTFnek5rWlJLMVZSZW5WSE56UTFiVGQwYzNGemJrdFNNQzlIYUdJMlQyTkJaM04zVTFWRGMzY0tVSFJUWVc1dGFFNDNVbTh2Unk4d1NVVmFOSGRCSzNwaE5XdDBla1JaWmpCNU9GTjBSa0Z5TWtadFprZzVNa1ZQZVdOSFFYUllUV3gxTWt0T1pGVjRhd3BoUWtab2Fpc3ZaMnA1ZFU1T1NYZERXa0l3VUhBMGJGSlRXRE5SU1ZZM1VFNUxXbk5wUTJwT2JXWjROMkp4ZFVKTFJtOXFUVWhFZVhGV1ozRm9jRzEyQ25WSVRVVmlURzAyUm5wbk5qSTNUMmROVVZkTU9FRlFZMU5CWjJsME5sWlpTVEJqYzBRM05XTjNZMHR2TWpZMGFVNW1TRWswTlV0bWVYaGllV1JqV1RjS2N6ZHNPWEl5ZDNjM1psVlljMWhJUXpKWmNXdGpTbkJGUmpSdldqZFhkblpZZUZwaU4ycE9ablE1YVRoSFFWUjZURXhzZEVkSlR6QlVkelZJYUdKM1FRcGtaazV5ZUZZelVFZEJaMlpIUm5VcmMxRnFURkpTTlhJMlNXcEthR3BPVm5aRlQyMW5XVmRSWm14SWRVUm1WVTlRTlcxeGR6ZExVeloxTUdWd2NITm1DbGhCVlhGTVNFTldPRTFWYUcxWVRFOVZkblZ3Y0U1VlFYZHhLMHc0UW1kdVYwSXlUM0psUTAxa1YyZ3pObU41U25VdlNFVkNlVUZITm10MWRFdElSSG9LWmpWd1JHdE5hRXhOT0doNFJVY3lURlpDTVhGaE5IbHRaVXhrT0ZkQmRrcEVhMnhoY1ZoUVlYcFhhbXBOY2xCMGVDdG1OVEZpY2tvMFVqVnVkMHRUVWdwYWNrTXdWRmh2SzJ4Q05sVXJkMmh6VmtGV1p6RlhWM0JNVFhkRGIzZ3JaVEJvY3poVVQwUlFiUzl5U1V4NmFUaDRXR3BDUkRWbGJIWTNPVVJKT0ZOVUNrODNXa0pzUmxGUWVUUk1kelV6WWxoaVNrVk9RMlpIUVc1eGVXbEpTRVJGYWl0bVduTlJhMDlPTkZvNVVFSXlXblY0V1RoaGJVczVUR3hGUTBGM1JVRUtRVkZMUTBGblFVZEpUR3RDY1dkalUzcE5NamxOUldaM00zSTBRVXhCUWxZMFIwdG9hVGh0ZEZaTFprZEdNelI0UmtWa1JtUlFaakZHUjA1cFQySlVWUXAxZEhwMVRqZHFMekJyUVdKdWVVaEJSRGN5UzNGVVprNWxSbXBQTUVWbGVHMWxRV2Q1V0c1MmJXczJSVXB4YUUxck4zRjVlWFpFYUROUlkyTmpOM0JGQ21oVldGTldOVmd4TlVOWFFucDVOM05LUVZwWFlreHpiRmxFT0VNd01VZ3hkV2xRVm05VFpFdEhiVlJDYVZsR1JraDBPV3RHU0dwTFJucFBaVm8xTlhFS2EzSmtRbXBtTUd0S05YcDBWSGxOTWt4U09VSlZaakV2YUhSWFUzbFFkREJsWldKUWIwOHJZbEp2Ukd4RFZtTjRjV3REV1d4RWMxWXZTbXhsYXpGVWVRcHllSEEzUjNkdU9WWjJNRXBDUTBOSmVTOWhja2xMVkhGRk5YVnFVMEp3UzA1d05YWlNaVEowZVhaTGFrOHlialkwTUd0Q1VEUkVZV2xyZDFocFNqRjBDbk5KWW5KS2RWbDNaVTlCVVhWUWJVUkVlbGhpVkVzM1MyTlJPRzVMWjFWSU1YSm5iRVp2VURaRFpuWnJXRTlPYkZvdlFrMXZVRTV3Um5kMVNGcEhXVTBLV1RWTlkyTkthbEU1VTBzemRVSkVPV3BvWVZZNEwxUXpkVkZ0TW14SFltNURXVEUyWkZscFEwTkJUblJWVUd4SmNGUlFOSFF2ZVdkRUx6QnhRbHB2VlFwWVRrSTFSQzgwUlV4VGVFY3JjeTg0UzIxU2RGWmtTMnBRU1dwSVpFOU5RVTV4Ymt4UUwwZEtWeTl5TkhjM2RVUkhURlJuWmtGcmIzVnhOVkZEUjI5c0NrMU9lR3BUZUZGUWVteFhhak5FTDNad1FqaGFTVzByWTBZeFNtSnpXRkZzVDBwWk5uZGpjRGwzWjFORGVtSkxZVFkxTlZoMlpFNXJkVVV4ZGprdlowNEtSekpLVkRoNlZEQmpXbTF5U2tOUlpuVnlRbEJOYjFwTGJIQmtWVTlTY1VkVmMyeDVLMWh6ZDFGeVFrTmlZVzVEZFdScmJGcE1VMXB5YTFkR1JGQmxlQXBtVDNCRWQxWlhXRzFxTjJsaGJrdzFNbWxZTVhSaVpYY3ZVbFpZU1VsSmFGTlhaRnBsVGt0UlZVbFJiRmRSTURaWmQwdERRVkZGUVRSa1NuRlhPV2M0Q2tob1VteENOazA1UkVGT2FsSkZSRzlSZDJSSVdtZGtOMDFSVkVJeGRHbzJaV05OUjFwcWJWZG1OR3RaZEdSWFYwdFBaaTlRUVhjNGVIZFhNalJPYzNvS1Fsa3djR2h6UWtoWlkwMWhXRkJZV0ZKcFFsWXJRWFJqY1doTU5VeEhZVUZDYW5OQ01HdzNhMDlwVFN0SVdHRXdWSEl6UzJoT1dUVkRiR3RFTkM5NGF3b3ZVM1IwVlRkTVVIaGlUVUZJWkdaM2FFSTBXRzFoVERsNVFYUjFUekZ1VWtKeVlVVTRRMDFyVlM5Q1IyWkhTa2RLYzNNelJITjFTemd4V2twNk1rbEdDa2RuVFdKNFRreEVkbFZwTm1NellYQjJXbloxUVhwbUt6Sldha0Z5UTJWS1NFZE1VVkI0U0ZodGQybDRVelJUTVVJd05URjNVRUl5YVhSRWFrdE1abWdLWld4dGEyTjJXR2xvZGpsR2RYWjVjbkptYWpsV1dtdFdja3BwVEd4VmMwTXdVVmhDYlRsdk0xbE5TakFyYVhGbFlsSklURlkxWkVsQ0wwNTNRakJRYlFvdmJsQjZObGRSTWpCcGIydHVkMHREUVZGRlFUZ3dOa2RZYVRKRVpXUktSelpqWWpkbVZFcFZTalEwV1hKNEswaHFhV0ZaWmswcmVtdHpZVzFxWVZnNUNuZ3dSM2RpWTA5c2RtdGFZM0JGVGxSR2QzaHRZbWc0VFVRd1FVMUxaazVwWlhsaFFWaFlLek50VEdWUU4wMVJXVzFNY2xsaVZHOXhTMlF6VFZjMFowNEtZalF4UlhGaVJVbHhSbXRXYjBwdGFXZFhVa3hsVlZCbGRsbEtPRkFyUmpSTFUwNVdWREI1WlZsTFVFaHljRmszUmswd2NrOUdSblI2Y204NVVUQXhlUW94TlRRMFJYSndlbnBXU2l0UFVrNU1UelZsVkc4NFZHbE1Ta0ZHVjJ0UVFTdHBLMXBzYzNOUVVHWnlVMmhMYlRsT05ETlVOV0pDZG1sWFdIaGFNM0F3Q25jeVZ6TkNSVEpNZGxkdUsxcG5RMmhCVG5KS1dreDViWFF2WVhCUVJUQnFUSE12YUZSa1RsTktUVTVaY1doYU9GSTNjMVZtVXpSV1lWRlVVbUl3ZDA4S1VYSndTMlV5VWxwbFZHRnVkRWhzY1U5eVZFNW1ZM2RrT1ZWM05ucHhZVzg0TkhoUWVFVXhXRVIzUzBOQlVVVkJlakUzUVZkTVRVeEZZbmM0ZDI1VEt3cHVTV1ZJTkhGbFVVbGxNVkJRV0hoc1VHcENWMHRKZERoQ09VSXhNR1V6VlV4RE1tcFRWMlJ1ZVhkNWQwd3hOREZsWWxobWNVVkVhV0UxYVdwd05rSjFDbUpJU1ZSb1dWbHJUamQwT1Rsd1prbGphekpYUkRWWVdYWjBMekF6YkVKU1YwdHZRVmx0U1dSV1lXMXVhbUpEUkVaUFQwWldVbXA2VkVOR1V6bGxaR2NLVFZCWlpHZ3djRVJFWVM5emEwTk9RekZUTjFvdlVuRlFabkJEYUVwNmRTdEpiVFpyTmpWbU4xQTJhV2hYU3pkR1dFbDJRMjR2VVhSS2VWQjJibWxFUmdweGN6VmpiV0ZrUVZoSFVWTm1NVmwxWVhwNU4xRmtkbFZhZDBzcmNGRjNibG8zUkd4NmNXMUJOamhMTjBVMFFUa3JNU3Q2UjJwTmVHeFlPRFJMYmtaMENtOW1XazB2Um1OSEswY3lUR2hqTVdOUUwwTmtWRmN4YVcxeWRXdG1Va0UwYUd4NFRIVXlZbXB6VG10RlZqa3dUMUZ0TjJ0Vk1tVm9aV042VmtWV0x6Y0thalpqZG14UlMwTkJVVUpGYUc5ak9XMTRMeXRtV1ZSUVluVlhRakl4ZW5OVWR6aFJjelZoWjB0TGNXdzNTRWQwUlhodWVIQjVRMVZ6UTBkaGNVcElWQXA0TUd4elZXcElhWFZtUmxoaWNFMWpPRWhNT0d4aWJsVTJXVVZhTVVaRVNGTkdkbGN5Vm5ZNFIwSlFRVzQzVFd0WE16SnNRV1Z4VnpCdWFsZFBha0YzQ2tObVkxaGFiVE52WkRORWVWWklWVEZrY25rM01sTnlXVzB3VkRWemNVTm5iekJCYjBsa1R5dENhRXcwU2paaUwzQkhRMEY0YURGclRWRkhiRGxNUjAwS01FbEZVVmhwYWtGUVpUWjBTREZTT0V4SmRqRlphMUJHYzNkYVZTOUNabk0wZFc1TWVESnhkR0pPY1ZSU1IwRkdiVXB4YmtKRmVFTm9SRVpLYlhaWFV3cG5XRzVRUkV4d1RETldUQzlrYTNGWFdWaG9SWEpaTUU1TlEzUkxiV2QyTW05eVdXdEpXRlZpVm5OMGMyWnJRVEZrWXk5MWQzTk1OV1JFU25GRmJHUkpDakZ0V0doYVNtcFJjeTg1TUZGTWNtNVVXRXcyY0U5SVRYaHRXRzgyWTJSMFFXOUpRa0ZFWmtGV1NTOXFUMEp4Um0xcWNGQkxOQzlYU0hOV2FXeFhTMFFLT1cxNlJDOXRNV2h6ZUZSYVFuVTJaV1pDZEZCTWFHUlZUWGd5U0hKb2VqUkVlbkIzUlhSeWRtMVZPRUZEU21KVFVHRXJhamh3ZEhRd1R6UTRNa2xQTndwQ1ZVdFVjeXRLZUVOeVpIQmpLelprVDJoVlNFdEhTU3NyWm5neGVHUkNPRmN3UlNzMFkyOUdkbVpzYUZoM2RrVldTSGx5Y0c1UldUbElaVGNyYUVGaUNtdDBUbnBEU1VkMWRsTTRVMFpaVG10UFQxbFlORnBPYzJWTmRFOVNVa0ZQVkZSQ1dqWm9jRlUxYkZKaE9EVnljV1p3Y0ZSbGIxUmlkbmx1UmxkMlUwMEtNSHBhUzIwM09GQnBZbWMxVDJSVlVFVXliekV4Y0hWMFp6Tm1iVGhtYzJkWGF6ZzRPRGhSZVVOYU4wcEtTMDFsTWxwdlZVMWxhR2RxVTJaRFkyRlpjd3BYYnpOaGRIWXZhSEV4TXpGMFZFNU1SVXBOUm5wa09XeEZVVVkzYUdFMlkyYzVOMFJEVlVwd0szZ3Jka1JPYnpFclVFaEVabkJSTTJwaVRUMEtMUzB0TFMxRlRrUWdVbE5CSUZCU1NWWkJWRVVnUzBWWkxTMHRMUzBLCiAgICB0b2tlbjogdW1iZjR1NTB5cW95N2w4dGlpZ3BtdzBodmppOHg5OWo2ejcxZ3NldXo2cHdwc2M5Mjh1aHFrcHlxOWJuY2FtMHRqN2NnaXpsbzV1NW9iMDB1aWlqZ2o2NTBzcmgzbGxlZDd5ajAwbWU5MDFvcjlrbGY5NWczNHdrbXg0eHE0MzAK +kind: Secret +metadata: + name: kubeconfig-secret + namespace: default diff --git a/k8s/kubeconfig-secret.yaml.template b/k8s/kubeconfig-secret.yaml.template new file mode 100644 index 0000000..6ace860 --- /dev/null +++ b/k8s/kubeconfig-secret.yaml.template @@ -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 + # 然后将输出粘贴到这里 diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml new file mode 100644 index 0000000..8860cd3 --- /dev/null +++ b/k8s/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: ai-agents + labels: + name: ai-agents + purpose: ai-agent-hosting diff --git a/k8s/rbac.yaml b/k8s/rbac.yaml new file mode 100644 index 0000000..c10b632 --- /dev/null +++ b/k8s/rbac.yaml @@ -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 diff --git a/k8s_manager.py b/k8s_manager.py new file mode 100644 index 0000000..7242341 --- /dev/null +++ b/k8s_manager.py @@ -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://:{service_port}/", + "health": f"http://:{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}") diff --git a/k8s_manager_new.py b/k8s_manager_new.py new file mode 100644 index 0000000..187dbe3 --- /dev/null +++ b/k8s_manager_new.py @@ -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 diff --git a/plans/API_DOCUMENTATION.md b/plans/API_DOCUMENTATION.md new file mode 100644 index 0000000..67b1e0a --- /dev/null +++ b/plans/API_DOCUMENTATION.md @@ -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://:` | +| 默认端口 | 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://:8080/", + "health": "http://: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=` - 快速获取网站内容 + +### 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 + ``` diff --git a/plans/jina_search_agent_plan.md b/plans/jina_search_agent_plan.md new file mode 100644 index 0000000..57eb6c7 --- /dev/null +++ b/plans/jina_search_agent_plan.md @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4e584ce --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/scripts/aggregate_agents_resources.py b/scripts/aggregate_agents_resources.py new file mode 100644 index 0000000..622febd --- /dev/null +++ b/scripts/aggregate_agents_resources.py @@ -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() diff --git a/scripts/init_database.sh b/scripts/init_database.sh new file mode 100644 index 0000000..ad05c68 --- /dev/null +++ b/scripts/init_database.sh @@ -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 "======================================================" diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..550dabe --- /dev/null +++ b/start.sh @@ -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 diff --git a/test_venv/bin/Activate.ps1 b/test_venv/bin/Activate.ps1 new file mode 100644 index 0000000..b49d77b --- /dev/null +++ b/test_venv/bin/Activate.ps1 @@ -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" diff --git a/test_venv/bin/activate b/test_venv/bin/activate new file mode 100644 index 0000000..56c3cc8 --- /dev/null +++ b/test_venv/bin/activate @@ -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 diff --git a/test_venv/bin/activate.csh b/test_venv/bin/activate.csh new file mode 100644 index 0000000..76ded77 --- /dev/null +++ b/test_venv/bin/activate.csh @@ -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 . +# Ported to Python 3.3 venv by Andrew Svetlov + +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 diff --git a/test_venv/bin/activate.fish b/test_venv/bin/activate.fish new file mode 100644 index 0000000..0b8fa56 --- /dev/null +++ b/test_venv/bin/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /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 diff --git a/test_venv/bin/normalizer b/test_venv/bin/normalizer new file mode 100755 index 0000000..87b0b0a --- /dev/null +++ b/test_venv/bin/normalizer @@ -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()) diff --git a/test_venv/bin/pip b/test_venv/bin/pip new file mode 100755 index 0000000..bf4336c --- /dev/null +++ b/test_venv/bin/pip @@ -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()) diff --git a/test_venv/bin/pip3 b/test_venv/bin/pip3 new file mode 100755 index 0000000..bf4336c --- /dev/null +++ b/test_venv/bin/pip3 @@ -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()) diff --git a/test_venv/bin/pip3.12 b/test_venv/bin/pip3.12 new file mode 100755 index 0000000..bf4336c --- /dev/null +++ b/test_venv/bin/pip3.12 @@ -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()) diff --git a/test_venv/bin/python b/test_venv/bin/python new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/test_venv/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/test_venv/bin/python3 b/test_venv/bin/python3 new file mode 120000 index 0000000..ae65fda --- /dev/null +++ b/test_venv/bin/python3 @@ -0,0 +1 @@ +/usr/bin/python3 \ No newline at end of file diff --git a/test_venv/bin/python3.12 b/test_venv/bin/python3.12 new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/test_venv/bin/python3.12 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/test_venv/lib64 b/test_venv/lib64 new file mode 120000 index 0000000..7951405 --- /dev/null +++ b/test_venv/lib64 @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/test_venv/pyvenv.cfg b/test_venv/pyvenv.cfg new file mode 100644 index 0000000..e3d4802 --- /dev/null +++ b/test_venv/pyvenv.cfg @@ -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 diff --git a/tests/test_create_agent.py b/tests/test_create_agent.py new file mode 100644 index 0000000..d45f01d --- /dev/null +++ b/tests/test_create_agent.py @@ -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) diff --git a/tests/test_delete_agent.py b/tests/test_delete_agent.py new file mode 100644 index 0000000..25764a1 --- /dev/null +++ b/tests/test_delete_agent.py @@ -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) diff --git a/tests/test_env_variables.py b/tests/test_env_variables.py new file mode 100644 index 0000000..e4a379d --- /dev/null +++ b/tests/test_env_variables.py @@ -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) diff --git a/tests/test_get_metrics.py b/tests/test_get_metrics.py new file mode 100644 index 0000000..750c7ab --- /dev/null +++ b/tests/test_get_metrics.py @@ -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) diff --git a/tests/test_get_status.py b/tests/test_get_status.py new file mode 100644 index 0000000..e6a8b63 --- /dev/null +++ b/tests/test_get_status.py @@ -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) diff --git a/tests/test_list_agents.py b/tests/test_list_agents.py new file mode 100644 index 0000000..175a09a --- /dev/null +++ b/tests/test_list_agents.py @@ -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) diff --git a/tests/test_v2_comprehensive.py b/tests/test_v2_comprehensive.py new file mode 100644 index 0000000..d842fcc --- /dev/null +++ b/tests/test_v2_comprehensive.py @@ -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() diff --git a/web_service/__init__.py b/web_service/__init__.py new file mode 100644 index 0000000..8e00b36 --- /dev/null +++ b/web_service/__init__.py @@ -0,0 +1 @@ +"""Web Service Package""" diff --git a/web_service/app.py b/web_service/app.py new file mode 100644 index 0000000..f5c5e62 --- /dev/null +++ b/web_service/app.py @@ -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" + ) diff --git a/web_service/config.py b/web_service/config.py new file mode 100644 index 0000000..6ab2ef1 --- /dev/null +++ b/web_service/config.py @@ -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")) +)