agent-前后端助手花花
This commit is contained in:
parent
bda9cb7782
commit
107e3be767
66
a2a_client.py
Normal file
66
a2a_client.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
"""
|
||||||
|
A2A 客户端测试脚本
|
||||||
|
通过 A2A 协议调用本地的 Dev Agent 服务。
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
python a2a_client.py "你好,介绍一下你自己"
|
||||||
|
python a2a_client.py --context my_session "你好" # 指定 context_id(会话)
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from a2a.client import create_client
|
||||||
|
from a2a.types.a2a_pb2 import SendMessageRequest, Message, Part, Role
|
||||||
|
|
||||||
|
|
||||||
|
A2A_URL = "http://127.0.0.1:8001"
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
parser = argparse.ArgumentParser(description="A2A 客户端测试")
|
||||||
|
parser.add_argument("message", help="发送给 agent 的消息")
|
||||||
|
parser.add_argument("--context", "-c", default="", help="context_id,用于会话续传")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(f"连接到: {A2A_URL}")
|
||||||
|
if args.context:
|
||||||
|
print(f"会话: {args.context}")
|
||||||
|
print(f"发送: {args.message}")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
client = await create_client(A2A_URL)
|
||||||
|
try:
|
||||||
|
# 构建请求
|
||||||
|
request = SendMessageRequest(
|
||||||
|
message=Message(
|
||||||
|
role=Role.ROLE_USER,
|
||||||
|
parts=[Part(text=args.message)],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if args.context:
|
||||||
|
request.message.context_id = args.context
|
||||||
|
|
||||||
|
# 发送消息(流式返回)
|
||||||
|
full_text = ""
|
||||||
|
context_id = ""
|
||||||
|
async for response in client.send_message(request):
|
||||||
|
# 流式响应里可能有 message、status_update 等
|
||||||
|
if response.HasField("message"):
|
||||||
|
msg = response.message
|
||||||
|
if msg.context_id:
|
||||||
|
context_id = msg.context_id
|
||||||
|
for part in msg.parts:
|
||||||
|
if part.text:
|
||||||
|
full_text += part.text
|
||||||
|
|
||||||
|
print("Agent 回复:")
|
||||||
|
print(full_text)
|
||||||
|
if context_id:
|
||||||
|
print(f"\n(context_id: {context_id})")
|
||||||
|
finally:
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@ -26,24 +26,62 @@ os.environ["PYTHONUTF8"] = "1"
|
|||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from google.adk.a2a.utils.agent_to_a2a import to_a2a
|
from google.adk.a2a.utils.agent_to_a2a import to_a2a
|
||||||
from my_agent.agent import root_agent
|
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
|
||||||
|
|
||||||
|
|
||||||
# 配置
|
# 配置
|
||||||
HOST = os.getenv("A2A_SERVER_HOST", "0.0.0.0")
|
HOST = os.getenv("A2A_SERVER_HOST", "0.0.0.0") # 监听地址
|
||||||
PORT = int(os.getenv("A2A_SERVER_PORT", "8001"))
|
PORT = int(os.getenv("A2A_SERVER_PORT", "8001"))
|
||||||
|
# Agent Card 中对外公布的地址(客户端用这个来连接,不能用 0.0.0.0)
|
||||||
|
PUBLIC_HOST = os.getenv("A2A_PUBLIC_HOST", "127.0.0.1")
|
||||||
|
|
||||||
|
# 数据目录
|
||||||
|
DATA_DIR = os.path.join(PROJECT_ROOT, "data")
|
||||||
|
os.makedirs(DATA_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# --- Session 服务:SQLite 持久化(重启不丢失对话历史)---
|
||||||
|
session_service = SqliteSessionService(
|
||||||
|
db_path=os.path.join(DATA_DIR, "sessions.db")
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Memory 服务:长期记忆(先用内存版,后续可换 Chroma 等向量库)---
|
||||||
|
memory_service = InMemoryMemoryService()
|
||||||
|
|
||||||
|
# --- Artifact 服务:工件存储(大文件等)---
|
||||||
|
artifact_service = InMemoryArtifactService()
|
||||||
|
|
||||||
|
# --- 构建 Runner:会话 + 记忆 + 压缩 一体化 ---
|
||||||
|
runner = Runner(
|
||||||
|
app=dev_app,
|
||||||
|
session_service=session_service,
|
||||||
|
memory_service=memory_service,
|
||||||
|
artifact_service=artifact_service,
|
||||||
|
auto_create_session=True,
|
||||||
|
)
|
||||||
|
|
||||||
# 用 ADK 官方工具把 agent 转成 A2A 服务
|
# 用 ADK 官方工具把 agent 转成 A2A 服务
|
||||||
# 会自动生成 agent card,暴露 /a2a/{agent_name} 端点
|
# 传入自定义 runner,启用 SQLite 持久化 + 上下文压缩 + 记忆
|
||||||
a2a_app = to_a2a(root_agent, port=PORT)
|
a2a_app = to_a2a(
|
||||||
|
agent=dev_app.root_agent,
|
||||||
|
host=PUBLIC_HOST, # agent card 里用的对外地址
|
||||||
|
port=PORT,
|
||||||
|
runner=runner,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("Dev Agent A2A Server 启动中...")
|
print("Dev Agent A2A Server 启动中...")
|
||||||
print(f" 监听地址: http://{HOST}:{PORT}")
|
print(f" 监听地址: http://{HOST}:{PORT}")
|
||||||
print(f" A2A 端点: http://{HOST}:{PORT}/a2a/{root_agent.name}")
|
print(f" 对外地址: http://{PUBLIC_HOST}:{PORT}")
|
||||||
print(f" Agent 卡片: http://{HOST}:{PORT}/.well-known/agent-card.json")
|
print(f" A2A 端点: http://{PUBLIC_HOST}:{PORT}/")
|
||||||
|
print(f" Agent 卡片: http://{PUBLIC_HOST}:{PORT}/.well-known/agent-card.json")
|
||||||
|
print(f" 会话持久化: SQLite ({DATA_DIR}/sessions.db)")
|
||||||
|
print(f" 上下文压缩: 每 20 轮自动摘要")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
uvicorn.run(a2a_app, host=HOST, port=PORT, log_level="info")
|
uvicorn.run(a2a_app, host=HOST, port=PORT, log_level="info")
|
||||||
|
|||||||
120
api_server.py
Normal file
120
api_server.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
# 确保项目根目录在 path 里
|
||||||
|
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
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"))
|
||||||
|
|
||||||
|
# 强制 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 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, "my_agent"),
|
||||||
|
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()
|
||||||
169
chat.py
Normal file
169
chat.py
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
# 确保项目根目录在 path 里
|
||||||
|
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
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"))
|
||||||
|
|
||||||
|
# 强制 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 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()
|
||||||
59
mcp_dev_agent/README.md
Normal file
59
mcp_dev_agent/README.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Dev Agent MCP Server
|
||||||
|
|
||||||
|
将 Dev Agent 封装为 MCP 工具,供 CodeBuddy / Cursor / Windsurf 等 MCP 客户端调用。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- **run_dev_agent** — 调用 Dev Agent 执行开发子任务
|
||||||
|
- 支持文件读写、终端命令执行、编译验证
|
||||||
|
- 支持指定 `session_id` 进行多轮对话
|
||||||
|
|
||||||
|
## 配置方法
|
||||||
|
|
||||||
|
### 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
| 变量 | 默认值 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `DEV_AGENT_API_URL` | `http://127.0.0.1:8001` | Dev Agent API Server 地址 |
|
||||||
|
| `DEV_AGENT_APP_NAME` | `dev_agent` | Agent 名称 |
|
||||||
|
| `DEV_AGENT_USER_ID` | `codebuddy` | 用户 ID(用于会话隔离) |
|
||||||
|
|
||||||
|
## 使用前提
|
||||||
|
|
||||||
|
1. 先启动 Dev Agent API Server:
|
||||||
|
```bash
|
||||||
|
python api_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 配置 MCP server(见上方配置方法)
|
||||||
|
|
||||||
|
3. 重启 CodeBuddy / IDE
|
||||||
|
|
||||||
|
## 工具参数
|
||||||
|
|
||||||
|
### run_dev_agent
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `task` | ✅ | 任务描述,越详细越好 |
|
||||||
|
| `session_id` | ❌ | 会话 ID,不传则用 `default`。用于多轮对话续聊 |
|
||||||
183
mcp_dev_agent/server.py
Normal file
183
mcp_dev_agent/server.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
"""
|
||||||
|
Dev Agent MCP Server
|
||||||
|
将 Dev Agent 封装为 MCP 工具,供 CodeBuddy 等 MCP 客户端调用。
|
||||||
|
|
||||||
|
功能:
|
||||||
|
- run_dev_agent: 提交任务给 Dev Agent 执行,返回执行结果
|
||||||
|
- 支持指定 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from mcp.server import Server
|
||||||
|
from mcp.types import Tool, TextContent
|
||||||
|
from mcp.server.stdio import stdio_server
|
||||||
|
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
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_USER_ID = os.getenv("DEV_AGENT_USER_ID", "codebuddy")
|
||||||
|
|
||||||
|
# MCP Server
|
||||||
|
server = Server("dev-agent-mcp")
|
||||||
|
|
||||||
|
|
||||||
|
@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"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@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"
|
||||||
|
|
||||||
|
if not task:
|
||||||
|
return [TextContent(type="text", text="错误:task 不能为空")]
|
||||||
|
|
||||||
|
# 构造请求
|
||||||
|
payload = {
|
||||||
|
"appName": DEV_AGENT_APP_NAME,
|
||||||
|
"userId": DEV_AGENT_USER_ID,
|
||||||
|
"sessionId": session_id,
|
||||||
|
"newMessage": {
|
||||||
|
"role": "user",
|
||||||
|
"parts": [{"text": task}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=600.0) as client: # 10 分钟超时
|
||||||
|
response = await client.post(
|
||||||
|
f"{DEV_AGENT_API_URL}/run",
|
||||||
|
json=payload,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
return [TextContent(
|
||||||
|
type="text",
|
||||||
|
text=f"调用 Dev Agent 失败(HTTP {response.status_code}):\n{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 是否已启动。"
|
||||||
|
)]
|
||||||
|
except Exception as e:
|
||||||
|
return [TextContent(type="text", text=f"调用 Dev Agent 出错: {e}")]
|
||||||
|
|
||||||
|
# 从事件列表中提取最终回复
|
||||||
|
result_text = _extract_final_response(events, session_id)
|
||||||
|
return [TextContent(type="text", text=result_text)]
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_final_response(events: list[dict], session_id: str) -> str:
|
||||||
|
"""从事件列表中提取 agent 的最终文本回复"""
|
||||||
|
if not events:
|
||||||
|
return "(无返回事件)"
|
||||||
|
|
||||||
|
final_text_parts = []
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
content = event.get("content", {})
|
||||||
|
role = content.get("role", "")
|
||||||
|
parts = content.get("parts", [])
|
||||||
|
author = event.get("author", "")
|
||||||
|
|
||||||
|
if role == "model" and author == DEV_AGENT_APP_NAME:
|
||||||
|
for part in parts:
|
||||||
|
if "text" in part:
|
||||||
|
final_text_parts.append(part["text"])
|
||||||
|
|
||||||
|
response = "\n".join(final_text_parts).strip()
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
# 如果没有找到最终回复,返回事件摘要
|
||||||
|
summary = f"共 {len(events)} 个事件\n"
|
||||||
|
for e in events[-5:]:
|
||||||
|
content = e.get("content", {})
|
||||||
|
parts = content.get("parts", [])
|
||||||
|
role = content.get("role", "")
|
||||||
|
author = e.get("author", "")
|
||||||
|
part_types = [list(p.keys())[0] for p in parts]
|
||||||
|
summary += f" - [{role}] {author}: {part_types}\n"
|
||||||
|
response = f"(未提取到最终文本回复)\n{summary}"
|
||||||
|
|
||||||
|
# 附上 session_id 方便续聊
|
||||||
|
response += f"\n\n---\nsession_id: {session_id}"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async 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 用二进制模式
|
||||||
|
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())
|
||||||
@ -3,6 +3,7 @@ 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_toolset import McpToolset
|
||||||
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
|
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
|
||||||
from google.adk.tools.function_tool import FunctionTool
|
from google.adk.tools.function_tool import FunctionTool
|
||||||
|
from google.adk.tools import preload_memory
|
||||||
from mcp.client.stdio import StdioServerParameters
|
from mcp.client.stdio import StdioServerParameters
|
||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
@ -30,13 +31,47 @@ filesystem_mcp = McpToolset(
|
|||||||
os.path.abspath(WORKSPACE_DIR),
|
os.path.abspath(WORKSPACE_DIR),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
timeout=30.0,
|
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 通信问题)---
|
# --- 终端命令执行工具(Python 原生,绕开 MCP 通信问题)---
|
||||||
async def run_command(command: str, cwd: str = None, timeout: int = 300) -> str:
|
async def run_command(command: str, cwd: str = None, timeout: int = 300000) -> str:
|
||||||
"""
|
"""
|
||||||
在终端中执行一条命令,返回输出结果。
|
在终端中执行一条命令,返回输出结果。
|
||||||
|
|
||||||
@ -94,6 +129,20 @@ async def run_command(command: str, cwd: str = None, timeout: int = 300) -> str:
|
|||||||
# 注册为 ADK 工具
|
# 注册为 ADK 工具
|
||||||
run_command_tool = FunctionTool(run_command)
|
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(
|
root_agent = LlmAgent(
|
||||||
model=LiteLlm(
|
model=LiteLlm(
|
||||||
model=model_name,
|
model=model_name,
|
||||||
@ -112,6 +161,11 @@ root_agent = LlmAgent(
|
|||||||
"生活里接地气、热心肠;不矫情,能扛项目压力,也会温柔安抚焦虑的开发同事\n"
|
"生活里接地气、热心肠;不矫情,能扛项目压力,也会温柔安抚焦虑的开发同事\n"
|
||||||
"由主控调度执行具体的开发任务。\n"
|
"由主控调度执行具体的开发任务。\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
"## 记忆能力\n"
|
||||||
|
"- 你拥有长期记忆,之前和用户的对话中提到的项目信息、技术偏好、任务历史都会被记住\n"
|
||||||
|
"- 系统会自动从记忆中检索与当前任务相关的历史上下文,注入到对话中\n"
|
||||||
|
"- 重要的项目信息(技术栈、目录结构、编码规范等)会自动沉淀到记忆里\n"
|
||||||
|
"\n"
|
||||||
"## 工作流程\n"
|
"## 工作流程\n"
|
||||||
"1. 先理解任务需求和项目上下文\n"
|
"1. 先理解任务需求和项目上下文\n"
|
||||||
"2. 使用文件系统工具浏览项目结构、读取相关文件\n"
|
"2. 使用文件系统工具浏览项目结构、读取相关文件\n"
|
||||||
@ -123,6 +177,7 @@ root_agent = LlmAgent(
|
|||||||
"- 所有文件操作限定在分配的工作目录范围内\n"
|
"- 所有文件操作限定在分配的工作目录范围内\n"
|
||||||
"- 你拥有的工具:文件系统操作(读/写/列目录)、终端命令执行\n"
|
"- 你拥有的工具:文件系统操作(读/写/列目录)、终端命令执行\n"
|
||||||
"- 你可以自主完成:代码编写、bug 修复、样式调整、接口修改、简单重构\n"
|
"- 你可以自主完成:代码编写、bug 修复、样式调整、接口修改、简单重构\n"
|
||||||
|
"- 遇到不熟悉的技术或 API,先查阅项目内的现有代码和文档参考\n"
|
||||||
"- 需要上报的情况:\n"
|
"- 需要上报的情况:\n"
|
||||||
" • 架构设计或重大技术选型决策\n"
|
" • 架构设计或重大技术选型决策\n"
|
||||||
" • 依赖包版本不兼容导致的编译/运行时错误(需要升级/降级依赖时)\n"
|
" • 依赖包版本不兼容导致的编译/运行时错误(需要升级/降级依赖时)\n"
|
||||||
@ -146,5 +201,6 @@ root_agent = LlmAgent(
|
|||||||
"**验证结果**:编译/测试是否通过,如有警告需列出\n"
|
"**验证结果**:编译/测试是否通过,如有警告需列出\n"
|
||||||
"**需要主控关注**:如有需要上报的问题,详细说明"
|
"**需要主控关注**:如有需要上报的问题,详细说明"
|
||||||
),
|
),
|
||||||
tools=[filesystem_mcp, run_command_tool],
|
tools=[filesystem_mcp, run_command_tool, preload_memory],
|
||||||
|
after_agent_callback=auto_save_memory_callback,
|
||||||
)
|
)
|
||||||
|
|||||||
23
my_agent/app.py
Normal file
23
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 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="dev_agent",
|
||||||
|
root_agent=root_agent,
|
||||||
|
events_compaction_config=compaction_config,
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue
Block a user