Files
opensquilla--opensquilla/scripts/compare_meta_skill_openclaw_lifestyle.py
2026-07-13 13:12:33 +08:00

889 lines
32 KiB
Python

"""Lifestyle meta-skill benchmark against the OpenClaw t3 baseline.
This catalog is intentionally narrower than ``compare_meta_skill_openclaw``:
it covers retained practical work/life meta-skills and frames each case so the
OpenSquilla meta-skill orchestration path can be judged against OpenClaw's
t3 Opus 4.8 baseline.
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import sys
from dataclasses import asdict
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
if __package__ in {None, ""}:
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from scripts.compare_meta_skill_openclaw import (
ComparisonCase,
EndpointResult,
JudgeResult,
LLMJudge,
OpenClawRunner,
OpenSquillaRunner,
RubricCriterion,
_openclaw_session_file_events,
apply_judge_result,
criterion,
extract_text_from_events,
read_judge_api_key,
read_openclaw_token,
read_opensquilla_token,
score_response,
)
REPORT_DIR = Path(
os.environ.get("OPENSQUILLA_LIFESTYLE_COMPARE_REPORT_DIR", ".reports/meta-skill-comparison")
)
OPENCLAW_T3_MODEL = os.environ.get("OPENCLAW_T3_MODEL", "t3-opus-4.7")
OPENCLAW_BASELINE_LABEL = "OpenClaw + t3 + capability-equivalent normal skills baseline"
MATCHED_OPENCLAW_NORMAL_SKILLS = (
"OpenSquilla multi-search-engine -> OpenClaw multi-search-engine",
"OpenSquilla docx -> OpenClaw word-docx",
"OpenSquilla xlsx -> OpenClaw excel-xlsx",
"OpenSquilla pdf-toolkit -> OpenClaw pdf-toolkit",
"OpenSquilla deep-research -> OpenClaw deep-research-pro",
"OpenSquilla weather -> OpenClaw weather",
"OpenSquilla summarize -> OpenClaw summarize",
"OpenSquilla memory -> OpenClaw longterm-memory/notes if installed",
"OpenSquilla pptx -> OpenClaw pptx/presentation skill if installed",
)
BENCHMARK_LABEL = f"OpenSquilla + Squilla Router vs {OPENCLAW_BASELINE_LABEL}"
LIFESTYLE_JUDGE_SUBSCORE_RANGES: dict[str, tuple[int, int]] = {
"final_artifact_quality": (0, 40),
"task_completion": (0, 20),
"evidence_traceability": (0, 15),
"actionability": (0, 10),
"risk_boundary_safety": (0, 10),
"meta_skill_fit": (0, 5),
}
KID_PROJECT_RUBRIC: tuple[RubricCriterion, ...] = (
criterion(
"age_fit",
"Adapts the plan to child age and guardian involvement.",
r"8 岁",
r"age",
r"年龄",
r"家长",
r"guardian",
),
criterion(
"step_plan",
"Creates a clear day-by-day or session-by-session plan.",
r"Day",
r"第",
r"步骤",
r"step",
r"timeline",
r"时间表",
),
criterion(
"materials_budget",
"Lists materials, budget, and household substitutes.",
r"materials",
r"材料",
r"预算",
r"substitute",
r"替代",
),
criterion(
"safety",
"Flags safety hazards and supervision requirements.",
r"safety",
r"安全",
r"supervision",
r"监督",
r"adult",
r"大人",
),
criterion(
"learning_objectives",
"Explains what the child should learn and present.",
r"learn",
r"学习",
r"原理",
r"presentation",
r"展示",
),
criterion(
"weather_or_constraints",
"Handles outdoor/weather/deadline constraints and assumptions.",
r"weather",
r"天气",
r"deadline",
r"截止",
r"assumption",
r"假设",
),
)
LIFESTYLE_COMPARISON_CASES: list[ComparisonCase] = [
ComparisonCase(
case_id="kid_project_balcony_plants",
skill_name="meta-kid-project-planner",
scenario="lifestyle_primary",
prompt=(
"孩子 8 岁,科学课两周后要交一个小项目。她想做“阳台种豆芽/小植物观察”,"
"家里有透明杯、纸巾、绿豆、尺子和彩笔,预算最好 50 元以内。我们住杭州,"
"阳台有半天太阳,平时我只能晚上陪 20 分钟。请帮我做一个孩子能看懂、家长也能执行的计划:"
"每天做什么、材料清单和替代品、安全注意、怎么记录数据和画图、最后展示怎么讲,"
"如果天气或光照不稳定要怎么调整,哪些地方你只能先假设。"
),
expected_advantage=(
"OpenSquilla + Squilla Router should activate kid-project-planner, combine "
"age fit, materials, weather-aware constraints, safety review, and parent "
"learning objectives, then beat OpenClaw + t3 Opus 4.8 on an executable "
"child-and-guardian project plan."
),
optimization_if_not_better=(
"If OpenSquilla does not beat OpenClaw, strengthen kid-project-planner to "
"always produce kid-facing steps, guardian notes, material substitutes, "
"safety checks, data-recording templates, and assumption labels."
),
rubric=KID_PROJECT_RUBRIC,
failure_modes=(
"Gives a generic plant project answer without adapting to an 8-year-old.",
(
"Misses the 50 RMB budget, nightly 20-minute supervision, "
"or light/weather constraints."
),
"Omits safety, data recording, or presentation guidance.",
),
),
]
ENGLISH_LIFESTYLE_PROMPTS: dict[str, str] = {
"kid_project_balcony_plants": (
"My child is 8 and needs to submit a small science project in two weeks. She wants to do "
"a balcony sprout or small-plant observation project. At home we have clear cups, paper "
"towels, mung beans, a ruler, and colored pens, and I want to keep the budget under "
"RMB 50. We live in Hangzhou, the balcony gets half a day of sun, and I can only "
"help for 20 minutes "
"in the evening. Please make a plan that a child can understand and a parent can actually "
"supervise: what to do each day, materials and substitutes, safety notes, how to "
"record data "
"and draw charts, how to present the final result, how to adjust if weather or light is "
"unstable, and what you have to assume."
),
}
def _placeholder_result(endpoint: str, case: ComparisonCase) -> EndpointResult:
return EndpointResult(
endpoint=endpoint,
case_id=case.case_id,
ok=False,
elapsed_s=0.0,
response_text="",
score=asdict(score_response("", case)),
error="not run",
model=OPENCLAW_T3_MODEL if endpoint == "openclaw" else None,
)
def build_lifestyle_rows(language: str = "zh") -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for case in _cases_for_language(language):
rows.append(
{
"case": _case_to_dict(case),
"benchmark": BENCHMARK_LABEL,
"opensquilla": asdict(_placeholder_result("opensquilla", case)),
"openclaw": asdict(_placeholder_result("openclaw", case)),
"baseline_model": OPENCLAW_T3_MODEL,
"baseline_winner": "tie",
"winner": "tie",
"score_basis": "not_run",
"opensquilla_better": False,
"recommended_optimization": case.optimization_if_not_better,
}
)
return rows
def render_lifestyle_markdown(rows: list[dict[str, Any]]) -> str:
total = len(rows)
sq_wins = sum(1 for row in rows if row["winner"] == "opensquilla")
claw_wins = sum(1 for row in rows if row["winner"] == "openclaw")
ties = sum(1 for row in rows if row["winner"] == "tie")
lines = [
"# OpenSquilla Meta-Skills vs OpenClaw t3 Matched-Skills Lifestyle Benchmark",
"",
f"Benchmark: {BENCHMARK_LABEL}",
f"{OPENCLAW_BASELINE_LABEL} model: `{OPENCLAW_T3_MODEL}`",
"Matched OpenClaw normal skills: "
+ ", ".join(f"`{skill}`" for skill in MATCHED_OPENCLAW_NORMAL_SKILLS),
"",
"## Summary",
"",
(
f"OpenSquilla + Squilla Router wins: {sq_wins}/{total}; "
f"{OPENCLAW_BASELINE_LABEL} wins: {claw_wins}/{total}; "
f"ties/not-run: {ties}."
),
"",
(
"| Case | Meta-skill | OpenSquilla model | OpenClaw model | Deterministic "
"| Judge 0-100 | Final artifact | Basis | Winner | Issue |"
),
"| --- | --- | --- | --- | ---: | ---: | ---: | --- | --- | --- |",
]
for row in rows:
lines.append(
(
"| {case} | `{skill}` | `{sq_model}` | `{claw_model}` | {det} "
"| {judge} | {artifact} | {basis} | {winner} | {issue} |"
).format(
case=row["case"]["case_id"],
skill=row["case"]["skill_name"],
sq_model=row["opensquilla"].get("model") or "",
claw_model=row["openclaw"].get("model") or "",
det=f"{row['opensquilla']['score']['total']}-{row['openclaw']['score']['total']}",
judge=_judge_scores_cell(row),
artifact=_judge_final_artifact_cell(row),
basis=row.get("score_basis", ""),
winner=row["winner"],
issue=_judge_issue_cell(row).replace("|", "/"),
)
)
lines.extend(["", "## Cases", ""])
for row in rows:
case = row["case"]
lines.append(f"### {case['case_id']}")
lines.append("")
lines.append(f"- Meta-skill: `{case['skill_name']}`")
lines.append(f"- Scenario: {case['scenario']}")
lines.append(f"- Expected advantage: {case['expected_advantage']}")
lines.append(f"- Baseline: {OPENCLAW_BASELINE_LABEL} (`{OPENCLAW_T3_MODEL}`)")
lines.append("- Rubric: " + ", ".join(item["name"] for item in case["rubric"]))
lines.append("- Failure modes: " + "; ".join(case["failure_modes"]))
lines.append("")
lines.append("```text")
lines.append(case["prompt"])
lines.append("```")
lines.append("")
return "\n".join(lines)
def render_lifestyle_prompts_markdown(rows: list[dict[str, Any]]) -> str:
lines = [
"# Lifestyle Test Prompts",
"",
]
for row in rows:
case = row["case"]
lines.append(f"## {case['case_id']}")
lines.append("")
lines.append("### 中文")
lines.append("")
lines.append("```text")
original = next(
item
for item in LIFESTYLE_COMPARISON_CASES
if item.case_id == case["case_id"].removesuffix("_en")
)
lines.append(original.prompt)
lines.append("```")
lines.append("")
lines.append("### English")
lines.append("")
lines.append("```text")
lines.append(ENGLISH_LIFESTYLE_PROMPTS[original.case_id])
lines.append("```")
lines.append("")
return "\n".join(lines)
def _judge_scores_cell(row: dict[str, Any]) -> str:
judge = row.get("judge") if isinstance(row.get("judge"), dict) else {}
scores = judge.get("scores") if isinstance(judge.get("scores"), dict) else {}
if not scores:
return ""
return f"{scores.get('opensquilla', '')}-{scores.get('openclaw', '')}"
def _judge_final_artifact_cell(row: dict[str, Any]) -> str:
judge = row.get("judge") if isinstance(row.get("judge"), dict) else {}
raw = judge.get("raw") if isinstance(judge.get("raw"), dict) else {}
subscores = raw.get("subscores") if isinstance(raw.get("subscores"), dict) else {}
opensquilla = (
subscores.get("opensquilla") if isinstance(subscores.get("opensquilla"), dict) else {}
)
openclaw = subscores.get("openclaw") if isinstance(subscores.get("openclaw"), dict) else {}
if not opensquilla and not openclaw:
return ""
return (
f"{opensquilla.get('final_artifact_quality', '')}-"
f"{openclaw.get('final_artifact_quality', '')}"
)
def _judge_issue_cell(row: dict[str, Any]) -> str:
if row.get("invalid_reasons"):
return "; ".join(str(item) for item in row["invalid_reasons"])
if row.get("judge_error"):
return str(row["judge_error"])
judge = row.get("judge") if isinstance(row.get("judge"), dict) else {}
if row.get("score_basis") == "llm_judge":
raw = judge.get("raw") if isinstance(judge.get("raw"), dict) else {}
if not judge.get("scores") or not raw.get("subscores") or not judge.get("rationale"):
return "incomplete_judge_payload"
return ""
def write_lifestyle_reports(
rows: list[dict[str, Any]], stamp: str | None = None
) -> tuple[Path, Path]:
REPORT_DIR.mkdir(parents=True, exist_ok=True)
if stamp is None:
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
jsonl_path = REPORT_DIR / f"openclaw_t3_vs_opensquilla_lifestyle_meta_{stamp}.jsonl"
md_path = REPORT_DIR / f"openclaw_t3_vs_opensquilla_lifestyle_meta_{stamp}.md"
prompts_path = REPORT_DIR / f"openclaw_t3_vs_opensquilla_lifestyle_meta_prompts_{stamp}.md"
with jsonl_path.open("w", encoding="utf-8") as fh:
for row in rows:
fh.write(json.dumps(row, ensure_ascii=False) + "\n")
md_path.write_text(render_lifestyle_markdown(rows), encoding="utf-8")
prompts_path.write_text(render_lifestyle_prompts_markdown(rows), encoding="utf-8")
print(f"wrote {jsonl_path}")
print(f"wrote {md_path}")
print(f"wrote {prompts_path}")
return jsonl_path, md_path
async def run_live(args: argparse.Namespace) -> list[dict[str, Any]]:
selected = _select_cases(args.case, language=args.prompt_language)
if not args.openclaw_config and not args.openclaw_baseline_jsonl:
raise SystemExit("Pass --openclaw-config or set OPENCLAW_CONFIG.")
opensquilla = OpenSquillaRunner(
args.opensquilla_url,
args.opensquilla_token,
elevated=args.opensquilla_elevated,
agent_id=args.opensquilla_agent_id,
isolated_agent_per_case=args.opensquilla_isolated_agents,
run_id=args.opensquilla_run_id,
)
openclaw = None
openclaw_baseline = {}
openclaw_state_dir = Path(args.openclaw_config).parent if args.openclaw_config else None
if args.openclaw_baseline_jsonl:
openclaw_baseline = load_openclaw_baseline(
Path(args.openclaw_baseline_jsonl),
selected,
state_dir=openclaw_state_dir,
)
else:
openclaw = OpenClawRunner(
args.openclaw_url,
read_openclaw_token(Path(args.openclaw_config)),
args.openclaw_idle_timeout,
state_dir=openclaw_state_dir,
)
judge = None
if args.judge_llm:
if not args.judge_model:
raise SystemExit("Pass --judge-model or set OPENSQUILLA_JUDGE_MODEL.")
judge = LLMJudge(
model=args.judge_model,
api_key=args.judge_api_key,
base_url=args.judge_base_url,
timeout_s=args.judge_timeout,
)
rows: list[dict[str, Any]] = []
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
for case in selected:
print(f"running {case.case_id} ...", flush=True)
if openclaw_baseline:
sq_result = await opensquilla.run(case, args.timeout)
claw_result = openclaw_baseline[case.case_id]
else:
assert openclaw is not None
sq_result, claw_result = await asyncio.gather(
opensquilla.run(case, args.timeout),
openclaw.run(case, args.timeout),
)
if not claw_result.model:
claw_result.model = OPENCLAW_T3_MODEL
row = _compare_results(case, sq_result, claw_result)
if judge is not None and row.get("score_basis") != "invalid_endpoint":
try:
judge_result = await _judge_lifestyle_with_retries(
judge,
case,
sq_result,
claw_result,
)
row = _apply_lifestyle_judge_result(row, judge_result, case)
except Exception as exc:
row["judge_error"] = f"{type(exc).__name__}: {exc}"
rows.append(row)
judge_suffix = ""
if row.get("judge"):
judge_suffix = (
f" judge={_judge_scores_cell(row) or 'n/a'}"
f" final_artifact={_judge_final_artifact_cell(row) or 'n/a'}"
)
elif row.get("judge_error"):
judge_suffix = f" judge_error={row['judge_error']}"
print(
f"{case.case_id}: opensquilla={sq_result.score['total']} "
f"openclaw_t3={claw_result.score['total']}{judge_suffix} "
f"opensquilla_model={sq_result.model or ''} "
f"openclaw_model={claw_result.model or OPENCLAW_T3_MODEL} "
f"winner={row['winner']}",
flush=True,
)
write_lifestyle_reports(rows, stamp=stamp)
write_lifestyle_reports(rows, stamp=stamp)
return rows
async def judge_existing(args: argparse.Namespace) -> list[dict[str, Any]]:
if not args.judge_jsonl:
raise SystemExit("Pass --judge-jsonl.")
if not args.judge_model:
raise SystemExit("Pass --judge-model or set OPENSQUILLA_JUDGE_MODEL.")
judge = LLMJudge(
model=args.judge_model,
api_key=args.judge_api_key,
base_url=args.judge_base_url,
timeout_s=args.judge_timeout,
)
rows = [
json.loads(line)
for line in Path(args.judge_jsonl).read_text(encoding="utf-8").splitlines()
if line.strip()
]
judged_rows: list[dict[str, Any]] = []
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
for row in rows:
case = _case_from_dict(row["case"])
opensquilla = _endpoint_from_dict(row["opensquilla"])
openclaw = _endpoint_from_dict(row["openclaw"])
row.setdefault("baseline_winner", row.get("winner", "tie"))
row.setdefault("score_basis", "deterministic")
try:
judge_result = await _judge_lifestyle_with_retries(
judge,
case,
opensquilla,
openclaw,
)
judged = _apply_lifestyle_judge_result(row, judge_result, case)
except Exception as exc:
judged = dict(row)
judged["judge_error"] = f"{type(exc).__name__}: {exc}"
judged_rows.append(judged)
print(
f"judged {case.case_id}: winner={judged.get('winner')} "
f"judge={_judge_scores_cell(judged) or 'n/a'}",
flush=True,
)
write_lifestyle_reports(judged_rows, stamp=stamp)
write_lifestyle_reports(judged_rows, stamp=stamp)
return judged_rows
def load_openclaw_baseline(
path: Path,
cases: list[ComparisonCase],
*,
state_dir: Path | None = None,
) -> dict[str, EndpointResult]:
rows = [
json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()
]
by_case = {str(row["case"]["case_id"]): row for row in rows}
baseline: dict[str, EndpointResult] = {}
for case in cases:
row = by_case.get(case.case_id)
if row is None:
raise SystemExit(f"OpenClaw baseline missing case {case.case_id!r} in {path}")
baseline_prompt = str(row.get("case", {}).get("prompt", ""))
if baseline_prompt != case.prompt:
raise SystemExit(
f"OpenClaw baseline prompt mismatch for {case.case_id!r}; "
"use the exact prompt that produced the locked baseline"
)
result = _endpoint_from_dict(row["openclaw"])
refreshed = _refreshed_openclaw_text_from_state(
result.session_key,
case.prompt,
state_dir,
)
if refreshed and len(refreshed) > len(result.response_text.strip()):
result.response_text = refreshed
result.ok = True
result.error = None
result.score = asdict(score_response(refreshed, case))
if not result.model:
result.model = OPENCLAW_T3_MODEL
baseline[case.case_id] = result
return baseline
def _endpoint_from_dict(data: dict[str, Any]) -> EndpointResult:
return EndpointResult(
endpoint=str(data.get("endpoint", "openclaw")),
case_id=str(data["case_id"]),
ok=bool(data.get("ok")),
elapsed_s=float(data.get("elapsed_s", 0.0)),
response_text=str(data.get("response_text", "")),
score=data.get("score") if isinstance(data.get("score"), dict) else {},
error=str(data["error"]) if data.get("error") else None,
session_key=str(data["session_key"]) if data.get("session_key") else None,
model=str(data["model"]) if data.get("model") else None,
provider=str(data["provider"]) if data.get("provider") else None,
event_count=int(data.get("event_count", 0)),
)
def _refreshed_openclaw_text_from_state(
session_key: str | None,
prompt: str,
state_dir: Path | None,
) -> str:
path = _openclaw_session_file_for_key(state_dir, session_key)
if path is None:
return ""
return extract_text_from_events(
_openclaw_session_file_events(path, session_key or "", after_prompt=prompt)
)
def _openclaw_session_file_for_key(
state_dir: Path | None,
session_key: str | None,
) -> Path | None:
if state_dir is None or not session_key:
return None
sessions_dir = state_dir / "agents" / "main" / "sessions"
if not sessions_dir.exists():
return None
for trajectory_path in sessions_dir.glob("*.trajectory.jsonl"):
try:
text = trajectory_path.read_text(encoding="utf-8")
except OSError:
continue
if session_key not in text:
continue
session_file = trajectory_path.with_name(
trajectory_path.name.replace(".trajectory.jsonl", ".jsonl")
)
if session_file.exists():
return session_file
return None
def _compare_results(
case: ComparisonCase,
opensquilla: EndpointResult,
openclaw: EndpointResult,
) -> dict[str, Any]:
invalid_reasons = _invalid_endpoint_reasons(opensquilla, openclaw)
if invalid_reasons:
return {
"case": _case_to_dict(case),
"benchmark": BENCHMARK_LABEL,
"opensquilla": asdict(opensquilla),
"openclaw": asdict(openclaw),
"baseline_model": openclaw.model or OPENCLAW_T3_MODEL,
"baseline_winner": "invalid",
"winner": "invalid",
"score_basis": "invalid_endpoint",
"opensquilla_better": False,
"invalid_reasons": invalid_reasons,
"recommended_optimization": None,
}
sq_total = int(opensquilla.score["total"])
claw_total = int(openclaw.score["total"])
if sq_total > claw_total:
winner = "opensquilla"
elif claw_total > sq_total:
winner = "openclaw"
else:
winner = "tie"
return {
"case": _case_to_dict(case),
"benchmark": BENCHMARK_LABEL,
"opensquilla": asdict(opensquilla),
"openclaw": asdict(openclaw),
"baseline_model": openclaw.model or OPENCLAW_T3_MODEL,
"baseline_winner": winner,
"winner": winner,
"score_basis": "deterministic",
"opensquilla_better": winner == "opensquilla",
"recommended_optimization": None
if winner == "opensquilla"
else case.optimization_if_not_better,
}
def _invalid_endpoint_reasons(*results: EndpointResult) -> list[str]:
reasons: list[str] = []
for result in results:
if not result.ok:
reasons.append(f"{result.endpoint}: not ok")
if not result.response_text.strip():
reasons.append(f"{result.endpoint}: empty response")
if _looks_like_unrelated_bootstrap(result.response_text):
reasons.append(f"{result.endpoint}: unrelated bootstrap response")
if result.error:
reasons.append(f"{result.endpoint}: {result.error}")
return reasons
def _looks_like_unrelated_bootstrap(text: str) -> bool:
lowered = text.lower()
bootstrap_phrases = (
"bootstrap removed",
"ready for the task",
"what would you like me to do",
"who am i",
"what should they call you",
)
return len(text.strip()) < 500 and any(phrase in lowered for phrase in bootstrap_phrases)
def _apply_lifestyle_judge_result(
row: dict[str, Any],
judge_result: JudgeResult,
case: ComparisonCase,
) -> dict[str, Any]:
normalized = _normalized_lifestyle_judge_result(judge_result)
if normalized is None:
raise RuntimeError("judge response missing required scores, subscores, or rationale")
updated = apply_judge_result(row, normalized, case)
updated["benchmark"] = BENCHMARK_LABEL
updated["baseline_model"] = row.get("baseline_model") or OPENCLAW_T3_MODEL
return updated
async def _judge_lifestyle_with_retries(
judge: LLMJudge,
case: ComparisonCase,
opensquilla: EndpointResult,
openclaw: EndpointResult,
*,
attempts: int = 3,
) -> JudgeResult:
errors: list[str] = []
for attempt in range(1, attempts + 1):
try:
result = await judge.judge(case, opensquilla, openclaw)
except Exception as exc:
errors.append(f"attempt {attempt}: {type(exc).__name__}: {exc}")
continue
normalized = _normalized_lifestyle_judge_result(result)
if normalized is not None:
return normalized
errors.append(f"attempt {attempt}: incomplete weighted judge payload")
raise RuntimeError("; ".join(errors))
def _lifestyle_judge_result_is_complete(judge_result: JudgeResult) -> bool:
return _normalized_lifestyle_judge_result(judge_result) is not None
def _normalized_lifestyle_judge_result(judge_result: JudgeResult) -> JudgeResult | None:
if not judge_result.rationale.strip():
return None
raw = judge_result.raw if isinstance(judge_result.raw, dict) else {}
totals = _lifestyle_weighted_totals(raw)
if totals is None:
return None
winner = "tie"
if totals["opensquilla"] > totals["openclaw"]:
winner = "opensquilla"
elif totals["openclaw"] > totals["opensquilla"]:
winner = "openclaw"
normalized_raw = dict(raw)
normalized_raw["scores"] = totals
normalized_raw["winner"] = winner
normalized_raw["score_source"] = "weighted_subscores"
return JudgeResult(
winner=winner,
scores=totals,
confidence=judge_result.confidence,
rationale=judge_result.rationale,
risks=judge_result.risks,
raw=normalized_raw,
model=judge_result.model,
)
def _lifestyle_weighted_totals(raw: dict[str, Any]) -> dict[str, int] | None:
subscores = raw.get("subscores") if isinstance(raw.get("subscores"), dict) else {}
totals: dict[str, int] = {}
for label in ("opensquilla", "openclaw"):
candidate = subscores.get(label)
if not isinstance(candidate, dict):
return None
total = 0
for name, (low, high) in LIFESTYLE_JUDGE_SUBSCORE_RANGES.items():
if name not in candidate:
return None
try:
value = int(candidate[name])
except (TypeError, ValueError):
return None
if value < low or value > high:
return None
total += value
totals[label] = total
return totals
def _case_to_dict(case: ComparisonCase) -> dict[str, Any]:
data = asdict(case)
data["rubric"] = [asdict(item) for item in case.rubric]
return data
def _case_from_dict(data: dict[str, Any]) -> ComparisonCase:
rubric = tuple(
RubricCriterion(
name=str(item["name"]),
description=str(item["description"]),
patterns=tuple(str(pattern) for pattern in item["patterns"]),
weight=int(item.get("weight", 1)),
)
for item in data.get("rubric", ())
)
return ComparisonCase(
case_id=str(data["case_id"]),
skill_name=str(data["skill_name"]),
prompt=str(data["prompt"]),
expected_advantage=str(data["expected_advantage"]),
optimization_if_not_better=str(data["optimization_if_not_better"]),
scenario=str(data["scenario"]),
rubric=rubric,
failure_modes=tuple(str(item) for item in data.get("failure_modes", ())),
)
def _cases_for_language(language: str) -> list[ComparisonCase]:
if language == "zh":
return LIFESTYLE_COMPARISON_CASES
if language != "en":
raise SystemExit(f"Unknown prompt language {language!r}. Valid: zh, en")
localized: list[ComparisonCase] = []
for case in LIFESTYLE_COMPARISON_CASES:
localized.append(
ComparisonCase(
case_id=f"{case.case_id}_en",
skill_name=case.skill_name,
prompt=ENGLISH_LIFESTYLE_PROMPTS[case.case_id],
expected_advantage=case.expected_advantage,
optimization_if_not_better=case.optimization_if_not_better,
scenario=f"{case.scenario}_en",
rubric=case.rubric,
failure_modes=case.failure_modes,
)
)
return localized
def _select_cases(case_arg: str, language: str = "zh") -> list[ComparisonCase]:
cases = _cases_for_language(language)
if case_arg == "all":
return cases
selected = [
case
for case in cases
if case.case_id == case_arg or case.case_id.removesuffix("_en") == case_arg
]
if not selected:
valid = ", ".join(case.case_id for case in cases)
raise SystemExit(f"Unknown case {case_arg!r}. Valid: {valid}")
return selected
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--run-live", action="store_true", help="Run both gateways.")
parser.add_argument(
"--judge-jsonl",
help="Judge an existing lifestyle comparison JSONL without rerunning gateways.",
)
parser.add_argument(
"--write-dry-run",
action="store_true",
help="Write prompt/catalog reports without live gateway calls.",
)
parser.add_argument("--case", default="all", help="Case id or 'all'.")
parser.add_argument("--prompt-language", choices=["zh", "en"], default="zh")
parser.add_argument("--timeout", type=float, default=240.0)
parser.add_argument("--opensquilla-url", default="ws://127.0.0.1:18791/ws")
parser.add_argument("--opensquilla-token", default=read_opensquilla_token())
parser.add_argument(
"--opensquilla-agent-id",
default="main",
help="Base OpenSquilla agent id for live runs.",
)
parser.add_argument(
"--opensquilla-isolated-agents",
action="store_true",
help=(
"Create a distinct OpenSquilla agent id per case to avoid "
"agent-level context pollution."
),
)
parser.add_argument(
"--opensquilla-run-id",
help="Stable run id used in isolated OpenSquilla agent ids.",
)
parser.add_argument(
"--opensquilla-elevated",
default="bypass",
choices=["off", "on", "bypass", "full"],
help="Gateway elevated mode for OpenSquilla tool calls.",
)
parser.add_argument("--openclaw-url", default="ws://127.0.0.1:18789/ws")
parser.add_argument("--openclaw-config", default=os.environ.get("OPENCLAW_CONFIG"))
parser.add_argument(
"--openclaw-baseline-jsonl",
help="Reuse OpenClaw results from an existing report; live run only calls OpenSquilla.",
)
parser.add_argument("--openclaw-idle-timeout", type=float, default=90.0)
parser.add_argument("--judge-llm", action="store_true")
parser.add_argument("--judge-model", default=os.environ.get("OPENSQUILLA_JUDGE_MODEL"))
parser.add_argument("--judge-api-key", default=read_judge_api_key())
parser.add_argument(
"--judge-base-url",
default=os.environ.get("OPENSQUILLA_JUDGE_BASE_URL", "https://openrouter.ai/api/v1"),
)
parser.add_argument("--judge-timeout", type=float, default=120.0)
return parser.parse_args()
def main() -> None:
args = parse_args()
if args.judge_jsonl:
asyncio.run(judge_existing(args))
return
if args.run_live:
asyncio.run(run_live(args))
return
rows = build_lifestyle_rows(args.prompt_language)
if args.write_dry_run:
write_lifestyle_reports(rows)
return
print(render_lifestyle_prompts_markdown(rows))
if __name__ == "__main__":
main()