diff --git a/README.md b/README.md new file mode 100644 index 0000000..f71bb91 --- /dev/null +++ b/README.md @@ -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 `,或直接调用 DELETE 会话 API,或直接删除 `data/sessions.db` 文件。 + +## 许可证 + +MIT diff --git a/a2a_server.py b/a2a_server.py index 3e55cd8..7a0fa92 100644 --- a/a2a_server.py +++ b/a2a_server.py @@ -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 # 配置 diff --git a/agents/__init__.py b/agents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/my_agent/__init__.py b/agents/luna/__init__.py similarity index 100% rename from my_agent/__init__.py rename to agents/luna/__init__.py diff --git a/agents/luna/agent.py b/agents/luna/agent.py new file mode 100644 index 0000000..46d7c9d --- /dev/null +++ b/agents/luna/agent.py @@ -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, +) diff --git a/agents/luna/api_server.py b/agents/luna/api_server.py new file mode 100644 index 0000000..94aeae5 --- /dev/null +++ b/agents/luna/api_server.py @@ -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() diff --git a/my_agent/app.py b/agents/luna/app.py similarity index 91% rename from my_agent/app.py rename to agents/luna/app.py index 61cc41d..f8501f1 100644 --- a/my_agent/app.py +++ b/agents/luna/app.py @@ -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, ) diff --git a/agents/luna/chat.py b/agents/luna/chat.py new file mode 100644 index 0000000..48d5ac6 --- /dev/null +++ b/agents/luna/chat.py @@ -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() diff --git a/agents/my_agent/__init__.py b/agents/my_agent/__init__.py new file mode 100644 index 0000000..0c38a7a --- /dev/null +++ b/agents/my_agent/__init__.py @@ -0,0 +1,2 @@ +# my_agent package +from . import agent \ No newline at end of file diff --git a/my_agent/agent.py b/agents/my_agent/agent.py similarity index 98% rename from my_agent/agent.py rename to agents/my_agent/agent.py index 5e1a92b..4679d78 100644 --- a/my_agent/agent.py +++ b/agents/my_agent/agent.py @@ -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" diff --git a/agents/my_agent/api_server.py b/agents/my_agent/api_server.py new file mode 100644 index 0000000..3e36560 --- /dev/null +++ b/agents/my_agent/api_server.py @@ -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() diff --git a/agents/my_agent/app.py b/agents/my_agent/app.py new file mode 100644 index 0000000..650e56e --- /dev/null +++ b/agents/my_agent/app.py @@ -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, +) diff --git a/agents/my_agent/chat.py b/agents/my_agent/chat.py new file mode 100644 index 0000000..0d1ac32 --- /dev/null +++ b/agents/my_agent/chat.py @@ -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() diff --git a/agents/qwen/.gitignore b/agents/qwen/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/agents/qwen/.gitignore @@ -0,0 +1 @@ +.env diff --git a/agents/qwen/__init__.py b/agents/qwen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/agents/qwen/agent.py b/agents/qwen/agent.py new file mode 100644 index 0000000..75a35fb --- /dev/null +++ b/agents/qwen/agent.py @@ -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, +) diff --git a/agents/qwen/api_server.py b/agents/qwen/api_server.py new file mode 100644 index 0000000..f2764b4 --- /dev/null +++ b/agents/qwen/api_server.py @@ -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() diff --git a/agents/qwen/app.py b/agents/qwen/app.py new file mode 100644 index 0000000..b8149ad --- /dev/null +++ b/agents/qwen/app.py @@ -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, +) diff --git a/agents/qwen/chat.py b/agents/qwen/chat.py new file mode 100644 index 0000000..e775f4e --- /dev/null +++ b/agents/qwen/chat.py @@ -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() diff --git a/data/sessions.db b/data/sessions.db new file mode 100644 index 0000000..1baef60 Binary files /dev/null and b/data/sessions.db differ diff --git a/data/sessions_luna.db b/data/sessions_luna.db new file mode 100644 index 0000000..3af08aa Binary files /dev/null and b/data/sessions_luna.db differ diff --git a/data/sessions_qwen.db b/data/sessions_qwen.db new file mode 100644 index 0000000..439bf80 Binary files /dev/null and b/data/sessions_qwen.db differ diff --git a/data/tasks/238726a8.json b/data/tasks/238726a8.json new file mode 100644 index 0000000..25d42ac --- /dev/null +++ b/data/tasks/238726a8.json @@ -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 用法:\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 禁止使用 方式(运行时 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 样式: