| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- """
- agentpaas launch — 一键启动 agent
- 流程:
- 1. 启动 PaaS 服务 (后台)
- 2. 创建租户 + API Key (如果需要)
- 3. 注册 agent (从 YAML 配置)
- 4. 开始对话 (支持语音 + 文字)
- 用法:
- agentpaas launch agent-config.yml
- agentpaas launch agent-config.yml --daemon
- agentpaas launch agent-config.yml --port 9000
- """
- from __future__ import annotations
- import json
- import os
- import secrets
- import signal
- import subprocess
- import sys
- import time
- import urllib.request
- import urllib.error
- from pathlib import Path
- def _wait_for_server(url, timeout=30):
- """等待 PaaS 服务就绪。"""
- for _ in range(timeout):
- try:
- req = urllib.request.Request("{}/health".format(url))
- with urllib.request.urlopen(req, timeout=2):
- return True
- except Exception:
- time.sleep(1)
- return False
- def _api_call(base_url, api_key, method, path, data=None):
- """调用 PaaS API。"""
- url = "{}{}".format(base_url.rstrip("/"), path)
- headers = {"Content-Type": "application/json"}
- if api_key:
- headers["Authorization"] = "Bearer {}".format(api_key)
- body = json.dumps(data, ensure_ascii=False).encode() if data else None
- req = urllib.request.Request(url, data=body, headers=headers, method=method)
- try:
- with urllib.request.urlopen(req, timeout=30) as resp:
- return json.loads(resp.read().decode())
- except urllib.error.HTTPError as e:
- err_body = e.read().decode()
- return {"error": err_body}
- except urllib.error.URLError as e:
- return {"error": str(e)}
- def _start_server(port, dev=False):
- """后台启动 PaaS 服务,返回进程。"""
- print("🚀 启动 AgentPaaS 服务 (port {})...".format(port))
- proc = subprocess.Popen(
- [sys.executable, "-m", "agentpaas", "serve", "--port", str(port)]
- + (["--dev"] if dev else []),
- stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- )
- print(" PID: {}".format(proc.pid))
- if not _wait_for_server("http://127.0.0.1:{}".format(port)):
- print(" ✗ 服务启动超时")
- proc.kill()
- return None
- print(" ✓ 服务就绪")
- return proc
- def _ensure_tenant(port):
- """确保有租户和 API Key,返回 api_key。"""
- from agentpaas.__main__ import _load_config, _save_config
- cfg = _load_config()
- if cfg.get("api_key"):
- # 测试现有 key 是否有效
- result = _api_call(
- "http://127.0.0.1:{}".format(port), cfg["api_key"],
- "GET", "/api/v1/agents")
- if "error" not in result:
- return cfg["api_key"]
- # 创建新租户
- print("\n📋 创建租户...")
- from agentpaas.db.session import get_db
- from agentpaas.db.models import gen_id, now_utc
- from agentpaas.api.middleware.auth import hash_key
- db = get_db()
- tid = gen_id("tn_")
- uid = gen_id("usr_")
- key = "ap_{}".format(secrets.token_hex(16))
- now = now_utc()
- db.execute(
- "INSERT INTO tenants (id,name,plan,status,created_at) VALUES (?,?,?,'active',?)",
- (tid, "launch", "free", now))
- db.execute(
- "INSERT INTO users (id,tenant_id,email,role,created_at) VALUES (?,?,?,'admin',?)",
- (uid, tid, "", now))
- db.execute(
- "INSERT INTO api_keys (id,tenant_id,user_id,key_hash,key_prefix,name,scopes,rate_limit,status,created_at) "
- "VALUES (?,?,?,?,?,'admin',?,600,'active',?)",
- (gen_id("key_"), tid, uid, hash_key(key), key[:8],
- json.dumps(["agents:*", "keys:*", "billing:read", "admin:*"]), now))
- db.commit()
- cfg["server"] = "http://127.0.0.1:{}".format(port)
- cfg["api_key"] = key
- _save_config(cfg)
- print(" ✓ 租户已创建, API Key: {}...".format(key[:12]))
- return key
- def _register_agent(port, api_key, config_path):
- """注册 agent 到 PaaS,返回 agent_id。"""
- import yaml
- config_path = Path(config_path).resolve()
- if not config_path.exists():
- print("✗ 配置文件不存在: {}".format(config_path))
- return None
- with open(config_path, "r", encoding="utf-8") as f:
- config = yaml.safe_load(f)
- name = config.get("name", config_path.stem)
- desc = config.get("description", "")
- tags = config.get("tags", [])
- if isinstance(tags, str):
- tags = [t.strip() for t in tags.split(",")]
- print("\n📝 注册 agent: {}".format(name))
- result = _api_call(
- "http://127.0.0.1:{}".format(port), api_key,
- "POST", "/api/v1/agents", {
- "name": name,
- "description": desc,
- "config": config,
- "tags": tags,
- })
- if "agent_id" in result:
- agent_id = result["agent_id"]
- version = result["version"]
- print(" ✓ Agent ID: {} (v{})".format(agent_id, version))
- return agent_id
- else:
- print(" ✗ 注册失败: {}".format(result.get("error", "")))
- return None
- def _chat_loop(agent_id, port, api_key, claude_code=True):
- """交互式对话循环(文字模式)。"""
- # 尝试加载 agent67 的 PersonalAssistant (Claude Code 模式)
- assistant = None
- if claude_code:
- try:
- project_root = Path(__file__).resolve().parent.parent
- agent_dir = project_root / "agentexample"
- if str(agent_dir) not in sys.path:
- sys.path.insert(0, str(agent_dir))
- if str(project_root) not in sys.path:
- sys.path.insert(0, str(project_root))
- # 从 PaaS 获取配置
- result = _api_call(
- "http://127.0.0.1:{}".format(port), api_key,
- "GET", "/api/v1/agents/{}".format(agent_id))
- config = result.get("config", {})
- system_prompt = config.get("systemPrompt", "")
- from agent67.core.claude_lam import ClaudeLam
- from agent67.core.assistant import PersonalAssistant
- from lambdagent import Context
- assistant = PersonalAssistant.__new__(PersonalAssistant)
- assistant.brain = ClaudeLam(
- "launch_agent", prompt=system_prompt,
- model="sonnet", max_tokens=config.get("model", {}).get("maxTokens", 4096))
- assistant.ctx = Context()
- assistant.conversation_history = []
- assistant.max_history = config.get("memory", {}).get("size", 20)
- except Exception as e:
- print(" ⚠️ Claude Code 模式不可用 ({}), 使用 PaaS API 模式".format(e))
- assistant = None
- mode = "Claude Code CLI" if assistant else "PaaS API"
- print("\n" + "═" * 55)
- print(" 对话已就绪! Agent: {}".format(agent_id))
- print(" 后端: {}".format(mode))
- print(" 输入 exit 退出 | runs 查看历史")
- print("═" * 55 + "\n")
- while True:
- try:
- user_input = input("You: ").strip()
- except (EOFError, KeyboardInterrupt):
- print("\n👋 再见!")
- break
- if not user_input:
- continue
- if user_input.lower() in ("exit", "quit", "bye"):
- print("👋 再见!")
- break
- if user_input.lower() == "runs":
- result = _api_call(
- "http://127.0.0.1:{}".format(port), api_key,
- "GET", "/api/v1/agents/{}/runs?limit=10".format(agent_id))
- runs = result.get("runs", [])
- if runs:
- for r in runs:
- print(" {} | {} | {}ms | {}".format(
- r["id"], r.get("status"), r.get("duration_ms", 0),
- (r.get("created_at") or "")[:19]))
- else:
- print(" (暂无记录)")
- continue
- if user_input.lower() == "trace" and assistant:
- assistant.print_trace()
- continue
- print()
- t0 = time.time()
- if assistant:
- response = assistant.chat(user_input)
- duration_ms = int((time.time() - t0) * 1000)
- steps = len(assistant.ctx.trace)
- # 回写到 PaaS
- _api_call(
- "http://127.0.0.1:{}".format(port), api_key,
- "POST", "/api/v1/agents/{}/runs/record".format(agent_id), {
- "input": user_input[:500], "output": response[:2000],
- "status": "completed", "duration_ms": duration_ms,
- "steps": steps, "source": "launch-claude-code",
- })
- else:
- result = _api_call(
- "http://127.0.0.1:{}".format(port), api_key,
- "POST", "/api/v1/agents/{}/run".format(agent_id),
- {"input": user_input})
- response = result.get("output", result.get("error", "(无回复)"))
- duration_ms = int((time.time() - t0) * 1000)
- print("🐑 lambda: {}".format(response))
- print(" [{:.1f}s]\n".format(duration_ms / 1000))
- # ── 公开入口 ────────────────────────────────────────────────
- def cmd_launch(config_path, port=8000):
- """一键启动: 服务 + 注册 + 对话。退出时自动关闭服务。"""
- proc = _start_server(port)
- if not proc:
- return
- try:
- api_key = _ensure_tenant(port)
- agent_id = _register_agent(port, api_key, config_path)
- if not agent_id:
- return
- _chat_loop(agent_id, port, api_key)
- except KeyboardInterrupt:
- print("\n\n🛑 中断")
- finally:
- print("\n关闭 PaaS 服务...")
- proc.terminate()
- try:
- proc.wait(timeout=5)
- except subprocess.TimeoutExpired:
- proc.kill()
- print(" ✓ 已关闭")
- LAUNCH_PID_FILE = Path.home() / ".agentpaas" / "launch_daemon.pid"
- LAUNCH_INFO_FILE = Path.home() / ".agentpaas" / "launch_daemon.json"
- def cmd_launch_daemon(config_path, port=8000):
- """后台启动: 服务 + 注册 agent,然后返回。不进入对话。
- 之后可以用 wechat / 其他终端 chat 连接。"""
- proc = _start_server(port)
- if not proc:
- return
- api_key = _ensure_tenant(port)
- agent_id = _register_agent(port, api_key, config_path)
- if not agent_id:
- proc.kill()
- return
- # 保存 daemon 信息
- LAUNCH_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
- LAUNCH_PID_FILE.write_text(str(proc.pid))
- LAUNCH_INFO_FILE.write_text(json.dumps({
- "pid": proc.pid,
- "port": port,
- "agent_id": agent_id,
- "api_key": api_key,
- "config": str(Path(config_path).resolve()),
- "started_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
- }, indent=2))
- print("\n✅ 后台运行中")
- print(" PID: {}".format(proc.pid))
- print(" Port: {}".format(port))
- print(" Agent: {}".format(agent_id))
- print(" API: http://127.0.0.1:{}".format(port))
- print()
- print("连接方式:")
- print(" agentpaas run {} 'Hello' # 单次调用".format(agent_id))
- print(" agentpaas wechat start {} # 微信桥接".format(agent_id))
- print()
- print("停止: kill {}".format(proc.pid))
|