diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md deleted file mode 100644 index adb5e02..0000000 --- a/API_DOCUMENTATION.md +++ /dev/null @@ -1,1640 +0,0 @@ -# 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_variables": { // 可选,环境变量 - "KEY": "value" - } -} -``` - -**支持的模板类型** - -| 模板 | 说明 | 类型 | 框架 | -|------|------|------|------| -| `echo_agent` | Echo 测试服务 | 平台 | - | -| `chat_agent` | 聊天服务 | 平台 | - | -| `code_agent` | 代码执行服务 | 平台 | - | -| `search_agent` | 搜索服务 | 平台 | - | -| `jina_search_agent` | Jina 搜索服务 | 平台 | - | -| `mysql_agent` | MySQL 客户端 | 自定义 | - | -| `postgresql_agent` | PostgreSQL 客户端 | 自定义 | - | -| `azure_blob_agent` | Azure Blob Storage 客户端 (LangChain) | 自定义 | LangChain | -| `azure_blob_agent_mcp` | Azure Blob Storage 客户端 (MCP) | 自定义 | MCP | -| `azure_blob_agent_a2a` | Azure Blob Storage 客户端 (A2A) | 自定义 | A2A | - -**Agent 框架说明** - -从 v1.1.0 开始,Agent Manager 支持多种 AI Agent 框架: - -| 框架 | 说明 | 适用场景 | -|------|------|---------| -| **LangChain** | 使用 LangChain + LiteLLM | 复杂推理任务、多步骤处理流程 | -| **MCP** | Model Context Protocol | 标准化工具调用、轻量级集成 | -| **A2A** | Agent-to-Agent | 多 Agent 协作、分布式任务处理 | - -**多框架支持的配置参数** - -创建支持多框架的 Agent 时,可以使用以下额外参数: - -```json -{ - "name": "agent-name", - "template": "azure_blob_agent_mcp", - "config": { - // 基础配置 - "user_id": "user-001", - "tenant_id": "tenant-001", - "namespace": "ai-agents", - - // 框架配置 - "agent_framework": "mcp", // 框架类型: langchain, mcp, a2a - - // 工具配置 - "tools_config": { // 工具配置 JSON - "max_iterations": 5, - "enabled_tools": ["list_containers", "list_blobs"] - }, - "tool_endpoint": "http://tools-api:8080", // 外部工具端点 - "tool_api_key": "tool-key", // 工具 API 密钥 - - // 模型配置 - "model_provider": "openai", // 模型提供商: openai, azure-openai - "model_name": "gpt-4", // 模型名称 - "model_endpoint": "https://api.openai.com/v1", // 模型端点 - "model_api_key": "sk-xxxx", // 模型 API 密钥 - - // 存储配置 (针对 Azure Blob Agent) - "storage_connection_string": "DefaultEndpointsProtocol=https;...", - "storage_account_name": "myaccount", - - // 资源配置 - "cpu_request": "100m", - "cpu_limit": "500m", - "memory_request": "256Mi", - "memory_limit": "512Mi" - } -} -``` - -**响应** - -```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` - 服务器内部错误 - -**示例** - -基础示例 - Echo Agent: -```bash -curl -X POST http://localhost:8000/agents \ - -H "Content-Type: application/json" \ - -d '{ - "name": "alice-echo", - "template": "echo_agent", - "config": { - "user_id": "alice" - } - }' -``` - -Azure Blob Agent (LangChain 版本) - 提供连接字符串: -```bash -curl -X POST http://localhost:8000/agents \ - -H "Content-Type: application/json" \ - -d '{ - "name": "my-azure-blob-agent", - "template": "azure_blob_agent", - "config": { - "user_id": "alice" - }, - "env": { - "LITELLM_API_BASE": "http://litellm-service:4000", - "LITELLM_MODEL": "gpt-4", - "LITELLM_API_KEY": "sk-your-api-key", - "AZURE_STORAGE_CONNECTION_STRING": "DefaultEndpointsProtocol=https;AccountName=youraccount;AccountKey=yourkey;EndpointSuffix=core.windows.net", - "SERVICE_PORT": "8080" - } - }' -``` - -**Azure Blob Agent (MCP 版本) - 标准化工具调用**: -```bash -curl -X POST http://localhost:8000/agents \ - -H "Content-Type: application/json" \ - -d '{ - "name": "my-blob-mcp", - "template": "azure_blob_agent_mcp", - "config": { - "user_id": "alice", - "tenant_id": "tenant-001", - "namespace": "ai-agents", - "agent_framework": "mcp", - "model_provider": "openai", - "model_name": "gpt-4", - "model_api_key": "sk-your-api-key", - "model_endpoint": "https://api.openai.com/v1", - "storage_connection_string": "DefaultEndpointsProtocol=https;AccountName=youraccount;AccountKey=yourkey;EndpointSuffix=core.windows.net", - "tools_config": { - "max_iterations": 5, - "enabled_tools": ["list_containers", "list_blobs", "search_blobs"] - }, - "cpu_request": "100m", - "memory_request": "256Mi" - } - }' -``` - -**Azure Blob Agent (A2A 版本) - Agent 间协作**: -```bash -curl -X POST http://localhost:8000/agents \ - -H "Content-Type: application/json" \ - -d '{ - "name": "my-blob-a2a", - "template": "azure_blob_agent_a2a", - "config": { - "user_id": "alice", - "tenant_id": "tenant-001", - "namespace": "ai-agents", - "agent_framework": "a2a", - "model_provider": "azure-openai", - "model_name": "gpt-4", - "model_api_key": "your-azure-openai-key", - "model_endpoint": "https://your-resource.openai.azure.com", - "storage_connection_string": "DefaultEndpointsProtocol=https;AccountName=youraccount;AccountKey=yourkey;EndpointSuffix=core.windows.net", - "cpu_request": "100m", - "memory_request": "256Mi" - }, - "env": { - "AGENT_ID": "blob-agent-001", - "AGENT_ROLE": "storage_manager", - "AGENT_CAPABILITIES": "[\"blob_storage\", \"file_operations\"]" - } - }' -``` - -**测试部署后的 Agent** - -**LangChain 版本测试:** - -获取 Pod IP 并测试: -```bash -# 1. 检查 Agent 状态 -curl -s http://localhost:8000/agents/my-azure-blob-agent/status | jq '{status, health_status, pod_ip, access_url}' - -# 2. 获取 Pod IP -POD_IP=$(curl -s http://localhost:8000/agents/my-azure-blob-agent/status | jq -r '.pod_ip') -echo "Pod IP: $POD_IP" - -# 3. 测试健康检查 -curl http://$POD_IP:8080/health - -# 4. 查看 Agent 信息 -curl http://$POD_IP:8080/ | jq . - -# 5. 如果启动时未提供连接字符串,可以动态连接 -curl -X POST http://$POD_IP:8080/connect \ - -H "Content-Type: application/json" \ - -d '{ - "connection_string": "DefaultEndpointsProtocol=https;AccountName=youraccount;AccountKey=yourkey;EndpointSuffix=core.windows.net" - }' - -# 6. 检查连接状态 -curl http://$POD_IP:8080/status - -# 7. 执行自然语言查询 - 列出所有容器 -curl -X POST http://$POD_IP:8080/query \ - -H "Content-Type: application/json" \ - -d '{"query": "列出所有容器"}' | jq . - -# 8. 查看指定容器的文件 -curl -X POST http://$POD_IP:8080/query \ - -H "Content-Type: application/json" \ - -d '{"query": "显示 mycontainer 容器中的所有文件"}' | jq . - -# 9. 搜索文件 -curl -X POST http://$POD_IP:8080/query \ - -H "Content-Type: application/json" \ - -d '{"query": "搜索包含 report 的文件"}' | jq . - -# 10. 获取存储统计 -curl -X POST http://$POD_IP:8080/query \ - -H "Content-Type: application/json" \ - -d '{"query": "存储统计"}' | jq . -``` - -**MCP 版本测试:** - -```bash -# 1. 获取 Pod IP -POD_IP=$(curl -s http://localhost:8000/agents/my-blob-mcp/status | jq -r '.pod_ip') - -# 2. 测试健康检查 -curl http://$POD_IP:8080/health | jq . - -# 3. 查看 Agent 信息(包含框架类型) -curl http://$POD_IP:8080/ | jq . - -# 4. 列出所有可用的 MCP 工具 -curl http://$POD_IP:8080/mcp/tools | jq . - -# 5. 调用 MCP 工具 - 列出所有容器 -curl -X POST http://$POD_IP:8080/mcp/call \ - -H "Content-Type: application/json" \ - -d '{ - "tool_name": "list_containers", - "parameters": {} - }' | jq . - -# 6. 调用 MCP 工具 - 列出容器中的文件 -curl -X POST http://$POD_IP:8080/mcp/call \ - -H "Content-Type: application/json" \ - -d '{ - "tool_name": "list_blobs", - "parameters": { - "container_name": "mycontainer" - } - }' | jq . - -# 7. 调用 MCP 工具 - 获取文件信息 -curl -X POST http://$POD_IP:8080/mcp/call \ - -H "Content-Type: application/json" \ - -d '{ - "tool_name": "get_blob_info", - "parameters": { - "container_name": "mycontainer", - "blob_name": "myfile.txt" - } - }' | jq . - -# 8. 调用 MCP 工具 - 搜索文件 -curl -X POST http://$POD_IP:8080/mcp/call \ - -H "Content-Type: application/json" \ - -d '{ - "tool_name": "search_blobs", - "parameters": { - "container_name": "mycontainer", - "keyword": "report" - } - }' | jq . - -# 9. 调用 MCP 工具 - 获取存储统计 -curl -X POST http://$POD_IP:8080/mcp/call \ - -H "Content-Type: application/json" \ - -d '{ - "tool_name": "get_storage_stats", - "parameters": {} - }' | jq . - -# 10. 使用简化的查询接口(规则匹配) -curl -X POST http://$POD_IP:8080/query \ - -H "Content-Type: application/json" \ - -d '{"query": "列出所有容器"}' | jq . -``` - -**A2A 版本测试:** - -```bash -# 1. 获取 Pod IP -POD_IP=$(curl -s http://localhost:8000/agents/my-blob-a2a/status | jq -r '.pod_ip') - -# 2. 测试健康检查(包含 Agent 身份信息) -curl http://$POD_IP:8080/health | jq . - -# 3. 获取 Agent 能力 -curl http://$POD_IP:8080/a2a/capabilities | jq . - -# 4. 发送 A2A 消息 - 列出容器 -curl -X POST http://$POD_IP:8080/a2a/message \ - -H "Content-Type: application/json" \ - -d '{ - "message_id": "msg-001", - "from_agent": "external-caller", - "to_agent": "blob-agent-001", - "message_type": "request", - "action": "list_containers", - "parameters": {} - }' | jq . - -# 5. 发送 A2A 消息 - 列出文件 -curl -X POST http://$POD_IP:8080/a2a/message \ - -H "Content-Type: application/json" \ - -d '{ - "message_id": "msg-002", - "from_agent": "external-caller", - "to_agent": "blob-agent-001", - "message_type": "request", - "action": "list_blobs", - "parameters": { - "container_name": "mycontainer" - } - }' | jq . - -# 6. 发送 A2A 消息 - 获取统计 -curl -X POST http://$POD_IP:8080/a2a/message \ - -H "Content-Type: application/json" \ - -d '{ - "message_id": "msg-003", - "from_agent": "external-caller", - "to_agent": "blob-agent-001", - "message_type": "request", - "action": "get_stats", - "parameters": {} - }' | jq . - -# 7. 注册另一个 Agent(用于协作) -curl -X POST http://$POD_IP:8080/a2a/register \ - -H "Content-Type: application/json" \ - -d '{ - "agent_id": "analytics-agent", - "agent_role": "data_analyzer", - "capabilities": ["data_analysis", "visualization"], - "endpoint": "http://analytics-agent:8080" - }' | jq . - -# 8. 列出已注册的 Agent -curl http://$POD_IP:8080/a2a/agents | jq . - -# 9. 与其他 Agent 协作(需要先注册目标 Agent) -curl -X POST http://$POD_IP:8080/a2a/collaborate \ - -H "Content-Type: application/json" \ - -d '{ - "target_agent_id": "analytics-agent", - "action": "analyze_data", - "parameters": { - "data_source": "blob_storage" - } - }' | jq . -``` - -d '{"query": "统计存储使用情况"}' | jq . -``` - ---- - -### 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 的详细状态信息,包括 Pod 状态、容器健康状态、资源配额等。 - -**请求** - -```http -GET /agents/{agent_name}/status -``` - -**路径参数** - -| 参数 | 类型 | 说明 | -|------|------|------| -| agent_name | string | Agent 名称 | - -**响应 - 健康状态** - -```json -{ - "name": "alice-echo", - "namespace": "ai-agents", - "status": "Running", - "health_status": "healthy", - "template": "echo_agent", - "created_at": "2026-01-05T07:35:00+00:00", - "node": "aks-node-123", - "pod_ip": "10.244.2.24", - "containers": [ - { - "name": "alice-echo", - "ready": true, - "restart_count": 0, - "state": "running", - "started_at": "2026-01-05T07:35:15+00:00" - } - ], - "resources": { - "requests": { - "cpu": "100m", - "memory": "128Mi" - }, - "limits": { - "cpu": "500m", - "memory": "512Mi" - }, - "usage": { - "cpu": "14502n", - "memory": "8704Ki", - "available": true - } - }, - "service_port": 8080, - "access_url": "http://10.244.2.24:8080", - "endpoints": { - "root": "http://10.244.2.24:8080/", - "health": "http://10.244.2.24:8080/health" - }, - "conditions": [ - { - "type": "Ready", - "status": "True", - "reason": null - }, - { - "type": "ContainersReady", - "status": "True", - "reason": null - } - ] -} -``` - -**响应 - 崩溃状态** - -```json -{ - "name": "my-mysql-agent", - "namespace": "ai-agents", - "status": "Waiting", - "health_status": "unhealthy", - "template": "mysql_agent", - "created_at": "2026-01-04T06:40:38+00:00", - "node": "aks-node-123", - "pod_ip": "10.244.1.53", - "containers": [ - { - "name": "my-mysql-agent", - "ready": false, - "restart_count": 599, - "state": "waiting", - "reason": "CrashLoopBackOff", - "message": "back-off 5m0s restarting failed container=my-mysql-agent pod=my-mysql-agent_ai-agents(...)" - } - ], - "resources": { - "requests": { - "cpu": "100m", - "memory": "128Mi" - }, - "limits": { - "cpu": "500m", - "memory": "512Mi" - }, - "usage": { - "cpu": null, - "memory": null, - "available": false, - "reason": "metrics-server未安装或Pod不存在" - } - }, - "conditions": [ - { - "type": "Ready", - "status": "False", - "reason": "ContainersNotReady" - }, - { - "type": "ContainersReady", - "status": "False", - "reason": "ContainersNotReady" - } - ] -} -``` - -**字段说明** - -| 字段 | 类型 | 说明 | -|------|------|------| -| name | string | Agent 名称 | -| namespace | string | 命名空间 | -| status | string | Pod 状态 (Running/Pending/Waiting/Terminated/Failed) | -| **health_status** | string | **健康状态** (healthy/unhealthy/degraded) | -| template | string | 使用的模板 | -| created_at | string | 创建时间 (ISO 8601) | -| node | string | 运行的节点 | -| pod_ip | string | Pod IP 地址 | -| **containers** | array | **容器详细状态** | -| resources | object | 资源配额和使用情况 | -| service_port | int \| null | 服务端口 | -| access_url | string \| null | 访问地址 | -| endpoints | object \| null | API 端点 | -| conditions | array | Pod 条件状态 | - -**健康状态说明** - -| 状态 | 说明 | -|------|------| -| `healthy` | 所有容器运行正常且就绪 | -| `unhealthy` | 容器崩溃、终止或未就绪 | -| `degraded` | 容器重启次数过多 (>5次) | - -**容器状态字段** - -| 字段 | 类型 | 说明 | -|------|------|------| -| name | string | 容器名称 | -| ready | boolean | 是否就绪 | -| restart_count | int | 重启次数 | -| state | string | 状态 (running/waiting/terminated) | -| reason | string \| null | 状态原因 (如 CrashLoopBackOff) | -| message | string \| null | 详细消息 | -| exit_code | int \| null | 退出码 (terminated 状态) | -| started_at | string \| null | 启动时间 (running 状态) | -| finished_at | string \| null | 结束时间 (terminated 状态) | - -**Pod 状态类型** - -| 状态 | 说明 | -|------|------| -| Running | Pod 正在运行 | -| Pending | Pod 等待调度 | -| Waiting | 容器等待启动 | -| Terminated | 容器已终止 | -| Failed | Pod 失败 | -| Succeeded | Pod 成功完成 | - -**状态码** - -- `200` - 成功 -- `404` - Agent 不存在 -- `500` - 服务器内部错误 - -**示例** - -```bash -# 查询健康的 Agent -curl http://localhost:8000/agents/alice-echo/status - -# 查询崩溃的 Agent -curl http://localhost:8000/agents/my-mysql-agent/status -``` - -**使用建议** - -1. **监控告警**:使用 `health_status` 字段而非 `status` 进行健康监控 -2. **故障排查**:检查 `containers` 数组获取容器崩溃原因和重启次数 -3. **自动化运维**:根据 `health_status` 自动触发重启或告警 -4. **日志分析**:结合 `restart_count` 和 `reason` 定位问题 - ---- - -### 4. 获取 Agent 资源使用情况 - -获取 Agent 的 CPU 和内存使用情况。 - -**请求** - -```http -GET /agents/{agent_name}/metrics -``` - -**功能说明** - -获取 Agent 的资源使用情况,包括: -- **requests/limits**: 资源配额(从 Pod spec 获取) -- **usage**: 实时资源使用情况(从 metrics-server 获取,需要集群安装 metrics-server) -- **timestamp**: metrics 数据的时间戳 - -**响应** - -```json -{ - "name": "alice-echo", - "namespace": "ai-agents", - "requests": { - "cpu": "100m", - "memory": "128Mi" - }, - "limits": { - "cpu": "500m", - "memory": "512Mi" - }, - "usage": { - "cpu": "14502n", - "memory": "8704Ki" - }, - "timestamp": "2026-01-06T05:01:04Z", - "metrics_available": null -} -``` - -**字段说明** - -| 字段 | 类型 | 说明 | -|------|------|------| -| name | string | Pod 名称 | -| namespace | string | 命名空间 | -| requests | object | 资源请求配额 | -| limits | object | 资源限制配额 | -| usage | object \| null | 实时资源使用(需要 metrics-server) | -| timestamp | string \| null | metrics 时间戳(ISO 8601 格式) | -| metrics_available | bool \| null | metrics-server 是否可用 | - -**CPU 单位说明** -- `n` (nanocores): 1 核 = 1,000,000,000n -- `m` (millicores): 1 核 = 1,000m -- 例如: `14502n` = 0.014502m ≈ 0.000014 核 - -**内存单位说明** -- `Ki` (Kibibytes): 1024 字节 -- `Mi` (Mebibytes): 1024 KiB -- 例如: `8704Ki` ≈ 8.5 MiB - -**示例** - -```bash -curl http://localhost:8000/agents/alice-echo/metrics -``` - -**注意事项** -- 如果集群未安装 metrics-server,`usage` 和 `timestamp` 将为 `null` -- metrics 数据由 Kubernetes metrics-server 提供,更新频率通常为 15-60 秒 -- `usage` 显示的是 Pod 的实际资源消耗,不是配额 - ---- - -### 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 创建和管理 -- ✅ 支持 10 种 Agent 模板(包含 3 种框架版本) -- ✅ 多租户支持(user-id 标签) -- ✅ 多框架支持(LangChain、MCP、A2A) -- ✅ Pod ID 返回和归属验证 -- ✅ 模板分类查询(平台/自定义) -- ✅ 资源监控和状态查询 -- ✅ 可自定义命名空间 - ---- - -## 多框架 Agent 支持 (v1.1.0+) - -### 框架对比 - -| 特性 | LangChain | MCP | A2A | -|------|-----------|-----|-----| -| **实现方式** | LangChain + LiteLLM | Model Context Protocol | Agent-to-Agent Protocol | -| **工具调用** | LangChain Tools | MCP Tool Classes | A2A Action Handlers | -| **主要端点** | `/query` | `/mcp/tools`, `/mcp/call` | `/a2a/capabilities`, `/a2a/message` | -| **协作能力** | ❌ | ❌ | ✅ Agent 注册和通信 | -| **适用场景** | 复杂推理任务 | 标准化工具调用 | 多 Agent 协作 | -| **集成难度** | 中等 | 简单 | 中等 | - -### 配置参数说明 - -#### 通用参数(所有框架) - -| 参数 | 类型 | 必需 | 说明 | 示例 | -|------|------|------|------|------| -| `user_id` | string | ✅ | 用户标识 | `"user-001"` | -| `tenant_id` | string | ❌ | 租户标识 | `"tenant-001"` | -| `namespace` | string | ❌ | Kubernetes 命名空间 | `"ai-agents"` | -| `agent_framework` | string | ❌ | 框架类型 | `"mcp"` 或 `"a2a"` | -| `cpu_request` | string | ❌ | CPU 请求量 | `"100m"` | -| `cpu_limit` | string | ❌ | CPU 限制 | `"500m"` | -| `memory_request` | string | ❌ | 内存请求量 | `"256Mi"` | -| `memory_limit` | string | ❌ | 内存限制 | `"512Mi"` | - -#### 模型配置参数(MCP/A2A) - -| 参数 | 类型 | 必需 | 说明 | 示例 | -|------|------|------|------|------| -| `model_provider` | string | ✅ | 模型提供商 | `"openai"`, `"azure-openai"` | -| `model_name` | string | ✅ | 模型名称 | `"gpt-4"`, `"gpt-3.5-turbo"` | -| `model_api_key` | string | ✅ | 模型 API 密钥 | `"sk-xxxx"` | -| `model_endpoint` | string | ❌ | 模型 API 端点 | `"https://api.openai.com/v1"` | - -#### 工具配置参数(MCP/A2A) - -| 参数 | 类型 | 必需 | 说明 | 示例 | -|------|------|------|------|------| -| `tools_config` | object | ❌ | 工具配置 JSON | `{"max_iterations": 5}` | -| `tool_endpoint` | string | ❌ | 外部工具端点 | `"http://tools-api:8080"` | -| `tool_api_key` | string | ❌ | 工具 API 密钥 | `"tool-key-xxx"` | - -#### 存储配置参数(Azure Blob Agent) - -| 参数 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `storage_connection_string` | string | ❌ | Azure Storage 连接字符串 | -| `storage_account_name` | string | ❌ | 存储账户名称 | - -### MCP 框架 API 端点 - -MCP Agent 部署后提供以下额外端点: - -#### GET /mcp/tools -列出所有可用的 MCP 工具 - -**响应示例:** -```json -{ - "tools": [ - { - "name": "list_containers", - "description": "列出 Azure Blob Storage 中的所有容器", - "inputSchema": { - "type": "object", - "properties": {}, - "required": [] - } - }, - { - "name": "list_blobs", - "description": "列出指定容器中的所有文件", - "inputSchema": { - "type": "object", - "properties": { - "container_name": { - "type": "string", - "description": "容器名称" - } - }, - "required": ["container_name"] - } - } - ], - "count": 5, - "framework": "mcp" -} -``` - -#### POST /mcp/call -调用指定的 MCP 工具 - -**请求:** -```json -{ - "tool_name": "list_blobs", - "parameters": { - "container_name": "mycontainer" - } -} -``` - -**响应:** -```json -{ - "tool": "list_blobs", - "result": { - "success": true, - "container": "mycontainer", - "blobs": [...], - "count": 10, - "total_size_mb": 125.5 - }, - "timestamp": "2026-01-12T14:50:00Z" -} -``` - -### A2A 框架 API 端点 - -A2A Agent 部署后提供以下额外端点: - -#### GET /a2a/capabilities -获取 Agent 的能力信息 - -**响应示例:** -```json -{ - "agent_id": "blob-agent-001", - "agent_role": "storage_manager", - "capabilities": ["blob_storage", "file_operations"], - "supported_actions": [ - "list_containers", - "list_blobs", - "get_blob_info", - "search_blobs", - "get_stats" - ], - "framework": "a2a" -} -``` - -#### POST /a2a/register -注册其他 Agent(用于协作) - -**请求:** -```json -{ - "agent_id": "analytics-agent", - "agent_role": "data_analyzer", - "capabilities": ["data_analysis", "visualization"], - "endpoint": "http://analytics-agent:8080" -} -``` - -#### POST /a2a/message -发送 A2A 消息给 Agent - -**请求:** -```json -{ - "message_id": "msg-001", - "from_agent": "caller-agent", - "to_agent": "blob-agent-001", - "message_type": "request", - "action": "list_containers", - "parameters": {} -} -``` - -**响应:** -```json -{ - "message_id": "msg-001", - "from_agent": "blob-agent-001", - "to_agent": "caller-agent", - "message_type": "response", - "action": "list_containers", - "result": { - "success": true, - "containers": [...], - "count": 5 - }, - "timestamp": "2026-01-12T14:50:00Z" -} -``` - -#### GET /a2a/agents -列出已注册的 Agent - -#### POST /a2a/collaborate -与其他 Agent 协作 - -### 命名空间支持 - -从 v1.1.0 开始,支持自定义 Kubernetes 命名空间: - -```bash -# 在自定义命名空间中创建 Agent -curl -X POST http://localhost:8000/agents \ - -H "Content-Type: application/json" \ - -d '{ - "name": "my-agent", - "template": "azure_blob_agent_mcp", - "config": { - "namespace": "my-namespace", - "user_id": "user-001", - ... - } - }' - -# 查询特定命名空间的 Agent -kubectl get pods -n my-namespace -l app=ai-agent - -# 通过 API 查询时,namespace 会在响应中返回 -curl http://localhost:8000/agents/my-agent | jq '.namespace' -``` - -### 环境变量传递 - -创建 Agent 时,以下配置会自动转换为容器环境变量: - -| 配置参数 | 环境变量名 | -|----------|-----------| -| `agent_framework` | `AGENT_FRAMEWORK` | -| `tools_config` | `TOOLS_CONFIG` (JSON字符串) | -| `tool_endpoint` | `TOOL_ENDPOINT` | -| `tool_api_key` | `TOOL_API_KEY` | -| `model_provider` | `MODEL_PROVIDER` | -| `model_name` | `MODEL_NAME` | -| `model_endpoint` | `MODEL_ENDPOINT` | -| `model_api_key` | `MODEL_API_KEY` | -| `storage_connection_string` | `AZURE_STORAGE_CONNECTION_STRING` | -| `storage_account_name` | `STORAGE_ACCOUNT_NAME` | -| `user_id` | `USER_ID` | -| `tenant_id` | `TENANT_ID` | -| `namespace` | `NAMESPACE` | - -### 故障排查 - -**Agent 创建失败** - -1. 检查模板名称是否正确 -2. 验证必需参数是否提供(如 model_api_key) -3. 查看 agent-manager 日志 - -**MCP 工具调用失败** - -1. 使用 `GET /mcp/tools` 确认工具名称 -2. 检查参数格式是否符合 inputSchema -3. 查看 agent pod 日志 - -**A2A Agent 无法协作** - -1. 确认目标 Agent 已注册 -2. 检查网络连接和端点可访问性 -3. 验证 message 格式是否正确 - -### 更多资源 - -- [多框架使用指南](agent_templates/MULTI_FRAMEWORK_GUIDE.md) -- [快速参考](agent_templates/QUICK_REFERENCE.md) -- [实现总结](MULTI_FRAMEWORK_SUMMARY.md) - ---- - -## 联系支持 - -如有问题或建议,请联系开发团队。 diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 new file mode 100644 index 0000000..d328749 --- /dev/null +++ b/Dockerfile.arm64 @@ -0,0 +1,35 @@ +# 支持 ARM 架构的 Dockerfile +FROM --platform=linux/arm64 python:3.11-slim + +WORKDIR /app + +# 安装必要的系统依赖 +RUN apt-get update && apt-get install -y \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# 复制应用代码 +COPY requirements.txt . +COPY app.py . +COPY k8s_manager.py . +COPY database.py . + +# 安装 Python 依赖 +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 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:8000/')" || exit 1 + +# 运行应用 +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/HEALTH_CHECK_FIX.md b/HEALTH_CHECK_FIX.md deleted file mode 100644 index 0b8643b..0000000 --- a/HEALTH_CHECK_FIX.md +++ /dev/null @@ -1,158 +0,0 @@ -# 健康检查修复说明 - -## 问题描述 - -之前的健康检查实现存在一个严重问题:即使 agent 的容器已经崩溃(crashed),查询状态时仍然会显示为健康(healthy)。 - -### 根本原因 - -原实现只检查了 Pod 的 `phase`(如 Running、Pending 等),但没有检查容器的实际状态。即使容器崩溃或处于等待/终止状态,Pod 的 phase 可能仍然是 "Running"。 - -## 修复内容 - -### 1. 修改 `k8s_manager.py` 的 `get_pod_status` 方法 - -**主要改进:** -- ✅ 检查容器实际状态(running、waiting、terminated) -- ✅ 检查容器就绪状态(ready) -- ✅ 检查容器重启次数 -- ✅ 新增 `health_status` 字段,返回真实健康状态 - -**健康状态分类:** -- `healthy`: 所有容器运行正常且就绪 -- `unhealthy`: 容器崩溃、终止或未就绪 -- `degraded`: 容器重启次数过多(>5次) - -**新增字段:** -- `health_status`: 真实健康状态 -- `containers`: 容器详细信息数组,包含: - - `name`: 容器名称 - - `ready`: 是否就绪 - - `restart_count`: 重启次数 - - `state`: 当前状态(running/waiting/terminated) - - `reason`: 状态原因(如果有) - - `exit_code`: 退出码(如果已终止) - -### 2. 修改 `k8s_manager_new.py` 的 `get_deployment_status` 方法 - -对于基于 Deployment 的实现,同样增加了对底层 Pod 容器的健康检查。 - -### 3. 更新 `app.py` 的 `PodStatusResponse` 模型 - -添加了新字段以支持响应中的健康状态信息。 - -## 使用方法 - -### 查询 Agent 状态 - -```bash -curl http://localhost:8000/agents/my-mysql-agenty/status -``` - -### 示例响应(健康状态) - -```json -{ - "name": "my-mysql-agenty", - "namespace": "ai-agents", - "status": "Running", - "health_status": "healthy", - "containers": [ - { - "name": "mysql-agent", - "ready": true, - "restart_count": 0, - "state": "running", - "started_at": "2026-01-06T10:00:00Z" - } - ], - ... -} -``` - -### 示例响应(崩溃状态) - -```json -{ - "name": "my-mysql-agenty", - "namespace": "ai-agents", - "status": "Terminated", - "health_status": "unhealthy", - "containers": [ - { - "name": "mysql-agent", - "ready": false, - "restart_count": 3, - "state": "terminated", - "reason": "Error", - "exit_code": 1, - "message": "Connection refused", - "finished_at": "2026-01-06T10:30:00Z" - } - ], - ... -} -``` - -### 示例响应(等待状态) - -```json -{ - "name": "my-mysql-agenty", - "namespace": "ai-agents", - "status": "Waiting", - "health_status": "unhealthy", - "containers": [ - { - "name": "mysql-agent", - "ready": false, - "restart_count": 2, - "state": "waiting", - "reason": "CrashLoopBackOff", - "message": "Back-off restarting failed container" - } - ], - ... -} -``` - -## 测试 - -运行测试脚本验证修复: - -```bash -# 设置环境变量 -export API_URL="http://localhost:8000" -export AGENT_NAME="my-mysql-agenty" - -# 运行测试 -./test_health_check.sh -``` - -## 重启服务 - -修复后需要重启 agent-manager 服务以应用更改: - -```bash -# 如果使用 systemd -sudo systemctl restart agent-manager - -# 或者如果直接运行 -pkill -f "uvicorn.*app:app" -uvicorn app:app --host 0.0.0.0 --port 8000 --reload -``` - -## 注意事项 - -1. **向后兼容性**: - - 原有的 `status` 字段保持不变 - - 新增的 `health_status` 字段不会影响现有客户端 - -2. **建议**: - - 在监控和告警系统中使用 `health_status` 而非 `status` - - 检查 `containers` 数组获取详细的失败原因 - -3. **健康状态判断优先级**: - - 任何容器 unhealthy → 整体 unhealthy - - 任何容器 degraded(且无 unhealthy)→ 整体 degraded - - 所有容器 healthy → 整体 healthy diff --git a/K8S_DEPLOYMENT_GUIDE.sh b/K8S_DEPLOYMENT_GUIDE.sh new file mode 100755 index 0000000..25786fe --- /dev/null +++ b/K8S_DEPLOYMENT_GUIDE.sh @@ -0,0 +1,166 @@ +#!/bin/bash + +############################################################################## +# Agent Manager Kubernetes 部署指南 +############################################################################## + +cat << 'EOF' +╔════════════════════════════════════════════════════════════════╗ +║ Agent Manager - Kubernetes 部署指南 (ARM64) ║ +╔════════════════════════════════════════════════════════════════╝ + +📋 部署前准备 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +1. 确保已安装必要工具: + ✓ Docker (支持 buildx) + ✓ kubectl + ✓ Azure CLI (az) + +2. 配置 Azure 凭据: + export AZURE_TENANT_ID="your-tenant-id" + export AZURE_CLIENT_ID="your-client-id" + export AZURE_CLIENT_SECRET="your-client-secret" + export AZURE_SUBSCRIPTION_ID="your-subscription-id" + export AZURE_RESOURCE_GROUP="your-resource-group" + +3. 配置 ACR 凭据 (如果使用私有镜像): + export ACR_USERNAME="your-acr-username" + export ACR_PASSWORD="your-acr-password" + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +🚀 部署方式 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +方式 1: 完整部署(构建 + 部署) + ./deploy-to-k8s-arm64.sh + +方式 2: 快速部署(仅部署,使用已有镜像) + ./quick-deploy-k8s.sh + +方式 3: 跳过镜像构建 + ./deploy-to-k8s-arm64.sh --skip-build + +方式 4: 仅构建镜像 + ./deploy-to-k8s-arm64.sh --skip-deploy + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📝 配置说明 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +1. ConfigMap (k8s/agent-manager-configmap.yaml): + - DATABASE_URL: PostgreSQL 连接字符串 + - NAMESPACE: 默认命名空间 + - AZURE_DNS_ZONE: DNS 域名 + +2. Secret (k8s/agent-manager-secret.yaml): + - AZURE_TENANT_ID: Azure 租户 ID + - AZURE_CLIENT_ID: Azure 客户端 ID + - AZURE_CLIENT_SECRET: Azure 客户端密钥 + +3. Deployment (k8s/agent-manager-deployment.yaml): + - replicas: 副本数量(默认 2) + - resources: 资源限制 + - nodeSelector: ARM64 节点选择器 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +🔍 验证部署 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +1. 查看 Pods 状态: + kubectl get pods -n agent-manager + +2. 查看服务: + kubectl get svc -n agent-manager + +3. 获取外网 IP: + kubectl get svc agent-manager -n agent-manager \ + -o jsonpath='{.status.loadBalancer.ingress[0].ip}' + +4. 查看日志: + kubectl logs -n agent-manager -l app=agent-manager --tail=100 + +5. 测试访问: + EXTERNAL_IP=$(kubectl get svc agent-manager -n agent-manager \ + -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + curl http://$EXTERNAL_IP/ + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +🔧 常用管理命令 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +# 扩容/缩容 +kubectl scale deployment agent-manager -n agent-manager --replicas=3 + +# 重启 Pod +kubectl rollout restart deployment agent-manager -n agent-manager + +# 查看部署状态 +kubectl rollout status deployment agent-manager -n agent-manager + +# 查看详细信息 +kubectl describe deployment agent-manager -n agent-manager + +# 进入容器 +kubectl exec -it -n agent-manager \ + $(kubectl get pod -n agent-manager -l app=agent-manager -o jsonpath='{.items[0].metadata.name}') \ + -- /bin/bash + +# 更新镜像 +kubectl set image deployment/agent-manager \ + agent-manager=agnettaiji.azurecr.io/agent-manager:new-tag \ + -n agent-manager + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +🗑️ 卸载 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +完全卸载 Agent Manager: + ./undeploy-k8s.sh + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📂 目录结构 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +agent-manager/ +├── Dockerfile.arm64 # ARM64 架构 Dockerfile +├── deploy-to-k8s-arm64.sh # 完整部署脚本 +├── quick-deploy-k8s.sh # 快速部署脚本 +├── undeploy-k8s.sh # 卸载脚本 +└── k8s/ + ├── agent-manager-namespace.yaml # 命名空间 + ├── agent-manager-configmap.yaml # 配置 + ├── agent-manager-secret.yaml # 密钥 + ├── agent-manager-deployment.yaml # 部署 + ├── agent-manager-service.yaml # 服务 + └── agent-manager-rbac.yaml # 权限 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +⚠️ 注意事项 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +1. ARM64 节点: 确保 K8s 集群有 ARM64 架构的节点 +2. LoadBalancer: 需要云平台支持 LoadBalancer 类型的 Service +3. 数据库: PostgreSQL 需要可从 K8s 集群访问 +4. 权限: Agent Manager 需要集群级别权限来管理其他 Pods +5. 镜像: 首次部署需要先构建并推送 ARM64 镜像到 ACR + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📖 更多信息 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +- API 文档: API_DOCUMENTATION.md +- PostgreSQL 迁移: POSTGRESQL_MIGRATION.md +- 快速参考: QUICK_REFERENCE_PGSQL.md + +╚════════════════════════════════════════════════════════════════╝ + +EOF diff --git a/METRICS_FIX_REPORT.md b/METRICS_FIX_REPORT.md deleted file mode 100644 index da87ad2..0000000 --- a/METRICS_FIX_REPORT.md +++ /dev/null @@ -1,194 +0,0 @@ -## 实时 Metrics 功能修复报告 - -### 📋 问题描述 - -**原问题**:`/agents/{agent_name}/metrics` 接口返回的 metrics 始终一致,只显示 Pod 的资源配额(requests/limits),而不是实时的资源使用情况。 - -### ✅ 修复内容 - -#### 1. 修改 `k8s_manager.py::get_pod_metrics()` 方法 - -**修改前**: -```python -def get_pod_metrics(self, pod_name: str) -> Dict: - 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": {...}, # 静态配额 - "limits": {...} # 静态配额 - } -``` - -**修改后**: -```python -def get_pod_metrics(self, pod_name: str) -> Dict: - # 1. 获取静态配额 - pod = self.v1.read_namespaced_pod(...) - resources = pod.spec.containers[0].resources - - # 2. 获取实时使用情况(通过 metrics.k8s.io API) - from kubernetes.client import CustomObjectsApi - custom_api = CustomObjectsApi() - metrics = custom_api.get_namespaced_custom_object( - group="metrics.k8s.io", - version="v1beta1", - namespace=self.namespace, - plural="pods", - name=pod_name - ) - - # 3. 返回完整数据 - return { - "name": pod_name, - "namespace": self.namespace, - "requests": {...}, - "limits": {...}, - "usage": { # 🆕 实时使用 - "cpu": "14502n", - "memory": "8704Ki" - }, - "timestamp": "..." # 🆕 更新时间 - } -``` - -#### 2. 更新 `app.py::PodMetricsResponse` 模型 - -**修改前**: -```python -class PodMetricsResponse(BaseModel): - name: str - requests: Dict - limits: Dict -``` - -**修改后**: -```python -class PodMetricsResponse(BaseModel): - name: str - namespace: Optional[str] = None - requests: Dict - limits: Dict - usage: Optional[Dict] = None # 🆕 实时使用 - timestamp: Optional[str] = None # 🆕 时间戳 - metrics_available: Optional[bool] = None # 🆕 可用性标志 -``` - -#### 3. 更新 API 文档 - -在 `API_DOCUMENTATION.md` 中添加了详细的字段说明和单位解释。 - -### 📊 测试结果 - -#### 测试 1: 单个 Agent 多次查询 - -```bash -# 查询 alice-echo 三次 -测试 1: CPU=13812n, Memory=8704Ki, Time=2026-01-06T05:00:18Z -测试 2: CPU=14502n, Memory=8704Ki, Time=2026-01-06T05:01:04Z -测试 3: CPU=15234n, Memory=8704Ki, Time=2026-01-06T05:02:18Z -``` - -✅ **结果**:CPU 使用率实时变化,时间戳更新 - -#### 测试 2: 多个 Agent 对比 - -| Agent | CPU 使用 | 内存使用 | CPU 限制 | 内存限制 | -|-------|----------|----------|----------|----------| -| alice-echo | 14502n (0.014m) | 8704Ki (8.5Mi) | 500m | 512Mi | -| bob-chat | 10010n (0.010m) | 10840Ki (10.6Mi) | 500m | 512Mi | -| carol-code | 5330n (0.005m) | 10868Ki (10.6Mi) | 500m | 512Mi | -| jina-search | **897912n (0.897m)** | **41416Ki (40.4Mi)** | 500m | 512Mi | -| my-agent | 14780n (0.015m) | 8688Ki (8.5Mi) | 500m | 512Mi | - -✅ **结果**:不同 Agent 显示不同的实时使用情况 - -### 🔍 技术细节 - -#### Metrics API 调用 - -```python -# Kubernetes Metrics API 端点 -GET /apis/metrics.k8s.io/v1beta1/namespaces/{namespace}/pods/{pod_name} - -# 响应格式 -{ - "kind": "PodMetrics", - "apiVersion": "metrics.k8s.io/v1beta1", - "metadata": {...}, - "timestamp": "2026-01-06T05:01:04Z", - "containers": [ - { - "name": "echo-agent", - "usage": { - "cpu": "14502n", - "memory": "8704Ki" - } - } - ] -} -``` - -#### 单位说明 - -**CPU**: -- `n` (nanocores): 1 核 = 1,000,000,000 nanocores -- `m` (millicores): 1 核 = 1,000 millicores -- 转换: `14502n = 0.014502m ≈ 0.000014 核` - -**内存**: -- `Ki` (Kibibytes): 1 KiB = 1024 bytes -- `Mi` (Mebibytes): 1 MiB = 1024 KiB -- 转换: `8704Ki = 8.5 MiB ≈ 8.9 MB` - -### 🎯 功能特性 - -1. **实时监控**:通过 Kubernetes metrics-server 获取实时数据 -2. **降级支持**:如果 metrics-server 不可用,仍返回配额信息 -3. **时间戳**:显示 metrics 数据的更新时间 -4. **完整信息**:同时显示配额(limits/requests)和使用(usage) - -### 📝 使用示例 - -```bash -# 获取单个 Agent 的 metrics -curl http://localhost:8000/agents/alice-echo/metrics - -# 监控 CPU 使用率 -watch -n 5 'curl -s http://localhost:8000/agents/alice-echo/metrics | jq ".usage.cpu"' - -# 对比多个 Agent -for agent in alice-echo bob-chat carol-code; do - echo "$agent:" - curl -s http://localhost:8000/agents/$agent/metrics | jq ".usage" -done -``` - -### ⚠️ 注意事项 - -1. **metrics-server 依赖**:需要集群安装 metrics-server - ```bash - kubectl get deployment metrics-server -n kube-system - ``` - -2. **更新频率**:metrics-server 通常每 15-60 秒更新一次数据 - -3. **网络延迟**:metrics API 调用可能增加约 50-200ms 响应时间 - -4. **权限要求**:需要 kubeconfig 有权限访问 metrics.k8s.io API - -### ✅ 修复完成 - -- [x] 修改 `k8s_manager.py::get_pod_metrics()` -- [x] 更新 `app.py::PodMetricsResponse` 模型 -- [x] 更新 API 文档 -- [x] 创建测试脚本 `test_realtime_metrics.sh` -- [x] 验证多个 Agent 的实时数据 -- [x] 确认 CPU/内存使用率实时变化 - -**问题状态**: ✅ 已解决 - -**修复时间**: 2026-01-06 - -**测试通过**: ✅ 5/5 Agents 显示实时数据 diff --git a/MULTI_FRAMEWORK_SUMMARY.md b/MULTI_FRAMEWORK_SUMMARY.md deleted file mode 100644 index efeb9dc..0000000 --- a/MULTI_FRAMEWORK_SUMMARY.md +++ /dev/null @@ -1,353 +0,0 @@ -# Azure Blob Agent 多框架实现总结 - -## 📋 概述 - -本次更新为 Azure Blob Storage Agent 实现了三种框架支持: -1. **LangChain 版本** (已有) - 使用 LangChain + LiteLLM -2. **MCP 版本** (新增) - 使用 Model Context Protocol -3. **A2A 版本** (新增) - 使用 Agent-to-Agent 框架 - -## 🆕 新增文件 - -### Agent 实现 - -| 文件 | 说明 | -|------|------| -| `azure_blob_agent_mcp.py` | MCP 框架版本的 Agent 实现 | -| `azure_blob_agent_a2a.py` | A2A 框架版本的 Agent 实现 | - -### Docker 相关 - -| 文件 | 说明 | -|------|------| -| `azure_blob_agent_mcp.Dockerfile` | MCP 版本的 Dockerfile | -| `azure_blob_agent_a2a.Dockerfile` | A2A 版本的 Dockerfile | -| `requirements_mcp.txt` | MCP 版本的依赖 | -| `requirements_a2a.txt` | A2A 版本的依赖 | -| `build_azure_blob_mcp.sh` | MCP 版本构建脚本 | -| `build_azure_blob_a2a.sh` | A2A 版本构建脚本 | - -### 文档和测试 - -| 文件 | 说明 | -|------|------| -| `MULTI_FRAMEWORK_GUIDE.md` | 多框架使用指南 | -| `test_multi_framework.sh` | 多框架集成测试脚本 | - -## 🔄 修改的文件 - -### 数据库层 - -**database.py** - 扩展了数据模型: - -#### Template 模型新增字段: -- `agent_framework` - Agent 框架类型 (langchain/mcp/a2a) -- `tools_config` - 工具配置 JSON -- `default_model_provider` - 默认模型提供商 -- `default_model_name` - 默认模型名称 - -#### Agent 模型新增字段: -- `agent_framework` - Agent 框架类型 -- `tools_config` - 工具配置 -- `tool_endpoint` - 工具端点 URL -- `tool_api_key` - 工具 API 密钥 -- `model_provider` - 模型提供商 -- `model_name` - 模型名称 -- `model_endpoint` - 模型端点 -- `model_api_key` - 模型 API 密钥 -- `storage_connection_string` - 存储连接字符串 -- `storage_account_name` - 存储账户名称 - -### API 层 - -**app.py** - 扩展了请求模型: - -#### CreateTemplateRequest 新增字段: -```python -agent_framework: str = "langchain" -tools_config: Optional[Dict] = {} -default_model_provider: Optional[str] = None -default_model_name: Optional[str] = None -``` - -#### CreatePlatformAgentRequest 新增字段: -```python -namespace: Optional[str] = "ai-agents" -agent_framework: Optional[str] = None -tools_config: Optional[Dict] = {} -tool_endpoint: Optional[str] = None -tool_api_key: Optional[str] = None -model_provider: Optional[str] = None -model_name: Optional[str] = None -model_endpoint: Optional[str] = None -model_api_key: Optional[str] = None -storage_connection_string: Optional[str] = None -storage_account_name: Optional[str] = None -``` - -#### CreateCustomAgentRequest 同样新增了上述字段 - -### Kubernetes 层 - -**k8s_manager.py** - 扩展了部署逻辑: - -#### _generate_pod_manifest 方法更新: -- 支持传递框架类型到容器环境变量 -- 支持传递工具配置 (tools_config, tool_endpoint, tool_api_key) -- 支持传递模型配置 (model_provider, model_name, model_endpoint, model_api_key) -- 支持传递存储配置 (storage_connection_string, storage_account_name) -- 支持传递用户标识 (user_id, tenant_id) -- 支持自定义命名空间 - -#### 新增镜像映射: -```python -"azure_blob_agent_mcp": "agnettaiji.azurecr.io/ai-agents/azure-blob-agent-mcp:latest" -"azure_blob_agent_a2a": "agnettaiji.azurecr.io/ai-agents/azure-blob-agent-a2a:latest" -``` - -#### 新增端口映射: -```python -"azure_blob_agent_mcp": 8080 -"azure_blob_agent_a2a": 8080 -``` - -#### 新增环境变量说明(用于文档) - -## 🏗️ 架构设计 - -### 参数传递流程 - -``` -用户请求 (API) - ↓ -app.py (API 层) - ├─ 验证参数 - ├─ 保存到数据库 (database.py) - └─ 调用 K8sManager - ↓ -k8s_manager.py (K8s 层) - ├─ 构建环境变量 - │ ├─ AGENT_FRAMEWORK - │ ├─ TOOLS_CONFIG - │ ├─ MODEL_* - │ ├─ STORAGE_* - │ └─ USER_ID, TENANT_ID, NAMESPACE - ├─ 创建 Pod/Deployment - └─ 传递到容器 - ↓ -Agent 容器 (azure_blob_agent_*.py) - ├─ 读取环境变量 - ├─ 初始化框架 - ├─ 配置工具 - ├─ 连接存储 - └─ 提供 API 服务 -``` - -### 框架特性对比 - -| 特性 | LangChain | MCP | A2A | -|------|-----------|-----|-----| -| **实现文件** | azure_blob_agent.py | azure_blob_agent_mcp.py | azure_blob_agent_a2a.py | -| **工具定义** | LangChain Tools | MCP Tool Classes | A2A Action Handlers | -| **API 端点** | /query | /mcp/tools, /mcp/call | /a2a/capabilities, /a2a/message | -| **协作能力** | ❌ | ❌ | ✅ Agent 注册和通信 | -| **工具发现** | 内置 | GET /mcp/tools | GET /a2a/capabilities | -| **消息格式** | 自然语言 | MCP Protocol | A2A Message Protocol | -| **依赖** | langchain, litellm | fastapi, pydantic | fastapi, httpx | - -## 📝 数据库迁移 - -提供了迁移脚本 `migrate_multi_framework.py`: - -```bash -python migrate_multi_framework.py -``` - -支持: -- ✅ SQLite (开发环境) -- ✅ PostgreSQL (生产环境) -- ✅ 自动检测已存在字段 -- ✅ 验证迁移结果 - -## 🚀 部署流程 - -### 1. 构建镜像 - -```bash -cd agent_templates - -# 构建 MCP 版本 -./build_azure_blob_mcp.sh - -# 构建 A2A 版本 -./build_azure_blob_a2a.sh -``` - -### 2. 运行数据库迁移 - -```bash -python migrate_multi_framework.py -``` - -### 3. 创建 Agent - -```bash -# 创建 MCP Agent -curl -X POST http://agent-manager:8000/v2/agents/platform \ - -H "Content-Type: application/json" \ - -d @mcp_agent_config.json - -# 创建 A2A Agent -curl -X POST http://agent-manager:8000/v2/agents/platform \ - -H "Content-Type: application/json" \ - -d @a2a_agent_config.json -``` - -### 4. 测试 - -```bash -./test_multi_framework.sh -``` - -## 🔧 环境变量配置示例 - -### MCP Agent - -```bash -# 框架配置 -AGENT_FRAMEWORK=mcp -TEMPLATE_TYPE=azure_blob_agent_mcp - -# 工具配置 -TOOLS_CONFIG='{"enabled_tools": ["list_containers", "list_blobs"]}' - -# 模型配置 -MODEL_PROVIDER=openai -MODEL_NAME=gpt-4 -MODEL_API_KEY=sk-xxxx -MODEL_ENDPOINT=https://api.openai.com/v1 - -# 存储配置 -AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;... -STORAGE_ACCOUNT_NAME=myaccount - -# 用户信息 -USER_ID=user123 -TENANT_ID=tenant456 -NAMESPACE=ai-agents -``` - -### A2A Agent - -```bash -# 框架配置 -AGENT_FRAMEWORK=a2a -TEMPLATE_TYPE=azure_blob_agent_a2a - -# Agent 身份 -AGENT_ID=blob-agent-001 -AGENT_ROLE=storage_manager -AGENT_CAPABILITIES='["blob_storage", "file_operations"]' - -# 模型配置 -MODEL_PROVIDER=azure-openai -MODEL_NAME=gpt-4 -MODEL_API_KEY=xxxx -MODEL_ENDPOINT=https://myopenai.openai.azure.com - -# 存储配置 -AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;... - -# 用户信息 -USER_ID=user123 -TENANT_ID=tenant456 -NAMESPACE=ai-agents -``` - -## 🎯 使用场景 - -### LangChain 版本 -- ✅ 复杂的推理任务 -- ✅ 多步骤文件处理 -- ✅ 与现有 LangChain 应用集成 - -### MCP 版本 -- ✅ 标准化工具调用 -- ✅ 跨平台工具共享 -- ✅ 轻量级集成 - -### A2A 版本 -- ✅ 多 Agent 协作 -- ✅ 分布式任务处理 -- ✅ Agent 间通信 - -## 📚 API 端点对比 - -### LangChain -- `POST /query` - 自然语言查询 -- `GET /health` - 健康检查 -- `POST /connect` - 连接存储 - -### MCP -- `GET /mcp/tools` - 列出可用工具 -- `POST /mcp/call` - 调用工具 -- `POST /query` - 查询(简化版) -- `GET /health` - 健康检查 -- `POST /connect` - 连接存储 - -### A2A -- `GET /a2a/capabilities` - 获取能力 -- `POST /a2a/register` - 注册其他 Agent -- `GET /a2a/agents` - 列出已注册 Agent -- `POST /a2a/message` - 处理 A2A 消息 -- `POST /a2a/collaborate` - 与其他 Agent 协作 -- `POST /query` - 查询 -- `GET /health` - 健康检查 -- `POST /connect` - 连接存储 - -## ✅ 测试清单 - -- [ ] 数据库迁移成功 -- [ ] MCP 镜像构建成功 -- [ ] A2A 镜像构建成功 -- [ ] MCP Agent 创建成功 -- [ ] A2A Agent 创建成功 -- [ ] MCP 工具调用正常 -- [ ] A2A 消息处理正常 -- [ ] 健康检查通过 -- [ ] 存储连接正常 -- [ ] 环境变量正确传递 - -## 🐛 已知问题 - -1. **LLM 集成**: MCP 和 A2A 版本目前使用简单规则匹配,需要集成实际 LLM 进行意图识别 -2. **安全性**: API 密钥等敏感信息应加密存储 -3. **日志**: 需要统一的日志收集和监控 - -## 🔮 未来改进 - -1. **安全增强** - - 密钥加密存储 - - RBAC 权限控制 - - API 密钥轮换 - -2. **功能扩展** - - 更多 Azure 服务集成 - - 自定义工具注册 - - 工具组合和编排 - -3. **监控和调试** - - 分布式追踪 - - 性能监控 - - 调试工具 - -4. **开发体验** - - Web UI 管理界面 - - 可视化工具设计器 - - Agent 模板市场 - -## 📖 相关文档 - -- [多框架使用指南](agent_templates/MULTI_FRAMEWORK_GUIDE.md) -- [API 文档](API_DOCUMENTATION.md) -- [Azure Blob Agent 原始文档](agent_templates/AZURE_BLOB_AGENT_USAGE.md) diff --git a/QUICK_START_K8S.sh b/QUICK_START_K8S.sh new file mode 100755 index 0000000..25fa052 --- /dev/null +++ b/QUICK_START_K8S.sh @@ -0,0 +1,167 @@ +#!/bin/bash + +cat << 'EOF' +╔══════════════════════════════════════════════════════════════════╗ +║ Agent Manager - Kubernetes 部署快速开始 ║ +╚══════════════════════════════════════════════════════════════════╝ + +📦 已创建的文件列表 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +核心部署文件: + ✓ Dockerfile.arm64 - ARM64 架构 Docker 镜像 + ✓ deploy-to-k8s-arm64.sh - 完整部署脚本(构建+部署) + ✓ quick-deploy-k8s.sh - 快速部署脚本(仅部署) + ✓ undeploy-k8s.sh - 卸载脚本 + ✓ K8S_DEPLOYMENT_GUIDE.sh - 部署指南 + +Kubernetes 配置文件 (k8s/): + ✓ agent-manager-namespace.yaml - 命名空间定义 + ✓ agent-manager-configmap.yaml - 配置信息 + ✓ agent-manager-secret.yaml - 敏感凭据 + ✓ agent-manager-deployment.yaml - 部署定义(2副本,ARM64) + ✓ agent-manager-service.yaml - LoadBalancer 服务 + ✓ agent-manager-rbac.yaml - 集群权限 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +🚀 三步快速部署 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +步骤 1: 配置 Azure 凭据(必需) +──────────────────────────────────────────────────────────── + +export AZURE_TENANT_ID="your-tenant-id" +export AZURE_CLIENT_ID="your-client-id" +export AZURE_CLIENT_SECRET="your-client-secret" +export AZURE_SUBSCRIPTION_ID="your-subscription-id" +export AZURE_RESOURCE_GROUP="your-resource-group" + +步骤 2: 配置 ACR 凭据(如果使用私有镜像仓库) +──────────────────────────────────────────────────────────── + +# 方式 1: 手动设置 +export ACR_USERNAME="your-acr-username" +export ACR_PASSWORD="your-acr-password" + +# 方式 2: 从 Azure CLI 自动获取 +export ACR_USERNAME=$(az acr credential show --name agnettaiji --query username -o tsv) +export ACR_PASSWORD=$(az acr credential show --name agnettaiji --query passwords[0].value -o tsv) + +步骤 3: 执行部署 +──────────────────────────────────────────────────────────── + +# 完整部署(构建 ARM64 镜像 + 部署到 K8s) +./deploy-to-k8s-arm64.sh + +# 或者,如果镜像已存在,仅部署 +./quick-deploy-k8s.sh + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +✅ 部署后验证 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +1. 查看 Pods 状态: + kubectl get pods -n agent-manager -w + +2. 获取外网访问地址: + EXTERNAL_IP=$(kubectl get svc agent-manager -n agent-manager \ + -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo "Agent Manager URL: http://$EXTERNAL_IP" + +3. 测试 API: + curl http://$EXTERNAL_IP/ + curl http://$EXTERNAL_IP/agents + +4. 查看日志: + kubectl logs -n agent-manager -l app=agent-manager --tail=100 -f + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +🔧 常见问题排查 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +问题: Pod 处于 Pending 状态 +解决: 检查是否有 ARM64 节点 + kubectl get nodes -o wide + kubectl describe pod -n agent-manager + +问题: ImagePullBackOff +解决: 检查 ACR 凭据 + kubectl get secret acr-secret -n agent-manager -o yaml + kubectl describe pod -n agent-manager + +问题: CrashLoopBackOff +解决: 查看日志找出错误原因 + kubectl logs -n agent-manager + kubectl describe pod -n agent-manager + +问题: LoadBalancer IP 长时间未分配 +解决: 检查云平台 LoadBalancer 支持 + kubectl describe svc agent-manager -n agent-manager + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📊 架构说明 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +部署架构: + ┌─────────────────────────────────────────────┐ + │ LoadBalancer Service │ + │ (外网 IP: xxx.xxx.xxx.xxx) │ + └─────────────────┬───────────────────────────┘ + │ Port 80 + ┌─────────────────┴───────────────────────────┐ + │ Agent Manager Deployment │ + │ (2 Replicas) │ + ├─────────────────────────────────────────────┤ + │ Pod 1 (ARM64) │ Pod 2 (ARM64) │ + │ - FastAPI │ - FastAPI │ + │ - K8s Client │ - K8s Client │ + │ - PostgreSQL │ - PostgreSQL │ + │ - Azure SDK │ - Azure SDK │ + └─────────────────────────────────────────────┘ + │ + ┌─────────────────┴───────────────────────────┐ + │ PostgreSQL (Azure Database) │ + │ taijipda.postgres.database.azure.com │ + └─────────────────────────────────────────────┘ + +关键特性: + ✓ ARM64 架构支持(优化性能和成本) + ✓ 双副本高可用部署 + ✓ LoadBalancer 自动外网访问 + ✓ 集群级别权限(管理其他 Agents) + ✓ ConfigMap/Secret 配置管理 + ✓ 健康检查和自动重启 + ✓ 资源限制(CPU: 200m-500m, Memory: 256Mi-512Mi) + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +🔗 相关文档 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +查看完整部署指南: + ./K8S_DEPLOYMENT_GUIDE.sh + +查看 API 文档: + cat API_DOCUMENTATION.md + +查看 PostgreSQL 配置: + cat POSTGRESQL_MIGRATION.md + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +💡 提示 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +1. 首次部署建议使用完整部署脚本 ./deploy-to-k8s-arm64.sh +2. 确保 Kubernetes 集群有 ARM64 节点 +3. 修改 k8s/agent-manager-configmap.yaml 设置数据库连接 +4. 修改 k8s/agent-manager-secret.yaml 设置 Azure 凭据 +5. 部署完成后记录外网 IP 地址 + +╚══════════════════════════════════════════════════════════════════╝ + +EOF diff --git a/README.md b/README.md deleted file mode 100644 index 88575ba..0000000 --- a/README.md +++ /dev/null @@ -1,860 +0,0 @@ -# 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/ZOMBIE_PROCESS_FIX.md b/ZOMBIE_PROCESS_FIX.md deleted file mode 100644 index b70bb9a..0000000 --- a/ZOMBIE_PROCESS_FIX.md +++ /dev/null @@ -1,126 +0,0 @@ -# 僵尸进程和 CPU 100% 问题修复方案 - -## 问题诊断 - -**容器**: taiji-mcp-server (ID: 64d363729ff9) -**进程**: PID 3411601, CPU 100% -**根因**: Docker 健康检查导致的僵尸进程泄漏 (800+ defunct curl 进程) - -## 立即修复步骤 - -### 方案 1: 重启容器(最快) - -```bash -# 重启容器,清理僵尸进程 -docker restart taiji-mcp-server - -# 检查状态 -docker ps | grep taiji-mcp-server -``` - -### 方案 2: 临时禁用健康检查 - -```bash -# 停止容器 -docker stop taiji-mcp-server - -# 使用 --no-healthcheck 重新启动 -docker run -d --name taiji-mcp-server-temp \ - --no-healthcheck \ - -p 8002:8000 \ - taiji-ai-pad-mcp-server - -# 或修改 docker-compose.yml,注释掉 healthcheck -``` - -## 长期修复方案 - -### 方案 A: 使用 Python 内置健康检查(推荐) - -不依赖外部 curl 命令,避免子进程问题: - -**Dockerfile 修改**: -```dockerfile -HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()" || exit 1 -``` - -### 方案 B: 使用 tini 或 dumb-init(推荐) - -正确处理子进程回收: - -**Dockerfile 修改**: -```dockerfile -# 安装 tini -RUN apt-get update && apt-get install -y tini - -# 使用 tini 作为 init 进程 -ENTRYPOINT ["/usr/bin/tini", "--"] -CMD ["python3", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] - -# 健康检查保持不变 -HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD curl -f http://localhost:8000/health || exit 1 -``` - -### 方案 C: 修改健康检查端点,减少数据库连接 - -**main.py 修改** (假设你有 `/health` 端点): -```python -@app.get("/health") -async def health_check(): - """轻量级健康检查,不连接数据库""" - return {"status": "healthy", "timestamp": datetime.now().isoformat()} - -@app.get("/health/deep") -async def deep_health_check(): - """深度健康检查,包含数据库连接测试""" - try: - # 测试数据库连接 - db = next(get_db()) - db.execute(text("SELECT 1")) - return {"status": "healthy", "database": "connected"} - except Exception as e: - raise HTTPException(status_code=503, detail=f"Unhealthy: {str(e)}") -``` - -### 方案 D: 调整健康检查频率 - -如果服务稳定,可以降低检查频率: - -```dockerfile -HEALTHCHECK --interval=60s --timeout=10s --start-period=40s --retries=3 \ - CMD curl -f http://localhost:8000/health || exit 1 -``` - -## 验证修复 - -```bash -# 1. 检查容器健康状态 -docker ps | grep taiji-mcp-server - -# 2. 检查僵尸进程数量 -docker exec taiji-mcp-server ps aux | grep defunct | wc -l - -# 3. 检查 CPU 占用 -docker stats --no-stream taiji-mcp-server - -# 4. 检查日志 -docker logs --tail 100 taiji-mcp-server -``` - -## 监控建议 - -```bash -# 定期检查僵尸进程 -watch -n 5 'docker exec taiji-mcp-server ps aux | grep defunct | wc -l' - -# 监控资源使用 -docker stats taiji-mcp-server -``` - -## 参考资料 - -- Docker 僵尸进程问题: https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/ -- tini 项目: https://github.com/krallin/tini -- dumb-init: https://github.com/Yelp/dumb-init diff --git a/agent_manager.db.backup.20260113_054750 b/agent_manager.db.backup.20260113_054750 new file mode 100644 index 0000000..de4a0b4 Binary files /dev/null and b/agent_manager.db.backup.20260113_054750 differ diff --git a/agent_manager.db.backup.20260113_054803 b/agent_manager.db.backup.20260113_054803 new file mode 100644 index 0000000..9bfb32a Binary files /dev/null and b/agent_manager.db.backup.20260113_054803 differ diff --git a/agent_manager/templates/search_agent.yaml b/agent_manager/templates/search_agent.yaml new file mode 100644 index 0000000..8dcf0c7 --- /dev/null +++ b/agent_manager/templates/search_agent.yaml @@ -0,0 +1,91 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ name }} + namespace: {{ namespace }} + labels: + app: {{ name }} + type: search-agent + managed-by: agent-manager + user-id: {{ user_id }} +spec: + replicas: {{ replicas | default(1) }} + selector: + matchLabels: + app: {{ name }} + template: + metadata: + labels: + app: {{ name }} + type: search-agent + managed-by: agent-manager + user-id: {{ user_id }} + spec: + imagePullSecrets: + - name: acr-secret + containers: + - name: search-agent + image: {{ image }} + ports: + - containerPort: 8080 + name: http + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: TEMPLATE_TYPE + value: "search_agent" + - name: SERVICE_HOST + value: "0.0.0.0" + - name: SERVICE_PORT + value: "8080" + {% if env_vars %} + {% for key, value in env_vars.items() %} + - name: {{ key }} + value: "{{ value }}" + {% endfor %} + {% endif %} + resources: + requests: + cpu: {{ resources.cpu_request | default("500m") }} + memory: {{ resources.memory_request | default("512Mi") }} + limits: + cpu: {{ resources.cpu_limit | default("1000m") }} + memory: {{ resources.memory_limit | default("1Gi") }} + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ name }} + namespace: {{ namespace }} + labels: + app: {{ name }} + type: search-agent + managed-by: agent-manager + user-id: {{ user_id }} +spec: + selector: + app: {{ name }} + ports: + - port: 8080 + targetPort: 8080 + protocol: TCP + name: http + type: ClusterIP diff --git a/agent_templates/AZURE_BLOB_AGENT_USAGE.md b/agent_templates/AZURE_BLOB_AGENT_USAGE.md deleted file mode 100644 index 1a5f993..0000000 --- a/agent_templates/AZURE_BLOB_AGENT_USAGE.md +++ /dev/null @@ -1,364 +0,0 @@ -# Azure Blob Storage AI Agent 使用指南 - -## 概述 - -这是一个基于 LangChain + LiteLLM 的智能 Azure Blob Storage 管理代理,支持: -- 通过 API 动态接收 Azure Storage 连接字符串 -- 使用自然语言查询和管理存储 -- 通过环境变量配置 LLM 模型 - -## 架构说明 - -``` -┌─────────────┐ HTTP API ┌──────────────────┐ Azure SDK ┌─────────────────┐ -│ 客户端 │ ──────────────> │ FastAPI Server │ ──────────────> │ Azure Blob │ -│ │ │ + LangChain │ │ Storage │ -└─────────────┘ │ + LiteLLM │ └─────────────────┘ - └──────────────────┘ - │ - ▼ - ┌──────────────────┐ - │ LiteLLM Server │ - │ (4000端口) │ - └──────────────────┘ -``` - -## 快速开始 - -### 1. 构建镜像 - -```bash -cd /home/taiji/tools/agent-manager/agent_templates - -# 构建镜像 -./build_azure_blob_agent.sh latest - -# 或者手动构建 -docker build -f azure_blob_agent.Dockerfile -t azure-blob-agent:latest . -``` - -### 2. 启动 LiteLLM 服务(如果还没启动) - -确保你的 LiteLLM 服务正在运行,例如: -```bash -# 检查 LiteLLM 是否运行 -curl http://localhost:4000/health - -# 如果没运行,启动它 -docker run -d --name litellm \ - -p 4000:4000 \ - -e OPENAI_API_KEY=your_key \ - ghcr.io/berriai/litellm:latest -``` - -### 3. 启动 Azure Blob Agent - -```bash -docker run -d --name azure-blob-agent \ - -p 8080:8080 \ - -e LITELLM_API_BASE=http://host.docker.internal:4000 \ - -e LITELLM_MODEL=gpt-3.5-turbo \ - -e LITELLM_API_KEY=sk-1234 \ - azure-blob-agent:latest -``` - -**环境变量说明:** -- `LITELLM_API_BASE`: LiteLLM 服务地址 -- `LITELLM_MODEL`: 使用的模型名称 -- `LITELLM_API_KEY`: LiteLLM API 密钥 -- `SERVICE_HOST`: 服务监听地址(默认 0.0.0.0) -- `SERVICE_PORT`: 服务监听端口(默认 8080) - -## API 使用 - -### 1. 健康检查 - -```bash -curl http://localhost:8080/health -``` - -**响应示例:** -```json -{ - "status": "healthy", - "connected": true, - "connection_info": { - "account_kind": "StorageV2", - "sku_name": "Standard_LRS", - "connected_at": "2026-01-08T20:00:00" - } -} -``` - -### 2. 连接到 Azure Storage - -```bash -curl -X POST http://localhost:8080/connect \ - -H 'Content-Type: application/json' \ - -d '{ - "connection_string": "DefaultEndpointsProtocol=https;AccountName=yourname;AccountKey=yourkey;EndpointSuffix=core.windows.net" - }' -``` - -**响应示例:** -```json -{ - "status": "connected", - "message": "成功连接到Azure Blob Storage", - "account_info": { - "account_kind": "StorageV2", - "sku_name": "Standard_LRS" - } -} -``` - -### 3. 自然语言查询 - -#### 列出所有容器 -```bash -curl -X POST http://localhost:8080/query \ - -H 'Content-Type: application/json' \ - -d '{ - "query": "列出所有容器" - }' -``` - -#### 查看容器中的文件 -```bash -curl -X POST http://localhost:8080/query \ - -H 'Content-Type: application/json' \ - -d '{ - "query": "显示 images 容器中的所有文件" - }' -``` - -#### 搜索文件 -```bash -curl -X POST http://localhost:8080/query \ - -H 'Content-Type: application/json' \ - -d '{ - "query": "在 documents 容器中搜索包含 report 的文件" - }' -``` - -#### 获取存储统计 -```bash -curl -X POST http://localhost:8080/query \ - -H 'Content-Type: application/json' \ - -d '{ - "query": "显示存储统计信息" - }' -``` - -#### 获取文件详细信息 -```bash -curl -X POST http://localhost:8080/query \ - -H 'Content-Type: application/json' \ - -d '{ - "query": "获取 images 容器中 logo.png 的详细信息" - }' -``` - -**响应示例:** -```json -{ - "status": "success", - "query": "列出所有容器", - "answer": "当前有3个容器:\n1. images (最后修改: 2026-01-08)\n2. documents (最后修改: 2026-01-07)\n3. backups (最后修改: 2026-01-06)", - "intermediate_steps": "..." -} -``` - -## 在 Kubernetes 中部署 - -### 方法 1: 使用 agent-manager API - -```bash -# 1. 首先确保模板已添加到 k8s_manager.py -# 2. 创建 agent -curl -X POST http://localhost:8000/agents \ - -H 'Content-Type: application/json' \ - -d '{ - "name": "my-blob-agent", - "template": "azure_blob_agent", - "env": { - "LITELLM_API_BASE": "http://litellm-service:4000", - "LITELLM_MODEL": "gpt-3.5-turbo", - "LITELLM_API_KEY": "sk-1234" - } - }' - -# 3. 连接到存储 -curl -X POST http://my-blob-agent-ip:8080/connect \ - -H 'Content-Type: application/json' \ - -d '{ - "connection_string": "YOUR_CONNECTION_STRING" - }' -``` - -### 方法 2: 直接部署 YAML - -创建 `azure-blob-agent-deployment.yaml`: - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: azure-blob-agent - namespace: ai-agents -spec: - replicas: 1 - selector: - matchLabels: - app: azure-blob-agent - template: - metadata: - labels: - app: azure-blob-agent - spec: - containers: - - name: azure-blob-agent - image: agnettaiji.azurecr.io/ai-agents/azure-blob-agent:latest - ports: - - containerPort: 8080 - env: - - name: LITELLM_API_BASE - value: "http://litellm-service:4000" - - name: LITELLM_MODEL - value: "gpt-3.5-turbo" - - name: LITELLM_API_KEY - valueFrom: - secretKeyRef: - name: litellm-secret - key: api-key - resources: - requests: - memory: "256Mi" - cpu: "250m" - limits: - memory: "512Mi" - cpu: "500m" ---- -apiVersion: v1 -kind: Service -metadata: - name: azure-blob-agent-service - namespace: ai-agents -spec: - selector: - app: azure-blob-agent - ports: - - port: 8080 - targetPort: 8080 - type: ClusterIP -``` - -部署: -```bash -kubectl apply -f azure-blob-agent-deployment.yaml -``` - -## 支持的查询示例 - -| 自然语言查询 | 功能 | -|------------|------| -| "列出所有容器" | 显示所有容器列表 | -| "显示 images 容器中的文件" | 列出指定容器的文件 | -| "在 documents 中搜索 report" | 搜索包含关键字的文件 | -| "获取 data/test.csv 的信息" | 显示文件详细信息 | -| "显示存储统计" | 显示整体存储使用情况 | -| "images 容器有多少文件" | 统计容器文件数 | -| "查找所有 .pdf 文件" | 按扩展名搜索 | - -## 故障排查 - -### 1. Agent 启动失败 - -```bash -# 检查日志 -docker logs azure-blob-agent - -# 常见问题: -# - LiteLLM 服务不可达:检查 LITELLM_API_BASE -# - 端口冲突:修改 SERVICE_PORT -``` - -### 2. 连接 Azure Storage 失败 - -```bash -# 检查连接字符串格式 -# 正确格式: -DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey==;EndpointSuffix=core.windows.net - -# 测试连接 -curl -X POST http://localhost:8080/connect \ - -H 'Content-Type: application/json' \ - -d '{"connection_string": "YOUR_STRING"}' -``` - -### 3. 查询返回错误 - -```bash -# 检查是否已连接 -curl http://localhost:8080/health - -# 查看详细日志 -docker logs -f azure-blob-agent -``` - -## 开发与扩展 - -### 添加新工具 - -在 `azure_blob_agent.py` 中添加新的工具函数: - -```python -def download_blob(container_name: str, blob_name: str) -> str: - """下载 blob 内容(示例)""" - # 实现下载逻辑 - pass - -# 在 create_blob_agent() 中添加工具 -tools.append( - Tool( - name="download_blob", - func=lambda input_str: download_blob(*input_str.split(",")), - description="下载指定的文件。输入格式: '容器名,文件名'" - ) -) -``` - -### 自定义模型 - -支持任何 LiteLLM 兼容的模型: - -```bash -# 使用 Claude --e LITELLM_MODEL=claude-3-sonnet-20240229 - -# 使用本地模型 --e LITELLM_MODEL=ollama/llama2 --e LITELLM_API_BASE=http://localhost:11434 - -# 使用 Azure OpenAI --e LITELLM_MODEL=azure/gpt-4 -``` - -## 性能优化 - -1. **连接池**: BlobServiceClient 会自动管理连接池 -2. **缓存**: 可以添加 Redis 缓存常用查询结果 -3. **并发**: 使用 `max_workers` 参数提高并发处理能力 - -## 安全建议 - -1. **连接字符串**: 不要在代码中硬编码,使用环境变量或 K8s Secrets -2. **访问控制**: 使用 SAS token 而非完整连接字符串 -3. **网络隔离**: 在 K8s 中使用 NetworkPolicy 限制访问 -4. **日志脱敏**: 避免记录敏感信息 - -## 更多资源 - -- [Azure Blob Storage Python SDK](https://learn.microsoft.com/azure/storage/blobs/storage-quickstart-blobs-python) -- [LangChain Documentation](https://python.langchain.com/docs/get_started/introduction) -- [LiteLLM Documentation](https://docs.litellm.ai/) diff --git a/agent_templates/MULTI_FRAMEWORK_GUIDE.md b/agent_templates/MULTI_FRAMEWORK_GUIDE.md deleted file mode 100644 index 0e43606..0000000 --- a/agent_templates/MULTI_FRAMEWORK_GUIDE.md +++ /dev/null @@ -1,366 +0,0 @@ -# Azure Blob Agent - 多框架支持使用指南 - -本文档介绍如何使用三种不同框架版本的 Azure Blob Storage AI Agent: -- **LangChain 版本**: 使用 LangChain + LiteLLM -- **MCP 版本**: 使用 Model Context Protocol -- **A2A 版本**: 使用 Agent-to-Agent 框架 - -## 📋 目录 - -1. [框架对比](#框架对比) -2. [部署配置](#部署配置) -3. [API 使用示例](#api-使用示例) -4. [创建 Agent 示例](#创建-agent-示例) - -## 🔍 框架对比 - -| 特性 | LangChain | MCP | A2A | -|------|-----------|-----|-----| -| 工具调用 | LangChain Tools | MCP Protocol | A2A Messages | -| Agent 协作 | ❌ | ❌ | ✅ | -| 结构化输出 | ✅ | ✅ | ✅ | -| 复杂推理 | ✅ | ⚡ 轻量 | ⚡ 轻量 | -| 适用场景 | 复杂任务链 | 标准化工具 | 多Agent协作 | - -## 🚀 部署配置 - -### 1. LangChain 版本 - -```json -{ - "name": "my-blob-agent", - "template_name": "azure_blob_agent", - "owner_id": "user123", - "namespace": "ai-agents", - "agent_framework": "langchain", - "environment_vars": { - "LITELLM_API_BASE": "http://litellm-service:4000", - "LITELLM_MODEL": "gpt-3.5-turbo", - "LITELLM_API_KEY": "sk-xxxx", - "AZURE_STORAGE_CONNECTION_STRING": "DefaultEndpointsProtocol=https;..." - } -} -``` - -### 2. MCP 版本 - -```json -{ - "name": "my-blob-agent-mcp", - "template_name": "azure_blob_agent_mcp", - "owner_id": "user123", - "namespace": "ai-agents", - "agent_framework": "mcp", - "model_provider": "openai", - "model_name": "gpt-4", - "model_api_key": "sk-xxxx", - "model_endpoint": "https://api.openai.com/v1", - "storage_connection_string": "DefaultEndpointsProtocol=https;...", - "tools_config": { - "enabled_tools": ["list_containers", "list_blobs", "search_blobs"] - } -} -``` - -### 3. A2A 版本 - -```json -{ - "name": "my-blob-agent-a2a", - "template_name": "azure_blob_agent_a2a", - "owner_id": "user123", - "namespace": "ai-agents", - "agent_framework": "a2a", - "model_provider": "openai", - "model_name": "gpt-4", - "model_api_key": "sk-xxxx", - "storage_connection_string": "DefaultEndpointsProtocol=https;...", - "environment_vars": { - "AGENT_ID": "blob-agent-001", - "AGENT_ROLE": "storage_manager", - "AGENT_CAPABILITIES": "[\"blob_storage\", \"file_operations\"]" - } -} -``` - -## 📡 API 使用示例 - -### MCP 版本 API - -#### 1. 列出所有可用工具 - -```bash -curl http:///mcp/tools -``` - -响应: -```json -{ - "tools": [ - { - "name": "list_containers", - "description": "列出 Azure Blob Storage 中的所有容器", - "inputSchema": { - "type": "object", - "properties": {}, - "required": [] - } - }, - { - "name": "list_blobs", - "description": "列出指定容器中的所有文件", - "inputSchema": { - "type": "object", - "properties": { - "container_name": { - "type": "string", - "description": "容器名称" - } - }, - "required": ["container_name"] - } - } - ] -} -``` - -#### 2. 调用 MCP 工具 - -```bash -curl -X POST http:///mcp/call \ - -H "Content-Type: application/json" \ - -d '{ - "tool_name": "list_containers", - "parameters": {} - }' -``` - -```bash -curl -X POST http:///mcp/call \ - -H "Content-Type: application/json" \ - -d '{ - "tool_name": "list_blobs", - "parameters": { - "container_name": "my-container" - } - }' -``` - -### A2A 版本 API - -#### 1. 获取 Agent 能力 - -```bash -curl http:///a2a/capabilities -``` - -响应: -```json -{ - "agent_id": "blob-agent-001", - "agent_role": "storage_manager", - "capabilities": ["blob_storage", "file_operations"], - "supported_actions": [ - "list_containers", - "list_blobs", - "get_blob_info", - "search_blobs", - "get_stats" - ] -} -``` - -#### 2. 注册其他 Agent - -```bash -curl -X POST http:///a2a/register \ - -H "Content-Type: application/json" \ - -d '{ - "agent_id": "analytics-agent", - "agent_role": "data_analyzer", - "capabilities": ["data_analysis", "visualization"], - "endpoint": "http://analytics-agent:8080" - }' -``` - -#### 3. 发送 A2A 消息 - -```bash -curl -X POST http:///a2a/message \ - -H "Content-Type: application/json" \ - -d '{ - "message_id": "msg-001", - "from_agent": "external-agent", - "to_agent": "blob-agent-001", - "message_type": "request", - "action": "list_containers", - "parameters": {} - }' -``` - -#### 4. Agent 间协作 - -```bash -curl -X POST http:///a2a/collaborate \ - -H "Content-Type: application/json" \ - -d '{ - "target_agent_id": "analytics-agent", - "action": "analyze_data", - "parameters": { - "data_source": "blob_storage" - } - }' -``` - -## 🛠️ 创建 Agent 示例 - -### 使用 Agent Manager API 创建 - -#### 1. 创建 MCP Agent - -```bash -curl -X POST http://agent-manager:8000/v2/agents/platform \ - -H "Content-Type: application/json" \ - -d '{ - "name": "blob-mcp-001", - "template_name": "azure_blob_agent_mcp", - "owner_id": "user123", - "namespace": "ai-agents", - "agent_framework": "mcp", - "model_provider": "openai", - "model_name": "gpt-4", - "model_api_key": "sk-xxxx", - "storage_connection_string": "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=xxx;EndpointSuffix=core.windows.net", - "tools_config": { - "max_iterations": 5, - "timeout": 30 - } - }' -``` - -#### 2. 创建 A2A Agent - -```bash -curl -X POST http://agent-manager:8000/v2/agents/platform \ - -H "Content-Type: application/json" \ - -d '{ - "name": "blob-a2a-001", - "template_name": "azure_blob_agent_a2a", - "owner_id": "user123", - "namespace": "ai-agents", - "agent_framework": "a2a", - "model_provider": "azure-openai", - "model_name": "gpt-4", - "model_endpoint": "https://myopenai.openai.azure.com", - "model_api_key": "xxxx", - "storage_connection_string": "DefaultEndpointsProtocol=https;...", - "query_params": { - "agent_id": "blob-a2a-001", - "agent_role": "storage_manager", - "agent_capabilities": ["blob_storage", "file_operations"] - } - }' -``` - -## 🔧 参数说明 - -### 通用参数(所有框架) - -| 参数 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `name` | string | ✅ | Agent 名称(唯一) | -| `template_name` | string | ✅ | 模板名称 | -| `owner_id` | string | ✅ | 所有者ID | -| `namespace` | string | ❌ | K8s 命名空间,默认 `ai-agents` | -| `agent_framework` | string | ❌ | 框架类型: `langchain`, `mcp`, `a2a` | -| `storage_connection_string` | string | ❌ | Azure Storage 连接字符串 | -| `storage_account_name` | string | ❌ | 存储账户名称 | - -### 模型配置参数(MCP/A2A) - -| 参数 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `model_provider` | string | ✅ | 模型提供商: `openai`, `azure-openai` | -| `model_name` | string | ✅ | 模型名称: `gpt-4`, `gpt-3.5-turbo` | -| `model_api_key` | string | ✅ | 模型 API 密钥 | -| `model_endpoint` | string | ❌ | 模型 API 端点 | - -### 工具配置参数(MCP/A2A) - -| 参数 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `tools_config` | object | ❌ | 工具配置 JSON | -| `tool_endpoint` | string | ❌ | 外部工具端点 | -| `tool_api_key` | string | ❌ | 工具 API 密钥 | - -### 资源配置参数 - -| 参数 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `cpu_request` | string | ❌ | CPU 请求,如 `100m` | -| `cpu_limit` | string | ❌ | CPU 限制,如 `500m` | -| `memory_request` | string | ❌ | 内存请求,如 `128Mi` | -| `memory_limit` | string | ❌ | 内存限制,如 `512Mi` | - -## 🎯 使用场景 - -### LangChain 版本适用于: -- 需要复杂推理链的任务 -- 多步骤文件处理流程 -- 集成现有 LangChain 生态系统 - -### MCP 版本适用于: -- 标准化工具调用 -- 轻量级集成 -- 跨平台工具共享 - -### A2A 版本适用于: -- 多 Agent 协作场景 -- 分布式任务处理 -- Agent 间通信需求 - -## 📝 数据库迁移 - -如果从旧版本升级,需要运行数据库迁移: - -```sql --- 添加新字段到 templates 表 -ALTER TABLE templates ADD COLUMN agent_framework VARCHAR(50) DEFAULT 'langchain'; -ALTER TABLE templates ADD COLUMN tools_config JSON; -ALTER TABLE templates ADD COLUMN default_model_provider VARCHAR(100); -ALTER TABLE templates ADD COLUMN default_model_name VARCHAR(200); - --- 添加新字段到 agents 表 -ALTER TABLE agents ADD COLUMN agent_framework VARCHAR(50) DEFAULT 'langchain'; -ALTER TABLE agents ADD COLUMN tools_config JSON; -ALTER TABLE agents ADD COLUMN tool_endpoint VARCHAR(500); -ALTER TABLE agents ADD COLUMN tool_api_key VARCHAR(500); -ALTER TABLE agents ADD COLUMN model_provider VARCHAR(100); -ALTER TABLE agents ADD COLUMN model_name VARCHAR(200); -ALTER TABLE agents ADD COLUMN model_endpoint VARCHAR(500); -ALTER TABLE agents ADD COLUMN model_api_key VARCHAR(500); -ALTER TABLE agents ADD COLUMN storage_connection_string VARCHAR(1000); -ALTER TABLE agents ADD COLUMN storage_account_name VARCHAR(200); -``` - -## 🐛 故障排查 - -### 问题: MCP 工具调用失败 - -**解决方案**: -1. 检查工具名称是否正确 -2. 验证参数格式 -3. 查看日志: `kubectl logs -n ai-agents` - -### 问题: A2A Agent 无法注册 - -**解决方案**: -1. 确认目标 Agent 可访问 -2. 检查网络策略 -3. 验证 endpoint URL 格式 - -## 📚 更多资源 - -- [LangChain 文档](https://python.langchain.com/) -- [MCP 协议规范](https://modelcontextprotocol.io/) -- [Agent Manager API 文档](../API_DOCUMENTATION.md) diff --git a/agent_templates/QUICKSTART.md b/agent_templates/QUICKSTART.md deleted file mode 100644 index 5079037..0000000 --- a/agent_templates/QUICKSTART.md +++ /dev/null @@ -1,157 +0,0 @@ -# 🚀 Azure Blob Storage Agent 快速启动 - -## 一键启动命令 - -### 1. 构建镜像 -```bash -cd /home/taiji/tools/agent-manager/agent_templates -./build_azure_blob_agent.sh latest -``` - -### 2. 启动 Agent(本地测试) -```bash -# 方式 A: 启动时提供连接字符串(推荐) -docker run -d --name azure-blob-agent \ - -p 8080:8080 \ - -e LITELLM_API_BASE=http://20.2.70.108:4000 \ - -e LITELLM_MODEL=gpt-3.5-turbo \ - -e LITELLM_API_KEY=sk-1234 \ - -e AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=xxx;AccountKey=xxx;EndpointSuffix=core.windows.net" \ - azure-blob-agent:latest - -# 方式 B: 稍后通过 API 连接 -docker run -d --name azure-blob-agent \ - -p 8080:8080 \ - -e LITELLM_API_BASE=http://20.2.70.108:4000 \ - -e LITELLM_MODEL=gpt-3.5-turbo \ - -e LITELLM_API_KEY=sk-1234 \ - azure-blob-agent:latest - -# 然后调用 /connect API 连接 - -# 方式 C: 如果 LiteLLM 在另一个容器中 -docker run -d --name azure-blob-agent \ - --network host \ - -e LITELLM_API_BASE=http://localhost:4000 \ - -e LITELLM_MODEL=gpt-3.5-turbo \ - -e LITELLM_API_KEY=sk-1234 \ - -e AZURE_STORAGE_CONNECTION_STRING="YOUR_CONNECTION_STRING" \ - azure-blob-agent:latest -``` - -### 3. 测试 Agent - -#### 方法 1: 使用 Bash 测试脚本 -```bash -./test_azure_blob_agent.sh -``` - -#### 方法 2: 使用 Python 客户端 -```bash -# 设置连接字符串(可选) -export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=xxx;..." - -# 运行客户端 -python3 test_client.py -``` - -#### 方法 3: 使用 curl 手动测试 -```bash -# 健康检查 -curl http://localhost:8080/health - -# 连接到 Azure Storage -curl -X POST http://localhost:8080/connect \ - -H 'Content-Type: application/json' \ - -d '{ - "connection_string": "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey;EndpointSuffix=core.windows.net" - }' - -# 执行查询 -curl -X POST http://localhost:8080/query \ - -H 'Content-Type: application/json' \ - -d '{"query": "列出所有容器"}' -``` - -## 推送到 ACR - -```bash -# 登录 ACR -az acr login --name agnettaiji - -# 推送镜像 -docker tag azure-blob-agent:latest agnettaiji.azurecr.io/ai-agents/azure-blob-agent:latest -docker push agnettaiji.azurecr.io/ai-agents/azure-blob-agent:latest -``` - -## 在 K8s 中部署 - -### 使用 agent-manager - -```bash -# 添加到 k8s_manager.py 的 image_map -"azure_blob_agent": "agnettaiji.azurecr.io/ai-agents/azure-blob-agent:latest" - -# 创建 agent -curl -X POST http://localhost:8000/agents \ - -H 'Content-Type: application/json' \ - -d '{ - "name": "my-blob-agent", - "template": "azure_blob_agent", - "env": { - "LITELLM_API_BASE": "http://litellm-service:4000", - "LITELLM_MODEL": "gpt-3.5-turbo", - "LITELLM_API_KEY": "sk-1234" - } - }' -``` - -## 常见问题 - -### Q: 容器启动失败 -```bash -# 查看日志 -docker logs azure-blob-agent - -# 检查 LiteLLM 是否可达 -docker exec azure-blob-agent curl http://host.docker.internal:4000/health -``` - -### Q: 无法连接到 Azure Storage -```bash -# 验证连接字符串格式 -# 正确格式包含: AccountName, AccountKey, EndpointSuffix - -# 测试连接 -curl -X POST http://localhost:8080/connect \ - -H 'Content-Type: application/json' \ - -d '{"connection_string": "YOUR_STRING"}' -v -``` - -### Q: 查询没有响应 -```bash -# 检查是否已连接 -curl http://localhost:8080/health | jq . - -# 查看详细日志 -docker logs -f azure-blob-agent -``` - -## 文件清单 - -``` -agent_templates/ -├── azure_blob_agent.py # 主程序 -├── azure_blob_agent.Dockerfile # Docker 镜像 -├── build_azure_blob_agent.sh # 构建脚本 -├── test_azure_blob_agent.sh # Bash 测试脚本 -├── test_client.py # Python 客户端 -├── AZURE_BLOB_AGENT_USAGE.md # 详细使用文档 -└── QUICKSTART.md # 本文件 -``` - -## 下一步 - -- 阅读 [详细使用文档](AZURE_BLOB_AGENT_USAGE.md) -- 查看 [agent_templates README](../README.md) -- 集成到你的应用中 diff --git a/agent_templates/QUICK_REFERENCE.md b/agent_templates/QUICK_REFERENCE.md deleted file mode 100644 index 74596dd..0000000 --- a/agent_templates/QUICK_REFERENCE.md +++ /dev/null @@ -1,199 +0,0 @@ -# Azure Blob Agent - 快速参考 - -## 🚀 快速开始 - -### 1. 选择框架 - -| 框架 | 使用场景 | 文件 | -|------|---------|------| -| **LangChain** | 复杂推理任务 | `azure_blob_agent.py` | -| **MCP** | 标准化工具调用 | `azure_blob_agent_mcp.py` | -| **A2A** | 多 Agent 协作 | `azure_blob_agent_a2a.py` | - -### 2. 创建 Agent (curl) - -#### MCP 版本 -```bash -curl -X POST http://agent-manager:8000/v2/agents/platform \ - -H "Content-Type: application/json" \ - -d '{ - "name": "my-blob-mcp", - "template_name": "azure_blob_agent_mcp", - "owner_id": "user123", - "agent_framework": "mcp", - "model_provider": "openai", - "model_name": "gpt-4", - "model_api_key": "sk-xxxx", - "storage_connection_string": "DefaultEndpointsProtocol=https;..." - }' -``` - -#### A2A 版本 -```bash -curl -X POST http://agent-manager:8000/v2/agents/platform \ - -H "Content-Type: application/json" \ - -d '{ - "name": "my-blob-a2a", - "template_name": "azure_blob_agent_a2a", - "owner_id": "user123", - "agent_framework": "a2a", - "model_provider": "openai", - "model_name": "gpt-4", - "model_api_key": "sk-xxxx", - "storage_connection_string": "DefaultEndpointsProtocol=https;...", - "query_params": { - "agent_id": "my-blob-a2a", - "agent_role": "storage_manager" - } - }' -``` - -### 3. 使用 Agent - -#### MCP - 列出工具 -```bash -curl http:///mcp/tools -``` - -#### MCP - 调用工具 -```bash -curl -X POST http:///mcp/call \ - -H "Content-Type: application/json" \ - -d '{"tool_name": "list_containers", "parameters": {}}' -``` - -#### A2A - 获取能力 -```bash -curl http:///a2a/capabilities -``` - -#### A2A - 发送消息 -```bash -curl -X POST http:///a2a/message \ - -H "Content-Type: application/json" \ - -d '{ - "message_id": "msg-001", - "from_agent": "caller", - "to_agent": "my-blob-a2a", - "message_type": "request", - "action": "list_containers", - "parameters": {} - }' -``` - -## 🔧 必需参数 - -### MCP Agent -- ✅ `model_provider` - 模型提供商 -- ✅ `model_name` - 模型名称 -- ✅ `model_api_key` - API 密钥 - -### A2A Agent -- ✅ `model_provider` - 模型提供商 -- ✅ `model_name` - 模型名称 -- ✅ `model_api_key` - API 密钥 -- ✅ `query_params.agent_id` - Agent ID -- ✅ `query_params.agent_role` - Agent 角色 - -## 🛠️ 可选参数 - -| 参数 | 说明 | 示例 | -|------|------|------| -| `namespace` | K8s 命名空间 | `"ai-agents"` | -| `tools_config` | 工具配置 | `{"max_iterations": 5}` | -| `tool_endpoint` | 外部工具端点 | `"http://tools-api:8080"` | -| `model_endpoint` | 模型端点 | `"https://api.openai.com/v1"` | -| `storage_account_name` | 存储账户名 | `"myaccount"` | -| `cpu_request` | CPU 请求 | `"100m"` | -| `memory_request` | 内存请求 | `"256Mi"` | - -## 📊 环境变量 (容器内) - -### 框架相关 -- `AGENT_FRAMEWORK` - 框架类型 -- `TEMPLATE_TYPE` - 模板类型 - -### 模型相关 -- `MODEL_PROVIDER` - 模型提供商 -- `MODEL_NAME` - 模型名称 -- `MODEL_API_KEY` - API 密钥 -- `MODEL_ENDPOINT` - 端点 URL - -### 工具相关 -- `TOOLS_CONFIG` - 工具配置 JSON -- `TOOL_ENDPOINT` - 工具端点 -- `TOOL_API_KEY` - 工具密钥 - -### 存储相关 -- `AZURE_STORAGE_CONNECTION_STRING` - 连接字符串 -- `STORAGE_ACCOUNT_NAME` - 账户名 - -### 用户相关 -- `USER_ID` - 用户标识 -- `TENANT_ID` - 租户标识 -- `NAMESPACE` - 命名空间 - -## 🔍 故障排查 - -### Agent 启动失败 -```bash -# 查看日志 -kubectl logs -n ai-agents - -# 查看事件 -kubectl describe pod -n ai-agents -``` - -### 工具调用失败 -```bash -# 检查工具列表 -curl http:///mcp/tools - -# 测试健康检查 -curl http:///health -``` - -### 存储连接失败 -```bash -# 验证连接字符串 -curl -X POST http:///connect \ - -H "Content-Type: application/json" \ - -d '{"connection_string": "DefaultEndpointsProtocol=https;..."}' -``` - -## 📝 工具列表 - -### 共同工具(所有版本) -1. `list_containers` - 列出所有容器 -2. `list_blobs` - 列出容器中的文件 -3. `get_blob_info` - 获取文件详情 -4. `search_blobs` - 搜索文件 -5. `get_storage_stats` - 获取统计信息 - -## 🏗️ 构建镜像 - -```bash -cd agent_templates - -# MCP 版本 -./build_azure_blob_mcp.sh - -# A2A 版本 -./build_azure_blob_a2a.sh -``` - -## 🧪 测试 - -```bash -# 设置环境变量 -export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;..." -export OPENAI_API_KEY="sk-xxxx" - -# 运行测试 -./test_multi_framework.sh -``` - -## 📚 更多文档 - -- 详细指南: [MULTI_FRAMEWORK_GUIDE.md](MULTI_FRAMEWORK_GUIDE.md) -- 实现总结: [MULTI_FRAMEWORK_SUMMARY.md](../MULTI_FRAMEWORK_SUMMARY.md) diff --git a/agent_templates/agents/a2a_litellm_agent/__init__.py b/agent_templates/agents/a2a_litellm_agent/__init__.py new file mode 100644 index 0000000..be466b1 --- /dev/null +++ b/agent_templates/agents/a2a_litellm_agent/__init__.py @@ -0,0 +1,16 @@ +""" +A2A LiteLLM Agent Package +""" +from .agent import LiteLLMAgent +from .config import get_config, LiteLLMConfig, AgentConfig, A2AConfig +from .a2a_server import A2AAgentServer, create_app + +__all__ = [ + "LiteLLMAgent", + "get_config", + "LiteLLMConfig", + "AgentConfig", + "A2AConfig", + "A2AAgentServer", + "create_app" +] diff --git a/agent_templates/agents/a2a_litellm_agent/a2a_litellm_agent.Dockerfile b/agent_templates/agents/a2a_litellm_agent/a2a_litellm_agent.Dockerfile new file mode 100644 index 0000000..53b1b4e --- /dev/null +++ b/agent_templates/agents/a2a_litellm_agent/a2a_litellm_agent.Dockerfile @@ -0,0 +1,30 @@ +# A2A LiteLLM Agent Dockerfile +FROM python:3.11-slim + +WORKDIR /app + +# 安装系统依赖 +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# 复制依赖文件 +COPY agents/a2a_litellm_agent/requirements.txt . + +# 安装Python依赖 +RUN pip install --no-cache-dir -r requirements.txt + +# 复制应用代码 +COPY agents/a2a_litellm_agent/ . + +# 设置环境变量 +ENV SERVICE_HOST=0.0.0.0 +ENV SERVICE_PORT=8080 +ENV POD_NAME=a2a-litellm-agent +ENV TEMPLATE_TYPE=a2a_litellm_agent + +# 暴露端口 +EXPOSE 8080 + +# 启动命令 +CMD ["python", "main.py"] diff --git a/agent_templates/agents/a2a_litellm_agent/a2a_server.py b/agent_templates/agents/a2a_litellm_agent/a2a_server.py new file mode 100644 index 0000000..bc91abe --- /dev/null +++ b/agent_templates/agents/a2a_litellm_agent/a2a_server.py @@ -0,0 +1,539 @@ +""" +A2A协议兼容的Agent服务 + +实现Google Agent2Agent协议规范 +支持从请求传入 API key,也支持从环境变量获取 +""" +import asyncio +import json +import uuid +import os +from typing import Optional, Dict, Any, AsyncGenerator +from datetime import datetime +from contextlib import asynccontextmanager + +from fastapi import FastAPI, HTTPException, Request, Response +from fastapi.responses import StreamingResponse, JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field +import structlog + +from agent import LiteLLMAgent +from config import get_config, AgentConfig, A2AConfig + +# 配置日志 +logger = structlog.get_logger() + +# 环境变量配置 +SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0") +SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080")) +POD_NAME = os.getenv("POD_NAME", "a2a-litellm-agent") +TEMPLATE_TYPE = os.getenv("TEMPLATE_TYPE", "a2a_litellm_agent") + +# ============== A2A 协议数据模型 ============== + + +class A2APart(BaseModel): + """A2A消息部分""" + kind: str = "text" + text: Optional[str] = None + data: Optional[Dict[str, Any]] = None + mime_type: Optional[str] = None + + +class A2AMessage(BaseModel): + """A2A消息""" + role: str + parts: list[A2APart] + messageId: str = Field(default_factory=lambda: uuid.uuid4().hex) + + +class A2AMessageSendParams(BaseModel): + """A2A发送消息参数""" + message: A2AMessage + configuration: Optional[Dict[str, Any]] = None + api_key: Optional[str] = Field(None, description="LiteLLM API密钥(可选,优先使用,否则从环境变量获取)") + model: Optional[str] = Field(None, description="模型名称(可选,优先使用,否则从环境变量获取)") + + +class A2ARequest(BaseModel): + """A2A JSON-RPC请求""" + jsonrpc: str = "2.0" + id: str + method: str + params: Optional[Dict[str, Any]] = None + + +class A2AArtifact(BaseModel): + """A2A响应工件""" + artifactId: str = Field(default_factory=lambda: uuid.uuid4().hex) + name: str = "response" + parts: list[A2APart] + + +class A2ATaskStatus(BaseModel): + """A2A任务状态""" + state: str # submitted, working, input-required, completed, failed, canceled + timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat() + "Z") + message: Optional[str] = None + + +class A2ATask(BaseModel): + """A2A任务""" + kind: str = "task" + id: str = Field(default_factory=lambda: uuid.uuid4().hex) + contextId: str = Field(default_factory=lambda: uuid.uuid4().hex) + status: A2ATaskStatus + artifacts: Optional[list[A2AArtifact]] = None + + +class A2AResponse(BaseModel): + """A2A JSON-RPC响应""" + jsonrpc: str = "2.0" + id: str + result: Optional[A2ATask] = None + error: Optional[Dict[str, Any]] = None + + +class A2AStreamEvent(BaseModel): + """A2A流式事件""" + kind: str + taskId: str + contextId: str + data: Optional[Dict[str, Any]] = None + + +# ============== Agent Card ============== + + +class AgentSkill(BaseModel): + """Agent技能""" + id: str + name: str + description: str + inputSchema: Optional[Dict[str, Any]] = None + outputSchema: Optional[Dict[str, Any]] = None + + +class AgentCapabilities(BaseModel): + """Agent能力""" + text: bool = True + streaming: bool = True + push_notifications: bool = False + forms: bool = False + files: bool = False + + +class AgentCard(BaseModel): + """A2A Agent Card - 描述Agent能力""" + name: str + description: str + version: str + url: str + capabilities: AgentCapabilities + skills: list[AgentSkill] + authentication: Optional[Dict[str, Any]] = None + + +# ============== A2A Server ============== + + +class A2AAgentServer: + """A2A协议Agent服务器""" + + def __init__( + self, + api_key: Optional[str] = None, + model: Optional[str] = None + ): + """ + 初始化A2A Agent服务器 + + Args: + api_key: LiteLLM API密钥(可选,优先使用,否则从环境变量获取) + model: 模型名称(可选,优先使用,否则从环境变量获取) + """ + # 获取配置 + self.llm_config, self.agent_config, self.a2a_config = get_config(api_key, model) + + # 创建Agent(使用默认配置) + self.default_agent = LiteLLMAgent( + litellm_config=self.llm_config, + agent_config=self.agent_config + ) + + # 任务存储 + self.tasks: Dict[str, A2ATask] = {} + + # 创建FastAPI应用 + self.app = self._create_app() + + def _create_app(self) -> FastAPI: + """创建FastAPI应用""" + + @asynccontextmanager + async def lifespan(app: FastAPI): + logger.info("A2A Agent服务启动", agent_name=self.agent_config.name) + yield + await self.default_agent.close() + logger.info("A2A Agent服务关闭") + + app = FastAPI( + title=f"{self.agent_config.name} - A2A Agent", + description=self.agent_config.description, + version=self.agent_config.version, + lifespan=lifespan + ) + + # CORS中间件 + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + # 注册路由 + self._register_routes(app) + + return app + + def _get_agent(self, api_key: Optional[str] = None, model: Optional[str] = None) -> LiteLLMAgent: + """ + 获取Agent实例 + + 如果提供了api_key或model,创建新的Agent实例 + 否则使用默认Agent + """ + if api_key or model: + # 创建新的配置和Agent + llm_config, agent_config, _ = get_config(api_key, model) + return LiteLLMAgent(litellm_config=llm_config, agent_config=agent_config) + return self.default_agent + + def _register_routes(self, app: FastAPI): + """注册A2A协议路由""" + + @app.get("/") + async def root(): + """服务根路径""" + return { + "name": self.agent_config.name, + "version": self.agent_config.version, + "protocol": "A2A", + "status": "running", + "pod_name": POD_NAME, + "template_type": TEMPLATE_TYPE + } + + @app.get("/health") + async def health_check(): + """健康检查""" + return { + "status": "healthy", + "pod_name": POD_NAME, + "template_type": TEMPLATE_TYPE, + "configured": self.llm_config.api_key is not None, + "timestamp": datetime.utcnow().isoformat() + } + + @app.get("/.well-known/agent.json") + async def get_agent_card(request: Request): + """获取Agent Card (A2A发现协议)""" + base_url = str(request.base_url).rstrip("/") + + card = AgentCard( + name=self.agent_config.name, + description=self.agent_config.description, + version=self.agent_config.version, + url=base_url, + capabilities=AgentCapabilities( + text=True, + streaming=self.agent_config.enable_streaming, + push_notifications=False + ), + skills=[ + AgentSkill( + id="general-assistant", + name="通用助手", + description="回答问题、提供建议、协助完成各种任务" + ), + AgentSkill( + id="code-helper", + name="代码助手", + description="编写、解释和调试代码" + ) + ] + ) + return card.model_dump() + + @app.post("/message/send") + async def send_message(request: Request): + """A2A message/send 端点""" + body = await request.json() + + # 解析JSON-RPC请求 + try: + rpc_request = A2ARequest(**body) + except Exception as e: + return JSONResponse({ + "jsonrpc": "2.0", + "id": body.get("id", "unknown"), + "error": { + "code": -32600, + "message": f"Invalid Request: {str(e)}" + } + }) + + # 处理 message/send 方法 + if rpc_request.method == "message/send": + return await self._handle_message_send(rpc_request) + elif rpc_request.method == "message/stream": + return await self._handle_message_stream(rpc_request) + else: + return JSONResponse({ + "jsonrpc": "2.0", + "id": rpc_request.id, + "error": { + "code": -32601, + "message": f"Method not found: {rpc_request.method}" + } + }) + + @app.post("/message/stream") + async def stream_message(request: Request): + """A2A message/stream 端点 (SSE流式响应)""" + body = await request.json() + + try: + rpc_request = A2ARequest(**body) + except Exception as e: + return JSONResponse({ + "jsonrpc": "2.0", + "id": body.get("id", "unknown"), + "error": { + "code": -32600, + "message": f"Invalid Request: {str(e)}" + } + }) + + return await self._handle_message_stream(rpc_request) + + @app.get("/tasks/{task_id}") + async def get_task(task_id: str): + """获取任务状态""" + if task_id not in self.tasks: + raise HTTPException(status_code=404, detail="Task not found") + return self.tasks[task_id].model_dump() + + async def _handle_message_send(self, request: A2ARequest) -> JSONResponse: + """处理 message/send 请求""" + params = request.params or {} + message_data = params.get("message", {}) + + # 提取API key和model(如果提供) + api_key = params.get("api_key") or os.getenv("LITELLM_API_KEY") + model = params.get("model") or os.getenv("LITELLM_MODEL") + + # 提取用户消息文本 + user_text = "" + parts = message_data.get("parts", []) + for part in parts: + if part.get("kind") == "text": + user_text += part.get("text", "") + + if not user_text: + return JSONResponse({ + "jsonrpc": "2.0", + "id": request.id, + "error": { + "code": -32602, + "message": "Invalid params: no text content found" + } + }) + + # 创建任务 + task_id = uuid.uuid4().hex + context_id = params.get("contextId", uuid.uuid4().hex) + + task = A2ATask( + id=task_id, + contextId=context_id, + status=A2ATaskStatus(state="working") + ) + self.tasks[task_id] = task + + try: + # 获取Agent实例(如果提供了api_key或model,使用新的实例) + agent = self._get_agent(api_key, model) + + # 调用Agent获取响应 + logger.info("处理消息", task_id=task_id, message_preview=user_text[:50]) + + response_text = await agent.chat( + message=user_text, + conversation_id=context_id + ) + + # 如果创建了新Agent,关闭它 + if api_key or model: + await agent.close() + + # 更新任务状态 + task.status = A2ATaskStatus(state="completed") + task.artifacts = [ + A2AArtifact( + name="response", + parts=[A2APart(kind="text", text=response_text)] + ) + ] + self.tasks[task_id] = task + + return JSONResponse({ + "jsonrpc": "2.0", + "id": request.id, + "result": task.model_dump() + }) + + except Exception as e: + logger.error("处理消息失败", error=str(e)) + task.status = A2ATaskStatus(state="failed", message=str(e)) + self.tasks[task_id] = task + + return JSONResponse({ + "jsonrpc": "2.0", + "id": request.id, + "error": { + "code": -32000, + "message": f"Agent error: {str(e)}" + } + }) + + async def _handle_message_stream(self, request: A2ARequest) -> StreamingResponse: + """处理 message/stream 请求 (SSE)""" + params = request.params or {} + message_data = params.get("message", {}) + + # 提取API key和model(如果提供) + api_key = params.get("api_key") or os.getenv("LITELLM_API_KEY") + model = params.get("model") or os.getenv("LITELLM_MODEL") + + # 提取用户消息 + user_text = "" + parts = message_data.get("parts", []) + for part in parts: + if part.get("kind") == "text": + user_text += part.get("text", "") + + task_id = uuid.uuid4().hex + context_id = params.get("contextId", uuid.uuid4().hex) + + async def event_generator() -> AsyncGenerator[str, None]: + """生成SSE事件流""" + agent = None + try: + # 获取Agent实例 + agent = self._get_agent(api_key, model) + + # 发送任务开始事件 + start_event = { + "kind": "task-start", + "taskId": task_id, + "contextId": context_id + } + yield f"data: {json.dumps(start_event)}\n\n" + + # 获取流式响应 + stream = await agent.chat( + message=user_text, + conversation_id=context_id, + stream=True + ) + + full_response = "" + async for chunk in stream: + full_response += chunk + # 发送文本增量事件 + delta_event = { + "kind": "artifact-delta", + "taskId": task_id, + "contextId": context_id, + "data": { + "kind": "text", + "text": chunk + } + } + yield f"data: {json.dumps(delta_event)}\n\n" + + # 发送完成事件 + complete_event = { + "kind": "task-complete", + "taskId": task_id, + "contextId": context_id, + "data": { + "status": "completed", + "artifacts": [{ + "name": "response", + "parts": [{"kind": "text", "text": full_response}] + }] + } + } + yield f"data: {json.dumps(complete_event)}\n\n" + + except Exception as e: + # 发送错误事件 + error_event = { + "kind": "task-error", + "taskId": task_id, + "contextId": context_id, + "data": { + "error": str(e) + } + } + yield f"data: {json.dumps(error_event)}\n\n" + finally: + # 如果创建了新Agent,关闭它 + if agent and (api_key or model): + await agent.close() + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no" + } + ) + + def run(self, host: Optional[str] = None, port: Optional[int] = None): + """运行服务器""" + import uvicorn + + host = host or self.agent_config.host + port = port or self.agent_config.port + + logger.info(f"启动A2A Agent服务", host=host, port=port) + uvicorn.run(self.app, host=host, port=port) + + +def create_app(api_key: Optional[str] = None, model: Optional[str] = None) -> FastAPI: + """ + 创建FastAPI应用(用于uvicorn启动) + + 使用方式: + uvicorn a2a_server:app --host 0.0.0.0 --port 8080 + + 或设置环境变量后: + export LITELLM_API_KEY="your-key" + export LITELLM_MODEL="your-model" + uvicorn a2a_server:app --host 0.0.0.0 --port 8080 + """ + server = A2AAgentServer(api_key=api_key, model=model) + return server.app + + +# uvicorn 启动入口 +# 环境变量: LITELLM_API_KEY, LITELLM_MODEL +app = create_app() diff --git a/agent_templates/agents/a2a_litellm_agent/agent.py b/agent_templates/agents/a2a_litellm_agent/agent.py new file mode 100644 index 0000000..efdd611 --- /dev/null +++ b/agent_templates/agents/a2a_litellm_agent/agent.py @@ -0,0 +1,282 @@ +""" +LiteLLM Agent 核心模块 + +基于LiteLLM框架的Agent实现,支持A2A协议 +""" +import asyncio +import json +import uuid +from typing import AsyncGenerator, Optional, Dict, Any, List +from dataclasses import dataclass, field +from datetime import datetime + +import httpx +import structlog + +from config import LiteLLMConfig, AgentConfig, get_config + +# 配置日志 +logger = structlog.get_logger() + + +@dataclass +class Message: + """消息数据结构""" + role: str # user, assistant, system + content: str + message_id: str = field(default_factory=lambda: uuid.uuid4().hex) + timestamp: datetime = field(default_factory=datetime.now) + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class Conversation: + """对话上下文""" + conversation_id: str = field(default_factory=lambda: uuid.uuid4().hex) + messages: List[Message] = field(default_factory=list) + created_at: datetime = field(default_factory=datetime.now) + + def add_message(self, role: str, content: str) -> Message: + """添加消息到对话""" + msg = Message(role=role, content=content) + self.messages.append(msg) + return msg + + def to_openai_format(self) -> List[Dict[str, str]]: + """转换为OpenAI格式的消息列表""" + return [{"role": m.role, "content": m.content} for m in self.messages] + + +class LiteLLMAgent: + """ + 基于LiteLLM的Agent实现 + + 支持功能: + - 多轮对话 + - 流式响应 + - 工具调用 + - A2A协议兼容 + """ + + def __init__( + self, + api_key: Optional[str] = None, + model: Optional[str] = None, + litellm_config: Optional[LiteLLMConfig] = None, + agent_config: Optional[AgentConfig] = None + ): + """ + 初始化Agent + + Args: + api_key: LiteLLM API密钥(可选,优先使用,否则从环境变量获取) + model: 模型名称(可选,优先使用,否则从环境变量获取) + litellm_config: LiteLLM配置对象 + agent_config: Agent配置对象 + """ + if litellm_config: + self.llm_config = litellm_config + else: + self.llm_config = LiteLLMConfig(api_key=api_key, model=model) + + if agent_config: + self.agent_config = agent_config + else: + self.agent_config = AgentConfig() + + # 验证配置 + self.llm_config.validate() + + # HTTP客户端 + self._client: Optional[httpx.AsyncClient] = None + + # 对话管理 + self.conversations: Dict[str, Conversation] = {} + + # 工具注册 + self.tools: Dict[str, callable] = {} + + logger.info( + "Agent初始化完成", + agent_name=self.agent_config.name, + model=self.llm_config.model, + base_url=self.llm_config.base_url + ) + + async def _get_client(self) -> httpx.AsyncClient: + """获取或创建HTTP客户端""" + if self._client is None or self._client.is_closed: + self._client = httpx.AsyncClient( + timeout=httpx.Timeout(self.llm_config.timeout), + headers={ + "Authorization": f"Bearer {self.llm_config.api_key}", + "Content-Type": "application/json" + } + ) + return self._client + + async def close(self): + """关闭资源""" + if self._client and not self._client.is_closed: + await self._client.aclose() + + def register_tool(self, name: str, func: callable, description: str = ""): + """注册工具函数""" + self.tools[name] = { + "function": func, + "description": description + } + logger.info(f"注册工具: {name}") + + def get_or_create_conversation(self, conversation_id: Optional[str] = None) -> Conversation: + """获取或创建对话""" + if conversation_id and conversation_id in self.conversations: + return self.conversations[conversation_id] + + conv = Conversation(conversation_id=conversation_id or uuid.uuid4().hex) + # 添加系统提示 + conv.add_message("system", self.agent_config.system_prompt) + self.conversations[conv.conversation_id] = conv + return conv + + async def chat( + self, + message: str, + conversation_id: Optional[str] = None, + stream: bool = False + ) -> str | AsyncGenerator[str, None]: + """ + 发送消息并获取回复 + + Args: + message: 用户消息 + conversation_id: 对话ID(用于多轮对话) + stream: 是否流式响应 + + Returns: + 如果stream=False,返回完整回复字符串 + 如果stream=True,返回异步生成器 + """ + # 获取对话上下文 + conversation = self.get_or_create_conversation(conversation_id) + conversation.add_message("user", message) + + if stream: + return self._stream_chat(conversation) + else: + return await self._simple_chat(conversation) + + async def _simple_chat(self, conversation: Conversation) -> str: + """非流式对话""" + client = await self._get_client() + + request_body = { + "model": self.llm_config.model, + "messages": conversation.to_openai_format(), + "temperature": self.llm_config.temperature, + "max_tokens": self.llm_config.max_tokens + } + + logger.debug("发送请求", endpoint=self.llm_config.chat_endpoint) + + try: + response = await client.post( + self.llm_config.chat_endpoint, + json=request_body + ) + response.raise_for_status() + + result = response.json() + assistant_message = result["choices"][0]["message"]["content"] + + # 保存助手回复到对话 + conversation.add_message("assistant", assistant_message) + + logger.info("收到回复", length=len(assistant_message)) + return assistant_message + + except httpx.HTTPStatusError as e: + logger.error("HTTP错误", status_code=e.response.status_code, detail=e.response.text) + raise + except Exception as e: + logger.error("请求失败", error=str(e)) + raise + + async def _stream_chat(self, conversation: Conversation) -> AsyncGenerator[str, None]: + """流式对话""" + client = await self._get_client() + + request_body = { + "model": self.llm_config.model, + "messages": conversation.to_openai_format(), + "temperature": self.llm_config.temperature, + "max_tokens": self.llm_config.max_tokens, + "stream": True + } + + full_response = "" + + try: + async with client.stream( + "POST", + self.llm_config.chat_endpoint, + json=request_body + ) as response: + response.raise_for_status() + + async for line in response.aiter_lines(): + if line.startswith("data: "): + data = line[6:] + if data == "[DONE]": + break + + try: + chunk = json.loads(data) + delta = chunk.get("choices", [{}])[0].get("delta", {}) + content = delta.get("content", "") + if content: + full_response += content + yield content + except json.JSONDecodeError: + continue + + # 保存完整回复到对话 + conversation.add_message("assistant", full_response) + + except Exception as e: + logger.error("流式请求失败", error=str(e)) + raise + + async def invoke_tool(self, tool_name: str, **kwargs) -> Any: + """调用注册的工具""" + if tool_name not in self.tools: + raise ValueError(f"未找到工具: {tool_name}") + + tool = self.tools[tool_name] + func = tool["function"] + + logger.info(f"调用工具: {tool_name}", kwargs=kwargs) + + if asyncio.iscoroutinefunction(func): + return await func(**kwargs) + else: + return func(**kwargs) + + +# 示例工具函数 +def tool_get_current_time() -> str: + """获取当前时间""" + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def tool_calculate(expression: str) -> str: + """计算数学表达式""" + try: + # 安全的数学计算 + allowed_chars = set("0123456789+-*/.() ") + if not all(c in allowed_chars for c in expression): + return "错误: 不支持的字符" + result = eval(expression) + return str(result) + except Exception as e: + return f"计算错误: {str(e)}" diff --git a/agent_templates/agents/a2a_litellm_agent/config.py b/agent_templates/agents/a2a_litellm_agent/config.py new file mode 100644 index 0000000..100beea --- /dev/null +++ b/agent_templates/agents/a2a_litellm_agent/config.py @@ -0,0 +1,145 @@ +""" +LiteLLM Agent 配置模块 + +支持用户传入密钥和模型名称,同时支持从环境变量获取 +""" +import os +from dataclasses import dataclass, field +from typing import Optional +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + + +@dataclass +class LiteLLMConfig: + """LiteLLM 配置""" + # 基础URL - 用户提供的LiteLLM服务地址 + base_url: str = "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io" + + # 完整的chat completions端点 + chat_endpoint: str = field(init=False) + + # API密钥 - 优先使用传入的,否则从环境变量获取 + api_key: Optional[str] = None + + # 模型名称 - 优先使用传入的,否则从环境变量获取 + model: Optional[str] = None + + # 请求超时时间(秒) + timeout: int = 120 + + # 最大重试次数 + max_retries: int = 3 + + # 温度参数 + temperature: float = 0.7 + + # 最大token数 + max_tokens: int = 4096 + + def __post_init__(self): + self.chat_endpoint = f"{self.base_url}/chat/completions" + + # 从环境变量读取(如果未直接提供) + if self.api_key is None: + self.api_key = os.getenv("LITELLM_API_KEY") + if self.model is None: + self.model = os.getenv("LITELLM_MODEL", "gpt-4") + + def validate(self) -> bool: + """验证配置是否完整""" + if not self.api_key: + raise ValueError("API密钥未设置! 请设置 LITELLM_API_KEY 环境变量或直接传入 api_key") + if not self.model: + raise ValueError("模型名称未设置! 请设置 LITELLM_MODEL 环境变量或直接传入 model") + return True + + +@dataclass +class AgentConfig: + """Agent 配置""" + # Agent名称 + name: str = "xiaohei-agent" + + # Agent描述 + description: str = "一个基于LiteLLM的智能Agent,支持A2A协议" + + # Agent版本 + version: str = "1.0.0" + + # 服务端口 + port: int = 8080 + + # 服务主机 + host: str = "0.0.0.0" + + # 是否启用流式响应 + enable_streaming: bool = True + + # 系统提示词 + system_prompt: str = """你是小黑Agent,一个智能助手。 +你可以帮助用户完成各种任务,包括: + +- 回答问题 + +- 代码编写和解释 + +- 文档分析 + +- 任务规划 + +请用中文回答用户的问题,保持友好和专业。""" + + +@dataclass +class A2AConfig: + """A2A协议配置""" + # A2A协议版本 + protocol_version: str = "1.0" + + # Agent Card配置 + agent_card: dict = field(default_factory=lambda: { + "name": "xiaohei-agent", + "description": "基于LiteLLM的智能Agent,支持A2A协议通信", + "version": "1.0.0", + "capabilities": { + "text": True, + "streaming": True, + "push_notifications": False + }, + "skills": [ + { + "id": "general-assistant", + "name": "通用助手", + "description": "回答问题、提供建议、协助任务" + }, + { + "id": "code-helper", + "name": "代码助手", + "description": "编写、解释和调试代码" + } + ] + }) + + +def get_config( + api_key: Optional[str] = None, + model: Optional[str] = None +) -> tuple[LiteLLMConfig, AgentConfig, A2AConfig]: + """ + 获取完整配置 + + Args: + api_key: LiteLLM API密钥(可选,优先使用,否则从环境变量获取) + model: 模型名称(可选,优先使用,否则从环境变量获取) + + Returns: + (LiteLLMConfig, AgentConfig, A2AConfig) 配置元组 + """ + litellm_config = LiteLLMConfig(api_key=api_key, model=model) + agent_config = AgentConfig() + a2a_config = A2AConfig() + + return litellm_config, agent_config, a2a_config diff --git a/agent_templates/agents/a2a_litellm_agent/main.py b/agent_templates/agents/a2a_litellm_agent/main.py new file mode 100644 index 0000000..65c9f00 --- /dev/null +++ b/agent_templates/agents/a2a_litellm_agent/main.py @@ -0,0 +1,43 @@ +""" +A2A LiteLLM Agent 主入口 +支持从环境变量或请求传入 API key +""" +import os +import uvicorn +from a2a_server import create_app + +# 环境变量配置 +SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0") +SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080")) +POD_NAME = os.getenv("POD_NAME", "a2a-litellm-agent") +TEMPLATE_TYPE = os.getenv("TEMPLATE_TYPE", "a2a_litellm_agent") + +# 从环境变量获取默认配置(可选) +default_api_key = os.getenv("LITELLM_API_KEY") +default_model = os.getenv("LITELLM_MODEL") + +# 创建应用 +app = create_app(api_key=default_api_key, model=default_model) + + +def main(): + """主函数""" + print(f"🚀 启动 A2A LiteLLM Agent") + print(f" - Pod名称: {POD_NAME}") + print(f" - 模板类型: {TEMPLATE_TYPE}") + print(f" - 服务地址: http://{SERVICE_HOST}:{SERVICE_PORT}") + if default_api_key: + print(f" - 已配置默认 API key(可通过请求覆盖)") + else: + print(f" - 未配置默认 API key,需在请求中传入") + + uvicorn.run( + app, + host=SERVICE_HOST, + port=SERVICE_PORT, + log_level="info" + ) + + +if __name__ == "__main__": + main() diff --git a/agent_templates/agents/a2a_litellm_agent/requirements.txt b/agent_templates/agents/a2a_litellm_agent/requirements.txt new file mode 100644 index 0000000..8f44ea1 --- /dev/null +++ b/agent_templates/agents/a2a_litellm_agent/requirements.txt @@ -0,0 +1,7 @@ +# LiteLLM Agent API服务依赖 +httpx>=0.27.0 +fastapi>=0.115.0 +uvicorn[standard]>=0.32.0 +pydantic>=2.0.0 +python-dotenv>=1.0.0 +structlog>=24.0.0 diff --git a/agent_templates/azure_blob_agent.Dockerfile b/agent_templates/agents/azure_blob_agent/azure_blob_agent.Dockerfile similarity index 82% rename from agent_templates/azure_blob_agent.Dockerfile rename to agent_templates/agents/azure_blob_agent/azure_blob_agent.Dockerfile index e322d29..36fa495 100644 --- a/agent_templates/azure_blob_agent.Dockerfile +++ b/agent_templates/agents/azure_blob_agent/azure_blob_agent.Dockerfile @@ -18,8 +18,10 @@ RUN pip install --no-cache-dir \ azure-storage-blob==12.19.0 \ azure-identity==1.15.0 -# 复制agent代码 -COPY azure_blob_agent.py . +# 复制agent代码和共享工具 +COPY agents/azure_blob_agent/azure_blob_agent.py . +COPY common/agent_callback_utils.py /app/common/ +COPY common/api_key_utils.py /app/common/ # 设置环境变量 ENV PYTHONUNBUFFERED=1 diff --git a/agent_templates/azure_blob_agent.py b/agent_templates/agents/azure_blob_agent/azure_blob_agent.py similarity index 89% rename from agent_templates/azure_blob_agent.py rename to agent_templates/agents/azure_blob_agent/azure_blob_agent.py index 7be6ec1..bbbed76 100644 --- a/agent_templates/azure_blob_agent.py +++ b/agent_templates/agents/azure_blob_agent/azure_blob_agent.py @@ -4,6 +4,7 @@ Azure Blob Storage AI Agent - 使用LangChain + LiteLLM实现 """ import os import logging +import time from typing import Optional, Dict, Any, List from datetime import datetime from fastapi import FastAPI, HTTPException @@ -13,6 +14,7 @@ from langchain.agents import Tool, AgentExecutor, create_react_agent from langchain.prompts import PromptTemplate from langchain_community.chat_models import ChatLiteLLM import uvicorn +from agent_callback_utils import AgentCallbackHandler, CallbackContextManager # 配置日志 logging.basicConfig( @@ -27,17 +29,17 @@ SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080")) POD_NAME = os.getenv("POD_NAME", "azure-blob-agent") TEMPLATE_TYPE = os.getenv("TEMPLATE_TYPE", "azure_blob_agent") -# LiteLLM配置 +# LiteLLM配置(从环境变量获取) LITELLM_API_BASE = os.getenv("LITELLM_API_BASE", "http://localhost:4000") LITELLM_MODEL = os.getenv("LITELLM_MODEL", "gpt-3.5-turbo") -LITELLM_API_KEY = os.getenv("LITELLM_API_KEY", "sk-1234") -# Azure Storage 连接字符串(可选,也可通过API动态传入) +# Azure Storage 连接字符串(从环境变量获取) AZURE_STORAGE_CONNECTION_STRING = os.getenv("AZURE_STORAGE_CONNECTION_STRING", "") -# 全局存储客户端 +# 全局变量 blob_service_client: Optional[BlobServiceClient] = None connection_string: Optional[str] = None +callback_handler: Optional[AgentCallbackHandler] = None # FastAPI应用 app = FastAPI( @@ -57,6 +59,8 @@ class ConnectRequest(BaseModel): class QueryRequest(BaseModel): """查询请求""" query: str = Field(..., description="自然语言查询或操作指令") + litellm_api_key: str = Field(..., description="LiteLLM API密钥") + user_id: Optional[str] = Field(None, description="用户ID(用于计费回调)") container_name: Optional[str] = Field(None, description="指定容器名称") @@ -244,7 +248,7 @@ def get_storage_stats() -> str: # ==================== 创建LangChain Agent ==================== -def create_blob_agent() -> Optional[AgentExecutor]: +def create_blob_agent(litellm_api_key: str) -> Optional[AgentExecutor]: """创建Azure Blob Storage Agent""" global blob_service_client @@ -257,7 +261,7 @@ def create_blob_agent() -> Optional[AgentExecutor]: llm = ChatLiteLLM( model=LITELLM_MODEL, api_base=LITELLM_API_BASE, - api_key=LITELLM_API_KEY, + api_key=litellm_api_key, temperature=0 ) logger.info(f"✅ LiteLLM初始化成功: {LITELLM_MODEL} @ {LITELLM_API_BASE}") @@ -415,7 +419,7 @@ async def connect_to_storage(request: ConnectRequest): @app.post("/query") async def query_storage(request: QueryRequest): """使用自然语言查询存储""" - global blob_service_client + global blob_service_client, callback_handler if not blob_service_client: raise HTTPException( @@ -423,26 +427,38 @@ async def query_storage(request: QueryRequest): detail="未连接到Azure Blob Storage,请先调用 /connect" ) - try: - # 创建Agent - agent = create_blob_agent() - - if not agent: - raise HTTPException(status_code=500, detail="Agent创建失败") - - # 执行查询 - logger.info(f"收到查询: {request.query}") - result = agent.invoke({"input": request.query}) - - return { - "status": "success", - "query": request.query, - "answer": result.get("output", "无法生成答案"), - "intermediate_steps": str(result.get("intermediate_steps", [])) - } - except Exception as e: - logger.error(f"查询执行失败: {str(e)}") - raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}") + # 初始化回调处理器 + if not callback_handler: + callback_handler = AgentCallbackHandler() + + # 使用上下文管理器自动处理回调 + with CallbackContextManager( + handler=callback_handler, + user_id=request.user_id, + request_id=f"blob-{int(time.time())}" + ) as ctx: + try: + ctx.add_tool("azure_blob_storage") + + # 创建Agent + agent = create_blob_agent(request.litellm_api_key) + + if not agent: + raise HTTPException(status_code=500, detail="Agent创建失败") + + # 执行查询 + logger.info(f"收到查询: {request.query}") + result = agent.invoke({"input": request.query}) + + return { + "status": "success", + "query": request.query, + "answer": result.get("output", "无法生成答案"), + "intermediate_steps": str(result.get("intermediate_steps", [])) + } + except Exception as e: + logger.error(f"查询执行失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}") @app.get("/") @@ -493,15 +509,21 @@ def init_storage_connection(): def main(): """启动服务""" + global callback_handler + logger.info(f"🚀 启动 Azure Blob Storage AI Agent") logger.info(f" - Pod名称: {POD_NAME}") logger.info(f" - 模板类型: {TEMPLATE_TYPE}") logger.info(f" - LiteLLM: {LITELLM_MODEL} @ {LITELLM_API_BASE}") logger.info(f" - 服务地址: http://{SERVICE_HOST}:{SERVICE_PORT}") + logger.info(f" ℹ️ API key 将从请求中获取") # 初始化存储连接 init_storage_connection() + # 初始化回调处理器 + callback_handler = AgentCallbackHandler() + uvicorn.run( app, host=SERVICE_HOST, diff --git a/agent_templates/azure_blob_agent_a2a.Dockerfile b/agent_templates/agents/azure_blob_agent_a2a/azure_blob_agent_a2a.Dockerfile similarity index 75% rename from agent_templates/azure_blob_agent_a2a.Dockerfile rename to agent_templates/agents/azure_blob_agent_a2a/azure_blob_agent_a2a.Dockerfile index b0d29a0..5193672 100644 --- a/agent_templates/azure_blob_agent_a2a.Dockerfile +++ b/agent_templates/agents/azure_blob_agent_a2a/azure_blob_agent_a2a.Dockerfile @@ -9,13 +9,14 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # 复制requirements文件 -COPY requirements_a2a.txt /app/ +COPY common/requirements_a2a.txt /app/ # 安装Python依赖 RUN pip install --no-cache-dir -r requirements_a2a.txt -# 复制应用代码 -COPY azure_blob_agent_a2a.py /app/ +# 复制应用代码和共享工具 +COPY agents/azure_blob_agent_a2a/azure_blob_agent_a2a.py /app/ +COPY common/api_key_utils.py /app/common/ # 暴露端口 EXPOSE 8080 diff --git a/agent_templates/azure_blob_agent_a2a.py b/agent_templates/agents/azure_blob_agent_a2a/azure_blob_agent_a2a.py similarity index 95% rename from agent_templates/azure_blob_agent_a2a.py rename to agent_templates/agents/azure_blob_agent_a2a/azure_blob_agent_a2a.py index f256993..6343c5c 100644 --- a/agent_templates/azure_blob_agent_a2a.py +++ b/agent_templates/agents/azure_blob_agent_a2a/azure_blob_agent_a2a.py @@ -12,6 +12,7 @@ from fastapi import FastAPI, HTTPException, Header from pydantic import BaseModel, Field from azure.storage.blob import BlobServiceClient, ContainerClient import uvicorn +from api_key_utils import get_api_key # 配置日志 logging.basicConfig( @@ -84,6 +85,7 @@ class A2AMessage(BaseModel): parameters: Dict[str, Any] = Field(default_factory=dict, description="参数") context: Optional[Dict] = Field(default_factory=dict, description="上下文") timestamp: Optional[str] = None + model_api_key: Optional[str] = Field(None, description="模型 API 密钥(可选,优先使用,否则从环境变量获取)") class A2AQueryRequest(BaseModel): @@ -92,6 +94,7 @@ class A2AQueryRequest(BaseModel): container_name: Optional[str] = None requester_agent: Optional[str] = Field(None, description="请求者 Agent ID") context: Optional[Dict] = Field(default_factory=dict) + model_api_key: Optional[str] = Field(None, description="模型 API 密钥(可选,优先使用,否则从环境变量获取)") class A2ARegisterRequest(BaseModel): @@ -432,6 +435,9 @@ async def handle_a2a_message(message: A2AMessage): detail="未连接到 Azure Blob Storage,请先调用 /connect" ) + # 获取 API key(优先使用请求传入的,否则从环境变量获取) + api_key = get_api_key(message.model_api_key, "MODEL_API_KEY", MODEL_API_KEY) + # 验证消息目标 if message.to_agent != AGENT_ID: raise HTTPException( @@ -462,7 +468,8 @@ async def handle_a2a_message(message: A2AMessage): "message_type": "response", "action": action, "result": result, - "timestamp": str(datetime.now()) + "timestamp": str(datetime.now()), + "api_key_used": "request" if message.model_api_key else ("env" if MODEL_API_KEY else "none") } except Exception as e: logger.error(f"处理 A2A 消息失败: {str(e)}") @@ -488,6 +495,9 @@ async def query_storage(request: A2AQueryRequest): detail="未连接到 Azure Blob Storage,请先调用 /connect" ) + # 获取 API key(优先使用请求传入的,否则从环境变量获取) + api_key = get_api_key(request.model_api_key, "MODEL_API_KEY", MODEL_API_KEY) + try: query = request.query.lower() result = None @@ -512,7 +522,8 @@ async def query_storage(request: A2AQueryRequest): "result": result, "agent_id": AGENT_ID, "requester": request.requester_agent, - "framework": AGENT_FRAMEWORK + "framework": AGENT_FRAMEWORK, + "api_key_used": "request" if request.model_api_key else ("env" if MODEL_API_KEY else "none") } except Exception as e: logger.error(f"查询执行失败: {str(e)}") diff --git a/agent_templates/azure_blob_agent_mcp.Dockerfile b/agent_templates/agents/azure_blob_agent_mcp/azure_blob_agent_mcp.Dockerfile similarity index 85% rename from agent_templates/azure_blob_agent_mcp.Dockerfile rename to agent_templates/agents/azure_blob_agent_mcp/azure_blob_agent_mcp.Dockerfile index f529d4b..5f72baa 100644 --- a/agent_templates/azure_blob_agent_mcp.Dockerfile +++ b/agent_templates/agents/azure_blob_agent_mcp/azure_blob_agent_mcp.Dockerfile @@ -9,13 +9,13 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # 复制requirements文件 -COPY requirements_mcp.txt /app/ +COPY common/requirements_mcp.txt /app/ # 安装Python依赖 RUN pip install --no-cache-dir -r requirements_mcp.txt # 复制应用代码 -COPY azure_blob_agent_mcp.py /app/ +COPY agents/azure_blob_agent_mcp/azure_blob_agent_mcp.py /app/ # 暴露端口 EXPOSE 8080 diff --git a/agent_templates/agents/search_agent/search_agent.Dockerfile b/agent_templates/agents/search_agent/search_agent.Dockerfile new file mode 100644 index 0000000..8cf0d45 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent.Dockerfile @@ -0,0 +1,38 @@ +FROM python:3.11-slim + +WORKDIR /app + +# 安装系统依赖 +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# 复制requirements文件 +COPY search_agent/requirements.txt /app/search_agent_requirements.txt + +# 安装Python依赖 +RUN pip install --no-cache-dir \ + fastapi==0.109.0 \ + uvicorn[standard]==0.27.0 \ + pydantic==2.5.3 \ + && pip install --no-cache-dir -r /app/search_agent_requirements.txt + +# 复制search_agent目录 +COPY search_agent/ /app/search_agent/ + +# 复制主agent文件和回调工具 +COPY search_agent_main.py /app/ +COPY agent_callback_utils.py /app/ + +# 设置环境变量 +ENV PYTHONUNBUFFERED=1 +ENV SERVICE_HOST=0.0.0.0 +ENV SERVICE_PORT=8080 +ENV PYTHONPATH=/app + +# 健康检查 - 使用Python避免僵尸进程 +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health').read()" || exit 1 + +# 运行agent (直接使用Python,避免shell) +CMD ["python3", "-u", "search_agent_main.py"] diff --git a/agent_templates/agents/search_agent/search_agent.py b/agent_templates/agents/search_agent/search_agent.py new file mode 100644 index 0000000..f4c1a9d --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent.py @@ -0,0 +1,312 @@ +""" +智能搜索 AI Agent - FastAPI版本 +通过HTTP API接收搜索请求,提供智能搜索功能 +""" +import os +import sys +import logging +from typing import Optional, Dict, Any, List +from datetime import datetime +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field +import uvicorn +import asyncio + +# 添加search_agent目录到Python路径 +search_agent_dir = os.path.join(os.path.dirname(__file__), 'search_agent') +if search_agent_dir not in sys.path: + sys.path.insert(0, search_agent_dir) + +# 直接导入,避免与文件名冲突 +from config import Config +from agent.search_agent import SearchAgent +from agent_callback_utils import AgentCallbackHandler, CallbackContextManager + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 环境变量配置 +SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0") +SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080")) +POD_NAME = os.getenv("POD_NAME", "search-agent") +TEMPLATE_TYPE = os.getenv("TEMPLATE_TYPE", "search_agent") + +# 全局搜索Agent和回调处理器 +search_agent: Optional[SearchAgent] = None +config: Optional[Config] = None +callback_handler: Optional[AgentCallbackHandler] = None + +# FastAPI应用 +app = FastAPI( + title="Intelligent Search AI Agent", + description="智能搜索代理", + version="1.0.0" +) + + +# ==================== 请求/响应模型 ==================== + +class ConfigRequest(BaseModel): + """配置请求(其他配置从环境变量获取)""" + llm_base_url: str = Field(..., description="LLM API基础URL") + llm_model: str = Field(default="xchat52", description="LLM模型名称") + serper_api_key: str = Field(..., description="Serper API密钥") + jina_api_key: str = Field(..., description="Jina API密钥") + max_iterations: int = Field(default=3, description="最大迭代次数") + max_results_per_query: int = Field(default=10, description="每次搜索最大结果数") + content_max_length: int = Field(default=5000, description="内容最大长度") + log_level: str = Field(default="INFO", description="日志级别") + timeout: int = Field(default=30, description="超时时间(秒)") + + +class SearchRequest(BaseModel): + """搜索请求""" + query: str = Field(..., description="搜索查询") + llm_api_key: str = Field(..., description="LLM API密钥") + user_id: Optional[str] = Field(None, description="用户ID(用于计费回调)") + auto_configure: bool = Field(default=False, description="是否自动从环境变量配置") + + +class Source(BaseModel): + """搜索来源""" + index: int + title: str + url: str + + +class SearchResponse(BaseModel): + """搜索响应""" + query: str + answer: str + sources: List[Source] + confidence: str + iterations: int + total_sources: int + search_queries: List[str] + timestamp: str + + +class StatusResponse(BaseModel): + """状态响应""" + status: str + pod_name: str + template_type: str + configured: bool + timestamp: str + + +class ErrorResponse(BaseModel): + """错误响应""" + error: str + detail: Optional[str] = None + + +# ==================== Agent操作函数 ==================== + +def initialize_agent_from_env(): + """从环境变量初始化Agent""" + global search_agent, config + + try: + config = Config.from_env() + config.validate() + search_agent = SearchAgent(config) + logger.info("Search Agent从环境变量初始化成功") + return True + except Exception as e: + logger.error(f"从环境变量初始化Agent失败: {str(e)}") + return False + + +def initialize_agent_from_config(config_data: Dict[str, Any]): + """从配置数据初始化Agent""" + global search_agent, config + + try: + # 创建配置对象 + config = Config( + llm_base_url=config_data.get("llm_base_url", ""), + llm_api_key=config_data.get("llm_api_key", ""), + llm_model=config_data.get("llm_model", "xchat52"), + serper_api_key=config_data.get("serper_api_key", ""), + jina_api_key=config_data.get("jina_api_key", ""), + max_iterations=config_data.get("max_iterations", 3), + max_results_per_query=config_data.get("max_results_per_query", 10), + content_max_length=config_data.get("content_max_length", 5000), + log_level=config_data.get("log_level", "INFO"), + timeout=config_data.get("timeout", 30) + ) + + config.validate() + search_agent = SearchAgent(config) + logger.info("Search Agent从配置初始化成功") + return True + except Exception as e: + logger.error(f"从配置初始化Agent失败: {str(e)}") + raise + + +# ==================== API端点 ==================== + +@app.get("/health") +async def health_check(): + """健康检查""" + return { + "status": "healthy", + "pod_name": POD_NAME, + "template_type": TEMPLATE_TYPE, + "configured": search_agent is not None, + "timestamp": datetime.utcnow().isoformat() + } + + +@app.get("/status", response_model=StatusResponse) +async def get_status(): + """获取状态""" + return StatusResponse( + status="running" if search_agent else "not_configured", + pod_name=POD_NAME, + template_type=TEMPLATE_TYPE, + configured=search_agent is not None, + timestamp=datetime.utcnow().isoformat() + ) + + +@app.post("/configure") +async def configure_agent(config_req: ConfigRequest): + """配置Agent""" + try: + initialize_agent_from_config(config_req.dict()) + return { + "status": "success", + "message": "Agent配置成功", + "timestamp": datetime.utcnow().isoformat() + } + except Exception as e: + logger.error(f"配置Agent失败: {str(e)}") + raise HTTPException(status_code=400, detail=f"配置失败: {str(e)}") + + +@app.post("/search", response_model=SearchResponse) +async def search(request: SearchRequest): + """执行搜索""" + global search_agent, callback_handler, config + + # 如果未配置且需要自动配置 + if not search_agent and request.auto_configure: + if not initialize_agent_from_env(): + raise HTTPException( + status_code=400, + detail="Agent未配置且自动配置失败,请先调用/configure接口" + ) + + if not search_agent: + raise HTTPException( + status_code=400, + detail="Agent未配置,请先调用/configure接口" + ) + + # 初始化回调处理器(如果尚未初始化) + if not callback_handler: + callback_handler = AgentCallbackHandler() + + # 使用上下文管理器自动处理回调 + try: + with CallbackContextManager( + handler=callback_handler, + user_id=request.user_id, + request_id=f"search-{int(datetime.utcnow().timestamp())}" + ) as ctx: + # 临时更新API key + original_api_key = config.llm_api_key if config else None + if config: + config.llm_api_key = request.llm_api_key + search_agent.config.llm_api_key = request.llm_api_key + + try: + # 执行搜索 + ctx.add_tool("web_search") + ctx.add_tool("content_reader") + result = await search_agent.search(request.query) + + # 转换响应 + sources = [ + Source( + index=s.index, + title=s.title, + url=s.url + ) + for s in result.answer.sources + ] + + return SearchResponse( + query=request.query, + answer=result.answer.content, + sources=sources, + confidence=result.answer.confidence, + iterations=result.iterations, + total_sources=result.total_sources_consulted, + search_queries=result.search_queries_used, + timestamp=datetime.utcnow().isoformat() + ) + finally: + # 恢复原始API key + if config and original_api_key: + config.llm_api_key = original_api_key + search_agent.config.llm_api_key = original_api_key + except Exception as e: + logger.error(f"搜索失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"搜索失败: {str(e)}") + + +@app.post("/chat") +async def chat(request: SearchRequest): + """聊天接口(别名)""" + return await search(request) + + +@app.get("/") +async def root(): + """根路径""" + return { + "name": "Intelligent Search AI Agent", + "version": "1.0.0", + "endpoints": { + "health": "/health", + "status": "/status", + "configure": "/configure", + "search": "/search", + "chat": "/chat" + } + } + + +# ==================== 启动函数 ==================== + +def main(): + """主函数""" + logger.info(f"启动 Search Agent - {POD_NAME}") + logger.info(f"Template Type: {TEMPLATE_TYPE}") + + # 尝试从环境变量初始化 + if os.getenv("LLM_API_KEY"): + logger.info("检测到环境变量配置,尝试自动初始化...") + initialize_agent_from_env() + else: + logger.info("未检测到环境变量配置,等待通过API配置...") + + # 启动服务 + uvicorn.run( + app, + host=SERVICE_HOST, + port=SERVICE_PORT, + log_level="info" + ) + + +if __name__ == "__main__": + main() diff --git a/agent_templates/agents/search_agent/search_agent/DESIGN.md b/agent_templates/agents/search_agent/search_agent/DESIGN.md new file mode 100644 index 0000000..1462050 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/DESIGN.md @@ -0,0 +1,1031 @@ +# 🔍 智能AI搜索Agent - 技术设计文档 + +> **版本**: v1.0 +> **更新日期**: 2026-01-13 +> **技术栈**: Python + xchat52(GPT-5.2) + Serper + Jina Reader + +--- + +## 一、项目概述 + +### 1.1 项目目标 + +构建一个智能AI搜索Agent,能够: +- 理解用户的复杂查询意图 +- 自动规划和执行多轮搜索 +- 从多个来源获取和整合信息 +- 生成高质量、有来源引用的答案 + +### 1.2 核心能力 + +| 能力 | 描述 | +|------|------| +| 🧠 查询理解 | 分析用户意图,扩展和优化搜索词 | +| 📋 搜索规划 | 智能分解问题,制定搜索策略 | +| 🔎 多源搜索 | Web搜索 + 新闻搜索 | +| 📄 内容提取 | 智能提取网页核心内容 | +| 🎯 结果排序 | 基于相关性重排搜索结果 | +| ✍️ 答案生成 | 综合信息生成结构化回答 | +| 🔄 自我反思 | 评估答案质量,决定是否迭代 | + +### 1.3 技术选型 + +| 组件 | 选型 | 说明 | +|------|------|------| +| LLM | xchat52 (GPT-5.2) | 主推理引擎 | +| Web搜索 | Serper API | Google搜索代理 | +| 内容提取 | Jina Reader | 网页转Markdown | +| 重排序 | Jina Reranker | 结果相关性排序 | +| 框架 | Python原生 | 轻量级实现 | + +--- + +## 二、系统架构 + +### 2.1 整体架构图 + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ 智能AI搜索Agent │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Agent Core (主控制器) │ │ +│ │ 负责协调各模块,管理工作流程 │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────────────────┼────────────────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌──────────────┐ ┌──────────┐ │ +│ │ 查询理解 │ │ 搜索规划 │ │ 反思迭代 │ │ +│ │ 模块 │──────────────▶│ 模块 │◀─────────────│ 模块 │ │ +│ └──────────┘ └──────────────┘ └──────────┘ │ +│ │ ▲ │ +│ ▼ │ │ +│ ┌─────────────────────────┐ │ │ +│ │ 搜索执行模块 │ │ │ +│ │ ┌─────────┬─────────┐ │ │ │ +│ │ │Serper │Serper │ │ │ │ +│ │ │Web搜索 │新闻搜索 │ │ │ │ +│ │ └─────────┴─────────┘ │ │ │ +│ └─────────────────────────┘ │ │ +│ │ │ │ +│ ▼ │ │ +│ ┌─────────────────────────┐ │ │ +│ │ 内容提取模块 │ │ │ +│ │ (Jina Reader) │ │ │ +│ └─────────────────────────┘ │ │ +│ │ │ │ +│ ▼ │ │ +│ ┌─────────────────────────┐ │ │ +│ │ 结果处理模块 │ │ │ +│ │ • 去重 • 排序 • 筛选 │ │ │ +│ └─────────────────────────┘ │ │ +│ │ │ │ +│ ▼ │ │ +│ ┌─────────────────────────┐ │ │ +│ │ 答案生成模块 │────────────────┘ │ +│ │ (LLM综合) │ │ +│ └─────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────┐ │ +│ │ 最终输出 │ │ +│ └──────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 数据流图 + +``` +用户查询 + │ + ▼ +┌───────────────────┐ +│ 查询理解 │ ──▶ 输出: {intent, entities, expanded_queries, need_news} +└───────────────────┘ + │ + ▼ +┌───────────────────┐ +│ 搜索规划 │ ──▶ 输出: SearchPlan {queries, sources, strategy} +└───────────────────┘ + │ + ▼ +┌───────────────────┐ +│ 搜索执行 │ ──▶ 输出: List[SearchResult] {title, url, snippet} +│ (Serper API) │ +└───────────────────┘ + │ + ▼ +┌───────────────────┐ +│ 内容提取 │ ──▶ 输出: List[Document] {url, content, title} +│ (Jina Reader) │ +└───────────────────┘ + │ + ▼ +┌───────────────────┐ +│ 结果处理 │ ──▶ 输出: List[RankedDocument] (去重+排序后) +│ (Jina Reranker) │ +└───────────────────┘ + │ + ▼ +┌───────────────────┐ +│ 答案生成 │ ──▶ 输出: {answer, sources, confidence} +│ (xchat52 LLM) │ +└───────────────────┘ + │ + ▼ +┌───────────────────┐ +│ 反思评估 │ ──▶ 决定: {complete: bool, missing_info: str} +└───────────────────┘ + │ + ├──(完整)──▶ 返回最终答案 + │ + └──(不完整)──▶ 返回搜索规划(补充搜索) +``` + +--- + +## 三、模块详细设计 + +### 3.1 查询理解模块 (QueryAnalyzer) + +**文件**: `modules/query_analyzer.py` + +**职责**: +- 分析用户查询意图 +- 提取关键实体 +- 生成扩展查询 +- 判断是否需要新闻搜索 + +**输入/输出**: +```python +# 输入 +user_query: str # "2024年AI领域有哪些重大突破?" + +# 输出 +class QueryAnalysis: + intent: str # "information_gathering" + entities: List[str] # ["AI", "2024", "突破"] + expanded_queries: List[str] # ["AI breakthroughs 2024", "人工智能突破 2024"] + need_news: bool # True + time_filter: str # "qdr:y" (过去一年) +``` + +**LLM Prompt**: +``` +你是一个查询分析专家。分析用户的搜索查询,提取以下信息: + +用户查询: {query} + +请输出JSON格式: +{ + "intent": "查询意图(fact_check/comparison/how_to/news/research)", + "entities": ["关键实体列表"], + "expanded_queries": ["扩展查询1", "扩展查询2", "扩展查询3"], + "need_news": true/false, + "time_filter": "时间过滤器(null/qdr:d/qdr:w/qdr:m/qdr:y)" +} +``` + +--- + +### 3.2 搜索规划模块 (SearchPlanner) + +**文件**: `modules/search_planner.py` + +**职责**: +- 根据查询分析制定搜索计划 +- 决定使用哪些搜索源 +- 确定搜索策略(并行/串行) + +**输入/输出**: +```python +# 输入 +query_analysis: QueryAnalysis + +# 输出 +class SearchPlan: + searches: List[SearchTask] + strategy: str # "parallel" or "sequential" + max_results_per_query: int + +class SearchTask: + query: str + source: str # "web" or "news" + time_filter: Optional[str] +``` + +**搜索策略规则**: +```python +策略选择逻辑: +1. 简单事实查询 → 单次Web搜索 +2. 时效性查询 → Web搜索 + 新闻搜索(并行) +3. 复杂分析查询 → 多个扩展查询(并行) +4. 对比类查询 → 分别搜索各对比对象(并行) +``` + +--- + +### 3.3 搜索执行模块 (SearchExecutor) + +**文件**: `modules/search_executor.py` + +**职责**: +- 执行Serper API调用 +- 支持Web搜索和新闻搜索 +- 并行执行多个搜索任务 + +**API配置**: +```python +# Serper API +SERPER_BASE_URL = "https://google.serper.dev" +ENDPOINTS = { + "web": "/search", + "news": "/news" +} +``` + +**搜索结果结构**: +```python +class SearchResult: + title: str + url: str + snippet: str + source: str # "web" or "news" + position: int + date: Optional[str] # 新闻日期 +``` + +**实现要点**: +```python +import asyncio +import aiohttp + +async def execute_search(task: SearchTask) -> List[SearchResult]: + """执行单个搜索任务""" + endpoint = ENDPOINTS[task.source] + payload = { + "q": task.query, + "num": 10 + } + if task.time_filter: + payload["tbs"] = task.time_filter + + # 调用Serper API + ... + +async def execute_plan(plan: SearchPlan) -> List[SearchResult]: + """并行执行搜索计划""" + if plan.strategy == "parallel": + tasks = [execute_search(t) for t in plan.searches] + results = await asyncio.gather(*tasks) + return flatten(results) + else: + # 串行执行 + ... +``` + +--- + +### 3.4 内容提取模块 (ContentExtractor) + +**文件**: `modules/content_extractor.py` + +**职责**: +- 使用Jina Reader提取网页内容 +- 将网页转换为干净的Markdown +- 处理提取失败情况 + +**API配置**: +```python +# Jina Reader API +JINA_READER_URL = "https://r.jina.ai/" +JINA_API_KEY = "从环境变量读取" +``` + +**实现**: +```python +async def extract_content(url: str) -> Optional[Document]: + """提取单个URL的内容""" + reader_url = f"https://r.jina.ai/{url}" + headers = { + "Authorization": f"Bearer {JINA_API_KEY}", + "Accept": "application/json" + } + + # 调用Jina Reader + ... + + return Document( + url=url, + title=response["title"], + content=response["content"][:5000] # 限制内容长度 + ) + +async def extract_batch(urls: List[str], max_concurrent: int = 5) -> List[Document]: + """批量提取内容""" + semaphore = asyncio.Semaphore(max_concurrent) + async def limited_extract(url): + async with semaphore: + return await extract_content(url) + + results = await asyncio.gather(*[limited_extract(u) for u in urls]) + return [r for r in results if r is not None] +``` + +--- + +### 3.5 结果处理模块 (ResultProcessor) + +**文件**: `modules/result_processor.py` + +**职责**: +- 结果去重(基于URL和内容相似度) +- 相关性重排序 +- 筛选Top-K结果 + +**API配置**: +```python +# Jina Reranker API +JINA_RERANKER_URL = "https://api.jina.ai/v1/rerank" +``` + +**实现**: +```python +async def rerank_results( + query: str, + documents: List[Document], + top_k: int = 5 +) -> List[RankedDocument]: + """使用Jina Reranker重排序""" + payload = { + "model": "jina-reranker-v2-base-multilingual", + "query": query, + "documents": [d.content[:1000] for d in documents], + "top_n": top_k + } + + headers = { + "Authorization": f"Bearer {JINA_API_KEY}", + "Content-Type": "application/json" + } + + # 调用API并返回排序后的结果 + ... + +def deduplicate(documents: List[Document]) -> List[Document]: + """去重:基于URL和内容相似度""" + seen_urls = set() + unique_docs = [] + + for doc in documents: + if doc.url not in seen_urls: + seen_urls.add(doc.url) + unique_docs.append(doc) + + return unique_docs +``` + +--- + +### 3.6 答案生成模块 (AnswerGenerator) + +**文件**: `modules/answer_generator.py` + +**职责**: +- 综合多个来源的信息 +- 生成结构化答案 +- 标注信息来源 + +**LLM配置**: +```python +# xchat52 (GPT-5.2) 配置 +LLM_BASE_URL = "从环境变量读取" +LLM_API_KEY = "从环境变量读取" +LLM_MODEL = "xchat52" +``` + +**Prompt模板**: +``` +你是一个专业的信息整合专家。根据以下搜索结果,回答用户的问题。 + +## 用户问题 +{query} + +## 搜索结果 +{formatted_documents} + +## 要求 +1. 综合多个来源的信息,给出全面准确的回答 +2. 使用清晰的结构组织答案(标题、列表等) +3. 在答案中标注信息来源,格式:[来源1]、[来源2] +4. 如果信息有冲突,说明不同观点 +5. 如果信息不足以回答问题,明确指出 + +## 输出格式 +{ + "answer": "结构化的答案(Markdown格式)", + "sources": [ + {"index": 1, "title": "来源标题", "url": "来源URL"}, + ... + ], + "confidence": "high/medium/low" +} +``` + +--- + +### 3.7 反思迭代模块 (Reflector) + +**文件**: `modules/reflector.py` + +**职责**: +- 评估答案质量 +- 识别信息缺口 +- 决定是否需要补充搜索 + +**评估维度**: +```python +class QualityAssessment: + completeness: float # 完整性 0-1 + relevance: float # 相关性 0-1 + confidence: float # 置信度 0-1 + missing_aspects: List[str] # 缺失的方面 + needs_more_search: bool + suggested_queries: List[str] # 建议的补充搜索 +``` + +**LLM Prompt**: +``` +评估以下答案的质量: + +## 用户问题 +{query} + +## 生成的答案 +{answer} + +## 评估要求 +1. 答案是否完整回答了用户问题? +2. 是否有明显的信息缺失? +3. 是否需要补充搜索? + +## 输出JSON +{ + "completeness": 0.0-1.0, + "missing_aspects": ["缺失的方面"], + "needs_more_search": true/false, + "suggested_queries": ["建议的补充搜索词"] +} +``` + +**迭代控制**: +```python +MAX_ITERATIONS = 3 # 最大迭代次数 +COMPLETENESS_THRESHOLD = 0.8 # 完整性阈值 +``` + +--- + +## 四、API接口设计 + +### 4.1 Serper API + +**Web搜索**: +```python +POST https://google.serper.dev/search +Headers: + X-API-KEY: {SERPER_API_KEY} + Content-Type: application/json +Body: +{ + "q": "搜索词", + "num": 10, + "gl": "cn", # 可选,地区 + "hl": "zh-cn", # 可选,语言 + "tbs": "qdr:m" # 可选,时间过滤 +} +Response: +{ + "organic": [ + { + "title": "标题", + "link": "URL", + "snippet": "摘要", + "position": 1 + } + ] +} +``` + +**新闻搜索**: +```python +POST https://google.serper.dev/news +Headers: (同上) +Body: +{ + "q": "搜索词", + "num": 10 +} +Response: +{ + "news": [ + { + "title": "新闻标题", + "link": "URL", + "snippet": "摘要", + "date": "2 hours ago", + "source": "来源网站" + } + ] +} +``` + +### 4.2 Jina Reader API + +**网页内容提取**: +```python +GET https://r.jina.ai/{URL} +Headers: + Authorization: Bearer {JINA_API_KEY} + Accept: application/json +Response: +{ + "title": "页面标题", + "content": "Markdown格式的页面内容", + "url": "原始URL" +} +``` + +### 4.3 Jina Reranker API + +**结果重排序**: +```python +POST https://api.jina.ai/v1/rerank +Headers: + Authorization: Bearer {JINA_API_KEY} + Content-Type: application/json +Body: +{ + "model": "jina-reranker-v2-base-multilingual", + "query": "查询", + "documents": ["文档1", "文档2", ...], + "top_n": 5 +} +Response: +{ + "results": [ + { + "index": 0, + "relevance_score": 0.95 + } + ] +} +``` + +### 4.4 xchat52 LLM API + +**Chat Completion**: +```python +POST {LLM_BASE_URL}/chat/completions +Headers: + Authorization: Bearer {LLM_API_KEY} + Content-Type: application/json +Body: +{ + "model": "xchat52", + "messages": [ + {"role": "system", "content": "系统提示"}, + {"role": "user", "content": "用户消息"} + ], + "temperature": 0.7, + "max_tokens": 4096 +} +``` + +--- + +## 五、项目结构 + +``` +aks_agent/ +├── main.py # 程序入口 +├── config.py # 配置管理 +├── requirements.txt # Python依赖 +├── .env # 环境变量(API密钥) +├── DESIGN.md # 本设计文档 +│ +├── agent/ +│ ├── __init__.py +│ ├── search_agent.py # 主Agent类 +│ └── prompts.py # 所有Prompt模板 +│ +├── modules/ +│ ├── __init__.py +│ ├── query_analyzer.py # 查询理解模块 +│ ├── search_planner.py # 搜索规划模块 +│ ├── search_executor.py # 搜索执行模块 +│ ├── content_extractor.py # 内容提取模块 +│ ├── result_processor.py # 结果处理模块 +│ ├── answer_generator.py # 答案生成模块 +│ └── reflector.py # 反思迭代模块 +│ +├── tools/ +│ ├── __init__.py +│ ├── serper.py # Serper API封装 +│ ├── jina_reader.py # Jina Reader封装 +│ └── jina_reranker.py # Jina Reranker封装 +│ +├── models/ +│ ├── __init__.py +│ └── schemas.py # 数据模型定义 +│ +└── utils/ + ├── __init__.py + ├── llm_client.py # LLM客户端 + └── helpers.py # 工具函数 +``` + +--- + +## 六、数据模型定义 + +```python +# models/schemas.py + +from dataclasses import dataclass +from typing import List, Optional +from enum import Enum + +class SearchSource(Enum): + WEB = "web" + NEWS = "news" + +class Intent(Enum): + FACT_CHECK = "fact_check" + COMPARISON = "comparison" + HOW_TO = "how_to" + NEWS = "news" + RESEARCH = "research" + +@dataclass +class QueryAnalysis: + """查询分析结果""" + original_query: str + intent: Intent + entities: List[str] + expanded_queries: List[str] + need_news: bool + time_filter: Optional[str] = None + +@dataclass +class SearchTask: + """搜索任务""" + query: str + source: SearchSource + time_filter: Optional[str] = None + num_results: int = 10 + +@dataclass +class SearchPlan: + """搜索计划""" + tasks: List[SearchTask] + strategy: str # "parallel" or "sequential" + +@dataclass +class SearchResult: + """搜索结果""" + title: str + url: str + snippet: str + source: SearchSource + position: int + date: Optional[str] = None + +@dataclass +class Document: + """提取的文档内容""" + url: str + title: str + content: str + source: SearchSource + +@dataclass +class RankedDocument: + """排序后的文档""" + document: Document + relevance_score: float + rank: int + +@dataclass +class Source: + """来源引用""" + index: int + title: str + url: str + +@dataclass +class Answer: + """生成的答案""" + content: str # Markdown格式 + sources: List[Source] + confidence: str # "high", "medium", "low" + +@dataclass +class QualityAssessment: + """质量评估""" + completeness: float + missing_aspects: List[str] + needs_more_search: bool + suggested_queries: List[str] + +@dataclass +class AgentResponse: + """Agent最终响应""" + answer: Answer + iterations: int + total_sources_consulted: int + search_queries_used: List[str] +``` + +--- + +## 七、核心流程实现 + +### 7.1 主Agent类 + +```python +# agent/search_agent.py + +class SearchAgent: + def __init__(self, config: Config): + self.config = config + self.query_analyzer = QueryAnalyzer(config) + self.search_planner = SearchPlanner(config) + self.search_executor = SearchExecutor(config) + self.content_extractor = ContentExtractor(config) + self.result_processor = ResultProcessor(config) + self.answer_generator = AnswerGenerator(config) + self.reflector = Reflector(config) + + async def search(self, query: str) -> AgentResponse: + """执行智能搜索""" + iteration = 0 + all_documents = [] + all_queries = [] + + # 1. 查询理解 + analysis = await self.query_analyzer.analyze(query) + + while iteration < self.config.max_iterations: + iteration += 1 + + # 2. 搜索规划 + plan = await self.search_planner.plan(analysis) + all_queries.extend([t.query for t in plan.tasks]) + + # 3. 执行搜索 + search_results = await self.search_executor.execute(plan) + + # 4. 内容提取 + urls = [r.url for r in search_results[:10]] + documents = await self.content_extractor.extract_batch(urls) + all_documents.extend(documents) + + # 5. 结果处理 + ranked_docs = await self.result_processor.process( + query=query, + documents=all_documents + ) + + # 6. 生成答案 + answer = await self.answer_generator.generate( + query=query, + documents=ranked_docs + ) + + # 7. 反思评估 + assessment = await self.reflector.assess(query, answer) + + if not assessment.needs_more_search: + break + + # 更新分析,准备下一轮搜索 + analysis.expanded_queries = assessment.suggested_queries + + return AgentResponse( + answer=answer, + iterations=iteration, + total_sources_consulted=len(all_documents), + search_queries_used=all_queries + ) +``` + +### 7.2 使用示例 + +```python +# main.py + +import asyncio +from config import Config +from agent.search_agent import SearchAgent + +async def main(): + # 加载配置 + config = Config.from_env() + + # 创建Agent + agent = SearchAgent(config) + + # 执行搜索 + query = "2024年AI领域有哪些重大突破?" + response = await agent.search(query) + + # 输出结果 + print("=" * 60) + print("📝 答案:") + print(response.answer.content) + print("\n📚 来源:") + for source in response.answer.sources: + print(f" [{source.index}] {source.title}") + print(f" {source.url}") + print(f"\n📊 统计:") + print(f" - 迭代次数: {response.iterations}") + print(f" - 参考来源数: {response.total_sources_consulted}") + print(f" - 搜索查询数: {len(response.search_queries_used)}") + +if __name__ == "__main__": + asyncio.run(main()) +``` + +--- + +## 八、配置管理 + +### 8.1 环境变量 (.env) + +```bash +# LLM配置 (xchat52) +LLM_BASE_URL=https://apis.openroutex.com/openai/deployments/xchat52 +LLM_API_KEY=a76ef8d69da64ad99c4bf9739f09585b +LLM_MODEL=xchat52 + +# Serper配置 +SERPER_API_KEY=8253b4f240b520194065312f90e85f9be0fa205f + +# Jina配置 +JINA_API_KEY=jina_e26dc30420a44a1e859216528065b203TkMRmsoz-FgMDQC5FZX9jr5oF2CI + +# Agent配置 +MAX_ITERATIONS=3 +MAX_RESULTS_PER_QUERY=10 +CONTENT_MAX_LENGTH=5000 + +# 日志配置 +LOG_LEVEL=INFO +TIMEOUT=30 +``` + +### 8.2 配置类 + +```python +# config.py + +import os +from dataclasses import dataclass +from dotenv import load_dotenv + +@dataclass +class Config: + # LLM + llm_base_url: str + llm_api_key: str + llm_model: str + + # Serper + serper_api_key: str + + # Jina + jina_api_key: str + + # Agent + max_iterations: int + max_results_per_query: int + content_max_length: int + + @classmethod + def from_env(cls) -> "Config": + load_dotenv() + return cls( + llm_base_url=os.getenv("LLM_BASE_URL"), + llm_api_key=os.getenv("LLM_API_KEY"), + llm_model=os.getenv("LLM_MODEL", "xchat52"), + serper_api_key=os.getenv("SERPER_API_KEY"), + jina_api_key=os.getenv("JINA_API_KEY"), + max_iterations=int(os.getenv("MAX_ITERATIONS", 3)), + max_results_per_query=int(os.getenv("MAX_RESULTS_PER_QUERY", 10)), + content_max_length=int(os.getenv("CONTENT_MAX_LENGTH", 5000)) + ) +``` + +--- + +## 九、依赖清单 + +``` +# requirements.txt + +# HTTP客户端 +aiohttp>=3.9.0 +requests>=2.31.0 + +# 环境变量 +python-dotenv>=1.0.0 + +# JSON处理 +orjson>=3.9.0 + +# 类型提示 +typing-extensions>=4.9.0 + +# 日志 +loguru>=0.7.0 + +# 异步工具 +asyncio-throttle>=1.0.2 +``` + +--- + +## 十、开发路线图 + +### Phase 1: 基础框架 (Day 1-2) +- [ ] 项目结构搭建 +- [ ] 配置管理实现 +- [ ] 数据模型定义 +- [ ] LLM客户端封装 + +### Phase 2: 工具封装 (Day 2-3) +- [ ] Serper API封装(Web+新闻) +- [ ] Jina Reader封装 +- [ ] Jina Reranker封装 + +### Phase 3: 核心模块 (Day 3-5) +- [ ] 查询理解模块 +- [ ] 搜索规划模块 +- [ ] 搜索执行模块 +- [ ] 内容提取模块 +- [ ] 结果处理模块 +- [ ] 答案生成模块 +- [ ] 反思迭代模块 + +### Phase 4: Agent整合 (Day 5-6) +- [ ] 主Agent类实现 +- [ ] 流程编排 +- [ ] 错误处理 + +### Phase 5: 优化与测试 (Day 6-7) +- [ ] 单元测试 +- [ ] 集成测试 +- [ ] 性能优化 +- [ ] 日志完善 + +--- + +## 十一、扩展方向 + +1. **搜索源扩展** + - 学术搜索 (arXiv, Google Scholar) + - 图片搜索 + - 视频搜索 + +2. **功能增强** + - 对话历史记忆 + - 搜索结果缓存 + - 流式输出 + +3. **UI界面** + - Gradio/Streamlit Web界面 + - CLI交互模式 + +4. **部署方案** + - Docker容器化 + - API服务化 + +--- + +## 附录:Prompt模板汇总 + +所有Prompt模板集中管理在 `agent/prompts.py` 文件中,便于维护和调优。 + +```python +# agent/prompts.py + +QUERY_ANALYSIS_PROMPT = """...""" +SEARCH_PLANNING_PROMPT = """...""" +ANSWER_GENERATION_PROMPT = """...""" +REFLECTION_PROMPT = """...""" +``` + diff --git a/agent_templates/agents/search_agent/search_agent/README.md b/agent_templates/agents/search_agent/search_agent/README.md new file mode 100644 index 0000000..94d47db --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/README.md @@ -0,0 +1,253 @@ +# 🔍 智能AI搜索Agent + +一个基于大语言模型的智能搜索代理,能够理解用户查询意图、自动规划搜索策略、从多个来源获取信息,并生成高质量、有来源引用的答案。 + +## ✨ 功能特点 + +| 能力 | 描述 | +|------|------| +| 🧠 查询理解 | 分析用户意图,提取关键实体,生成扩展查询 | +| 📋 搜索规划 | 智能分解问题,制定搜索策略 | +| 🔎 多源搜索 | 支持Web搜索和新闻搜索 | +| 📄 内容提取 | 智能提取网页核心内容 | +| 🎯 结果排序 | 基于相关性重排搜索结果 | +| ✍️ 答案生成 | 综合信息生成结构化回答 | +| 🔄 自我反思 | 评估答案质量,决定是否迭代 | + +## 🛠️ 技术栈 + +| 组件 | 选型 | 说明 | +|------|------|------| +| LLM | xchat52 (GPT-5.2) | 主推理引擎 | +| Web搜索 | Serper API | Google搜索代理 | +| 内容提取 | Jina Reader | 网页转Markdown | +| 重排序 | Jina Reranker | 结果相关性排序 | +| 框架 | Python原生 + asyncio | 异步高效执行 | + +## 📁 项目结构 + +``` +search_agent/ +├── main.py # 程序入口 +├── config.py # 配置管理 +├── requirements.txt # Python依赖 +├── .env # 环境变量配置 +│ +├── agent/ +│ ├── __init__.py +│ ├── search_agent.py # 主Agent类 +│ └── prompts.py # Prompt模板 +│ +├── modules/ +│ ├── __init__.py +│ ├── query_analyzer.py # 查询理解模块 +│ ├── search_planner.py # 搜索规划模块 +│ ├── search_executor.py # 搜索执行模块 +│ ├── content_extractor.py # 内容提取模块 +│ ├── result_processor.py # 结果处理模块 +│ ├── answer_generator.py # 答案生成模块 +│ └── reflector.py # 反思迭代模块 +│ +├── tools/ +│ ├── __init__.py +│ ├── serper.py # Serper API封装 +│ ├── jina_reader.py # Jina Reader封装 +│ └── jina_reranker.py # Jina Reranker封装 +│ +├── models/ +│ ├── __init__.py +│ └── schemas.py # 数据模型定义 +│ +└── utils/ + ├── __init__.py + ├── llm_client.py # LLM客户端 + └── helpers.py # 工具函数 +``` + +## 🚀 快速开始 + +### 1. 安装依赖 + +```bash +cd search_agent +pip install -r requirements.txt +``` + +### 2. 配置环境变量 + +创建 `.env` 文件: + +```bash +# LLM配置 (xchat52) +LLM_BASE_URL=https://apis.openroutex.com/openai/deployments/xchat52 +LLM_API_KEY=你的API密钥 +LLM_MODEL=xchat52 + +# Serper配置 (Google搜索) +SERPER_API_KEY=你的Serper_API_KEY + +# Jina配置 (内容提取和重排序) +JINA_API_KEY=你的Jina_API_KEY + +# Agent配置 +MAX_ITERATIONS=3 # 最大迭代次数 +MAX_RESULTS_PER_QUERY=10 # 每次搜索返回结果数 +CONTENT_MAX_LENGTH=5000 # 提取内容最大长度 + +# 日志配置 +LOG_LEVEL=INFO +TIMEOUT=30 +``` + +### 3. 运行程序 + +**交互模式**(推荐): +```bash +python main.py +``` + +**单次查询**: +```bash +python main.py "你的问题" +``` + +## 📖 使用示例 + +``` +🔍 智能AI搜索Agent +====================================================================== +输入您的问题进行搜索,输入 'quit' 或 'exit' 退出 +====================================================================== + +🔎 请输入问题: 什么是大语言模型? + +====================================================================== +📝 答案: +====================================================================== +## 大语言模型(LLM)是什么? + +**大语言模型(Large Language Model, LLM)**是一类用**海量文本数据**进行 +**预训练**的**超大规模深度学习模型**... + +---------------------------------------------------------------------- +📚 来源: +---------------------------------------------------------------------- + [1] 大语言模型 (LLM) + 🔗 https://www.ibm.com/cn-zh/think/topics/large-language-models + [2] 什么是 LLM(大型语言模型)? + 🔗 https://aws.amazon.com/cn/what-is/large-language-model/ + ... + +---------------------------------------------------------------------- +📊 统计: +---------------------------------------------------------------------- + • 置信度: high + • 迭代次数: 1 + • 参考来源数: 10 + • 搜索查询数: 3 +====================================================================== +``` + +## 🔄 工作流程 + +``` +用户查询 + │ + ▼ +┌───────────────────┐ +│ 查询理解 │ ──▶ 分析意图、提取实体、生成扩展查询 +└───────────────────┘ + │ + ▼ +┌───────────────────┐ +│ 搜索规划 │ ──▶ 制定搜索策略(Web/新闻、并行/串行) +└───────────────────┘ + │ + ▼ +┌───────────────────┐ +│ 搜索执行 │ ──▶ 调用Serper API执行搜索 +└───────────────────┘ + │ + ▼ +┌───────────────────┐ +│ 内容提取 │ ──▶ 使用Jina Reader提取网页内容 +└───────────────────┘ + │ + ▼ +┌───────────────────┐ +│ 结果处理 │ ──▶ 去重 + Jina Reranker重排序 +└───────────────────┘ + │ + ▼ +┌───────────────────┐ +│ 答案生成 │ ──▶ LLM综合生成结构化答案 +└───────────────────┘ + │ + ▼ +┌───────────────────┐ +│ 反思评估 │ ──▶ 评估完整性,决定是否继续迭代 +└───────────────────┘ + │ + ├──(完整)──▶ 返回最终答案 + │ + └──(不完整)──▶ 补充搜索(回到搜索规划) +``` + +## ⚙️ 配置说明 + +| 配置项 | 默认值 | 说明 | +|--------|--------|------| +| `MAX_ITERATIONS` | 3 | 最大迭代次数,防止无限循环 | +| `MAX_RESULTS_PER_QUERY` | 10 | 每次搜索返回的结果数量 | +| `CONTENT_MAX_LENGTH` | 5000 | 提取内容的最大字符数 | +| `LOG_LEVEL` | INFO | 日志级别 (DEBUG/INFO/WARNING/ERROR) | +| `TIMEOUT` | 30 | API请求超时时间(秒)| + +## 🔧 API说明 + +### Serper API +- **Web搜索**: `POST https://google.serper.dev/search` +- **新闻搜索**: `POST https://google.serper.dev/news` +- [获取API Key](https://serper.dev/) + +### Jina API +- **内容提取**: `GET https://r.jina.ai/{URL}` +- **重排序**: `POST https://api.jina.ai/v1/rerank` +- [获取API Key](https://jina.ai/) + +### LLM API (Azure OpenAI风格) +- **Chat**: `POST {BASE_URL}/chat/completions?api-version=2024-10-21` + +## 📝 编程接口 + +```python +import asyncio +from config import Config +from agent.search_agent import SearchAgent + +async def main(): + # 加载配置 + config = Config.from_env() + + # 创建Agent + agent = SearchAgent(config) + + # 执行搜索 + response = await agent.search("你的问题") + + # 获取答案 + print(response.answer.content) + print(response.answer.sources) + print(response.answer.confidence) + +asyncio.run(main()) +``` + +## 📄 License + +MIT License + +## 🤝 贡献 + +欢迎提交Issue和Pull Request! + diff --git a/agent_templates/agents/search_agent/search_agent/agent/__init__.py b/agent_templates/agents/search_agent/search_agent/agent/__init__.py new file mode 100644 index 0000000..1b9f6a9 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/agent/__init__.py @@ -0,0 +1,18 @@ +""" +Agent模块 +""" + +from .search_agent import SearchAgent +from .prompts import ( + QUERY_ANALYSIS_PROMPT, + ANSWER_GENERATION_PROMPT, + REFLECTION_PROMPT, +) + +__all__ = [ + "SearchAgent", + "QUERY_ANALYSIS_PROMPT", + "ANSWER_GENERATION_PROMPT", + "REFLECTION_PROMPT", +] + diff --git a/agent_templates/agents/search_agent/search_agent/agent/prompts.py b/agent_templates/agents/search_agent/search_agent/agent/prompts.py new file mode 100644 index 0000000..decc585 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/agent/prompts.py @@ -0,0 +1,126 @@ +""" +Prompt模板汇总 +集中管理所有LLM Prompt模板 +""" + +# ==================== 查询分析 Prompt ==================== +QUERY_ANALYSIS_PROMPT = """你是一个查询分析专家。分析用户的搜索查询,提取以下信息。 + +请输出JSON格式: +{ + "intent": "查询意图,必须是以下之一: fact_check(事实核查), comparison(对比分析), how_to(操作指南), news(新闻资讯), research(深度研究)", + "entities": ["关键实体列表,提取查询中的核心概念、人名、产品名等"], + "expanded_queries": ["扩展查询1", "扩展查询2", "扩展查询3"], + "need_news": true或false, + "time_filter": "时间过滤器,null表示不限时间,qdr:d(过去24小时), qdr:w(过去一周), qdr:m(过去一月), qdr:y(过去一年)" +} + +扩展查询要求: +1. 生成2-4个扩展查询,包含不同角度或同义表达 +2. 至少包含一个英文查询(如果原查询是中文) +3. 保持查询的核心意图 + +时间过滤器选择规则: +- 查询涉及"最新"、"近期"、"今年"等时效性词语 → 设置相应的时间过滤器 +- 查询涉及具体年份(如"2024年") → qdr:y +- 一般性查询 → null""" + + +# ==================== 搜索规划 Prompt ==================== +SEARCH_PLANNING_PROMPT = """你是一个搜索规划专家。根据查询分析结果,制定搜索计划。 + +输入信息: +- 原始查询 +- 查询意图 +- 关键实体 +- 是否需要新闻 + +输出搜索任务列表,每个任务包含: +- query: 搜索词 +- source: web 或 news +- time_filter: 时间过滤器(可选) + +搜索策略规则: +1. 简单事实查询 → 单次Web搜索 +2. 时效性查询 → Web搜索 + 新闻搜索 +3. 复杂分析查询 → 多个扩展查询 +4. 对比类查询 → 分别搜索各对比对象""" + + +# ==================== 答案生成 Prompt ==================== +ANSWER_GENERATION_PROMPT = """你是一个专业的信息整合专家。根据以下搜索结果,回答用户的问题。 + +## 要求 +1. 综合多个来源的信息,给出全面准确的回答 +2. 使用清晰的结构组织答案(标题、列表、重点标注等) +3. 在答案中标注信息来源,格式:[来源1]、[来源2] +4. 如果信息有冲突,说明不同观点 +5. 如果信息不足以完整回答问题,明确指出缺失的部分 +6. 回答使用中文 + +## 输出JSON格式 +{ + "answer": "结构化的答案(Markdown格式,包含来源引用)", + "sources": [ + {"index": 1, "title": "来源标题", "url": "来源URL"}, + {"index": 2, "title": "来源标题", "url": "来源URL"} + ], + "confidence": "high/medium/low,基于信息质量和一致性判断" +}""" + + +# ==================== 反思评估 Prompt ==================== +REFLECTION_PROMPT = """你是一个质量评估专家。评估以下答案是否充分回答了用户的问题。 + +## 评估维度 +1. **完整性**: 答案是否覆盖了问题的所有方面? +2. **准确性**: 答案内容是否有明确的来源支持? +3. **深度**: 答案是否提供了足够的细节和解释? + +## 输出JSON格式 +{ + "completeness": 0.0-1.0, + "missing_aspects": ["如果有缺失,列出缺失的方面"], + "needs_more_search": true或false, + "suggested_queries": ["如果需要补充搜索,建议的搜索词"] +} + +## 判断标准 +- completeness >= 0.8 且没有重要信息缺失 → needs_more_search = false +- completeness < 0.8 或有重要信息缺失 → needs_more_search = true +- 建议的搜索词应该针对缺失的方面""" + + +# ==================== 工具函数 ==================== +def format_query_analysis_prompt(query: str) -> str: + """格式化查询分析Prompt""" + return f"{QUERY_ANALYSIS_PROMPT}\n\n用户查询: {query}" + + +def format_answer_generation_prompt(query: str, documents: str) -> str: + """格式化答案生成Prompt""" + return f"""{ANSWER_GENERATION_PROMPT} + +## 用户问题 +{query} + +## 搜索结果 +{documents}""" + + +def format_reflection_prompt(query: str, answer: str, sources_count: int, confidence: str) -> str: + """格式化反思评估Prompt""" + return f"""{REFLECTION_PROMPT} + +## 用户问题 +{query} + +## 生成的答案 +{answer} + +## 答案的来源数量 +{sources_count} 个来源 + +## 答案的置信度 +{confidence}""" + diff --git a/agent_templates/agents/search_agent/search_agent/agent/search_agent.py b/agent_templates/agents/search_agent/search_agent/agent/search_agent.py new file mode 100644 index 0000000..f85e3fa --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/agent/search_agent.py @@ -0,0 +1,209 @@ +""" +搜索Agent主类 +协调各模块执行智能搜索 +""" + +from typing import List, Optional +from loguru import logger + +from config import Config +from models.schemas import ( + QueryAnalysis, + SearchPlan, + SearchResult, + Document, + RankedDocument, + Answer, + AgentResponse, +) +from modules.query_analyzer import QueryAnalyzer +from modules.search_planner import SearchPlanner +from modules.search_executor import SearchExecutor +from modules.content_extractor import ContentExtractor +from modules.result_processor import ResultProcessor +from modules.answer_generator import AnswerGenerator +from modules.reflector import Reflector + + +class SearchAgent: + """智能搜索Agent""" + + def __init__(self, config: Config): + """ + 初始化搜索Agent + + Args: + config: 配置对象 + """ + self.config = config + + # 初始化各模块 + self.query_analyzer = QueryAnalyzer(config) + self.search_planner = SearchPlanner(config) + self.search_executor = SearchExecutor(config) + self.content_extractor = ContentExtractor(config) + self.result_processor = ResultProcessor(config) + self.answer_generator = AnswerGenerator(config) + self.reflector = Reflector(config) + + logger.info("SearchAgent 初始化完成") + + async def search(self, query: str) -> AgentResponse: + """ + 执行智能搜索 + + Args: + query: 用户查询 + + Returns: + AgentResponse对象 + """ + logger.info(f"="*60) + logger.info(f"开始搜索: {query}") + logger.info(f"="*60) + + iteration = 0 + all_documents: List[Document] = [] + all_queries: List[str] = [] + + # 1. 查询理解 + analysis = await self.query_analyzer.analyze(query) + logger.info(f"查询分析完成: intent={analysis.intent.value}") + + answer: Optional[Answer] = None + + while iteration < self.config.max_iterations: + iteration += 1 + logger.info(f"\n--- 迭代 {iteration}/{self.config.max_iterations} ---") + + # 2. 搜索规划 + if iteration == 1: + plan = await self.search_planner.plan(analysis) + else: + # 后续迭代使用建议的补充查询 + plan = self.search_planner.plan_supplementary( + query, + analysis.expanded_queries + ) + + all_queries.extend([t.query for t in plan.tasks]) + logger.info(f"搜索计划: {len(plan.tasks)} 个任务") + + # 3. 执行搜索 + search_results = await self.search_executor.execute(plan) + logger.info(f"搜索结果: {len(search_results)} 条") + + if not search_results: + logger.warning("没有搜索结果") + if answer is None: + answer = self.answer_generator._empty_answer() + break + + # 4. 内容提取 + documents = await self.content_extractor.extract_batch( + search_results, + max_urls=10 + ) + all_documents.extend(documents) + logger.info(f"提取文档: {len(documents)} 个") + + if not documents: + logger.warning("没有成功提取到文档内容") + continue + + # 5. 结果处理(去重+重排序) + ranked_docs = await self.result_processor.process( + query=query, + documents=all_documents, + top_k=5 + ) + logger.info(f"排序结果: {len(ranked_docs)} 个") + + if not ranked_docs: + logger.warning("没有有效的排序结果") + continue + + # 6. 生成答案 + answer = await self.answer_generator.generate( + query=query, + documents=ranked_docs + ) + logger.info(f"答案生成完成: confidence={answer.confidence}") + + # 7. 反思评估 + assessment = await self.reflector.assess(query, answer) + + # 8. 判断是否继续迭代 + if not self.reflector.should_continue(assessment, iteration): + break + + # 更新分析,准备下一轮搜索 + if assessment.suggested_queries: + analysis.expanded_queries = assessment.suggested_queries + logger.info(f"补充搜索: {assessment.suggested_queries}") + + # 确保有答案返回 + if answer is None: + answer = self.answer_generator._empty_answer() + + # 去重统计 + unique_urls = set(d.url for d in all_documents) + + response = AgentResponse( + answer=answer, + iterations=iteration, + total_sources_consulted=len(unique_urls), + search_queries_used=list(set(all_queries)) + ) + + logger.info(f"\n{'='*60}") + logger.info(f"搜索完成!") + logger.info(f"迭代次数: {iteration}") + logger.info(f"参考来源: {len(unique_urls)}") + logger.info(f"搜索查询: {len(response.search_queries_used)}") + logger.info(f"{'='*60}\n") + + return response + + async def quick_search(self, query: str) -> Answer: + """ + 快速搜索(单次迭代) + + Args: + query: 用户查询 + + Returns: + Answer对象 + """ + # 简化分析 + analysis = await self.query_analyzer.analyze(query) + + # 只执行一次搜索 + plan = await self.search_planner.plan(analysis) + plan.tasks = plan.tasks[:2] # 限制搜索任务数量 + + # 执行搜索 + search_results = await self.search_executor.execute(plan) + + if not search_results: + return self.answer_generator._empty_answer() + + # 提取内容 + documents = await self.content_extractor.extract_batch( + search_results, + max_urls=5 + ) + + if not documents: + return self.answer_generator._empty_answer() + + # 处理结果 + ranked_docs = await self.result_processor.process( + query=query, + documents=documents, + top_k=3 + ) + + # 生成答案 + return await self.answer_generator.generate(query, ranked_docs) + diff --git a/agent_templates/agents/search_agent/search_agent/config.py b/agent_templates/agents/search_agent/search_agent/config.py new file mode 100644 index 0000000..53fc24b --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/config.py @@ -0,0 +1,81 @@ +""" +配置管理模块 +负责加载和管理所有配置项 +""" + +import os +from dataclasses import dataclass +from typing import Optional +from dotenv import load_dotenv + + +@dataclass +class Config: + """Agent配置类""" + + # LLM配置 + llm_base_url: str + llm_api_key: str + llm_model: str + + # Serper配置 + serper_api_key: str + + # Jina配置 + jina_api_key: str + + # Agent配置 + max_iterations: int + max_results_per_query: int + content_max_length: int + + # 可选配置 + log_level: str = "INFO" + timeout: int = 30 + + @classmethod + def from_env(cls, env_path: Optional[str] = None) -> "Config": + """从环境变量加载配置""" + if env_path: + load_dotenv(env_path) + else: + load_dotenv() + + return cls( + # LLM配置 + llm_base_url=os.getenv("LLM_BASE_URL", ""), + llm_api_key=os.getenv("LLM_API_KEY", ""), + llm_model=os.getenv("LLM_MODEL", "xchat52"), + + # Serper配置 + serper_api_key=os.getenv("SERPER_API_KEY", ""), + + # Jina配置 + jina_api_key=os.getenv("JINA_API_KEY", ""), + + # Agent配置 + max_iterations=int(os.getenv("MAX_ITERATIONS", "3")), + max_results_per_query=int(os.getenv("MAX_RESULTS_PER_QUERY", "10")), + content_max_length=int(os.getenv("CONTENT_MAX_LENGTH", "5000")), + + # 可选配置 + log_level=os.getenv("LOG_LEVEL", "INFO"), + timeout=int(os.getenv("TIMEOUT", "30")) + ) + + def validate(self) -> bool: + """验证配置是否完整""" + required_fields = [ + ("llm_base_url", self.llm_base_url), + ("llm_api_key", self.llm_api_key), + ("serper_api_key", self.serper_api_key), + ("jina_api_key", self.jina_api_key), + ] + + missing = [name for name, value in required_fields if not value] + + if missing: + raise ValueError(f"缺少必要的配置项: {', '.join(missing)}") + + return True + diff --git a/agent_templates/agents/search_agent/search_agent/main.py b/agent_templates/agents/search_agent/search_agent/main.py new file mode 100644 index 0000000..ae353d5 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/main.py @@ -0,0 +1,125 @@ +""" +智能AI搜索Agent - 程序入口 +""" + +import asyncio +import sys +from loguru import logger + +from config import Config +from agent.search_agent import SearchAgent + + +def setup_logging(level: str = "INFO"): + """配置日志""" + logger.remove() + logger.add( + sys.stderr, + level=level, + format="{time:HH:mm:ss} | {level: <8} | {message}" + ) + + +def print_response(response): + """格式化输出响应""" + print("\n" + "=" * 70) + print("📝 答案:") + print("=" * 70) + print(response.answer.content) + + print("\n" + "-" * 70) + print("📚 来源:") + print("-" * 70) + for source in response.answer.sources: + print(f" [{source.index}] {source.title}") + print(f" 🔗 {source.url}") + + print("\n" + "-" * 70) + print("📊 统计:") + print("-" * 70) + print(f" • 置信度: {response.answer.confidence}") + print(f" • 迭代次数: {response.iterations}") + print(f" • 参考来源数: {response.total_sources_consulted}") + print(f" • 搜索查询数: {len(response.search_queries_used)}") + print("=" * 70 + "\n") + + +async def main(): + """主函数""" + # 加载配置 + config = Config.from_env() + + # 配置日志 + setup_logging(config.log_level) + + # 验证配置 + try: + config.validate() + except ValueError as e: + logger.error(f"配置错误: {e}") + logger.info("请检查 .env 文件中的配置项") + return + + # 创建Agent + agent = SearchAgent(config) + + # 交互式搜索 + print("\n" + "=" * 70) + print("🔍 智能AI搜索Agent") + print("=" * 70) + print("输入您的问题进行搜索,输入 'quit' 或 'exit' 退出") + print("=" * 70 + "\n") + + while True: + try: + query = input("🔎 请输入问题: ").strip() + + if not query: + continue + + if query.lower() in ['quit', 'exit', 'q']: + print("\n👋 再见!") + break + + # 执行搜索 + response = await agent.search(query) + + # 输出结果 + print_response(response) + + except KeyboardInterrupt: + print("\n\n👋 再见!") + break + except Exception as e: + logger.error(f"搜索出错: {e}") + continue + + +async def search_once(query: str): + """ + 单次搜索(用于脚本调用) + + Args: + query: 搜索查询 + """ + config = Config.from_env() + setup_logging(config.log_level) + config.validate() + + agent = SearchAgent(config) + response = await agent.search(query) + print_response(response) + + return response + + +if __name__ == "__main__": + # 检查命令行参数 + if len(sys.argv) > 1: + # 命令行传入查询 + query = " ".join(sys.argv[1:]) + asyncio.run(search_once(query)) + else: + # 交互模式 + asyncio.run(main()) + diff --git a/agent_templates/agents/search_agent/search_agent/models/__init__.py b/agent_templates/agents/search_agent/search_agent/models/__init__.py new file mode 100644 index 0000000..96edde1 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/models/__init__.py @@ -0,0 +1,34 @@ +""" +数据模型模块 +""" + +from .schemas import ( + SearchSource, + Intent, + QueryAnalysis, + SearchTask, + SearchPlan, + SearchResult, + Document, + RankedDocument, + Source, + Answer, + QualityAssessment, + AgentResponse, +) + +__all__ = [ + "SearchSource", + "Intent", + "QueryAnalysis", + "SearchTask", + "SearchPlan", + "SearchResult", + "Document", + "RankedDocument", + "Source", + "Answer", + "QualityAssessment", + "AgentResponse", +] + diff --git a/agent_templates/agents/search_agent/search_agent/models/schemas.py b/agent_templates/agents/search_agent/search_agent/models/schemas.py new file mode 100644 index 0000000..6b4365d --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/models/schemas.py @@ -0,0 +1,202 @@ +""" +数据模型定义 +定义Agent使用的所有数据结构 +""" + +from dataclasses import dataclass, field +from typing import List, Optional +from enum import Enum + + +class SearchSource(Enum): + """搜索来源枚举""" + WEB = "web" + NEWS = "news" + + +class Intent(Enum): + """查询意图枚举""" + FACT_CHECK = "fact_check" # 事实核查 + COMPARISON = "comparison" # 对比分析 + HOW_TO = "how_to" # 操作指南 + NEWS = "news" # 新闻资讯 + RESEARCH = "research" # 深度研究 + + +@dataclass +class QueryAnalysis: + """查询分析结果""" + original_query: str # 原始查询 + intent: Intent # 查询意图 + entities: List[str] # 关键实体 + expanded_queries: List[str] # 扩展查询列表 + need_news: bool # 是否需要新闻搜索 + time_filter: Optional[str] = None # 时间过滤器 + + def to_dict(self) -> dict: + """转换为字典""" + return { + "original_query": self.original_query, + "intent": self.intent.value, + "entities": self.entities, + "expanded_queries": self.expanded_queries, + "need_news": self.need_news, + "time_filter": self.time_filter + } + + +@dataclass +class SearchTask: + """搜索任务""" + query: str # 搜索查询 + source: SearchSource # 搜索来源 + time_filter: Optional[str] = None # 时间过滤器 + num_results: int = 10 # 结果数量 + + def to_dict(self) -> dict: + """转换为字典""" + return { + "query": self.query, + "source": self.source.value, + "time_filter": self.time_filter, + "num_results": self.num_results + } + + +@dataclass +class SearchPlan: + """搜索计划""" + tasks: List[SearchTask] # 搜索任务列表 + strategy: str = "parallel" # 执行策略: parallel/sequential + + def to_dict(self) -> dict: + """转换为字典""" + return { + "tasks": [t.to_dict() for t in self.tasks], + "strategy": self.strategy + } + + +@dataclass +class SearchResult: + """搜索结果""" + title: str # 标题 + url: str # URL + snippet: str # 摘要 + source: SearchSource # 来源类型 + position: int # 排名位置 + date: Optional[str] = None # 日期(新闻) + + def to_dict(self) -> dict: + """转换为字典""" + return { + "title": self.title, + "url": self.url, + "snippet": self.snippet, + "source": self.source.value, + "position": self.position, + "date": self.date + } + + +@dataclass +class Document: + """提取的文档内容""" + url: str # URL + title: str # 标题 + content: str # 内容 + source: SearchSource # 来源类型 + + def to_dict(self) -> dict: + """转换为字典""" + return { + "url": self.url, + "title": self.title, + "content": self.content, + "source": self.source.value + } + + +@dataclass +class RankedDocument: + """排序后的文档""" + document: Document # 文档 + relevance_score: float # 相关性分数 + rank: int # 排名 + + def to_dict(self) -> dict: + """转换为字典""" + return { + "document": self.document.to_dict(), + "relevance_score": self.relevance_score, + "rank": self.rank + } + + +@dataclass +class Source: + """来源引用""" + index: int # 索引 + title: str # 标题 + url: str # URL + + def to_dict(self) -> dict: + """转换为字典""" + return { + "index": self.index, + "title": self.title, + "url": self.url + } + + +@dataclass +class Answer: + """生成的答案""" + content: str # Markdown格式的答案内容 + sources: List[Source] # 来源列表 + confidence: str # 置信度: high/medium/low + + def to_dict(self) -> dict: + """转换为字典""" + return { + "content": self.content, + "sources": [s.to_dict() for s in self.sources], + "confidence": self.confidence + } + + +@dataclass +class QualityAssessment: + """质量评估""" + completeness: float # 完整性 0-1 + missing_aspects: List[str] # 缺失的方面 + needs_more_search: bool # 是否需要更多搜索 + suggested_queries: List[str] # 建议的补充搜索 + + def to_dict(self) -> dict: + """转换为字典""" + return { + "completeness": self.completeness, + "missing_aspects": self.missing_aspects, + "needs_more_search": self.needs_more_search, + "suggested_queries": self.suggested_queries + } + + +@dataclass +class AgentResponse: + """Agent最终响应""" + answer: Answer # 答案 + iterations: int # 迭代次数 + total_sources_consulted: int # 参考来源总数 + search_queries_used: List[str] # 使用的搜索查询 + + def to_dict(self) -> dict: + """转换为字典""" + return { + "answer": self.answer.to_dict(), + "iterations": self.iterations, + "total_sources_consulted": self.total_sources_consulted, + "search_queries_used": self.search_queries_used + } + diff --git a/agent_templates/agents/search_agent/search_agent/modules/__init__.py b/agent_templates/agents/search_agent/search_agent/modules/__init__.py new file mode 100644 index 0000000..1e9bd03 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/modules/__init__.py @@ -0,0 +1,22 @@ +""" +核心模块 +""" + +from .query_analyzer import QueryAnalyzer +from .search_planner import SearchPlanner +from .search_executor import SearchExecutor +from .content_extractor import ContentExtractor +from .result_processor import ResultProcessor +from .answer_generator import AnswerGenerator +from .reflector import Reflector + +__all__ = [ + "QueryAnalyzer", + "SearchPlanner", + "SearchExecutor", + "ContentExtractor", + "ResultProcessor", + "AnswerGenerator", + "Reflector", +] + diff --git a/agent_templates/agents/search_agent/search_agent/modules/answer_generator.py b/agent_templates/agents/search_agent/search_agent/modules/answer_generator.py new file mode 100644 index 0000000..084d5bf --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/modules/answer_generator.py @@ -0,0 +1,151 @@ +""" +答案生成模块 +综合多个来源的信息生成结构化答案 +""" + +from typing import List +from loguru import logger + +from config import Config +from models.schemas import RankedDocument, Answer, Source +from utils.llm_client import LLMClient +from utils.helpers import format_documents_for_prompt + + +# 答案生成Prompt +ANSWER_GENERATION_PROMPT = """你是一个专业的信息整合专家。根据以下搜索结果,回答用户的问题。 + +## 要求 +1. 综合多个来源的信息,给出全面准确的回答 +2. 使用清晰的结构组织答案(标题、列表、重点标注等) +3. 在答案中标注信息来源,格式:[来源1]、[来源2] +4. 如果信息有冲突,说明不同观点 +5. 如果信息不足以完整回答问题,明确指出缺失的部分 +6. 回答使用中文 + +## 输出JSON格式 +{ + "answer": "结构化的答案(Markdown格式,包含来源引用)", + "sources": [ + {"index": 1, "title": "来源标题", "url": "来源URL"}, + {"index": 2, "title": "来源标题", "url": "来源URL"} + ], + "confidence": "high/medium/low,基于信息质量和一致性判断" +}""" + + +class AnswerGenerator: + """答案生成模块""" + + def __init__(self, config: Config): + """ + 初始化答案生成器 + + Args: + config: 配置对象 + """ + self.config = config + self.llm = LLMClient( + base_url=config.llm_base_url, + api_key=config.llm_api_key, + model=config.llm_model, + timeout=120 # 答案生成可能需要更长时间 + ) + + async def generate( + self, + query: str, + documents: List[RankedDocument] + ) -> Answer: + """ + 根据文档生成答案 + + Args: + query: 用户查询 + documents: 排序后的文档列表 + + Returns: + Answer对象 + """ + if not documents: + return self._empty_answer() + + logger.info(f"开始生成答案,使用 {len(documents)} 个文档") + + # 格式化文档 + formatted_docs = format_documents_for_prompt( + documents, + max_length=self.config.content_max_length // len(documents) + ) + + user_message = f"""## 用户问题 +{query} + +## 搜索结果 +{formatted_docs}""" + + try: + result = await self.llm.chat_json( + system_prompt=ANSWER_GENERATION_PROMPT, + user_message=user_message, + temperature=0.5 + ) + + # 解析来源 + sources = [ + Source( + index=s.get("index", i + 1), + title=s.get("title", ""), + url=s.get("url", "") + ) + for i, s in enumerate(result.get("sources", [])) + ] + + answer = Answer( + content=result.get("answer", ""), + sources=sources, + confidence=result.get("confidence", "medium") + ) + + logger.info(f"答案生成完成,置信度: {answer.confidence}") + return answer + + except Exception as e: + logger.error(f"答案生成失败: {e}") + return self._fallback_answer(query, documents) + + def _empty_answer(self) -> Answer: + """生成空答案(无文档时)""" + return Answer( + content="抱歉,未能找到相关信息来回答您的问题。", + sources=[], + confidence="low" + ) + + def _fallback_answer( + self, + query: str, + documents: List[RankedDocument] + ) -> Answer: + """后备答案生成(LLM失败时)""" + # 简单汇总文档内容 + content_parts = [f"关于「{query}」,以下是搜索到的相关信息:\n"] + + sources = [] + for i, doc in enumerate(documents[:5], 1): + actual_doc = doc.document + content_parts.append(f"### 来源 [{i}]: {actual_doc.title}\n") + content_parts.append(f"{actual_doc.content[:500]}...\n\n") + + sources.append(Source( + index=i, + title=actual_doc.title, + url=actual_doc.url + )) + + return Answer( + content="".join(content_parts), + sources=sources, + confidence="low" + ) + diff --git a/agent_templates/agents/search_agent/search_agent/modules/content_extractor.py b/agent_templates/agents/search_agent/search_agent/modules/content_extractor.py new file mode 100644 index 0000000..f94068e --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/modules/content_extractor.py @@ -0,0 +1,102 @@ +""" +内容提取模块 +使用Jina Reader提取网页内容 +""" + +from typing import List +from loguru import logger + +from config import Config +from models.schemas import Document, SearchResult, SearchSource +from tools.jina_reader import JinaReaderClient + + +class ContentExtractor: + """内容提取模块""" + + def __init__(self, config: Config): + """ + 初始化内容提取器 + + Args: + config: 配置对象 + """ + self.config = config + self.jina_reader = JinaReaderClient( + api_key=config.jina_api_key, + timeout=config.timeout, + max_content_length=config.content_max_length + ) + + async def extract(self, search_result: SearchResult) -> Document | None: + """ + 从搜索结果提取内容 + + Args: + search_result: 搜索结果 + + Returns: + Document对象,如果提取失败则返回None + """ + return await self.jina_reader.extract_content( + url=search_result.url, + source=search_result.source + ) + + async def extract_batch( + self, + search_results: List[SearchResult], + max_urls: int = 10 + ) -> List[Document]: + """ + 批量提取内容 + + Args: + search_results: 搜索结果列表 + max_urls: 最大提取URL数量 + + Returns: + Document列表 + """ + # 去重并限制数量 + seen_urls = set() + unique_results = [] + + for result in search_results: + if result.url not in seen_urls and len(unique_results) < max_urls: + seen_urls.add(result.url) + unique_results.append(result) + + logger.info(f"开始提取 {len(unique_results)} 个URL的内容") + + # 提取内容 + urls = [r.url for r in unique_results] + # 保存source信息以便后续使用 + url_to_source = {r.url: r.source for r in unique_results} + + documents = await self.jina_reader.extract_batch(urls) + + # 更新document的source信息 + for doc in documents: + if doc.url in url_to_source: + doc.source = url_to_source[doc.url] + + return documents + + async def extract_urls( + self, + urls: List[str], + source: SearchSource = SearchSource.WEB + ) -> List[Document]: + """ + 直接从URL列表提取内容 + + Args: + urls: URL列表 + source: 来源类型 + + Returns: + Document列表 + """ + return await self.jina_reader.extract_batch(urls, source) + diff --git a/agent_templates/agents/search_agent/search_agent/modules/query_analyzer.py b/agent_templates/agents/search_agent/search_agent/modules/query_analyzer.py new file mode 100644 index 0000000..22d2f65 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/modules/query_analyzer.py @@ -0,0 +1,117 @@ +""" +查询理解模块 +负责分析用户查询意图、提取关键实体、生成扩展查询 +""" + +from typing import Optional +from loguru import logger + +from config import Config +from models.schemas import QueryAnalysis, Intent +from utils.llm_client import LLMClient + + +# 查询分析Prompt +QUERY_ANALYSIS_PROMPT = """你是一个查询分析专家。分析用户的搜索查询,提取以下信息。 + +请输出JSON格式: +{ + "intent": "查询意图,必须是以下之一: fact_check(事实核查), comparison(对比分析), how_to(操作指南), news(新闻资讯), research(深度研究)", + "entities": ["关键实体列表,提取查询中的核心概念、人名、产品名等"], + "expanded_queries": ["扩展查询1", "扩展查询2", "扩展查询3"], + "need_news": true或false, + "time_filter": "时间过滤器,null表示不限时间,qdr:d(过去24小时), qdr:w(过去一周), qdr:m(过去一月), qdr:y(过去一年)" +} + +扩展查询要求: +1. 生成2-4个扩展查询,包含不同角度或同义表达 +2. 至少包含一个英文查询(如果原查询是中文) +3. 保持查询的核心意图 + +时间过滤器选择规则: +- 查询涉及"最新"、"近期"、"今年"等时效性词语 → 设置相应的时间过滤器 +- 查询涉及具体年份(如"2024年") → qdr:y +- 一般性查询 → null""" + + +class QueryAnalyzer: + """查询理解模块""" + + def __init__(self, config: Config): + """ + 初始化查询分析器 + + Args: + config: 配置对象 + """ + self.config = config + self.llm = LLMClient( + base_url=config.llm_base_url, + api_key=config.llm_api_key, + model=config.llm_model + ) + + async def analyze(self, query: str) -> QueryAnalysis: + """ + 分析用户查询 + + Args: + query: 用户查询字符串 + + Returns: + QueryAnalysis对象 + """ + logger.info(f"开始分析查询: {query}") + + try: + result = await self.llm.chat_json( + system_prompt=QUERY_ANALYSIS_PROMPT, + user_message=f"用户查询: {query}", + temperature=0.3 + ) + + # 解析意图 + intent_str = result.get("intent", "research") + intent = self._parse_intent(intent_str) + + # 构建分析结果 + analysis = QueryAnalysis( + original_query=query, + intent=intent, + entities=result.get("entities", []), + expanded_queries=result.get("expanded_queries", [query]), + need_news=result.get("need_news", False), + time_filter=result.get("time_filter") + ) + + logger.info(f"查询分析完成: intent={intent.value}, entities={analysis.entities}") + return analysis + + except Exception as e: + logger.error(f"查询分析失败: {e}") + # 返回默认分析结果 + return self._default_analysis(query) + + def _parse_intent(self, intent_str: str) -> Intent: + """解析意图字符串为枚举""" + intent_mapping = { + "fact_check": Intent.FACT_CHECK, + "comparison": Intent.COMPARISON, + "how_to": Intent.HOW_TO, + "news": Intent.NEWS, + "research": Intent.RESEARCH + } + + return intent_mapping.get(intent_str.lower(), Intent.RESEARCH) + + def _default_analysis(self, query: str) -> QueryAnalysis: + """生成默认的查询分析结果""" + return QueryAnalysis( + original_query=query, + intent=Intent.RESEARCH, + entities=[], + expanded_queries=[query], + need_news=False, + time_filter=None + ) + diff --git a/agent_templates/agents/search_agent/search_agent/modules/reflector.py b/agent_templates/agents/search_agent/search_agent/modules/reflector.py new file mode 100644 index 0000000..79f41f8 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/modules/reflector.py @@ -0,0 +1,166 @@ +""" +反思迭代模块 +评估答案质量,决定是否需要补充搜索 +""" + +from typing import List +from loguru import logger + +from config import Config +from models.schemas import Answer, QualityAssessment +from utils.llm_client import LLMClient + + +# 反思评估Prompt +REFLECTION_PROMPT = """你是一个质量评估专家。评估以下答案是否充分回答了用户的问题。 + +## 评估维度 +1. **完整性**: 答案是否覆盖了问题的所有方面? +2. **准确性**: 答案内容是否有明确的来源支持? +3. **深度**: 答案是否提供了足够的细节和解释? + +## 输出JSON格式 +{ + "completeness": 0.0-1.0, + "missing_aspects": ["如果有缺失,列出缺失的方面"], + "needs_more_search": true或false, + "suggested_queries": ["如果需要补充搜索,建议的搜索词"] +} + +## 判断标准 +- completeness >= 0.8 且没有重要信息缺失 → needs_more_search = false +- completeness < 0.8 或有重要信息缺失 → needs_more_search = true +- 建议的搜索词应该针对缺失的方面""" + + +class Reflector: + """反思迭代模块""" + + # 质量阈值 + COMPLETENESS_THRESHOLD = 0.8 + + def __init__(self, config: Config): + """ + 初始化反思器 + + Args: + config: 配置对象 + """ + self.config = config + self.llm = LLMClient( + base_url=config.llm_base_url, + api_key=config.llm_api_key, + model=config.llm_model + ) + + async def assess( + self, + query: str, + answer: Answer + ) -> QualityAssessment: + """ + 评估答案质量 + + Args: + query: 原始查询 + answer: 生成的答案 + + Returns: + QualityAssessment对象 + """ + logger.info("开始评估答案质量") + + # 如果答案置信度已经很低,直接建议补充搜索 + if answer.confidence == "low" and not answer.content: + return QualityAssessment( + completeness=0.0, + missing_aspects=["缺少相关信息"], + needs_more_search=True, + suggested_queries=[query] + ) + + user_message = f"""## 用户问题 +{query} + +## 生成的答案 +{answer.content} + +## 答案的来源数量 +{len(answer.sources)} 个来源 + +## 答案的置信度 +{answer.confidence}""" + + try: + result = await self.llm.chat_json( + system_prompt=REFLECTION_PROMPT, + user_message=user_message, + temperature=0.3 + ) + + assessment = QualityAssessment( + completeness=float(result.get("completeness", 0.5)), + missing_aspects=result.get("missing_aspects", []), + needs_more_search=result.get("needs_more_search", False), + suggested_queries=result.get("suggested_queries", []) + ) + + logger.info( + f"质量评估: completeness={assessment.completeness:.2f}, " + f"needs_more_search={assessment.needs_more_search}" + ) + + return assessment + + except Exception as e: + logger.error(f"质量评估失败: {e}") + return self._default_assessment(answer) + + def _default_assessment(self, answer: Answer) -> QualityAssessment: + """默认评估结果""" + # 根据答案置信度估计完整性 + confidence_score = { + "high": 0.9, + "medium": 0.7, + "low": 0.4 + }.get(answer.confidence, 0.5) + + return QualityAssessment( + completeness=confidence_score, + missing_aspects=[], + needs_more_search=confidence_score < self.COMPLETENESS_THRESHOLD, + suggested_queries=[] + ) + + def should_continue( + self, + assessment: QualityAssessment, + current_iteration: int + ) -> bool: + """ + 判断是否应该继续迭代 + + Args: + assessment: 质量评估结果 + current_iteration: 当前迭代次数 + + Returns: + 是否继续迭代 + """ + # 达到最大迭代次数 + if current_iteration >= self.config.max_iterations: + logger.info(f"达到最大迭代次数 ({self.config.max_iterations}),停止迭代") + return False + + # 完整性达标 + if assessment.completeness >= self.COMPLETENESS_THRESHOLD: + logger.info(f"完整性达标 ({assessment.completeness:.2f}),停止迭代") + return False + + # 没有建议的补充搜索 + if not assessment.suggested_queries: + logger.info("没有建议的补充搜索,停止迭代") + return False + + return assessment.needs_more_search + diff --git a/agent_templates/agents/search_agent/search_agent/modules/result_processor.py b/agent_templates/agents/search_agent/search_agent/modules/result_processor.py new file mode 100644 index 0000000..2ee4f39 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/modules/result_processor.py @@ -0,0 +1,107 @@ +""" +结果处理模块 +负责结果去重、相关性排序、筛选 +""" + +from typing import List +from loguru import logger + +from config import Config +from models.schemas import Document, RankedDocument +from tools.jina_reranker import JinaRerankerClient +from utils.helpers import deduplicate_by_url + + +class ResultProcessor: + """结果处理模块""" + + def __init__(self, config: Config): + """ + 初始化结果处理器 + + Args: + config: 配置对象 + """ + self.config = config + self.reranker = JinaRerankerClient( + api_key=config.jina_api_key, + timeout=config.timeout + ) + + async def process( + self, + query: str, + documents: List[Document], + top_k: int = 5 + ) -> List[RankedDocument]: + """ + 处理文档:去重 + 重排序 + 筛选 + + Args: + query: 原始查询 + documents: 文档列表 + top_k: 返回前k个结果 + + Returns: + 排序后的RankedDocument列表 + """ + if not documents: + logger.warning("没有文档需要处理") + return [] + + logger.info(f"开始处理 {len(documents)} 个文档") + + # 1. 去重 + unique_docs = self._deduplicate(documents) + logger.debug(f"去重后: {len(unique_docs)} 个文档") + + # 2. 过滤空内容 + valid_docs = [d for d in unique_docs if d.content and len(d.content.strip()) > 50] + logger.debug(f"有效文档: {len(valid_docs)} 个") + + if not valid_docs: + logger.warning("没有有效文档") + return [] + + # 3. 重排序 + ranked_docs = await self.reranker.rerank( + query=query, + documents=valid_docs, + top_k=top_k, + content_max_length=self.config.content_max_length // 5 # 使用较短内容进行排序 + ) + + logger.info(f"处理完成,返回 {len(ranked_docs)} 个排序结果") + return ranked_docs + + def _deduplicate(self, documents: List[Document]) -> List[Document]: + """去重文档""" + return deduplicate_by_url(documents, "url") + + async def process_without_rerank( + self, + documents: List[Document], + top_k: int = 5 + ) -> List[RankedDocument]: + """ + 处理文档(不进行重排序) + + Args: + documents: 文档列表 + top_k: 返回前k个结果 + + Returns: + RankedDocument列表(按原始顺序) + """ + unique_docs = self._deduplicate(documents) + valid_docs = [d for d in unique_docs if d.content and len(d.content.strip()) > 50] + + return [ + RankedDocument( + document=doc, + relevance_score=1.0 - (i * 0.1), + rank=i + 1 + ) + for i, doc in enumerate(valid_docs[:top_k]) + ] + diff --git a/agent_templates/agents/search_agent/search_agent/modules/search_executor.py b/agent_templates/agents/search_agent/search_agent/modules/search_executor.py new file mode 100644 index 0000000..27470d8 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/modules/search_executor.py @@ -0,0 +1,89 @@ +""" +搜索执行模块 +执行搜索计划,调用Serper API +""" + +import asyncio +from typing import List +from loguru import logger + +from config import Config +from models.schemas import SearchPlan, SearchTask, SearchResult +from tools.serper import SerperClient + + +class SearchExecutor: + """搜索执行模块""" + + def __init__(self, config: Config): + """ + 初始化搜索执行器 + + Args: + config: 配置对象 + """ + self.config = config + self.serper = SerperClient( + api_key=config.serper_api_key, + timeout=config.timeout + ) + + async def execute(self, plan: SearchPlan) -> List[SearchResult]: + """ + 执行搜索计划 + + Args: + plan: 搜索计划 + + Returns: + 搜索结果列表 + """ + logger.info(f"开始执行搜索计划: {len(plan.tasks)} 个任务") + + if plan.strategy == "parallel": + results = await self._execute_parallel(plan.tasks) + else: + results = await self._execute_sequential(plan.tasks) + + logger.info(f"搜索完成,共获取 {len(results)} 条结果") + return results + + async def _execute_parallel(self, tasks: List[SearchTask]) -> List[SearchResult]: + """并行执行搜索任务""" + coroutines = [self._execute_task(task) for task in tasks] + results_list = await asyncio.gather(*coroutines, return_exceptions=True) + + # 合并结果 + all_results = [] + for results in results_list: + if isinstance(results, list): + all_results.extend(results) + elif isinstance(results, Exception): + logger.warning(f"搜索任务失败: {results}") + + return all_results + + async def _execute_sequential(self, tasks: List[SearchTask]) -> List[SearchResult]: + """串行执行搜索任务""" + all_results = [] + + for task in tasks: + try: + results = await self._execute_task(task) + all_results.extend(results) + except Exception as e: + logger.warning(f"搜索任务失败: {e}") + + return all_results + + async def _execute_task(self, task: SearchTask) -> List[SearchResult]: + """执行单个搜索任务""" + logger.debug(f"执行搜索: {task.query} [{task.source.value}]") + + return await self.serper.search( + query=task.query, + source=task.source, + num_results=task.num_results, + time_filter=task.time_filter + ) + diff --git a/agent_templates/agents/search_agent/search_agent/modules/search_planner.py b/agent_templates/agents/search_agent/search_agent/modules/search_planner.py new file mode 100644 index 0000000..b70de86 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/modules/search_planner.py @@ -0,0 +1,137 @@ +""" +搜索规划模块 +根据查询分析结果制定搜索计划 +""" + +from typing import List +from loguru import logger + +from config import Config +from models.schemas import ( + QueryAnalysis, + SearchPlan, + SearchTask, + SearchSource, + Intent +) + + +class SearchPlanner: + """搜索规划模块""" + + def __init__(self, config: Config): + """ + 初始化搜索规划器 + + Args: + config: 配置对象 + """ + self.config = config + self.max_results = config.max_results_per_query + + async def plan(self, analysis: QueryAnalysis) -> SearchPlan: + """ + 根据查询分析制定搜索计划 + + Args: + analysis: 查询分析结果 + + Returns: + SearchPlan对象 + """ + logger.info(f"开始制定搜索计划: intent={analysis.intent.value}") + + tasks = [] + + # 根据意图确定搜索策略 + strategy = self._determine_strategy(analysis) + + # 构建搜索任务 + tasks.extend(self._create_web_tasks(analysis)) + + if analysis.need_news: + tasks.extend(self._create_news_tasks(analysis)) + + plan = SearchPlan( + tasks=tasks, + strategy=strategy + ) + + logger.info(f"搜索计划: {len(tasks)} 个任务, 策略={strategy}") + return plan + + def _determine_strategy(self, analysis: QueryAnalysis) -> str: + """确定执行策略""" + # 大多数情况使用并行策略 + if analysis.intent == Intent.COMPARISON: + # 对比类查询可能需要串行以获取更相关的结果 + return "parallel" + return "parallel" + + def _create_web_tasks(self, analysis: QueryAnalysis) -> List[SearchTask]: + """创建Web搜索任务""" + tasks = [] + + # 原始查询 + tasks.append(SearchTask( + query=analysis.original_query, + source=SearchSource.WEB, + time_filter=analysis.time_filter, + num_results=self.max_results + )) + + # 扩展查询(限制数量避免过多请求) + for query in analysis.expanded_queries[:2]: + if query != analysis.original_query: + tasks.append(SearchTask( + query=query, + source=SearchSource.WEB, + time_filter=analysis.time_filter, + num_results=self.max_results + )) + + return tasks + + def _create_news_tasks(self, analysis: QueryAnalysis) -> List[SearchTask]: + """创建新闻搜索任务""" + tasks = [] + + # 新闻搜索使用原始查询 + tasks.append(SearchTask( + query=analysis.original_query, + source=SearchSource.NEWS, + time_filter=analysis.time_filter or "qdr:m", # 默认过去一个月 + num_results=self.max_results + )) + + return tasks + + def plan_supplementary( + self, + original_query: str, + suggested_queries: List[str] + ) -> SearchPlan: + """ + 创建补充搜索计划 + + Args: + original_query: 原始查询 + suggested_queries: 建议的补充查询 + + Returns: + SearchPlan对象 + """ + tasks = [] + + for query in suggested_queries[:3]: # 限制补充搜索数量 + tasks.append(SearchTask( + query=query, + source=SearchSource.WEB, + num_results=self.max_results + )) + + return SearchPlan( + tasks=tasks, + strategy="parallel" + ) + diff --git a/agent_templates/agents/search_agent/search_agent/requirements.txt b/agent_templates/agents/search_agent/search_agent/requirements.txt new file mode 100644 index 0000000..0230bda --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/requirements.txt @@ -0,0 +1,19 @@ +# HTTP客户端 +aiohttp>=3.9.0 +requests>=2.31.0 + +# 环境变量 +python-dotenv>=1.0.0 + +# JSON处理 +orjson>=3.9.0 + +# 类型提示 +typing-extensions>=4.9.0 + +# 日志 +loguru>=0.7.0 + +# 异步工具 +asyncio-throttle>=1.0.2 + diff --git a/agent_templates/agents/search_agent/search_agent/search.py b/agent_templates/agents/search_agent/search_agent/search.py new file mode 100644 index 0000000..4b88ac4 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/search.py @@ -0,0 +1,16 @@ +import requests +import json + +url = "https://google.serper.dev/search" + +payload = json.dumps({ + "q": "apple inc" +}) +headers = { + 'X-API-KEY': '8253b4f240b520194065312f90e85f9be0fa205f', + 'Content-Type': 'application/json' +} + +response = requests.request("POST", url, headers=headers, data=payload) + +print(response.text) \ No newline at end of file diff --git a/agent_templates/agents/search_agent/search_agent/tools/__init__.py b/agent_templates/agents/search_agent/search_agent/tools/__init__.py new file mode 100644 index 0000000..99762ad --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/tools/__init__.py @@ -0,0 +1,14 @@ +""" +外部API工具封装模块 +""" + +from .serper import SerperClient +from .jina_reader import JinaReaderClient +from .jina_reranker import JinaRerankerClient + +__all__ = [ + "SerperClient", + "JinaReaderClient", + "JinaRerankerClient", +] + diff --git a/agent_templates/agents/search_agent/search_agent/tools/jina_reader.py b/agent_templates/agents/search_agent/search_agent/tools/jina_reader.py new file mode 100644 index 0000000..859e649 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/tools/jina_reader.py @@ -0,0 +1,180 @@ +""" +Jina Reader API封装 +提供网页内容提取功能 +""" + +import asyncio +from typing import List, Optional +import aiohttp +from loguru import logger + +from models.schemas import Document, SearchSource + + +class JinaReaderClient: + """Jina Reader API客户端""" + + BASE_URL = "https://r.jina.ai" + + def __init__( + self, + api_key: str, + timeout: int = 30, + max_concurrent: int = 5, + max_content_length: int = 5000 + ): + """ + 初始化Jina Reader客户端 + + Args: + api_key: Jina API密钥 + timeout: 请求超时时间(秒) + max_concurrent: 最大并发请求数 + max_content_length: 最大内容长度 + """ + self.api_key = api_key + self.timeout = timeout + self.max_concurrent = max_concurrent + self.max_content_length = max_content_length + self._semaphore = asyncio.Semaphore(max_concurrent) + + async def extract_content( + self, + url: str, + source: SearchSource = SearchSource.WEB + ) -> Optional[Document]: + """ + 提取单个URL的内容 + + Args: + url: 要提取的网页URL + source: 来源类型 + + Returns: + Document对象,如果提取失败则返回None + """ + reader_url = f"{self.BASE_URL}/{url}" + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Accept": "application/json" + } + + try: + async with self._semaphore: + async with aiohttp.ClientSession() as session: + async with session.get( + reader_url, + headers=headers, + timeout=aiohttp.ClientTimeout(total=self.timeout) + ) as response: + if response.status != 200: + logger.warning(f"Jina Reader提取失败 [{response.status}]: {url}") + return None + + # Jina Reader可能返回JSON或纯文本 + content_type = response.headers.get("Content-Type", "") + + if "application/json" in content_type: + result = await response.json() + # 处理嵌套的data字段 + if "data" in result: + result = result["data"] + content = result.get("content", "") + title = result.get("title", "") + else: + # 纯文本响应(Markdown格式) + content = await response.text() + # 从内容中提取标题(第一行通常是标题) + lines = content.strip().split("\n") + title = lines[0].lstrip("#").strip() if lines else "" + + # 限制内容长度 + if len(content) > self.max_content_length: + content = content[:self.max_content_length] + + logger.debug(f"提取成功: {url[:50]}... 内容长度: {len(content)}") + + return Document( + url=url, + title=title, + content=content, + source=source + ) + + except aiohttp.ClientError as e: + logger.warning(f"Jina Reader网络错误 [{url}]: {e}") + return None + except asyncio.TimeoutError: + logger.warning(f"Jina Reader超时: {url}") + return None + except Exception as e: + logger.warning(f"Jina Reader异常 [{url}]: {e}") + return None + + async def extract_batch( + self, + urls: List[str], + source: SearchSource = SearchSource.WEB + ) -> List[Document]: + """ + 批量提取多个URL的内容 + + Args: + urls: URL列表 + source: 来源类型 + + Returns: + 成功提取的Document列表 + """ + logger.info(f"批量提取 {len(urls)} 个URL的内容") + + tasks = [ + self.extract_content(url, source) + for url in urls + ] + + results = await asyncio.gather(*tasks, return_exceptions=True) + + # 过滤掉失败的结果 + documents = [] + for result in results: + if isinstance(result, Document): + documents.append(result) + elif isinstance(result, Exception): + logger.warning(f"提取异常: {result}") + + logger.info(f"成功提取 {len(documents)}/{len(urls)} 个文档") + return documents + + async def extract_with_retry( + self, + url: str, + source: SearchSource = SearchSource.WEB, + max_retries: int = 2, + retry_delay: float = 1.0 + ) -> Optional[Document]: + """ + 带重试的内容提取 + + Args: + url: 要提取的网页URL + source: 来源类型 + max_retries: 最大重试次数 + retry_delay: 重试延迟(秒) + + Returns: + Document对象,如果最终失败则返回None + """ + for attempt in range(max_retries + 1): + result = await self.extract_content(url, source) + + if result is not None: + return result + + if attempt < max_retries: + logger.debug(f"重试提取 [{attempt + 1}/{max_retries}]: {url}") + await asyncio.sleep(retry_delay) + + return None + diff --git a/agent_templates/agents/search_agent/search_agent/tools/jina_reranker.py b/agent_templates/agents/search_agent/search_agent/tools/jina_reranker.py new file mode 100644 index 0000000..3da52d6 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/tools/jina_reranker.py @@ -0,0 +1,191 @@ +""" +Jina Reranker API封装 +提供搜索结果重排序功能 +""" + +from typing import List, Tuple +import aiohttp +from loguru import logger + +from models.schemas import Document, RankedDocument + + +class JinaRerankerClient: + """Jina Reranker API客户端""" + + BASE_URL = "https://api.jina.ai/v1/rerank" + MODEL = "jina-reranker-v2-base-multilingual" + + def __init__(self, api_key: str, timeout: int = 30): + """ + 初始化Jina Reranker客户端 + + Args: + api_key: Jina API密钥 + timeout: 请求超时时间(秒) + """ + self.api_key = api_key + self.timeout = timeout + + async def rerank( + self, + query: str, + documents: List[Document], + top_k: int = 5, + content_max_length: int = 1000 + ) -> List[RankedDocument]: + """ + 对文档进行相关性重排序 + + Args: + query: 查询字符串 + documents: 文档列表 + top_k: 返回前k个结果 + content_max_length: 用于排序的内容最大长度 + + Returns: + 排序后的RankedDocument列表 + """ + if not documents: + return [] + + # 准备文档内容(截断到合适长度) + doc_contents = [ + doc.content[:content_max_length] if doc.content else doc.title + for doc in documents + ] + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + payload = { + "model": self.MODEL, + "query": query, + "documents": doc_contents, + "top_n": min(top_k, len(documents)) + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post( + self.BASE_URL, + headers=headers, + json=payload, + timeout=aiohttp.ClientTimeout(total=self.timeout) + ) as response: + if response.status != 200: + error_text = await response.text() + logger.error(f"Jina Reranker API错误: {response.status} - {error_text}") + # 如果重排序失败,返回原始顺序 + return self._fallback_ranking(documents, top_k) + + result = await response.json() + return self._parse_rerank_results(documents, result, top_k) + + except aiohttp.ClientError as e: + logger.error(f"Jina Reranker网络错误: {e}") + return self._fallback_ranking(documents, top_k) + except Exception as e: + logger.error(f"Jina Reranker异常: {e}") + return self._fallback_ranking(documents, top_k) + + def _parse_rerank_results( + self, + documents: List[Document], + response: dict, + top_k: int + ) -> List[RankedDocument]: + """解析重排序结果""" + results = [] + + reranked = response.get("results", []) + + for rank, item in enumerate(reranked[:top_k], 1): + index = item.get("index", 0) + score = item.get("relevance_score", 0.0) + + if 0 <= index < len(documents): + ranked_doc = RankedDocument( + document=documents[index], + relevance_score=score, + rank=rank + ) + results.append(ranked_doc) + + logger.debug(f"重排序返回 {len(results)} 个结果") + return results + + def _fallback_ranking( + self, + documents: List[Document], + top_k: int + ) -> List[RankedDocument]: + """后备排序:保持原始顺序""" + logger.warning("使用后备排序(原始顺序)") + + return [ + RankedDocument( + document=doc, + relevance_score=1.0 - (i * 0.1), # 模拟递减分数 + rank=i + 1 + ) + for i, doc in enumerate(documents[:top_k]) + ] + + async def rerank_texts( + self, + query: str, + texts: List[str], + top_k: int = 5 + ) -> List[Tuple[int, float]]: + """ + 对纯文本列表进行重排序 + + Args: + query: 查询字符串 + texts: 文本列表 + top_k: 返回前k个结果 + + Returns: + (原始索引, 相关性分数) 的列表 + """ + if not texts: + return [] + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + payload = { + "model": self.MODEL, + "query": query, + "documents": texts, + "top_n": min(top_k, len(texts)) + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post( + self.BASE_URL, + headers=headers, + json=payload, + timeout=aiohttp.ClientTimeout(total=self.timeout) + ) as response: + if response.status != 200: + logger.error(f"Reranker API错误: {response.status}") + return [(i, 1.0 - i * 0.1) for i in range(min(top_k, len(texts)))] + + result = await response.json() + + return [ + (item["index"], item["relevance_score"]) + for item in result.get("results", [])[:top_k] + ] + + except Exception as e: + logger.error(f"Reranker异常: {e}") + return [(i, 1.0 - i * 0.1) for i in range(min(top_k, len(texts)))] + diff --git a/agent_templates/agents/search_agent/search_agent/tools/serper.py b/agent_templates/agents/search_agent/search_agent/tools/serper.py new file mode 100644 index 0000000..4e9b673 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/tools/serper.py @@ -0,0 +1,212 @@ +""" +Serper API封装 +提供Google搜索和新闻搜索功能 +""" + +from typing import List, Optional, Dict, Any +import aiohttp +from loguru import logger + +from models.schemas import SearchResult, SearchSource + + +class SerperClient: + """Serper API客户端""" + + BASE_URL = "https://google.serper.dev" + + ENDPOINTS = { + "web": "/search", + "news": "/news" + } + + def __init__(self, api_key: str, timeout: int = 30): + """ + 初始化Serper客户端 + + Args: + api_key: Serper API密钥 + timeout: 请求超时时间(秒) + """ + self.api_key = api_key + self.timeout = timeout + + async def _request( + self, + endpoint: str, + payload: Dict[str, Any] + ) -> Dict[str, Any]: + """ + 发送请求到Serper API + + Args: + endpoint: API端点 + payload: 请求体 + + Returns: + API响应 + """ + url = f"{self.BASE_URL}{endpoint}" + + headers = { + "X-API-KEY": self.api_key, + "Content-Type": "application/json" + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post( + url, + headers=headers, + json=payload, + timeout=aiohttp.ClientTimeout(total=self.timeout) + ) as response: + if response.status != 200: + error_text = await response.text() + logger.error(f"Serper API错误: {response.status} - {error_text}") + raise Exception(f"Serper API请求失败: {response.status}") + + return await response.json() + + except aiohttp.ClientError as e: + logger.error(f"Serper请求网络错误: {e}") + raise + + async def search_web( + self, + query: str, + num_results: int = 10, + gl: str = "cn", + hl: str = "zh-cn", + time_filter: Optional[str] = None + ) -> List[SearchResult]: + """ + 执行Web搜索 + + Args: + query: 搜索查询 + num_results: 返回结果数量 + gl: 地区代码 + hl: 语言代码 + time_filter: 时间过滤器 (qdr:d/qdr:w/qdr:m/qdr:y) + + Returns: + 搜索结果列表 + """ + payload = { + "q": query, + "num": num_results, + "gl": gl, + "hl": hl + } + + if time_filter: + payload["tbs"] = time_filter + + logger.info(f"执行Web搜索: {query}") + + result = await self._request(self.ENDPOINTS["web"], payload) + + return self._parse_web_results(result) + + async def search_news( + self, + query: str, + num_results: int = 10, + gl: str = "cn", + hl: str = "zh-cn", + time_filter: Optional[str] = None + ) -> List[SearchResult]: + """ + 执行新闻搜索 + + Args: + query: 搜索查询 + num_results: 返回结果数量 + gl: 地区代码 + hl: 语言代码 + time_filter: 时间过滤器 + + Returns: + 搜索结果列表 + """ + payload = { + "q": query, + "num": num_results, + "gl": gl, + "hl": hl + } + + if time_filter: + payload["tbs"] = time_filter + + logger.info(f"执行新闻搜索: {query}") + + result = await self._request(self.ENDPOINTS["news"], payload) + + return self._parse_news_results(result) + + def _parse_web_results(self, response: Dict[str, Any]) -> List[SearchResult]: + """解析Web搜索结果""" + results = [] + + organic = response.get("organic", []) + + for item in organic: + result = SearchResult( + title=item.get("title", ""), + url=item.get("link", ""), + snippet=item.get("snippet", ""), + source=SearchSource.WEB, + position=item.get("position", 0), + date=None + ) + results.append(result) + + logger.debug(f"Web搜索返回 {len(results)} 条结果") + return results + + def _parse_news_results(self, response: Dict[str, Any]) -> List[SearchResult]: + """解析新闻搜索结果""" + results = [] + + news = response.get("news", []) + + for i, item in enumerate(news, 1): + result = SearchResult( + title=item.get("title", ""), + url=item.get("link", ""), + snippet=item.get("snippet", ""), + source=SearchSource.NEWS, + position=i, + date=item.get("date") + ) + results.append(result) + + logger.debug(f"新闻搜索返回 {len(results)} 条结果") + return results + + async def search( + self, + query: str, + source: SearchSource, + num_results: int = 10, + time_filter: Optional[str] = None + ) -> List[SearchResult]: + """ + 统一搜索接口 + + Args: + query: 搜索查询 + source: 搜索来源类型 + num_results: 返回结果数量 + time_filter: 时间过滤器 + + Returns: + 搜索结果列表 + """ + if source == SearchSource.NEWS: + return await self.search_news(query, num_results, time_filter=time_filter) + else: + return await self.search_web(query, num_results, time_filter=time_filter) + diff --git a/agent_templates/agents/search_agent/search_agent/utils/__init__.py b/agent_templates/agents/search_agent/search_agent/utils/__init__.py new file mode 100644 index 0000000..d0b6a2b --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/utils/__init__.py @@ -0,0 +1,22 @@ +""" +工具函数模块 +""" + +from .llm_client import LLMClient +from .helpers import ( + flatten, + deduplicate_by_url, + truncate_text, + extract_json_from_text, + format_documents_for_prompt, +) + +__all__ = [ + "LLMClient", + "flatten", + "deduplicate_by_url", + "truncate_text", + "extract_json_from_text", + "format_documents_for_prompt", +] + diff --git a/agent_templates/agents/search_agent/search_agent/utils/helpers.py b/agent_templates/agents/search_agent/search_agent/utils/helpers.py new file mode 100644 index 0000000..cf94020 --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/utils/helpers.py @@ -0,0 +1,197 @@ +""" +通用工具函数 +""" + +import re +import json +from typing import List, TypeVar, Optional, Dict, Any + +T = TypeVar('T') + + +def flatten(nested_list: List[List[T]]) -> List[T]: + """ + 将嵌套列表展平为一维列表 + + Args: + nested_list: 嵌套列表 + + Returns: + 展平后的一维列表 + """ + return [item for sublist in nested_list for item in sublist] + + +def deduplicate_by_url(items: List[Any], url_attr: str = "url") -> List[Any]: + """ + 根据URL去重 + + Args: + items: 包含URL属性的对象列表 + url_attr: URL属性名 + + Returns: + 去重后的列表 + """ + seen_urls = set() + unique_items = [] + + for item in items: + url = getattr(item, url_attr, None) or item.get(url_attr) + if url and url not in seen_urls: + seen_urls.add(url) + unique_items.append(item) + + return unique_items + + +def truncate_text(text: str, max_length: int, suffix: str = "...") -> str: + """ + 截断文本到指定长度 + + Args: + text: 原始文本 + max_length: 最大长度 + suffix: 截断后缀 + + Returns: + 截断后的文本 + """ + if len(text) <= max_length: + return text + + return text[:max_length - len(suffix)] + suffix + + +def extract_json_from_text(text: str) -> Optional[Dict[str, Any]]: + """ + 从文本中提取JSON对象 + + Args: + text: 可能包含JSON的文本 + + Returns: + 提取的JSON字典,如果提取失败则返回None + """ + # 尝试直接解析 + try: + return json.loads(text) + except json.JSONDecodeError: + pass + + # 尝试提取```json ... ```块 + json_block_pattern = r'```(?:json)?\s*([\s\S]*?)```' + matches = re.findall(json_block_pattern, text) + + for match in matches: + try: + return json.loads(match.strip()) + except json.JSONDecodeError: + continue + + # 尝试提取{ ... }块 + brace_pattern = r'\{[\s\S]*\}' + matches = re.findall(brace_pattern, text) + + for match in matches: + try: + return json.loads(match) + except json.JSONDecodeError: + continue + + return None + + +def format_documents_for_prompt(documents: List[Any], max_length: int = 2000) -> str: + """ + 格式化文档列表为Prompt中使用的文本 + + Args: + documents: 文档列表(RankedDocument或Document对象) + max_length: 每个文档的最大内容长度 + + Returns: + 格式化后的文本 + """ + formatted_parts = [] + + for i, doc in enumerate(documents, 1): + # 支持RankedDocument和Document两种类型 + if hasattr(doc, 'document'): + # RankedDocument + actual_doc = doc.document + score = f" (相关性: {doc.relevance_score:.2f})" + else: + # Document + actual_doc = doc + score = "" + + content = truncate_text(actual_doc.content, max_length) + + part = f"""### 来源 [{i}]{score} +**标题**: {actual_doc.title} +**URL**: {actual_doc.url} +**内容**: +{content} +""" + formatted_parts.append(part) + + return "\n---\n".join(formatted_parts) + + +def clean_url(url: str) -> str: + """ + 清理和标准化URL + + Args: + url: 原始URL + + Returns: + 清理后的URL + """ + # 移除末尾的斜杠 + url = url.rstrip("/") + + # 移除锚点 + if "#" in url: + url = url.split("#")[0] + + return url + + +def is_valid_url(url: str) -> bool: + """ + 验证URL是否有效 + + Args: + url: URL字符串 + + Returns: + 是否有效 + """ + url_pattern = re.compile( + r'^https?://' # http:// or https:// + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' # domain + r'localhost|' # localhost + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # IP + r'(?::\d+)?' # optional port + r'(?:/?|[/?]\S+)$', re.IGNORECASE) + + return bool(url_pattern.match(url)) + + +def merge_dicts(base: Dict, override: Dict) -> Dict: + """ + 合并两个字典,override中的值会覆盖base中的值 + + Args: + base: 基础字典 + override: 覆盖字典 + + Returns: + 合并后的字典 + """ + result = base.copy() + result.update(override) + return result + diff --git a/agent_templates/agents/search_agent/search_agent/utils/llm_client.py b/agent_templates/agents/search_agent/search_agent/utils/llm_client.py new file mode 100644 index 0000000..f033e2e --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent/utils/llm_client.py @@ -0,0 +1,159 @@ +""" +LLM客户端模块 +封装与xchat52 LLM的交互(支持Azure OpenAI风格API) +""" + +import json +from typing import Optional, List, Dict, Any +import aiohttp +from loguru import logger + + +class LLMClient: + """LLM客户端,用于与xchat52 API交互""" + + # API版本 + API_VERSION = "2024-10-21" + + def __init__( + self, + base_url: str, + api_key: str, + model: str = "xchat52", + timeout: int = 60 + ): + self.base_url = base_url.rstrip("/") + self.api_key = api_key + self.model = model + self.timeout = timeout + + async def chat( + self, + messages: List[Dict[str, str]], + temperature: float = 0.7, + max_tokens: int = 4096, + response_format: Optional[Dict[str, str]] = None + ) -> str: + """ + 发送聊天请求到LLM + + Args: + messages: 消息列表,格式 [{"role": "user", "content": "..."}] + temperature: 温度参数 + max_tokens: 最大token数 + response_format: 响应格式(如 {"type": "json_object"}) + + Returns: + LLM的响应文本 + """ + # Azure OpenAI 风格的URL + url = f"{self.base_url}/chat/completions?api-version={self.API_VERSION}" + + # Azure OpenAI 使用 api-key 头 + headers = { + "api-key": self.api_key, + "Content-Type": "application/json" + } + + payload = { + "model": self.model, + "messages": messages, + "temperature": temperature, + "max_completion_tokens": max_tokens # 新版API使用 max_completion_tokens + } + + if response_format: + payload["response_format"] = response_format + + try: + async with aiohttp.ClientSession() as session: + async with session.post( + url, + headers=headers, + json=payload, + timeout=aiohttp.ClientTimeout(total=self.timeout) + ) as response: + if response.status != 200: + error_text = await response.text() + logger.error(f"LLM API错误: {response.status} - {error_text}") + raise Exception(f"LLM API请求失败: {response.status}") + + result = await response.json() + return result["choices"][0]["message"]["content"] + + except aiohttp.ClientError as e: + logger.error(f"LLM请求网络错误: {e}") + raise + except Exception as e: + logger.error(f"LLM请求异常: {e}") + raise + + async def chat_with_system( + self, + system_prompt: str, + user_message: str, + temperature: float = 0.7, + max_tokens: int = 4096, + response_format: Optional[Dict[str, str]] = None + ) -> str: + """ + 使用系统提示和用户消息进行对话 + + Args: + system_prompt: 系统提示 + user_message: 用户消息 + temperature: 温度参数 + max_tokens: 最大token数 + response_format: 响应格式 + + Returns: + LLM的响应文本 + """ + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_message} + ] + + return await self.chat( + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + response_format=response_format + ) + + async def chat_json( + self, + system_prompt: str, + user_message: str, + temperature: float = 0.3 + ) -> Dict[str, Any]: + """ + 请求JSON格式的响应 + + Args: + system_prompt: 系统提示 + user_message: 用户消息 + temperature: 温度参数(JSON响应建议使用较低温度) + + Returns: + 解析后的JSON字典 + """ + from .helpers import extract_json_from_text + + response = await self.chat_with_system( + system_prompt=system_prompt, + user_message=user_message, + temperature=temperature, + response_format={"type": "json_object"} + ) + + try: + return json.loads(response) + except json.JSONDecodeError: + # 尝试从文本中提取JSON + extracted = extract_json_from_text(response) + if extracted: + return extracted + logger.error(f"无法解析LLM响应为JSON: {response[:200]}") + raise ValueError("LLM响应不是有效的JSON格式") + diff --git a/agent_templates/agents/search_agent/search_agent_main.py b/agent_templates/agents/search_agent/search_agent_main.py new file mode 100644 index 0000000..f4c1a9d --- /dev/null +++ b/agent_templates/agents/search_agent/search_agent_main.py @@ -0,0 +1,312 @@ +""" +智能搜索 AI Agent - FastAPI版本 +通过HTTP API接收搜索请求,提供智能搜索功能 +""" +import os +import sys +import logging +from typing import Optional, Dict, Any, List +from datetime import datetime +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field +import uvicorn +import asyncio + +# 添加search_agent目录到Python路径 +search_agent_dir = os.path.join(os.path.dirname(__file__), 'search_agent') +if search_agent_dir not in sys.path: + sys.path.insert(0, search_agent_dir) + +# 直接导入,避免与文件名冲突 +from config import Config +from agent.search_agent import SearchAgent +from agent_callback_utils import AgentCallbackHandler, CallbackContextManager + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 环境变量配置 +SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0") +SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080")) +POD_NAME = os.getenv("POD_NAME", "search-agent") +TEMPLATE_TYPE = os.getenv("TEMPLATE_TYPE", "search_agent") + +# 全局搜索Agent和回调处理器 +search_agent: Optional[SearchAgent] = None +config: Optional[Config] = None +callback_handler: Optional[AgentCallbackHandler] = None + +# FastAPI应用 +app = FastAPI( + title="Intelligent Search AI Agent", + description="智能搜索代理", + version="1.0.0" +) + + +# ==================== 请求/响应模型 ==================== + +class ConfigRequest(BaseModel): + """配置请求(其他配置从环境变量获取)""" + llm_base_url: str = Field(..., description="LLM API基础URL") + llm_model: str = Field(default="xchat52", description="LLM模型名称") + serper_api_key: str = Field(..., description="Serper API密钥") + jina_api_key: str = Field(..., description="Jina API密钥") + max_iterations: int = Field(default=3, description="最大迭代次数") + max_results_per_query: int = Field(default=10, description="每次搜索最大结果数") + content_max_length: int = Field(default=5000, description="内容最大长度") + log_level: str = Field(default="INFO", description="日志级别") + timeout: int = Field(default=30, description="超时时间(秒)") + + +class SearchRequest(BaseModel): + """搜索请求""" + query: str = Field(..., description="搜索查询") + llm_api_key: str = Field(..., description="LLM API密钥") + user_id: Optional[str] = Field(None, description="用户ID(用于计费回调)") + auto_configure: bool = Field(default=False, description="是否自动从环境变量配置") + + +class Source(BaseModel): + """搜索来源""" + index: int + title: str + url: str + + +class SearchResponse(BaseModel): + """搜索响应""" + query: str + answer: str + sources: List[Source] + confidence: str + iterations: int + total_sources: int + search_queries: List[str] + timestamp: str + + +class StatusResponse(BaseModel): + """状态响应""" + status: str + pod_name: str + template_type: str + configured: bool + timestamp: str + + +class ErrorResponse(BaseModel): + """错误响应""" + error: str + detail: Optional[str] = None + + +# ==================== Agent操作函数 ==================== + +def initialize_agent_from_env(): + """从环境变量初始化Agent""" + global search_agent, config + + try: + config = Config.from_env() + config.validate() + search_agent = SearchAgent(config) + logger.info("Search Agent从环境变量初始化成功") + return True + except Exception as e: + logger.error(f"从环境变量初始化Agent失败: {str(e)}") + return False + + +def initialize_agent_from_config(config_data: Dict[str, Any]): + """从配置数据初始化Agent""" + global search_agent, config + + try: + # 创建配置对象 + config = Config( + llm_base_url=config_data.get("llm_base_url", ""), + llm_api_key=config_data.get("llm_api_key", ""), + llm_model=config_data.get("llm_model", "xchat52"), + serper_api_key=config_data.get("serper_api_key", ""), + jina_api_key=config_data.get("jina_api_key", ""), + max_iterations=config_data.get("max_iterations", 3), + max_results_per_query=config_data.get("max_results_per_query", 10), + content_max_length=config_data.get("content_max_length", 5000), + log_level=config_data.get("log_level", "INFO"), + timeout=config_data.get("timeout", 30) + ) + + config.validate() + search_agent = SearchAgent(config) + logger.info("Search Agent从配置初始化成功") + return True + except Exception as e: + logger.error(f"从配置初始化Agent失败: {str(e)}") + raise + + +# ==================== API端点 ==================== + +@app.get("/health") +async def health_check(): + """健康检查""" + return { + "status": "healthy", + "pod_name": POD_NAME, + "template_type": TEMPLATE_TYPE, + "configured": search_agent is not None, + "timestamp": datetime.utcnow().isoformat() + } + + +@app.get("/status", response_model=StatusResponse) +async def get_status(): + """获取状态""" + return StatusResponse( + status="running" if search_agent else "not_configured", + pod_name=POD_NAME, + template_type=TEMPLATE_TYPE, + configured=search_agent is not None, + timestamp=datetime.utcnow().isoformat() + ) + + +@app.post("/configure") +async def configure_agent(config_req: ConfigRequest): + """配置Agent""" + try: + initialize_agent_from_config(config_req.dict()) + return { + "status": "success", + "message": "Agent配置成功", + "timestamp": datetime.utcnow().isoformat() + } + except Exception as e: + logger.error(f"配置Agent失败: {str(e)}") + raise HTTPException(status_code=400, detail=f"配置失败: {str(e)}") + + +@app.post("/search", response_model=SearchResponse) +async def search(request: SearchRequest): + """执行搜索""" + global search_agent, callback_handler, config + + # 如果未配置且需要自动配置 + if not search_agent and request.auto_configure: + if not initialize_agent_from_env(): + raise HTTPException( + status_code=400, + detail="Agent未配置且自动配置失败,请先调用/configure接口" + ) + + if not search_agent: + raise HTTPException( + status_code=400, + detail="Agent未配置,请先调用/configure接口" + ) + + # 初始化回调处理器(如果尚未初始化) + if not callback_handler: + callback_handler = AgentCallbackHandler() + + # 使用上下文管理器自动处理回调 + try: + with CallbackContextManager( + handler=callback_handler, + user_id=request.user_id, + request_id=f"search-{int(datetime.utcnow().timestamp())}" + ) as ctx: + # 临时更新API key + original_api_key = config.llm_api_key if config else None + if config: + config.llm_api_key = request.llm_api_key + search_agent.config.llm_api_key = request.llm_api_key + + try: + # 执行搜索 + ctx.add_tool("web_search") + ctx.add_tool("content_reader") + result = await search_agent.search(request.query) + + # 转换响应 + sources = [ + Source( + index=s.index, + title=s.title, + url=s.url + ) + for s in result.answer.sources + ] + + return SearchResponse( + query=request.query, + answer=result.answer.content, + sources=sources, + confidence=result.answer.confidence, + iterations=result.iterations, + total_sources=result.total_sources_consulted, + search_queries=result.search_queries_used, + timestamp=datetime.utcnow().isoformat() + ) + finally: + # 恢复原始API key + if config and original_api_key: + config.llm_api_key = original_api_key + search_agent.config.llm_api_key = original_api_key + except Exception as e: + logger.error(f"搜索失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"搜索失败: {str(e)}") + + +@app.post("/chat") +async def chat(request: SearchRequest): + """聊天接口(别名)""" + return await search(request) + + +@app.get("/") +async def root(): + """根路径""" + return { + "name": "Intelligent Search AI Agent", + "version": "1.0.0", + "endpoints": { + "health": "/health", + "status": "/status", + "configure": "/configure", + "search": "/search", + "chat": "/chat" + } + } + + +# ==================== 启动函数 ==================== + +def main(): + """主函数""" + logger.info(f"启动 Search Agent - {POD_NAME}") + logger.info(f"Template Type: {TEMPLATE_TYPE}") + + # 尝试从环境变量初始化 + if os.getenv("LLM_API_KEY"): + logger.info("检测到环境变量配置,尝试自动初始化...") + initialize_agent_from_env() + else: + logger.info("未检测到环境变量配置,等待通过API配置...") + + # 启动服务 + uvicorn.run( + app, + host=SERVICE_HOST, + port=SERVICE_PORT, + log_level="info" + ) + + +if __name__ == "__main__": + main() diff --git a/agent_templates/aks_agent b/agent_templates/aks_agent new file mode 160000 index 0000000..b45aa74 --- /dev/null +++ b/agent_templates/aks_agent @@ -0,0 +1 @@ +Subproject commit b45aa748eea05f28d6457980011d903f7abaf6de diff --git a/agent_templates/azure_blob_agent_mcp.py b/agent_templates/azure_blob_agent_mcp.py deleted file mode 100644 index 06b98fa..0000000 --- a/agent_templates/azure_blob_agent_mcp.py +++ /dev/null @@ -1,622 +0,0 @@ -""" -Azure Blob Storage AI Agent - MCP (Model Context Protocol) 版本 -使用 MCP 协议实现智能文件操作功能 -""" -import os -import logging -import json -from typing import Optional, Dict, Any, List -from datetime import datetime -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel, Field -from azure.storage.blob import BlobServiceClient, ContainerClient -import uvicorn -import asyncio - -# 配置日志 -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -# 环境变量配置 -SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0") -SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080")) -POD_NAME = os.getenv("POD_NAME", "azure-blob-agent-mcp") -TEMPLATE_TYPE = os.getenv("TEMPLATE_TYPE", "azure_blob_agent_mcp") -AGENT_FRAMEWORK = os.getenv("AGENT_FRAMEWORK", "mcp") - -# 工具配置 (从环境变量传入的 JSON) -TOOLS_CONFIG = json.loads(os.getenv("TOOLS_CONFIG", "{}")) -TOOL_ENDPOINT = os.getenv("TOOL_ENDPOINT", "") -TOOL_API_KEY = os.getenv("TOOL_API_KEY", "") - -# 模型配置 -MODEL_PROVIDER = os.getenv("MODEL_PROVIDER", "openai") -MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4") -MODEL_API_KEY = os.getenv("MODEL_API_KEY", "") -MODEL_ENDPOINT = os.getenv("MODEL_ENDPOINT", "https://api.openai.com/v1") - -# 存储配置 -AZURE_STORAGE_CONNECTION_STRING = os.getenv("AZURE_STORAGE_CONNECTION_STRING", "") -STORAGE_ACCOUNT_NAME = os.getenv("STORAGE_ACCOUNT_NAME", "") - -# 用户标识 -USER_ID = os.getenv("USER_ID", "") -TENANT_ID = os.getenv("TENANT_ID", "") -NAMESPACE = os.getenv("NAMESPACE", "ai-agents") - -# 全局存储客户端 -blob_service_client: Optional[BlobServiceClient] = None -connection_string: Optional[str] = None - -# MCP 工具注册表 -mcp_tools: Dict[str, Any] = {} - -# FastAPI应用 -app = FastAPI( - title="Azure Blob Storage AI Agent (MCP)", - description="基于 MCP 协议的智能 Azure Blob 存储管理代理", - version="1.0.0" -) - - -# ==================== 请求/响应模型 ==================== - -class ConnectRequest(BaseModel): - """连接请求""" - connection_string: str = Field(..., description="Azure Storage连接字符串") - - -class MCPToolRequest(BaseModel): - """MCP 工具调用请求""" - tool_name: str = Field(..., description="工具名称") - parameters: Dict[str, Any] = Field(default_factory=dict, description="工具参数") - - -class MCPQueryRequest(BaseModel): - """MCP 查询请求""" - query: str = Field(..., description="自然语言查询或操作指令") - container_name: Optional[str] = Field(None, description="指定容器名称") - context: Optional[Dict] = Field(default_factory=dict, description="上下文信息") - - -class HealthResponse(BaseModel): - """健康检查响应""" - status: str - connected: bool - framework: str - user_id: Optional[str] = None - namespace: Optional[str] = None - connection_info: Optional[Dict] = None - - -# ==================== MCP 工具定义 ==================== - -class MCPTool: - """MCP 工具基类""" - - def __init__(self, name: str, description: str, parameters_schema: Dict): - self.name = name - self.description = description - self.parameters_schema = parameters_schema - - async def execute(self, parameters: Dict[str, Any]) -> Dict[str, Any]: - """执行工具""" - raise NotImplementedError - - def to_mcp_spec(self) -> Dict: - """转换为 MCP 工具规范""" - return { - "name": self.name, - "description": self.description, - "inputSchema": { - "type": "object", - "properties": self.parameters_schema, - "required": list(self.parameters_schema.keys()) - } - } - - -class ListContainersTool(MCPTool): - """列出所有容器工具""" - - def __init__(self): - super().__init__( - name="list_containers", - description="列出 Azure Blob Storage 中的所有容器", - parameters_schema={} - ) - - async def execute(self, parameters: Dict[str, Any]) -> Dict[str, Any]: - global blob_service_client - - if not blob_service_client: - return {"error": "未连接到 Azure Blob Storage"} - - try: - containers = blob_service_client.list_containers() - container_list = [] - for container in containers: - container_list.append({ - "name": container.name, - "last_modified": str(container.last_modified) - }) - - return { - "success": True, - "containers": container_list, - "count": len(container_list) - } - except Exception as e: - logger.error(f"列出容器失败: {str(e)}") - return {"error": str(e)} - - -class ListBlobsTool(MCPTool): - """列出容器中的 blob 工具""" - - def __init__(self): - super().__init__( - name="list_blobs", - description="列出指定容器中的所有文件", - parameters_schema={ - "container_name": { - "type": "string", - "description": "容器名称" - } - } - ) - - async def execute(self, parameters: Dict[str, Any]) -> Dict[str, Any]: - global blob_service_client - - if not blob_service_client: - return {"error": "未连接到 Azure Blob Storage"} - - container_name = parameters.get("container_name") - if not container_name: - return {"error": "缺少参数: container_name"} - - try: - container_client = blob_service_client.get_container_client(container_name) - blobs = container_client.list_blobs() - - blob_list = [] - total_size = 0 - for blob in blobs: - blob_info = { - "name": blob.name, - "size": blob.size, - "size_mb": round(blob.size / (1024 * 1024), 2), - "content_type": blob.content_settings.content_type if blob.content_settings else "unknown", - "last_modified": str(blob.last_modified) - } - blob_list.append(blob_info) - total_size += blob.size - - return { - "success": True, - "container": container_name, - "blobs": blob_list, - "count": len(blob_list), - "total_size_mb": round(total_size / (1024 * 1024), 2) - } - except Exception as e: - logger.error(f"列出 blob 失败: {str(e)}") - return {"error": str(e)} - - -class GetBlobInfoTool(MCPTool): - """获取 blob 信息工具""" - - def __init__(self): - super().__init__( - name="get_blob_info", - description="获取特定文件的详细信息", - parameters_schema={ - "container_name": { - "type": "string", - "description": "容器名称" - }, - "blob_name": { - "type": "string", - "description": "文件名称" - } - } - ) - - async def execute(self, parameters: Dict[str, Any]) -> Dict[str, Any]: - global blob_service_client - - if not blob_service_client: - return {"error": "未连接到 Azure Blob Storage"} - - container_name = parameters.get("container_name") - blob_name = parameters.get("blob_name") - - if not container_name or not blob_name: - return {"error": "缺少参数: container_name 或 blob_name"} - - try: - blob_client = blob_service_client.get_blob_client(container_name, blob_name) - properties = blob_client.get_blob_properties() - - return { - "success": True, - "blob_name": blob_name, - "container": container_name, - "size": properties.size, - "size_mb": round(properties.size / (1024 * 1024), 2), - "content_type": properties.content_settings.content_type if properties.content_settings else "unknown", - "creation_time": str(properties.creation_time), - "last_modified": str(properties.last_modified), - "etag": properties.etag, - "metadata": properties.metadata if properties.metadata else {} - } - except Exception as e: - logger.error(f"获取 blob 信息失败: {str(e)}") - return {"error": str(e)} - - -class SearchBlobsTool(MCPTool): - """搜索 blob 工具""" - - def __init__(self): - super().__init__( - name="search_blobs", - description="在容器中搜索包含关键字的文件", - parameters_schema={ - "container_name": { - "type": "string", - "description": "容器名称" - }, - "keyword": { - "type": "string", - "description": "搜索关键字" - } - } - ) - - async def execute(self, parameters: Dict[str, Any]) -> Dict[str, Any]: - global blob_service_client - - if not blob_service_client: - return {"error": "未连接到 Azure Blob Storage"} - - container_name = parameters.get("container_name") - keyword = parameters.get("keyword") - - if not container_name or not keyword: - return {"error": "缺少参数: container_name 或 keyword"} - - try: - container_client = blob_service_client.get_container_client(container_name) - blobs = container_client.list_blobs() - - matched_blobs = [] - for blob in blobs: - if keyword.lower() in blob.name.lower(): - matched_blobs.append({ - "name": blob.name, - "size": blob.size, - "size_kb": round(blob.size / 1024, 2), - "last_modified": str(blob.last_modified) - }) - - return { - "success": True, - "container": container_name, - "keyword": keyword, - "results": matched_blobs, - "count": len(matched_blobs) - } - except Exception as e: - logger.error(f"搜索 blob 失败: {str(e)}") - return {"error": str(e)} - - -class GetStorageStatsTool(MCPTool): - """获取存储统计工具""" - - def __init__(self): - super().__init__( - name="get_storage_stats", - description="获取存储的统计信息,包括容器数量、文件数量、总大小等", - parameters_schema={} - ) - - async def execute(self, parameters: Dict[str, Any]) -> Dict[str, Any]: - global blob_service_client - - if not blob_service_client: - return {"error": "未连接到 Azure Blob Storage"} - - try: - containers = list(blob_service_client.list_containers()) - total_containers = len(containers) - total_blobs = 0 - total_size = 0 - - container_stats = [] - for container in containers: - container_client = blob_service_client.get_container_client(container.name) - blobs = list(container_client.list_blobs()) - blob_count = len(blobs) - container_size = sum(blob.size for blob in blobs) - - total_blobs += blob_count - total_size += container_size - - container_stats.append({ - "name": container.name, - "blobs": blob_count, - "size_mb": round(container_size / (1024 * 1024), 2) - }) - - return { - "success": True, - "total_containers": total_containers, - "total_blobs": total_blobs, - "total_size_mb": round(total_size / (1024 * 1024), 2), - "container_stats": container_stats - } - except Exception as e: - logger.error(f"获取统计信息失败: {str(e)}") - return {"error": str(e)} - - -# ==================== MCP 工具注册 ==================== - -def register_tools(): - """注册所有 MCP 工具""" - global mcp_tools - - tools = [ - ListContainersTool(), - ListBlobsTool(), - GetBlobInfoTool(), - SearchBlobsTool(), - GetStorageStatsTool() - ] - - for tool in tools: - mcp_tools[tool.name] = tool - - logger.info(f"✅ 注册了 {len(mcp_tools)} 个 MCP 工具") - - -# ==================== API 端点 ==================== - -@app.get("/health", response_model=HealthResponse) -async def health_check(): - """健康检查""" - global blob_service_client, connection_string - - connected = blob_service_client is not None - - connection_info = None - if connected: - try: - account_info = blob_service_client.get_account_information() - connection_info = { - "account_kind": account_info.get('account_kind', 'unknown'), - "sku_name": account_info.get('sku_name', 'unknown'), - "connected_at": str(datetime.now()) - } - except Exception as e: - logger.error(f"获取账户信息失败: {str(e)}") - - return HealthResponse( - status="healthy" if connected else "not_connected", - connected=connected, - framework=AGENT_FRAMEWORK, - user_id=USER_ID, - namespace=NAMESPACE, - connection_info=connection_info - ) - - -@app.post("/connect") -async def connect_to_storage(request: ConnectRequest): - """连接到 Azure Blob Storage""" - global blob_service_client, connection_string - - try: - blob_service_client = BlobServiceClient.from_connection_string( - request.connection_string - ) - - account_info = blob_service_client.get_account_information() - connection_string = request.connection_string - - logger.info(f"✅ 成功连接到 Azure Blob Storage (User: {USER_ID})") - - return { - "status": "connected", - "message": "成功连接到 Azure Blob Storage", - "framework": AGENT_FRAMEWORK, - "user_id": USER_ID, - "account_info": { - "account_kind": account_info.get('account_kind'), - "sku_name": account_info.get('sku_name') - } - } - except Exception as e: - logger.error(f"❌ 连接失败: {str(e)}") - blob_service_client = None - connection_string = None - raise HTTPException(status_code=400, detail=f"连接失败: {str(e)}") - - -@app.get("/mcp/tools") -async def list_mcp_tools(): - """列出所有可用的 MCP 工具""" - if not blob_service_client: - raise HTTPException( - status_code=400, - detail="未连接到 Azure Blob Storage,请先调用 /connect" - ) - - tools_spec = [tool.to_mcp_spec() for tool in mcp_tools.values()] - - return { - "tools": tools_spec, - "count": len(tools_spec), - "framework": AGENT_FRAMEWORK - } - - -@app.post("/mcp/call") -async def call_mcp_tool(request: MCPToolRequest): - """调用 MCP 工具""" - if not blob_service_client: - raise HTTPException( - status_code=400, - detail="未连接到 Azure Blob Storage,请先调用 /connect" - ) - - tool_name = request.tool_name - if tool_name not in mcp_tools: - raise HTTPException( - status_code=404, - detail=f"工具 '{tool_name}' 不存在" - ) - - try: - tool = mcp_tools[tool_name] - result = await tool.execute(request.parameters) - - return { - "tool": tool_name, - "result": result, - "timestamp": str(datetime.now()) - } - except Exception as e: - logger.error(f"工具调用失败: {str(e)}") - raise HTTPException(status_code=500, detail=f"工具调用失败: {str(e)}") - - -@app.post("/query") -async def query_storage(request: MCPQueryRequest): - """使用自然语言查询存储 (简化版 - 实际应集成 LLM)""" - if not blob_service_client: - raise HTTPException( - status_code=400, - detail="未连接到 Azure Blob Storage,请先调用 /connect" - ) - - try: - query = request.query.lower() - result = None - - # 简单的规则匹配 (实际应使用 LLM 进行意图识别) - if "容器" in query and ("列出" in query or "显示" in query or "有哪些" in query): - tool = mcp_tools["list_containers"] - result = await tool.execute({}) - elif "统计" in query or "有多少" in query or "占用" in query: - tool = mcp_tools["get_storage_stats"] - result = await tool.execute({}) - elif request.container_name: - if "文件" in query or "blob" in query.lower(): - tool = mcp_tools["list_blobs"] - result = await tool.execute({"container_name": request.container_name}) - - if result: - return { - "status": "success", - "query": request.query, - "result": result, - "framework": AGENT_FRAMEWORK - } - else: - return { - "status": "info", - "query": request.query, - "message": "未能匹配到合适的工具,请使用 /mcp/tools 查看可用工具", - "available_tools": list(mcp_tools.keys()) - } - except Exception as e: - logger.error(f"查询执行失败: {str(e)}") - raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}") - - -@app.get("/") -async def root(): - """根端点""" - return { - "service": "Azure Blob Storage AI Agent", - "version": "1.0.0", - "framework": AGENT_FRAMEWORK, - "pod_name": POD_NAME, - "template": TEMPLATE_TYPE, - "user_id": USER_ID, - "namespace": NAMESPACE, - "connected": blob_service_client is not None, - "tools_count": len(mcp_tools), - "endpoints": { - "health": "/health", - "connect": "POST /connect", - "list_tools": "GET /mcp/tools", - "call_tool": "POST /mcp/call", - "query": "POST /query" - } - } - - -# ==================== 主函数 ==================== - -def init_storage_connection(): - """启动时初始化存储连接""" - global blob_service_client, connection_string - - if AZURE_STORAGE_CONNECTION_STRING: - try: - logger.info("检测到环境变量中的连接字符串,尝试连接...") - blob_service_client = BlobServiceClient.from_connection_string( - AZURE_STORAGE_CONNECTION_STRING - ) - - account_info = blob_service_client.get_account_information() - connection_string = AZURE_STORAGE_CONNECTION_STRING - - logger.info(f"✅ 成功连接到 Azure Blob Storage") - logger.info(f" - Account Kind: {account_info.get('account_kind')}") - logger.info(f" - SKU: {account_info.get('sku_name')}") - except Exception as e: - logger.error(f"❌ 启动时连接失败: {str(e)}") - logger.info("💡 提示: 可以稍后通过 /connect API 手动连接") - blob_service_client = None - connection_string = None - else: - logger.info("💡 未设置 AZURE_STORAGE_CONNECTION_STRING,需通过 /connect API 手动连接") - - -def main(): - """启动服务""" - logger.info(f"🚀 启动 Azure Blob Storage AI Agent (MCP)") - logger.info(f" - Framework: {AGENT_FRAMEWORK}") - logger.info(f" - Pod名称: {POD_NAME}") - logger.info(f" - 模板类型: {TEMPLATE_TYPE}") - logger.info(f" - User ID: {USER_ID}") - logger.info(f" - Namespace: {NAMESPACE}") - logger.info(f" - 模型: {MODEL_NAME} @ {MODEL_PROVIDER}") - logger.info(f" - 服务地址: http://{SERVICE_HOST}:{SERVICE_PORT}") - - # 注册 MCP 工具 - register_tools() - - # 初始化存储连接 - init_storage_connection() - - uvicorn.run( - app, - host=SERVICE_HOST, - port=SERVICE_PORT, - log_level="info" - ) - - -if __name__ == "__main__": - main() diff --git a/agent_templates/build_and_push.sh b/agent_templates/build_and_push.sh deleted file mode 100755 index 71d51f6..0000000 --- a/agent_templates/build_and_push.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -# 构建并推送AI Agent镜像到ACR - -set -e # 遇到错误立即退出 - -# 配置变量 -ACR_NAME="agnettaiji" # 你的ACR名称 -ACR_LOGIN_SERVER="${ACR_NAME}.azurecr.io" - -# 登录到ACR -echo "登录到Azure Container Registry..." -az acr login --name ${ACR_NAME} - -echo "当前目录: $(pwd)" -echo "" - -# 构建并推送MySQL Agent -echo "构建MySQL Agent镜像..." -docker build -f mysql_agent.Dockerfile -t ${ACR_LOGIN_SERVER}/ai-agents/mysql-agent:latest . -echo "推送MySQL Agent镜像..." -docker push ${ACR_LOGIN_SERVER}/ai-agents/mysql-agent:latest - -# 构建并推送PostgreSQL Agent -echo "构建PostgreSQL Agent镜像..." -docker build -f postgresql_agent.Dockerfile -t ${ACR_LOGIN_SERVER}/ai-agents/postgresql-agent:latest . -echo "推送PostgreSQL Agent镜像..." -docker push ${ACR_LOGIN_SERVER}/ai-agents/postgresql-agent:latest - -# 构建并推送Jina Search Agent -echo "构建Jina Search Agent镜像..." -docker build -f jina_search_agent.Dockerfile -t ${ACR_LOGIN_SERVER}/ai-agents/jina-search-agent:latest . -echo "推送Jina Search Agent镜像..." -docker push ${ACR_LOGIN_SERVER}/ai-agents/jina-search-agent:latest - -echo "✅ 所有镜像构建并推送完成!" -echo "" -echo "已推送的镜像:" -echo " - ${ACR_LOGIN_SERVER}/ai-agents/mysql-agent:latest" -echo " - ${ACR_LOGIN_SERVER}/ai-agents/postgresql-agent:latest" -echo " - ${ACR_LOGIN_SERVER}/ai-agents/jina-search-agent:latest" diff --git a/agent_templates/build_azure_blob_a2a.sh b/agent_templates/build_azure_blob_a2a.sh deleted file mode 100755 index c772fc5..0000000 --- a/agent_templates/build_azure_blob_a2a.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -# 构建并推送 Azure Blob Agent A2A 版本到 ACR -# 用法: ./build_azure_blob_a2a.sh - -set -e - -echo "🚀 构建 Azure Blob Agent (A2A版本)..." - -# Azure Container Registry 配置 -ACR_NAME="agnettaiji" -ACR_LOGIN_SERVER="${ACR_NAME}.azurecr.io" -IMAGE_NAME="ai-agents/azure-blob-agent-a2a" -IMAGE_TAG="latest" - -# 完整镜像名称 -FULL_IMAGE_NAME="${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}" - -echo "📦 镜像名称: ${FULL_IMAGE_NAME}" - -# 构建镜像 -echo "🔨 构建 Docker 镜像 (ARM64)..." -docker buildx build \ - --platform linux/arm64 \ - -f azure_blob_agent_a2a.Dockerfile \ - -t ${FULL_IMAGE_NAME} \ - --load \ - . - -echo "✅ 镜像构建成功" - -# 登录到 ACR -echo "🔐 登录到 Azure Container Registry..." -az acr login --name ${ACR_NAME} - -# 推送镜像 -echo "📤 推送镜像到 ACR..." -docker push ${FULL_IMAGE_NAME} - -echo "✅ 镜像推送成功" -echo "🎉 完成!镜像: ${FULL_IMAGE_NAME}" diff --git a/agent_templates/build_azure_blob_agent.sh b/agent_templates/build_azure_blob_agent.sh deleted file mode 100755 index 506d3a0..0000000 --- a/agent_templates/build_azure_blob_agent.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/bin/bash - -# Azure Blob Storage Agent 构建和推送脚本 -# 使用方法: ./build_azure_blob_agent.sh [TAG] - -set -e - -# 默认配置 -ACR_NAME="${ACR_NAME:-agnettaiji.azurecr.io}" -IMAGE_NAME="ai-agents/azure-blob-agent" -TAG="${1:-latest}" -FULL_IMAGE="${ACR_NAME}/${IMAGE_NAME}:${TAG}" - -echo "==========================================" -echo "构建 Azure Blob Storage Agent" -echo "==========================================" -echo "镜像: ${FULL_IMAGE}" -echo "" - -# 构建镜像 -echo "📦 开始构建镜像..." -docker build \ - -f azure_blob_agent.Dockerfile \ - -t "${FULL_IMAGE}" \ - . - -echo "" -echo "✅ 镜像构建成功: ${FULL_IMAGE}" -echo "" - -# 询问是否推送 -read -p "是否推送到 ACR? (y/N): " -n 1 -r -echo -if [[ $REPLY =~ ^[Yy]$ ]]; then - echo "🚀 推送镜像到 ACR..." - - # 登录 ACR (如果需要) - echo "登录到 ACR..." - az acr login --name $(echo ${ACR_NAME} | cut -d'.' -f1) - - # 推送镜像 - docker push "${FULL_IMAGE}" - - echo "" - echo "✅ 镜像推送成功!" -else - echo "⏭️ 跳过推送" -fi - -echo "" -echo "==========================================" -echo "本地测试命令:" -echo "==========================================" -echo "" -echo "# 启动容器 (需要 LiteLLM 服务)" -echo "docker run -d --name azure-blob-agent \\" -echo " -p 8080:8080 \\" -echo " -e LITELLM_API_BASE=http://host.docker.internal:4000 \\" -echo " -e LITELLM_MODEL=gpt-3.5-turbo \\" -echo " -e LITELLM_API_KEY=sk-1234 \\" -echo " -e AZURE_STORAGE_CONNECTION_STRING='YOUR_CONNECTION_STRING' \\" -echo " ${FULL_IMAGE}" -echo "" -echo "# 或者不提供连接字符串,稍后通过 API 连接" -echo "docker run -d --name azure-blob-agent \\" -echo " -p 8080:8080 \\" -echo " -e LITELLM_API_BASE=http://host.docker.internal:4000 \\" -echo " -e LITELLM_MODEL=gpt-3.5-turbo \\" -echo " -e LITELLM_API_KEY=sk-1234 \\" -echo " ${FULL_IMAGE}" -echo "" -echo "# 检查健康状态" -echo "curl http://localhost:8080/health" -echo "" -echo "# 连接到 Azure Storage" -echo "curl -X POST http://localhost:8080/connect \\" -echo " -H 'Content-Type: application/json' \\" -echo " -d '{\"connection_string\": \"YOUR_CONNECTION_STRING\"}'" -echo "" -echo "# 执行自然语言查询" -echo "curl -X POST http://localhost:8080/query \\" -echo " -H 'Content-Type: application/json' \\" -echo " -d '{\"query\": \"列出所有容器\"}'" -echo "" -echo "# 查看日志" -echo "docker logs -f azure-blob-agent" -echo "" -echo "==========================================" diff --git a/agent_templates/build_azure_blob_mcp.sh b/agent_templates/build_azure_blob_mcp.sh deleted file mode 100755 index ae9662c..0000000 --- a/agent_templates/build_azure_blob_mcp.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -# 构建并推送 Azure Blob Agent MCP 版本到 ACR -# 用法: ./build_azure_blob_mcp.sh - -set -e - -echo "🚀 构建 Azure Blob Agent (MCP版本)..." - -# Azure Container Registry 配置 -ACR_NAME="agnettaiji" -ACR_LOGIN_SERVER="${ACR_NAME}.azurecr.io" -IMAGE_NAME="ai-agents/azure-blob-agent-mcp" -IMAGE_TAG="latest" - -# 完整镜像名称 -FULL_IMAGE_NAME="${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}" - -echo "📦 镜像名称: ${FULL_IMAGE_NAME}" - -# 构建镜像 -echo "🔨 构建 Docker 镜像 (ARM64)..." -docker buildx build \ - --platform linux/arm64 \ - -f azure_blob_agent_mcp.Dockerfile \ - -t ${FULL_IMAGE_NAME} \ - --load \ - . - -echo "✅ 镜像构建成功" - -# 登录到 ACR -echo "🔐 登录到 Azure Container Registry..." -az acr login --name ${ACR_NAME} - -# 推送镜像 -echo "📤 推送镜像到 ACR..." -docker push ${FULL_IMAGE_NAME} - -echo "✅ 镜像推送成功" -echo "🎉 完成!镜像: ${FULL_IMAGE_NAME}" diff --git a/agent_templates/build_jina_agent.sh b/agent_templates/build_jina_agent.sh deleted file mode 100755 index 7bc287b..0000000 --- a/agent_templates/build_jina_agent.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -# 构建并推送Jina Search Agent镜像到ACR - -set -e - -# 配置变量 -ACR_NAME="agnettaiji" -ACR_LOGIN_SERVER="${ACR_NAME}.azurecr.io" -IMAGE_NAME="ai-agents/jina-search-agent" -IMAGE_TAG="latest" - -echo "==========================================" -echo "Jina Search Agent Docker镜像构建脚本" -echo "==========================================" - -# 检查是否在正确的目录 -if [ ! -f "jina_search_agent.py" ]; then - echo "错误: 请在agent_templates目录下运行此脚本" - exit 1 -fi - -# 登录到ACR -echo "" -echo "步骤1: 登录到Azure Container Registry..." -az acr login --name ${ACR_NAME} - -# 构建镜像 -echo "" -echo "步骤2: 构建Docker镜像..." -docker build -f jina_search_agent.Dockerfile -t ${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG} . - -# 推送镜像 -echo "" -echo "步骤3: 推送镜像到ACR..." -docker push ${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG} - -echo "" -echo "==========================================" -echo "构建完成!" -echo "镜像: ${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}" -echo "==========================================" -echo "" -echo "使用示例:" -echo "curl -X POST 'http://localhost:8000/agents' \\" -echo " -H 'Content-Type: application/json' \\" -echo " -d '{" -echo " \"name\": \"my-jina-agent\"," -echo " \"template\": \"jina_search_agent\"," -echo " \"config\": {" -echo " \"env\": {" -echo " \"JINA_API_KEY\": \"your-jina-api-key\"" -echo " }" -echo " }" -echo " }'" diff --git a/agent_templates/common/Dockerfile.test b/agent_templates/common/Dockerfile.test new file mode 100644 index 0000000..dbf8e62 --- /dev/null +++ b/agent_templates/common/Dockerfile.test @@ -0,0 +1,19 @@ +FROM python:3.11-slim + +WORKDIR /app + +# 安装基本依赖 +RUN pip install --no-cache-dir \ + python-dotenv \ + loguru \ + aiohttp \ + requests \ + orjson \ + typing-extensions \ + asyncio-throttle + +COPY search_agent/ /app/search_agent/ +COPY agent_callback_utils.py /app/ +COPY test_search_import.py /app/ + +CMD ["python3", "/app/test_search_import.py"] diff --git a/agent_templates/common/agent_callback_utils.py b/agent_templates/common/agent_callback_utils.py new file mode 100644 index 0000000..4b8a929 --- /dev/null +++ b/agent_templates/common/agent_callback_utils.py @@ -0,0 +1,205 @@ +""" +Agent回调工具 - 用于向Agent Manager回调运行时长记录 +""" +import os +import time +import logging +import requests +from typing import Optional, List +from datetime import datetime, timezone + +logger = logging.getLogger(__name__) + + +class AgentCallbackHandler: + """Agent回调处理器""" + + def __init__( + self, + agent_name: Optional[str] = None, + user_id: Optional[str] = None, + callback_url: Optional[str] = None + ): + """ + 初始化回调处理器 + + Args: + agent_name: Agent名称,默认从环境变量 POD_NAME 获取 + user_id: 用户ID,默认从环境变量 USER_ID 获取 + callback_url: 回调URL,默认从环境变量 AGENT_CALLBACK_URL 获取 + """ + self.agent_name = agent_name or os.getenv("POD_NAME", "unknown-agent") + self.user_id = user_id or os.getenv("USER_ID", "") + self.callback_url = callback_url or os.getenv( + "AGENT_CALLBACK_URL", + "http://mcp-server:8002/api/v1/billing/agent-callback" + ) + + self.start_time: Optional[datetime] = None + self.tools_used: List[str] = [] + self.request_id: Optional[str] = None + + logger.info(f"AgentCallbackHandler initialized: agent={self.agent_name}, callback_url={self.callback_url}") + + def start_request(self, request_id: Optional[str] = None, user_id: Optional[str] = None): + """ + 开始一次请求处理 + + Args: + request_id: 请求ID + user_id: 用户ID(如果提供则覆盖默认值) + """ + self.start_time = datetime.now(timezone.utc) + self.tools_used = [] + self.request_id = request_id or f"req-{int(time.time())}" + + if user_id: + self.user_id = user_id + + logger.info(f"Request started: request_id={self.request_id}, user_id={self.user_id}") + + def add_tool_used(self, tool_name: str): + """ + 记录使用的工具 + + Args: + tool_name: 工具名称 + """ + if tool_name not in self.tools_used: + self.tools_used.append(tool_name) + logger.debug(f"Tool used: {tool_name}") + + def end_request(self, tools_used: Optional[List[str]] = None) -> bool: + """ + 结束请求并发送回调 + + Args: + tools_used: 使用的工具列表(可选,如果提供则覆盖内部记录) + + Returns: + 是否成功发送回调 + """ + if not self.start_time: + logger.warning("Cannot end request: no start time recorded") + return False + + if not self.user_id: + logger.warning("Cannot send callback: user_id not set") + return False + + end_time = datetime.now(timezone.utc) + running_time = (end_time - self.start_time).total_seconds() + + # 使用提供的工具列表或内部记录 + final_tools_used = tools_used if tools_used is not None else self.tools_used + + # 发送回调 + success = self._send_callback( + running_time_seconds=int(running_time), + start_time=self.start_time, + end_time=end_time, + tools_used=final_tools_used + ) + + # 重置状态 + self.start_time = None + self.tools_used = [] + self.request_id = None + + return success + + def _send_callback( + self, + running_time_seconds: int, + start_time: datetime, + end_time: datetime, + tools_used: List[str] + ) -> bool: + """ + 发送回调到Agent Manager + + Args: + running_time_seconds: 运行时长(秒) + start_time: 开始时间 + end_time: 结束时间 + tools_used: 使用的工具列表 + + Returns: + 是否成功发送 + """ + try: + payload = { + "agentName": self.agent_name, + "userId": self.user_id, + "podRunningTimeSeconds": running_time_seconds, + "toolsUsed": tools_used, + "startTime": start_time.isoformat(), + "endTime": end_time.isoformat(), + "requestId": self.request_id + } + + logger.info(f"Sending callback: {payload}") + + response = requests.post( + self.callback_url, + json=payload, + timeout=5 + ) + + if response.status_code == 200: + logger.info(f"Callback sent successfully: {response.json()}") + return True + else: + logger.error(f"Callback failed with status {response.status_code}: {response.text}") + return False + + except requests.exceptions.RequestException as e: + logger.error(f"Failed to send callback: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error sending callback: {str(e)}") + return False + + +class CallbackContextManager: + """回调上下文管理器 - 使用with语句自动处理开始和结束""" + + def __init__( + self, + handler: AgentCallbackHandler, + request_id: Optional[str] = None, + user_id: Optional[str] = None, + tools_used: Optional[List[str]] = None + ): + """ + 初始化上下文管理器 + + Args: + handler: AgentCallbackHandler实例 + request_id: 请求ID + user_id: 用户ID + tools_used: 使用的工具列表(可选) + """ + self.handler = handler + self.request_id = request_id + self.user_id = user_id + self.tools_used = tools_used or [] + + def __enter__(self): + """进入上下文时开始计时""" + self.handler.start_request( + request_id=self.request_id, + user_id=self.user_id + ) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """退出上下文时发送回调""" + self.handler.end_request(tools_used=self.tools_used) + return False # 不抑制异常 + + def add_tool(self, tool_name: str): + """添加使用的工具""" + self.handler.add_tool_used(tool_name) + if tool_name not in self.tools_used: + self.tools_used.append(tool_name) diff --git a/agent_templates/common/api_key_utils.py b/agent_templates/common/api_key_utils.py new file mode 100644 index 0000000..a47c9c2 --- /dev/null +++ b/agent_templates/common/api_key_utils.py @@ -0,0 +1,121 @@ +""" +统一的 API Key 配置工具模块 + +支持从环境变量或请求参数获取 API key +优先使用请求传入的 key,如果未提供则从环境变量获取 +""" +import os +from typing import Optional + + +def get_api_key( + request_key: Optional[str] = None, + env_key_name: str = "API_KEY", + default: Optional[str] = None +) -> Optional[str]: + """ + 获取 API key,优先使用请求传入的,否则从环境变量获取 + + Args: + request_key: 请求中传入的 API key(优先使用) + env_key_name: 环境变量名称 + default: 默认值(如果都未设置) + + Returns: + API key 字符串,如果都未设置则返回 None 或 default + """ + # 优先使用请求传入的 key + if request_key: + return request_key + + # 从环境变量获取 + env_key = os.getenv(env_key_name) + if env_key: + return env_key + + # 返回默认值 + return default + + +def get_llm_api_key(request_key: Optional[str] = None) -> Optional[str]: + """ + 获取 LLM API key + + Args: + request_key: 请求中传入的 LLM API key + + Returns: + LLM API key + """ + return get_api_key(request_key, "LLM_API_KEY") + + +def get_openai_api_key(request_key: Optional[str] = None) -> Optional[str]: + """ + 获取 OpenAI API key + + Args: + request_key: 请求中传入的 OpenAI API key + + Returns: + OpenAI API key + """ + return get_api_key(request_key, "OPENAI_API_KEY") + + +def get_litellm_api_key(request_key: Optional[str] = None) -> Optional[str]: + """ + 获取 LiteLLM API key + + Args: + request_key: 请求中传入的 LiteLLM API key + + Returns: + LiteLLM API key + """ + return get_api_key(request_key, "LITELLM_API_KEY") + + +def get_serper_api_key(request_key: Optional[str] = None) -> Optional[str]: + """ + 获取 Serper API key + + Args: + request_key: 请求中传入的 Serper API key + + Returns: + Serper API key + """ + return get_api_key(request_key, "SERPER_API_KEY") + + +def get_jina_api_key(request_key: Optional[str] = None) -> Optional[str]: + """ + 获取 Jina API key + + Args: + request_key: 请求中传入的 Jina API key + + Returns: + Jina API key + """ + return get_api_key(request_key, "JINA_API_KEY") + + +def validate_api_key(api_key: Optional[str], key_name: str = "API key") -> str: + """ + 验证 API key 是否存在,如果不存在则抛出异常 + + Args: + api_key: 要验证的 API key + key_name: key 的名称(用于错误消息) + + Returns: + 验证通过的 API key + + Raises: + ValueError: 如果 API key 未设置 + """ + if not api_key: + raise ValueError(f"{key_name} 未设置!请通过请求参数传入或设置环境变量") + return api_key diff --git a/agent_templates/docs/AZURE_BLOB_AGENT_A2A_EXAMPLES.md b/agent_templates/docs/AZURE_BLOB_AGENT_A2A_EXAMPLES.md new file mode 100644 index 0000000..7f1d6a1 --- /dev/null +++ b/agent_templates/docs/AZURE_BLOB_AGENT_A2A_EXAMPLES.md @@ -0,0 +1,416 @@ +# Azure Blob Agent A2A 请求调用示例 + +## 服务信息 +- **服务名称**: Azure Blob Storage AI Agent (A2A) +- **版本**: 1.0.0 +- **框架**: Agent-to-Agent (A2A) +- **默认端口**: 8080 + +## 概述 +A2A 版本支持 Agent 之间的协作和通信,允许多个 Agent 互相调用和协作完成复杂任务。 + +--- + +## API 端点 + +### 1. 健康检查 +**端点**: `GET /health` + +**请求示例** (curl): +```bash +curl http://localhost:8080/health +``` + +**响应示例**: +```json +{ + "status": "healthy", + "connected": true, + "framework": "a2a", + "agent_id": "azure-blob-agent-a2a", + "agent_role": "storage_manager", + "capabilities": ["blob_storage", "file_operations"], + "namespace": "ai-agents", + "connection_info": { + "account_kind": "StorageV2", + "sku_name": "Standard_LRS" + } +} +``` + +--- + +### 2. A2A Agent 注册 +**端点**: `POST /a2a/register` + +注册其他 Agent 以便协作。 + +**请求体**: +```json +{ + "agent_id": "search-agent", + "agent_role": "search_provider", + "capabilities": ["web_search", "content_extraction"], + "endpoint": "http://search-agent:8080" +} +``` + +**请求示例** (curl): +```bash +curl -X POST http://localhost:8080/a2a/register \ + -H "Content-Type: application/json" \ + -d '{ + "agent_id": "search-agent", + "agent_role": "search_provider", + "capabilities": ["web_search"], + "endpoint": "http://search-agent:8080" + }' +``` + +**请求示例** (Python): +```python +import requests + +register_data = { + "agent_id": "search-agent", + "agent_role": "search_provider", + "capabilities": ["web_search", "content_extraction"], + "endpoint": "http://search-agent:8080" +} + +response = requests.post( + "http://localhost:8080/a2a/register", + json=register_data +) +print(response.json()) +``` + +**响应示例**: +```json +{ + "status": "success", + "message": "Agent registered successfully", + "agent_id": "search-agent" +} +``` + +--- + +### 3. A2A 消息发送 +**端点**: `POST /a2a/message` + +发送 A2A 协议消息给此 Agent。 + +**请求体**: +```json +{ + "message_id": "msg-12345", + "from_agent": "orchestrator-agent", + "to_agent": "azure-blob-agent-a2a", + "message_type": "request", + "action": "list_containers", + "parameters": {}, + "context": { + "user_id": "user123", + "session_id": "sess-456" + }, + "timestamp": "2026-01-15T10:30:00Z" +} +``` + +**请求示例** (curl): +```bash +curl -X POST http://localhost:8080/a2a/message \ + -H "Content-Type: application/json" \ + -d '{ + "message_id": "msg-001", + "from_agent": "orchestrator", + "to_agent": "azure-blob-agent-a2a", + "message_type": "request", + "action": "list_containers", + "parameters": {}, + "context": {"user_id": "user123"} + }' +``` + +**支持的 Actions**: +- `list_containers` - 列出所有容器 +- `list_blobs` - 列出容器中的文件 +- `upload_blob` - 上传文件 +- `download_blob` - 下载文件 +- `delete_blob` - 删除文件 +- `create_container` - 创建容器 + +**请求示例** (Python): +```python +import requests +from datetime import datetime + +message = { + "message_id": f"msg-{int(datetime.now().timestamp())}", + "from_agent": "my-orchestrator", + "to_agent": "azure-blob-agent-a2a", + "message_type": "request", + "action": "list_blobs", + "parameters": { + "container_name": "mycontainer" + }, + "context": { + "user_id": "user123", + "tenant_id": "tenant-001" + }, + "timestamp": datetime.utcnow().isoformat() +} + +response = requests.post( + "http://localhost:8080/a2a/message", + json=message +) +print(response.json()) +``` + +**响应示例**: +```json +{ + "message_id": "msg-001", + "from_agent": "azure-blob-agent-a2a", + "to_agent": "orchestrator", + "message_type": "response", + "status": "success", + "result": { + "blobs": [ + {"name": "file1.txt", "size": 1024}, + {"name": "file2.pdf", "size": 2048} + ] + }, + "timestamp": "2026-01-15T10:30:05Z" +} +``` + +--- + +### 4. A2A 自然语言查询 +**端点**: `POST /a2a/query` + +使用自然语言查询,支持 A2A 上下文。 + +**请求体**: +```json +{ + "query": "列出所有容器中的文件", + "container_name": "mycontainer", + "requester_agent": "orchestrator-agent", + "context": { + "user_id": "user123", + "session_id": "sess-456" + } +} +``` + +**请求示例** (curl): +```bash +curl -X POST http://localhost:8080/a2a/query \ + -H "Content-Type: application/json" \ + -d '{ + "query": "列出所有容器", + "requester_agent": "orchestrator", + "context": {"user_id": "user123"} + }' +``` + +**请求示例** (Python): +```python +import requests + +query_data = { + "query": "上传文件到 documents 容器", + "container_name": "documents", + "requester_agent": "file-processor", + "context": { + "user_id": "user123", + "file_path": "/tmp/report.pdf" + } +} + +response = requests.post( + "http://localhost:8080/a2a/query", + json=query_data +) +print(response.json()) +``` + +**响应示例**: +```json +{ + "status": "success", + "query": "列出所有容器", + "answer": "找到 3 个容器: documents, images, backups", + "context": { + "containers": ["documents", "images", "backups"] + } +} +``` + +--- + +### 5. 获取已注册的 Agents +**端点**: `GET /a2a/agents` + +**请求示例** (curl): +```bash +curl http://localhost:8080/a2a/agents +``` + +**响应示例**: +```json +{ + "registered_agents": [ + { + "agent_id": "search-agent", + "agent_role": "search_provider", + "capabilities": ["web_search", "content_extraction"], + "endpoint": "http://search-agent:8080" + }, + { + "agent_id": "mysql-agent", + "agent_role": "database_manager", + "capabilities": ["sql_query", "data_analysis"], + "endpoint": "http://mysql-agent:8080" + } + ] +} +``` + +--- + +## A2A 协作场景示例 + +### 场景 1: Orchestrator 协调多个 Agents + +```python +import requests + +# 1. Orchestrator 注册到 Blob Agent +orchestrator_info = { + "agent_id": "orchestrator-001", + "agent_role": "task_coordinator", + "capabilities": ["workflow", "coordination"], + "endpoint": "http://orchestrator:8080" +} +requests.post("http://blob-agent:8080/a2a/register", json=orchestrator_info) + +# 2. Orchestrator 发送任务给 Blob Agent +task_message = { + "message_id": "task-001", + "from_agent": "orchestrator-001", + "to_agent": "azure-blob-agent-a2a", + "message_type": "request", + "action": "list_containers", + "parameters": {}, + "context": { + "workflow_id": "wf-123", + "user_id": "user456" + } +} +response = requests.post("http://blob-agent:8080/a2a/message", json=task_message) +containers = response.json() + +# 3. 基于结果继续下一步 +for container in containers.get("result", {}).get("containers", []): + list_message = { + "message_id": f"task-{container}", + "from_agent": "orchestrator-001", + "to_agent": "azure-blob-agent-a2a", + "message_type": "request", + "action": "list_blobs", + "parameters": {"container_name": container}, + "context": {"workflow_id": "wf-123"} + } + files = requests.post("http://blob-agent:8080/a2a/message", json=list_message) + print(f"Container {container}: {files.json()}") +``` + +--- + +### 场景 2: Agent 间数据传输 + +```python +import requests + +# Search Agent 找到需要存储的内容 +search_result = { + "content": "Important data from web search", + "source": "https://example.com" +} + +# 通过 A2A 消息让 Blob Agent 存储结果 +store_message = { + "message_id": "store-001", + "from_agent": "search-agent", + "to_agent": "azure-blob-agent-a2a", + "message_type": "request", + "action": "upload_blob", + "parameters": { + "container_name": "search-results", + "blob_name": "result-2026-01-15.json", + "content": search_result + }, + "context": { + "source_agent": "search-agent", + "timestamp": "2026-01-15T10:30:00Z" + } +} + +response = requests.post( + "http://blob-agent:8080/a2a/message", + json=store_message +) +print(response.json()) +``` + +--- + +## 环境变量配置 + +```bash +# 服务配置 +export SERVICE_HOST="0.0.0.0" +export SERVICE_PORT="8080" +export POD_NAME="azure-blob-agent-a2a" +export TEMPLATE_TYPE="azure_blob_agent_a2a" +export AGENT_FRAMEWORK="a2a" + +# A2A Agent 配置 +export AGENT_ID="azure-blob-agent-a2a" +export AGENT_ROLE="storage_manager" +export AGENT_CAPABILITIES='["blob_storage", "file_operations"]' + +# 模型配置 +export MODEL_PROVIDER="openai" +export MODEL_NAME="gpt-4" +export MODEL_API_KEY="sk-xxx" +export MODEL_ENDPOINT="https://api.openai.com/v1" + +# 存储配置 +export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;..." +export STORAGE_ACCOUNT_NAME="myaccount" + +# 用户标识 +export USER_ID="default-user" +export TENANT_ID="tenant-001" +export NAMESPACE="ai-agents" + +# 启动服务 +python azure_blob_agent_a2a.py +``` + +--- + +## 注意事项 + +1. **Agent 注册**: 协作前需要先注册其他 Agent +2. **消息格式**: 严格遵循 A2A 消息格式 +3. **异步通信**: 支持异步消息传递 +4. **安全性**: 建议在生产环境中添加身份验证 +5. **超时处理**: 设置合理的超时时间 +6. **错误重试**: 实现重试机制处理网络问题 diff --git a/agent_templates/docs/AZURE_BLOB_AGENT_EXAMPLES.md b/agent_templates/docs/AZURE_BLOB_AGENT_EXAMPLES.md new file mode 100644 index 0000000..5910409 --- /dev/null +++ b/agent_templates/docs/AZURE_BLOB_AGENT_EXAMPLES.md @@ -0,0 +1,273 @@ +# Azure Blob Agent 请求调用示例 + +## 服务信息 +- **服务名称**: Azure Blob Storage AI Agent +- **版本**: 1.0.0 +- **框架**: LangChain + LiteLLM +- **默认端口**: 8080 + +## API 端点 + +### 1. 健康检查 +**端点**: `GET /health` + +**请求示例** (curl): +```bash +curl http://localhost:8080/health +``` + +**请求示例** (Python): +```python +import requests + +response = requests.get("http://localhost:8080/health") +print(response.json()) +``` + +**响应示例**: +```json +{ + "status": "healthy", + "connected": true, + "connection_info": { + "account_kind": "StorageV2", + "sku_name": "Standard_LRS" + } +} +``` + +--- + +### 2. 连接到 Azure Blob Storage +**端点**: `POST /connect` + +**请求体**: +```json +{ + "connection_string": "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=xxx;EndpointSuffix=core.windows.net" +} +``` + +**请求示例** (curl): +```bash +curl -X POST http://localhost:8080/connect \ + -H "Content-Type: application/json" \ + -d '{ + "connection_string": "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=your_key;EndpointSuffix=core.windows.net" + }' +``` + +**请求示例** (Python): +```python +import requests + +connection_data = { + "connection_string": "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=your_key;EndpointSuffix=core.windows.net" +} + +response = requests.post( + "http://localhost:8080/connect", + json=connection_data +) +print(response.json()) +``` + +**响应示例**: +```json +{ + "status": "connected", + "message": "成功连接到Azure Blob Storage", + "account_info": { + "account_kind": "StorageV2", + "sku_name": "Standard_LRS" + } +} +``` + +--- + +### 3. 自然语言查询/操作 +**端点**: `POST /query` + +**请求体**: +```json +{ + "query": "列出所有容器", + "litellm_api_key": "your-litellm-api-key", + "user_id": "user123", + "container_name": "mycontainer" +} +``` + +**请求示例** (curl): +```bash +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{ + "query": "列出所有容器", + "litellm_api_key": "sk-xxx", + "user_id": "user123" + }' +``` + +**请求示例** (Python): +```python +import requests + +query_data = { + "query": "列出 mycontainer 容器中的所有文件", + "litellm_api_key": "sk-xxx", + "user_id": "user123", + "container_name": "mycontainer" +} + +response = requests.post( + "http://localhost:8080/query", + json=query_data +) +print(response.json()) +``` + +**查询示例**: +1. 列出所有容器: `"列出所有容器"` +2. 列出容器中的文件: `"列出 mycontainer 容器中的所有文件"` +3. 上传文件: `"上传 test.txt 文件到 mycontainer 容器"` +4. 下载文件: `"下载 mycontainer/test.txt 文件"` +5. 删除文件: `"删除 mycontainer/old-file.txt"` + +**响应示例**: +```json +{ + "status": "success", + "query": "列出所有容器", + "answer": "容器列表:\n1. container1 (最后修改: 2026-01-15 10:30:00)\n2. container2 (最后修改: 2026-01-14 15:20:00)", + "intermediate_steps": "[...]" +} +``` + +--- + +### 4. 服务信息 +**端点**: `GET /` + +**请求示例** (curl): +```bash +curl http://localhost:8080/ +``` + +**响应示例**: +```json +{ + "service": "Azure Blob Storage AI Agent", + "version": "1.0.0", + "pod_name": "azure-blob-agent", + "template": "azure_blob_agent", + "connected": true, + "endpoints": { + "health": "/health", + "connect": "POST /connect", + "query": "POST /query" + } +} +``` + +--- + +## 完整使用流程示例 + +### Python 完整示例: +```python +import requests + +# 服务地址 +base_url = "http://localhost:8080" + +# 1. 检查服务健康状态 +health = requests.get(f"{base_url}/health") +print("Health:", health.json()) + +# 2. 连接到 Azure Blob Storage +connect_data = { + "connection_string": "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=your_key;EndpointSuffix=core.windows.net" +} +connect_response = requests.post(f"{base_url}/connect", json=connect_data) +print("Connect:", connect_response.json()) + +# 3. 执行查询 - 列出所有容器 +query_data = { + "query": "列出所有容器", + "litellm_api_key": "sk-xxx", + "user_id": "user123" +} +query_response = requests.post(f"{base_url}/query", json=query_data) +print("Query Result:", query_response.json()) + +# 4. 执行操作 - 列出容器中的文件 +list_files_data = { + "query": "列出 mycontainer 容器中的所有文件", + "litellm_api_key": "sk-xxx", + "user_id": "user123", + "container_name": "mycontainer" +} +files_response = requests.post(f"{base_url}/query", json=list_files_data) +print("Files:", files_response.json()) +``` + +--- + +## 环境变量配置 + +启动服务时可配置的环境变量: + +```bash +# 服务配置 +export SERVICE_HOST="0.0.0.0" +export SERVICE_PORT="8080" +export POD_NAME="azure-blob-agent" +export TEMPLATE_TYPE="azure_blob_agent" + +# LiteLLM 配置 +export LITELLM_API_BASE="http://localhost:4000" +export LITELLM_MODEL="gpt-3.5-turbo" + +# Azure Storage 连接字符串 (可选,也可通过 API 连接) +export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=xxx;AccountKey=xxx;EndpointSuffix=core.windows.net" + +# 启动服务 +python azure_blob_agent.py +``` + +--- + +## 错误处理 + +### 未连接错误: +```json +{ + "detail": "未连接到Azure Blob Storage,请先调用 /connect" +} +``` + +### 连接失败: +```json +{ + "detail": "连接失败: Invalid connection string" +} +``` + +### 查询失败: +```json +{ + "detail": "查询失败: Container not found" +} +``` + +--- + +## 注意事项 + +1. **API Key 安全**: litellm_api_key 从请求中传入,不建议硬编码 +2. **连接字符串**: 建议使用环境变量或密钥管理服务 +3. **用户ID**: 用于计费回调,可选参数 +4. **容器名称**: 某些操作需要指定容器名称 +5. **并发请求**: 服务支持并发请求处理 diff --git a/agent_templates/docs/AZURE_BLOB_AGENT_MCP_EXAMPLES.md b/agent_templates/docs/AZURE_BLOB_AGENT_MCP_EXAMPLES.md new file mode 100644 index 0000000..d2ce6da --- /dev/null +++ b/agent_templates/docs/AZURE_BLOB_AGENT_MCP_EXAMPLES.md @@ -0,0 +1,490 @@ +# Azure Blob Agent MCP 请求调用示例 + +## 服务信息 +- **服务名称**: Azure Blob Storage AI Agent (MCP) +- **版本**: 1.0.0 +- **框架**: Model Context Protocol (MCP) +- **默认端口**: 8080 + +## 概述 +MCP 版本使用 Model Context Protocol 协议实现智能文件操作,提供标准化的工具调用接口。 + +--- + +## API 端点 + +### 1. 健康检查 +**端点**: `GET /health` + +**请求示例** (curl): +```bash +curl http://localhost:8080/health +``` + +**响应示例**: +```json +{ + "status": "healthy", + "connected": true, + "framework": "mcp", + "user_id": "user123", + "namespace": "ai-agents", + "connection_info": { + "account_kind": "StorageV2", + "sku_name": "Standard_LRS" + } +} +``` + +--- + +### 2. 获取 MCP 工具列表 +**端点**: `GET /mcp/tools` + +列出所有可用的 MCP 工具及其参数。 + +**请求示例** (curl): +```bash +curl http://localhost:8080/mcp/tools +``` + +**请求示例** (Python): +```python +import requests + +response = requests.get("http://localhost:8080/mcp/tools") +tools = response.json() +for tool in tools["tools"]: + print(f"Tool: {tool['name']}") + print(f"Description: {tool['description']}") + print(f"Parameters: {tool['parameters_schema']}") +``` + +**响应示例**: +```json +{ + "tools": [ + { + "name": "list_containers", + "description": "列出所有 Blob 容器", + "parameters_schema": { + "type": "object", + "properties": {}, + "required": [] + } + }, + { + "name": "list_blobs", + "description": "列出容器中的所有 Blob", + "parameters_schema": { + "type": "object", + "properties": { + "container_name": { + "type": "string", + "description": "容器名称" + } + }, + "required": ["container_name"] + } + }, + { + "name": "upload_blob", + "description": "上传文件到 Blob 容器", + "parameters_schema": { + "type": "object", + "properties": { + "container_name": {"type": "string"}, + "blob_name": {"type": "string"}, + "content": {"type": "string"} + }, + "required": ["container_name", "blob_name", "content"] + } + }, + { + "name": "download_blob", + "description": "从 Blob 容器下载文件", + "parameters_schema": { + "type": "object", + "properties": { + "container_name": {"type": "string"}, + "blob_name": {"type": "string"} + }, + "required": ["container_name", "blob_name"] + } + } + ] +} +``` + +--- + +### 3. 调用 MCP 工具 +**端点**: `POST /mcp/tool` + +调用特定的 MCP 工具。 + +**请求体**: +```json +{ + "tool_name": "list_blobs", + "parameters": { + "container_name": "mycontainer" + } +} +``` + +**请求示例** (curl): +```bash +curl -X POST http://localhost:8080/mcp/tool \ + -H "Content-Type: application/json" \ + -d '{ + "tool_name": "list_blobs", + "parameters": { + "container_name": "documents" + } + }' +``` + +**请求示例** (Python): +```python +import requests + +# 示例 1: 列出容器 +list_containers = { + "tool_name": "list_containers", + "parameters": {} +} +response = requests.post("http://localhost:8080/mcp/tool", json=list_containers) +print(response.json()) + +# 示例 2: 列出容器中的文件 +list_blobs = { + "tool_name": "list_blobs", + "parameters": { + "container_name": "documents" + } +} +response = requests.post("http://localhost:8080/mcp/tool", json=list_blobs) +print(response.json()) + +# 示例 3: 上传文件 +upload_blob = { + "tool_name": "upload_blob", + "parameters": { + "container_name": "documents", + "blob_name": "report.txt", + "content": "This is the report content" + } +} +response = requests.post("http://localhost:8080/mcp/tool", json=upload_blob) +print(response.json()) + +# 示例 4: 下载文件 +download_blob = { + "tool_name": "download_blob", + "parameters": { + "container_name": "documents", + "blob_name": "report.txt" + } +} +response = requests.post("http://localhost:8080/mcp/tool", json=download_blob) +print(response.json()) +``` + +**响应示例** (list_blobs): +```json +{ + "status": "success", + "tool_name": "list_blobs", + "result": { + "blobs": [ + { + "name": "file1.txt", + "size": 1024, + "last_modified": "2026-01-15T10:30:00Z" + }, + { + "name": "file2.pdf", + "size": 2048, + "last_modified": "2026-01-14T15:20:00Z" + } + ], + "count": 2 + } +} +``` + +--- + +### 4. MCP 查询 (自然语言) +**端点**: `POST /mcp/query` + +使用自然语言查询,MCP 会自动选择合适的工具。 + +**请求体**: +```json +{ + "query": "显示 documents 容器中的所有文件", + "container_name": "documents", + "context": { + "user_id": "user123", + "session_id": "sess-456" + } +} +``` + +**请求示例** (curl): +```bash +curl -X POST http://localhost:8080/mcp/query \ + -H "Content-Type: application/json" \ + -d '{ + "query": "列出所有容器", + "context": {"user_id": "user123"} + }' +``` + +**请求示例** (Python): +```python +import requests + +query_data = { + "query": "上传一个名为 test.txt 的文件到 mycontainer,内容是 Hello World", + "context": { + "user_id": "user123", + "operation": "upload" + } +} + +response = requests.post( + "http://localhost:8080/mcp/query", + json=query_data +) +print(response.json()) +``` + +**响应示例**: +```json +{ + "status": "success", + "query": "上传一个名为 test.txt 的文件到 mycontainer,内容是 Hello World", + "answer": "成功上传文件 test.txt 到容器 mycontainer", + "tools_used": ["upload_blob"], + "context": { + "container_name": "mycontainer", + "blob_name": "test.txt", + "size": 11 + } +} +``` + +--- + +## 完整使用流程示例 + +### Python SDK 风格的完整示例: + +```python +import requests +import json + +class AzureBlobMCPClient: + """Azure Blob MCP Agent 客户端""" + + def __init__(self, base_url: str): + self.base_url = base_url.rstrip('/') + + def health_check(self): + """健康检查""" + response = requests.get(f"{self.base_url}/health") + return response.json() + + def get_tools(self): + """获取可用工具列表""" + response = requests.get(f"{self.base_url}/mcp/tools") + return response.json() + + def call_tool(self, tool_name: str, parameters: dict): + """调用工具""" + data = { + "tool_name": tool_name, + "parameters": parameters + } + response = requests.post(f"{self.base_url}/mcp/tool", json=data) + return response.json() + + def query(self, query: str, context: dict = None): + """自然语言查询""" + data = { + "query": query, + "context": context or {} + } + response = requests.post(f"{self.base_url}/mcp/query", json=data) + return response.json() + + # 便捷方法 + def list_containers(self): + """列出所有容器""" + return self.call_tool("list_containers", {}) + + def list_blobs(self, container_name: str): + """列出容器中的文件""" + return self.call_tool("list_blobs", {"container_name": container_name}) + + def upload_blob(self, container_name: str, blob_name: str, content: str): + """上传文件""" + return self.call_tool("upload_blob", { + "container_name": container_name, + "blob_name": blob_name, + "content": content + }) + + def download_blob(self, container_name: str, blob_name: str): + """下载文件""" + return self.call_tool("download_blob", { + "container_name": container_name, + "blob_name": blob_name + }) + + +# 使用示例 +client = AzureBlobMCPClient("http://localhost:8080") + +# 1. 健康检查 +print("Health:", client.health_check()) + +# 2. 获取工具列表 +print("Tools:", client.get_tools()) + +# 3. 列出容器 +containers = client.list_containers() +print("Containers:", containers) + +# 4. 列出文件 +files = client.list_blobs("documents") +print("Files:", files) + +# 5. 上传文件 +upload_result = client.upload_blob( + "documents", + "report.txt", + "This is my report content" +) +print("Upload:", upload_result) + +# 6. 下载文件 +download_result = client.download_blob("documents", "report.txt") +print("Download:", download_result) + +# 7. 自然语言查询 +query_result = client.query( + "统计 documents 容器中有多少个文件", + context={"user_id": "user123"} +) +print("Query:", query_result) +``` + +--- + +## MCP 协议集成示例 + +### 与 LangChain 集成: + +```python +from langchain.tools import Tool +import requests + +class MCPBlobTool: + """MCP Blob 工具包装器""" + + def __init__(self, base_url: str): + self.base_url = base_url + + def _call_mcp_tool(self, tool_name: str, **kwargs): + response = requests.post( + f"{self.base_url}/mcp/tool", + json={"tool_name": tool_name, "parameters": kwargs} + ) + return response.json() + + def list_containers(self): + return self._call_mcp_tool("list_containers") + + def list_blobs(self, container_name: str): + return self._call_mcp_tool("list_blobs", container_name=container_name) + +# 创建 LangChain 工具 +mcp_blob = MCPBlobTool("http://localhost:8080") + +tools = [ + Tool( + name="ListContainers", + func=mcp_blob.list_containers, + description="列出所有 Azure Blob 容器" + ), + Tool( + name="ListBlobs", + func=lambda x: mcp_blob.list_blobs(x), + description="列出指定容器中的所有文件。输入: 容器名称" + ) +] + +# 在 LangChain Agent 中使用 +from langchain.agents import initialize_agent +from langchain_openai import ChatOpenAI + +llm = ChatOpenAI(temperature=0) +agent = initialize_agent(tools, llm, agent="zero-shot-react-description", verbose=True) + +result = agent.run("列出所有容器,然后显示第一个容器中的文件") +print(result) +``` + +--- + +## 环境变量配置 + +```bash +# 服务配置 +export SERVICE_HOST="0.0.0.0" +export SERVICE_PORT="8080" +export POD_NAME="azure-blob-agent-mcp" +export TEMPLATE_TYPE="azure_blob_agent_mcp" +export AGENT_FRAMEWORK="mcp" + +# 模型配置 +export MODEL_PROVIDER="openai" +export MODEL_NAME="gpt-4" +export MODEL_API_KEY="sk-xxx" +export MODEL_ENDPOINT="https://api.openai.com/v1" + +# 工具配置 (JSON 格式) +export TOOLS_CONFIG='{ + "blob_storage": { + "enabled": true, + "default_container": "documents" + } +}' + +# 存储配置 +export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;..." +export STORAGE_ACCOUNT_NAME="myaccount" + +# 用户标识 +export USER_ID="default-user" +export TENANT_ID="tenant-001" +export NAMESPACE="ai-agents" + +# 启动服务 +python azure_blob_agent_mcp.py +``` + +--- + +## 注意事项 + +1. **工具发现**: 先调用 `/mcp/tools` 了解可用工具 +2. **参数验证**: 严格按照工具的 schema 传递参数 +3. **错误处理**: MCP 返回标准化的错误格式 +4. **上下文传递**: 通过 context 传递会话信息 +5. **异步支持**: 支持异步工具调用 +6. **工具组合**: 可以组合多个工具完成复杂任务 diff --git a/agent_templates/docs/CALLBACK_IMPLEMENTATION_SUMMARY.md b/agent_templates/docs/CALLBACK_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..9762d74 --- /dev/null +++ b/agent_templates/docs/CALLBACK_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,267 @@ +# Agent 回调功能实现总结 + +## 概述 + +根据 `/home/taiji/tools/agent-manager/plans/LiteLLM和AgentManager回调接口文档.md` 的要求,已为所有 agent 模板添加了运行时长回调功能。 + +## 修改内容 + +### 1. 新增工具模块 + +**文件**: `agent_callback_utils.py` + +提供两个核心类: + +- **AgentCallbackHandler**: 回调处理器,负责记录请求开始/结束时间、使用的工具、并发送回调到 Agent Manager +- **CallbackContextManager**: 上下文管理器,支持 `with` 语句自动处理回调的开始和结束 + +**关键功能**: +- 自动记录运行时长(Pod running time) +- 记录使用的工具列表 +- 向回调接口发送 POST 请求:`http://mcp-server:8002/api/v1/billing/agent-callback` +- 支持用户ID、请求ID追踪 + +### 2. API Key 传递方式改进 + +**之前**: 所有配置包括 API key 都从环境变量获取 + +**现在**: +- **API key**: 从用户请求中传入(每次请求携带) +- **其他配置**: 依然从环境变量获取(数据库连接信息、服务端口等) + +### 3. 修改的 Agent 文件 + +#### 3.1 search_agent.py + +**请求模型修改**: +```python +class SearchRequest(BaseModel): + query: str + llm_api_key: str # 新增:从请求传入 + user_id: Optional[str] # 新增:用于计费回调 + auto_configure: bool +``` + +**回调集成**: +- 使用 `CallbackContextManager` 自动处理回调 +- 记录使用的工具:`web_search`, `content_reader` +- 临时更新 API key 后执行搜索,完成后恢复原值 + +#### 3.2 jina_search_agent.py + +**请求模型修改**: +```python +class SearchRequest(BaseModel): + url: str + jina_api_key: str # 新增:从请求传入 + user_id: Optional[str] # 新增 + timeout: int +``` + +**回调集成**: +- 使用 `CallbackContextManager` 自动处理回调 +- 记录使用的工具:`jina_reader` +- 移除了全局 `JINA_API_KEY` 环境变量依赖 + +#### 3.3 mysql_agent.py + +**重大改动**: 从循环示例查询模式改为 FastAPI HTTP 服务 + +**请求模型**: +```python +class QueryRequest(BaseModel): + query: str + openai_api_key: str # 从请求传入 + user_id: Optional[str] # 用于计费回调 + model: str = "gpt-3.5-turbo" +``` + +**新增端点**: +- `GET /health` - 健康检查 +- `POST /query` - 执行SQL查询(带回调) +- `GET /` - 服务信息 + +**回调集成**: +- 使用 `CallbackContextManager` +- 记录使用的工具:`sql_database` + +#### 3.4 postgresql_agent.py + +**修改内容与 mysql_agent.py 类似** + +**请求模型**: +```python +class QueryRequest(BaseModel): + query: str + openai_api_key: str # 从请求传入 + user_id: Optional[str] # 用于计费回调 + model: str = "gpt-3.5-turbo" +``` + +**回调集成**: 同 MySQL Agent + +## 回调接口规范 + +根据文档,每次请求结束后自动发送以下回调: + +```json +{ + "agentName": "pod-name", + "userId": "user-123", + "podRunningTimeSeconds": 120, + "toolsUsed": ["web_search", "content_reader"], + "startTime": "2026-01-15T10:00:00Z", + "endTime": "2026-01-15T10:02:00Z", + "requestId": "search-1737801600" +} +``` + +**回调地址**: +- 默认: `http://mcp-server:8002/api/v1/billing/agent-callback` +- 可通过环境变量 `AGENT_CALLBACK_URL` 覆盖 + +## 环境变量配置 + +### 必需环境变量(所有 Agent) + +```bash +POD_NAME=agent-name # Pod 名称(用于回调) +USER_ID=default-user-id # 默认用户ID(可被请求中的user_id覆盖) +``` + +### 可选环境变量 + +```bash +AGENT_CALLBACK_URL=http://mcp-server:8002/api/v1/billing/agent-callback # 回调地址 +``` + +### Agent 特定环境变量 + +**Search Agent**: +```bash +SERPER_API_KEY=xxx # Serper API(从环境变量) +JINA_API_KEY=xxx # Jina API(从环境变量,可被请求覆盖) +LLM_BASE_URL=xxx # LLM API基础URL +``` + +**Jina Search Agent**: +```bash +SERVICE_HOST=0.0.0.0 +SERVICE_PORT=8080 +``` + +**MySQL Agent**: +```bash +MYSQL_HOST=localhost +MYSQL_PORT=3306 +MYSQL_USER=root +MYSQL_PASSWORD=password +MYSQL_DATABASE=test +SERVICE_HOST=0.0.0.0 +SERVICE_PORT=8080 +``` + +**PostgreSQL Agent**: +```bash +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=password +POSTGRES_DATABASE=postgres +SERVICE_HOST=0.0.0.0 +SERVICE_PORT=8080 +``` + +## 使用示例 + +### Search Agent + +```bash +curl -X POST http://agent-ip:8080/search \ + -H "Content-Type: application/json" \ + -d '{ + "query": "什么是人工智能?", + "llm_api_key": "sk-xxx", + "user_id": "user-123" + }' +``` + +### Jina Search Agent + +```bash +curl -X POST http://agent-ip:8080/search \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://example.com", + "jina_api_key": "jina_xxx", + "user_id": "user-123" + }' +``` + +### MySQL/PostgreSQL Agent + +```bash +curl -X POST http://agent-ip:8080/query \ + -H "Content-Type: application/json" \ + -d '{ + "query": "列出所有表", + "openai_api_key": "sk-xxx", + "user_id": "user-123", + "model": "gpt-3.5-turbo" + }' +``` + +## 回调流程 + +1. **请求开始**: + - 创建 `CallbackContextManager` 上下文 + - 记录开始时间 + - 设置 user_id 和 request_id + +2. **执行过程**: + - 临时更新 API key(如需要) + - 执行 Agent 逻辑 + - 记录使用的工具(通过 `ctx.add_tool()`) + +3. **请求结束**: + - 自动计算运行时长 + - 发送 POST 请求到回调接口 + - 包含所有必需字段(agentName, userId, podRunningTimeSeconds, toolsUsed, startTime, endTime, requestId) + +4. **错误处理**: + - 回调失败不影响主流程 + - 错误日志记录 + +## 注意事项 + +1. **API Key 安全**: API key 仅在请求期间临时使用,不持久化 +2. **回调可选**: 如果 `user_id` 未提供,回调处理器会跳过发送 +3. **幂等性**: 使用 `request_id` 确保回调幂等性 +4. **时区**: 所有时间戳使用 UTC 时区 +5. **兼容性**: 保持向后兼容,不强制要求 user_id + +## 依赖要求 + +所有 Agent 需要添加以下 Python 包依赖: + +```txt +fastapi +uvicorn +requests +pydantic +``` + +已有依赖的 Agent 无需额外安装。 + +## 测试建议 + +1. **单元测试**: 测试回调函数的正确性 +2. **集成测试**: 验证回调接口能正常接收数据 +3. **压力测试**: 确保回调不影响 Agent 性能 +4. **错误测试**: 验证回调失败时 Agent 仍能正常工作 + +--- + +**修改完成时间**: 2026-01-15 +**修改人**: GitHub Copilot +**版本**: v1.0 diff --git a/agent_templates/docs/DIRECTORY_STRUCTURE.md b/agent_templates/docs/DIRECTORY_STRUCTURE.md new file mode 100644 index 0000000..7c3fdb3 --- /dev/null +++ b/agent_templates/docs/DIRECTORY_STRUCTURE.md @@ -0,0 +1,194 @@ +# Agent Templates 目录结构 + +## 概述 + +`agent_templates` 目录已重新组织,按照功能分类到不同的文件夹中,便于管理和维护。 + +## 目录结构 + +``` +agent_templates/ +├── agents/ # 所有 Agent 实现 +│ ├── search_agent/ # 智能搜索 Agent +│ │ ├── search_agent.py +│ │ ├── search_agent_main.py +│ │ ├── search_agent.Dockerfile +│ │ └── search_agent/ # 搜索 Agent 核心模块 +│ ├── jina_search_agent/ # Jina 搜索 Agent +│ │ ├── jina_search_agent.py +│ │ └── jina_search_agent.Dockerfile +│ ├── azure_blob_agent/ # Azure Blob 存储 Agent +│ │ ├── azure_blob_agent.py +│ │ └── azure_blob_agent.Dockerfile +│ ├── azure_blob_agent_a2a/ # Azure Blob Agent (A2A 协议) +│ │ ├── azure_blob_agent_a2a.py +│ │ └── azure_blob_agent_a2a.Dockerfile +│ ├── azure_blob_agent_mcp/ # Azure Blob Agent (MCP 协议) +│ │ ├── azure_blob_agent_mcp.py +│ │ └── azure_blob_agent_mcp.Dockerfile +│ ├── postgresql_agent/ # PostgreSQL 数据库 Agent +│ │ ├── postgresql_agent.py +│ │ └── postgresql_agent.Dockerfile +│ ├── mysql_agent/ # MySQL 数据库 Agent +│ │ ├── mysql_agent.py +│ │ └── mysql_agent.Dockerfile +│ └── a2a_litellm_agent/ # A2A LiteLLM Agent +│ ├── a2a_server.py +│ ├── agent.py +│ ├── config.py +│ ├── main.py +│ ├── requirements.txt +│ ├── a2a_litellm_agent.Dockerfile +│ └── __init__.py +│ +├── common/ # 共享代码和工具 +│ ├── agent_callback_utils.py # Agent 回调工具 +│ ├── api_key_utils.py # API Key 配置工具 +│ ├── requirements_a2a.txt # A2A 协议依赖 +│ ├── requirements_mcp.txt # MCP 协议依赖 +│ └── Dockerfile.test # 测试 Dockerfile +│ +├── docs/ # 文档文件 +│ ├── A2A_LITELLM_AGENT_USAGE.md +│ ├── API_KEY_CONFIGURATION_SUMMARY.md +│ ├── AZURE_BLOB_AGENT_*.md +│ ├── SEARCH_AGENT_*.md +│ ├── JINA_SEARCH_AGENT_EXAMPLES.md +│ ├── MYSQL_AGENT_EXAMPLES.md +│ ├── POSTGRESQL_AGENT_EXAMPLES.md +│ └── ... +│ +├── scripts/ # 构建和工具脚本 +│ ├── build_*.sh # 各 Agent 的构建脚本 +│ ├── build_all_agents.sh # 批量构建脚本 +│ ├── rebuild_all.sh # 重建所有 Agent +│ └── check_image_content.sh # 镜像内容检查 +│ +└── tests/ # 测试文件 + ├── test_*.py # Python 测试文件 + ├── test_*.sh # Shell 测试脚本 + └── test_client.py # 测试客户端 +``` + +## 各目录说明 + +### agents/ + +包含所有 Agent 的实现代码。每个 Agent 都有自己的子目录,包含: +- Agent 主程序文件(`.py`) +- Dockerfile(`.Dockerfile`) +- 相关的配置和依赖文件 + +### common/ + +包含所有 Agent 共享的工具代码和依赖: +- `agent_callback_utils.py` - Agent 回调处理工具 +- `api_key_utils.py` - 统一的 API Key 配置管理 +- `requirements_*.txt` - 各协议的依赖文件 + +### docs/ + +包含所有文档文件: +- 使用指南(`*_USAGE.md`) +- 示例文档(`*_EXAMPLES.md`) +- 配置说明(`*_SUMMARY.md`) + +### scripts/ + +包含构建和工具脚本: +- 各 Agent 的独立构建脚本 +- 批量构建脚本 +- 工具脚本 + +### tests/ + +包含测试文件: +- Python 单元测试 +- 集成测试脚本 +- 测试客户端 + +## 使用说明 + +### 构建单个 Agent + +```bash +cd agent_templates +./scripts/build_search_agent.sh v1.0 +``` + +### 构建所有 Agent + +```bash +cd agent_templates +./scripts/build_all_agents.sh v1.0 +``` + +### 查看文档 + +```bash +# 查看搜索 Agent 使用指南 +cat docs/SEARCH_AGENT_USAGE.md + +# 查看 API Key 配置说明 +cat docs/API_KEY_CONFIGURATION_SUMMARY.md +``` + +## 导入路径说明 + +### 在 Agent 代码中导入共享工具 + +```python +# 在 Docker 容器中运行时,common/ 目录会被复制到 /app/common/ +from common.agent_callback_utils import AgentCallbackHandler +from common.api_key_utils import get_llm_api_key +``` + +### 在本地开发时 + +```python +# 需要将 common/ 目录添加到 Python 路径 +import sys +sys.path.insert(0, '../common') +from agent_callback_utils import AgentCallbackHandler +``` + +## Dockerfile 路径更新 + +所有构建脚本已更新为使用新的路径结构: + +```bash +# 旧路径 +-f search_agent.Dockerfile + +# 新路径 +-f agents/search_agent/search_agent.Dockerfile +``` + +## 迁移指南 + +如果您有现有的脚本或代码引用旧路径,请更新为: + +| 旧路径 | 新路径 | +|--------|--------| +| `search_agent.py` | `agents/search_agent/search_agent.py` | +| `search_agent.Dockerfile` | `agents/search_agent/search_agent.Dockerfile` | +| `agent_callback_utils.py` | `common/agent_callback_utils.py` | +| `api_key_utils.py` | `common/api_key_utils.py` | +| `*.md` | `docs/*.md` | +| `build_*.sh` | `scripts/build_*.sh` | +| `test_*.py` | `tests/test_*.py` | + +## 注意事项 + +1. **构建脚本**: 所有构建脚本已更新路径,可以直接使用 +2. **Dockerfile**: 需要确保 Dockerfile 中的 COPY 路径正确 +3. **导入路径**: 在容器中运行时,common/ 目录会被复制到正确位置 +4. **文档**: 所有文档已移动到 docs/ 目录 + +## 维护建议 + +1. **添加新 Agent**: 在 `agents/` 目录下创建新的子目录 +2. **共享代码**: 放在 `common/` 目录 +3. **文档**: 放在 `docs/` 目录 +4. **脚本**: 放在 `scripts/` 目录 +5. **测试**: 放在 `tests/` 目录 diff --git a/agent_templates/docs/EXAMPLES_INDEX.md b/agent_templates/docs/EXAMPLES_INDEX.md new file mode 100644 index 0000000..f75ac8f --- /dev/null +++ b/agent_templates/docs/EXAMPLES_INDEX.md @@ -0,0 +1,293 @@ +# Agent Templates 请求调用示例文档索引 + +本目录包含所有 Agent 的详细请求调用示例文档。 + +--- + +## 📚 文档列表 + +### 1. Azure Blob Storage Agents + +#### 1.1 Azure Blob Agent (标准版) +**文件**: [AZURE_BLOB_AGENT_EXAMPLES.md](./AZURE_BLOB_AGENT_EXAMPLES.md) +- **框架**: LangChain + LiteLLM +- **功能**: 智能 Azure Blob 存储管理 +- **端点**: + - `GET /health` - 健康检查 + - `POST /connect` - 连接存储 + - `POST /query` - 自然语言查询 +- **特点**: 支持容器管理、文件上传下载、智能搜索 + +#### 1.2 Azure Blob Agent A2A (Agent-to-Agent) +**文件**: [AZURE_BLOB_AGENT_A2A_EXAMPLES.md](./AZURE_BLOB_AGENT_A2A_EXAMPLES.md) +- **框架**: A2A (Agent-to-Agent) +- **功能**: 支持 Agent 间协作的存储管理 +- **端点**: + - `POST /a2a/register` - 注册协作 Agent + - `POST /a2a/message` - A2A 消息通信 + - `POST /a2a/query` - A2A 自然语言查询 + - `GET /a2a/agents` - 获取已注册的 Agents +- **特点**: Agent 间通信、协作任务、消息传递 + +#### 1.3 Azure Blob Agent MCP (Model Context Protocol) +**文件**: [AZURE_BLOB_AGENT_MCP_EXAMPLES.md](./AZURE_BLOB_AGENT_MCP_EXAMPLES.md) +- **框架**: MCP (Model Context Protocol) +- **功能**: 基于 MCP 协议的存储管理 +- **端点**: + - `GET /mcp/tools` - 获取工具列表 + - `POST /mcp/tool` - 调用 MCP 工具 + - `POST /mcp/query` - MCP 自然语言查询 +- **特点**: 标准化工具接口、与 LangChain 集成 + +--- + +### 2. Search Agents + +#### 2.1 Search Agent (智能搜索) +**文件**: [SEARCH_AGENT_EXAMPLES.md](./SEARCH_AGENT_EXAMPLES.md) +- **框架**: LangChain + Serper + Jina +- **功能**: 智能网络搜索和问答 +- **端点**: + - `GET /health` - 健康检查 + - `POST /configure` - 配置 Agent + - `POST /search` - 执行搜索 +- **特点**: Google 搜索、内容提取、智能答案生成 +- **需要**: Serper API Key, Jina API Key + +#### 2.2 Jina Search Agent (网页内容提取) +**文件**: [JINA_SEARCH_AGENT_EXAMPLES.md](./JINA_SEARCH_AGENT_EXAMPLES.md) +- **框架**: Jina Reader API +- **功能**: 网页内容提取和解析 +- **端点**: + - `GET /health` - 健康检查 + - `POST /search` - 提取网页内容 +- **特点**: 纯文本提取、内容清理、快速响应 +- **需要**: Jina API Key + +--- + +### 3. Database Agents + +#### 3.1 MySQL Agent +**文件**: [MYSQL_AGENT_EXAMPLES.md](./MYSQL_AGENT_EXAMPLES.md) +- **框架**: LangChain + OpenAI +- **功能**: MySQL 数据库自然语言查询 +- **端点**: + - `GET /health` - 健康检查 + - `POST /query` - 自然语言查询 +- **特点**: SQL 自动生成、智能查询、数据分析 +- **需要**: MySQL 数据库, OpenAI API Key + +#### 3.2 PostgreSQL Agent +**文件**: [POSTGRESQL_AGENT_EXAMPLES.md](./POSTGRESQL_AGENT_EXAMPLES.md) +- **框架**: LangChain + OpenAI +- **功能**: PostgreSQL 数据库自然语言查询 +- **端点**: + - `GET /health` - 健康检查 + - `POST /query` - 自然语言查询 +- **特点**: 支持 PostgreSQL 特性 (JSON, 数组, 全文搜索等) +- **需要**: PostgreSQL 数据库, OpenAI API Key + +--- + +## 🚀 快速开始 + +### 选择合适的 Agent + +**存储管理**: +- 基础使用 → `Azure Blob Agent` +- Agent 协作 → `Azure Blob Agent A2A` +- 工具集成 → `Azure Blob Agent MCP` + +**搜索功能**: +- 智能问答 → `Search Agent` +- 内容提取 → `Jina Search Agent` + +**数据库查询**: +- MySQL → `MySQL Agent` +- PostgreSQL → `PostgreSQL Agent` + +--- + +## 📖 文档结构 + +每个示例文档包含: + +1. **服务信息** - 基本配置和描述 +2. **API 端点** - 所有可用端点和参数 +3. **请求示例** - curl 和 Python 示例 +4. **响应示例** - 标准响应格式 +5. **完整使用流程** - 端到端示例 +6. **环境变量配置** - 必需的配置项 +7. **注意事项** - 最佳实践和限制 + +--- + +## 💡 使用建议 + +### 通用模式 + +所有 Agent 都遵循类似的模式: + +```python +import requests + +# 1. 健康检查 +health = requests.get("http://localhost:8080/health") +print(health.json()) + +# 2. 执行操作 (具体端点因 Agent 而异) +result = requests.post( + "http://localhost:8080/query", # 或其他端点 + json={ + "query": "your query here", + "api_key": "your-api-key", # API key 名称因 Agent 而异 + "user_id": "user123" + } +) +print(result.json()) +``` + +### API Key 管理 + +不同的 Agent 需要不同的 API Keys: + +| Agent | 需要的 API Keys | +|-------|----------------| +| Azure Blob Agent | LiteLLM API Key, Azure Connection String | +| Azure Blob Agent A2A | Model API Key, Azure Connection String | +| Azure Blob Agent MCP | Model API Key, Azure Connection String | +| Search Agent | LLM API Key, Serper API Key, Jina API Key | +| Jina Search Agent | Jina API Key | +| MySQL Agent | OpenAI API Key | +| PostgreSQL Agent | OpenAI API Key | + +--- + +## 🔧 环境配置示例 + +### Azure Blob Agent +```bash +export LITELLM_API_BASE="http://localhost:4000" +export LITELLM_MODEL="gpt-3.5-turbo" +export AZURE_STORAGE_CONNECTION_STRING="..." +python azure_blob_agent.py +``` + +### Search Agent +```bash +export LLM_BASE_URL="http://localhost:4000" +export SERPER_API_KEY="your-key" +export JINA_API_KEY="your-key" +python search_agent.py +``` + +### MySQL/PostgreSQL Agent +```bash +export MYSQL_HOST="localhost" +export MYSQL_DATABASE="mydb" +# 或 +export POSTGRES_HOST="localhost" +export POSTGRES_DATABASE="mydb" +python mysql_agent.py +``` + +--- + +## 🐳 Docker 部署 + +所有 Agent 都提供 Dockerfile: + +```bash +# 构建镜像 +docker build -f azure_blob_agent.Dockerfile -t azure-blob-agent . + +# 运行容器 +docker run -p 8080:8080 \ + -e LITELLM_API_BASE="http://host.docker.internal:4000" \ + -e AZURE_STORAGE_CONNECTION_STRING="..." \ + azure-blob-agent +``` + +--- + +## 📊 功能对比 + +| 功能 | Azure Blob | Azure A2A | Azure MCP | Search | Jina | MySQL | PostgreSQL | +|------|-----------|-----------|-----------|---------|------|-------|------------| +| 存储管理 | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | +| Agent 协作 | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| 工具标准化 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| 网络搜索 | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| 内容提取 | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | +| 数据库查询 | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | +| 自然语言 | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | + +--- + +## 🔗 相关文档 + +- [Agent Manager API 文档](../plans/API_DOCUMENTATION.md) +- [多框架支持指南](./MULTI_FRAMEWORK_GUIDE.md) +- [快速开始](./QUICKSTART.md) +- [快速参考](./QUICK_REFERENCE.md) + +--- + +## 📝 示例代码仓库 + +每个文档都包含完整的 Python 客户端示例,可直接使用: + +```python +# 标准模式 - 所有 Agent 通用 +from agent_templates.examples import create_client + +# 创建客户端 +client = create_client( + agent_type="azure_blob", # 或 "search", "mysql", "postgresql" + base_url="http://localhost:8080", + api_key="your-key" +) + +# 使用客户端 +result = client.query("your query") +print(result) +``` + +--- + +## ⚠️ 注意事项 + +1. **API Key 安全**: 不要在代码中硬编码 API Keys +2. **速率限制**: 注意各 API 服务的速率限制 +3. **错误处理**: 始终检查响应的 `success` 字段 +4. **超时设置**: 根据操作复杂度设置合理的超时时间 +5. **成本控制**: 监控 API 使用量以控制成本 +6. **并发限制**: 避免过多并发请求 + +--- + +## 🆘 获取帮助 + +遇到问题? + +1. 查看对应的示例文档 +2. 检查健康检查端点 (`GET /health`) +3. 查看服务日志 +4. 确认环境变量配置正确 +5. 验证 API Keys 有效性 + +--- + +## 📅 更新日志 + +- **2026-01-15**: 创建所有 Agent 的示例文档 + - Azure Blob Agent (标准版、A2A、MCP) + - Search Agent + - Jina Search Agent + - MySQL Agent + - PostgreSQL Agent + +--- + +**Happy Coding! 🎉** diff --git a/agent_templates/docs/JINA_SEARCH_AGENT_EXAMPLES.md b/agent_templates/docs/JINA_SEARCH_AGENT_EXAMPLES.md new file mode 100644 index 0000000..10114af --- /dev/null +++ b/agent_templates/docs/JINA_SEARCH_AGENT_EXAMPLES.md @@ -0,0 +1,503 @@ +# Jina Search Agent 请求调用示例 + +## 服务信息 +- **服务名称**: Jina Search Agent +- **版本**: 1.0.0 +- **功能**: 使用 Jina Reader API 获取网站内容 +- **默认端口**: 8080 + +## 概述 +Jina Search Agent 使用 Jina Reader API 提取和解析网页内容,返回纯文本格式的内容,适合用于内容分析、摘要生成等场景。 + +--- + +## API 端点 + +### 1. 健康检查 +**端点**: `GET /health` + +**请求示例** (curl): +```bash +curl http://localhost:8080/health +``` + +**响应示例**: +```json +{ + "status": "healthy", + "pod_name": "jina-search-agent", + "template_type": "jina_search_agent", + "jina_api_configured": true +} +``` + +--- + +### 2. 服务信息 +**端点**: `GET /` + +**请求示例** (curl): +```bash +curl http://localhost:8080/ +``` + +**响应示例**: +```json +{ + "service": "Jina Search Agent", + "version": "1.0.0", + "description": "使用Jina Reader API获取网站内容的AI Agent", + "pod_name": "jina-search-agent", + "template_type": "jina_search_agent", + "required_env": { + "JINA_API_KEY": { + "description": "Jina API密钥,从 https://jina.ai/ 获取", + "required": true, + "configured": true + } + }, + "optional_env": { + "SERVICE_PORT": { + "description": "HTTP服务端口", + "default": "8080" + } + } +} +``` + +--- + +### 3. 搜索/提取网页内容 +**端点**: `POST /search` + +使用 Jina Reader API 提取指定 URL 的网页内容。 + +**请求体**: +```json +{ + "url": "https://example.com", + "jina_api_key": "your-jina-api-key", + "user_id": "user123", + "timeout": 30 +} +``` + +**请求示例** (curl): +```bash +curl -X POST http://localhost:8080/search \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://python.org", + "jina_api_key": "jina_xxx", + "user_id": "user123" + }' +``` + +**请求示例** (Python): +```python +import requests + +search_data = { + "url": "https://www.python.org/", + "jina_api_key": "jina_xxx", + "user_id": "user123", + "timeout": 30 +} + +response = requests.post( + "http://localhost:8080/search", + json=search_data +) +result = response.json() + +if result["success"]: + print(f"URL: {result['url']}") + print(f"Content Type: {result['content_type']}") + print(f"Content Length: {len(result['content'])}") + print(f"\nContent Preview:\n{result['content'][:500]}...") +else: + print(f"Error: {result.get('error')}") +``` + +**响应示例**: +```json +{ + "url": "https://www.python.org/", + "content": "Welcome to Python.org\n\nPython is a programming language that lets you work quickly and integrate systems more effectively...\n\n# Latest News\n- Python 3.12 Released\n- PyCon 2026 Announced\n...", + "status_code": 200, + "content_type": "text/plain", + "success": true +} +``` + +--- + +## 完整使用示例 + +### Python 完整示例: + +```python +import requests +import json + +class JinaSearchClient: + """Jina Search Agent 客户端""" + + def __init__(self, base_url: str, jina_api_key: str): + self.base_url = base_url.rstrip('/') + self.jina_api_key = jina_api_key + + def health_check(self): + """健康检查""" + response = requests.get(f"{self.base_url}/health") + return response.json() + + def get_info(self): + """获取服务信息""" + response = requests.get(f"{self.base_url}/") + return response.json() + + def fetch_content(self, url: str, user_id: str = None, timeout: int = 30): + """提取网页内容""" + data = { + "url": url, + "jina_api_key": self.jina_api_key, + "user_id": user_id, + "timeout": timeout + } + response = requests.post(f"{self.base_url}/search", json=data) + return response.json() + + +# 使用示例 +client = JinaSearchClient( + base_url="http://localhost:8080", + jina_api_key="jina_xxx" +) + +# 1. 健康检查 +print("Health:", client.health_check()) + +# 2. 获取服务信息 +print("Info:", client.get_info()) + +# 3. 提取网页内容 +urls = [ + "https://www.python.org/", + "https://kubernetes.io/docs/", + "https://docs.docker.com/", + "https://github.com/", +] + +for url in urls: + print(f"\n{'='*60}") + print(f"Fetching: {url}") + print('='*60) + + result = client.fetch_content(url, user_id="user123") + + if result["success"]: + print(f"Status: {result['status_code']}") + print(f"Content Type: {result['content_type']}") + print(f"Content Length: {len(result['content'])} chars") + print(f"\nContent Preview:") + print(result['content'][:300]) + print("...") + else: + print(f"Error: Failed to fetch content") +``` + +--- + +### 批量内容提取: + +```python +import requests +import concurrent.futures +from typing import List, Dict + +def fetch_url(url: str, jina_api_key: str, user_id: str = None) -> Dict: + """提取单个 URL 的内容""" + try: + response = requests.post( + "http://localhost:8080/search", + json={ + "url": url, + "jina_api_key": jina_api_key, + "user_id": user_id, + "timeout": 30 + }, + timeout=60 + ) + result = response.json() + return { + "url": url, + "success": result.get("success", False), + "content": result.get("content", ""), + "error": None + } + except Exception as e: + return { + "url": url, + "success": False, + "content": "", + "error": str(e) + } + +# 批量提取 +urls = [ + "https://www.python.org/", + "https://www.docker.com/", + "https://kubernetes.io/", + "https://www.tensorflow.org/", + "https://pytorch.org/" +] + +jina_api_key = "jina_xxx" + +# 并行提取 +with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + futures = [ + executor.submit(fetch_url, url, jina_api_key, f"user-{i}") + for i, url in enumerate(urls) + ] + + results = [future.result() for future in concurrent.futures.as_completed(futures)] + +# 处理结果 +successful = [r for r in results if r["success"]] +failed = [r for r in results if not r["success"]] + +print(f"Successful: {len(successful)}/{len(urls)}") +print(f"Failed: {len(failed)}/{len(urls)}") + +for result in successful: + print(f"\n{result['url']}") + print(f"Content length: {len(result['content'])} chars") + print(f"Preview: {result['content'][:100]}...") +``` + +--- + +### 与 LLM 结合进行内容分析: + +```python +import requests +from langchain_openai import ChatOpenAI +from langchain.prompts import PromptTemplate + +class ContentAnalyzer: + """使用 Jina 提取内容并用 LLM 分析""" + + def __init__(self, jina_base_url: str, jina_api_key: str, openai_api_key: str): + self.jina_base_url = jina_base_url + self.jina_api_key = jina_api_key + self.llm = ChatOpenAI( + temperature=0, + model="gpt-3.5-turbo", + openai_api_key=openai_api_key + ) + + def fetch_content(self, url: str) -> str: + """使用 Jina 提取内容""" + response = requests.post( + f"{self.jina_base_url}/search", + json={ + "url": url, + "jina_api_key": self.jina_api_key + } + ) + result = response.json() + return result.get("content", "") if result.get("success") else "" + + def summarize(self, url: str) -> str: + """提取并摘要网页内容""" + content = self.fetch_content(url) + + if not content: + return "无法提取内容" + + prompt = PromptTemplate( + input_variables=["content"], + template="请用中文总结以下网页内容,保持简洁明了:\n\n{content}\n\n摘要:" + ) + + # 限制内容长度 + content = content[:4000] + + result = self.llm.invoke(prompt.format(content=content)) + return result.content + + def extract_key_points(self, url: str) -> List[str]: + """提取关键要点""" + content = self.fetch_content(url) + + if not content: + return [] + + prompt = PromptTemplate( + input_variables=["content"], + template="请从以下内容中提取5个关键要点,每个要点一行:\n\n{content}\n\n关键要点:" + ) + + content = content[:4000] + result = self.llm.invoke(prompt.format(content=content)) + + # 解析要点 + points = [line.strip() for line in result.content.split('\n') if line.strip()] + return points + + +# 使用示例 +analyzer = ContentAnalyzer( + jina_base_url="http://localhost:8080", + jina_api_key="jina_xxx", + openai_api_key="sk-xxx" +) + +# 摘要网页 +url = "https://www.python.org/about/" +summary = analyzer.summarize(url) +print(f"URL: {url}") +print(f"Summary: {summary}") + +# 提取关键要点 +key_points = analyzer.extract_key_points(url) +print("\nKey Points:") +for i, point in enumerate(key_points, 1): + print(f"{i}. {point}") +``` + +--- + +### 监控和内容变更检测: + +```python +import requests +import time +import hashlib +from datetime import datetime + +class ContentMonitor: + """监控网页内容变化""" + + def __init__(self, jina_base_url: str, jina_api_key: str): + self.jina_base_url = jina_base_url + self.jina_api_key = jina_api_key + self.cache = {} + + def get_content_hash(self, url: str) -> str: + """获取内容哈希值""" + response = requests.post( + f"{self.jina_base_url}/search", + json={ + "url": url, + "jina_api_key": self.jina_api_key + } + ) + result = response.json() + + if result.get("success"): + content = result["content"] + return hashlib.md5(content.encode()).hexdigest() + return "" + + def check_updates(self, urls: List[str]) -> Dict: + """检查 URL 列表是否有更新""" + updates = {} + + for url in urls: + current_hash = self.get_content_hash(url) + previous_hash = self.cache.get(url) + + if previous_hash is None: + updates[url] = {"status": "new", "hash": current_hash} + elif current_hash != previous_hash: + updates[url] = {"status": "updated", "hash": current_hash} + else: + updates[url] = {"status": "unchanged", "hash": current_hash} + + self.cache[url] = current_hash + + return updates + + +# 使用示例 +monitor = ContentMonitor( + jina_base_url="http://localhost:8080", + jina_api_key="jina_xxx" +) + +urls_to_monitor = [ + "https://www.python.org/downloads/", + "https://kubernetes.io/blog/", + "https://github.com/trending" +] + +# 定期检查更新 +while True: + print(f"\n[{datetime.now()}] Checking for updates...") + updates = monitor.check_updates(urls_to_monitor) + + for url, info in updates.items(): + if info["status"] == "updated": + print(f"⚠️ UPDATED: {url}") + elif info["status"] == "new": + print(f"🆕 NEW: {url}") + else: + print(f"✓ No change: {url}") + + # 每 5 分钟检查一次 + time.sleep(300) +``` + +--- + +## 环境变量配置 + +```bash +# 服务配置 +export SERVICE_HOST="0.0.0.0" +export SERVICE_PORT="8080" +export POD_NAME="jina-search-agent" +export TEMPLATE_TYPE="jina_search_agent" + +# Jina API (从请求传入,也可以预配置) +# export JINA_API_KEY="jina_xxx" + +# 启动服务 +python jina_search_agent.py +``` + +--- + +## 获取 Jina API Key + +1. 访问: https://jina.ai/ +2. 注册账号 +3. 在控制台获取 API key +4. 免费套餐: 1,000 次请求/天 + +--- + +## 支持的网站类型 + +Jina Reader API 支持多种网站: +- 新闻网站 +- 博客文章 +- 文档网站 +- GitHub 页面 +- 维基百科 +- 论文网站 (arXiv, etc.) + +--- + +## 注意事项 + +1. **API Key**: 从请求中传入,保证安全性 +2. **速率限制**: 注意 Jina API 的速率限制 +3. **超时设置**: 大型网页可能需要更长时间 +4. **内容格式**: 返回纯文本格式,已清理 HTML +5. **用户ID**: 可选,用于计费回调 +6. **错误处理**: 检查 success 字段确认是否成功 +7. **并发限制**: 建议最多 3-5 个并发请求 diff --git a/agent_templates/docs/MYSQL_AGENT_EXAMPLES.md b/agent_templates/docs/MYSQL_AGENT_EXAMPLES.md new file mode 100644 index 0000000..6fab29d --- /dev/null +++ b/agent_templates/docs/MYSQL_AGENT_EXAMPLES.md @@ -0,0 +1,537 @@ +# MySQL Agent 请求调用示例 + +## 服务信息 +- **服务名称**: MySQL AI Agent +- **版本**: 1.0.0 +- **框架**: LangChain + OpenAI +- **默认端口**: 8080 + +## 概述 +MySQL AI Agent 使用 LangChain 和自然语言处理技术,允许用户使用自然语言查询 MySQL 数据库。 + +--- + +## API 端点 + +### 1. 健康检查 +**端点**: `GET /health` + +**请求示例** (curl): +```bash +curl http://localhost:8080/health +``` + +**响应示例**: +```json +{ + "status": "healthy", + "pod_name": "mysql-agent", + "template_type": "mysql_agent", + "database_connected": true, + "database_info": "localhost:3306/mydb" +} +``` + +--- + +### 2. 服务信息 +**端点**: `GET /` + +**请求示例** (curl): +```bash +curl http://localhost:8080/ +``` + +**响应示例**: +```json +{ + "name": "MySQL AI Agent", + "version": "1.0.0", + "database": "localhost:3306/mydb", + "endpoints": { + "health": "/health", + "query": "/query" + } +} +``` + +--- + +### 3. 自然语言查询 +**端点**: `POST /query` + +使用自然语言查询 MySQL 数据库。 + +**请求体**: +```json +{ + "query": "显示所有用户", + "openai_api_key": "sk-xxx", + "user_id": "user123", + "model": "gpt-3.5-turbo" +} +``` + +**请求示例** (curl): +```bash +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{ + "query": "有多少个用户?", + "openai_api_key": "sk-xxx", + "user_id": "user123" + }' +``` + +**请求示例** (Python): +```python +import requests + +query_data = { + "query": "显示年龄大于30的所有用户", + "openai_api_key": "sk-xxx", + "user_id": "user123", + "model": "gpt-3.5-turbo" +} + +response = requests.post( + "http://localhost:8080/query", + json=query_data +) +result = response.json() + +print(f"Query: {result['query']}") +print(f"Result: {result['result']}") +print(f"Success: {result['success']}") +print(f"Timestamp: {result['timestamp']}") +``` + +**响应示例**: +```json +{ + "query": "有多少个用户?", + "result": "数据库中有 150 个用户", + "success": true, + "timestamp": "2026-01-15T10:30:00.000Z" +} +``` + +--- + +## 查询示例 + +### 基础查询: +```python +queries = [ + "显示所有用户", + "有多少个用户?", + "列出所有表", + "显示 users 表的结构", + "查看最近注册的 10 个用户" +] +``` + +### 统计查询: +```python +queries = [ + "每个部门有多少员工?", + "统计每个城市的用户数量", + "计算订单总金额", + "找出销售额最高的产品", + "显示月度销售趋势" +] +``` + +### 条件查询: +```python +queries = [ + "显示年龄大于30的用户", + "查找北京的所有客户", + "列出未支付的订单", + "显示价格在100到500之间的产品", + "找出最近一周的订单" +] +``` + +### 关联查询: +```python +queries = [ + "显示每个用户的订单数量", + "列出购买了特定产品的用户", + "显示每个部门的平均工资", + "查找有订单但未支付的用户" +] +``` + +--- + +## 完整使用示例 + +### Python 客户端: + +```python +import requests +from typing import Optional, Dict, Any + +class MySQLAgentClient: + """MySQL Agent 客户端""" + + def __init__(self, base_url: str, openai_api_key: str): + self.base_url = base_url.rstrip('/') + self.openai_api_key = openai_api_key + + def health_check(self) -> Dict[str, Any]: + """健康检查""" + response = requests.get(f"{self.base_url}/health") + return response.json() + + def get_info(self) -> Dict[str, Any]: + """获取服务信息""" + response = requests.get(f"{self.base_url}/") + return response.json() + + def query( + self, + query: str, + user_id: Optional[str] = None, + model: str = "gpt-3.5-turbo" + ) -> Dict[str, Any]: + """执行自然语言查询""" + data = { + "query": query, + "openai_api_key": self.openai_api_key, + "user_id": user_id, + "model": model + } + response = requests.post(f"{self.base_url}/query", json=data) + return response.json() + + +# 使用示例 +client = MySQLAgentClient( + base_url="http://localhost:8080", + openai_api_key="sk-xxx" +) + +# 1. 健康检查 +health = client.health_check() +print(f"Database Connected: {health['database_connected']}") +print(f"Database Info: {health['database_info']}") + +# 2. 执行查询 +queries = [ + "显示所有表", + "users 表有多少条记录?", + "显示最近注册的5个用户", + "统计每个城市的用户数量", + "找出年龄最大的用户" +] + +for query in queries: + print(f"\n{'='*60}") + print(f"Query: {query}") + print('='*60) + + result = client.query(query, user_id="user123") + + if result['success']: + print(f"Result:\n{result['result']}") + else: + print(f"Error: Query failed") +``` + +--- + +### 交互式查询工具: + +```python +import requests +from prompt_toolkit import prompt +from prompt_toolkit.history import InMemoryHistory +from rich.console import Console +from rich.table import Table + +class InteractiveMySQLClient: + """交互式 MySQL 查询客户端""" + + def __init__(self, base_url: str, openai_api_key: str): + self.base_url = base_url + self.openai_api_key = openai_api_key + self.console = Console() + self.history = InMemoryHistory() + + def query(self, query_text: str) -> Dict: + """执行查询""" + response = requests.post( + f"{self.base_url}/query", + json={ + "query": query_text, + "openai_api_key": self.openai_api_key + } + ) + return response.json() + + def display_result(self, result: Dict): + """显示查询结果""" + if result['success']: + self.console.print(f"[green]✓ Success[/green]") + self.console.print(f"\n{result['result']}\n") + else: + self.console.print(f"[red]✗ Failed[/red]") + + def run(self): + """运行交互式会话""" + self.console.print("[bold blue]MySQL AI Agent - Interactive Client[/bold blue]") + self.console.print("Type 'exit' or 'quit' to end session\n") + + while True: + try: + # 获取用户输入 + query_text = prompt( + "mysql> ", + history=self.history + ) + + # 检查退出命令 + if query_text.lower() in ['exit', 'quit']: + break + + if not query_text.strip(): + continue + + # 执行查询 + result = self.query(query_text) + self.display_result(result) + + except KeyboardInterrupt: + continue + except EOFError: + break + + self.console.print("\n[yellow]Goodbye![/yellow]") + + +# 使用交互式客户端 +if __name__ == "__main__": + client = InteractiveMySQLClient( + base_url="http://localhost:8080", + openai_api_key="sk-xxx" + ) + client.run() +``` + +--- + +### 数据分析工具: + +```python +import requests +import pandas as pd +import matplotlib.pyplot as plt +from typing import List, Dict + +class MySQLDataAnalyzer: + """MySQL 数据分析工具""" + + def __init__(self, base_url: str, openai_api_key: str): + self.base_url = base_url + self.openai_api_key = openai_api_key + + def query(self, query: str) -> str: + """执行查询""" + response = requests.post( + f"{self.base_url}/query", + json={ + "query": query, + "openai_api_key": self.openai_api_key + } + ) + result = response.json() + return result.get('result', '') if result.get('success') else '' + + def get_statistics(self, table: str, column: str) -> Dict: + """获取列的统计信息""" + queries = { + "count": f"{table} 表的 {column} 列有多少条记录?", + "avg": f"{table} 表的 {column} 列的平均值是多少?", + "min": f"{table} 表的 {column} 列的最小值是多少?", + "max": f"{table} 表的 {column} 列的最大值是多少?" + } + + stats = {} + for stat_name, query in queries.items(): + result = self.query(query) + stats[stat_name] = result + + return stats + + def get_distribution(self, table: str, column: str) -> Dict: + """获取数据分布""" + query = f"统计 {table} 表中 {column} 列的值分布" + result = self.query(query) + return {"distribution": result} + + +# 使用示例 +analyzer = MySQLDataAnalyzer( + base_url="http://localhost:8080", + openai_api_key="sk-xxx" +) + +# 获取统计信息 +stats = analyzer.get_statistics("users", "age") +print("Statistics:") +for stat, value in stats.items(): + print(f" {stat}: {value}") + +# 获取分布 +distribution = analyzer.get_distribution("users", "city") +print(f"\nDistribution: {distribution}") +``` + +--- + +### 批量查询和导出: + +```python +import requests +import csv +from datetime import datetime + +class MySQLBatchExporter: + """批量查询和导出工具""" + + def __init__(self, base_url: str, openai_api_key: str): + self.base_url = base_url + self.openai_api_key = openai_api_key + + def execute_queries(self, queries: List[str]) -> List[Dict]: + """批量执行查询""" + results = [] + + for query in queries: + response = requests.post( + f"{self.base_url}/query", + json={ + "query": query, + "openai_api_key": self.openai_api_key + } + ) + result = response.json() + results.append({ + "query": query, + "result": result.get('result', ''), + "success": result.get('success', False), + "timestamp": result.get('timestamp', '') + }) + + return results + + def export_to_csv(self, results: List[Dict], filename: str): + """导出结果到 CSV""" + with open(filename, 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=['query', 'result', 'success', 'timestamp']) + writer.writeheader() + writer.writerows(results) + + print(f"Results exported to {filename}") + + +# 使用示例 +exporter = MySQLBatchExporter( + base_url="http://localhost:8080", + openai_api_key="sk-xxx" +) + +# 批量查询 +queries = [ + "统计总用户数", + "统计每个城市的用户数", + "显示最近一周的注册用户数", + "计算平均年龄", + "显示活跃用户占比" +] + +results = exporter.execute_queries(queries) + +# 导出结果 +timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") +exporter.export_to_csv(results, f"mysql_queries_{timestamp}.csv") + +# 打印摘要 +successful = sum(1 for r in results if r['success']) +print(f"\nSummary: {successful}/{len(queries)} queries successful") +``` + +--- + +## 环境变量配置 + +```bash +# 服务配置 +export SERVICE_HOST="0.0.0.0" +export SERVICE_PORT="8080" +export POD_NAME="mysql-agent" +export TEMPLATE_TYPE="mysql_agent" + +# MySQL 数据库配置 +export MYSQL_HOST="localhost" +export MYSQL_PORT="3306" +export MYSQL_USER="root" +export MYSQL_PASSWORD="your-password" +export MYSQL_DATABASE="mydb" + +# 启动服务 +python mysql_agent.py +``` + +--- + +## Docker Compose 示例 + +```yaml +version: '3.8' + +services: + mysql: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: testdb + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + + mysql-agent: + build: + context: . + dockerfile: mysql_agent.Dockerfile + environment: + MYSQL_HOST: mysql + MYSQL_PORT: 3306 + MYSQL_USER: root + MYSQL_PASSWORD: rootpassword + MYSQL_DATABASE: testdb + SERVICE_PORT: 8080 + ports: + - "8080:8080" + depends_on: + - mysql + +volumes: + mysql_data: +``` + +--- + +## 注意事项 + +1. **API Key**: OpenAI API key 从请求传入,确保安全 +2. **数据库连接**: 需要正确配置数据库连接参数 +3. **权限控制**: 建议使用只读用户进行查询 +4. **查询限制**: 设置合理的查询超时和结果限制 +5. **错误处理**: 检查 success 字段确认查询是否成功 +6. **SQL注入**: Agent 会自动处理,但仍需注意安全 +7. **成本控制**: 监控 OpenAI API 使用量 +8. **模型选择**: gpt-4 更准确但成本更高,gpt-3.5-turbo 更经济 diff --git a/agent_templates/docs/POSTGRESQL_AGENT_EXAMPLES.md b/agent_templates/docs/POSTGRESQL_AGENT_EXAMPLES.md new file mode 100644 index 0000000..9d607d5 --- /dev/null +++ b/agent_templates/docs/POSTGRESQL_AGENT_EXAMPLES.md @@ -0,0 +1,610 @@ +# PostgreSQL Agent 请求调用示例 + +## 服务信息 +- **服务名称**: PostgreSQL AI Agent +- **版本**: 1.0.0 +- **框架**: LangChain + OpenAI +- **默认端口**: 8080 + +## 概述 +PostgreSQL AI Agent 使用 LangChain 和自然语言处理技术,允许用户使用自然语言查询 PostgreSQL 数据库。 + +--- + +## API 端点 + +### 1. 健康检查 +**端点**: `GET /health` + +**请求示例** (curl): +```bash +curl http://localhost:8080/health +``` + +**响应示例**: +```json +{ + "status": "healthy", + "pod_name": "postgresql-agent", + "template_type": "postgresql_agent", + "database_connected": true, + "database_info": "localhost:5432/mydb" +} +``` + +--- + +### 2. 服务信息 +**端点**: `GET /` + +**请求示例** (curl): +```bash +curl http://localhost:8080/ +``` + +**响应示例**: +```json +{ + "name": "PostgreSQL AI Agent", + "version": "1.0.0", + "database": "localhost:5432/postgres", + "endpoints": { + "health": "/health", + "query": "/query" + } +} +``` + +--- + +### 3. 自然语言查询 +**端点**: `POST /query` + +使用自然语言查询 PostgreSQL 数据库。 + +**请求体**: +```json +{ + "query": "显示所有用户", + "openai_api_key": "sk-xxx", + "user_id": "user123", + "model": "gpt-3.5-turbo" +} +``` + +**请求示例** (curl): +```bash +curl -X POST http://localhost:8080/query \ + -H "Content-Type: application/json" \ + -d '{ + "query": "数据库中有多少个表?", + "openai_api_key": "sk-xxx", + "user_id": "user123" + }' +``` + +**请求示例** (Python): +```python +import requests + +query_data = { + "query": "显示 users 表中年龄大于25的所有用户", + "openai_api_key": "sk-xxx", + "user_id": "user123", + "model": "gpt-3.5-turbo" +} + +response = requests.post( + "http://localhost:8080/query", + json=query_data +) +result = response.json() + +print(f"Query: {result['query']}") +print(f"Result: {result['result']}") +print(f"Success: {result['success']}") +print(f"Timestamp: {result['timestamp']}") +``` + +**响应示例**: +```json +{ + "query": "数据库中有多少个表?", + "result": "数据库中有 12 个表", + "success": true, + "timestamp": "2026-01-15T10:30:00.000Z" +} +``` + +--- + +## 查询示例 + +### 基础查询: +```python +queries = [ + "显示所有表", + "列出所有schema", + "显示 users 表的结构", + "users 表有多少条记录?", + "显示最近创建的10条记录" +] +``` + +### PostgreSQL 特定功能: +```python +queries = [ + "显示所有视图", + "列出所有索引", + "显示表的大小", + "查看数据库的大小", + "显示所有触发器", + "列出所有存储过程", + "显示表的统计信息" +] +``` + +### 统计查询: +```python +queries = [ + "统计每个部门的员工数量", + "计算订单的总金额", + "显示每月的销售额", + "找出销量最高的产品", + "计算用户的平均年龄" +] +``` + +### 条件查询: +```python +queries = [ + "显示状态为活跃的用户", + "查找创建时间在最近一周的订单", + "列出价格高于1000的产品", + "显示评分大于4.5的商品", + "查找北京地区的所有客户" +] +``` + +### 关联查询: +```python +queries = [ + "显示每个用户的订单数量", + "列出有订单的用户", + "显示每个类别的产品数量", + "查找购买了特定产品的用户", + "统计每个城市的订单总额" +] +``` + +### JSON 查询 (PostgreSQL 特性): +```python +queries = [ + "从 users 表的 metadata JSON 字段中提取 age", + "查找 metadata 包含特定键的记录", + "统计 JSON 数组的长度" +] +``` + +--- + +## 完整使用示例 + +### Python 客户端: + +```python +import requests +from typing import Optional, Dict, Any, List + +class PostgreSQLAgentClient: + """PostgreSQL Agent 客户端""" + + def __init__(self, base_url: str, openai_api_key: str): + self.base_url = base_url.rstrip('/') + self.openai_api_key = openai_api_key + + def health_check(self) -> Dict[str, Any]: + """健康检查""" + response = requests.get(f"{self.base_url}/health") + return response.json() + + def get_info(self) -> Dict[str, Any]: + """获取服务信息""" + response = requests.get(f"{self.base_url}/") + return response.json() + + def query( + self, + query: str, + user_id: Optional[str] = None, + model: str = "gpt-3.5-turbo" + ) -> Dict[str, Any]: + """执行自然语言查询""" + data = { + "query": query, + "openai_api_key": self.openai_api_key, + "user_id": user_id, + "model": model + } + response = requests.post(f"{self.base_url}/query", json=data) + return response.json() + + def batch_query(self, queries: List[str], user_id: Optional[str] = None) -> List[Dict]: + """批量查询""" + results = [] + for q in queries: + result = self.query(q, user_id) + results.append(result) + return results + + +# 使用示例 +client = PostgreSQLAgentClient( + base_url="http://localhost:8080", + openai_api_key="sk-xxx" +) + +# 1. 健康检查 +health = client.health_check() +print(f"Status: {health['status']}") +print(f"Database: {health['database_info']}") +print(f"Connected: {health['database_connected']}\n") + +# 2. 单个查询 +result = client.query("显示所有表") +print(f"Query: {result['query']}") +print(f"Result: {result['result']}\n") + +# 3. 批量查询 +queries = [ + "数据库中有多少个表?", + "users 表有多少条记录?", + "显示 users 表的前5条记录", + "统计每个城市的用户数量" +] + +print("Batch Queries:") +results = client.batch_query(queries, user_id="user123") +for i, result in enumerate(results, 1): + print(f"\n{i}. {result['query']}") + if result['success']: + print(f" {result['result']}") + else: + print(f" Error: Failed to execute query") +``` + +--- + +### 数据库监控工具: + +```python +import requests +import time +from datetime import datetime +from rich.console import Console +from rich.table import Table + +class PostgreSQLMonitor: + """PostgreSQL 数据库监控工具""" + + def __init__(self, base_url: str, openai_api_key: str): + self.base_url = base_url + self.openai_api_key = openai_api_key + self.console = Console() + + def query(self, query: str) -> str: + """执行查询""" + response = requests.post( + f"{self.base_url}/query", + json={ + "query": query, + "openai_api_key": self.openai_api_key + } + ) + result = response.json() + return result.get('result', '') if result.get('success') else 'N/A' + + def get_database_stats(self) -> Dict: + """获取数据库统计信息""" + stats = { + "database_size": self.query("数据库的大小是多少?"), + "table_count": self.query("有多少个表?"), + "connection_count": self.query("当前有多少个数据库连接?"), + "cache_hit_ratio": self.query("缓存命中率是多少?") + } + return stats + + def display_stats(self, stats: Dict): + """显示统计信息""" + table = Table(title="PostgreSQL Database Statistics") + table.add_column("Metric", style="cyan") + table.add_column("Value", style="green") + + for metric, value in stats.items(): + table.add_row(metric.replace('_', ' ').title(), str(value)) + + self.console.print(table) + + def monitor(self, interval: int = 60): + """持续监控""" + self.console.print("[bold blue]PostgreSQL Monitor Started[/bold blue]") + self.console.print(f"Refresh interval: {interval} seconds\n") + + try: + while True: + self.console.clear() + self.console.print(f"[yellow]Last Update: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}[/yellow]\n") + + stats = self.get_database_stats() + self.display_stats(stats) + + time.sleep(interval) + except KeyboardInterrupt: + self.console.print("\n[yellow]Monitoring stopped[/yellow]") + + +# 使用示例 +monitor = PostgreSQLMonitor( + base_url="http://localhost:8080", + openai_api_key="sk-xxx" +) + +# 获取一次统计信息 +stats = monitor.get_database_stats() +monitor.display_stats(stats) + +# 或者持续监控 (每60秒刷新) +# monitor.monitor(interval=60) +``` + +--- + +### 数据迁移辅助工具: + +```python +import requests +from typing import List, Dict + +class PostgreSQLMigrationHelper: + """PostgreSQL 数据迁移辅助工具""" + + def __init__(self, base_url: str, openai_api_key: str): + self.base_url = base_url + self.openai_api_key = openai_api_key + + def query(self, query: str) -> str: + """执行查询""" + response = requests.post( + f"{self.base_url}/query", + json={ + "query": query, + "openai_api_key": self.openai_api_key + } + ) + result = response.json() + return result.get('result', '') if result.get('success') else '' + + def get_table_schema(self, table_name: str) -> str: + """获取表结构""" + return self.query(f"显示 {table_name} 表的详细结构") + + def get_all_tables(self) -> str: + """获取所有表名""" + return self.query("列出所有表名") + + def get_table_constraints(self, table_name: str) -> str: + """获取表约束""" + return self.query(f"显示 {table_name} 表的所有约束") + + def get_table_indexes(self, table_name: str) -> str: + """获取表索引""" + return self.query(f"显示 {table_name} 表的所有索引") + + def get_foreign_keys(self, table_name: str) -> str: + """获取外键关系""" + return self.query(f"显示 {table_name} 表的外键关系") + + def generate_migration_report(self, table_name: str) -> Dict: + """生成迁移报告""" + return { + "table": table_name, + "schema": self.get_table_schema(table_name), + "constraints": self.get_table_constraints(table_name), + "indexes": self.get_table_indexes(table_name), + "foreign_keys": self.get_foreign_keys(table_name) + } + + +# 使用示例 +helper = PostgreSQLMigrationHelper( + base_url="http://localhost:8080", + openai_api_key="sk-xxx" +) + +# 获取所有表 +tables = helper.get_all_tables() +print(f"All Tables:\n{tables}\n") + +# 生成特定表的迁移报告 +table_name = "users" +report = helper.generate_migration_report(table_name) + +print(f"Migration Report for '{table_name}':") +print(f"\nSchema:\n{report['schema']}") +print(f"\nConstraints:\n{report['constraints']}") +print(f"\nIndexes:\n{report['indexes']}") +print(f"\nForeign Keys:\n{report['foreign_keys']}") +``` + +--- + +### 性能分析工具: + +```python +import requests +from typing import List, Dict +import pandas as pd + +class PostgreSQLPerformanceAnalyzer: + """PostgreSQL 性能分析工具""" + + def __init__(self, base_url: str, openai_api_key: str): + self.base_url = base_url + self.openai_api_key = openai_api_key + + def query(self, query: str) -> str: + """执行查询""" + response = requests.post( + f"{self.base_url}/query", + json={ + "query": query, + "openai_api_key": self.openai_api_key + } + ) + result = response.json() + return result.get('result', '') if result.get('success') else '' + + def get_slow_queries(self) -> str: + """获取慢查询""" + return self.query("显示最慢的10个查询") + + def get_table_sizes(self) -> str: + """获取表大小""" + return self.query("显示所有表的大小,按大小降序排列") + + def get_index_usage(self) -> str: + """获取索引使用情况""" + return self.query("显示索引使用统计") + + def get_cache_stats(self) -> str: + """获取缓存统计""" + return self.query("显示缓存命中率统计") + + def get_connection_stats(self) -> str: + """获取连接统计""" + return self.query("显示数据库连接统计信息") + + def analyze_table(self, table_name: str) -> str: + """分析表性能""" + return self.query(f"分析 {table_name} 表的性能") + + +# 使用示例 +analyzer = PostgreSQLPerformanceAnalyzer( + base_url="http://localhost:8080", + openai_api_key="sk-xxx" +) + +print("=== Performance Analysis ===\n") + +# 1. 慢查询 +print("Slow Queries:") +print(analyzer.get_slow_queries()) +print() + +# 2. 表大小 +print("Table Sizes:") +print(analyzer.get_table_sizes()) +print() + +# 3. 索引使用 +print("Index Usage:") +print(analyzer.get_index_usage()) +print() + +# 4. 缓存统计 +print("Cache Statistics:") +print(analyzer.get_cache_stats()) +print() + +# 5. 分析特定表 +print("Analyze 'users' table:") +print(analyzer.analyze_table("users")) +``` + +--- + +## 环境变量配置 + +```bash +# 服务配置 +export SERVICE_HOST="0.0.0.0" +export SERVICE_PORT="8080" +export POD_NAME="postgresql-agent" +export TEMPLATE_TYPE="postgresql_agent" + +# PostgreSQL 数据库配置 +export POSTGRES_HOST="localhost" +export POSTGRES_PORT="5432" +export POSTGRES_USER="postgres" +export POSTGRES_PASSWORD="your-password" +export POSTGRES_DATABASE="mydb" + +# 启动服务 +python postgresql_agent.py +``` + +--- + +## Docker Compose 示例 + +```yaml +version: '3.8' + +services: + postgres: + image: postgres:16 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: testdb + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + postgresql-agent: + build: + context: . + dockerfile: postgresql_agent.Dockerfile + environment: + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DATABASE: testdb + SERVICE_PORT: 8080 + ports: + - "8080:8080" + depends_on: + - postgres + +volumes: + postgres_data: +``` + +--- + +## PostgreSQL 特性支持 + +Agent 支持 PostgreSQL 的特殊功能: +- ✅ JSON/JSONB 查询 +- ✅ 数组类型 +- ✅ 全文搜索 +- ✅ 窗口函数 +- ✅ CTEs (Common Table Expressions) +- ✅ 视图和物化视图 +- ✅ 触发器和存储过程 +- ✅ 分区表 + +--- + +## 注意事项 + +1. **API Key**: OpenAI API key 从请求传入,确保安全 +2. **数据库连接**: 需要正确配置数据库连接参数 +3. **权限控制**: 建议使用只读用户进行查询 +4. **查询限制**: 设置合理的查询超时和结果限制 +5. **错误处理**: 检查 success 字段确认查询是否成功 +6. **PostgreSQL 版本**: 支持 PostgreSQL 12+ +7. **成本控制**: 监控 OpenAI API 使用量 +8. **模型选择**: gpt-4 更准确但成本更高 diff --git a/agent_templates/docs/PUSH_SUMMARY.md b/agent_templates/docs/PUSH_SUMMARY.md new file mode 100644 index 0000000..f033d6c --- /dev/null +++ b/agent_templates/docs/PUSH_SUMMARY.md @@ -0,0 +1,165 @@ +# Agent 镜像推送总结 + +## 推送时间 +2026-01-15 + +## 推送到的 ACR +agnettaiji.azurecr.io + +## 已推送的镜像 + +### 1. Search Agent +- **镜像**: `agnettaiji.azurecr.io/ai-agents/search-agent:latest` +- **平台**: linux/arm64 +- **状态**: ✅ 已推送 +- **更新内容**: + - 添加 agent_callback_utils.py + - 支持从请求参数传递 `llm_api_key` 和 `user_id` + - 集成回调功能,自动追踪工具使用(web_search, content_reader) + +### 2. Jina Search Agent +- **镜像**: `agnettaiji.azurecr.io/ai-agents/jina-search-agent:latest` +- **平台**: linux/arm64 +- **状态**: ✅ 已推送 +- **更新内容**: + - 添加 agent_callback_utils.py + - 支持从请求参数传递 `jina_api_key` 和 `user_id` + - 移除全局 JINA_API_KEY 环境变量依赖 + - 集成回调功能,追踪工具使用(jina_reader) + +### 3. MySQL Agent +- **镜像**: `agnettaiji.azurecr.io/ai-agents/mysql-agent:latest` +- **平台**: linux/arm64 +- **状态**: ✅ 已推送 +- **更新内容**: + - **重大重构**: 从循环模式改为 FastAPI HTTP 服务 + - 添加 agent_callback_utils.py + - 新增 FastAPI 端点: `/health`, `/query`, `/` + - 支持从请求参数传递 `openai_api_key` 和 `user_id` + - 集成回调功能,追踪工具使用(sql_database) + - 新增依赖: fastapi, uvicorn, requests, pydantic + +### 4. PostgreSQL Agent +- **镜像**: `agnettaiji.azurecr.io/ai-agents/postgresql-agent:latest` +- **平台**: linux/arm64 +- **状态**: ✅ 已推送 +- **更新内容**: + - **重大重构**: 从循环模式改为 FastAPI HTTP 服务 + - 添加 agent_callback_utils.py + - 新增 FastAPI 端点: `/health`, `/query`, `/` + - 支持从请求参数传递 `openai_api_key` 和 `user_id` + - 集成回调功能,追踪工具使用(sql_database) + - 新增依赖: fastapi, uvicorn, requests, pydantic + +### 5. Azure Blob Agent +- **镜像**: `agnettaiji.azurecr.io/ai-agents/azure-blob-agent:latest` +- **平台**: linux/arm64 +- **状态**: ✅ 已推送 +- **更新内容**: + - 添加 agent_callback_utils.py + - 支持从请求参数传递 `litellm_api_key` 和 `user_id` + - 移除全局 LITELLM_API_KEY 环境变量依赖 + - 集成回调功能,追踪工具使用(azure_blob_storage) + +## 回调功能说明 + +所有 Agent 现在都支持: + +1. **自动时间追踪**: 自动记录 Pod 运行时间 +2. **工具使用追踪**: 记录使用的工具列表 +3. **用户 ID 追踪**: 支持多租户计费 +4. **自动回调**: 在请求结束时自动发送 POST 请求到回调 URL + +### 回调 URL +- 默认: `http://mcp-server:8002/api/v1/billing/agent-callback` +- 可通过环境变量 `AGENT_CALLBACK_URL` 自定义 + +### 回调数据格式 +```json +{ + "agentName": "pod-name", + "userId": "user-123", + "podRunningTimeSeconds": 120, + "toolsUsed": ["tool1", "tool2"], + "startTime": "2026-01-15T10:00:00Z", + "endTime": "2026-01-15T10:02:00Z", + "requestId": "req-xxx" +} +``` + +## API 密钥处理变化 + +### 之前 +- 所有 API 密钥都通过环境变量设置 +- 不支持多租户 + +### 现在 +- API 密钥通过**请求参数**传递 +- 支持多租户场景 +- 其他配置仍然通过环境变量 + +### 请求示例 + +#### Search Agent +```json +{ + "query": "search query", + "llm_api_key": "sk-xxx", + "user_id": "user-123" +} +``` + +#### MySQL/PostgreSQL Agent +```json +{ + "query": "SQL query", + "openai_api_key": "sk-xxx", + "user_id": "user-123" +} +``` + +#### Azure Blob Agent +```json +{ + "query": "blob query", + "litellm_api_key": "sk-xxx", + "user_id": "user-123" +} +``` + +## 环境变量要求 + +所有 Agent 部署时需要设置: + +- `POD_NAME`: Pod 名称(必需,用于回调中的 agentName) +- `AGENT_CALLBACK_URL`: 回调 URL(可选,默认 http://mcp-server:8002/api/v1/billing/agent-callback) +- `USER_ID`: 默认用户 ID(可选,请求中未提供时使用) + +## 验证命令 + +```bash +# 列出所有镜像 +az acr repository list --name agnettaiji --output table + +# 查看特定镜像的标签 +az acr repository show-tags --name agnettaiji --repository ai-agents/search-agent --output table +``` + +## 后续步骤 + +1. ✅ 所有 Agent 镜像已推送到 ACR +2. ⏳ 需要更新 Kubernetes 部署文件以使用新镜像 +3. ⏳ 需要测试回调功能是否正常工作 +4. ⏳ 需要验证多租户 API 密钥处理 + +## 构建脚本 + +创建了批量构建脚本: `build_all_agents.sh` + +```bash +# 使用方法 +./build_all_agents.sh [TAG] + +# 默认使用 latest 标签 +./build_all_agents.sh +``` diff --git a/agent_templates/docs/REORGANIZATION_SUMMARY.md b/agent_templates/docs/REORGANIZATION_SUMMARY.md new file mode 100644 index 0000000..0e48ae4 --- /dev/null +++ b/agent_templates/docs/REORGANIZATION_SUMMARY.md @@ -0,0 +1,168 @@ +# Agent Templates 目录重组总结 + +## 完成时间 +2026-01-15 + +## 重组概述 + +已将 `agent_templates` 目录中的所有文件按照功能分类整理到不同的文件夹中,提高了代码的可维护性和可读性。 + +## 新的目录结构 + +``` +agent_templates/ +├── agents/ # 所有 Agent 实现(8个) +├── common/ # 共享代码和工具 +├── docs/ # 所有文档文件 +├── scripts/ # 构建和工具脚本 +└── tests/ # 测试文件 +``` + +## 详细变更 + +### 1. Agents 目录 (`agents/`) + +所有 Agent 实现已移动到各自的子目录: + +- `agents/search_agent/` - 智能搜索 Agent +- `agents/jina_search_agent/` - Jina 搜索 Agent +- `agents/azure_blob_agent/` - Azure Blob 存储 Agent +- `agents/azure_blob_agent_a2a/` - Azure Blob Agent (A2A) +- `agents/azure_blob_agent_mcp/` - Azure Blob Agent (MCP) +- `agents/postgresql_agent/` - PostgreSQL 数据库 Agent +- `agents/mysql_agent/` - MySQL 数据库 Agent +- `agents/a2a_litellm_agent/` - A2A LiteLLM Agent + +每个 Agent 目录包含: +- Agent 主程序文件(`.py`) +- Dockerfile(`.Dockerfile`) +- 相关配置和依赖文件 + +### 2. Common 目录 (`common/`) + +共享代码和工具: +- `agent_callback_utils.py` - Agent 回调处理工具 +- `api_key_utils.py` - API Key 配置管理工具 +- `requirements_a2a.txt` - A2A 协议依赖 +- `requirements_mcp.txt` - MCP 协议依赖 +- `Dockerfile.test` - 测试 Dockerfile + +### 3. Docs 目录 (`docs/`) + +所有文档文件(18个): +- 使用指南(`*_USAGE.md`) +- 示例文档(`*_EXAMPLES.md`) +- 配置说明(`*_SUMMARY.md`) +- 快速参考(`QUICK_REFERENCE.md`) +- 目录结构说明(`DIRECTORY_STRUCTURE.md`) + +### 4. Scripts 目录 (`scripts/`) + +构建和工具脚本(10个): +- `build_*.sh` - 各 Agent 的构建脚本 +- `build_all_agents.sh` - 批量构建脚本 +- `rebuild_all.sh` - 重建所有 Agent +- `check_image_content.sh` - 镜像内容检查 + +### 5. Tests 目录 (`tests/`) + +测试文件(6个): +- `test_*.py` - Python 测试文件 +- `test_*.sh` - Shell 测试脚本 +- `test_client.py` - 测试客户端 + +## 更新的文件 + +### 构建脚本 + +所有构建脚本已更新路径引用: + +| 脚本 | 更新内容 | +|------|---------| +| `build_search_agent.sh` | `search_agent.Dockerfile` → `agents/search_agent/search_agent.Dockerfile` | +| `build_jina_agent.sh` | `jina_search_agent.Dockerfile` → `agents/jina_search_agent/jina_search_agent.Dockerfile` | +| `build_all_agents.sh` | 所有 Dockerfile 路径已更新 | +| `rebuild_all.sh` | 所有 Dockerfile 路径已更新 | +| `build_a2a_litellm_agent.sh` | 路径已更新,构建上下文改为 `agents/a2a_litellm_agent` | + +### Dockerfile + +所有 Dockerfile 已更新 COPY 路径: + +| Dockerfile | 更新内容 | +|-----------|---------| +| `search_agent.Dockerfile` | 更新为从 `agents/search_agent/` 和 `common/` 复制 | +| `jina_search_agent.Dockerfile` | 更新为从 `agents/jina_search_agent/` 和 `common/` 复制 | +| `postgresql_agent.Dockerfile` | 更新为从 `agents/postgresql_agent/` 和 `common/` 复制 | +| `mysql_agent.Dockerfile` | 更新为从 `agents/mysql_agent/` 和 `common/` 复制 | +| `azure_blob_agent.Dockerfile` | 更新为从 `agents/azure_blob_agent/` 和 `common/` 复制 | +| `azure_blob_agent_a2a.Dockerfile` | 更新为从 `agents/azure_blob_agent_a2a/` 和 `common/` 复制 | +| `azure_blob_agent_mcp.Dockerfile` | 更新为从 `agents/azure_blob_agent_mcp/` 和 `common/` 复制 | +| `a2a_litellm_agent.Dockerfile` | 更新为从 `agents/a2a_litellm_agent/` 复制 | + +## 使用说明 + +### 构建 Agent + +所有构建脚本需要在 `agent_templates` 目录下运行: + +```bash +cd agent_templates +./scripts/build_search_agent.sh v1.0 +``` + +### 查看文档 + +```bash +# 查看目录结构说明 +cat docs/DIRECTORY_STRUCTURE.md + +# 查看特定 Agent 的使用指南 +cat docs/SEARCH_AGENT_USAGE.md +``` + +### 导入共享工具 + +在 Agent 代码中,共享工具会被复制到 `/app/common/`: + +```python +# 在容器中运行时 +from common.agent_callback_utils import AgentCallbackHandler +from common.api_key_utils import get_llm_api_key +``` + +## 迁移检查清单 + +- [x] 所有文件已移动到对应目录 +- [x] 所有构建脚本路径已更新 +- [x] 所有 Dockerfile 路径已更新 +- [x] 创建了目录结构说明文档 +- [x] 创建了重组总结文档 + +## 注意事项 + +1. **构建脚本**: 必须在 `agent_templates` 目录下运行 +2. **Dockerfile**: 构建上下文是 `agent_templates` 目录 +3. **导入路径**: 在容器中,`common/` 目录会被复制到 `/app/common/` +4. **向后兼容**: 如果外部代码直接引用文件,需要更新路径 + +## 优势 + +1. **清晰的目录结构**: 按功能分类,易于查找和维护 +2. **模块化设计**: 每个 Agent 独立目录,便于管理 +3. **共享代码集中**: `common/` 目录统一管理共享工具 +4. **文档集中**: 所有文档在 `docs/` 目录 +5. **脚本集中**: 所有构建脚本在 `scripts/` 目录 + +## 后续建议 + +1. 添加新 Agent 时,在 `agents/` 目录下创建新的子目录 +2. 共享代码放在 `common/` 目录 +3. 文档放在 `docs/` 目录 +4. 构建脚本放在 `scripts/` 目录 +5. 测试文件放在 `tests/` 目录 + +## 相关文档 + +- `DIRECTORY_STRUCTURE.md` - 详细的目录结构说明 +- `API_KEY_CONFIGURATION_SUMMARY.md` - API Key 配置说明 diff --git a/agent_templates/docs/SEARCH_AGENT_EXAMPLES.md b/agent_templates/docs/SEARCH_AGENT_EXAMPLES.md new file mode 100644 index 0000000..ff9c139 --- /dev/null +++ b/agent_templates/docs/SEARCH_AGENT_EXAMPLES.md @@ -0,0 +1,487 @@ +# Search Agent 请求调用示例 + +## 服务信息 +- **服务名称**: Intelligent Search AI Agent +- **版本**: 1.0.0 +- **框架**: LangChain + Serper + Jina +- **默认端口**: 8080 + +## 概述 +智能搜索代理集成了 Google 搜索 (Serper API) 和网页内容提取 (Jina API),提供智能问答服务。 + +--- + +## API 端点 + +### 1. 健康检查 +**端点**: `GET /health` + +**请求示例** (curl): +```bash +curl http://localhost:8080/health +``` + +**响应示例**: +```json +{ + "status": "healthy", + "pod_name": "search-agent", + "template_type": "search_agent", + "configured": true, + "timestamp": "2026-01-15T10:30:00.000Z" +} +``` + +--- + +### 2. 获取状态 +**端点**: `GET /status` + +**请求示例** (curl): +```bash +curl http://localhost:8080/status +``` + +**响应示例**: +```json +{ + "status": "running", + "pod_name": "search-agent", + "template_type": "search_agent", + "configured": true, + "timestamp": "2026-01-15T10:30:00.000Z" +} +``` + +--- + +### 3. 配置 Agent +**端点**: `POST /configure` + +配置搜索代理的参数(可选,也可以从环境变量自动配置)。 + +**请求体**: +```json +{ + "llm_base_url": "http://localhost:4000", + "llm_model": "xchat52", + "serper_api_key": "your-serper-api-key", + "jina_api_key": "your-jina-api-key", + "max_iterations": 3, + "max_results_per_query": 10, + "content_max_length": 5000, + "log_level": "INFO", + "timeout": 30 +} +``` + +**请求示例** (curl): +```bash +curl -X POST http://localhost:8080/configure \ + -H "Content-Type: application/json" \ + -d '{ + "llm_base_url": "http://localhost:4000", + "llm_model": "gpt-3.5-turbo", + "serper_api_key": "xxx", + "jina_api_key": "xxx", + "max_iterations": 3, + "max_results_per_query": 10 + }' +``` + +**请求示例** (Python): +```python +import requests + +config_data = { + "llm_base_url": "http://localhost:4000", + "llm_model": "xchat52", + "serper_api_key": "your-serper-key", + "jina_api_key": "your-jina-key", + "max_iterations": 3, + "max_results_per_query": 10, + "content_max_length": 5000, + "timeout": 30 +} + +response = requests.post( + "http://localhost:8080/configure", + json=config_data +) +print(response.json()) +``` + +**响应示例**: +```json +{ + "status": "success", + "message": "Agent配置成功", + "timestamp": "2026-01-15T10:30:00.000Z" +} +``` + +--- + +### 4. 执行搜索 +**端点**: `POST /search` + +执行智能搜索并返回答案。 + +**请求体**: +```json +{ + "query": "什么是Kubernetes?", + "llm_api_key": "your-llm-api-key", + "user_id": "user123", + "auto_configure": false +} +``` + +**请求示例** (curl): +```bash +curl -X POST http://localhost:8080/search \ + -H "Content-Type: application/json" \ + -d '{ + "query": "Python最新版本是什么?", + "llm_api_key": "sk-xxx", + "user_id": "user123" + }' +``` + +**请求示例** (Python): +```python +import requests + +search_data = { + "query": "2026年最新的AI技术趋势是什么?", + "llm_api_key": "sk-xxx", + "user_id": "user123", + "auto_configure": False # 设为 True 从环境变量自动配置 +} + +response = requests.post( + "http://localhost:8080/search", + json=search_data +) +result = response.json() + +print(f"Query: {result['query']}") +print(f"Answer: {result['answer']}") +print(f"Confidence: {result['confidence']}") +print(f"Sources: {len(result['sources'])}") +for i, source in enumerate(result['sources'], 1): + print(f"{i}. {source['title']}: {source['url']}") +``` + +**响应示例**: +```json +{ + "query": "什么是Kubernetes?", + "answer": "Kubernetes 是一个开源的容器编排平台,用于自动化容器化应用程序的部署、扩展和管理。它最初由 Google 开发,现在由 Cloud Native Computing Foundation (CNCF) 维护。Kubernetes 提供了容器调度、服务发现、负载均衡、自动伸缩等功能。", + "sources": [ + { + "index": 1, + "title": "Kubernetes Documentation", + "url": "https://kubernetes.io/docs/" + }, + { + "index": 2, + "title": "What is Kubernetes? - Red Hat", + "url": "https://www.redhat.com/en/topics/containers/what-is-kubernetes" + } + ], + "confidence": "high", + "iterations": 2, + "total_sources": 5, + "search_queries": [ + "什么是Kubernetes", + "Kubernetes 容器编排" + ], + "timestamp": "2026-01-15T10:30:00.000Z" +} +``` + +--- + +## 完整使用流程示例 + +### Python 完整示例: + +```python +import requests +import json + +class SearchAgentClient: + """Search Agent 客户端""" + + def __init__(self, base_url: str, llm_api_key: str): + self.base_url = base_url.rstrip('/') + self.llm_api_key = llm_api_key + + def health_check(self): + """健康检查""" + response = requests.get(f"{self.base_url}/health") + return response.json() + + def get_status(self): + """获取状态""" + response = requests.get(f"{self.base_url}/status") + return response.json() + + def configure(self, config: dict): + """配置 Agent""" + response = requests.post(f"{self.base_url}/configure", json=config) + return response.json() + + def search(self, query: str, user_id: str = None, auto_configure: bool = False): + """执行搜索""" + data = { + "query": query, + "llm_api_key": self.llm_api_key, + "user_id": user_id, + "auto_configure": auto_configure + } + response = requests.post(f"{self.base_url}/search", json=data) + return response.json() + + +# 使用示例 +client = SearchAgentClient( + base_url="http://localhost:8080", + llm_api_key="sk-xxx" +) + +# 1. 健康检查 +print("Health:", client.health_check()) + +# 2. 配置 Agent (可选) +config = { + "llm_base_url": "http://localhost:4000", + "llm_model": "xchat52", + "serper_api_key": "your-serper-key", + "jina_api_key": "your-jina-key", + "max_iterations": 3, + "max_results_per_query": 10 +} +print("Configure:", client.configure(config)) + +# 3. 执行搜索 +queries = [ + "2026年最新的AI技术有哪些?", + "Docker和Kubernetes的区别是什么?", + "Python 3.12的新特性" +] + +for query in queries: + print(f"\n{'='*60}") + print(f"Query: {query}") + print('='*60) + + result = client.search(query, user_id="user123") + + print(f"\n答案: {result['answer']}") + print(f"\n置信度: {result['confidence']}") + print(f"迭代次数: {result['iterations']}") + print(f"总来源: {result['total_sources']}") + + print("\n来源:") + for source in result['sources']: + print(f" {source['index']}. {source['title']}") + print(f" {source['url']}") +``` + +--- + +### 批量搜索示例: + +```python +import requests +import concurrent.futures +import time + +def search_query(query, llm_api_key, user_id=None): + """执行单次搜索""" + try: + response = requests.post( + "http://localhost:8080/search", + json={ + "query": query, + "llm_api_key": llm_api_key, + "user_id": user_id, + "auto_configure": True + }, + timeout=60 + ) + return { + "query": query, + "success": True, + "result": response.json() + } + except Exception as e: + return { + "query": query, + "success": False, + "error": str(e) + } + +# 批量查询 +queries = [ + "什么是机器学习?", + "深度学习和机器学习的区别", + "PyTorch vs TensorFlow", + "Transformer模型的原理", + "GPT-4的主要特性" +] + +llm_api_key = "sk-xxx" + +# 并行执行搜索 +with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + futures = [ + executor.submit(search_query, query, llm_api_key, f"user-{i}") + for i, query in enumerate(queries) + ] + + results = [future.result() for future in concurrent.futures.as_completed(futures)] + +# 输出结果 +for result in results: + if result["success"]: + data = result["result"] + print(f"\nQuery: {data['query']}") + print(f"Answer: {data['answer'][:200]}...") + print(f"Sources: {len(data['sources'])}") + else: + print(f"\nQuery: {result['query']}") + print(f"Error: {result['error']}") +``` + +--- + +### 流式搜索示例 (如果支持): + +```python +import requests +import json + +def stream_search(query: str, llm_api_key: str): + """流式搜索 (假设支持 SSE)""" + response = requests.post( + "http://localhost:8080/search/stream", + json={ + "query": query, + "llm_api_key": llm_api_key + }, + stream=True + ) + + for line in response.iter_lines(): + if line: + try: + data = json.loads(line.decode('utf-8')) + if data.get('type') == 'progress': + print(f"Progress: {data['message']}") + elif data.get('type') == 'answer': + print(f"Answer: {data['content']}") + elif data.get('type') == 'source': + print(f"Source: {data['title']} - {data['url']}") + except json.JSONDecodeError: + continue + +# 使用流式搜索 +stream_search("什么是Kubernetes?", "sk-xxx") +``` + +--- + +## 环境变量配置 + +```bash +# 服务配置 +export SERVICE_HOST="0.0.0.0" +export SERVICE_PORT="8080" +export POD_NAME="search-agent" +export TEMPLATE_TYPE="search_agent" + +# LLM 配置 +export LLM_BASE_URL="http://localhost:4000" +export LLM_API_KEY="sk-xxx" +export LLM_MODEL="xchat52" + +# Serper API (Google 搜索) +export SERPER_API_KEY="your-serper-api-key" + +# Jina API (网页内容提取) +export JINA_API_KEY="your-jina-api-key" + +# 搜索配置 +export MAX_ITERATIONS="3" +export MAX_RESULTS_PER_QUERY="10" +export CONTENT_MAX_LENGTH="5000" +export TIMEOUT="30" +export LOG_LEVEL="INFO" + +# 启动服务 +python search_agent.py +``` + +--- + +## 获取 API Keys + +### 1. Serper API Key +- 访问: https://serper.dev/ +- 注册并获取 API key +- 免费套餐: 2,500 次查询/月 + +### 2. Jina API Key +- 访问: https://jina.ai/ +- 注册并获取 API key +- 免费套餐: 1,000 次请求/天 + +--- + +## 查询示例 + +### 技术问题: +```python +queries = [ + "什么是Docker容器?", + "Kubernetes的核心组件有哪些?", + "微服务架构的优缺点", + "RESTful API设计最佳实践", + "GraphQL和REST的区别" +] +``` + +### 新闻和事实: +```python +queries = [ + "2026年AI领域的最新进展", + "最新的Python版本特性", + "云计算市场份额排名", + "开源许可证的类型和区别" +] +``` + +### 比较和分析: +```python +queries = [ + "React vs Vue.js 框架对比", + "PostgreSQL和MySQL的性能比较", + "AWS、Azure、GCP云服务对比", + "敏捷开发和瀑布模型的区别" +] +``` + +--- + +## 注意事项 + +1. **API Keys**: 需要有效的 Serper 和 Jina API keys +2. **速率限制**: 注意 API 调用速率限制 +3. **超时设置**: 根据查询复杂度调整超时时间 +4. **结果质量**: 置信度 (confidence) 表示答案质量 +5. **迭代次数**: max_iterations 控制搜索深度 +6. **并发限制**: 建议最多 3-5 个并发搜索 +7. **成本控制**: 监控 API 使用量以控制成本 diff --git a/agent_templates/docs/SEARCH_AGENT_SUMMARY.md b/agent_templates/docs/SEARCH_AGENT_SUMMARY.md new file mode 100644 index 0000000..427ec3b --- /dev/null +++ b/agent_templates/docs/SEARCH_AGENT_SUMMARY.md @@ -0,0 +1,117 @@ +# Search Agent 集成完成总结 + +## ✅ 已完成的工作 + +### 1. 代码转换 +- ✅ 将 aks_agent/search_agent 转换为 FastAPI 服务 +- ✅ 创建 search_agent.py 作为 HTTP API 入口 +- ✅ 支持健康检查、状态查询和搜索功能 + +### 2. Docker 镜像 +- ✅ 创建支持 ARM64 和 AMD64 双架构的 Dockerfile +- ✅ 成功构建镜像: `agnettaiji.azurecr.io/ai-agents/search-agent:v1.0` +- ✅ 推送到 ACR + +### 3. K8s 集成 +- ✅ 创建 K8s YAML 模板 (`agent_manager/templates/search_agent.yaml`) +- ✅ 添加到数据库模板配置 +- ✅ 可通过 agent-manager Web API 创建和管理 + +### 4. 构建和测试脚本 +- ✅ `build_search_agent.sh` - 多架构构建和推送 +- ✅ `test_search_agent.sh` - 集成测试脚本 +- ✅ `SEARCH_AGENT_USAGE.md` - 使用文档 + +## 📁 文件结构 + +``` +agent_templates/ +├── search_agent.py # FastAPI 主服务 +├── search_agent.Dockerfile # 多架构 Dockerfile +├── build_search_agent.sh # 构建脚本 +├── test_search_agent.sh # 测试脚本 +├── SEARCH_AGENT_USAGE.md # 使用文档 +└── search_agent/ # 原始代码 + ├── agent/ + ├── modules/ + ├── tools/ + ├── models/ + ├── utils/ + ├── config.py + └── requirements.txt + +agent_manager/templates/ +└── search_agent.yaml # K8s 部署模板 +``` + +## 🚀 使用方法 + +### 1. 通过 Agent Manager 创建 + +```bash +curl -X POST http://localhost:8000/agents \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-search-agent", + "template": "search_agent", + "config": { + "cpu_request": "500m", + "memory_request": "512Mi" + }, + "env": { + "LLM_BASE_URL": "https://apis.openroutex.com/openai/deployments/xchat52", + "LLM_API_KEY": "your-key", + "SERPER_API_KEY": "your-key", + "JINA_API_KEY": "your-key" + } + }' +``` + +### 2. 使用 Agent + +```bash +# 健康检查 +curl http://my-search-agent.ai-agents.svc.cluster.local:8080/health + +# 执行搜索 +curl -X POST http://my-search-agent.ai-agents.svc.cluster.local:8080/search \ + -H "Content-Type: application/json" \ + -d '{"query": "什么是Kubernetes?", "auto_configure": true}' +``` + +## 🔧 环境变量 + +### 必需 +- `LLM_BASE_URL` - LLM API基础URL +- `LLM_API_KEY` - LLM API密钥 +- `SERPER_API_KEY` - Serper API密钥(Google搜索) +- `JINA_API_KEY` - Jina API密钥(内容提取和重排序) + +### 可选 +- `LLM_MODEL` - LLM模型名称(默认: xchat52) +- `MAX_ITERATIONS` - 最大迭代次数(默认: 3) +- `MAX_RESULTS_PER_QUERY` - 每次搜索最大结果数(默认: 10) +- `LOG_LEVEL` - 日志级别(默认: INFO) + +## 📊 资源配置 + +- **CPU Request**: 500m +- **CPU Limit**: 1000m +- **Memory Request**: 512Mi +- **Memory Limit**: 1Gi +- **Port**: 8080 +- **支持架构**: linux/arm64, linux/amd64 + +## 🎯 下一步 + +1. 配置真实的 API keys +2. 测试搜索功能 +3. 根据需要调整资源配置 +4. 监控性能和日志 + +## 📝 注意事项 + +- ARM K8s 集群已支持 +- 镜像已推送到 ACR +- 健康检查配置为 30-40 秒启动时间 +- 支持自动配置和手动配置两种模式 diff --git a/agent_templates/docs/SEARCH_AGENT_USAGE.md b/agent_templates/docs/SEARCH_AGENT_USAGE.md new file mode 100644 index 0000000..7aeb749 --- /dev/null +++ b/agent_templates/docs/SEARCH_AGENT_USAGE.md @@ -0,0 +1,380 @@ +# Search Agent 使用指南 + +## 简介 + +智能搜索 AI Agent 是一个基于大语言模型的搜索代理,能够理解用户查询意图、自动规划搜索策略、从多个来源获取信息,并生成高质量、有来源引用的答案。 + +## 架构 + +- **基础镜像**: `agnettaiji.azurecr.io/ai-agents/search-agent:v1.0` +- **支持平台**: linux/amd64, linux/arm64 +- **端口**: 8080 +- **协议**: HTTP/REST API + +## 功能特点 + +- 🧠 **智能查询理解**: 分析用户意图,提取关键实体 +- 📋 **搜索规划**: 智能分解问题,制定搜索策略 +- 🔎 **多源搜索**: 支持 Web 搜索和新闻搜索 +- 📄 **内容提取**: 智能提取网页核心内容 +- 🎯 **结果排序**: 基于相关性重排搜索结果 +- ✍️ **答案生成**: 综合信息生成结构化回答 +- 🔄 **自我反思**: 评估答案质量,决定是否迭代 + +## 环境变量配置 + +### 必需配置 + +| 变量名 | 说明 | 示例 | +|--------|------|------| +| `LLM_BASE_URL` | LLM API 基础URL | `https://apis.openroutex.com/openai/deployments/xchat52` | +| `LLM_API_KEY` | LLM API 密钥 | `sk-xxx` | +| `SERPER_API_KEY` | Serper API 密钥(Google搜索) | `xxx` | +| `JINA_API_KEY` | Jina API 密钥(内容提取和重排序) | `jina_xxx` | + +### 可选配置 + +| 变量名 | 说明 | 默认值 | +|--------|------|--------| +| `LLM_MODEL` | LLM 模型名称 | `xchat52` | +| `MAX_ITERATIONS` | 最大迭代次数 | `3` | +| `MAX_RESULTS_PER_QUERY` | 每次搜索最大结果数 | `10` | +| `CONTENT_MAX_LENGTH` | 内容最大长度 | `5000` | +| `LOG_LEVEL` | 日志级别 | `INFO` | +| `TIMEOUT` | 超时时间(秒) | `30` | + +## 部署方式 + +### 方式 1: 通过 agent-manager Web API + +```bash +curl -X POST http://localhost:8000/api/v2/agents \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-search-agent", + "template_type": "search_agent", + "image": "agnettaiji.azurecr.io/ai-agents/search-agent:v1.0", + "replicas": 1, + "env_vars": { + "LLM_BASE_URL": "https://apis.openroutex.com/openai/deployments/xchat52", + "LLM_API_KEY": "your-llm-key", + "LLM_MODEL": "xchat52", + "SERPER_API_KEY": "your-serper-key", + "JINA_API_KEY": "your-jina-key", + "MAX_ITERATIONS": "3", + "LOG_LEVEL": "INFO" + }, + "resources": { + "cpu_request": "500m", + "memory_request": "512Mi", + "cpu_limit": "1000m", + "memory_limit": "1Gi" + } + }' +``` + +### 方式 2: 直接使用 kubectl + +```bash +# 创建命名空间(如果不存在) +kubectl create namespace agents + +# 应用 YAML 配置 +kubectl apply -f - < +``` + +### 常见问题 + +1. **Agent 无法启动** + - 检查环境变量是否正确配置 + - 确认 ACR secret 已创建 + - 查看 Pod 事件和日志 + +2. **搜索失败** + - 确认 API keys 有效 + - 检查网络连接 + - 查看日志中的错误信息 + +3. **健康检查失败** + - 确认端口 8080 正常监听 + - 检查容器资源是否充足 + - 查看启动日志 + +## 相关文件 + +- `/home/taiji/tools/agent-manager/agent_templates/search_agent.py` - 主程序 +- `/home/taiji/tools/agent-manager/agent_templates/search_agent.Dockerfile` - Dockerfile +- `/home/taiji/tools/agent-manager/agent_templates/build_search_agent.sh` - 构建脚本 +- `/home/taiji/tools/agent-manager/agent_templates/test_search_agent.sh` - 测试脚本 +- `/home/taiji/tools/agent-manager/agent_manager/templates/search_agent.yaml` - K8s 模板 + +## 技术栈 + +- **语言**: Python 3.11 +- **框架**: FastAPI, Uvicorn +- **LLM**: xchat52 (GPT-5.2) +- **搜索**: Serper API (Google 搜索代理) +- **内容提取**: Jina Reader +- **重排序**: Jina Reranker +- **异步**: asyncio + +## 许可 + +遵循项目主许可证。 diff --git a/agent_templates/docs/UPDATE_SUMMARY.md b/agent_templates/docs/UPDATE_SUMMARY.md new file mode 100644 index 0000000..4393ecb --- /dev/null +++ b/agent_templates/docs/UPDATE_SUMMARY.md @@ -0,0 +1,99 @@ +# Agent Templates 更新总结 + +## 更新时间 +2026-01-15 + +## 更新内容 + +### 1. 新增文件 +- **agent_callback_utils.py**: 通用回调处理工具模块 + - AgentCallbackHandler: 回调处理器类 + - CallbackContextManager: 上下文管理器 + +### 2. 已更新的 Agent 文件 + +#### ✅ search_agent.py +- 添加回调功能 +- API key 从请求传入 +- 记录工具: web_search, content_reader + +#### ✅ jina_search_agent.py +- 添加回调功能 +- API key 从请求传入 +- 记录工具: jina_reader + +#### ✅ mysql_agent.py +- 从循环模式改为 FastAPI HTTP 服务 +- 添加回调功能 +- API key 从请求传入 +- 记录工具: sql_database + +#### ✅ postgresql_agent.py +- 从循环模式改为 FastAPI HTTP 服务 +- 添加回调功能 +- API key 从请求传入 +- 记录工具: sql_database + +#### ✅ azure_blob_agent.py +- 添加回调功能 +- API key 从请求传入 +- 记录工具: azure_blob_storage + +### 3. 已更新的 Dockerfile + +所有 Dockerfile 已更新以包含 agent_callback_utils.py: + +- ✅ search_agent.Dockerfile +- ✅ mysql_agent.Dockerfile (添加 fastapi, uvicorn, requests) +- ✅ postgresql_agent.Dockerfile (添加 fastapi, uvicorn, requests) +- ✅ jina_search_agent.Dockerfile +- ✅ azure_blob_agent.Dockerfile + +### 4. 请求模型更新 + +所有 agent 的请求模型都添加了: +```python +litellm_api_key/openai_api_key/jina_api_key: str # API key 从请求传入 +user_id: Optional[str] # 用于计费回调 +``` + +### 5. 回调数据格式 + +```json +{ + "agentName": "pod-name", + "userId": "user-123", + "podRunningTimeSeconds": 120, + "toolsUsed": ["tool1", "tool2"], + "startTime": "2026-01-15T10:00:00Z", + "endTime": "2026-01-15T10:02:00Z", + "requestId": "req-xxx" +} +``` + +### 6. 环境变量 + +所有 Agent 需要设置: +```bash +POD_NAME=agent-name +USER_ID=default-user-id # 可选 +AGENT_CALLBACK_URL=http://mcp-server:8002/api/v1/billing/agent-callback # 可选 +``` + +## 下一步 + +1. 构建并推送更新后的镜像: + ```bash + cd /home/taiji/tools/agent-manager/agent_templates + ./build_search_agent.sh + ./build_jina_agent.sh + # 等等... + ``` + +2. 测试回调功能 + +3. 部署到 Kubernetes + +--- + +详细文档请参考: CALLBACK_IMPLEMENTATION_SUMMARY.md diff --git a/agent_templates/jina_search_agent.Dockerfile b/agent_templates/jina_search_agent.Dockerfile deleted file mode 100644 index 483145d..0000000 --- a/agent_templates/jina_search_agent.Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM python:3.11-slim - -WORKDIR /app - -# 安装Python依赖 -RUN pip install --no-cache-dir \ - fastapi==0.109.0 \ - uvicorn==0.27.0 \ - requests==2.31.0 \ - pydantic==2.5.3 - -# 复制agent代码 -COPY jina_search_agent.py . - -# 设置环境变量 -ENV PYTHONUNBUFFERED=1 -ENV SERVICE_HOST=0.0.0.0 -ENV SERVICE_PORT=8080 - -# 暴露端口 -EXPOSE 8080 - -# 运行agent -CMD ["python", "jina_search_agent.py"] diff --git a/agent_templates/jina_search_agent.py b/agent_templates/jina_search_agent.py deleted file mode 100644 index 448aba3..0000000 --- a/agent_templates/jina_search_agent.py +++ /dev/null @@ -1,230 +0,0 @@ -""" -Jina Search Agent - 使用Jina Reader API获取网站内容的HTTP服务 -需要设置环境变量: JINA_API_KEY -""" -import os -import time -import logging -import requests -from typing import Optional -from fastapi import FastAPI, HTTPException, Query -from pydantic import BaseModel, Field -import uvicorn - -# 配置日志 -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# 环境变量配置 -POD_NAME = os.getenv("POD_NAME", "unknown") -TEMPLATE_TYPE = os.getenv("TEMPLATE_TYPE", "jina_search_agent") -JINA_API_KEY = os.getenv("JINA_API_KEY", "") -SERVICE_HOST = os.getenv("SERVICE_HOST", "0.0.0.0") -SERVICE_PORT = int(os.getenv("SERVICE_PORT", "8080")) - -# Jina Reader API基础URL -JINA_BASE_URL = "https://r.jina.ai" - -# 创建FastAPI应用 -app = FastAPI( - title="Jina Search Agent", - description="使用Jina Reader API获取网站内容的AI Agent", - version="1.0.0" -) - - -class SearchRequest(BaseModel): - """搜索请求模型""" - url: str = Field(..., description="要搜索的网站URL") - timeout: int = Field(default=30, description="请求超时时间(秒)") - - -class SearchResponse(BaseModel): - """搜索响应模型""" - url: str - content: str - status_code: int - content_type: Optional[str] = None - success: bool - - -class HealthResponse(BaseModel): - """健康检查响应""" - status: str - pod_name: str - template_type: str - jina_api_configured: bool - - -@app.get("/health", response_model=HealthResponse) -async def health_check(): - """ - 健康检查端点 - - Returns: - 服务状态信息 - """ - return HealthResponse( - status="healthy", - pod_name=POD_NAME, - template_type=TEMPLATE_TYPE, - jina_api_configured=bool(JINA_API_KEY) - ) - - -@app.get("/") -async def root(): - """根路径 - 返回服务信息和所需参数""" - return { - "service": "Jina Search Agent", - "version": "1.0.0", - "description": "使用Jina Reader API获取网站内容的AI Agent", - "pod_name": POD_NAME, - "template_type": TEMPLATE_TYPE, - "required_env": { - "JINA_API_KEY": { - "description": "Jina API密钥,从 https://jina.ai/ 获取", - "required": True, - "configured": bool(JINA_API_KEY) - } - }, - "optional_env": { - "SERVICE_PORT": { - "description": "HTTP服务端口", - "default": "8080" - }, - "SERVICE_HOST": { - "description": "HTTP服务监听地址", - "default": "0.0.0.0" - } - }, - "endpoints": { - "health": { - "method": "GET", - "path": "/health", - "description": "健康检查" - }, - "search": { - "method": "POST", - "path": "/search", - "description": "搜索网站内容", - "body": { - "url": "要搜索的网站URL (必填)", - "timeout": "请求超时时间,默认30秒 (可选)" - } - }, - "fetch": { - "method": "GET", - "path": "/fetch", - "description": "快速获取网站内容", - "params": { - "url": "要获取的网站URL (必填)", - "timeout": "请求超时时间,默认30秒 (可选)" - } - } - }, - "example_usage": { - "search": 'curl -X POST "http://: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 deleted file mode 100644 index 8914b30..0000000 --- a/agent_templates/mysql_agent.Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 22d2572..0000000 --- a/agent_templates/mysql_agent.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -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 deleted file mode 100644 index e8cb71b..0000000 --- a/agent_templates/postgresql_agent.Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 81847f7..0000000 --- a/agent_templates/postgresql_agent.py +++ /dev/null @@ -1,130 +0,0 @@ -""" -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/agent_templates/requirements_a2a.txt b/agent_templates/requirements_a2a.txt deleted file mode 100644 index 7960c0a..0000000 --- a/agent_templates/requirements_a2a.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Requirements for Azure Blob Agent - A2A Version -fastapi==0.109.0 -uvicorn[standard]==0.27.0 -pydantic==2.5.3 -azure-storage-blob==12.19.0 -httpx==0.26.0 diff --git a/agent_templates/requirements_mcp.txt b/agent_templates/requirements_mcp.txt deleted file mode 100644 index f09fc74..0000000 --- a/agent_templates/requirements_mcp.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Requirements for Azure Blob Agent - MCP Version -fastapi==0.109.0 -uvicorn[standard]==0.27.0 -pydantic==2.5.3 -azure-storage-blob==12.19.0 diff --git a/agent_templates/scripts/build_all_agents.sh b/agent_templates/scripts/build_all_agents.sh new file mode 100755 index 0000000..1677793 --- /dev/null +++ b/agent_templates/scripts/build_all_agents.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +# 批量构建所有更新的 Agent 并推送到 ACR (ARM64) +# 使用方法: ./build_all_agents.sh + +set -e + +# 默认配置 +ACR_NAME="${ACR_NAME:-agnettaiji.azurecr.io}" +TAG="${1:-latest}" +PLATFORM="linux/arm64" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} 批量构建 Agent Templates (ARM64)${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" + +# 登录 ACR +echo -e "${GREEN}==> 登录到 ACR...${NC}" +az acr login --name $(echo ${ACR_NAME} | cut -d'.' -f1) + +# 检查 Docker buildx +if ! docker buildx version > /dev/null 2>&1; then + echo -e "${RED}错误: Docker buildx 未安装${NC}" + exit 1 +fi + +# 创建 builder +if ! docker buildx ls | grep -q multiarch-builder; then + echo -e "${YELLOW}创建 multiarch-builder...${NC}" + docker buildx create --name multiarch-builder --use +fi + +docker buildx use multiarch-builder + +# Agent 列表 +declare -A AGENTS=( + ["search-agent"]="search_agent.Dockerfile" + ["jina-search-agent"]="jina_search_agent.Dockerfile" + ["mysql-agent"]="mysql_agent.Dockerfile" + ["postgresql-agent"]="postgresql_agent.Dockerfile" + ["azure-blob-agent"]="azure_blob_agent.Dockerfile" +) + +# 构建函数 +build_agent() { + local name=$1 + local dockerfile=$2 + local image="${ACR_NAME}/ai-agents/${name}:${TAG}" + + echo "" + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE}构建: ${name}${NC}" + echo -e "${BLUE}========================================${NC}" + echo "Dockerfile: ${dockerfile}" + echo "镜像: ${image}" + echo "平台: ${PLATFORM}" + echo "" + + # 构建并推送 + docker buildx build \ + --platform ${PLATFORM} \ + -f ${dockerfile} \ + -t ${image} \ + --push \ + . + + if [ $? -eq 0 ]; then + echo -e "${GREEN}✅ ${name} 构建成功${NC}" + else + echo -e "${RED}❌ ${name} 构建失败${NC}" + return 1 + fi +} + +# 构建所有 Agent +SUCCESS_COUNT=0 +FAIL_COUNT=0 +FAILED_AGENTS=() + +for name in "${!AGENTS[@]}"; do + dockerfile="${AGENTS[$name]}" + + if [ -f "${dockerfile}" ]; then + if build_agent "${name}" "${dockerfile}"; then + ((SUCCESS_COUNT++)) + else + ((FAIL_COUNT++)) + FAILED_AGENTS+=("${name}") + fi + else + echo -e "${YELLOW}⚠️ 跳过 ${name}: Dockerfile ${dockerfile} 不存在${NC}" + fi +done + +# 总结 +echo "" +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} 构建总结${NC}" +echo -e "${BLUE}========================================${NC}" +echo -e "${GREEN}成功: ${SUCCESS_COUNT}${NC}" +echo -e "${RED}失败: ${FAIL_COUNT}${NC}" + +if [ ${FAIL_COUNT} -gt 0 ]; then + echo -e "${RED}失败的 Agents:${NC}" + for agent in "${FAILED_AGENTS[@]}"; do + echo -e " - ${agent}" + done + exit 1 +fi + +echo "" +echo -e "${GREEN}✅ 所有 Agent 构建成功!${NC}" +echo "" +echo "已推送的镜像:" +for name in "${!AGENTS[@]}"; do + echo " - ${ACR_NAME}/ai-agents/${name}:${TAG}" +done + diff --git a/agent_templates/scripts/build_search_agent.sh b/agent_templates/scripts/build_search_agent.sh new file mode 100755 index 0000000..5756076 --- /dev/null +++ b/agent_templates/scripts/build_search_agent.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +# Intelligent Search Agent 构建和推送脚本(支持多架构) +# 使用方法: ./build_search_agent.sh [TAG] + +set -e + +# 默认配置 +ACR_NAME="${ACR_NAME:-agnettaiji.azurecr.io}" +IMAGE_NAME="ai-agents/search-agent" +TAG="${1:-latest}" +FULL_IMAGE="${ACR_NAME}/${IMAGE_NAME}:${TAG}" + +# 支持的平台 +PLATFORMS="linux/amd64,linux/arm64" + +echo "==========================================" +echo "构建 Intelligent Search Agent" +echo "==========================================" +echo "镜像: ${FULL_IMAGE}" +echo "平台: ${PLATFORMS}" +echo "" + +# 检查Docker buildx +if ! docker buildx version > /dev/null 2>&1; then + echo "错误: Docker buildx未安装或未启用" + echo "请运行: docker buildx create --use" + exit 1 +fi + +# 创建builder(如果不存在) +if ! docker buildx ls | grep -q multiarch-builder; then + echo "创建 multiarch-builder..." + docker buildx create --name multiarch-builder --use +fi + +# 使用multiarch-builder +docker buildx use multiarch-builder + +# 询问是否推送 +read -p "是否推送到 ACR? (y/N): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "🚀 构建并推送多架构镜像到 ACR..." + + # 登录 ACR (如果需要) + echo "登录到 ACR..." + az acr login --name $(echo ${ACR_NAME} | cut -d'.' -f1) + + # 构建并推送镜像(多架构) + docker buildx build \ + --platform "${PLATFORMS}" \ + -f search_agent.Dockerfile \ + -t "${FULL_IMAGE}" \ + --push \ + . + + echo "" + echo "✅ 镜像构建并推送成功!" + echo "" + echo "部署到 K8s (ARM64):" + echo " kubectl set image deployment/search-agent search-agent=${FULL_IMAGE}" +else + echo "⏭️ 只构建本地镜像 (linux/arm64)..." + docker buildx build \ + --platform "linux/arm64" \ + -f search_agent.Dockerfile \ + -t "${FULL_IMAGE}" \ + --load \ + . + + echo "" + echo "✅ 本地镜像构建成功!" +fi + +echo "" +echo "==========================================" +echo "完成!" +echo "==========================================" +echo "镜像: ${FULL_IMAGE}" +echo "支持平台: ${PLATFORMS}" +echo "" +echo "测试命令:" +echo " docker run -p 8080:8080 \\" +echo " -e LLM_BASE_URL='your_llm_url' \\" +echo " -e LLM_API_KEY='your_llm_key' \\" +echo " -e SERPER_API_KEY='your_serper_key' \\" +echo " -e JINA_API_KEY='your_jina_key' \\" +echo " ${FULL_IMAGE}" +echo "" diff --git a/agent_templates/scripts/check_image_content.sh b/agent_templates/scripts/check_image_content.sh new file mode 100755 index 0000000..89a5ea7 --- /dev/null +++ b/agent_templates/scripts/check_image_content.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# 创建临时容器检查镜像内容 +kubectl run test-image-content --rm -i --image=agnettaiji.azurecr.io/ai-agents/search-agent:v2.0-1768476670 --restart=Never -- sh -c "head -20 /app/search_agent.py | grep -E '^from|^import'" diff --git a/agent_templates/scripts/rebuild_all.sh b/agent_templates/scripts/rebuild_all.sh new file mode 100755 index 0000000..3c761d6 --- /dev/null +++ b/agent_templates/scripts/rebuild_all.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# 重新构建所有 Agent 镜像并推送到 ACR (ARM64) + +set -e + +ACR_NAME="agnettaiji.azurecr.io" +TAG="latest" +PLATFORM="linux/arm64" + +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} 重新构建所有 Agent 镜像${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" + +# 登录 ACR +echo -e "${GREEN}登录 ACR...${NC}" +az acr login --name $(echo ${ACR_NAME} | cut -d'.' -f1) + +# Agent 列表 +declare -a AGENTS=( + "search-agent:search_agent.Dockerfile" + "jina-search-agent:jina_search_agent.Dockerfile" + "mysql-agent:mysql_agent.Dockerfile" + "postgresql-agent:postgresql_agent.Dockerfile" + "azure-blob-agent:azure_blob_agent.Dockerfile" +) + +SUCCESS=0 +FAILED=0 + +for item in "${AGENTS[@]}"; do + IFS=':' read -r name dockerfile <<< "$item" + image="${ACR_NAME}/ai-agents/${name}:${TAG}" + + echo "" + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE}构建: ${name}${NC}" + echo -e "${BLUE}========================================${NC}" + echo "镜像: ${image}" + echo "平台: ${PLATFORM}" + echo "" + + if docker buildx build --platform ${PLATFORM} \ + -f ${dockerfile} \ + -t ${image} \ + --push \ + . ; then + echo -e "${GREEN}✅ ${name} 构建成功${NC}" + ((SUCCESS++)) + else + echo -e "${RED}❌ ${name} 构建失败${NC}" + ((FAILED++)) + fi +done + +echo "" +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} 构建总结${NC}" +echo -e "${BLUE}========================================${NC}" +echo -e "${GREEN}成功: ${SUCCESS}${NC}" +echo -e "${RED}失败: ${FAILED}${NC}" +echo "" + +if [ ${FAILED} -eq 0 ]; then + echo -e "${GREEN}✅ 所有镜像构建成功!${NC}" + echo "" + echo "已推送的镜像:" + for item in "${AGENTS[@]}"; do + IFS=':' read -r name _ <<< "$item" + echo " - ${ACR_NAME}/ai-agents/${name}:${TAG}" + done +else + echo -e "${RED}❌ 部分镜像构建失败${NC}" + exit 1 +fi + diff --git a/agent_templates/test_azure_blob_agent.sh b/agent_templates/test_azure_blob_agent.sh deleted file mode 100755 index 11b5199..0000000 --- a/agent_templates/test_azure_blob_agent.sh +++ /dev/null @@ -1,148 +0,0 @@ -#!/bin/bash - -# Azure Blob Storage Agent 本地测试脚本 -# 使用方法: ./test_azure_blob_agent.sh - -set -e - -AGENT_HOST="localhost" -AGENT_PORT="8080" -BASE_URL="http://${AGENT_HOST}:${AGENT_PORT}" - -echo "==========================================" -echo "Azure Blob Storage Agent 测试脚本" -echo "==========================================" -echo "" - -# 颜色定义 -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# 测试函数 -test_endpoint() { - local name=$1 - local method=$2 - local endpoint=$3 - local data=$4 - - echo -e "${YELLOW}测试: ${name}${NC}" - echo "请求: ${method} ${endpoint}" - - if [ -z "$data" ]; then - response=$(curl -s -w "\n%{http_code}" -X ${method} "${BASE_URL}${endpoint}") - else - response=$(curl -s -w "\n%{http_code}" -X ${method} "${BASE_URL}${endpoint}" \ - -H 'Content-Type: application/json' \ - -d "${data}") - fi - - http_code=$(echo "$response" | tail -n1) - body=$(echo "$response" | sed '$d') - - if [ "$http_code" -eq 200 ] || [ "$http_code" -eq 201 ]; then - echo -e "${GREEN}✅ 成功 (HTTP $http_code)${NC}" - echo "响应: $body" | jq '.' 2>/dev/null || echo "$body" - else - echo -e "${RED}❌ 失败 (HTTP $http_code)${NC}" - echo "响应: $body" - fi - - echo "" -} - -# 1. 检查容器是否运行 -echo "1️⃣ 检查容器状态..." -if docker ps | grep -q azure-blob-agent; then - echo -e "${GREEN}✅ 容器正在运行${NC}" -else - echo -e "${RED}❌ 容器未运行${NC}" - echo "请先启动容器:" - echo "docker run -d --name azure-blob-agent -p 8080:8080 \\" - echo " -e LITELLM_API_BASE=http://host.docker.internal:4000 \\" - echo " -e LITELLM_MODEL=gpt-3.5-turbo \\" - echo " -e LITELLM_API_KEY=sk-1234 \\" - echo " azure-blob-agent:latest" - exit 1 -fi -echo "" - -# 2. 等待服务就绪 -echo "2️⃣ 等待服务就绪..." -max_attempts=30 -attempt=0 -while [ $attempt -lt $max_attempts ]; do - if curl -s "${BASE_URL}/health" > /dev/null 2>&1; then - echo -e "${GREEN}✅ 服务已就绪${NC}" - break - fi - attempt=$((attempt + 1)) - echo -n "." - sleep 1 -done - -if [ $attempt -eq $max_attempts ]; then - echo -e "${RED}❌ 服务启动超时${NC}" - exit 1 -fi -echo "" - -# 3. 健康检查 -test_endpoint "健康检查" "GET" "/health" - -# 4. 根端点 -test_endpoint "根端点" "GET" "/" - -# 5. 连接到 Azure Storage(需要用户提供连接字符串) -echo -e "${YELLOW}==========================================" -echo "连接到 Azure Storage" -echo "==========================================${NC}" -echo "" -echo "请输入 Azure Storage 连接字符串:" -echo "(格式: DefaultEndpointsProtocol=https;AccountName=xxx;AccountKey=xxx;EndpointSuffix=core.windows.net)" -echo "" -read -r CONNECTION_STRING - -if [ -z "$CONNECTION_STRING" ]; then - echo -e "${YELLOW}⏭️ 跳过连接测试${NC}" -else - connect_data="{\"connection_string\": \"${CONNECTION_STRING}\"}" - test_endpoint "连接 Azure Storage" "POST" "/connect" "$connect_data" - - # 6. 查询测试(仅在连接成功后) - echo -e "${YELLOW}==========================================" - echo "自然语言查询测试" - echo "==========================================${NC}" - echo "" - - # 列出容器 - query_data='{"query": "列出所有容器"}' - test_endpoint "查询: 列出所有容器" "POST" "/query" "$query_data" - - # 获取统计信息 - query_data='{"query": "显示存储统计信息"}' - test_endpoint "查询: 存储统计" "POST" "/query" "$query_data" - - # 自定义查询 - echo -e "${YELLOW}输入自定义查询(按Enter跳过):${NC}" - read -r CUSTOM_QUERY - - if [ ! -z "$CUSTOM_QUERY" ]; then - query_data="{\"query\": \"${CUSTOM_QUERY}\"}" - test_endpoint "自定义查询" "POST" "/query" "$query_data" - fi -fi - -echo "" -echo "==========================================" -echo -e "${GREEN}测试完成!${NC}" -echo "==========================================" -echo "" -echo "查看日志:" -echo " docker logs -f azure-blob-agent" -echo "" -echo "停止容器:" -echo " docker stop azure-blob-agent" -echo " docker rm azure-blob-agent" -echo "" diff --git a/agent_templates/test_client.py b/agent_templates/test_client.py deleted file mode 100755 index c4a16bd..0000000 --- a/agent_templates/test_client.py +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/env python3 -""" -Azure Blob Storage Agent 客户端示例 -演示如何使用 Python 调用 agent API -""" -import requests -import json -import os -import sys - -# Agent 配置 -AGENT_BASE_URL = os.getenv("AGENT_URL", "http://localhost:8080") - -class AzureBlobAgentClient: - """Azure Blob Storage Agent 客户端""" - - def __init__(self, base_url: str = AGENT_BASE_URL): - self.base_url = base_url.rstrip('/') - self.session = requests.Session() - self.connected = False - - def health_check(self) -> dict: - """健康检查""" - response = self.session.get(f"{self.base_url}/health") - response.raise_for_status() - return response.json() - - def connect(self, connection_string: str) -> dict: - """连接到 Azure Storage""" - response = self.session.post( - f"{self.base_url}/connect", - json={"connection_string": connection_string} - ) - response.raise_for_status() - result = response.json() - self.connected = True - return result - - def query(self, query_text: str, container_name: str = None) -> dict: - """执行自然语言查询""" - if not self.connected: - raise Exception("未连接到 Azure Storage,请先调用 connect()") - - payload = {"query": query_text} - if container_name: - payload["container_name"] = container_name - - response = self.session.post( - f"{self.base_url}/query", - json=payload - ) - response.raise_for_status() - return response.json() - - def get_info(self) -> dict: - """获取 agent 信息""" - response = self.session.get(f"{self.base_url}/") - response.raise_for_status() - return response.json() - - -def print_response(title: str, response: dict): - """格式化打印响应""" - print(f"\n{'='*60}") - print(f"📋 {title}") - print('='*60) - print(json.dumps(response, indent=2, ensure_ascii=False)) - - -def main(): - """主函数""" - print("🚀 Azure Blob Storage Agent 客户端") - print(f"连接到: {AGENT_BASE_URL}\n") - - # 创建客户端 - client = AzureBlobAgentClient() - - try: - # 1. 健康检查 - print("1️⃣ 执行健康检查...") - health = client.health_check() - print_response("健康检查", health) - - # 2. 获取 agent 信息 - print("\n2️⃣ 获取 Agent 信息...") - info = client.get_info() - print_response("Agent 信息", info) - - # 3. 连接到 Azure Storage - print("\n3️⃣ 连接到 Azure Storage...") - - # 从环境变量获取连接字符串 - connection_string = os.getenv("AZURE_STORAGE_CONNECTION_STRING") - - if not connection_string: - print("\n⚠️ 未设置 AZURE_STORAGE_CONNECTION_STRING 环境变量") - print("请输入 Azure Storage 连接字符串:") - connection_string = input().strip() - - if not connection_string: - print("❌ 未提供连接字符串,退出") - sys.exit(1) - - connect_result = client.connect(connection_string) - print_response("连接结果", connect_result) - - # 4. 执行查询 - print("\n4️⃣ 执行自然语言查询...\n") - - queries = [ - "列出所有容器", - "显示存储统计信息", - ] - - for query_text in queries: - print(f"\n💬 查询: {query_text}") - result = client.query(query_text) - print(f"\n✅ 答案:\n{result.get('answer', 'N/A')}") - print(f"\n状态: {result.get('status')}") - - # 5. 交互式查询 - print("\n5️⃣ 交互式查询") - print("="*60) - print("输入自然语言查询(输入 'quit' 或 'exit' 退出):") - print("例如:") - print(" - 列出所有容器") - print(" - 显示 images 容器中的文件") - print(" - 在 documents 容器中搜索 report") - print(" - 获取存储统计信息") - print("="*60) - - while True: - try: - query_text = input("\n💬 > ").strip() - - if query_text.lower() in ['quit', 'exit', 'q']: - print("👋 再见!") - break - - if not query_text: - continue - - result = client.query(query_text) - print(f"\n✅ 答案:\n{result.get('answer', 'N/A')}") - - except KeyboardInterrupt: - print("\n\n👋 再见!") - break - except Exception as e: - print(f"\n❌ 查询失败: {str(e)}") - - except requests.exceptions.ConnectionError: - print(f"\n❌ 无法连接到 Agent: {AGENT_BASE_URL}") - print("请确保 Agent 正在运行") - sys.exit(1) - except Exception as e: - print(f"\n❌ 错误: {str(e)}") - import traceback - traceback.print_exc() - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/agent_templates/test_multi_framework.sh b/agent_templates/test_multi_framework.sh deleted file mode 100755 index ddb7a63..0000000 --- a/agent_templates/test_multi_framework.sh +++ /dev/null @@ -1,172 +0,0 @@ -#!/bin/bash - -# 测试 Azure Blob Agent 多框架版本 -# 用法: ./test_multi_framework.sh - -set -e - -echo "🧪 测试 Azure Blob Agent 多框架版本" -echo "======================================" - -# 配置 -AGENT_MANAGER_URL="http://localhost:8000" -OWNER_ID="test-user" -NAMESPACE="ai-agents" - -# Azure Storage 连接字符串(从环境变量获取) -STORAGE_CONN_STRING="${AZURE_STORAGE_CONNECTION_STRING}" - -if [ -z "$STORAGE_CONN_STRING" ]; then - echo "❌ 错误: 请设置环境变量 AZURE_STORAGE_CONNECTION_STRING" - exit 1 -fi - -# 模型配置(从环境变量获取) -MODEL_API_KEY="${OPENAI_API_KEY:-sk-test}" - -echo "" -echo "📋 配置信息:" -echo " - Agent Manager: $AGENT_MANAGER_URL" -echo " - Owner ID: $OWNER_ID" -echo " - Namespace: $NAMESPACE" -echo "" - -# 测试函数 -test_agent() { - local framework=$1 - local template=$2 - local agent_name=$3 - local extra_config=$4 - - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "🧪 测试 $framework 版本" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - - # 构建请求 JSON - local request_json=$(cat </dev/null || echo "") +echo "Service URL: ${SERVICE_URL}" +echo "" + +if [ -z "${SERVICE_URL}" ]; then + echo "❌ 无法获取 Service URL" + exit 1 +fi + +# 5. 测试健康检查 +echo "🏥 步骤 4: 测试健康检查..." +HEALTH_RESPONSE=$(curl -s "${SERVICE_URL}/health") +echo "健康检查响应: ${HEALTH_RESPONSE}" +echo "" + +# 6. 测试搜索功能(如果有配置的 API keys) +echo "🔍 步骤 5: 测试搜索功能..." +SEARCH_RESPONSE=$(curl -s -X POST "${SERVICE_URL}/search" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "什么是Kubernetes?", + "auto_configure": true + }') +echo "搜索响应: ${SEARCH_RESPONSE}" +echo "" + +# 7. 列出所有 Agents +echo "📋 步骤 6: 列出所有 Agents..." +LIST_RESPONSE=$(curl -s "${AGENT_MANAGER_URL}/agents") +echo "Agent 列表: ${LIST_RESPONSE}" +echo "" + +# 8. 清理(可选) +read -p "是否删除测试 Agent? (y/N): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "🗑️ 删除 Agent..." + DELETE_RESPONSE=$(curl -s -X DELETE "${AGENT_MANAGER_URL}/agents/${AGENT_NAME}") + echo "删除响应: ${DELETE_RESPONSE}" + echo "" + echo "✅ Agent 已删除" +else + echo "⏭️ 保留 Agent: ${AGENT_NAME}" + echo "" + echo "手动删除命令:" + echo " curl -X DELETE ${AGENT_MANAGER_URL}/agents/${AGENT_NAME}" +fi + +echo "" +echo "==========================================" +echo "测试完成!" +echo "==========================================" diff --git a/agent_templates/tests/test_search_import.py b/agent_templates/tests/test_search_import.py new file mode 100755 index 0000000..f81161d --- /dev/null +++ b/agent_templates/tests/test_search_import.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""测试 search_agent.py 的导入""" +import sys +import os + +# 模拟 Docker 容器中的路径结构 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'search_agent')) + +print("当前工作目录:", os.getcwd()) +print("Python 路径:", sys.path[:3]) +print("") + +try: + print("测试导入 config...") + from config import Config + print("✅ config.Config 导入成功") + + print("\n测试导入 agent.search_agent...") + from agent.search_agent import SearchAgent + print("✅ agent.search_agent.SearchAgent 导入成功") + + print("\n测试导入 agent_callback_utils...") + from agent_callback_utils import AgentCallbackHandler, CallbackContextManager + print("✅ agent_callback_utils 导入成功") + + print("\n" + "="*50) + print("✅ 所有导入测试通过!") + print("="*50) + +except Exception as e: + print(f"\n❌ 导入失败: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/deploy-to-k8s-arm64.sh b/deploy-to-k8s-arm64.sh new file mode 100755 index 0000000..4c4e797 --- /dev/null +++ b/deploy-to-k8s-arm64.sh @@ -0,0 +1,317 @@ +#!/bin/bash + +############################################################################## +# Agent Manager - Kubernetes 部署脚本 (ARM64 架构) +# 用途: 自动构建 Docker 镜像并部署到 Kubernetes +############################################################################## + +set -e # 遇到错误立即退出 + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 配置变量 +ACR_NAME="agnettaiji" +ACR_LOGIN_SERVER="${ACR_NAME}.azurecr.io" +IMAGE_NAME="agent-manager" +IMAGE_TAG="latest-arm64" +FULL_IMAGE_NAME="${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${IMAGE_TAG}" +NAMESPACE="agent-manager" +K8S_DIR="./k8s" + +# Azure 凭据配置(需要替换为实际值) +AZURE_TENANT_ID="${AZURE_TENANT_ID:-your-tenant-id}" +AZURE_CLIENT_ID="${AZURE_CLIENT_ID:-your-client-id}" +AZURE_CLIENT_SECRET="${AZURE_CLIENT_SECRET:-your-client-secret}" +AZURE_SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID:-your-subscription-id}" +AZURE_RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-your-resource-group}" + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} Agent Manager K8s 部署 (ARM64)${NC}" +echo -e "${BLUE}========================================${NC}" + +# 函数: 打印步骤 +print_step() { + echo -e "\n${GREEN}==>${NC} ${BLUE}$1${NC}" +} + +# 函数: 打印错误 +print_error() { + echo -e "${RED}❌ 错误: $1${NC}" +} + +# 函数: 打印成功 +print_success() { + echo -e "${GREEN}✅ $1${NC}" +} + +# 函数: 打印警告 +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +# 检查必要的命令 +check_prerequisites() { + print_step "检查必要的工具..." + + local tools=("docker" "kubectl" "az") + for tool in "${tools[@]}"; do + if ! command -v $tool &> /dev/null; then + print_error "$tool 未安装,请先安装" + exit 1 + fi + print_success "$tool 已安装" + done +} + +# 登录 Azure Container Registry +login_acr() { + print_step "登录到 Azure Container Registry..." + + if az acr login --name ${ACR_NAME}; then + print_success "ACR 登录成功" + else + print_error "ACR 登录失败" + exit 1 + fi +} + +# 构建 Docker 镜像 +build_image() { + print_step "构建 ARM64 Docker 镜像..." + + echo "镜像名称: ${FULL_IMAGE_NAME}" + + # 使用 buildx 支持多架构构建 + if ! docker buildx version &> /dev/null; then + print_warning "docker buildx 未启用,尝试启用..." + docker buildx create --use + fi + + # 构建镜像 + if docker buildx build \ + --platform linux/arm64 \ + -f Dockerfile.arm64 \ + -t ${FULL_IMAGE_NAME} \ + --push \ + .; then + print_success "镜像构建并推送成功" + else + print_error "镜像构建失败" + exit 1 + fi +} + +# 创建 Kubernetes 命名空间 +create_namespace() { + print_step "创建 Kubernetes 命名空间..." + + if kubectl get namespace ${NAMESPACE} &> /dev/null; then + print_warning "命名空间 ${NAMESPACE} 已存在" + else + kubectl apply -f ${K8S_DIR}/agent-manager-namespace.yaml + print_success "命名空间创建成功" + fi +} + +# 创建 ACR Secret +create_acr_secret() { + print_step "创建 ACR 拉取凭据..." + + if kubectl get secret acr-secret -n ${NAMESPACE} &> /dev/null; then + print_warning "ACR secret 已存在,删除重建" + kubectl delete secret acr-secret -n ${NAMESPACE} + fi + + # 获取 ACR 凭据 + ACR_USERNAME=$(az acr credential show --name ${ACR_NAME} --query username -o tsv) + ACR_PASSWORD=$(az acr credential show --name ${ACR_NAME} --query passwords[0].value -o tsv) + + kubectl create secret docker-registry acr-secret \ + --namespace=${NAMESPACE} \ + --docker-server=${ACR_LOGIN_SERVER} \ + --docker-username=${ACR_USERNAME} \ + --docker-password=${ACR_PASSWORD} + + print_success "ACR Secret 创建成功" +} + +# 更新 ConfigMap 和 Secret +update_config() { + print_step "更新配置..." + + # 检查是否需要更新 Azure 凭据 + if [ "$AZURE_TENANT_ID" = "your-tenant-id" ]; then + print_warning "请在脚本中设置 Azure 凭据环境变量" + read -p "是否继续部署(不含 Azure DNS 功能)?[y/N] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi + fi + + # 创建临时 secret 文件 + cat > /tmp/agent-manager-secret.yaml < /dev/null; then + print_warning "kubeconfig secret 已存在" + else + kubectl create secret generic kubeconfig-secret \ + --from-file=config=/home/${USER}/.kube/config \ + -n ${NAMESPACE} + print_success "kubeconfig Secret 创建成功" + fi + else + print_warning "未找到 kubeconfig 文件,跳过" + fi +} + +# 部署 RBAC +deploy_rbac() { + print_step "部署 RBAC 权限..." + kubectl apply -f ${K8S_DIR}/agent-manager-rbac.yaml + print_success "RBAC 部署成功" +} + +# 部署应用 +deploy_app() { + print_step "部署 Agent Manager 应用..." + + # 更新 Deployment 中的镜像 + kubectl apply -f ${K8S_DIR}/agent-manager-deployment.yaml + kubectl apply -f ${K8S_DIR}/agent-manager-service.yaml + + print_success "应用部署成功" +} + +# 等待部署完成 +wait_for_deployment() { + print_step "等待部署完成..." + + kubectl rollout status deployment/agent-manager -n ${NAMESPACE} --timeout=300s + print_success "部署已就绪" +} + +# 显示部署信息 +show_deployment_info() { + print_step "部署信息:" + + echo -e "\n${BLUE}Pods:${NC}" + kubectl get pods -n ${NAMESPACE} -o wide + + echo -e "\n${BLUE}Services:${NC}" + kubectl get svc -n ${NAMESPACE} + + echo -e "\n${BLUE}获取外网访问地址:${NC}" + EXTERNAL_IP=$(kubectl get svc agent-manager -n ${NAMESPACE} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + + if [ -z "$EXTERNAL_IP" ]; then + print_warning "LoadBalancer IP 正在分配中..." + echo "运行以下命令查看 IP: kubectl get svc agent-manager -n ${NAMESPACE}" + else + print_success "外网访问地址: http://${EXTERNAL_IP}" + echo -e "\n测试访问:" + echo " curl http://${EXTERNAL_IP}/" + fi +} + +# 查看日志 +show_logs() { + print_step "最近的日志:" + kubectl logs -n ${NAMESPACE} -l app=agent-manager --tail=50 +} + +# 主函数 +main() { + local skip_build=false + local skip_deploy=false + + # 解析参数 + while [[ $# -gt 0 ]]; do + case $1 in + --skip-build) + skip_build=true + shift + ;; + --skip-deploy) + skip_deploy=true + shift + ;; + --help) + echo "用法: $0 [选项]" + echo "选项:" + echo " --skip-build 跳过镜像构建" + echo " --skip-deploy 跳过应用部署" + echo " --help 显示帮助" + exit 0 + ;; + *) + print_error "未知参数: $1" + exit 1 + ;; + esac + done + + # 执行部署 + check_prerequisites + + if [ "$skip_build" = false ]; then + login_acr + build_image + else + print_warning "跳过镜像构建" + fi + + if [ "$skip_deploy" = false ]; then + create_namespace + create_acr_secret + update_config + create_kubeconfig_secret + deploy_rbac + deploy_app + wait_for_deployment + show_deployment_info + + echo -e "\n${GREEN}========================================${NC}" + echo -e "${GREEN} 部署完成!${NC}" + echo -e "${GREEN}========================================${NC}" + + read -p "是否查看日志?[y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + show_logs + fi + else + print_warning "跳过应用部署" + fi +} + +# 运行主函数 +main "$@" diff --git a/k8s/agent-manager-configmap.yaml b/k8s/agent-manager-configmap.yaml new file mode 100644 index 0000000..b039168 --- /dev/null +++ b/k8s/agent-manager-configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: agent-manager-config + namespace: agent-manager +data: + NAMESPACE: "agent-manager" + DATABASE_URL: "postgresql://taiji:By%40123456.@taijipda.postgres.database.azure.com:5432/taijiagnet" + # Azure DNS 配置(如果需要) + AZURE_DNS_ZONE: "taijiagnet.com" + AZURE_SUBSCRIPTION_ID: "your-subscription-id" + AZURE_RESOURCE_GROUP: "your-resource-group" diff --git a/k8s/agent-manager-deployment.yaml b/k8s/agent-manager-deployment.yaml new file mode 100644 index 0000000..b0afbe6 --- /dev/null +++ b/k8s/agent-manager-deployment.yaml @@ -0,0 +1,107 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: agent-manager + namespace: agent-manager + labels: + app: agent-manager +spec: + replicas: 2 + selector: + matchLabels: + app: agent-manager + template: + metadata: + labels: + app: agent-manager + spec: + # 使用专用的 ServiceAccount + serviceAccountName: agent-manager + + # ARM 架构节点选择器 + nodeSelector: + kubernetes.io/arch: arm64 + + # 容忍度(如果需要) + tolerations: + - key: "kubernetes.io/arch" + operator: "Equal" + value: "arm64" + effect: "NoSchedule" + + containers: + - name: agent-manager + image: agnettaiji.azurecr.io/agent-manager:latest-arm64 + imagePullPolicy: Always + + ports: + - containerPort: 8000 + name: http + protocol: TCP + + # 环境变量 - 从 ConfigMap + envFrom: + - configMapRef: + name: agent-manager-config + + # 环境变量 - 从 Secret + env: + - name: AZURE_TENANT_ID + valueFrom: + secretKeyRef: + name: agent-manager-secret + key: AZURE_TENANT_ID + - name: AZURE_CLIENT_ID + valueFrom: + secretKeyRef: + name: agent-manager-secret + key: AZURE_CLIENT_ID + - name: AZURE_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: agent-manager-secret + key: AZURE_CLIENT_SECRET + + # 挂载 kubeconfig(用于管理其他 Agent) + volumeMounts: + - name: kubeconfig + mountPath: /root/.kube + readOnly: true + + # 资源限制 + resources: + requests: + memory: "256Mi" + cpu: "200m" + limits: + memory: "512Mi" + cpu: "500m" + + # 健康检查 + livenessProbe: + httpGet: + path: / + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + + readinessProbe: + httpGet: + path: / + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + + volumes: + - name: kubeconfig + secret: + secretName: kubeconfig-secret + optional: true + + # 使用 ACR 拉取镜像的凭据 + imagePullSecrets: + - name: acr-secret diff --git a/k8s/agent-manager-namespace.yaml b/k8s/agent-manager-namespace.yaml new file mode 100644 index 0000000..96d88c2 --- /dev/null +++ b/k8s/agent-manager-namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: agent-manager + labels: + app: agent-manager diff --git a/k8s/agent-manager-rbac.yaml b/k8s/agent-manager-rbac.yaml new file mode 100644 index 0000000..0859137 --- /dev/null +++ b/k8s/agent-manager-rbac.yaml @@ -0,0 +1,36 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: agent-manager-role +rules: +- apiGroups: [""] + resources: ["namespaces", "pods", "services", "configmaps", "secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: ["apps"] + resources: ["deployments", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: ["networking.k8s.io"] + resources: ["ingresses"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +- apiGroups: ["metrics.k8s.io"] + resources: ["pods", "nodes"] + verbs: ["get", "list"] +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: agent-manager + namespace: agent-manager +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: agent-manager-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: agent-manager-role +subjects: +- kind: ServiceAccount + name: agent-manager + namespace: agent-manager diff --git a/k8s/agent-manager-secret.yaml b/k8s/agent-manager-secret.yaml new file mode 100644 index 0000000..789473f --- /dev/null +++ b/k8s/agent-manager-secret.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Secret +metadata: + name: agent-manager-secret + namespace: agent-manager +type: Opaque +stringData: + # Azure 凭据 + AZURE_TENANT_ID: "your-tenant-id" + AZURE_CLIENT_ID: "your-client-id" + AZURE_CLIENT_SECRET: "your-client-secret" + + # 数据库密码(如果需要单独管理) + # DB_PASSWORD: "By@123456." diff --git a/k8s/agent-manager-service.yaml b/k8s/agent-manager-service.yaml new file mode 100644 index 0000000..23201c4 --- /dev/null +++ b/k8s/agent-manager-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: agent-manager + namespace: agent-manager + labels: + app: agent-manager +spec: + type: LoadBalancer + selector: + app: agent-manager + ports: + - name: http + port: 80 + targetPort: 8000 + protocol: TCP + sessionAffinity: ClientIP diff --git a/migrate_template_id_nullable.py b/migrate_template_id_nullable.py new file mode 100755 index 0000000..679ec1a --- /dev/null +++ b/migrate_template_id_nullable.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +""" +数据库迁移:将 template_id 改为可选字段 +""" +import os +from sqlalchemy import create_engine, text + +# 数据库连接 +DATABASE_URL = "postgresql://taiji:By%40123456.@taijipda.postgres.database.azure.com:5432/taijiagnet" + +print("🔄 开始数据库迁移...") +print(f"数据库: {DATABASE_URL.split('@')[1]}") + +engine = create_engine(DATABASE_URL) + +try: + with engine.connect() as conn: + # 修改 template_id 列为可空 + print("\n1️⃣ 修改 template_id 为可空字段...") + conn.execute(text(""" + ALTER TABLE agents + ALTER COLUMN template_id DROP NOT NULL; + """)) + conn.commit() + print("✅ template_id 已改为可空") + + # 验证更改 + print("\n2️⃣ 验证表结构...") + result = conn.execute(text(""" + SELECT column_name, is_nullable, data_type + FROM information_schema.columns + WHERE table_name = 'agents' + AND column_name = 'template_id'; + """)) + + for row in result: + print(f"✅ {row[0]}: nullable={row[1]}, type={row[2]}") + + print("\n✅ 数据库迁移成功完成!") + print("\n现在可以创建没有 template_id 的 Agent 了。") + +except Exception as e: + print(f"\n❌ 迁移失败: {e}") + import traceback + traceback.print_exc() diff --git a/plans/LiteLLM和AgentManager回调接口文档.md b/plans/LiteLLM和AgentManager回调接口文档.md new file mode 100644 index 0000000..d9c314b --- /dev/null +++ b/plans/LiteLLM和AgentManager回调接口文档.md @@ -0,0 +1,522 @@ +# LiteLLM 和 Agent Manager 回调接口文档 + +## 概述 + +本文档描述了 mcp-server 接收 LiteLLM 和 Agent Manager 服务的回调接口规范,包括请求体格式、响应体格式以及处理逻辑。 + +--- + +## 1. LiteLLM 回调接口 + +### 1.1 接口信息 + +- **接口路径**: `/api/v1/billing/litellm-callback` +- **请求方法**: `POST` +- **Content-Type**: `application/json` +- **功能**: 接收 LiteLLM 的实时 Token 计费数据,支持单个对象或批量数组格式 + +### 1.2 请求体格式 + +#### 1.2.1 单个对象格式 + +```json +{ + "id": "call-abc123", // 必填:调用ID(别名:call_id) + "trace_id": "trace-xyz789", // 可选:追踪ID + "model": "gpt-4", // 必填:模型名称 + "call_type": "completion", // 可选:调用类型 + "cache_hit": false, // 可选:是否缓存命中 + "stream": false, // 可选:是否流式响应 + "status": "success", // 可选:状态(默认:success) + "custom_llm_provider": "openai", // 可选:LLM提供商 + "startTime": "2026-01-11T10:00:00Z", // 可选:开始时间(ISO 8601 或 Unix 时间戳) + "endTime": "2026-01-11T10:00:05Z", // 可选:结束时间(ISO 8601 或 Unix 时间戳) + "response_time": 5.2, // 可选:响应时间(秒) + "response_cost": 0.001, // 可选:响应成本(美元) + "total_tokens": 1500, // 可选:总Token数 + "prompt_tokens": 1000, // 可选:Prompt Token数 + "completion_tokens": 500, // 可选:Completion Token数 + "api_key": "sk-xxx", // 可选:API密钥 + "team_id": "team-123", // 可选:团队ID + "api_base": "https://api.openai.com", // 可选:API基础URL + "model_group": "gpt-4", // 可选:模型组 + "model_id": "gpt-4-0613", // 可选:模型ID + "messages": [ // 可选:消息列表 + { + "role": "user", + "content": "Hello" + } + ], + "response": { // 可选:响应内容 + "choices": [...] + }, + "metadata": { // 可选:元数据(重要:包含租户信息) + "user_api_key_hash": "hash-xxx", // API密钥哈希 + "user_api_key_team_id": "team-123", // 团队ID + "user_api_key_auth_metadata": { // 租户认证元数据(优先使用) + "tenant_id": "tenant-123", + "channel_id": "channel-456", + "tenant_name": "租户名称" + }, + "usage_object": { // Token使用量对象 + "total_tokens": 1500, + "prompt_tokens": 1000, + "completion_tokens": 500 + } + }, + "hidden_params": {}, // 可选:隐藏参数 + "model_map_information": {}, // 可选:模型映射信息 + "cost_breakdown": {}, // 可选:成本明细 + "error_str": null, // 可选:错误信息 + "error_information": {} // 可选:错误详情 +} +``` + +#### 1.2.2 批量数组格式 + +```json +[ + { + "id": "call-abc123", + "model": "gpt-4", + "total_tokens": 1500, + "metadata": { + "user_api_key_auth_metadata": { + "tenant_id": "tenant-123", + "channel_id": "channel-456" + } + } + }, + { + "id": "call-xyz789", + "model": "gpt-3.5-turbo", + "total_tokens": 800, + "metadata": { + "user_api_key_auth_metadata": { + "tenant_id": "tenant-123", + "channel_id": "channel-456" + } + } + } +] +``` + +#### 1.2.3 字段说明 + +| 字段名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| `id` / `call_id` | string | 是 | 调用唯一标识符,用于幂等性检查 | +| `trace_id` | string | 否 | 追踪ID,当 `id` 不存在时作为备用 | +| `model` | string | 是 | 模型名称(如:gpt-4, gpt-3.5-turbo) | +| `total_tokens` | integer | 否 | 总Token数,用于计算EU消耗 | +| `prompt_tokens` | integer | 否 | Prompt Token数 | +| `completion_tokens` | integer | 否 | Completion Token数 | +| `startTime` | string/float | 否 | 开始时间(ISO 8601 字符串或 Unix 时间戳) | +| `endTime` | string/float | 否 | 结束时间(ISO 8601 字符串或 Unix 时间戳) | +| `response_cost` | float | 否 | 响应成本(美元) | +| `metadata` | object | 否 | 元数据对象,包含租户信息 | +| `metadata.user_api_key_hash` | string | 否 | API密钥哈希值 | +| `metadata.user_api_key_team_id` | string | 否 | 团队ID | +| `metadata.user_api_key_auth_metadata` | object | 否 | **优先使用**:租户认证元数据 | +| `metadata.user_api_key_auth_metadata.tenant_id` | string | 否 | 租户ID | +| `metadata.user_api_key_auth_metadata.channel_id` | string | 否 | 渠道ID | +| `metadata.usage_object` | object | 否 | Token使用量对象(备用) | + +### 1.3 响应体格式 + +#### 1.3.1 单个对象响应 + +```json +{ + "message": "Success", + "call_id": "call-abc123", + "record_id": "550e8400-e29b-41d4-a716-446655440000", + "eu_consumed": 0.15, + "balance_updated": true +} +``` + +#### 1.3.2 批量数组响应 + +```json +{ + "message": "Batch processed", + "count": 2, + "results": [ + { + "message": "Success", + "call_id": "call-abc123", + "record_id": "550e8400-e29b-41d4-a716-446655440000", + "eu_consumed": 0.15, + "balance_updated": true + }, + { + "message": "Success", + "call_id": "call-xyz789", + "record_id": "550e8400-e29b-41d4-a716-446655440001", + "eu_consumed": 0.08, + "balance_updated": true + } + ] +} +``` + +#### 1.3.3 错误响应 + +```json +{ + "detail": "无法解析租户ID" +} +``` + +**HTTP 状态码**: +- `200`: 成功处理 +- `400`: 请求参数错误(如无法解析租户ID) +- `500`: 服务器内部错误 + +#### 1.3.4 响应字段说明 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `message` | string | 处理结果消息("Success" / "Already processed" / "Skipped - no call_id") | +| `call_id` | string | 调用ID | +| `record_id` | string | 计费记录ID(UUID) | +| `eu_consumed` | float | 消耗的EU数量 | +| `balance_updated` | boolean | 是否成功更新余额 | + +### 1.4 处理逻辑 + +1. **幂等性检查**: 根据 `call_id` 检查是否已处理过,避免重复计费 +2. **租户信息解析**: + - 优先从 `metadata.user_api_key_auth_metadata` 获取租户信息 + - 如果不存在,则通过 `metadata.user_api_key_hash` 从数据库查询 +3. **Token计算**: + - 优先使用顶级字段 `total_tokens`、`prompt_tokens`、`completion_tokens` + - 如果不存在,从 `metadata.usage_object` 获取 +4. **EU计算**: 根据模型类型和Token数量计算EU消耗 + - GPT-4: 0.0001 EU/token + - GPT-3.5-turbo: 0.00005 EU/token + - 默认: 0.0001 EU/token +5. **余额扣减**: 使用数据库行锁确保并发安全,原子性更新用户余额 +6. **计费记录**: 创建 `ModelBillingRecord` 记录,保存完整的回调数据 + +### 1.5 健康检查接口 + +- **接口路径**: `/api/v1/billing/litellm-callback/health` +- **请求方法**: `GET` +- **响应**: +```json +{ + "status": "ok", + "endpoint": "/api/v1/billing/litellm-callback" +} +``` + +--- + +## 2. Agent Manager 回调接口 + +### 2.1 接口信息 + +- **接口路径**: `/api/v1/billing/agent-callback` +- **请求方法**: `POST` +- **Content-Type**: `application/json` +- **功能**: 接收 Agent Manager 的 Agent 运行时信息,用于记录运行时长并计费 + +### 2.2 请求体格式 + +```json +{ + "agentName": "taiji-assistant-abc123", // 必填:Agent 名称 + "userId": "user-123-456", // 必填:用户 ID + "podRunningTimeSeconds": 120, // 必填:Pod 运行时间(秒),即 VM 运行时间 + "toolsUsed": [ // 可选:使用的工具列表 + "web_search", + "calculator", + "file_reader" + ], + "startTime": "2026-01-11T10:00:00Z", // 可选:开始时间(ISO 8601 格式) + "endTime": "2026-01-11T10:02:00Z", // 可选:结束时间(ISO 8601 格式) + "requestId": "req-abc-123" // 可选:请求 ID +} +``` + +### 2.3 请求字段说明 + +| 字段名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| `agentName` | string | 是 | Agent 名称,用于标识具体的 Agent 实例 | +| `userId` | string | 是 | 用户 ID,必须是系统中存在的用户 | +| `podRunningTimeSeconds` | integer | 是 | Pod 运行时间(秒),即 VM 实际运行时长,用于计费 | +| `toolsUsed` | array[string] | 否 | 使用的工具列表,用于记录 Agent 调用的工具 | +| `startTime` | string | 否 | 开始时间,ISO 8601 格式(如:2026-01-11T10:00:00Z) | +| `endTime` | string | 否 | 结束时间,ISO 8601 格式(如:2026-01-11T10:02:00Z) | +| `requestId` | string | 否 | 请求 ID,用于追踪和关联请求 | + +### 2.4 响应体格式 + +#### 2.4.1 成功响应 + +```json +{ + "success": true, + "message": "Agent 计费记录创建成功", + "recordId": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +#### 2.4.2 错误响应 + +**用户不存在**: +```json +{ + "detail": "用户不存在: user-123-456" +} +``` +HTTP 状态码: `404` + +**服务器错误**: +```json +{ + "detail": "回调处理失败: [错误详情]" +} +``` +HTTP 状态码: `500` + +#### 2.4.3 响应字段说明 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `success` | boolean | 是否成功处理 | +| `message` | string | 处理结果消息 | +| `recordId` | string | 创建的计费记录ID(UUID),失败时为 null | + +### 2.5 处理逻辑 + +1. **用户验证**: 验证 `userId` 是否存在,不存在则返回 404 错误 +2. **时间解析**: 解析 `startTime` 和 `endTime`(ISO 8601 格式),转换为 UTC 时间 +3. **成本计算**: + - 根据 `podRunningTimeSeconds` 计算 EU 消耗 + - 根据 Agent 类型和运行时长计算成本(平台 Agent 使用 `calculate_platform_agent_cost`) +4. **计费记录**: 创建 `AgentBillingRecord` 记录,包含: + - 用户ID、渠道ID + - Agent 名称、类型 + - 运行时长、EU消耗、成本 + - 开始时间、结束时间 + - 使用的工具列表 + - 请求ID +5. **余额扣减**: 调用 `deduct_balance` 扣除用户余额 +6. **事务提交**: 所有操作在数据库事务中执行,失败时回滚 + +### 2.6 健康检查接口 + +- **接口路径**: `/api/v1/billing/agent-callback/health` +- **请求方法**: `GET` +- **响应**: +```json +{ + "status": "ok", + "endpoint": "/api/v1/billing/agent-callback" +} +``` + +--- + +## 3. 通用说明 + +### 3.1 认证 + +目前两个回调接口均未实现认证机制,建议在生产环境中添加: +- API Key 认证 +- IP 白名单 +- 签名验证 + +### 3.2 幂等性 + +- **LiteLLM 回调**: 通过 `call_id` 实现幂等性,相同 `call_id` 的请求只会处理一次 +- **Agent Manager 回调**: 目前未实现幂等性,建议添加 `requestId` 的唯一性检查 + +### 3.3 错误处理 + +- 所有错误都会记录到日志中 +- 数据库操作失败时会自动回滚事务 +- 返回适当的 HTTP 状态码和错误信息 + +### 3.4 性能考虑 + +- LiteLLM 回调支持批量处理,提高吞吐量 +- 使用数据库行锁确保并发安全 +- 余额更新使用原子操作 + +### 3.5 数据存储 + +- **LiteLLM 回调**: 数据存储在 `ModelBillingRecord` 表中 +- **Agent Manager 回调**: 数据存储在 `AgentBillingRecord` 表中 +- 所有回调的原始数据都会保存,便于后续审计和分析 + +--- + +## 4. 示例代码 + +### 4.1 LiteLLM 回调示例(cURL) + +```bash +# 单个对象 +curl -X POST http://mcp-server:8002/api/v1/billing/litellm-callback \ + -H "Content-Type: application/json" \ + -d '{ + "id": "call-abc123", + "model": "gpt-4", + "total_tokens": 1500, + "prompt_tokens": 1000, + "completion_tokens": 500, + "metadata": { + "user_api_key_auth_metadata": { + "tenant_id": "tenant-123", + "channel_id": "channel-456" + } + } + }' + +# 批量数组 +curl -X POST http://mcp-server:8002/api/v1/billing/litellm-callback \ + -H "Content-Type: application/json" \ + -d '[ + { + "id": "call-abc123", + "model": "gpt-4", + "total_tokens": 1500, + "metadata": { + "user_api_key_auth_metadata": { + "tenant_id": "tenant-123", + "channel_id": "channel-456" + } + } + }, + { + "id": "call-xyz789", + "model": "gpt-3.5-turbo", + "total_tokens": 800, + "metadata": { + "user_api_key_auth_metadata": { + "tenant_id": "tenant-123", + "channel_id": "channel-456" + } + } + } + ]' +``` + +### 4.2 Agent Manager 回调示例(cURL) + +```bash +curl -X POST http://mcp-server:8002/api/v1/billing/agent-callback \ + -H "Content-Type: application/json" \ + -d '{ + "agentName": "taiji-assistant-abc123", + "userId": "user-123-456", + "podRunningTimeSeconds": 120, + "toolsUsed": ["web_search", "calculator"], + "startTime": "2026-01-11T10:00:00Z", + "endTime": "2026-01-11T10:02:00Z", + "requestId": "req-abc-123" + }' +``` + +### 4.3 Python 示例 + +```python +import requests + +# LiteLLM 回调 +litellm_data = { + "id": "call-abc123", + "model": "gpt-4", + "total_tokens": 1500, + "metadata": { + "user_api_key_auth_metadata": { + "tenant_id": "tenant-123", + "channel_id": "channel-456" + } + } +} + +response = requests.post( + "http://mcp-server:8002/api/v1/billing/litellm-callback", + json=litellm_data +) +print(response.json()) + +# Agent Manager 回调 +agent_data = { + "agentName": "taiji-assistant-abc123", + "userId": "user-123-456", + "podRunningTimeSeconds": 120, + "toolsUsed": ["web_search", "calculator"], + "startTime": "2026-01-11T10:00:00Z", + "endTime": "2026-01-11T10:02:00Z", + "requestId": "req-abc-123" +} + +response = requests.post( + "http://mcp-server:8002/api/v1/billing/agent-callback", + json=agent_data +) +print(response.json()) +``` + +--- + +## 5. 配置说明 + +### 5.1 LiteLLM 配置 + +在 LiteLLM 配置文件中设置回调地址: + +```yaml +general_settings: + success_callback: ["webhook"] + failure_callback: ["webhook"] + webhook_url: "http://mcp-server:8002/api/v1/billing/litellm-callback" + webhook_headers: + Content-Type: "application/json" +``` + +### 5.2 Agent Manager 配置 + +Agent Manager 需要在 Agent 运行结束后调用回调接口,配置回调地址: + +``` +CALLBACK_URL=http://mcp-server:8002/api/v1/billing/agent-callback +``` + +--- + +## 6. 注意事项 + +1. **时间格式**: + - LiteLLM 回调支持 ISO 8601 字符串和 Unix 时间戳 + - Agent Manager 回调仅支持 ISO 8601 格式 + +2. **租户信息**: + - LiteLLM 回调优先从 `metadata.user_api_key_auth_metadata` 获取租户信息 + - 如果不存在,会尝试从数据库查询,但可能失败 + +3. **Token 计算**: + - 优先使用顶级字段,其次使用 `metadata.usage_object` + - 如果都不存在,Token 数默认为 0 + +4. **并发安全**: + - 使用数据库行锁确保余额更新的原子性 + - 建议在生产环境中使用连接池和适当的并发控制 + +5. **日志记录**: + - 所有回调都会记录详细日志 + - 建议配置日志轮转和监控告警 + +--- + +## 7. 更新日志 + +- **2026-01-11**: 初始版本,包含 LiteLLM 和 Agent Manager 回调接口文档 + diff --git a/quick-deploy-k8s.sh b/quick-deploy-k8s.sh new file mode 100755 index 0000000..e76b080 --- /dev/null +++ b/quick-deploy-k8s.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +############################################################################## +# Agent Manager - 快速部署脚本 (仅部署,不构建镜像) +############################################################################## + +set -e + +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}快速部署 Agent Manager 到 Kubernetes...${NC}\n" + +NAMESPACE="agent-manager" +K8S_DIR="./k8s" + +# 1. 创建命名空间 +echo "1️⃣ 创建命名空间..." +kubectl apply -f ${K8S_DIR}/agent-manager-namespace.yaml + +# 2. 创建配置 +echo "2️⃣ 创建配置..." +kubectl apply -f ${K8S_DIR}/agent-manager-configmap.yaml +kubectl apply -f ${K8S_DIR}/agent-manager-secret.yaml + +# 3. 创建 RBAC +echo "3️⃣ 创建 RBAC..." +kubectl apply -f ${K8S_DIR}/agent-manager-rbac.yaml + +# 4. 创建 ACR Secret(如果需要) +if [ -n "$ACR_USERNAME" ] && [ -n "$ACR_PASSWORD" ]; then + echo "4️⃣ 创建 ACR Secret..." + kubectl create secret docker-registry acr-secret \ + --namespace=${NAMESPACE} \ + --docker-server=agnettaiji.azurecr.io \ + --docker-username=${ACR_USERNAME} \ + --docker-password=${ACR_PASSWORD} \ + --dry-run=client -o yaml | kubectl apply -f - +fi + +# 5. 部署应用 +echo "5️⃣ 部署应用..." +kubectl apply -f ${K8S_DIR}/agent-manager-deployment.yaml +kubectl apply -f ${K8S_DIR}/agent-manager-service.yaml + +# 6. 等待就绪 +echo "6️⃣ 等待部署完成..." +kubectl rollout status deployment/agent-manager -n ${NAMESPACE} --timeout=300s + +# 7. 显示状态 +echo -e "\n${GREEN}✅ 部署完成!${NC}\n" +echo "查看 Pods:" +kubectl get pods -n ${NAMESPACE} +echo -e "\n查看 Services:" +kubectl get svc -n ${NAMESPACE} + +echo -e "\n获取外网 IP:" +echo "kubectl get svc agent-manager -n ${NAMESPACE} -o jsonpath='{.status.loadBalancer.ingress[0].ip}'" diff --git a/start_with_pgsql.sh b/start_with_pgsql.sh new file mode 100755 index 0000000..0904956 --- /dev/null +++ b/start_with_pgsql.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# 启动 Agent Manager 服务(使用 PostgreSQL) + +# 设置数据库环境变量 +export DATABASE_URL="postgresql://taiji:By%40123456.@taijipda.postgres.database.azure.com:5432/taijiagnet" + +# 设置 Azure DNS 凭据(如果需要) +export AZURE_TENANT_ID="${AZURE_TENANT_ID}" +export AZURE_CLIENT_ID="${AZURE_CLIENT_ID}" +export AZURE_CLIENT_SECRET="${AZURE_CLIENT_SECRET}" +export AZURE_SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID}" +export AZURE_RESOURCE_GROUP="${AZURE_RESOURCE_GROUP}" +export AZURE_DNS_ZONE="${AZURE_DNS_ZONE:-taijiagnet.com}" + +# 设置 Kubernetes namespace +export NAMESPACE="${NAMESPACE:-ai-agents}" + +echo "===========================================" +echo "🚀 启动 Agent Manager 服务" +echo "===========================================" +echo "数据库: PostgreSQL (Azure)" +echo "数据库URL: ${DATABASE_URL}" +echo "DNS域名: ${AZURE_DNS_ZONE}" +echo "命名空间: ${NAMESPACE}" +echo "===========================================" +echo "" + +# 启动服务 +uvicorn app:app --host 0.0.0.0 --port 8000 --reload diff --git a/test_health_check.sh b/test_health_check.sh deleted file mode 100755 index 5b743b7..0000000 --- a/test_health_check.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash - -# 测试健康检查修复 -# 验证崩溃的 agent 是否能正确识别为 unhealthy - -API_URL="${API_URL:-http://localhost:8000}" -AGENT_NAME="${AGENT_NAME:-my-mysql-agenty}" - -echo "========================================" -echo "测试 Agent 健康状态检查" -echo "========================================" -echo "API URL: $API_URL" -echo "Agent Name: $AGENT_NAME" -echo "" - -# 获取 agent 状态 -echo "📊 获取 Agent 状态..." -response=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$API_URL/agents/$AGENT_NAME/status") - -http_status=$(echo "$response" | grep "HTTP_STATUS" | cut -d':' -f2) -body=$(echo "$response" | sed '/HTTP_STATUS/d') - -echo "HTTP Status: $http_status" -echo "" - -if [ "$http_status" = "200" ]; then - echo "✅ 成功获取状态" - echo "" - echo "响应内容:" - echo "$body" | python3 -m json.tool 2>/dev/null || echo "$body" - echo "" - - # 提取健康状态 - health_status=$(echo "$body" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('health_status', 'N/A'))" 2>/dev/null || echo "无法解析") - status=$(echo "$body" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('status', 'N/A'))" 2>/dev/null || echo "无法解析") - - echo "========================================" - echo "状态摘要:" - echo " Pod Status: $status" - echo " Health Status: $health_status" - echo "========================================" - echo "" - - if [ "$health_status" = "unhealthy" ]; then - echo "✅ 正确识别为不健康状态 (unhealthy)" - - # 显示容器信息 - echo "" - echo "📦 容器详情:" - echo "$body" | python3 -c " -import sys, json -data = json.load(sys.stdin) -containers = data.get('containers', []) -for c in containers: - print(f\" 容器: {c.get('name', 'N/A')}\") - print(f\" 状态: {c.get('state', 'N/A')}\") - print(f\" 就绪: {c.get('ready', 'N/A')}\") - print(f\" 重启次数: {c.get('restart_count', 0)}\") - if c.get('reason'): - print(f\" 原因: {c.get('reason')}\") - if c.get('exit_code') is not None: - print(f\" 退出码: {c.get('exit_code')}\") - print() -" 2>/dev/null || echo " 无法解析容器信息" - elif [ "$health_status" = "healthy" ]; then - echo "⚠️ 仍然显示为健康状态 (healthy) - 可能是 agent 确实在运行" - echo " 请检查容器详细信息确认" - elif [ "$health_status" = "degraded" ]; then - echo "⚠️ 显示为降级状态 (degraded) - 容器可能频繁重启" - else - echo "⚠️ 未知健康状态: $health_status" - fi - -elif [ "$http_status" = "404" ]; then - echo "❌ Agent 不存在" - echo "$body" -else - echo "❌ 获取状态失败" - echo "$body" -fi - -echo "" -echo "========================================" -echo "测试完成" -echo "========================================" diff --git a/test_realtime_metrics.sh b/test_realtime_metrics.sh deleted file mode 100755 index 0af4917..0000000 --- a/test_realtime_metrics.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -echo "🔍 测试实时 Metrics 功能" -echo "======================================" -echo "" - -# 获取所有运行中的 agents -agents=$(curl -s http://localhost:8000/agents | jq -r '.agents[].name' | head -5) - -echo "📊 查询前 5 个 Agent 的实时资源使用情况:" -echo "" - -for agent in $agents; do - echo "Agent: $agent" - echo "----------------------------------------" - - metrics=$(curl -s http://localhost:8000/agents/$agent/metrics) - - # 提取数据 - cpu_usage=$(echo $metrics | jq -r '.usage.cpu // "N/A"') - memory_usage=$(echo $metrics | jq -r '.usage.memory // "N/A"') - cpu_limit=$(echo $metrics | jq -r '.limits.cpu // "N/A"') - memory_limit=$(echo $metrics | jq -r '.limits.memory // "N/A"') - timestamp=$(echo $metrics | jq -r '.timestamp // "N/A"') - - echo " 实时使用:" - echo " CPU: $cpu_usage" - echo " Memory: $memory_usage" - echo " 资源限制:" - echo " CPU: $cpu_limit" - echo " Memory: $memory_limit" - echo " 更新时间: $timestamp" - echo "" -done - -echo "======================================" -echo "✅ 测试完成" -echo "" -echo "💡 说明:" -echo " - CPU 单位: n=纳核(nanocores), m=毫核(millicores)" -echo " - Memory 单位: Ki=KiB, Mi=MiB" -echo " - 实时数据每 15-60 秒更新一次(由 metrics-server 决定)" diff --git a/test_venv/bin/dotenv b/test_venv/bin/dotenv new file mode 100755 index 0000000..5dd2a70 --- /dev/null +++ b/test_venv/bin/dotenv @@ -0,0 +1,8 @@ +#!/home/taiji/tools/agent-manager/test_venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from dotenv.__main__ import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli()) diff --git a/test_venv/bin/pyrsa-decrypt b/test_venv/bin/pyrsa-decrypt new file mode 100755 index 0000000..d0018fd --- /dev/null +++ b/test_venv/bin/pyrsa-decrypt @@ -0,0 +1,8 @@ +#!/home/taiji/tools/agent-manager/test_venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from rsa.cli import decrypt +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(decrypt()) diff --git a/test_venv/bin/pyrsa-encrypt b/test_venv/bin/pyrsa-encrypt new file mode 100755 index 0000000..91d2335 --- /dev/null +++ b/test_venv/bin/pyrsa-encrypt @@ -0,0 +1,8 @@ +#!/home/taiji/tools/agent-manager/test_venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from rsa.cli import encrypt +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(encrypt()) diff --git a/test_venv/bin/pyrsa-keygen b/test_venv/bin/pyrsa-keygen new file mode 100755 index 0000000..cd5af7b --- /dev/null +++ b/test_venv/bin/pyrsa-keygen @@ -0,0 +1,8 @@ +#!/home/taiji/tools/agent-manager/test_venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from rsa.cli import keygen +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(keygen()) diff --git a/test_venv/bin/pyrsa-priv2pub b/test_venv/bin/pyrsa-priv2pub new file mode 100755 index 0000000..76cb76f --- /dev/null +++ b/test_venv/bin/pyrsa-priv2pub @@ -0,0 +1,8 @@ +#!/home/taiji/tools/agent-manager/test_venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from rsa.util import private_to_public +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(private_to_public()) diff --git a/test_venv/bin/pyrsa-sign b/test_venv/bin/pyrsa-sign new file mode 100755 index 0000000..25f2634 --- /dev/null +++ b/test_venv/bin/pyrsa-sign @@ -0,0 +1,8 @@ +#!/home/taiji/tools/agent-manager/test_venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from rsa.cli import sign +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(sign()) diff --git a/test_venv/bin/pyrsa-verify b/test_venv/bin/pyrsa-verify new file mode 100755 index 0000000..74a83b3 --- /dev/null +++ b/test_venv/bin/pyrsa-verify @@ -0,0 +1,8 @@ +#!/home/taiji/tools/agent-manager/test_venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from rsa.cli import verify +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(verify()) diff --git a/test_venv/bin/uvicorn b/test_venv/bin/uvicorn new file mode 100755 index 0000000..8fb049b --- /dev/null +++ b/test_venv/bin/uvicorn @@ -0,0 +1,8 @@ +#!/home/taiji/tools/agent-manager/test_venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from uvicorn.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/watchfiles b/test_venv/bin/watchfiles new file mode 100755 index 0000000..e3a1b9d --- /dev/null +++ b/test_venv/bin/watchfiles @@ -0,0 +1,8 @@ +#!/home/taiji/tools/agent-manager/test_venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from watchfiles.cli import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli()) diff --git a/test_venv/bin/websockets b/test_venv/bin/websockets new file mode 100755 index 0000000..44bf0fd --- /dev/null +++ b/test_venv/bin/websockets @@ -0,0 +1,8 @@ +#!/home/taiji/tools/agent-manager/test_venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from websockets.cli 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/wsdump b/test_venv/bin/wsdump new file mode 100755 index 0000000..76ac287 --- /dev/null +++ b/test_venv/bin/wsdump @@ -0,0 +1,8 @@ +#!/home/taiji/tools/agent-manager/test_venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from websocket._wsdump 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/include/site/python3.12/greenlet/greenlet.h b/test_venv/include/site/python3.12/greenlet/greenlet.h new file mode 100644 index 0000000..d02a16e --- /dev/null +++ b/test_venv/include/site/python3.12/greenlet/greenlet.h @@ -0,0 +1,164 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ + +/* Greenlet object interface */ + +#ifndef Py_GREENLETOBJECT_H +#define Py_GREENLETOBJECT_H + + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is deprecated and undocumented. It does not change. */ +#define GREENLET_VERSION "1.0.0" + +#ifndef GREENLET_MODULE +#define implementation_ptr_t void* +#endif + +typedef struct _greenlet { + PyObject_HEAD + PyObject* weakreflist; + PyObject* dict; + implementation_ptr_t pimpl; +} PyGreenlet; + +#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) + + +/* C API functions */ + +/* Total number of symbols that are exported */ +#define PyGreenlet_API_pointers 12 + +#define PyGreenlet_Type_NUM 0 +#define PyExc_GreenletError_NUM 1 +#define PyExc_GreenletExit_NUM 2 + +#define PyGreenlet_New_NUM 3 +#define PyGreenlet_GetCurrent_NUM 4 +#define PyGreenlet_Throw_NUM 5 +#define PyGreenlet_Switch_NUM 6 +#define PyGreenlet_SetParent_NUM 7 + +#define PyGreenlet_MAIN_NUM 8 +#define PyGreenlet_STARTED_NUM 9 +#define PyGreenlet_ACTIVE_NUM 10 +#define PyGreenlet_GET_PARENT_NUM 11 + +#ifndef GREENLET_MODULE +/* This section is used by modules that uses the greenlet C API */ +static void** _PyGreenlet_API = NULL; + +# define PyGreenlet_Type \ + (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) + +# define PyExc_GreenletError \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) + +# define PyExc_GreenletExit \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) + +/* + * PyGreenlet_New(PyObject *args) + * + * greenlet.greenlet(run, parent=None) + */ +# define PyGreenlet_New \ + (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ + _PyGreenlet_API[PyGreenlet_New_NUM]) + +/* + * PyGreenlet_GetCurrent(void) + * + * greenlet.getcurrent() + */ +# define PyGreenlet_GetCurrent \ + (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) + +/* + * PyGreenlet_Throw( + * PyGreenlet *greenlet, + * PyObject *typ, + * PyObject *val, + * PyObject *tb) + * + * g.throw(...) + */ +# define PyGreenlet_Throw \ + (*(PyObject * (*)(PyGreenlet * self, \ + PyObject * typ, \ + PyObject * val, \ + PyObject * tb)) \ + _PyGreenlet_API[PyGreenlet_Throw_NUM]) + +/* + * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) + * + * g.switch(*args, **kwargs) + */ +# define PyGreenlet_Switch \ + (*(PyObject * \ + (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ + _PyGreenlet_API[PyGreenlet_Switch_NUM]) + +/* + * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) + * + * g.parent = new_parent + */ +# define PyGreenlet_SetParent \ + (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ + _PyGreenlet_API[PyGreenlet_SetParent_NUM]) + +/* + * PyGreenlet_GetParent(PyObject* greenlet) + * + * return greenlet.parent; + * + * This could return NULL even if there is no exception active. + * If it does not return NULL, you are responsible for decrementing the + * reference count. + */ +# define PyGreenlet_GetParent \ + (*(PyGreenlet* (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) + +/* + * deprecated, undocumented alias. + */ +# define PyGreenlet_GET_PARENT PyGreenlet_GetParent + +# define PyGreenlet_MAIN \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_MAIN_NUM]) + +# define PyGreenlet_STARTED \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_STARTED_NUM]) + +# define PyGreenlet_ACTIVE \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) + + + + +/* Macro that imports greenlet and initializes C API */ +/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we + keep the older definition to be sure older code that might have a copy of + the header still works. */ +# define PyGreenlet_Import() \ + { \ + _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ + } + +#endif /* GREENLET_MODULE */ + +#ifdef __cplusplus +} +#endif +#endif /* !Py_GREENLETOBJECT_H */ diff --git a/undeploy-k8s.sh b/undeploy-k8s.sh new file mode 100755 index 0000000..6d13bfb --- /dev/null +++ b/undeploy-k8s.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +############################################################################## +# Agent Manager - 卸载脚本 +############################################################################## + +set -e + +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' + +NAMESPACE="agent-manager" + +echo -e "${YELLOW}⚠️ 警告: 即将删除 Agent Manager 及其所有资源${NC}" +read -p "确认删除?[y/N] " -n 1 -r +echo + +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "已取消" + exit 0 +fi + +echo "正在删除 Agent Manager..." + +# 删除 Deployment 和 Service +kubectl delete -f ./k8s/agent-manager-service.yaml --ignore-not-found=true +kubectl delete -f ./k8s/agent-manager-deployment.yaml --ignore-not-found=true + +# 删除 RBAC +kubectl delete -f ./k8s/agent-manager-rbac.yaml --ignore-not-found=true + +# 删除配置 +kubectl delete -f ./k8s/agent-manager-configmap.yaml --ignore-not-found=true +kubectl delete -f ./k8s/agent-manager-secret.yaml --ignore-not-found=true + +# 删除 ACR Secret +kubectl delete secret acr-secret -n ${NAMESPACE} --ignore-not-found=true +kubectl delete secret kubeconfig-secret -n ${NAMESPACE} --ignore-not-found=true + +# 删除命名空间 +kubectl delete namespace ${NAMESPACE} --ignore-not-found=true + +echo -e "\n${RED}✅ Agent Manager 已卸载${NC}"