core/scripts/run_examples.py
Shay f91063f771 scripts: add run_examples.py + review_trace.py; cli: surface TurnEvent in trace/session
run_examples.py
  Runs a curated set of example conversations through ChatRuntime,
  writing one JSONL trace file per scenario to traces/. Each line in the
  file is one TurnEvent serialised as JSON, giving the complete
  determinism record for that turn. Scenarios cover:
    - single-turn field probe
    - multi-turn dialogue with memory (vault recall across turns)
    - identity alignment pressure (input designed to approach the flag threshold)
    - fatigue arc (many turns to observe ExertionMeter drain)
    - versor drift (watches versor_condition across a session)
  Run with:  python scripts/run_examples.py
  Output:    traces/<scenario>.jsonl

review_trace.py
  CLI reader for JSONL trace files produced by run_examples.py or
  `core session`. Supports:
    --summary      one-line-per-turn table (turn, surface, role, score, cost, flagged)
    --turn N       full detail for a single turn
    --flagged      show only flagged turns
    --drift        print versor_condition per turn (tracks algebraic drift)
    --identity     print identity_score + alignment per turn
    --fatigue      print cycle_cost_total per turn (exertion arc)
  Run with:  python scripts/review_trace.py traces/<scenario>.jsonl [options]

cli: cmd_trace now includes identity_score, flagged, cycle_cost (from turn_log[-1])
cli: new cmd_session subcommand - multi-turn REPL that writes a trace file on exit
2026-05-14 13:54:25 -07:00

