Files
nisse/scripts/classifier-evals.py
T
dongarandClaude Opus 5.5 154a0c3fa8
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m42s
CI / e2e (pull_request) Successful in 4m0s
M7.1: extract nisse-core
What isn't the dev agent's moves to nisse-core, unchanged in behaviour,
with its tests: canonical, render, render_anthropic, llm, catalog, probe,
context, views, state, reconcile, worker, cancel, dump, transport, matrix,
execution, testing, vault, memory, oplog, envfile and gitea (whole).

- nisse_core.state.StateStore holds the tables every agent has; an
  agent's subclass adds its tables (version, migrations), roles and
  views. nisse_dev.state.StateStore adds the build VM blocks (v5).
- views.project takes what each role is shown, rather than a fixed table.
- Vault takes the agent's name (agent=), for provenance.
- A test that nisse_core never imports an agent.
- The loop, config, git, delegation, expert, tools and evals stay in
  nisse-dev until nisse-ops has a loop to share.

Docs: the Development Plan marks M7.1 done and notes that Phase 6 waits on
the agent host and Terraform; Architecture Part L names the package.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
2026-10-13 18:37:01 +00:00

62 lines
1.9 KiB
Python

"""Export the action classifier's judgements from a state store (L4, M3.2).
uv run python scripts/classifier-evals.py STATE_DB [...] > cases.jsonl
uv run python scripts/classifier-evals.py STATE_DB --overridden
One JSON line per distinct judgement: exactly what the classifier was shown,
its verdict and reason, the model, what happened (ran, asked, allowed,
refused), and whether the operator overrode it. Overridden cases are the
ones worth adding to tests/integration/classifier_cases.yaml, with the
verdict the operator's choice implies.
The store is opened read-only, so a live agent's database is safe to read.
"""
import argparse
import json
import sqlite3
import sys
from dataclasses import asdict
from pathlib import Path
from nisse_core.canonical import StoredEvent, from_payload
from nisse_dev.evals import classifier_cases
def events(path: Path) -> list[StoredEvent]:
conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
rows = conn.execute(
"SELECT id, ts, type, payload, work_item, actor FROM canonical_events "
"WHERE type = 'action_verdict' ORDER BY id"
).fetchall()
conn.close()
return [
StoredEvent(
id=row[0],
ts=row[1],
event=from_payload(row[2], json.loads(row[3])),
work_item=row[4],
actor=row[5],
)
for row in rows
]
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("stores", nargs="+", type=Path)
parser.add_argument(
"--overridden", action="store_true", help="only cases the operator overrode"
)
args = parser.parse_args()
for store in args.stores:
for case in classifier_cases(events(store)):
if args.overridden and not case.overridden:
continue
print(json.dumps({"store": str(store), **asdict(case)}))
return 0
if __name__ == "__main__":
sys.exit(main())