Files
hermes-mac13 93758f3720 feat: 从 tutti-selfhost 拆出 local_rig 独立仓
- local_rig/ 完整包(7 源文件 + ui.html + CHANGELOG)
- tests/test_local_rig.py + test_rig_real_agent.py(纯 local_rig 测试)
- scripts/15-criterion4-live.sh(判据④活环境门)
- README.md(定位/快速开始/与 tutti-selfhost 的关系)
- requirements.txt(唯一外部依赖 pyyaml)
- .gitignore

来源: figmar/tutti-selfhost @ 3b23853 (2026-10-06)
原 PR #43 已合入 tutti-selfhost main
2026-10-06 13:36:14 +08:00

201 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""rig.yaml 解析 + 校验(借鉴 OpenRig pods/members/edges schema)。
schema:
version: "0.1"
name: my-rig
pods:
- id: dev
label: Development
members:
- id: lead
runtime: hermes-cli | terminal | hermes-acp
target: hermes # hermes-* 运行时用
command: "python3 qa.py" # terminal 运行时用
edges:
- kind: delegates_to | can_observe | collaborates_with
from: lead
to: qa
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import yaml
VALID_RUNTIMES = {"terminal", "hermes-cli", "hermes-acp"}
VALID_EDGE_KINDS = {"delegates_to", "can_observe", "collaborates_with"}
_ID_RE = re.compile(r"^[a-z][a-z0-9_-]{0,31}$")
class RigSchemaError(ValueError):
"""rig.yaml 格式/语义错误。"""
@dataclass
class Member:
id: str
runtime: str
target: str | None = None # hermes-* 运行时
command: str | None = None # terminal 运行时
profile: str = "default"
cwd: str = "."
# 【dsh@mac13 补】真机实测:hermes oneshot 不带 `-m` 时服务端直接报
# `HTTP 400: The supported API model names are deepseek-flash, deepseek-v4-pro, but you passed .`
# ⇒ 模型必须显式传。默认值取本机 provider 支持的名字,可在 rig.yaml 覆盖。
model: str = "deepseek-flash"
def __post_init__(self):
if self.runtime not in VALID_RUNTIMES:
raise RigSchemaError(f"member '{self.id}': runtime '{self.runtime}' 不合法,可选 {sorted(VALID_RUNTIMES)}")
if self.runtime == "terminal" and not self.command:
raise RigSchemaError(f"member '{self.id}': terminal 运行时必须指定 command")
if self.runtime in ("hermes-cli", "hermes-acp") and not self.target:
raise RigSchemaError(f"member '{self.id}': {self.runtime} 运行时必须指定 target")
@dataclass
class Edge:
kind: str
from_id: str
to_id: str
def __post_init__(self):
if self.kind not in VALID_EDGE_KINDS:
raise RigSchemaError(f"edge kind '{self.kind}' 不合法,可选 {sorted(VALID_EDGE_KINDS)}")
@dataclass
class Pod:
id: str
label: str = ""
members: list[Member] = field(default_factory=list)
edges: list[Edge] = field(default_factory=list)
@dataclass
class Rig:
name: str
pods: list[Pod] = field(default_factory=list)
version: str = "0.1"
def all_members(self) -> dict[str, Member]:
"""扁平化所有 member,key = 'pod_id.member_id'。"""
out: dict[str, Member] = {}
for pod in self.pods:
for m in pod.members:
out[f"{pod.id}.{m.id}"] = m
return out
def resolve_edge(self, e: Edge) -> tuple[str, str]:
"""把 from/to 解析为 'pod.member' 全名。"""
return self._resolve(e.from_id), self._resolve(e.to_id)
def _resolve(self, ref: str) -> str:
if "." in ref:
pod_id, _, member_id = ref.partition(".")
for pod in self.pods:
if pod.id == pod_id:
for m in pod.members:
if m.id == member_id:
return f"{pod_id}.{member_id}"
raise RigSchemaError(f"edge 引用 '{ref}' 找不到对应 member")
# 无 pod 前缀:全局查找
for pod in self.pods:
for m in pod.members:
if m.id == ref:
return f"{pod.id}.{m.id}"
raise RigSchemaError(f"edge 引用 '{ref}' 找不到对应 member")
def _check_id(id_val: str, ctx: str):
if not _ID_RE.match(id_val or ""):
raise RigSchemaError(f"{ctx} id '{id_val}' 不合法(须小写字母开头,≤32 字符)")
def parse_rig(path: str | Path) -> Rig:
"""解析 rig.yaml 并校验。"""
p = Path(path)
if not p.exists():
raise RigSchemaError(f"rig 文件不存在: {p}")
data = yaml.safe_load(p.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise RigSchemaError("rig.yaml 顶层必须是 mapping")
name = data.get("name")
if not name or not isinstance(name, str):
raise RigSchemaError("rig.yaml 缺少 name(字符串)")
pods_raw = data.get("pods") or []
if not isinstance(pods_raw, list) or not pods_raw:
raise RigSchemaError("rig.yaml 缺少 pods(非空列表)")
pods: list[Pod] = []
pod_ids: set[str] = set()
for i, pr in enumerate(pods_raw):
if not isinstance(pr, dict):
raise RigSchemaError(f"pods[{i}] 必须是 mapping")
pid = pr.get("id")
_check_id(pid, f"pods[{i}]")
if pid in pod_ids:
raise RigSchemaError(f"重复 pod id: {pid}")
pod_ids.add(pid)
members: list[Member] = []
member_ids: set[str] = set()
for j, mr in enumerate(pr.get("members") or []):
if not isinstance(mr, dict):
raise RigSchemaError(f"pod '{pid}' members[{j}] 必须是 mapping")
mid = mr.get("id")
_check_id(mid, f"pod '{pid}' members[{j}]")
if mid in member_ids:
raise RigSchemaError(f"pod '{pid}' 重复 member id: {mid}")
member_ids.add(mid)
members.append(Member(
id=mid,
runtime=mr.get("runtime", "terminal"),
target=mr.get("target"),
command=mr.get("command"),
profile=mr.get("profile", "default"),
cwd=mr.get("cwd", "."),
model=mr.get("model", "deepseek-flash"), # 【dsh@mac13 补】可在 rig.yaml 覆盖
))
edges: list[Edge] = []
for k, er in enumerate(pr.get("edges") or []):
if not isinstance(er, dict):
raise RigSchemaError(f"pod '{pid}' edges[{k}] 必须是 mapping")
edges.append(Edge(
kind=er.get("kind", "delegates_to"),
from_id=er.get("from", ""),
to_id=er.get("to", ""),
))
pods.append(Pod(id=pid, label=pr.get("label", ""), members=members, edges=edges))
rig = Rig(name=name, pods=pods, version=str(data.get("version", "0.1")))
# 全局校验:edge 引用必须存在
all_ids = set(rig.all_members().keys())
for pod in pods:
for e in pod.edges:
try:
rig.resolve_edge(e)
except RigSchemaError:
raise
# 确保 from/to 在已知 member 里
for ref in (e.from_id, e.to_id):
resolved = rig._resolve(ref)
if resolved not in all_ids:
raise RigSchemaError(f"pod '{pod.id}' edge 引用 '{ref}' → '{resolved}' 不是已知 member")
return rig
def load_rig(path: str | Path) -> Rig:
"""parse_rig 的别名(语义更清晰)。"""
return parse_rig(path)