forked from zhanggangyong/agent_management
new version
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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"]
|
||||
@@ -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
|
||||
Executable
+166
@@ -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
|
||||
@@ -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 显示实时数据
|
||||
@@ -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)
|
||||
Executable
+167
@@ -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 <pod-name>
|
||||
|
||||
问题: ImagePullBackOff
|
||||
解决: 检查 ACR 凭据
|
||||
kubectl get secret acr-secret -n agent-manager -o yaml
|
||||
kubectl describe pod -n agent-manager <pod-name>
|
||||
|
||||
问题: CrashLoopBackOff
|
||||
解决: 查看日志找出错误原因
|
||||
kubectl logs -n agent-manager <pod-name>
|
||||
kubectl describe pod -n agent-manager <pod-name>
|
||||
|
||||
问题: 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
|
||||
@@ -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 | 更新资源限制逻辑,明确按需创建和配额分配机制 |
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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/)
|
||||
@@ -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://<agent-url>/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://<agent-url>/mcp/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tool_name": "list_containers",
|
||||
"parameters": {}
|
||||
}'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST http://<agent-url>/mcp/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tool_name": "list_blobs",
|
||||
"parameters": {
|
||||
"container_name": "my-container"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### A2A 版本 API
|
||||
|
||||
#### 1. 获取 Agent 能力
|
||||
|
||||
```bash
|
||||
curl http://<agent-url>/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://<agent-url>/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://<agent-url>/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://<agent-url>/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 <pod-name> -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)
|
||||
@@ -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)
|
||||
- 集成到你的应用中
|
||||
@@ -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://<agent-url>/mcp/tools
|
||||
```
|
||||
|
||||
#### MCP - 调用工具
|
||||
```bash
|
||||
curl -X POST http://<agent-url>/mcp/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tool_name": "list_containers", "parameters": {}}'
|
||||
```
|
||||
|
||||
#### A2A - 获取能力
|
||||
```bash
|
||||
curl http://<agent-url>/a2a/capabilities
|
||||
```
|
||||
|
||||
#### A2A - 发送消息
|
||||
```bash
|
||||
curl -X POST http://<agent-url>/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 <pod-name> -n ai-agents
|
||||
|
||||
# 查看事件
|
||||
kubectl describe pod <pod-name> -n ai-agents
|
||||
```
|
||||
|
||||
### 工具调用失败
|
||||
```bash
|
||||
# 检查工具列表
|
||||
curl http://<agent-url>/mcp/tools
|
||||
|
||||
# 测试健康检查
|
||||
curl http://<agent-url>/health
|
||||
```
|
||||
|
||||
### 存储连接失败
|
||||
```bash
|
||||
# 验证连接字符串
|
||||
curl -X POST http://<agent-url>/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)
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
@@ -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)}"
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
+4
-2
@@ -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
|
||||
+49
-27
@@ -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,
|
||||
+4
-3
@@ -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
|
||||
+13
-2
@@ -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)}")
|
||||
+2
-2
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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!
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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}"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||
)
|
||||
|
||||
|
||||
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())
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
]
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
外部API工具封装模块
|
||||
"""
|
||||
|
||||
from .serper import SerperClient
|
||||
from .jina_reader import JinaReaderClient
|
||||
from .jina_reranker import JinaRerankerClient
|
||||
|
||||
__all__ = [
|
||||
"SerperClient",
|
||||
"JinaReaderClient",
|
||||
"JinaRerankerClient",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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格式")
|
||||
|
||||
@@ -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()
|
||||
Submodule
+1
Submodule agent_templates/aks_agent added at b45aa748ee
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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}"
|
||||
@@ -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 "=========================================="
|
||||
@@ -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}"
|
||||
@@ -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 " }'"
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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. **错误重试**: 实现重试机制处理网络问题
|
||||
@@ -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. **并发请求**: 服务支持并发请求处理
|
||||
@@ -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. **工具组合**: 可以组合多个工具完成复杂任务
|
||||
@@ -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
|
||||
@@ -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/` 目录
|
||||
@@ -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! 🎉**
|
||||
@@ -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 个并发请求
|
||||
@@ -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 更经济
|
||||
@@ -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 更准确但成本更高
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 配置说明
|
||||
@@ -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 使用量以控制成本
|
||||
@@ -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 秒启动时间
|
||||
- 支持自动配置和手动配置两种模式
|
||||
@@ -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 - <<EOF
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: my-search-agent
|
||||
namespace: agents
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: my-search-agent
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: my-search-agent
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: acr-secret
|
||||
containers:
|
||||
- name: search-agent
|
||||
image: agnettaiji.azurecr.io/ai-agents/search-agent:v1.0
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: LLM_BASE_URL
|
||||
value: "https://apis.openroutex.com/openai/deployments/xchat52"
|
||||
- name: LLM_API_KEY
|
||||
value: "your-llm-key"
|
||||
- name: SERPER_API_KEY
|
||||
value: "your-serper-key"
|
||||
- name: JINA_API_KEY
|
||||
value: "your-jina-key"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: my-search-agent
|
||||
namespace: agents
|
||||
spec:
|
||||
selector:
|
||||
app: my-search-agent
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
EOF
|
||||
```
|
||||
|
||||
### 方式 3: 使用 Docker 本地测试
|
||||
|
||||
```bash
|
||||
docker run -p 8080:8080 \
|
||||
-e LLM_BASE_URL='https://apis.openroutex.com/openai/deployments/xchat52' \
|
||||
-e LLM_API_KEY='your-llm-key' \
|
||||
-e SERPER_API_KEY='your-serper-key' \
|
||||
-e JINA_API_KEY='your-jina-key' \
|
||||
agnettaiji.azurecr.io/ai-agents/search-agent:v1.0
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 健康检查
|
||||
|
||||
```bash
|
||||
GET /health
|
||||
|
||||
# 响应示例
|
||||
{
|
||||
"status": "healthy",
|
||||
"pod_name": "search-agent-xxx",
|
||||
"template_type": "search_agent",
|
||||
"configured": true,
|
||||
"timestamp": "2026-01-13T04:45:00.000000"
|
||||
}
|
||||
```
|
||||
|
||||
### 获取状态
|
||||
|
||||
```bash
|
||||
GET /status
|
||||
|
||||
# 响应示例
|
||||
{
|
||||
"status": "running",
|
||||
"pod_name": "search-agent-xxx",
|
||||
"template_type": "search_agent",
|
||||
"configured": true,
|
||||
"timestamp": "2026-01-13T04:45:00.000000"
|
||||
}
|
||||
```
|
||||
|
||||
### 配置 Agent(如果未通过环境变量配置)
|
||||
|
||||
```bash
|
||||
POST /configure
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"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,
|
||||
"max_results_per_query": 10,
|
||||
"content_max_length": 5000,
|
||||
"log_level": "INFO",
|
||||
"timeout": 30
|
||||
}
|
||||
```
|
||||
|
||||
### 执行搜索
|
||||
|
||||
```bash
|
||||
POST /search
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"query": "什么是 Kubernetes?",
|
||||
"auto_configure": false
|
||||
}
|
||||
|
||||
# 响应示例
|
||||
{
|
||||
"query": "什么是 Kubernetes?",
|
||||
"answer": "Kubernetes 是一个开源的容器编排平台...",
|
||||
"sources": [
|
||||
{
|
||||
"index": 1,
|
||||
"title": "Kubernetes 官方文档",
|
||||
"url": "https://kubernetes.io/docs/"
|
||||
}
|
||||
],
|
||||
"confidence": "high",
|
||||
"iterations": 1,
|
||||
"total_sources": 5,
|
||||
"search_queries": ["Kubernetes 是什么", "Kubernetes 容器编排"],
|
||||
"timestamp": "2026-01-13T04:45:00.000000"
|
||||
}
|
||||
```
|
||||
|
||||
### 聊天接口(别名)
|
||||
|
||||
```bash
|
||||
POST /chat
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"query": "Python 和 Go 语言的区别是什么?"
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### Python 客户端
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Agent 服务地址
|
||||
agent_url = "http://my-search-agent.agents.svc.cluster.local:8080"
|
||||
|
||||
# 执行搜索
|
||||
response = requests.post(
|
||||
f"{agent_url}/search",
|
||||
json={
|
||||
"query": "什么是微服务架构?",
|
||||
"auto_configure": False
|
||||
}
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
print(f"答案: {result['answer']}")
|
||||
print(f"来源数: {len(result['sources'])}")
|
||||
print(f"置信度: {result['confidence']}")
|
||||
```
|
||||
|
||||
### cURL 示例
|
||||
|
||||
```bash
|
||||
# 搜索
|
||||
curl -X POST http://my-search-agent:8080/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"query": "Docker 容器的优势是什么?"
|
||||
}'
|
||||
|
||||
# 健康检查
|
||||
curl http://my-search-agent:8080/health
|
||||
```
|
||||
|
||||
## 构建和推送
|
||||
|
||||
### 构建镜像
|
||||
|
||||
```bash
|
||||
cd /home/taiji/tools/agent-manager/agent_templates
|
||||
./build_search_agent.sh v1.0
|
||||
```
|
||||
|
||||
### 推送到 ACR
|
||||
|
||||
在构建过程中选择 `y` 推送,或手动推送:
|
||||
|
||||
```bash
|
||||
# 登录 ACR
|
||||
az acr login --name agnettaiji
|
||||
|
||||
# 构建并推送
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-f search_agent.Dockerfile \
|
||||
-t agnettaiji.azurecr.io/ai-agents/search-agent:v1.0 \
|
||||
--push \
|
||||
.
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
### 使用测试脚本
|
||||
|
||||
```bash
|
||||
cd /home/taiji/tools/agent-manager/agent_templates
|
||||
./test_search_agent.sh
|
||||
```
|
||||
|
||||
### 手动测试
|
||||
|
||||
```bash
|
||||
# 1. 创建 Agent
|
||||
curl -X POST http://localhost:8000/api/v2/agents \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @search_agent_config.json
|
||||
|
||||
# 2. 查看状态
|
||||
curl http://localhost:8000/api/v2/agents/my-search-agent
|
||||
|
||||
# 3. 测试搜索
|
||||
curl -X POST http://my-search-agent:8080/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "测试查询"}'
|
||||
|
||||
# 4. 删除 Agent
|
||||
curl -X DELETE http://localhost:8000/api/v2/agents/my-search-agent
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 查看日志
|
||||
|
||||
```bash
|
||||
kubectl logs -n agents deployment/my-search-agent
|
||||
```
|
||||
|
||||
### 查看 Pod 状态
|
||||
|
||||
```bash
|
||||
kubectl get pods -n agents -l app=my-search-agent
|
||||
kubectl describe pod -n agents <pod-name>
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
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
|
||||
|
||||
## 许可
|
||||
|
||||
遵循项目主许可证。
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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://<pod-ip>:8080/search" -H "Content-Type: application/json" -d \'{"url": "https://www.example.com"}\'',
|
||||
"fetch": 'curl "http://<pod-ip>:8080/fetch?url=https://www.example.com"'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.post("/search", response_model=SearchResponse)
|
||||
async def search(request: SearchRequest):
|
||||
"""
|
||||
搜索网站内容
|
||||
|
||||
使用Jina Reader API获取指定URL的网站内容
|
||||
|
||||
Args:
|
||||
request: 包含URL和选项的搜索请求
|
||||
|
||||
Returns:
|
||||
网站内容和元数据
|
||||
"""
|
||||
if not JINA_API_KEY:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="JINA_API_KEY未配置,请设置环境变量"
|
||||
)
|
||||
|
||||
logger.info(f"[{POD_NAME}] 搜索请求: {request.url}")
|
||||
|
||||
try:
|
||||
# 构建Jina Reader API请求
|
||||
jina_url = f"{JINA_BASE_URL}/{request.url}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {JINA_API_KEY}"
|
||||
}
|
||||
|
||||
# 发送请求
|
||||
response = requests.get(
|
||||
jina_url,
|
||||
headers=headers,
|
||||
timeout=request.timeout
|
||||
)
|
||||
|
||||
logger.info(f"[{POD_NAME}] Jina API响应状态: {response.status_code}")
|
||||
|
||||
return SearchResponse(
|
||||
url=request.url,
|
||||
content=response.text,
|
||||
status_code=response.status_code,
|
||||
content_type=response.headers.get("Content-Type"),
|
||||
success=response.status_code == 200
|
||||
)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error(f"[{POD_NAME}] 请求超时: {request.url}")
|
||||
raise HTTPException(
|
||||
status_code=504,
|
||||
detail=f"请求超时({request.timeout}秒)"
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"[{POD_NAME}] 请求失败: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=f"请求失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/fetch", response_model=SearchResponse)
|
||||
async def fetch(
|
||||
url: str = Query(..., description="要获取的网站URL"),
|
||||
timeout: int = Query(default=30, description="请求超时时间(秒)")
|
||||
):
|
||||
"""
|
||||
快速获取网站内容(GET方式)
|
||||
|
||||
Args:
|
||||
url: 要获取的网站URL
|
||||
timeout: 请求超时时间
|
||||
|
||||
Returns:
|
||||
网站内容和元数据
|
||||
"""
|
||||
request = SearchRequest(url=url, timeout=timeout)
|
||||
return await search(request)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数 - 启动HTTP服务"""
|
||||
logger.info(f"Jina Search Agent启动: {POD_NAME} (模板: {TEMPLATE_TYPE})")
|
||||
logger.info(f"服务地址: {SERVICE_HOST}:{SERVICE_PORT}")
|
||||
logger.info(f"JINA_API_KEY已配置: {bool(JINA_API_KEY)}")
|
||||
|
||||
if not JINA_API_KEY:
|
||||
logger.warning("⚠️ JINA_API_KEY未设置,API调用将失败")
|
||||
|
||||
# 启动uvicorn服务
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=SERVICE_HOST,
|
||||
port=SERVICE_PORT,
|
||||
log_level="info"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+126
@@ -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
|
||||
|
||||
+90
@@ -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 ""
|
||||
+3
@@ -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'"
|
||||
Executable
+83
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
@@ -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()
|
||||
@@ -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 <<EOF
|
||||
{
|
||||
"name": "$agent_name",
|
||||
"template_name": "$template",
|
||||
"owner_id": "$OWNER_ID",
|
||||
"namespace": "$NAMESPACE",
|
||||
"agent_framework": "$framework",
|
||||
"storage_connection_string": "$STORAGE_CONN_STRING",
|
||||
"model_provider": "openai",
|
||||
"model_name": "gpt-4",
|
||||
"model_api_key": "$MODEL_API_KEY",
|
||||
"tools_config": {
|
||||
"max_iterations": 5
|
||||
}
|
||||
$extra_config
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "📤 创建 Agent..."
|
||||
response=$(curl -s -X POST "$AGENT_MANAGER_URL/v2/agents/platform" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$request_json")
|
||||
|
||||
echo "✅ 响应: $response"
|
||||
|
||||
# 检查是否创建成功
|
||||
if echo "$response" | grep -q "id"; then
|
||||
echo "✅ Agent 创建成功"
|
||||
|
||||
# 等待 Agent 启动
|
||||
echo "⏳ 等待 Agent 启动..."
|
||||
sleep 10
|
||||
|
||||
# 获取 Agent 状态
|
||||
echo "📊 获取 Agent 状态..."
|
||||
status_response=$(curl -s "$AGENT_MANAGER_URL/v2/agents/$agent_name")
|
||||
echo "$status_response" | jq '.'
|
||||
|
||||
# 提取 service_url
|
||||
service_url=$(echo "$status_response" | jq -r '.service_url // empty')
|
||||
|
||||
if [ -n "$service_url" ]; then
|
||||
echo "🌐 Service URL: $service_url"
|
||||
|
||||
# 测试健康检查
|
||||
echo "💓 测试健康检查..."
|
||||
health_response=$(curl -s "$service_url/health")
|
||||
echo "$health_response" | jq '.'
|
||||
|
||||
# 根据框架测试特定功能
|
||||
case $framework in
|
||||
"mcp")
|
||||
echo "🔧 测试 MCP 工具列表..."
|
||||
curl -s "$service_url/mcp/tools" | jq '.'
|
||||
|
||||
echo "🔧 测试 MCP 工具调用..."
|
||||
curl -s -X POST "$service_url/mcp/call" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tool_name": "list_containers", "parameters": {}}' | jq '.'
|
||||
;;
|
||||
"a2a")
|
||||
echo "🤝 测试 A2A 能力..."
|
||||
curl -s "$service_url/a2a/capabilities" | jq '.'
|
||||
|
||||
echo "🤝 测试 A2A 消息..."
|
||||
curl -s -X POST "$service_url/a2a/message" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"message_id": "test-001",
|
||||
"from_agent": "test-agent",
|
||||
"to_agent": "blob-agent",
|
||||
"message_type": "request",
|
||||
"action": "list_containers",
|
||||
"parameters": {}
|
||||
}' | jq '.'
|
||||
;;
|
||||
"langchain")
|
||||
echo "🔗 测试查询..."
|
||||
curl -s -X POST "$service_url/query" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "列出所有容器"}' | jq '.'
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "⚠️ 警告: 未找到 service_url"
|
||||
fi
|
||||
|
||||
# 删除测试 Agent
|
||||
echo "🗑️ 删除测试 Agent..."
|
||||
delete_response=$(curl -s -X DELETE "$AGENT_MANAGER_URL/v2/agents/$agent_name")
|
||||
echo "$delete_response" | jq '.'
|
||||
|
||||
else
|
||||
echo "❌ Agent 创建失败"
|
||||
echo "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 运行测试
|
||||
echo "🚀 开始测试..."
|
||||
echo ""
|
||||
|
||||
# 测试 MCP 版本
|
||||
test_agent "mcp" "azure_blob_agent_mcp" "test-blob-mcp" ""
|
||||
|
||||
# 测试 A2A 版本
|
||||
test_agent "a2a" "azure_blob_agent_a2a" "test-blob-a2a" ',
|
||||
"query_params": {
|
||||
"agent_id": "test-blob-a2a",
|
||||
"agent_role": "storage_manager"
|
||||
}'
|
||||
|
||||
# 测试 LangChain 版本(如果已部署)
|
||||
# test_agent "langchain" "azure_blob_agent" "test-blob-langchain" ',
|
||||
# "environment_vars": {
|
||||
# "LITELLM_API_BASE": "http://litellm-service:4000",
|
||||
# "LITELLM_MODEL": "gpt-3.5-turbo",
|
||||
# "LITELLM_API_KEY": "sk-test"
|
||||
# }'
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "✅ 所有测试完成"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Search Agent 测试脚本
|
||||
# 测试通过 agent-manager 创建和使用 search agent
|
||||
|
||||
set -e
|
||||
|
||||
AGENT_MANAGER_URL="${AGENT_MANAGER_URL:-http://localhost:8000}"
|
||||
AGENT_NAME="test-search-agent-$(date +%s)"
|
||||
IMAGE="agnettaiji.azurecr.io/ai-agents/search-agent:v1.0"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Search Agent 集成测试"
|
||||
echo "=========================================="
|
||||
echo "Agent Manager: ${AGENT_MANAGER_URL}"
|
||||
echo "Agent Name: ${AGENT_NAME}"
|
||||
echo "Image: ${IMAGE}"
|
||||
echo ""
|
||||
|
||||
# 1. 创建 Agent
|
||||
echo "📝 步骤 1: 创建 Search Agent..."
|
||||
CREATE_RESPONSE=$(curl -s -X POST "${AGENT_MANAGER_URL}/agents" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "'"${AGENT_NAME}"'",
|
||||
"template": "search_agent",
|
||||
"config": {
|
||||
"cpu_request": "500m",
|
||||
"memory_request": "512Mi",
|
||||
"cpu_limit": "1000m",
|
||||
"memory_limit": "1Gi"
|
||||
},
|
||||
"env": {
|
||||
"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"
|
||||
}
|
||||
}')
|
||||
|
||||
echo "响应: ${CREATE_RESPONSE}"
|
||||
echo ""
|
||||
|
||||
# 检查创建是否成功
|
||||
if echo "${CREATE_RESPONSE}" | grep -q "error"; then
|
||||
echo "❌ 创建失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Agent 创建成功"
|
||||
echo ""
|
||||
|
||||
# 2. 等待 Agent 启动
|
||||
echo "⏳ 步骤 2: 等待 Agent 启动..."
|
||||
sleep 10
|
||||
|
||||
# 3. 获取 Agent 状态
|
||||
echo "📊 步骤 3: 获取 Agent 状态..."
|
||||
STATUS_RESPONSE=$(curl -s "${AGENT_MANAGER_URL}/agents/${AGENT_NAME}/status")
|
||||
echo "状态: ${STATUS_RESPONSE}"
|
||||
echo ""
|
||||
|
||||
# 4. 获取 Agent 服务 URL
|
||||
SERVICE_URL=$(echo "${STATUS_RESPONSE}" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('service_url', ''))" 2>/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 "=========================================="
|
||||
Executable
+34
@@ -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)
|
||||
Executable
+317
@@ -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 <<EOF
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: agent-manager-secret
|
||||
namespace: agent-manager
|
||||
type: Opaque
|
||||
stringData:
|
||||
AZURE_TENANT_ID: "${AZURE_TENANT_ID}"
|
||||
AZURE_CLIENT_ID: "${AZURE_CLIENT_ID}"
|
||||
AZURE_CLIENT_SECRET: "${AZURE_CLIENT_SECRET}"
|
||||
EOF
|
||||
|
||||
kubectl apply -f ${K8S_DIR}/agent-manager-configmap.yaml
|
||||
kubectl apply -f /tmp/agent-manager-secret.yaml
|
||||
rm -f /tmp/agent-manager-secret.yaml
|
||||
|
||||
print_success "配置已更新"
|
||||
}
|
||||
|
||||
# 创建 kubeconfig Secret(如果需要)
|
||||
create_kubeconfig_secret() {
|
||||
print_step "创建 kubeconfig Secret..."
|
||||
|
||||
if [ -f ~/.kube/config ]; then
|
||||
if kubectl get secret kubeconfig-secret -n ${NAMESPACE} &> /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 "$@"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user