agent1.1版本 修改了mcp逻辑 多agent测试通过
This commit is contained in:
parent
18cf1c7e1a
commit
6f3f9c7df1
336
README.md
Normal file
336
README.md
Normal file
@ -0,0 +1,336 @@
|
||||
# Dev Agent — 全栈开发子 Agent
|
||||
|
||||
基于 Google ADK (Agent Development Kit) 构建的全栈开发子 Agent,通过 A2A / MCP / REST API 多种方式调用,支持文件操作、终端命令、会话持久化、上下文压缩、长期记忆。
|
||||
|
||||
## 架构总览
|
||||
|
||||
```
|
||||
用户 / CodeBuddy(主控)
|
||||
│
|
||||
├── MCP ──► mcp_dev_agent/server.py ──┐
|
||||
│ │
|
||||
└── REST ─► api_server.py ◄─────────────┘
|
||||
│
|
||||
▼
|
||||
App(dev_app)
|
||||
│ events_compaction_config(LLM 摘要压缩)
|
||||
▼
|
||||
LlmAgent(dev_agent / 花花)
|
||||
│
|
||||
┌─────────┼─────────┐
|
||||
▼ ▼ ▼
|
||||
文件系统 终端命令 记忆系统
|
||||
MCP run_command preload_memory
|
||||
```
|
||||
|
||||
## 核心能力
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| **文件系统操作** | 读/写/列目录/搜索等 14 个工具(MCP: server-filesystem) |
|
||||
| **终端命令执行** | 异步 subprocess,支持编译/构建/测试 |
|
||||
| **网络搜索** | Tavily 搜索 + Fetch 抓取(默认关闭,见下文说明) |
|
||||
| **SQLite 会话持久化** | 重启不丢,多入口共享 |
|
||||
| **上下文自动压缩** | 每 20 轮 LLM 摘要,长对话不爆 context window |
|
||||
| **长期记忆框架** | InMemory + 自动存取,可扩展为 Chroma 向量库 |
|
||||
| **REST API** | `/run`、`/run_sse`、会话管理、Swagger UI |
|
||||
| **MCP 接口** | 可直接接入 CodeBuddy / Cursor / Windsurf |
|
||||
| **A2A 协议** | Agent-to-Agent 标准协议(备用方案) |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
```bash
|
||||
# 进入项目目录
|
||||
cd d:/nzy/workspace_python/agent
|
||||
|
||||
# 安装依赖(已装可跳过)
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 配置环境变量
|
||||
# 编辑 my_agent/.env,填入你的 vLLM API Key 等
|
||||
```
|
||||
|
||||
### 2. 启动 API Server
|
||||
|
||||
这是最主要的服务入口,提供 REST API + Swagger UI。
|
||||
|
||||
```bash
|
||||
python api_server.py
|
||||
```
|
||||
|
||||
启动后访问:
|
||||
- **Swagger UI**: http://127.0.0.1:8001/docs — 浏览器直接测试接口
|
||||
- **健康检查**: http://127.0.0.1:8001/health
|
||||
- **列出 Agent**: http://127.0.0.1:8001/list-apps
|
||||
|
||||
### 3. 配置 MCP(CodeBuddy 调用)
|
||||
|
||||
在 CodeBuddy 的 MCP 配置中添加:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"dev-agent": {
|
||||
"command": "python",
|
||||
"args": ["d:/nzy/workspace_python/agent/mcp_dev_agent/server.py"],
|
||||
"env": {
|
||||
"DEV_AGENT_API_URL": "http://127.0.0.1:8001",
|
||||
"DEV_AGENT_APP_NAME": "dev_agent",
|
||||
"DEV_AGENT_USER_ID": "codebuddy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
重启 CodeBuddy 后,即可通过 `run_dev_agent` 工具调用 Dev Agent。
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 方式一:Swagger UI(最直观)
|
||||
|
||||
打开 http://127.0.0.1:8001/docs ,在浏览器里直接测试。
|
||||
|
||||
**常用接口**:
|
||||
|
||||
- `POST /run` — 同步运行 agent,返回完整事件列表
|
||||
- `POST /run_sse` — SSE 流式运行
|
||||
- `GET /apps/{app}/users/{user}/sessions/{id}` — 获取会话
|
||||
- `POST /apps/{app}/users/{user}/sessions/{id}` — 创建会话
|
||||
|
||||
### 方式二:命令行对话
|
||||
|
||||
```bash
|
||||
# 新会话
|
||||
python chat.py
|
||||
|
||||
# 指定 session_id 继续对话
|
||||
python chat.py --session my_session
|
||||
|
||||
# 列出所有会话
|
||||
python chat.py --list
|
||||
|
||||
# 删除会话
|
||||
python chat.py --delete my_session
|
||||
```
|
||||
|
||||
### 方式三:MCP 工具(CodeBuddy / Cursor)
|
||||
|
||||
配置好 MCP 后,直接让 IDE 中的 AI 调用 `run_dev_agent` 工具。
|
||||
|
||||
**工具参数**:
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `task` | ✅ | 任务描述,越详细越好 |
|
||||
| `session_id` | ❌ | 会话 ID,不传则为 `default`。用于多轮续聊 |
|
||||
|
||||
### 方式四:curl 直接调用
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8001/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"appName": "dev_agent",
|
||||
"userId": "test_user",
|
||||
"sessionId": "test_001",
|
||||
"newMessage": {
|
||||
"role": "user",
|
||||
"parts": [{"text": "你好,请介绍一下你自己"}]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
agent/
|
||||
├── api_server.py # REST API Server(主入口)
|
||||
├── chat.py # CLI 对话工具
|
||||
├── a2a_server.py # A2A Server(备用,已被 API Server 取代)
|
||||
├── a2a_client.py # A2A 客户端测试(调试用)
|
||||
├── test_sse_client.py # SSE 测试(调试用)
|
||||
│
|
||||
├── my_agent/
|
||||
│ ├── __init__.py
|
||||
│ ├── agent.py # Agent 定义(人设、工具、instruction)
|
||||
│ ├── app.py # App 容器(上下文压缩配置)
|
||||
│ └── .env # 环境变量配置
|
||||
│
|
||||
├── mcp_dev_agent/ # Dev Agent MCP Server(CodeBuddy 入口)
|
||||
│ ├── server.py
|
||||
│ └── README.md
|
||||
│
|
||||
├── mcp_server/ # 旧版 MCP Server(已废弃,保留参考)
|
||||
│ └── ...
|
||||
│
|
||||
├── mcp_tools/ # 备用 MCP 工具(已废弃,保留参考)
|
||||
│ └── command_executor/
|
||||
│
|
||||
├── data/ # 数据目录(运行时生成)
|
||||
│ └── sessions.db # SQLite 会话数据库
|
||||
│
|
||||
└── PLAN.md # 项目计划文档
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 环境变量(my_agent/.env)
|
||||
|
||||
```env
|
||||
# vLLM API 配置
|
||||
VLLM_API_BASE=https://9router.aqroid.cn/v1 # vLLM 端点地址
|
||||
VLLM_MODEL=aq-first-combo # 模型名
|
||||
VLLM_API_KEY=sk-... # API Key
|
||||
|
||||
# Agent 工作目录(文件系统 MCP 根目录)
|
||||
AGENT_WORKSPACE_DIR=D:\nzy\workspace_git
|
||||
|
||||
# Tavily 搜索 API Key(启用搜索工具时需要)
|
||||
TAVILY_API_KEY=tvly-dev-...
|
||||
|
||||
# Windows 编码
|
||||
PYTHONUTF8=1
|
||||
```
|
||||
|
||||
### API Server 配置
|
||||
|
||||
通过环境变量或直接修改 `agents/my_agent/api_server.py`:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `API_SERVER_HOST` | `0.0.0.0` | 监听地址 |
|
||||
| `API_SERVER_PORT` | `8001` | 监听端口 |
|
||||
|
||||
### MCP Server 配置
|
||||
|
||||
通过环境变量配置:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `DEV_AGENT_API_URL` | `http://127.0.0.1:8001` | API Server 地址 |
|
||||
| `DEV_AGENT_APP_NAME` | `dev_agent` | Agent 名称 |
|
||||
| `DEV_AGENT_USER_ID` | `codebuddy` | 用户 ID(会话隔离用) |
|
||||
|
||||
## 会话与记忆
|
||||
|
||||
### 会话持久化
|
||||
|
||||
所有会话存储在 `data/sessions.db`(SQLite),重启服务不丢失。
|
||||
|
||||
- **同入口续聊**:同一个 session_id 下次接着聊
|
||||
- **跨入口共享**:API Server、CLI、MCP 都用同一个数据库
|
||||
|
||||
### 上下文压缩
|
||||
|
||||
长对话会自动摘要压缩(默认每 20 轮),防止 context window 溢出:
|
||||
|
||||
- 滑动窗口压缩 + 重叠摘要(保持连续性)
|
||||
- Token 超阈值紧急压缩(默认 50k)
|
||||
- 原始事件完整保留(可回溯)
|
||||
|
||||
配置在 `agents/my_agent/app.py` 的 `EventsCompactionConfig`。
|
||||
|
||||
### 长期记忆
|
||||
|
||||
当前使用 `InMemoryMemoryService`(内存版),特性:
|
||||
|
||||
- 每轮对话结束自动保存(`after_agent_callback`)
|
||||
- 每轮对话开始自动加载相关记忆(`preload_memory`)
|
||||
- 进程重启后记忆丢失
|
||||
|
||||
**后续可扩展**:替换为 `ChromaMemoryService` 等向量数据库,实现持久化语义搜索。
|
||||
|
||||
## 工具说明
|
||||
|
||||
### 文件系统工具(14 个)
|
||||
|
||||
read_file、read_text_file、read_media_file、read_multiple_files、write_file、edit_file、create_directory、list_directory、list_directory_with_sizes、directory_tree、move_file、search_files、get_file_info、list_allowed_directories
|
||||
|
||||
### 终端命令
|
||||
|
||||
- **run_command** — 执行终端命令,支持自定义工作目录和超时
|
||||
|
||||
### 记忆工具
|
||||
|
||||
- **preload_memory** — 每轮自动检索并注入相关历史记忆(系统自动调用,不占工具回合)
|
||||
|
||||
### 网络搜索工具(默认关闭)
|
||||
|
||||
Tavily 搜索 + Fetch 抓取默认注释掉了,因为 vLLM 端点有 413(请求体过大)限制。如需启用:
|
||||
|
||||
1. 取消 `agents/my_agent/agent.py` 中 `fetch_mcp` 和 `tavily_mcp` 的注释
|
||||
2. 配置 `TAVILY_API_KEY` 环境变量
|
||||
|
||||
## 工作流程
|
||||
|
||||
标准工作流程:
|
||||
|
||||
1. 理解任务需求和项目上下文
|
||||
2. 使用文件系统工具浏览项目结构、读取相关文件
|
||||
3. 编写或修改代码
|
||||
4. 使用 run_command 运行编译/构建/测试
|
||||
5. 验证结果后,结构化报告完成情况
|
||||
|
||||
**报告格式**:
|
||||
- 状态:成功 / 部分完成 / 失败(需上报)
|
||||
- 修改的文件:列出所有修改的文件路径
|
||||
- 变更摘要:简述做了什么
|
||||
- 验证结果:编译/测试是否通过
|
||||
- 需要主控关注:如有问题,详细说明
|
||||
|
||||
## 部署说明
|
||||
|
||||
### 本地开发
|
||||
|
||||
```bash
|
||||
# 终端 1:启动 API Server
|
||||
python api_server.py
|
||||
|
||||
# 终端 2(可选):用 CLI 测试
|
||||
python chat.py
|
||||
|
||||
# 或者直接用 Swagger UI:http://127.0.0.1:8001/docs
|
||||
```
|
||||
|
||||
### 上云准备
|
||||
|
||||
- API Server 是标准 FastAPI 应用,可直接部署到任何支持 Python 的平台
|
||||
- SQLite 会话数据库需换成数据库服务(PostgreSQL / MySQL)
|
||||
- MemoryService 需换成托管向量数据库(Chroma / Pinecone / Vertex AI)
|
||||
- 文件系统 MCP 需接入云存储或挂载盘
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 组件 | 技术 | 版本 |
|
||||
|------|------|------|
|
||||
| Agent 框架 | Google ADK | 2.5.0 |
|
||||
| LLM 接入 | LiteLLM + vLLM (OpenAI 兼容) | 1.80.0 |
|
||||
| MCP | Model Context Protocol SDK | 1.29.0 |
|
||||
| HTTP 服务 | FastAPI + Uvicorn | - |
|
||||
| 会话存储 | SQLite | - |
|
||||
| A2A 协议 | a2a-sdk | 1.1.2 |
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 启动后 MCP 工具连不上?
|
||||
|
||||
A: 第一次启动 npx 需要下载 MCP 包,可能需要 30 秒到 1 分钟。如果超时,检查网络连接。
|
||||
|
||||
### Q: 调用时报 413 Request Entity Too Large?
|
||||
|
||||
A: vLLM 端点的 nginx 限制了请求体大小。当前已暂时关闭 Tavily 和 Fetch 工具以减小请求体。如需要启用,需联系端点管理员调大限制。
|
||||
|
||||
### Q: 会话数据存在哪?
|
||||
|
||||
A: `data/sessions.db`,SQLite 格式,可用任何 SQLite 工具打开查看。
|
||||
|
||||
### Q: 怎么重置会话?
|
||||
|
||||
A: 用 CLI 的 `python chat.py --delete <session_id>`,或直接调用 DELETE 会话 API,或直接删除 `data/sessions.db` 文件。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
@ -19,7 +19,7 @@ if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(os.path.join(PROJECT_ROOT, "my_agent", ".env"))
|
||||
load_dotenv(os.path.join(PROJECT_ROOT, "agents/my_agent", ".env"))
|
||||
|
||||
# 强制 UTF-8
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
@ -30,7 +30,7 @@ from google.adk.runners import Runner
|
||||
from google.adk.sessions.sqlite_session_service import SqliteSessionService
|
||||
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from my_agent.app import dev_app
|
||||
from agents.my_agent.app import dev_app
|
||||
|
||||
|
||||
# 配置
|
||||
|
||||
0
agents/__init__.py
Normal file
0
agents/__init__.py
Normal file
200
agents/luna/agent.py
Normal file
200
agents/luna/agent.py
Normal file
@ -0,0 +1,200 @@
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
|
||||
from google.adk.tools.function_tool import FunctionTool
|
||||
from google.adk.tools import preload_memory
|
||||
from mcp.client.stdio import StdioServerParameters
|
||||
import os
|
||||
import asyncio
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# --- 使用 vLLM 端点的智能体 ---
|
||||
api_base_url = os.getenv("VLLM_API_BASE", "https://9router.aqroid.cn/v1")
|
||||
model_name = os.getenv("VLLM_MODEL", "")
|
||||
api_key = os.getenv("VLLM_API_KEY", "")
|
||||
|
||||
# Agent 可访问的工作目录
|
||||
WORKSPACE_DIR = os.getenv("AGENT_WORKSPACE_DIR", r"D:\nzy\workspace_git")
|
||||
|
||||
# --- 文件系统 MCP 工具 ---
|
||||
filesystem_mcp = McpToolset(
|
||||
connection_params=StdioConnectionParams(
|
||||
server_params=StdioServerParameters(
|
||||
command="npx",
|
||||
args=[
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem",
|
||||
os.path.abspath(WORKSPACE_DIR),
|
||||
],
|
||||
),
|
||||
timeout=300000,
|
||||
),
|
||||
)
|
||||
|
||||
# --- 网页抓取 MCP 工具(Fetch)---
|
||||
# 暂时注释:请求体过大导致 413 错误,后续需要时再启用
|
||||
# fetch_mcp = McpToolset(
|
||||
# connection_params=StdioConnectionParams(
|
||||
# server_params=StdioServerParameters(
|
||||
# command="npx",
|
||||
# args=[
|
||||
# "-y",
|
||||
# "@modelcontextprotocol/server-fetch",
|
||||
# ],
|
||||
# ),
|
||||
# timeout=300000,
|
||||
# ),
|
||||
# )
|
||||
|
||||
# --- Tavily 搜索 MCP 工具 ---
|
||||
# 暂时注释:请求体过大导致 413 错误,后续需要时再启用
|
||||
# tavily_api_key = os.getenv("TAVILY_API_KEY", "")
|
||||
# tavily_mcp = McpToolset(
|
||||
# connection_params=StdioConnectionParams(
|
||||
# server_params=StdioServerParameters(
|
||||
# command="npx",
|
||||
# args=[
|
||||
# "-y",
|
||||
# "tavily-mcp@latest",
|
||||
# ],
|
||||
# env={
|
||||
# "TAVILY_API_KEY": tavily_api_key,
|
||||
# },
|
||||
# ),
|
||||
# timeout=300000,
|
||||
# ),
|
||||
# )
|
||||
|
||||
|
||||
# --- 终端命令执行工具(Python 原生,绕开 MCP 通信问题)---
|
||||
async def run_command(command: str, cwd: str = None, timeout: int = 300000) -> str:
|
||||
"""
|
||||
在终端中执行一条命令,返回输出结果。
|
||||
|
||||
Args:
|
||||
command: 要执行的命令,如 'npm run build'、'python -m pytest' 等
|
||||
cwd: 命令执行的工作目录,默认使用 AGENT_WORKSPACE_DIR
|
||||
timeout: 超时时间(秒),默认 300
|
||||
|
||||
Returns:
|
||||
命令执行结果(stdout + stderr + 状态)
|
||||
"""
|
||||
if not command:
|
||||
return "错误:命令不能为空"
|
||||
|
||||
work_dir = cwd or os.path.abspath(WORKSPACE_DIR)
|
||||
if not os.path.isdir(work_dir):
|
||||
return f"错误:工作目录不存在 {work_dir}"
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
command,
|
||||
cwd=work_dir,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
||||
proc.communicate(), timeout=timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
return f"命令执行超时({timeout}秒): {command}"
|
||||
except Exception as e:
|
||||
return f"命令执行出错: {e}"
|
||||
|
||||
stdout = stdout_bytes.decode("utf-8", errors="replace")
|
||||
stderr = stderr_bytes.decode("utf-8", errors="replace")
|
||||
|
||||
parts = []
|
||||
if stdout:
|
||||
parts.append(f"[stdout]\n{stdout}")
|
||||
if stderr:
|
||||
parts.append(f"[stderr]\n{stderr}")
|
||||
|
||||
output = "\n".join(parts) if parts else "(无输出)"
|
||||
|
||||
max_len = 10000
|
||||
if len(output) > max_len:
|
||||
output = output[:max_len] + f"\n\n...(输出已截断,共 {len(output)} 字符)"
|
||||
|
||||
status = "成功" if proc.returncode == 0 else f"失败 (退出码 {proc.returncode})"
|
||||
return f"命令执行{status}\n{output}"
|
||||
|
||||
|
||||
# 注册为 ADK 工具
|
||||
run_command_tool = FunctionTool(run_command)
|
||||
|
||||
|
||||
# --- 记忆自动保存回调 ---
|
||||
async def auto_save_memory_callback(callback_context):
|
||||
"""
|
||||
每轮对话结束后,自动将会话保存到长期记忆中。
|
||||
memory_service 未配置时静默忽略。
|
||||
"""
|
||||
try:
|
||||
await callback_context.add_session_to_memory()
|
||||
except (ValueError, AttributeError):
|
||||
# memory_service 未配置或不可用时跳过
|
||||
pass
|
||||
|
||||
|
||||
root_agent = LlmAgent(
|
||||
model=LiteLlm(
|
||||
model=model_name,
|
||||
api_base=api_base_url,
|
||||
api_key=api_key if api_key else None,
|
||||
custom_llm_provider="openai",
|
||||
),
|
||||
name="luna_agent",
|
||||
description="全栈开发子 Agent(gpt-5.6-luna),可以读写文件、浏览目录、执行开发任务。",
|
||||
instruction=(
|
||||
"你是luna,一个使用gpt5.6-luna的前后端开发子agent\n"
|
||||
"\n"
|
||||
"## 记忆能力\n"
|
||||
"- 你拥有长期记忆,之前和用户的对话中提到的项目信息、技术偏好、任务历史都会被记住\n"
|
||||
"- 系统会自动从记忆中检索与当前任务相关的历史上下文,注入到对话中\n"
|
||||
"- 重要的项目信息(技术栈、目录结构、编码规范等)会自动沉淀到记忆里\n"
|
||||
"\n"
|
||||
"## 工作流程\n"
|
||||
"1. 先理解任务需求和项目上下文\n"
|
||||
"2. 使用文件系统工具浏览项目结构、读取相关文件\n"
|
||||
"3. 编写或修改代码\n"
|
||||
"4. 使用 run_command 工具运行编译/构建/测试,确保代码可正常工作\n"
|
||||
"5. 验证结果后,按指定格式报告完成情况\n"
|
||||
"\n"
|
||||
"## 工作边界\n"
|
||||
"- 所有文件操作限定在分配的工作目录范围内\n"
|
||||
"- 你拥有的工具:文件系统操作(读/写/列目录)、终端命令执行\n"
|
||||
"- 你可以自主完成:代码编写、bug 修复、样式调整、接口修改、简单重构\n"
|
||||
"- 遇到不熟悉的技术或 API,先查阅项目内的现有代码和文档参考\n"
|
||||
"- 需要上报的情况:\n"
|
||||
" • 架构设计或重大技术选型决策\n"
|
||||
" • 依赖包版本不兼容导致的编译/运行时错误(需要升级/降级依赖时)\n"
|
||||
" • 工具调用异常、环境配置问题、命令超时等非代码问题\n"
|
||||
" • 超出你能力范围或不确定的问题\n"
|
||||
"\n"
|
||||
"## 编译/构建守则\n"
|
||||
"- 写完代码后,优先运行编译或构建命令验证\n"
|
||||
"- 编译报错时,先判断错误类型:\n"
|
||||
" • 代码语法/逻辑错误 → 自行修复后重试\n"
|
||||
" • 依赖缺失或版本不兼容 → 上报,由主控决定处理方式\n"
|
||||
" • 环境/工具问题 → 上报\n"
|
||||
"- 连续修复 3 次仍无法通过编译时,上报当前状态和所有错误信息\n"
|
||||
"- 只有编译通过后才算任务完成\n"
|
||||
"\n"
|
||||
"## 报告格式\n"
|
||||
"完成任务后,结构化报告:\n"
|
||||
"**状态**:成功 / 部分完成 / 失败(需上报)\n"
|
||||
"**修改的文件**:列出所有修改的文件路径\n"
|
||||
"**变更摘要**:简述做了什么\n"
|
||||
"**验证结果**:编译/测试是否通过,如有警告需列出\n"
|
||||
"**需要主控关注**:如有需要上报的问题,详细说明"
|
||||
),
|
||||
tools=[filesystem_mcp, run_command_tool, preload_memory],
|
||||
after_agent_callback=auto_save_memory_callback,
|
||||
)
|
||||
126
agents/luna/api_server.py
Normal file
126
agents/luna/api_server.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""
|
||||
Luna Agent API Server
|
||||
使用 ADK 官方 ApiServer 构建 REST API 服务,支持:
|
||||
- REST API 调用 agent(/run、/run_sse)
|
||||
- 会话管理(创建/获取/删除,SQLite 持久化)
|
||||
- Swagger UI 交互式文档(/docs)
|
||||
- 上下文自动压缩
|
||||
- 长期记忆(InMemory,后续可换向量库)
|
||||
|
||||
启动方式:
|
||||
python api_server.py
|
||||
|
||||
主要端点:
|
||||
GET /list-apps 列出所有 agent
|
||||
POST /run 同步运行 agent
|
||||
POST /run_sse 流式运行 agent(SSE)
|
||||
GET /apps/{app}/users/{user}/sessions/{session} 获取会话
|
||||
POST /apps/{app}/users/{user}/sessions/{session} 创建会话
|
||||
GET /docs Swagger UI
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 脚本所在目录(作为 .env / data 等相对路径的基准)
|
||||
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
# 项目根目录(往上两级),确保 from agents.xxx.xxx import 可用
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(PROJECT_ROOT, "../.."))
|
||||
if _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(os.path.join(PROJECT_ROOT, "", ".env"))
|
||||
|
||||
# 强制 UTF-8
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
|
||||
import uvicorn
|
||||
from google.adk.cli.api_server import ApiServer
|
||||
from google.adk.cli.utils.base_agent_loader import BaseAgentLoader
|
||||
from google.adk.sessions.sqlite_session_service import SqliteSessionService
|
||||
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService
|
||||
from google.adk.evaluation.in_memory_eval_sets_manager import InMemoryEvalSetsManager
|
||||
from google.adk.evaluation.local_eval_set_results_manager import LocalEvalSetResultsManager
|
||||
from agents.luna.app import dev_app
|
||||
|
||||
|
||||
# 配置
|
||||
HOST = os.getenv("API_SERVER_HOST", "0.0.0.0")
|
||||
PORT = int(os.getenv("API_SERVER_PORT", "8002"))
|
||||
|
||||
# 数据目录
|
||||
DATA_DIR = os.path.join(PROJECT_ROOT, "../../data")
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
|
||||
class DevAgentLoader(BaseAgentLoader):
|
||||
"""自定义 agent 加载器,直接返回我们的 App 对象(带 compaction 配置)"""
|
||||
|
||||
def load_agent(self, agent_name: str):
|
||||
if agent_name == dev_app.name:
|
||||
return dev_app
|
||||
raise ValueError(f"Agent not found: {agent_name}")
|
||||
|
||||
def list_agents(self) -> list[str]:
|
||||
return [dev_app.name]
|
||||
|
||||
|
||||
def create_api_server() -> ApiServer:
|
||||
"""构造 ApiServer 实例"""
|
||||
# 会话服务:SQLite 持久化
|
||||
session_service = SqliteSessionService(
|
||||
db_path=os.path.join(DATA_DIR, "sessions_luna.db")
|
||||
)
|
||||
|
||||
# 记忆服务:长期记忆(先用内存版)
|
||||
memory_service = InMemoryMemoryService()
|
||||
|
||||
# 工件服务
|
||||
artifact_service = InMemoryArtifactService()
|
||||
|
||||
# 认证服务(暂不需要,内存版占位)
|
||||
credential_service = InMemoryCredentialService()
|
||||
|
||||
# 评测集管理(暂不需要,占位)
|
||||
eval_sets_manager = InMemoryEvalSetsManager()
|
||||
eval_set_results_manager = LocalEvalSetResultsManager(agents_dir=DATA_DIR)
|
||||
|
||||
return ApiServer(
|
||||
agent_loader=DevAgentLoader(),
|
||||
session_service=session_service,
|
||||
memory_service=memory_service,
|
||||
artifact_service=artifact_service,
|
||||
credential_service=credential_service,
|
||||
eval_sets_manager=eval_sets_manager,
|
||||
eval_set_results_manager=eval_set_results_manager,
|
||||
agents_dir=os.path.join(PROJECT_ROOT, ""),
|
||||
auto_create_session=True,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
api_server = create_api_server()
|
||||
fastapi_app = api_server.get_fast_api_app()
|
||||
|
||||
print("=" * 60)
|
||||
print("Luna Agent API Server 启动中...")
|
||||
print(f" 模型: {dev_app.root_agent.model.model}")
|
||||
print(f" 监听地址: http://{HOST}:{PORT}")
|
||||
print(f" Swagger UI: http://{HOST}:{PORT}/docs")
|
||||
print(f" 同步运行: POST http://{HOST}:{PORT}/run")
|
||||
print(f" 流式运行: POST http://{HOST}:{PORT}/run_sse")
|
||||
print(f" 列出agent: GET http://{HOST}:{PORT}/list-apps")
|
||||
print(f" 会话持久化: SQLite ({DATA_DIR}/sessions_luna.db)")
|
||||
print(f" 上下文压缩: 已启用")
|
||||
print("=" * 60)
|
||||
|
||||
uvicorn.run(fastapi_app, host=HOST, port=PORT, log_level="info")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -4,7 +4,7 @@ Dev Agent App 配置
|
||||
"""
|
||||
from google.adk.apps import App
|
||||
from google.adk.apps._configs import EventsCompactionConfig # 实验性 API
|
||||
from my_agent.agent import root_agent
|
||||
from agents.luna.agent import root_agent
|
||||
|
||||
|
||||
# 上下文压缩配置(长对话自动摘要,防止爆 context window)
|
||||
@ -17,7 +17,7 @@ compaction_config = EventsCompactionConfig(
|
||||
|
||||
# App 容器:管理 agent + 压缩配置
|
||||
dev_app = App(
|
||||
name="dev_agent",
|
||||
name="luna_agent",
|
||||
root_agent=root_agent,
|
||||
events_compaction_config=compaction_config,
|
||||
)
|
||||
173
agents/luna/chat.py
Normal file
173
agents/luna/chat.py
Normal file
@ -0,0 +1,173 @@
|
||||
"""
|
||||
Luna Agent 命令行交互工具
|
||||
使用配置好的 Runner(SQLite 会话持久化 + Memory + 上下文压缩),
|
||||
退出后再次进入同一个 session_id 可以继续对话。
|
||||
|
||||
使用方式:
|
||||
python chat.py # 新会话,自动生成 session_id
|
||||
python chat.py --session my_session # 指定 session_id
|
||||
python chat.py --list # 列出所有会话
|
||||
python chat.py --delete my_session # 删除某个会话
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import argparse
|
||||
|
||||
# 脚本所在目录(作为 .env / data 等相对路径的基准)
|
||||
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
# 项目根目录(往上两级),确保 from agents.xxx.xxx import 可用
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(PROJECT_ROOT, "../.."))
|
||||
if _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(os.path.join(PROJECT_ROOT, "", ".env"))
|
||||
|
||||
# 强制 UTF-8
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions.sqlite_session_service import SqliteSessionService
|
||||
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.genai import types
|
||||
from agents.luna.app import dev_app
|
||||
|
||||
|
||||
# 数据目录
|
||||
DATA_DIR = os.path.join(PROJECT_ROOT, "../../data")
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
# 同一个数据库,A2A server 和 CLI 共享
|
||||
DB_PATH = os.path.join(DATA_DIR, "sessions_luna.db")
|
||||
|
||||
USER_ID = "local_user"
|
||||
|
||||
|
||||
def get_runner() -> Runner:
|
||||
"""创建带 SQLite 会话持久化的 Runner"""
|
||||
session_service = SqliteSessionService(db_path=DB_PATH)
|
||||
memory_service = InMemoryMemoryService()
|
||||
artifact_service = InMemoryArtifactService()
|
||||
|
||||
return Runner(
|
||||
app=dev_app,
|
||||
session_service=session_service,
|
||||
memory_service=memory_service,
|
||||
artifact_service=artifact_service,
|
||||
auto_create_session=True,
|
||||
)
|
||||
|
||||
|
||||
async def list_sessions():
|
||||
"""列出所有会话"""
|
||||
runner = get_runner()
|
||||
response = await runner.session_service.list_sessions(
|
||||
app_name=dev_app.name,
|
||||
user_id=USER_ID,
|
||||
)
|
||||
sessions = response.sessions
|
||||
if not sessions:
|
||||
print("(暂无会话)")
|
||||
return
|
||||
|
||||
print(f"共 {len(sessions)} 个会话:\n")
|
||||
for s in sessions:
|
||||
# 取第一条用户消息作为摘要
|
||||
preview = ""
|
||||
for e in s.events:
|
||||
if e.content and e.content.parts and e.author == "user":
|
||||
text = e.content.parts[0].text[:50]
|
||||
preview = f" → {text}"
|
||||
break
|
||||
print(f" [{s.id}] {preview}")
|
||||
print(f" 更新时间: {s.last_update_time}")
|
||||
|
||||
|
||||
async def delete_session(session_id: str):
|
||||
"""删除指定会话"""
|
||||
runner = get_runner()
|
||||
try:
|
||||
await runner.session_service.delete_session(
|
||||
app_name=dev_app.name,
|
||||
user_id=USER_ID,
|
||||
session_id=session_id,
|
||||
)
|
||||
print(f"会话 [{session_id}] 已删除")
|
||||
except Exception as e:
|
||||
print(f"删除失败: {e}")
|
||||
|
||||
|
||||
async def chat(session_id: str | None = None):
|
||||
"""交互式对话"""
|
||||
runner = get_runner()
|
||||
|
||||
# 如果没有指定 session_id,自动创建
|
||||
if not session_id:
|
||||
session = await runner.session_service.create_session(
|
||||
app_name=dev_app.name,
|
||||
user_id=USER_ID,
|
||||
)
|
||||
session_id = session.id
|
||||
print(f"新会话已创建,session_id: {session_id}")
|
||||
print(f"下次可用 `python chat.py --session {session_id}` 继续\n")
|
||||
|
||||
print(f"=== Luna Agent 对话 ===")
|
||||
print(f"模型: {dev_app.root_agent.model.model}")
|
||||
print(f"Session: {session_id}")
|
||||
print(f"输入消息开始对话,输入 quit / exit 退出\n")
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("你: ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\n再见!")
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
if user_input.lower() in ("quit", "exit", "退出"):
|
||||
print("再见!")
|
||||
break
|
||||
|
||||
print("Luna: ", end="", flush=True)
|
||||
|
||||
try:
|
||||
async for event in runner.run_async(
|
||||
user_id=USER_ID,
|
||||
session_id=session_id,
|
||||
new_message=types.Content(parts=[types.Part(text=user_input)]),
|
||||
):
|
||||
if event.is_final_response():
|
||||
# 最终回复
|
||||
for part in event.content.parts:
|
||||
if hasattr(part, "text") and part.text:
|
||||
print(part.text, end="", flush=True)
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"\n[出错] {e}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Luna Agent 命令行交互工具")
|
||||
parser.add_argument("--session", "-s", help="会话 ID,指定后继续该会话")
|
||||
parser.add_argument("--list", "-l", action="store_true", help="列出所有会话")
|
||||
parser.add_argument("--delete", "-d", help="删除指定会话")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list:
|
||||
asyncio.run(list_sessions())
|
||||
elif args.delete:
|
||||
asyncio.run(delete_session(args.delete))
|
||||
else:
|
||||
asyncio.run(chat(args.session))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
agents/my_agent/__init__.py
Normal file
2
agents/my_agent/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# my_agent package
|
||||
from . import agent
|
||||
@ -150,8 +150,8 @@ root_agent = LlmAgent(
|
||||
api_key=api_key if api_key else None,
|
||||
custom_llm_provider="openai",
|
||||
),
|
||||
name="dev_agent",
|
||||
description="全栈开发子 Agent,可以读写文件、浏览目录、执行开发任务。",
|
||||
name="my_agent",
|
||||
description="全栈开发子 Agent(aq-first-combo),可以读写文件、浏览目录、执行开发任务。",
|
||||
instruction=(
|
||||
"全栈开发子 Agent\n"
|
||||
"\n"
|
||||
125
agents/my_agent/api_server.py
Normal file
125
agents/my_agent/api_server.py
Normal file
@ -0,0 +1,125 @@
|
||||
"""
|
||||
Dev Agent API Server
|
||||
使用 ADK 官方 ApiServer 构建 REST API 服务,支持:
|
||||
- REST API 调用 agent(/run、/run_sse)
|
||||
- 会话管理(创建/获取/删除,SQLite 持久化)
|
||||
- Swagger UI 交互式文档(/docs)
|
||||
- 上下文自动压缩
|
||||
- 长期记忆(InMemory,后续可换向量库)
|
||||
|
||||
启动方式:
|
||||
python api_server.py
|
||||
|
||||
主要端点:
|
||||
GET /list-apps 列出所有 agent
|
||||
POST /run 同步运行 agent
|
||||
POST /run_sse 流式运行 agent(SSE)
|
||||
GET /apps/{app}/users/{user}/sessions/{session} 获取会话
|
||||
POST /apps/{app}/users/{user}/sessions/{session} 创建会话
|
||||
GET /docs Swagger UI
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 脚本所在目录(作为 .env / data 等相对路径的基准)
|
||||
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
# 项目根目录(往上两级),确保 from agents.xxx.xxx import 可用
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(PROJECT_ROOT, "../.."))
|
||||
if _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(os.path.join(PROJECT_ROOT, "", ".env"))
|
||||
|
||||
# 强制 UTF-8
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
|
||||
import uvicorn
|
||||
from google.adk.cli.api_server import ApiServer
|
||||
from google.adk.cli.utils.base_agent_loader import BaseAgentLoader
|
||||
from google.adk.sessions.sqlite_session_service import SqliteSessionService
|
||||
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService
|
||||
from google.adk.evaluation.in_memory_eval_sets_manager import InMemoryEvalSetsManager
|
||||
from google.adk.evaluation.local_eval_set_results_manager import LocalEvalSetResultsManager
|
||||
from agents.my_agent.app import dev_app
|
||||
|
||||
|
||||
# 配置
|
||||
HOST = os.getenv("API_SERVER_HOST", "0.0.0.0")
|
||||
PORT = int(os.getenv("API_SERVER_PORT", "8001"))
|
||||
|
||||
# 数据目录
|
||||
DATA_DIR = os.path.join(PROJECT_ROOT, "../../data")
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
|
||||
class DevAgentLoader(BaseAgentLoader):
|
||||
"""自定义 agent 加载器,直接返回我们的 App 对象(带 compaction 配置)"""
|
||||
|
||||
def load_agent(self, agent_name: str):
|
||||
if agent_name == dev_app.name:
|
||||
return dev_app
|
||||
raise ValueError(f"Agent not found: {agent_name}")
|
||||
|
||||
def list_agents(self) -> list[str]:
|
||||
return [dev_app.name]
|
||||
|
||||
|
||||
def create_api_server() -> ApiServer:
|
||||
"""构造 ApiServer 实例"""
|
||||
# 会话服务:SQLite 持久化
|
||||
session_service = SqliteSessionService(
|
||||
db_path=os.path.join(DATA_DIR, "sessions.db")
|
||||
)
|
||||
|
||||
# 记忆服务:长期记忆(先用内存版)
|
||||
memory_service = InMemoryMemoryService()
|
||||
|
||||
# 工件服务
|
||||
artifact_service = InMemoryArtifactService()
|
||||
|
||||
# 认证服务(暂不需要,内存版占位)
|
||||
credential_service = InMemoryCredentialService()
|
||||
|
||||
# 评测集管理(暂不需要,占位)
|
||||
eval_sets_manager = InMemoryEvalSetsManager()
|
||||
eval_set_results_manager = LocalEvalSetResultsManager(agents_dir=DATA_DIR)
|
||||
|
||||
return ApiServer(
|
||||
agent_loader=DevAgentLoader(),
|
||||
session_service=session_service,
|
||||
memory_service=memory_service,
|
||||
artifact_service=artifact_service,
|
||||
credential_service=credential_service,
|
||||
eval_sets_manager=eval_sets_manager,
|
||||
eval_set_results_manager=eval_set_results_manager,
|
||||
agents_dir=os.path.join(PROJECT_ROOT, ""),
|
||||
auto_create_session=True,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
api_server = create_api_server()
|
||||
fastapi_app = api_server.get_fast_api_app()
|
||||
|
||||
print("=" * 60)
|
||||
print("Dev Agent API Server 启动中...")
|
||||
print(f" 监听地址: http://{HOST}:{PORT}")
|
||||
print(f" Swagger UI: http://{HOST}:{PORT}/docs")
|
||||
print(f" 同步运行: POST http://{HOST}:{PORT}/run")
|
||||
print(f" 流式运行: POST http://{HOST}:{PORT}/run_sse")
|
||||
print(f" 列出agent: GET http://{HOST}:{PORT}/list-apps")
|
||||
print(f" 会话持久化: SQLite ({DATA_DIR}/sessions.db)")
|
||||
print(f" 上下文压缩: 已启用")
|
||||
print("=" * 60)
|
||||
|
||||
uvicorn.run(fastapi_app, host=HOST, port=PORT, log_level="info")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
23
agents/my_agent/app.py
Normal file
23
agents/my_agent/app.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""
|
||||
Dev Agent App 配置
|
||||
使用 ADK App 包装 agent,配置上下文压缩、插件等。
|
||||
"""
|
||||
from google.adk.apps import App
|
||||
from google.adk.apps._configs import EventsCompactionConfig # 实验性 API
|
||||
from agents.my_agent.agent import root_agent
|
||||
|
||||
|
||||
# 上下文压缩配置(长对话自动摘要,防止爆 context window)
|
||||
compaction_config = EventsCompactionConfig(
|
||||
compaction_interval=20, # 每 20 个用户轮次压缩一次
|
||||
overlap_size=3, # 重叠 3 轮,保持连续性
|
||||
token_threshold=50000, # token 超 50k 紧急压缩
|
||||
event_retention_size=30, # 压缩时保留最近 30 条原始事件
|
||||
)
|
||||
|
||||
# App 容器:管理 agent + 压缩配置
|
||||
dev_app = App(
|
||||
name="my_agent",
|
||||
root_agent=root_agent,
|
||||
events_compaction_config=compaction_config,
|
||||
)
|
||||
174
agents/my_agent/chat.py
Normal file
174
agents/my_agent/chat.py
Normal file
@ -0,0 +1,174 @@
|
||||
"""
|
||||
Dev Agent 命令行交互工具
|
||||
使用配置好的 Runner(SQLite 会话持久化 + Memory + 上下文压缩),
|
||||
退出后再次进入同一个 session_id 可以继续对话。
|
||||
|
||||
使用方式:
|
||||
python chat.py # 新会话,自动生成 session_id
|
||||
python chat.py --session my_session # 指定 session_id
|
||||
python chat.py --list # 列出所有会话
|
||||
python chat.py --delete my_session # 删除某个会话
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import argparse
|
||||
|
||||
# 脚本所在目录(作为 .env / data 等相对路径的基准)
|
||||
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
# 项目根目录(往上两级),确保 from agents.xxx.xxx import 可用
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(PROJECT_ROOT, "../.."))
|
||||
if _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(os.path.join(PROJECT_ROOT, "", ".env"))
|
||||
|
||||
# 强制 UTF-8
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions.sqlite_session_service import SqliteSessionService
|
||||
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.genai import types
|
||||
from agents.my_agent.app import dev_app
|
||||
|
||||
|
||||
# 数据目录
|
||||
DATA_DIR = os.path.join(PROJECT_ROOT, "../../data")
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
# 同一个数据库,A2A server 和 CLI 共享
|
||||
DB_PATH = os.path.join(DATA_DIR, "sessions.db")
|
||||
|
||||
USER_ID = "local_user"
|
||||
|
||||
|
||||
def get_runner() -> Runner:
|
||||
"""创建带 SQLite 会话持久化的 Runner"""
|
||||
session_service = SqliteSessionService(db_path=DB_PATH)
|
||||
memory_service = InMemoryMemoryService()
|
||||
artifact_service = InMemoryArtifactService()
|
||||
|
||||
return Runner(
|
||||
app=dev_app,
|
||||
session_service=session_service,
|
||||
memory_service=memory_service,
|
||||
artifact_service=artifact_service,
|
||||
auto_create_session=True,
|
||||
)
|
||||
|
||||
|
||||
async def list_sessions():
|
||||
"""列出所有会话"""
|
||||
runner = get_runner()
|
||||
response = await runner.session_service.list_sessions(
|
||||
app_name=dev_app.name,
|
||||
user_id=USER_ID,
|
||||
)
|
||||
sessions = response.sessions
|
||||
if not sessions:
|
||||
print("(暂无会话)")
|
||||
return
|
||||
|
||||
print(f"共 {len(sessions)} 个会话:\n")
|
||||
for s in sessions:
|
||||
# 取第一条用户消息作为摘要
|
||||
preview = ""
|
||||
for e in s.events:
|
||||
if e.content and e.content.parts and e.author == "user":
|
||||
text = e.content.parts[0].text[:50]
|
||||
preview = f" → {text}"
|
||||
break
|
||||
print(f" [{s.id}] {preview}")
|
||||
print(f" 更新时间: {s.last_update_time}")
|
||||
|
||||
|
||||
async def delete_session(session_id: str):
|
||||
"""删除指定会话"""
|
||||
runner = get_runner()
|
||||
try:
|
||||
await runner.session_service.delete_session(
|
||||
app_name=dev_app.name,
|
||||
user_id=USER_ID,
|
||||
session_id=session_id,
|
||||
)
|
||||
print(f"会话 [{session_id}] 已删除")
|
||||
except Exception as e:
|
||||
print(f"删除失败: {e}")
|
||||
|
||||
|
||||
async def chat(session_id: str | None = None):
|
||||
"""交互式对话"""
|
||||
runner = get_runner()
|
||||
|
||||
# 如果没有指定 session_id,自动创建
|
||||
if not session_id:
|
||||
session = await runner.session_service.create_session(
|
||||
app_name=dev_app.name,
|
||||
user_id=USER_ID,
|
||||
)
|
||||
session_id = session.id
|
||||
print(f"新会话已创建,session_id: {session_id}")
|
||||
print(f"下次可用 `python chat.py --session {session_id}` 继续\n")
|
||||
|
||||
print(f"=== Dev Agent 对话 ===")
|
||||
print(f"Session: {session_id}")
|
||||
print(f"输入消息开始对话,输入 quit / exit 退出\n")
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("你: ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\n再见!")
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
if user_input.lower() in ("quit", "exit", "退出"):
|
||||
print("再见!")
|
||||
break
|
||||
|
||||
print("花花: ", end="", flush=True)
|
||||
|
||||
try:
|
||||
full_response = ""
|
||||
async for event in runner.run_async(
|
||||
user_id=USER_ID,
|
||||
session_id=session_id,
|
||||
new_message=types.Content(parts=[types.Part(text=user_input)]),
|
||||
):
|
||||
if event.is_final_response():
|
||||
# 最终回复
|
||||
for part in event.content.parts:
|
||||
if hasattr(part, "text") and part.text:
|
||||
print(part.text, end="", flush=True)
|
||||
full_response += part.text
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"\n[出错] {e}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Dev Agent 命令行交互工具")
|
||||
parser.add_argument("--session", "-s", help="会话 ID,指定后继续该会话")
|
||||
parser.add_argument("--list", "-l", action="store_true", help="列出所有会话")
|
||||
parser.add_argument("--delete", "-d", help="删除指定会话")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list:
|
||||
asyncio.run(list_sessions())
|
||||
elif args.delete:
|
||||
asyncio.run(delete_session(args.delete))
|
||||
else:
|
||||
asyncio.run(chat(args.session))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
agents/qwen/.gitignore
vendored
Normal file
1
agents/qwen/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.env
|
||||
0
agents/qwen/__init__.py
Normal file
0
agents/qwen/__init__.py
Normal file
200
agents/qwen/agent.py
Normal file
200
agents/qwen/agent.py
Normal file
@ -0,0 +1,200 @@
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
|
||||
from google.adk.tools.function_tool import FunctionTool
|
||||
from google.adk.tools import preload_memory
|
||||
from mcp.client.stdio import StdioServerParameters
|
||||
import os
|
||||
import asyncio
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# --- 使用 vLLM 端点的智能体 ---
|
||||
api_base_url = os.getenv("VLLM_API_BASE", "https://9router.aqroid.cn/v1")
|
||||
model_name = os.getenv("VLLM_MODEL", "")
|
||||
api_key = os.getenv("VLLM_API_KEY", "")
|
||||
|
||||
# Agent 可访问的工作目录
|
||||
WORKSPACE_DIR = os.getenv("AGENT_WORKSPACE_DIR", r"D:\nzy\workspace_git")
|
||||
|
||||
# --- 文件系统 MCP 工具 ---
|
||||
filesystem_mcp = McpToolset(
|
||||
connection_params=StdioConnectionParams(
|
||||
server_params=StdioServerParameters(
|
||||
command="npx",
|
||||
args=[
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem",
|
||||
os.path.abspath(WORKSPACE_DIR),
|
||||
],
|
||||
),
|
||||
timeout=300000,
|
||||
),
|
||||
)
|
||||
|
||||
# --- 网页抓取 MCP 工具(Fetch)---
|
||||
# 暂时注释:请求体过大导致 413 错误,后续需要时再启用
|
||||
# fetch_mcp = McpToolset(
|
||||
# connection_params=StdioConnectionParams(
|
||||
# server_params=StdioServerParameters(
|
||||
# command="npx",
|
||||
# args=[
|
||||
# "-y",
|
||||
# "@modelcontextprotocol/server-fetch",
|
||||
# ],
|
||||
# ),
|
||||
# timeout=300000,
|
||||
# ),
|
||||
# )
|
||||
|
||||
# --- Tavily 搜索 MCP 工具 ---
|
||||
# 暂时注释:请求体过大导致 413 错误,后续需要时再启用
|
||||
# tavily_api_key = os.getenv("TAVILY_API_KEY", "")
|
||||
# tavily_mcp = McpToolset(
|
||||
# connection_params=StdioConnectionParams(
|
||||
# server_params=StdioServerParameters(
|
||||
# command="npx",
|
||||
# args=[
|
||||
# "-y",
|
||||
# "tavily-mcp@latest",
|
||||
# ],
|
||||
# env={
|
||||
# "TAVILY_API_KEY": tavily_api_key,
|
||||
# },
|
||||
# ),
|
||||
# timeout=300000,
|
||||
# ),
|
||||
# )
|
||||
|
||||
|
||||
# --- 终端命令执行工具(Python 原生,绕开 MCP 通信问题)---
|
||||
async def run_command(command: str, cwd: str = None, timeout: int = 300000) -> str:
|
||||
"""
|
||||
在终端中执行一条命令,返回输出结果。
|
||||
|
||||
Args:
|
||||
command: 要执行的命令,如 'npm run build'、'python -m pytest' 等
|
||||
cwd: 命令执行的工作目录,默认使用 AGENT_WORKSPACE_DIR
|
||||
timeout: 超时时间(秒),默认 300
|
||||
|
||||
Returns:
|
||||
命令执行结果(stdout + stderr + 状态)
|
||||
"""
|
||||
if not command:
|
||||
return "错误:命令不能为空"
|
||||
|
||||
work_dir = cwd or os.path.abspath(WORKSPACE_DIR)
|
||||
if not os.path.isdir(work_dir):
|
||||
return f"错误:工作目录不存在 {work_dir}"
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
command,
|
||||
cwd=work_dir,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
||||
proc.communicate(), timeout=timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
proc.kill()
|
||||
await proc.wait()
|
||||
return f"命令执行超时({timeout}秒): {command}"
|
||||
except Exception as e:
|
||||
return f"命令执行出错: {e}"
|
||||
|
||||
stdout = stdout_bytes.decode("utf-8", errors="replace")
|
||||
stderr = stderr_bytes.decode("utf-8", errors="replace")
|
||||
|
||||
parts = []
|
||||
if stdout:
|
||||
parts.append(f"[stdout]\n{stdout}")
|
||||
if stderr:
|
||||
parts.append(f"[stderr]\n{stderr}")
|
||||
|
||||
output = "\n".join(parts) if parts else "(无输出)"
|
||||
|
||||
max_len = 10000
|
||||
if len(output) > max_len:
|
||||
output = output[:max_len] + f"\n\n...(输出已截断,共 {len(output)} 字符)"
|
||||
|
||||
status = "成功" if proc.returncode == 0 else f"失败 (退出码 {proc.returncode})"
|
||||
return f"命令执行{status}\n{output}"
|
||||
|
||||
|
||||
# 注册为 ADK 工具
|
||||
run_command_tool = FunctionTool(run_command)
|
||||
|
||||
|
||||
# --- 记忆自动保存回调 ---
|
||||
async def auto_save_memory_callback(callback_context):
|
||||
"""
|
||||
每轮对话结束后,自动将会话保存到长期记忆中。
|
||||
memory_service 未配置时静默忽略。
|
||||
"""
|
||||
try:
|
||||
await callback_context.add_session_to_memory()
|
||||
except (ValueError, AttributeError):
|
||||
# memory_service 未配置或不可用时跳过
|
||||
pass
|
||||
|
||||
|
||||
root_agent = LlmAgent(
|
||||
model=LiteLlm(
|
||||
model=model_name,
|
||||
api_base=api_base_url,
|
||||
api_key=api_key if api_key else None,
|
||||
custom_llm_provider="openai",
|
||||
),
|
||||
name="qwen_agent",
|
||||
description="全栈开发子 Agent(qwen/astron-code),可以读写文件、浏览目录、执行开发任务。",
|
||||
instruction=(
|
||||
"千问-全栈开发子 Agent\n"
|
||||
"\n"
|
||||
"## 记忆能力\n"
|
||||
"- 你拥有长期记忆,之前和用户的对话中提到的项目信息、技术偏好、任务历史都会被记住\n"
|
||||
"- 系统会自动从记忆中检索与当前任务相关的历史上下文,注入到对话中\n"
|
||||
"- 重要的项目信息(技术栈、目录结构、编码规范等)会自动沉淀到记忆里\n"
|
||||
"\n"
|
||||
"## 工作流程\n"
|
||||
"1. 先理解任务需求和项目上下文\n"
|
||||
"2. 使用文件系统工具浏览项目结构、读取相关文件\n"
|
||||
"3. 编写或修改代码\n"
|
||||
"4. 使用 run_command 工具运行编译/构建/测试,确保代码可正常工作\n"
|
||||
"5. 验证结果后,按指定格式报告完成情况\n"
|
||||
"\n"
|
||||
"## 工作边界\n"
|
||||
"- 所有文件操作限定在分配的工作目录范围内\n"
|
||||
"- 你拥有的工具:文件系统操作(读/写/列目录)、终端命令执行\n"
|
||||
"- 你可以自主完成:代码编写、bug 修复、样式调整、接口修改、简单重构\n"
|
||||
"- 遇到不熟悉的技术或 API,先查阅项目内的现有代码和文档参考\n"
|
||||
"- 需要上报的情况:\n"
|
||||
" • 架构设计或重大技术选型决策\n"
|
||||
" • 依赖包版本不兼容导致的编译/运行时错误(需要升级/降级依赖时)\n"
|
||||
" • 工具调用异常、环境配置问题、命令超时等非代码问题\n"
|
||||
" • 超出你能力范围或不确定的问题\n"
|
||||
"\n"
|
||||
"## 编译/构建守则\n"
|
||||
"- 写完代码后,优先运行编译或构建命令验证\n"
|
||||
"- 编译报错时,先判断错误类型:\n"
|
||||
" • 代码语法/逻辑错误 → 自行修复后重试\n"
|
||||
" • 依赖缺失或版本不兼容 → 上报,由主控决定处理方式\n"
|
||||
" • 环境/工具问题 → 上报\n"
|
||||
"- 连续修复 3 次仍无法通过编译时,上报当前状态和所有错误信息\n"
|
||||
"- 只有编译通过后才算任务完成\n"
|
||||
"\n"
|
||||
"## 报告格式\n"
|
||||
"完成任务后,结构化报告:\n"
|
||||
"**状态**:成功 / 部分完成 / 失败(需上报)\n"
|
||||
"**修改的文件**:列出所有修改的文件路径\n"
|
||||
"**变更摘要**:简述做了什么\n"
|
||||
"**验证结果**:编译/测试是否通过,如有警告需列出\n"
|
||||
"**需要主控关注**:如有需要上报的问题,详细说明"
|
||||
),
|
||||
tools=[filesystem_mcp, run_command_tool, preload_memory],
|
||||
after_agent_callback=auto_save_memory_callback,
|
||||
)
|
||||
126
agents/qwen/api_server.py
Normal file
126
agents/qwen/api_server.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""
|
||||
Qwen Agent API Server
|
||||
使用 ADK 官方 ApiServer 构建 REST API 服务,支持:
|
||||
- REST API 调用 agent(/run、/run_sse)
|
||||
- 会话管理(创建/获取/删除,SQLite 持久化)
|
||||
- Swagger UI 交互式文档(/docs)
|
||||
- 上下文自动压缩
|
||||
- 长期记忆(InMemory,后续可换向量库)
|
||||
|
||||
启动方式:
|
||||
python api_server.py
|
||||
|
||||
主要端点:
|
||||
GET /list-apps 列出所有 agent
|
||||
POST /run 同步运行 agent
|
||||
POST /run_sse 流式运行 agent(SSE)
|
||||
GET /apps/{app}/users/{user}/sessions/{session} 获取会话
|
||||
POST /apps/{app}/users/{user}/sessions/{session} 创建会话
|
||||
GET /docs Swagger UI
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 脚本所在目录(作为 .env / data 等相对路径的基准)
|
||||
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
# 项目根目录(往上两级),确保 from agents.xxx.xxx import 可用
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(PROJECT_ROOT, "../.."))
|
||||
if _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(os.path.join(PROJECT_ROOT, "", ".env"))
|
||||
|
||||
# 强制 UTF-8
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
|
||||
import uvicorn
|
||||
from google.adk.cli.api_server import ApiServer
|
||||
from google.adk.cli.utils.base_agent_loader import BaseAgentLoader
|
||||
from google.adk.sessions.sqlite_session_service import SqliteSessionService
|
||||
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService
|
||||
from google.adk.evaluation.in_memory_eval_sets_manager import InMemoryEvalSetsManager
|
||||
from google.adk.evaluation.local_eval_set_results_manager import LocalEvalSetResultsManager
|
||||
from agents.qwen.app import dev_app
|
||||
|
||||
|
||||
# 配置
|
||||
HOST = os.getenv("API_SERVER_HOST", "0.0.0.0")
|
||||
PORT = int(os.getenv("API_SERVER_PORT", "8003"))
|
||||
|
||||
# 数据目录
|
||||
DATA_DIR = os.path.join(PROJECT_ROOT, "../../data")
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
|
||||
class DevAgentLoader(BaseAgentLoader):
|
||||
"""自定义 agent 加载器,直接返回我们的 App 对象(带 compaction 配置)"""
|
||||
|
||||
def load_agent(self, agent_name: str):
|
||||
if agent_name == dev_app.name:
|
||||
return dev_app
|
||||
raise ValueError(f"Agent not found: {agent_name}")
|
||||
|
||||
def list_agents(self) -> list[str]:
|
||||
return [dev_app.name]
|
||||
|
||||
|
||||
def create_api_server() -> ApiServer:
|
||||
"""构造 ApiServer 实例"""
|
||||
# 会话服务:SQLite 持久化
|
||||
session_service = SqliteSessionService(
|
||||
db_path=os.path.join(DATA_DIR, "sessions_qwen.db")
|
||||
)
|
||||
|
||||
# 记忆服务:长期记忆(先用内存版)
|
||||
memory_service = InMemoryMemoryService()
|
||||
|
||||
# 工件服务
|
||||
artifact_service = InMemoryArtifactService()
|
||||
|
||||
# 认证服务(暂不需要,内存版占位)
|
||||
credential_service = InMemoryCredentialService()
|
||||
|
||||
# 评测集管理(暂不需要,占位)
|
||||
eval_sets_manager = InMemoryEvalSetsManager()
|
||||
eval_set_results_manager = LocalEvalSetResultsManager(agents_dir=DATA_DIR)
|
||||
|
||||
return ApiServer(
|
||||
agent_loader=DevAgentLoader(),
|
||||
session_service=session_service,
|
||||
memory_service=memory_service,
|
||||
artifact_service=artifact_service,
|
||||
credential_service=credential_service,
|
||||
eval_sets_manager=eval_sets_manager,
|
||||
eval_set_results_manager=eval_set_results_manager,
|
||||
agents_dir=os.path.join(PROJECT_ROOT, ""),
|
||||
auto_create_session=True,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
api_server = create_api_server()
|
||||
fastapi_app = api_server.get_fast_api_app()
|
||||
|
||||
print("=" * 60)
|
||||
print("Qwen Agent API Server 启动中...")
|
||||
print(f" 模型: {dev_app.root_agent.model.model}")
|
||||
print(f" 监听地址: http://{HOST}:{PORT}")
|
||||
print(f" Swagger UI: http://{HOST}:{PORT}/docs")
|
||||
print(f" 同步运行: POST http://{HOST}:{PORT}/run")
|
||||
print(f" 流式运行: POST http://{HOST}:{PORT}/run_sse")
|
||||
print(f" 列出agent: GET http://{HOST}:{PORT}/list-apps")
|
||||
print(f" 会话持久化: SQLite ({DATA_DIR}/sessions_qwen.db)")
|
||||
print(f" 上下文压缩: 已启用")
|
||||
print("=" * 60)
|
||||
|
||||
uvicorn.run(fastapi_app, host=HOST, port=PORT, log_level="info")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
23
agents/qwen/app.py
Normal file
23
agents/qwen/app.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""
|
||||
Dev Agent App 配置
|
||||
使用 ADK App 包装 agent,配置上下文压缩、插件等。
|
||||
"""
|
||||
from google.adk.apps import App
|
||||
from google.adk.apps._configs import EventsCompactionConfig # 实验性 API
|
||||
from agents.qwen.agent import root_agent
|
||||
|
||||
|
||||
# 上下文压缩配置(长对话自动摘要,防止爆 context window)
|
||||
compaction_config = EventsCompactionConfig(
|
||||
compaction_interval=20, # 每 20 个用户轮次压缩一次
|
||||
overlap_size=3, # 重叠 3 轮,保持连续性
|
||||
token_threshold=50000, # token 超 50k 紧急压缩
|
||||
event_retention_size=30, # 压缩时保留最近 30 条原始事件
|
||||
)
|
||||
|
||||
# App 容器:管理 agent + 压缩配置
|
||||
dev_app = App(
|
||||
name="qwen_agent",
|
||||
root_agent=root_agent,
|
||||
events_compaction_config=compaction_config,
|
||||
)
|
||||
173
agents/qwen/chat.py
Normal file
173
agents/qwen/chat.py
Normal file
@ -0,0 +1,173 @@
|
||||
"""
|
||||
Qwen Agent 命令行交互工具
|
||||
使用配置好的 Runner(SQLite 会话持久化 + Memory + 上下文压缩),
|
||||
退出后再次进入同一个 session_id 可以继续对话。
|
||||
|
||||
使用方式:
|
||||
python chat.py # 新会话,自动生成 session_id
|
||||
python chat.py --session my_session # 指定 session_id
|
||||
python chat.py --list # 列出所有会话
|
||||
python chat.py --delete my_session # 删除某个会话
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import argparse
|
||||
|
||||
# 脚本所在目录(作为 .env / data 等相对路径的基准)
|
||||
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
# 项目根目录(往上两级),确保 from agents.xxx.xxx import 可用
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(PROJECT_ROOT, "../.."))
|
||||
if _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(os.path.join(PROJECT_ROOT, "", ".env"))
|
||||
|
||||
# 强制 UTF-8
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions.sqlite_session_service import SqliteSessionService
|
||||
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
||||
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from google.genai import types
|
||||
from agents.qwen.app import dev_app
|
||||
|
||||
|
||||
# 数据目录
|
||||
DATA_DIR = os.path.join(PROJECT_ROOT, "../../data")
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
# 同一个数据库,A2A server 和 CLI 共享
|
||||
DB_PATH = os.path.join(DATA_DIR, "sessions_qwen.db")
|
||||
|
||||
USER_ID = "local_user"
|
||||
|
||||
|
||||
def get_runner() -> Runner:
|
||||
"""创建带 SQLite 会话持久化的 Runner"""
|
||||
session_service = SqliteSessionService(db_path=DB_PATH)
|
||||
memory_service = InMemoryMemoryService()
|
||||
artifact_service = InMemoryArtifactService()
|
||||
|
||||
return Runner(
|
||||
app=dev_app,
|
||||
session_service=session_service,
|
||||
memory_service=memory_service,
|
||||
artifact_service=artifact_service,
|
||||
auto_create_session=True,
|
||||
)
|
||||
|
||||
|
||||
async def list_sessions():
|
||||
"""列出所有会话"""
|
||||
runner = get_runner()
|
||||
response = await runner.session_service.list_sessions(
|
||||
app_name=dev_app.name,
|
||||
user_id=USER_ID,
|
||||
)
|
||||
sessions = response.sessions
|
||||
if not sessions:
|
||||
print("(暂无会话)")
|
||||
return
|
||||
|
||||
print(f"共 {len(sessions)} 个会话:\n")
|
||||
for s in sessions:
|
||||
# 取第一条用户消息作为摘要
|
||||
preview = ""
|
||||
for e in s.events:
|
||||
if e.content and e.content.parts and e.author == "user":
|
||||
text = e.content.parts[0].text[:50]
|
||||
preview = f" → {text}"
|
||||
break
|
||||
print(f" [{s.id}] {preview}")
|
||||
print(f" 更新时间: {s.last_update_time}")
|
||||
|
||||
|
||||
async def delete_session(session_id: str):
|
||||
"""删除指定会话"""
|
||||
runner = get_runner()
|
||||
try:
|
||||
await runner.session_service.delete_session(
|
||||
app_name=dev_app.name,
|
||||
user_id=USER_ID,
|
||||
session_id=session_id,
|
||||
)
|
||||
print(f"会话 [{session_id}] 已删除")
|
||||
except Exception as e:
|
||||
print(f"删除失败: {e}")
|
||||
|
||||
|
||||
async def chat(session_id: str | None = None):
|
||||
"""交互式对话"""
|
||||
runner = get_runner()
|
||||
|
||||
# 如果没有指定 session_id,自动创建
|
||||
if not session_id:
|
||||
session = await runner.session_service.create_session(
|
||||
app_name=dev_app.name,
|
||||
user_id=USER_ID,
|
||||
)
|
||||
session_id = session.id
|
||||
print(f"新会话已创建,session_id: {session_id}")
|
||||
print(f"下次可用 `python chat.py --session {session_id}` 继续\n")
|
||||
|
||||
print(f"=== Qwen Agent 对话 ===")
|
||||
print(f"模型: {dev_app.root_agent.model.model}")
|
||||
print(f"Session: {session_id}")
|
||||
print(f"输入消息开始对话,输入 quit / exit 退出\n")
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("你: ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\n再见!")
|
||||
break
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
if user_input.lower() in ("quit", "exit", "退出"):
|
||||
print("再见!")
|
||||
break
|
||||
|
||||
print("Qwen: ", end="", flush=True)
|
||||
|
||||
try:
|
||||
async for event in runner.run_async(
|
||||
user_id=USER_ID,
|
||||
session_id=session_id,
|
||||
new_message=types.Content(parts=[types.Part(text=user_input)]),
|
||||
):
|
||||
if event.is_final_response():
|
||||
# 最终回复
|
||||
for part in event.content.parts:
|
||||
if hasattr(part, "text") and part.text:
|
||||
print(part.text, end="", flush=True)
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"\n[出错] {e}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Qwen Agent 命令行交互工具")
|
||||
parser.add_argument("--session", "-s", help="会话 ID,指定后继续该会话")
|
||||
parser.add_argument("--list", "-l", action="store_true", help="列出所有会话")
|
||||
parser.add_argument("--delete", "-d", help="删除指定会话")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list:
|
||||
asyncio.run(list_sessions())
|
||||
elif args.delete:
|
||||
asyncio.run(delete_session(args.delete))
|
||||
else:
|
||||
asyncio.run(chat(args.session))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
data/sessions.db
Normal file
BIN
data/sessions.db
Normal file
Binary file not shown.
BIN
data/sessions_luna.db
Normal file
BIN
data/sessions_luna.db
Normal file
Binary file not shown.
BIN
data/sessions_qwen.db
Normal file
BIN
data/sessions_qwen.db
Normal file
Binary file not shown.
18
data/tasks/238726a8.json
Normal file
18
data/tasks/238726a8.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "238726a8",
|
||||
"description": "【重要任务】重构勤务统计页面(schedulingStatistical.vue),将现有占位页面改为大屏可视化风格,像素级复刻老项目样式。\n\n==== 项目信息 ====\n项目路径:d:/nzy/workspace_git/Baseplatform-Ui-V3\n目标文件:src/views/serviceManage/schedulingStatistical.vue\n老项目参考:D:/nzy/workspace_git/baseplatform-ui/src/views/postPage/schedulingStatistical.vue\n\n==== 必须遵守的迁移规范(非常重要) ====\n\n1. 【页面外壳】使用 BgAndInfo 组件:\n import BgAndInfo from '/@/components/Common/bgAndInfo.vue'\n 用法:<bg-and-info title-menu=\"勤务管理\" title-name=\"勤务统计\" step-url=\"/frontend/schedulingStatistical\" bg-size=\"small\">\n 参考已实现的 serviceIndexDd.vue 和 workData.vue\n\n2. 【颜色规范】深色科技感大屏风格:\n 主色:#29B3FF\n 背景:#0A1A29\n 文字主色:#eaf8ff\n 文字次色:#7aabc6\n 强调色:#FFC34D (金黄)、#55E1CA (青色)、#FF6565 (红色)\n 面板背景:rgba(4, 21, 40, 0.5) ~ (0.6)\n 面板边框:rgba(41, 179, 255, 0.15) ~ (0.3)\n\n3. 【图片引用规则 — 绝对不能错】\n 禁止使用 <img src=\"/@/assets/...\"> 方式(运行时 vite 无法解析)\n 必须用 CSS background-image: url('/@/assets/...') 方式\n 所有图标都这样写,包括按钮图标、列表图标等\n\n4. 【图标资源位置】\n sandImg 通用图标:src/assets/sandImg/ (133个,从老项目拷贝的)\n service 专属图标:src/assets/service/ (42个,从老项目拷贝的)\n work 专属图标:src/assets/map_image/work/ (45个)\n 背景图:src/assets/map_image/ 下的 ponding/, small/, schdulingCommand/ 等\n\n5. 【技术栈】\n Vue3 Composition API + TypeScript\n ref / reactive / computed / onMounted / onUnmounted\n 图表:import * as echarts from 'echarts'\n 图表在 onMounted 中初始化,onUnmounted 中 dispose\n 样式:<style scoped lang=\"scss\">\n\n6. 【数据】\n 全部用 Mock 数据,不要调真实 API\n 数据量适中:列表8-15条,图表5-12个数据点\n 数据字段名尽量参考老项目,方便后续接 API\n\n7. 【参考页面】\n 勤务排班:src/views/serviceManage/serviceIndexDd.vue (已完成,左右+中部布局)\n 施工智管:src/views/serviceManage/workData.vue (已完成,左列表+右4面板)\n 情报研判相关:src/views/intelligenceAnalysis/trafficPerception.vue\n\n8. 【老项目分析】\n 先读取老项目 D:/nzy/workspace_git/baseplatform-ui/src/views/postPage/schedulingStatistical.vue 的完整代码\n 分析布局结构和功能模块\n 然后根据老项目的布局来实现\n\n9. 【验收标准】\n - 构建通过:npx vite build 无错误\n - 无 <img src=\"/@/assets/...\"> 写法\n - 视觉风格与老项目一致(布局、颜色、字体)\n - 交互完整(切换、筛选、列表点击等)\n - 全部 Mock 数据\n\n请先分析老项目页面结构,然后再开始编码。完成后告诉我你做了什么。",
|
||||
"project_path": "d:/nzy/workspace_git/Baseplatform-Ui-V3",
|
||||
"requirements": "",
|
||||
"status": "completed",
|
||||
"created_at": 1785464745.188234,
|
||||
"updated_at": 1785465287.061655,
|
||||
"result": {
|
||||
"summary": "(无响应)",
|
||||
"full_response": "",
|
||||
"tool_calls_count": 0,
|
||||
"tool_calls_sample": [],
|
||||
"status": "empty"
|
||||
},
|
||||
"logs": [],
|
||||
"extra": {}
|
||||
}
|
||||
18
data/tasks/3baaed45.json
Normal file
18
data/tasks/3baaed45.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "3baaed45",
|
||||
"description": "测试任务:请在 d:/nzy/workspace_git/Baseplatform-Ui-V3/ 目录下创建一个文件 test-dev-agent-2.txt,内容写 'hello from dev agent test 2',然后读取文件并报告内容。",
|
||||
"project_path": "d:/nzy/workspace_git/Baseplatform-Ui-V3",
|
||||
"requirements": "",
|
||||
"status": "completed",
|
||||
"created_at": 1785467946.3815768,
|
||||
"updated_at": 1785467957.7573347,
|
||||
"result": {
|
||||
"summary": "(无响应)",
|
||||
"full_response": "",
|
||||
"tool_calls_count": 0,
|
||||
"tool_calls_sample": [],
|
||||
"status": "empty"
|
||||
},
|
||||
"logs": [],
|
||||
"extra": {}
|
||||
}
|
||||
18
data/tasks/57549fbd.json
Normal file
18
data/tasks/57549fbd.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "57549fbd",
|
||||
"description": "重构施工智管页面(workData.vue),将现有简单的卡片网格页面改为大屏可视化风格,像素级复刻老项目的样式和布局。\n\n【老项目参考】\n老项目路径:D:/nzy/workspace_git/baseplatform-ui/src/views/work/workData.vue\n请先读取老项目页面的完整代码,了解布局结构。\n\n【重构经验和规范】\n1. 使用 BgAndInfo 组件作为页面外壳:import BgAndInfo from '/@/components/Common/bgAndInfo.vue',用法 <bg-and-info title-menu=\"勤务管理\" title-name=\"施工智管\" step-url=\"/frontend/workData\">\n2. 整体风格:深色科技感大屏,主色 #29B3FF,背景 #0A1A29,文字主色 #eaf8ff,次色 #7aabc6\n3. 图片引用必须用 CSS background-image: url('/@/assets/...') 方式,绝对不能用 <img src=\"/@/assets/...\">(运行时vite无法解析)\n4. 图标使用 src/assets/sandImg/ 目录下的老项目图标(已全部拷贝,133个PNG)\n5. 使用 Mock 数据,不需要接真实 API,数据量适中即可(8-15条)\n6. Vue3 Composition API + TypeScript,ref/reactive/computed\n7. 图表用 ECharts(import * as echarts from 'echarts'),在 onMounted 中初始化,onUnmounted 中 dispose\n8. 样式用 <style scoped lang=\"scss\">\n9. 现有页面可以全部重写,不用保留\n10. 参考已有的大屏页面布局风格:src/views/commandDispatch/carManager/index.vue(左右面板布局)和 src/views/commandDispatch/sandIndex/index.vue(整体框架)\n\n【页面功能区】\n老项目施工智管大致包含:顶部筛选工具栏(时间、类型、区域筛选等)、施工项目列表/卡片网格(项目名称、位置、状态、工期、责任人等信息)。请根据老项目实际结构来实现。\n\n【验收标准】\n- 构建通过:npx vite build 无错误\n- 无 <img src=\"/@/assets/...\"> 写法\n- 视觉风格与老项目一致(布局、颜色、字体)\n- 功能交互完整(筛选、切换、详情等)\n- 全是 Mock 数据,无真实 API 调用",
|
||||
"project_path": "d:/nzy/workspace_git/Baseplatform-Ui-V3",
|
||||
"requirements": "",
|
||||
"status": "completed",
|
||||
"created_at": 1785460630.3209536,
|
||||
"updated_at": 1785460700.2011487,
|
||||
"result": {
|
||||
"summary": "(无响应)",
|
||||
"full_response": "",
|
||||
"tool_calls_count": 0,
|
||||
"tool_calls_sample": [],
|
||||
"status": "empty"
|
||||
},
|
||||
"logs": [],
|
||||
"extra": {}
|
||||
}
|
||||
18
data/tasks/72ed062f.json
Normal file
18
data/tasks/72ed062f.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "72ed062f",
|
||||
"description": "在 Baseplatform-Ui-V3 项目的 src/views/serviceManage/ 目录下创建一个名为 dev-agent-test.md 的测试文件,文件内容为:# Dev Agent 测试文件\n\n这是一个测试文件,用于验证 dev-agent 可以正常编辑文件。\n\n- 项目路径:D:/nzy/workspace_git/Baseplatform-Ui-V3\n- 操作:创建测试文件",
|
||||
"project_path": "d:/nzy/workspace_git/Baseplatform-Ui-V3",
|
||||
"requirements": "",
|
||||
"status": "completed",
|
||||
"created_at": 1785464261.6740465,
|
||||
"updated_at": 1785464284.0124946,
|
||||
"result": {
|
||||
"summary": "(无响应)",
|
||||
"full_response": "",
|
||||
"tool_calls_count": 0,
|
||||
"tool_calls_sample": [],
|
||||
"status": "empty"
|
||||
},
|
||||
"logs": [],
|
||||
"extra": {}
|
||||
}
|
||||
18
data/tasks/ecf6aced.json
Normal file
18
data/tasks/ecf6aced.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "ecf6aced",
|
||||
"description": "测试任务:请在项目目录下创建一个名为 dev-agent-test.txt 的文件,内容为 hello from dev-agent,然后返回文件的完整路径。确认你能正常读写文件。",
|
||||
"project_path": "d:\\nzy\\workspace_git\\Baseplatform-Ui-V3",
|
||||
"requirements": "",
|
||||
"status": "completed",
|
||||
"created_at": 1785463307.6479685,
|
||||
"updated_at": 1785463318.5050843,
|
||||
"result": {
|
||||
"summary": "(无响应)",
|
||||
"full_response": "",
|
||||
"tool_calls_count": 0,
|
||||
"tool_calls_sample": [],
|
||||
"status": "empty"
|
||||
},
|
||||
"logs": [],
|
||||
"extra": {}
|
||||
}
|
||||
22
mcp_dev_agent/luna_server.py
Normal file
22
mcp_dev_agent/luna_server.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""
|
||||
Luna Agent MCP Server (gpt-5.6-luna)
|
||||
独立入口脚本,直接用 venv python 启动。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 硬编码配置(优先级高于环境变量)
|
||||
os.environ["DEV_AGENT_API_URL"] = "http://127.0.0.1:8002"
|
||||
os.environ["DEV_AGENT_APP_NAME"] = "luna_agent"
|
||||
os.environ["DEV_AGENT_USER_ID"] = "codebuddy"
|
||||
|
||||
# 确保项目根目录在 path 里
|
||||
_PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if _PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, _PROJECT_ROOT)
|
||||
|
||||
# 导入通用 server 并运行
|
||||
from mcp_dev_agent.server import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
22
mcp_dev_agent/my_agent_server.py
Normal file
22
mcp_dev_agent/my_agent_server.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""
|
||||
My Agent MCP Server (aq-first-combo)
|
||||
独立入口脚本,直接用 venv python 启动。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 硬编码配置(优先级高于环境变量)
|
||||
os.environ["DEV_AGENT_API_URL"] = "http://127.0.0.1:8001"
|
||||
os.environ["DEV_AGENT_APP_NAME"] = "my_agent"
|
||||
os.environ["DEV_AGENT_USER_ID"] = "codebuddy"
|
||||
|
||||
# 确保项目根目录在 path 里
|
||||
_PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if _PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, _PROJECT_ROOT)
|
||||
|
||||
# 导入通用 server 并运行
|
||||
from mcp_dev_agent.server import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
22
mcp_dev_agent/qwen_server.py
Normal file
22
mcp_dev_agent/qwen_server.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""
|
||||
Qwen Agent MCP Server (astron-code-latest)
|
||||
独立入口脚本,直接用 venv python 启动。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 硬编码配置(优先级高于环境变量)
|
||||
os.environ["DEV_AGENT_API_URL"] = "http://127.0.0.1:8003"
|
||||
os.environ["DEV_AGENT_APP_NAME"] = "qwen_agent"
|
||||
os.environ["DEV_AGENT_USER_ID"] = "codebuddy"
|
||||
|
||||
# 确保项目根目录在 path 里
|
||||
_PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if _PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, _PROJECT_ROOT)
|
||||
|
||||
# 导入通用 server 并运行
|
||||
from mcp_dev_agent.server import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -7,88 +7,46 @@ Dev Agent MCP Server
|
||||
- 支持指定 session_id 进行多轮对话
|
||||
- 自动提取最终回复文本
|
||||
|
||||
启动方式(MCP 配置):
|
||||
{
|
||||
"mcpServers": {
|
||||
"dev-agent": {
|
||||
"command": "python",
|
||||
"args": ["d:/nzy/workspace_python/agent/mcp_dev_agent/server.py"],
|
||||
"env": {
|
||||
"DEV_AGENT_API_URL": "http://127.0.0.1:8001",
|
||||
"DEV_AGENT_APP_NAME": "dev_agent",
|
||||
"DEV_AGENT_USER_ID": "codebuddy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
环境变量配置:
|
||||
- DEV_AGENT_API_URL: API Server 地址(默认 http://127.0.0.1:8001)
|
||||
- DEV_AGENT_APP_NAME: 调用的 agent 名称(默认 my_agent)
|
||||
- DEV_AGENT_USER_ID: 用户 ID(默认 codebuddy)
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
# 强制 UTF-8
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
|
||||
import httpx
|
||||
from mcp.server import Server
|
||||
from mcp.types import Tool, TextContent
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
|
||||
# 配置
|
||||
DEV_AGENT_API_URL = os.getenv("DEV_AGENT_API_URL", "http://127.0.0.1:8001")
|
||||
DEV_AGENT_APP_NAME = os.getenv("DEV_AGENT_APP_NAME", "dev_agent")
|
||||
DEV_AGENT_APP_NAME = os.getenv("DEV_AGENT_APP_NAME", "my_agent")
|
||||
DEV_AGENT_USER_ID = os.getenv("DEV_AGENT_USER_ID", "codebuddy")
|
||||
|
||||
# MCP Server
|
||||
server = Server("dev-agent-mcp")
|
||||
mcp = FastMCP(name=f"dev-agent-{DEV_AGENT_APP_NAME}")
|
||||
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools() -> list[Tool]:
|
||||
"""列出可用工具"""
|
||||
return [
|
||||
Tool(
|
||||
name="run_dev_agent",
|
||||
description=(
|
||||
"调用 Dev Agent(全栈开发子 Agent)执行开发任务。\n"
|
||||
"Dev Agent 可以读写文件、运行终端命令、执行编译/构建/测试。\n"
|
||||
"适用于:代码编写、bug 修复、项目搭建、编译验证等开发子任务。\n"
|
||||
"传参说明:task 是任务描述,session_id 可选,不传则创建新会话。"
|
||||
),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task": {
|
||||
"type": "string",
|
||||
"description": "要 Dev Agent 执行的任务描述,越详细越好",
|
||||
},
|
||||
"session_id": {
|
||||
"type": "string",
|
||||
"description": "可选,会话 ID,用于多轮对话/续聊",
|
||||
},
|
||||
},
|
||||
"required": ["task"],
|
||||
},
|
||||
),
|
||||
]
|
||||
@mcp.tool()
|
||||
async def run_dev_agent(task: str, session_id: str = "default") -> str:
|
||||
"""调用 Dev Agent(全栈开发子 Agent)执行开发任务。
|
||||
|
||||
Dev Agent 可以读写文件、运行终端命令、执行编译/构建/测试。
|
||||
适用于:代码编写、bug 修复、项目搭建、编译验证等开发子任务。
|
||||
|
||||
@server.call_tool()
|
||||
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
||||
"""调用工具"""
|
||||
if name == "run_dev_agent":
|
||||
return await _run_dev_agent(arguments)
|
||||
else:
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
|
||||
async def _run_dev_agent(args: dict[str, Any]) -> list[TextContent]:
|
||||
"""调用 Dev Agent API 执行任务"""
|
||||
task = args.get("task", "").strip()
|
||||
session_id = args.get("session_id") or "default"
|
||||
Args:
|
||||
task: 要 Dev Agent 执行的任务描述,越详细越好
|
||||
session_id: 可选,会话 ID,用于多轮对话/续聊,默认 "default"
|
||||
|
||||
Returns:
|
||||
Dev Agent 的执行结果
|
||||
"""
|
||||
if not task:
|
||||
return [TextContent(type="text", text="错误:task 不能为空")]
|
||||
return "错误:task 不能为空"
|
||||
|
||||
# 构造请求
|
||||
payload = {
|
||||
@ -110,24 +68,22 @@ async def _run_dev_agent(args: dict[str, Any]) -> list[TextContent]:
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return [TextContent(
|
||||
type="text",
|
||||
text=f"调用 Dev Agent 失败(HTTP {response.status_code}):\n{response.text[:500]}"
|
||||
)]
|
||||
return (
|
||||
f"调用 Dev Agent 失败(HTTP {response.status_code}):\n"
|
||||
f"{response.text[:500]}"
|
||||
)
|
||||
|
||||
events = response.json()
|
||||
except httpx.ConnectError:
|
||||
return [TextContent(
|
||||
type="text",
|
||||
text=f"无法连接到 Dev Agent API Server({DEV_AGENT_API_URL})\n"
|
||||
f"请确认 api_server.py 是否已启动。"
|
||||
)]
|
||||
return (
|
||||
f"无法连接到 Dev Agent API Server({DEV_AGENT_API_URL})\n"
|
||||
f"请确认 api_server.py 是否已启动。"
|
||||
)
|
||||
except Exception as e:
|
||||
return [TextContent(type="text", text=f"调用 Dev Agent 出错: {e}")]
|
||||
return f"调用 Dev Agent 出错: {e}"
|
||||
|
||||
# 从事件列表中提取最终回复
|
||||
result_text = _extract_final_response(events, session_id)
|
||||
return [TextContent(type="text", text=result_text)]
|
||||
return _extract_final_response(events, session_id)
|
||||
|
||||
|
||||
def _extract_final_response(events: list[dict], session_id: str) -> str:
|
||||
@ -167,17 +123,16 @@ def _extract_final_response(events: list[dict], session_id: str) -> str:
|
||||
return response
|
||||
|
||||
|
||||
async def main():
|
||||
def main():
|
||||
"""stdio 模式启动 MCP server"""
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(read_stream, write_stream, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 确保 Windows 下 stdio 用二进制模式
|
||||
# Windows stdio 二进制模式
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
|
||||
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
||||
|
||||
asyncio.run(main())
|
||||
mcp.run(transport="stdio")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
|
||||
# 确保项目根目录在 path 里
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
@ -19,7 +18,7 @@ from google.genai import types
|
||||
class AgentRunner:
|
||||
def __init__(self):
|
||||
# 延迟导入 agent,避免循环导入
|
||||
from my_agent.agent import root_agent
|
||||
from agents.my_agent.agent import root_agent
|
||||
self.agent = root_agent
|
||||
self.session_service = InMemorySessionService()
|
||||
self._runner: Runner = None
|
||||
@ -58,38 +57,67 @@ class AgentRunner:
|
||||
session_id = f"task_{task_id}"
|
||||
user_id = "task_manager"
|
||||
|
||||
all_text = []
|
||||
tool_calls = []
|
||||
all_text = [] # 收集所有 model 消息中的文本
|
||||
tool_calls = [] # 收集所有 function_call
|
||||
final_text = [] # 最终响应的文本
|
||||
event_count = 0
|
||||
error_msg = None
|
||||
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
new_message=types.Content(
|
||||
role="user",
|
||||
parts=[types.Part(text=prompt)],
|
||||
),
|
||||
):
|
||||
# 收集文本输出
|
||||
if hasattr(event, 'output') and event.output:
|
||||
content = event.output
|
||||
if hasattr(content, 'parts'):
|
||||
for part in content.parts:
|
||||
if hasattr(part, 'text') and part.text:
|
||||
all_text.append(part.text)
|
||||
if hasattr(part, 'function_call') and part.function_call:
|
||||
tool_calls.append({
|
||||
"name": part.function_call.name,
|
||||
"args": dict(part.function_call.args) if hasattr(part.function_call, 'args') else {},
|
||||
})
|
||||
try:
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
new_message=types.Content(
|
||||
role="user",
|
||||
parts=[types.Part(text=prompt)],
|
||||
),
|
||||
):
|
||||
event_count += 1
|
||||
|
||||
result_text = "".join(all_text)
|
||||
# 跳过没有 content 的事件
|
||||
if not event.content:
|
||||
continue
|
||||
|
||||
# 只收集 model 角色的消息(不是 user / function_response 等)
|
||||
content = event.content
|
||||
role = getattr(content, "role", "")
|
||||
if role != "model":
|
||||
continue
|
||||
|
||||
# 遍历 parts 收集文本和 function_call
|
||||
parts = getattr(content, "parts", [])
|
||||
for part in parts:
|
||||
# 文本
|
||||
if hasattr(part, "text") and part.text:
|
||||
all_text.append(part.text)
|
||||
# 最终回复(没有 function_call 的 model 消息)
|
||||
if event.is_final_response():
|
||||
final_text.append(part.text)
|
||||
|
||||
# function_call
|
||||
if hasattr(part, "function_call") and part.function_call:
|
||||
fc = part.function_call
|
||||
tool_calls.append({
|
||||
"name": fc.name,
|
||||
"args": dict(fc.args) if hasattr(fc, "args") else {},
|
||||
})
|
||||
|
||||
result_text = "\n".join(final_text) if final_text else "\n".join(all_text)
|
||||
status = "success" if result_text.strip() else "empty"
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"{type(e).__name__}: {e}"
|
||||
result_text = f"任务执行出错:{error_msg}"
|
||||
status = "error"
|
||||
|
||||
return {
|
||||
"summary": self._extract_summary(result_text),
|
||||
"full_response": result_text,
|
||||
"tool_calls_count": len(tool_calls),
|
||||
"tool_calls_sample": tool_calls[:10], # 只保留前 10 个
|
||||
"status": "success" if result_text else "empty",
|
||||
"tool_calls_sample": tool_calls[:10],
|
||||
"event_count": event_count,
|
||||
"status": status,
|
||||
"error": error_msg,
|
||||
}
|
||||
|
||||
def _build_prompt(self, description: str, project_path: str, requirements: str) -> str:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user