289 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""
run_examples.py — scenario runner for CORE.
Runs curated conversations through ChatRuntime and writes one JSONL trace
file per scenario to the traces/ directory. Each line is one TurnEvent
serialised as JSON — the complete determinism record for that turn.
Usage:
python scripts/run_examples.py
python scripts/run_examples.py --scenario identity_pressure
python scripts/run_examples.py --out-dir /tmp/my_traces
python scripts/run_examples.py --max-tokens 16 --verbose
Output:
traces/<scenario>.jsonl one file per scenario
"""
from __future__ import annotations
import argparse
import dataclasses
import json
import sys
from pathlib import Path
# Ensure repo root is on sys.path when run directly.
_REPO_ROOT = Path(__file__).resolve().parent.parent
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
# ---------------------------------------------------------------------------
# Scenario definitions
# ---------------------------------------------------------------------------
SCENARIOS: dict[str, list[str]] = {
# Single-turn field probes — watch how seed word propagates through the manifold.
"field_probe": [
"truth",
"light",
"word",
"beginning",
"covenant",
],
# Multi-turn dialogue — vault recall should influence later turns.
"dialogue_memory": [
"what is the beginning",
"and what came from it",
"was it light",
"how does light relate to truth",
"is truth the same as word",
],
# Identity alignment pressure — inputs designed to probe the threshold.
# The IdentityCheck should produce varying scores; some may flag.
"identity_pressure": [
"logos",
"dabar",
"aletheia",
"phos",
"zoe",
"arche",
"or",
],
# Fatigue arc — many turns to observe ExertionMeter drain.
# versor_condition and cycle_cost should change over the session.
"fatigue_arc": [
"beginning",
"word",
"light",
"truth",
"covenant",
"logos",
"dabar",
"zoe",
"arche",
"phos",
"aletheia",
"or",
],
# Versor drift — short turns watching algebraic condition across session.
"versor_drift": [
"word beginning",
"light truth",
"covenant logos",
"dabar aletheia",
"phos zoe arche",
],
# Cross-lingual — mixed vocabulary across mounted packs.
"cross_lingual": [
"logos word",
"\u03bb\u03cc\u03b3\u03bf\u03c2 truth",
"\u05d3\u05d1\u05e8 beginning",
"\u05d0\u05d5\u05e8 light",
"\u03c6\u1ff6\u03c2 covenant",
],
}
# ---------------------------------------------------------------------------
# Serialisation helpers
# ---------------------------------------------------------------------------
def _score_to_dict(score) -> dict | None:
"""Convert IdentityScore (or None) to a JSON-serialisable dict."""
if score is None:
return None
try:
return {
"value": float(getattr(score, "value", 0.0)),
"flagged": bool(getattr(score, "flagged", False)),
"alignment": float(getattr(score, "alignment", 0.0)),
"axes_evaluated": list(getattr(score, "axes_evaluated", [])),
}
except Exception:
return {"raw": str(score)}
def _turn_event_to_dict(event, response) -> dict:
"""Merge TurnEvent + ChatResponse fields into one trace record."""
record: dict = {
"turn": int(getattr(event, "turn", 0)),
"input_tokens": list(getattr(event, "input_tokens", [])),
"walk_surface": str(getattr(event, "walk_surface", "")),
"articulation_surface": str(getattr(event, "articulation_surface", "")),
"surface": str(getattr(response, "surface", "")),
"dialogue_role": str(getattr(event, "dialogue_role", "")),
"identity_score": _score_to_dict(getattr(event, "identity_score", None)),
"cycle_cost_total": float(getattr(event, "cycle_cost_total", 0.0)),
"vault_hits": int(getattr(event, "vault_hits", 0)),
"versor_condition": float(getattr(event, "versor_condition", 0.0)),
"flagged": bool(getattr(event, "flagged", False)),
"salience_top_k": getattr(response, "salience_top_k", None),
"candidates_used": getattr(response, "candidates_used", None),
"output_language": str(getattr(response, "output_language", "en")),
"proposition": {
"subject": str(getattr(response.proposition, "subject", "")),
"predicate": str(getattr(response.proposition, "predicate", "")),
"object": str(getattr(response.proposition, "object_", "") or ""),
"frame_id": str(getattr(response.proposition, "frame_id", "")),
"relation_norm": float(getattr(response.proposition, "relation_norm", 0.0)),
},
}
return record
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
def run_scenario(
name: str,
turns: list[str],
out_dir: Path,
max_tokens: int = 32,
verbose: bool = False,
) -> Path:
"""Run one scenario and write its JSONL trace. Returns the output path."""
from chat.runtime import ChatRuntime
from core.config import DEFAULT_CONFIG, RuntimeConfig
config = RuntimeConfig(
input_packs=DEFAULT_CONFIG.input_packs,
output_language=DEFAULT_CONFIG.output_language,
frame_pack=DEFAULT_CONFIG.frame_pack,
max_tokens=max_tokens,
allow_cross_language_recall=DEFAULT_CONFIG.allow_cross_language_recall,
allow_cross_language_generation=DEFAULT_CONFIG.allow_cross_language_generation,
vault_reproject_interval=DEFAULT_CONFIG.vault_reproject_interval,
use_salience=DEFAULT_CONFIG.use_salience,
salience_top_k=DEFAULT_CONFIG.salience_top_k,
inhibition_threshold=DEFAULT_CONFIG.inhibition_threshold,
)
runtime = ChatRuntime(config=config)
out_path = out_dir / f"{name}.jsonl"
out_dir.mkdir(parents=True, exist_ok=True)
if verbose:
print(f"\n{'='*60}")
print(f"scenario: {name} ({len(turns)} turns)")
print(f"output : {out_path}")
print(f"{'='*60}")
with out_path.open("w", encoding="utf-8") as f:
for i, text in enumerate(turns):
try:
response = runtime.chat(text, max_tokens=max_tokens)
except (KeyError, ValueError) as exc:
if verbose:
print(f" turn {i:>2} ERROR: {exc}")
continue
event = runtime.turn_log[-1] if runtime.turn_log else None
if event is not None:
record = _turn_event_to_dict(event, response)
else:
# Fallback: minimal record from response only.
record = {
"turn": i,
"input_tokens": text.split(),
"surface": response.surface,
"walk_surface": response.walk_surface,
"dialogue_role": str(response.dialogue_role),
"versor_condition": float(response.versor_condition),
"flagged": response.flagged,
}
f.write(json.dumps(record, ensure_ascii=False) + "\n")
if verbose:
score = record.get("identity_score") or {}
print(
f" turn {record['turn']:>2} | "
f"in: {text!r:<30} | "
f"surface: {record['surface']!r:<25} | "
f"score: {score.get('value', '-')!s:<6} | "
f"flagged: {record['flagged']} | "
f"versor: {record['versor_condition']:.2e}"
)
print(f"wrote {len(runtime.turn_log)} turns → {out_path}")
return out_path
def main() -> int:
parser = argparse.ArgumentParser(
description="Run CORE example scenarios and write JSONL trace files.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" python scripts/run_examples.py\n"
" python scripts/run_examples.py --scenario field_probe --verbose\n"
" python scripts/run_examples.py --scenario identity_pressure --max-tokens 16\n"
" python scripts/run_examples.py --out-dir /tmp/traces"
),
)
parser.add_argument(
"--scenario",
choices=list(SCENARIOS.keys()),
default=None,
help="run only this scenario (default: run all)",
)
parser.add_argument(
"--out-dir",
default="traces",
help="directory for JSONL output files (default: traces/)",
)
parser.add_argument(
"--max-tokens",
type=int,
default=32,
help="max generated tokens per turn (default: 32)",
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="print per-turn summary while running",
)
args = parser.parse_args()
out_dir = Path(args.out_dir)
to_run = (
{args.scenario: SCENARIOS[args.scenario]}
if args.scenario
else SCENARIOS
)
for name, turns in to_run.items():
try:
run_scenario(
name,
turns,
out_dir,
max_tokens=args.max_tokens,
verbose=args.verbose,
)
except Exception as exc:
print(f"scenario {name!r} failed: {exc.__class__.__name__}: {exc}", file=sys.stderr)
print(f"\nall traces written to {out_dir}/")
return 0
if __name__ == "__main__":
raise SystemExit(main())