WU-A 迁移面 spike:便宜档生成核心(多轮 ReAct + 五工具沙箱 + done 门)用 Python 在新建 cheap-worker/ 重写成 AgentScope agent,import 复用 tier2 框架层(model client/四道熔断/历史压缩/observability), check/build/九门 shell-out 现有 node/cjs(不重写 esbuild/CDP/形状门)。 - U1 地基:跨包 sys.path import tier2 worker + 代理旁路 + key 从内网文档注入 env + M3 装配 - U2 五工具:read/list/write Python 实现 + check/build shell-out(形状门三态与 Node 一致) - U3 LittleJS system prompt:prompt.mjs Python 化(两条 footgun 正反例 + 输入契约) - U4 scaffold/stage/play 薄壳 + ensure_play_spec(据 state 形态产 play-spec 治链路假绿) - U5 ReAct 主编排:内置 Agent+ReActConfig+外层有界 resume + done 门 + 收口 stage/smoke/九门 - U6 对照 Node:产物形态 + 九门逐门 + 过门率对比 验收:同 brief Python 新路 vs Node 旧路,产物都真 src/ 6 文件、九门都 9/9 全 PASS、过门率 1.0=1.0 → spikePass=True。端到端单局 attempt 1 一次收敛、148s。落点 cheap-worker/ 对 tier2/amodel-gen 零改动(结构性零回归)。测试 38/38 全绿。 origin: docs/plans/2026-06-26-001-feat-cheap-worker-python-spike-plan.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
86 lines
3.9 KiB
Python
86 lines
3.9 KiB
Python
"""
|
||
_bootstrap.py — cheap-worker 地基:跨包 import tier2 worker 框架层 + 代理旁路 + key 注入 + M3 model 装配。
|
||
|
||
对照源:tier2 worker/client.py(代理旁路 + key)、worker/config.py(build_model_openai)、
|
||
worker/agent_loop/studio.py:31-58(sys.path 兜底范式)。
|
||
|
||
两条内网阶段约束(AGENTS.md §9)落地:
|
||
· key 进项目文档 docs/内网凭据与端点.md、不进 env var;client.get_api_key() 只读 env NEWAPI_KEY,
|
||
故本模块在装 model 前先把文档里的 key 解析注入 os.environ(对 Node m3.mjs getKey「优先 env 否则解析文档」同款单一事实源)。
|
||
· 代理旁路时序:install_proxy_bypass 必须在 import anthropic/openai 之前——已由 config.py 内部(import 时先
|
||
resolve_base_url 装旁路再 import agentscope/openai)保证;本模块不在 import config 前 import openai,故安全。
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# ── ① 跨包 import 兜底:把 tier2/gen-worker/ 加进 sys.path(worker / observability 两个顶层包据此可 import)──
|
||
# 本文件在 cheap-worker/ 下 → parents[1] = repo 根 → /tier2/gen-worker。
|
||
# 目录名 gen-worker 含连字符不能直接 import;照 studio.py 范式把 gen-worker/ 本身加进 sys.path,再 import 其下合法包名 worker。
|
||
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
||
_GEN_WORKER = _REPO_ROOT / "tier2" / "gen-worker"
|
||
if str(_GEN_WORKER) not in sys.path:
|
||
sys.path.insert(0, str(_GEN_WORKER))
|
||
|
||
# 内网凭据单一事实源(内网阶段允许入仓)。
|
||
_CRED_DOC = _REPO_ROOT / "docs" / "内网凭据与端点.md"
|
||
|
||
# spike 的便宜档模型固定 MiniMax-M3(KTD6:与 Node amodel-gen 同模型,保证对照公平;reframe 长期可换更便宜模型)。
|
||
SPIKE_MODEL = "MiniMax-M3"
|
||
|
||
|
||
def ensure_api_key_env() -> str:
|
||
"""确保 os.environ['NEWAPI_KEY'] 可用:已设则用;否则从 docs/内网凭据与端点.md 解析注入(内网允许入仓)。
|
||
|
||
Returns:
|
||
解析到的 key(sk- 开头)。
|
||
|
||
Raises:
|
||
RuntimeError: env 未设且文档无可解析的 key。
|
||
"""
|
||
key = os.environ.get("NEWAPI_KEY")
|
||
if key:
|
||
return key
|
||
try:
|
||
doc = _CRED_DOC.read_text(encoding="utf-8")
|
||
m = re.search(r"NEWAPI_KEY[^\n]*?(sk-[A-Za-z0-9]+)", doc)
|
||
if m:
|
||
os.environ["NEWAPI_KEY"] = m.group(1)
|
||
return m.group(1)
|
||
except OSError:
|
||
pass
|
||
raise RuntimeError(
|
||
f"找不到 NEWAPI_KEY(请设 env,或确认 {_CRED_DOC} 含 sk- 开头的 key;"
|
||
"内网阶段 key 进文档不进 env,见 AGENTS.md §9)"
|
||
)
|
||
|
||
|
||
def load_framework() -> dict:
|
||
"""import 复用 tier2 worker 框架层,返回常用件句柄字典(config / client / middleware)。
|
||
|
||
在 import config(会触发 client 装代理旁路 + import agentscope/openai)前先 ensure_api_key_env,
|
||
保证后续 config.build_model_openai 内部 client.get_api_key() 拿得到 key。
|
||
"""
|
||
ensure_api_key_env()
|
||
# 跨包 import;sys.path 已在模块级兜底。代理旁路由 config import 时内部装好。
|
||
from worker import config, client, middleware # noqa: E402
|
||
return {"config": config, "client": client, "middleware": middleware}
|
||
|
||
|
||
def build_cheap_model(max_tokens: int = 16000):
|
||
"""装一个便宜档 spike 用的 M3 模型(OpenAI 协议、base_url 自动补 /v1、record=True 记 token)。
|
||
|
||
复用 tier2 config.build_model_openai:它内部自动补 /v1、装代理旁路、取 key,与 Node m3.mjs 走同一
|
||
OpenAI 协议 /v1/chat/completions。
|
||
|
||
Args:
|
||
max_tokens: 单轮最大输出 token(默认 16000,对 m3.mjs)。
|
||
|
||
Returns:
|
||
RecordingOpenAIChatModel 实例(OpenAIChatModel 子类,带 .records 成本台账)。
|
||
"""
|
||
fw = load_framework()
|
||
return fw["config"].build_model_openai(SPIKE_MODEL, max_tokens=max_tokens, record=True)
|