agent1.3版本 新增agent创建以后自动注册功能 新增agent创建脚本
This commit is contained in:
parent
b8311451c8
commit
81acd16783
@ -45,8 +45,12 @@ from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactServ
|
||||
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 google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
||||
from agents.luna.app import dev_app
|
||||
|
||||
# A2A 网关接入(注册 + 心跳),放最底部 import 以免循环依赖
|
||||
import gateway_client
|
||||
|
||||
|
||||
# 配置
|
||||
HOST = os.getenv("API_SERVER_HOST", "0.0.0.0")
|
||||
@ -82,6 +86,9 @@ def create_api_server() -> ApiServer:
|
||||
# 认证服务(暂不需要,内存版占位)
|
||||
credential_service = InMemoryCredentialService()
|
||||
|
||||
# 记忆服务(内存版占位:满足 ApiServer 必填参数,不含记忆工具/回调,不会注入记忆)
|
||||
memory_service = InMemoryMemoryService()
|
||||
|
||||
# 评测集管理(暂不需要,占位)
|
||||
eval_sets_manager = InMemoryEvalSetsManager()
|
||||
eval_set_results_manager = LocalEvalSetResultsManager(agents_dir=DATA_DIR)
|
||||
@ -89,6 +96,7 @@ def create_api_server() -> ApiServer:
|
||||
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,
|
||||
@ -102,6 +110,13 @@ def main():
|
||||
api_server = create_api_server()
|
||||
fastapi_app = api_server.get_fast_api_app()
|
||||
|
||||
# 向 A2A 网关注册并启动心跳(注册失败不阻塞服务启动)
|
||||
gateway_ok = gateway_client.register_agent(dev_app.name, f"http://127.0.0.1:{PORT}")
|
||||
if gateway_ok:
|
||||
gateway_client.start_heartbeat(dev_app.name)
|
||||
else:
|
||||
print("[gateway] 注册失败,跳过心跳(网关可能未启动或 auth 不对)")
|
||||
|
||||
print("=" * 60)
|
||||
print("Luna Agent API Server 启动中...")
|
||||
print(f" 模型: {dev_app.root_agent.model.model}")
|
||||
|
||||
21
agents/my_agent/agent_restart.err.log
Normal file
21
agents/my_agent/agent_restart.err.log
Normal file
@ -0,0 +1,21 @@
|
||||
C:\Users\nzy\AppData\Local\Programs\Python\Python314\Lib\site-packages\google\adk\features\_feature_decorator.py:72: UserWarning: [EXPERIMENTAL] feature FeatureName.PLUGGABLE_AUTH is enabled.
|
||||
check_feature_enabled()
|
||||
D:\nzy\workspace_python\agent\agents\my_agent\app.py:11: UserWarning: [EXPERIMENTAL] EventsCompactionConfig: This feature is experimental and may change or be removed in future versions without notice. It may introduce breaking changes at any time.
|
||||
compaction_config = EventsCompactionConfig(
|
||||
D:\nzy\workspace_python\agent\agents\my_agent\api_server.py:86: UserWarning: [EXPERIMENTAL] InMemoryCredentialService: This feature is experimental and may change or be removed in future versions without notice. It may introduce breaking changes at any time.
|
||||
credential_service = InMemoryCredentialService()
|
||||
C:\Users\nzy\AppData\Local\Programs\Python\Python314\Lib\site-packages\google\adk\auth\credential_service\in_memory_credential_service.py:33: UserWarning: [EXPERIMENTAL] BaseCredentialService: This feature is experimental and may change or be removed in future versions without notice. It may introduce breaking changes at any time.
|
||||
super().__init__()
|
||||
Traceback (most recent call last):
|
||||
File "D:\nzy\workspace_python\agent\agents\my_agent\api_server.py", line 134, in <module>
|
||||
main()
|
||||
~~~~^^
|
||||
File "D:\nzy\workspace_python\agent\agents\my_agent\api_server.py", line 105, in main
|
||||
api_server = create_api_server()
|
||||
File "D:\nzy\workspace_python\agent\agents\my_agent\api_server.py", line 92, in create_api_server
|
||||
return ApiServer(
|
||||
agent_loader=DevAgentLoader(),
|
||||
...<6 lines>...
|
||||
auto_create_session=True,
|
||||
)
|
||||
TypeError: ApiServer.__init__() missing 1 required keyword-only argument: 'memory_service'
|
||||
0
agents/my_agent/agent_restart.log
Normal file
0
agents/my_agent/agent_restart.log
Normal file
@ -45,8 +45,12 @@ from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactServ
|
||||
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 google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
||||
from agents.my_agent.app import dev_app
|
||||
|
||||
# A2A 网关接入(注册 + 心跳),放最底部 import 以免循环依赖
|
||||
import gateway_client
|
||||
|
||||
|
||||
# 配置
|
||||
HOST = os.getenv("API_SERVER_HOST", "0.0.0.0")
|
||||
@ -82,6 +86,9 @@ def create_api_server() -> ApiServer:
|
||||
# 认证服务(暂不需要,内存版占位)
|
||||
credential_service = InMemoryCredentialService()
|
||||
|
||||
# 记忆服务(内存版占位:满足 ApiServer 必填参数,不含记忆工具/回调,不会注入记忆)
|
||||
memory_service = InMemoryMemoryService()
|
||||
|
||||
# 评测集管理(暂不需要,占位)
|
||||
eval_sets_manager = InMemoryEvalSetsManager()
|
||||
eval_set_results_manager = LocalEvalSetResultsManager(agents_dir=DATA_DIR)
|
||||
@ -89,6 +96,7 @@ def create_api_server() -> ApiServer:
|
||||
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,
|
||||
@ -102,6 +110,17 @@ def main():
|
||||
api_server = create_api_server()
|
||||
fastapi_app = api_server.get_fast_api_app()
|
||||
|
||||
# 挂载 A2A 网关任务接收端点(POST /tasks/{request_id})
|
||||
from agents.my_agent.task_receiver import router as task_router
|
||||
fastapi_app.include_router(task_router)
|
||||
|
||||
# 向 A2A 网关注册并启动心跳(注册失败不阻塞服务启动)
|
||||
gateway_ok = gateway_client.register_agent(dev_app.name, f"http://127.0.0.1:{PORT}")
|
||||
if gateway_ok:
|
||||
gateway_client.start_heartbeat(dev_app.name)
|
||||
else:
|
||||
print("[gateway] 注册失败,跳过心跳(网关可能未启动或 auth 不对)")
|
||||
|
||||
print("=" * 60)
|
||||
print("Dev Agent API Server 启动中...")
|
||||
print(f" 监听地址: http://{HOST}:{PORT}")
|
||||
|
||||
120
agents/my_agent/task_receiver.py
Normal file
120
agents/my_agent/task_receiver.py
Normal file
@ -0,0 +1,120 @@
|
||||
"""A2A 网关任务接收端点:接收网关主动推送的任务,后台执行 agent,完成后回传结果。
|
||||
|
||||
契约(网关 relay.py dispatch_command 推送):
|
||||
POST {endpoint}/tasks/{request_id}
|
||||
body: {"auth": GATEWAY_AUTH, "request_id": str, "payload": {...}}
|
||||
成功响应 202(立即确认),执行完成后由后台线程回传网关 /api/agent/result。
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
import gateway_client
|
||||
from agents.my_agent.app import dev_app
|
||||
from google.adk.runners import InMemoryRunner
|
||||
from google.genai import types as genai_types
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(tags=["tasks"])
|
||||
|
||||
# 与 api_server.py 一致的导入路径(独立运行时兜底)
|
||||
import os
|
||||
import sys
|
||||
|
||||
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
if PROJECT_ROOT not in sys.path:
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
|
||||
def _payload_to_prompt(payload: dict) -> str:
|
||||
"""将网关任务 payload 转换为 agent 的用户指令。"""
|
||||
if not payload:
|
||||
return "请执行任务并汇报结果。"
|
||||
if "prompt" in payload and payload["prompt"]:
|
||||
return str(payload["prompt"])
|
||||
if "cmd" in payload and payload["cmd"]:
|
||||
return f"请执行以下命令并汇报执行结果:\n{payload['cmd']}"
|
||||
# 兜底:序列化整个 payload
|
||||
return "请根据以下任务载荷执行并汇报结果:\n" + str(payload)
|
||||
|
||||
|
||||
async def _run_agent_once(prompt: str, request_id: str) -> tuple[str, str]:
|
||||
"""用 ADK InMemoryRunner 运行一次 agent,返回 (接受任务后的首条回复, 最终总结)。
|
||||
|
||||
dev_app 是 App 容器(根 agent 非裸 LlmAgent),run_async 不会自动创建
|
||||
session,需先用 runner.session_service 显式创建。
|
||||
"""
|
||||
runner = InMemoryRunner(app=dev_app)
|
||||
session_id = f"task-{request_id}"
|
||||
await runner.session_service.create_session(
|
||||
app_name=runner.app_name,
|
||||
user_id="gateway",
|
||||
session_id=session_id,
|
||||
)
|
||||
texts: list[str] = []
|
||||
final_text = ""
|
||||
async for event in runner.run_async(
|
||||
user_id="gateway",
|
||||
session_id=session_id,
|
||||
new_message=genai_types.Content(role="user", parts=[genai_types.Part(text=prompt)]),
|
||||
):
|
||||
if event.is_final_response():
|
||||
if event.content and event.content.parts:
|
||||
for part in event.content.parts:
|
||||
text = getattr(part, "text", None)
|
||||
if text:
|
||||
final_text += text
|
||||
break
|
||||
if event.content and event.content.parts:
|
||||
for part in event.content.parts:
|
||||
text = getattr(part, "text", None)
|
||||
if text:
|
||||
texts.append(text)
|
||||
# 首条回复 = 接受任务后的第一条回应;最终总结 = final response
|
||||
reply = (texts[0] if texts else final_text).strip()
|
||||
summary = final_text.strip() or "\n".join(t for t in texts if t).strip() or "(无输出)"
|
||||
return reply, summary
|
||||
|
||||
|
||||
def _execute_and_report(request_id: str, payload: dict) -> None:
|
||||
"""后台线程:执行 agent,成功后回传 success,异常回传 failed。"""
|
||||
try:
|
||||
prompt = _payload_to_prompt(payload)
|
||||
reply, summary = asyncio.run(_run_agent_once(prompt, request_id))
|
||||
gateway_client.report_result(
|
||||
request_id,
|
||||
agent_id=dev_app.name,
|
||||
status="success",
|
||||
progress=100,
|
||||
result={"reply": reply, "output": summary},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("agent task failed request=%s", request_id)
|
||||
gateway_client.report_result(
|
||||
request_id,
|
||||
agent_id=dev_app.name,
|
||||
status="failed",
|
||||
progress=100,
|
||||
error_info=str(e),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/tasks/{request_id}")
|
||||
async def receive_task(request_id: str, request: Request):
|
||||
"""接收网关推送的任务,立即 202 确认,后台执行。"""
|
||||
body = await request.json()
|
||||
if body.get("auth") != gateway_client.GATEWAY_AUTH:
|
||||
raise HTTPException(status_code=401, detail="invalid auth")
|
||||
payload = body.get("payload") or {}
|
||||
threading.Thread(
|
||||
target=_execute_and_report,
|
||||
args=(request_id, payload),
|
||||
name=f"task-{request_id[:8]}",
|
||||
daemon=True,
|
||||
).start()
|
||||
logger.info("task received request=%s payload=%s", request_id, payload)
|
||||
return JSONResponse(status_code=202, content={"ok": True, "request_id": request_id, "status": "accepted"})
|
||||
@ -45,8 +45,12 @@ from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactServ
|
||||
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 google.adk.memory.in_memory_memory_service import InMemoryMemoryService
|
||||
from agents.qwen.app import dev_app
|
||||
|
||||
# A2A 网关接入(注册 + 心跳),放最底部 import 以免循环依赖
|
||||
import gateway_client
|
||||
|
||||
|
||||
# 配置
|
||||
HOST = os.getenv("API_SERVER_HOST", "0.0.0.0")
|
||||
@ -82,6 +86,9 @@ def create_api_server() -> ApiServer:
|
||||
# 认证服务(暂不需要,内存版占位)
|
||||
credential_service = InMemoryCredentialService()
|
||||
|
||||
# 记忆服务(内存版占位:满足 ApiServer 必填参数,不含记忆工具/回调,不会注入记忆)
|
||||
memory_service = InMemoryMemoryService()
|
||||
|
||||
# 评测集管理(暂不需要,占位)
|
||||
eval_sets_manager = InMemoryEvalSetsManager()
|
||||
eval_set_results_manager = LocalEvalSetResultsManager(agents_dir=DATA_DIR)
|
||||
@ -89,6 +96,7 @@ def create_api_server() -> ApiServer:
|
||||
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,
|
||||
@ -102,6 +110,13 @@ def main():
|
||||
api_server = create_api_server()
|
||||
fastapi_app = api_server.get_fast_api_app()
|
||||
|
||||
# 向 A2A 网关注册并启动心跳(注册失败不阻塞服务启动)
|
||||
gateway_ok = gateway_client.register_agent(dev_app.name, f"http://127.0.0.1:{PORT}")
|
||||
if gateway_ok:
|
||||
gateway_client.start_heartbeat(dev_app.name)
|
||||
else:
|
||||
print("[gateway] 注册失败,跳过心跳(网关可能未启动或 auth 不对)")
|
||||
|
||||
print("=" * 60)
|
||||
print("Qwen Agent API Server 启动中...")
|
||||
print(f" 模型: {dev_app.root_agent.model.model}")
|
||||
|
||||
196
create_agent.py
Normal file
196
create_agent.py
Normal file
@ -0,0 +1,196 @@
|
||||
"""
|
||||
快速创建新 Agent 脚本
|
||||
从 luna 模板复制出一个新 agent 目录,自动替换所有唯一标识符,并:
|
||||
- 生成 agents/<dir>/ 下的 agent.py / app.py / api_server.py / chat.py / .env / __init__.py
|
||||
- 生成 mcp_dev_agent/<dir>_server.py 入口
|
||||
- 更新 C:/Users/nzy/.codebuddy/.mcp.json 追加 MCP server 条目
|
||||
|
||||
命名约定(沿用 luna/qwen):
|
||||
DIR = 用户输入(如 claude)
|
||||
AGENT = {DIR}_agent (App name / agent name)
|
||||
DB = sessions_{DIR}.db
|
||||
MCP = kebab-case(如 claude-agent)
|
||||
|
||||
用法:
|
||||
python create_agent.py claude --model opcode/claude-sonnet
|
||||
python create_agent.py claude --model opcode/claude-sonnet --port 8005 --display "Claude Dev Agent"
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
# 强制 UTF-8(Windows 控制台默认 GBK,无法打印 ✓/→ 等字符)
|
||||
if sys.platform == "win32":
|
||||
os.environ.setdefault("PYTHONUTF8", "1")
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 仓库根目录
|
||||
REPO_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
AGENTS_DIR = os.path.join(REPO_ROOT, "agents")
|
||||
MCP_DIR = os.path.join(REPO_ROOT, "mcp_dev_agent")
|
||||
TEMPLATE_DIR = os.path.join(AGENTS_DIR, "luna")
|
||||
MCP_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".codebuddy", ".mcp.json")
|
||||
VENV_PYTHON = os.path.join(REPO_ROOT, ".venv", "Scripts", "python.exe")
|
||||
|
||||
# 模板中需要替换的标识符(按顺序执行,先长后短避免误伤)
|
||||
REPLACEMENTS = [
|
||||
("luna_agent", "{AGENT}"), # 覆盖所有 app/agent name 引用
|
||||
("luna", "{DIR}"), # 剩余:模块路径、db 名、instruction 内称呼
|
||||
("Luna", "{TITLE}"), # 首字母大写:Luna Agent / Luna: / === Luna
|
||||
("8002", "{PORT}"), # 默认端口
|
||||
("opcode/deepseek-v4-flash", "{MODEL}"), # .env 的 VLLM_MODEL
|
||||
]
|
||||
|
||||
# 复制时排除的目录/文件
|
||||
SKIP_DIR_NAMES = {"__pycache__", ".adk", ".git", ".idea"}
|
||||
SKIP_FILENAMES = {".gitignore"}
|
||||
|
||||
|
||||
def interact(arg_dir: str, model: str, port: int, display: str) -> tuple[str, str, int, str]:
|
||||
"""补齐缺失参数(缺省时交互式询问)"""
|
||||
d = arg_dir
|
||||
if not d:
|
||||
d = input("Agent 目录名(如 claude): ").strip()
|
||||
if not model:
|
||||
model = input("模型名(如 opcode/claude-sonnet): ").strip()
|
||||
if port is None:
|
||||
port = auto_next_port()
|
||||
if not display:
|
||||
display = f"{d.title()} Agent ({model}) 全栈开发助手"
|
||||
return d, model, port, display
|
||||
|
||||
|
||||
def auto_next_port() -> int:
|
||||
"""扫描 agents/*/.env 的 API_SERVER_PORT,取最大值 +1"""
|
||||
max_port = 8000
|
||||
if os.path.isdir(AGENTS_DIR):
|
||||
for entry in os.listdir(AGENTS_DIR):
|
||||
env_path = os.path.join(AGENTS_DIR, entry, ".env")
|
||||
if os.path.isfile(env_path):
|
||||
m = re.search(r"API_SERVER_PORT\s*=\s*(\d+)", open(env_path, encoding="utf-8").read())
|
||||
if m:
|
||||
max_port = max(max_port, int(m.group(1)))
|
||||
return max_port + 1
|
||||
|
||||
|
||||
def copy_tree(src: str, dst: str) -> None:
|
||||
"""递归复制,跳过 __pycache__/.adk/.git 等"""
|
||||
os.makedirs(dst, exist_ok=True)
|
||||
for name in os.listdir(src):
|
||||
s = os.path.join(src, name)
|
||||
d = os.path.join(dst, name)
|
||||
if os.path.isdir(s):
|
||||
if name in SKIP_DIR_NAMES:
|
||||
continue
|
||||
copy_tree(s, d)
|
||||
else:
|
||||
if name in SKIP_FILENAMES:
|
||||
continue
|
||||
shutil.copy2(s, d)
|
||||
|
||||
|
||||
def apply_replacements(path: str, mapping: dict) -> None:
|
||||
"""对文件内容按顺序做字符串替换"""
|
||||
with open(path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
for old, key in REPLACEMENTS:
|
||||
content = content.replace(old, mapping.get(key, ""))
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def update_mcp_config(mcp_key: str, server_file: str, display: str) -> None:
|
||||
"""在 .mcp.json 的 mcpServers 中追加一条"""
|
||||
if not os.path.isfile(MCP_CONFIG_PATH):
|
||||
print(f"[warn] 未找到 {MCP_CONFIG_PATH},跳过 MCP 配置更新")
|
||||
return
|
||||
|
||||
with open(MCP_CONFIG_PATH, encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
|
||||
if mcp_key in cfg.get("mcpServers", {}):
|
||||
print(f"[warn] .mcp.json 已存在 {mcp_key} 条目,跳过")
|
||||
return
|
||||
|
||||
cfg.setdefault("mcpServers", {})[mcp_key] = {
|
||||
"type": "stdio",
|
||||
"command": VENV_PYTHON,
|
||||
"args": [server_file],
|
||||
"description": display,
|
||||
}
|
||||
|
||||
with open(MCP_CONFIG_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f" ✓ 已更新 {MCP_CONFIG_PATH}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="创建一个新的 Dev Agent(从 luna 模板)")
|
||||
parser.add_argument("dir", nargs="?", help="agent 目录名(如 claude)")
|
||||
parser.add_argument("--model", default=None, help="模型名(如 opcode/claude-sonnet)")
|
||||
parser.add_argument("--port", type=int, default=None, help="API 端口,默认自动取最大+1")
|
||||
parser.add_argument("--display", default=None, help="MCP 描述,默认 '{Dir} Agent ({model}) 全栈开发助手'")
|
||||
args = parser.parse_args()
|
||||
|
||||
d, model, port, display = interact(args.dir, args.model, args.port, args.display)
|
||||
|
||||
if not d or not model:
|
||||
print("错误:目录名和模型名不能为空")
|
||||
sys.exit(1)
|
||||
|
||||
agent = f"{d}_agent"
|
||||
title = d.title()
|
||||
db = f"sessions_{d}.db"
|
||||
mcp_key = f"{d.replace('_', '-')}-agent"
|
||||
target_dir = os.path.join(AGENTS_DIR, d)
|
||||
server_file = os.path.join(MCP_DIR, f"{d}_server.py")
|
||||
|
||||
mapping = {"{AGENT}": agent, "{DIR}": d, "{TITLE}": title,
|
||||
"{PORT}": str(port), "{MODEL}": model}
|
||||
|
||||
# 1. 校验目标目录不存在
|
||||
if os.path.exists(target_dir):
|
||||
print(f"错误:目录已存在 {target_dir}")
|
||||
sys.exit(1)
|
||||
if os.path.exists(server_file):
|
||||
print(f"错误:MCP server 已存在 {server_file}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"创建 Agent: {d} (agent={agent}, port={port}, model={model})")
|
||||
print(f" 数据库: {db}")
|
||||
|
||||
# 2. 复制模板目录
|
||||
copy_tree(TEMPLATE_DIR, target_dir)
|
||||
# 3. 替换所有文件里的标识符
|
||||
for root, _dirs, files in os.walk(target_dir):
|
||||
for fn in files:
|
||||
apply_replacements(os.path.join(root, fn), mapping)
|
||||
# 4. 重写 __init__.py
|
||||
with open(os.path.join(target_dir, "__init__.py"), "w", encoding="utf-8") as f:
|
||||
f.write(f"# {d} package\nfrom . import agent\n")
|
||||
print(f" ✓ 已生成 agents/{d}/")
|
||||
|
||||
# 5. 生成 MCP server 入口
|
||||
shutil.copy2(os.path.join(MCP_DIR, "luna_server.py"), server_file)
|
||||
apply_replacements(server_file, mapping)
|
||||
print(f" ✓ 已生成 {server_file}")
|
||||
|
||||
# 6. 更新 .mcp.json
|
||||
update_mcp_config(mcp_key, server_file, display)
|
||||
|
||||
print("\n完成!启动方式:")
|
||||
print(f" cd agents/{d} && python api_server.py # 启动 API Server(自动注册到网关)")
|
||||
print(f" cd agents/{d} && python chat.py # 命令行对话")
|
||||
print(f" python agent_status.py --agent {agent} # 查看状态(需先手动加入 AGENTS 映射)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
140
gateway_client.py
Normal file
140
gateway_client.py
Normal file
@ -0,0 +1,140 @@
|
||||
"""A2A 网关接入客户端:Agent 服务启动后自动注册 + 心跳保活 + 注销。
|
||||
|
||||
用法(在 api_server.py 的 main() 中):
|
||||
from gateway_client import register_agent, start_heartbeat, unregister_agent
|
||||
|
||||
if register_agent(dev_app.name, f"http://127.0.0.1:{PORT}"):
|
||||
start_heartbeat(dev_app.name)
|
||||
|
||||
环境变量:
|
||||
GATEWAY_URL 网关地址,默认 http://127.0.0.1:8000
|
||||
GATEWAY_AUTH 网关认证密码,需与网关 GATEWAY_AUTH 一致
|
||||
AGENT_TAGS 能力标签,逗号分隔,默认 code
|
||||
AGENT_MAX_CONCURRENT 最大并发数,默认 1
|
||||
HEARTBEAT_INTERVAL 心跳间隔秒,默认 10
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
GATEWAY_URL = os.getenv("GATEWAY_URL", "http://127.0.0.1:8000").rstrip("/")
|
||||
GATEWAY_AUTH = os.getenv("GATEWAY_AUTH", "dev-gateway-auth")
|
||||
HEARTBEAT_INTERVAL = int(os.getenv("HEARTBEAT_INTERVAL", "10"))
|
||||
|
||||
_client = httpx.Client(timeout=5.0)
|
||||
|
||||
|
||||
def _parse_tags() -> list[str]:
|
||||
raw = os.getenv("AGENT_TAGS", "code")
|
||||
return [t.strip() for t in raw.split(",") if t.strip()]
|
||||
|
||||
|
||||
def register_agent(agent_id: str, endpoint: str, agent_tags: list[str] | None = None,
|
||||
max_concurrent: int | None = None) -> bool:
|
||||
"""向网关注册本 Agent。成功返回 True,失败(网关未启动/认证失败)返回 False。"""
|
||||
tags = agent_tags if agent_tags is not None else _parse_tags()
|
||||
max_conc = max_concurrent or int(os.getenv("AGENT_MAX_CONCURRENT", "1"))
|
||||
body = {
|
||||
"auth": GATEWAY_AUTH,
|
||||
"agent_id": agent_id,
|
||||
"endpoint": endpoint,
|
||||
"agent_tags": tags,
|
||||
"max_concurrent": max_conc,
|
||||
"current_load": 0,
|
||||
}
|
||||
try:
|
||||
resp = _client.post(f"{GATEWAY_URL}/api/agent/register", json=body)
|
||||
if resp.status_code == 200:
|
||||
logger.info("registered to gateway agent=%s endpoint=%s tags=%s", agent_id, endpoint, tags)
|
||||
return True
|
||||
logger.error("register failed agent=%s status=%s body=%s", agent_id, resp.status_code, resp.text)
|
||||
except httpx.HTTPError as e:
|
||||
logger.error("register network error agent=%s err=%s", agent_id, e)
|
||||
return False
|
||||
|
||||
|
||||
def _heartbeat_once(agent_id: str, current_load: int) -> bool:
|
||||
try:
|
||||
resp = _client.post(
|
||||
f"{GATEWAY_URL}/api/agent/heartbeat",
|
||||
json={"agent_id": agent_id, "current_load": current_load},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
return True
|
||||
logger.warning("heartbeat failed status=%s body=%s", resp.status_code, resp.text)
|
||||
except httpx.HTTPError as e:
|
||||
logger.warning("heartbeat network error err=%s", e)
|
||||
return False
|
||||
|
||||
|
||||
def _heartbeat_loop(agent_id: str) -> None:
|
||||
while True:
|
||||
try:
|
||||
load = get_current_load()
|
||||
except Exception:
|
||||
load = 0
|
||||
_heartbeat_once(agent_id, load)
|
||||
time.sleep(HEARTBEAT_INTERVAL)
|
||||
|
||||
|
||||
def start_heartbeat(agent_id: str) -> threading.Thread:
|
||||
"""启动后台心跳线程(daemon,随进程退出)。"""
|
||||
t = threading.Thread(target=_heartbeat_loop, args=(agent_id,), name=f"gateway-hb-{agent_id}", daemon=True)
|
||||
t.start()
|
||||
logger.info("heartbeat started agent=%s interval=%ss", agent_id, HEARTBEAT_INTERVAL)
|
||||
return t
|
||||
|
||||
|
||||
def unregister_agent(agent_id: str) -> bool:
|
||||
"""向网关注销本 Agent。"""
|
||||
try:
|
||||
resp = _client.post(f"{GATEWAY_URL}/api/agent/unregister", json={"agent_id": agent_id})
|
||||
if resp.status_code in (200, 404):
|
||||
logger.info("unregistered from gateway agent=%s", agent_id)
|
||||
return True
|
||||
logger.warning("unregister failed status=%s body=%s", resp.status_code, resp.text)
|
||||
except httpx.HTTPError as e:
|
||||
logger.error("unregister network error agent=%s err=%s", agent_id, e)
|
||||
return False
|
||||
|
||||
|
||||
def report_result(request_id: str, agent_id: str, status: str = "success",
|
||||
progress: int = 100, result: dict | None = None,
|
||||
error_info: str | None = None) -> bool:
|
||||
"""任务执行完成后,将结果回传给网关(POST /api/agent/result)。
|
||||
|
||||
Args:
|
||||
request_id: 网关分配的任务 ID
|
||||
agent_id: 本 Agent ID
|
||||
status: success / failed
|
||||
progress: 进度百分比
|
||||
result: 结果字典(可选)
|
||||
error_info: 错误信息(失败时必填)
|
||||
"""
|
||||
body = {
|
||||
"request_id": request_id,
|
||||
"agent_id": agent_id,
|
||||
"status": status,
|
||||
"progress": progress,
|
||||
"result": result,
|
||||
"error_info": error_info,
|
||||
}
|
||||
try:
|
||||
resp = _client.post(f"{GATEWAY_URL}/api/agent/result", json=body)
|
||||
if resp.status_code == 200:
|
||||
logger.info("result reported request=%s status=%s", request_id, status)
|
||||
return True
|
||||
logger.error("report result failed request=%s status=%s body=%s", request_id, resp.status_code, resp.text)
|
||||
except httpx.HTTPError as e:
|
||||
logger.error("report result network error request=%s err=%s", request_id, e)
|
||||
return False
|
||||
|
||||
|
||||
def get_current_load() -> int:
|
||||
"""Agent 当前负载。子类/调用方可覆写以报告真实并发数。"""
|
||||
return 0
|
||||
Loading…
Reference in New Issue
Block a user