feat(s51): hitl CLI — python -m portfolio_optimiser.hitl pending|route
Gate: pytest tests/test_hitl.py -k cli → 4 passed.
This commit is contained in:
parent
62d6b40eae
commit
c4c55fd417
2 changed files with 133 additions and 1 deletions
|
|
@ -31,11 +31,12 @@ S5.1 scope. The static import-graph probe guards hitl's OWN logic against a MAF-
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from pydantic import BaseModel, Field, ValidationError, model_validator
|
||||
|
||||
# Mirrored INLINE from verdicts.py (NOT imported — verdicts.py:29 pulls agent_framework). The inbox
|
||||
# predicate must match load_verdicts_from_dir EXACTLY, else pending would count as judged a file the
|
||||
|
|
@ -239,3 +240,56 @@ def _load_json_dict(file: Path) -> dict[str, Any] | None:
|
|||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
return data if isinstance(data, dict) else None
|
||||
|
||||
|
||||
# --- CLI: python -m portfolio_optimiser.hitl pending|route (Step 4) --------------------------------
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
"""CLI entry: ``python -m portfolio_optimiser.hitl pending|route`` — the operator inspection tool.
|
||||
``pending`` prints one ``run_id verdict_id outcome_type`` line per un-judged proposal; ``route``
|
||||
prints ``run_id verdict_id → <expert|UNROUTABLE> [dim:<id>][ AMBIGUOUS]``. Output is sorted /
|
||||
deterministic. A config-load error → structured ``hitl: <reason>`` on stderr + rc 1 (never a
|
||||
traceback). rc 0 on success."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="portfolio_optimiser.hitl",
|
||||
description="HITL-inspeksjon (S5.1): vis ventende forslag (outbox uten inbox-dom) og rut dem "
|
||||
"til fagekspert etter kostnadskode-prefiks. Leser mapper; ingen modellkall, ingen skriving.",
|
||||
)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p_pending = sub.add_parser("pending", help="list proposals still awaiting an expert verdict")
|
||||
p_pending.add_argument("--outbox-dir", required=True, help="run outbox (proposal/outcome files)")
|
||||
p_pending.add_argument("--verdict-dir", required=True, help="expert verdict inbox")
|
||||
|
||||
p_route = sub.add_parser("route", help="route pending proposals to experts by cost-code prefix")
|
||||
p_route.add_argument("--outbox-dir", required=True, help="run outbox (proposal/outcome files)")
|
||||
p_route.add_argument("--verdict-dir", required=True, help="expert verdict inbox")
|
||||
p_route.add_argument("--routing-config", required=True, help="dimension→expert routing config")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.command == "pending":
|
||||
for proposal in pending(args.outbox_dir, args.verdict_dir):
|
||||
print(f"{proposal.run_id} {proposal.verdict_id} {proposal.outcome_type}")
|
||||
return 0
|
||||
|
||||
try:
|
||||
config = load_routing_config(args.routing_config)
|
||||
except (FileNotFoundError, ValidationError, ValueError) as exc:
|
||||
print(f"hitl: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
for routed in route(args.outbox_dir, args.verdict_dir, config):
|
||||
p = routed.pending
|
||||
if routed.expert is None:
|
||||
print(f"{p.run_id} {p.verdict_id} → UNROUTABLE")
|
||||
else:
|
||||
suffix = " AMBIGUOUS" if routed.ambiguous else ""
|
||||
print(f"{p.run_id} {p.verdict_id} → {routed.expert} [dim:{routed.dimension_id}]{suffix}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover - console entry
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue