- 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
201 lines
6.8 KiB
Python
201 lines
6.8 KiB
Python
"""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)
|