AliIqbal05's picture
Initialize Judge-GPT Space (#1)
335b794
Raw
History Blame Contribute Delete
20.8 kB
from __future__ import annotations
import json
import re
import time
from collections import Counter
from collections.abc import Callable, Iterable
from pydantic import ValidationError
from .cases import get_case
from .llm import ModelCall, ModelResult, call_small_model
from .models import AgentTurn, CasePacket, JurorVote, TrialEvent, TrialRequest, Verdict
from .retrieval import build_live_case
GPT_OSS_MODEL = "openai/gpt-oss-20b"
OPENBMB_MODEL = "openbmb/AgentCPM-Explore"
NEMOTRON_MODEL = "nvidia/Nemotron-Orchestrator-8B"
OPENAI_PROVIDER = "auto"
OPENBMB_PROVIDER = "featherless-ai"
NEMOTRON_PROVIDER = "featherless-ai"
MODEL_BUDGET = [
("Presiding Advocate", GPT_OSS_MODEL, 20.0),
("Clerk of Style", OPENBMB_MODEL, 4.0),
("Juror/Auditor Ring", NEMOTRON_MODEL, 8.0),
]
TOTAL_PARAMS_B = sum(item[2] for item in MODEL_BUDGET)
JUDGE_NAME = "Marcus Aurelius"
JUDGE_PERSONA = "Stoic duty, restraint, public reason, and disciplined judgment"
JUROR_PERSONAS = {
"Karl Marx": "class power, material conditions, exploitation, institutional incentives",
"John Stuart Mill": "liberty, harm principle, utility, individual rights",
"Confucius": "social harmony, role duty, ritual order, moral cultivation",
"Cleopatra VII": "sovereign pragmatism, diplomacy, survival, legitimacy under pressure",
"Niccolo Machiavelli": "political realism, stability, power, consequences over ideals",
"Jensen Huang": "technological optimism, operator mindset, systems thinking, innovation tradeoffs",
}
JUROR_NAMES = list(JUROR_PERSONAS)
class RequiredModelError(RuntimeError):
"""Raised when a required courtroom model call cannot produce usable output."""
ModelRunner = Callable[..., ModelResult]
def _turn(agent: str, role: str, result: ModelResult, model: str, confidence: float) -> AgentTurn:
return AgentTurn(
agent=agent,
role=role,
content=result.text,
model=model,
confidence=confidence,
input=getattr(result, "input_text", ""),
)
def _case_summary(packet: CasePacket) -> str:
return (
f"{packet.title}. Charge: {packet.charge}\n"
f"Claimant: {packet.claimant_claim}\n"
f"Respondent: {packet.respondent_claim}"
)
def _evidence_summary(packet: CasePacket) -> str:
return "\n".join(
f"{item.id}: {item.title}; direction={item.supports}; reliability={item.reliability:.2f}; note={item.note}"
for item in packet.evidence
)
def _call_trace(calls: list[ModelCall]) -> list[dict]:
return [call.__dict__ for call in calls]
def resolve_case(request: TrialRequest) -> tuple[CasePacket, dict]:
if request.case_id == "live":
packet = build_live_case(request.search_query, request.hypothetical)
if packet:
return packet, {"mode": "live"}
raise RuntimeError("Live retrieval produced too little usable evidence; no fallback case will be substituted.")
return get_case(request.case_id), {"mode": "cached"}
def _generate_role(model_runner: ModelRunner | None = None, **kwargs) -> ModelResult:
if model_runner is not None:
return model_runner(**kwargs)
return call_small_model(**kwargs)
def _required_role(model_runner: ModelRunner | None, model_calls: list[ModelCall], **kwargs) -> ModelResult:
try:
result = _generate_role(model_runner, **kwargs)
except Exception as exc:
raise RequiredModelError(f"{kwargs.get('agent', 'Model')} unavailable: {exc}") from exc
model_calls.append(result.call)
if not result.call.ok:
error = result.call.error or "model call did not complete"
raise RequiredModelError(f"{kwargs.get('agent', 'Model')} unavailable: {error}")
if not result.text.strip():
raise RequiredModelError(f"{kwargs.get('agent', 'Model')} returned an empty response.")
return result
def _trace(packet: CasePacket, source_trace: dict, model_calls: list[ModelCall]) -> dict:
return {
"case_id": packet.id,
"model_budget_b": TOTAL_PARAMS_B,
"models": [{"role": role, "model": model, "params_b": params} for role, model, params in MODEL_BUDGET],
"model_calls": _call_trace(model_calls),
"live_model_call_count": sum(1 for call in model_calls if call.ok),
"attempted_model_call_count": len(model_calls),
**source_trace,
}
def _emit(
packet: CasePacket,
source_trace: dict,
model_calls: list[ModelCall],
event: TrialEvent,
delay: float,
) -> TrialEvent:
event.trace = _trace(packet, source_trace, model_calls)
if delay > 0:
time.sleep(delay)
return event
def _extract_json(text: str) -> object:
stripped = text.strip()
if stripped.startswith("```"):
stripped = re.sub(r"^```(?:json)?\s*", "", stripped, flags=re.I)
stripped = re.sub(r"\s*```$", "", stripped)
try:
return json.loads(stripped)
except json.JSONDecodeError:
match = re.search(r"(\{.*\}|\[.*\])", stripped, flags=re.S)
if not match:
raise
return json.loads(match.group(1))
def _parse_jury_votes(result: ModelResult, packet: CasePacket) -> list[JurorVote]:
try:
data = _extract_json(result.text)
except json.JSONDecodeError as exc:
raise RequiredModelError(f"Nemotron Jury returned invalid JSON: {exc.msg}") from exc
raw_votes = data.get("votes") if isinstance(data, dict) else data
if not isinstance(raw_votes, list):
raise RequiredModelError("Nemotron Jury output must contain a votes list.")
if len(raw_votes) != len(JUROR_NAMES):
raise RequiredModelError("Nemotron Jury must return exactly six juror votes.")
known_evidence = {item.id for item in packet.evidence}
votes: list[JurorVote] = []
try:
for item in raw_votes:
vote = JurorVote.model_validate(item)
votes.append(vote)
except ValidationError as exc:
raise RequiredModelError(f"Nemotron Jury vote schema is invalid: {exc.errors()[0]['msg']}") from exc
if [vote.juror for vote in votes] != JUROR_NAMES:
raise RequiredModelError("Nemotron Jury must return votes in the fixed juror order.")
for vote in votes:
expected_persona = JUROR_PERSONAS[vote.juror]
if vote.persona.strip().lower() != expected_persona:
raise RequiredModelError(f"{vote.juror} persona must be '{expected_persona}'.")
if not vote.reason.strip():
raise RequiredModelError(f"{vote.juror} must include a rationale.")
if not vote.evidence_ids or any(evidence_id not in known_evidence for evidence_id in vote.evidence_ids):
raise RequiredModelError(f"{vote.juror} must cite known evidence IDs.")
return votes
def _majority_finding(votes: list[JurorVote]) -> str:
counts = Counter(vote.vote for vote in votes)
top = counts.most_common()
if not top:
return "uncertain"
if len(top) > 1 and top[0][1] == top[1][1]:
return "mixed"
if top[0][0] == "uncertain":
return "uncertain"
return top[0][0]
def _verdict_from_votes(votes: list[JurorVote]) -> Verdict:
finding = _majority_finding(votes)
evidence_ids = []
for vote in votes:
for evidence_id in vote.evidence_ids:
if evidence_id not in evidence_ids:
evidence_ids.append(evidence_id)
cited = evidence_ids[:4]
counts = Counter(vote.vote for vote in votes)
vote_line = ", ".join(f"{name}: {counts.get(name, 0)}" for name in ("liable", "not_liable", "uncertain"))
decree_by_finding = {
"liable": "The jury majority finds liability on the miniature record.",
"not_liable": "The jury majority does not find liability on the miniature record.",
"mixed": "The jury divides too closely for a clean finding.",
"uncertain": "The jury leaves the court with unresolved uncertainty.",
}
remedy_by_finding = {
"liable": "Enter symbolic censure and proportional repair.",
"not_liable": "Dismiss without prejudice to stronger proof.",
"mixed": "Record a divided result and preserve the exhibits for later review.",
"uncertain": "Withhold sanction and identify the proof gaps before any retrial.",
}
return Verdict(
finding=finding, # type: ignore[arg-type]
decree=decree_by_finding[finding],
rationale=f"Jury vote: {vote_line}. Cited evidence IDs: {', '.join(cited)}.",
evidence_ids=cited,
uncertainty=(
"Uncertainty remains visible: this is an AI-native miniature trial. Retrieved facts, cached "
"packets, and model inferences are separated in the trace and should not be treated as legal advice."
),
remedy=remedy_by_finding[finding],
)
def _jury_task() -> str:
personas = "\n".join(f"- {name}: {persona}" for name, persona in JUROR_PERSONAS.items())
return (
"Return JSON only with a top-level 'votes' array. Create exactly one vote for each juror, in this order: "
f"{', '.join(JUROR_NAMES)}. Valid vote values are liable, not_liable, uncertain. Each item must contain "
"juror, persona, vote, reason, and evidence_ids. The persona value must exactly match the profile below. "
"Each reason should be one concise sentence and each evidence_ids list must cite evidence IDs from the record. "
"Vote through the named public-history worldview, not a generic juror role.\n"
f"{personas}"
)
def run_trial(request: TrialRequest, model_runner: ModelRunner | None = None) -> list[TrialEvent]:
return list(stream_trial(request, delay=0.0, model_runner=model_runner))
def stream_trial(
request: TrialRequest,
delay: float = 0.0,
model_runner: ModelRunner | None = None,
) -> Iterable[TrialEvent]:
packet, source_trace = resolve_case(request)
case_summary = _case_summary(packet)
evidence_summary = _evidence_summary(packet)
model_calls: list[ModelCall] = []
hypo = request.hypothetical.strip()
hypo_line = f"\n\nUser hypothetical admitted as a blue-ribbon sidebar: {hypo}" if hypo else ""
clerk = _required_role(
model_runner,
model_calls,
agent="Clerk Meridian",
role="clerk",
model=OPENBMB_MODEL,
case_summary=case_summary,
evidence_summary=evidence_summary,
task="Announce the case by name, identify the parties, and read the charge.",
provider=OPENBMB_PROVIDER,
max_tokens=110,
)
yield _emit(
packet,
source_trace,
model_calls,
TrialEvent(
phase="intake",
title="The Court Convenes",
body=f"{packet.title}\n{packet.subtitle}\n\nCharge: {packet.charge}{hypo_line}",
turns=[_turn("Clerk Meridian", "clerk", clerk, OPENBMB_MODEL, 0.88)],
evidence=packet.evidence,
),
delay,
)
judge_open = _required_role(
model_runner,
model_calls,
agent=JUDGE_NAME,
role="judge",
model=GPT_OSS_MODEL,
case_summary=case_summary,
evidence_summary=evidence_summary,
task=(
f"As {JUDGE_NAME}, a Stoic courtroom judge guided by {JUDGE_PERSONA}, explain the proceeding "
"and the burden of proof in one or two disciplined sentences."
),
provider=OPENAI_PROVIDER,
max_tokens=110,
)
yield _emit(
packet,
source_trace,
model_calls,
TrialEvent(
phase="intake",
title="The Burden Is Set",
body="The bench defines how the miniature court will weigh the record.",
turns=[_turn(JUDGE_NAME, "judge", judge_open, GPT_OSS_MODEL, 0.88)],
evidence=packet.evidence,
),
delay,
)
claimant_opening = _required_role(
model_runner,
model_calls,
agent="Advocate Auric",
role="claimant advocate",
model=GPT_OSS_MODEL,
case_summary=case_summary,
evidence_summary=evidence_summary,
task="Make the claimant's opening statement alone. Cite the strongest claimant-side exhibit.",
provider=OPENAI_PROVIDER,
max_tokens=130,
)
yield _emit(
packet,
source_trace,
model_calls,
TrialEvent(
phase="claims",
title="Claimant Opening",
body=packet.claimant_claim,
turns=[_turn("Advocate Auric", "claimant advocate", claimant_opening, GPT_OSS_MODEL, 0.88)],
evidence=packet.evidence,
),
delay,
)
respondent_opening = _required_role(
model_runner,
model_calls,
agent="Counsel Sable",
role="respondent advocate",
model=GPT_OSS_MODEL,
case_summary=case_summary,
evidence_summary=evidence_summary,
task="Make the respondent's opening statement alone. Emphasize uncertainty and cite a helpful exhibit.",
provider=OPENAI_PROVIDER,
max_tokens=130,
)
yield _emit(
packet,
source_trace,
model_calls,
TrialEvent(
phase="opening",
title="Respondent Opening",
body=packet.respondent_claim,
turns=[_turn("Counsel Sable", "respondent advocate", respondent_opening, GPT_OSS_MODEL, 0.88)],
evidence=packet.evidence,
),
delay,
)
auditor = _required_role(
model_runner,
model_calls,
agent="Auditor Prism",
role="evidence auditor",
model=NEMOTRON_MODEL,
case_summary=case_summary,
evidence_summary=evidence_summary,
task="Present the evidence record. Identify the strongest exhibit and the weakest inference.",
provider=NEMOTRON_PROVIDER,
max_tokens=150,
)
yield _emit(
packet,
source_trace,
model_calls,
TrialEvent(
phase="evidence",
title="The Record Is Audited",
body="\n".join(f"{item.id}: {item.title} | reliability {item.reliability:.2f} | {item.note}" for item in packet.evidence),
turns=[_turn("Auditor Prism", "evidence auditor", auditor, NEMOTRON_MODEL, 0.86)],
evidence=packet.evidence,
),
delay,
)
judge_question = _required_role(
model_runner,
model_calls,
agent=JUDGE_NAME,
role="judge",
model=GPT_OSS_MODEL,
case_summary=case_summary,
evidence_summary=evidence_summary,
task=(
f"As {JUDGE_NAME}, ask one sharp hinge question that would change the outcome if answered. "
"Use Stoic restraint and public reason."
),
provider=OPENAI_PROVIDER,
max_tokens=100,
)
yield _emit(
packet,
source_trace,
model_calls,
TrialEvent(
phase="questions",
title="The Hinge Question",
body="The bench asks the single question that could turn the record.",
turns=[_turn(JUDGE_NAME, "judge", judge_question, GPT_OSS_MODEL, 0.88)],
evidence=packet.evidence,
),
delay,
)
claimant_answer = _required_role(
model_runner,
model_calls,
agent="Advocate Auric",
role="claimant advocate",
model=GPT_OSS_MODEL,
case_summary=case_summary,
evidence_summary=evidence_summary,
task=f"Answer {JUDGE_NAME}'s hinge question for the claimant: {judge_question.text}",
provider=OPENAI_PROVIDER,
max_tokens=130,
)
yield _emit(
packet,
source_trace,
model_calls,
TrialEvent(
phase="questions",
title="Claimant Answers the Bench",
body="The claimant answers the hinge question.",
turns=[_turn("Advocate Auric", "claimant advocate", claimant_answer, GPT_OSS_MODEL, 0.88)],
evidence=packet.evidence,
),
delay,
)
respondent_answer = _required_role(
model_runner,
model_calls,
agent="Counsel Sable",
role="respondent advocate",
model=GPT_OSS_MODEL,
case_summary=case_summary,
evidence_summary=evidence_summary,
task=f"Answer {JUDGE_NAME}'s hinge question for the respondent: {judge_question.text}",
provider=OPENAI_PROVIDER,
max_tokens=130,
)
yield _emit(
packet,
source_trace,
model_calls,
TrialEvent(
phase="questions",
title="Respondent Answers the Bench",
body="The respondent answers the hinge question.",
turns=[_turn("Counsel Sable", "respondent advocate", respondent_answer, GPT_OSS_MODEL, 0.88)],
evidence=packet.evidence,
),
delay,
)
jury_panel = _required_role(
model_runner,
model_calls,
agent="Nemotron Jury",
role="juror panel",
model=NEMOTRON_MODEL,
case_summary=case_summary,
evidence_summary=evidence_summary,
task="Announce that the six named jurors retire to vote. Do not reveal the votes yet.",
provider=NEMOTRON_PROVIDER,
max_tokens=100,
)
yield _emit(
packet,
source_trace,
model_calls,
TrialEvent(
phase="deliberation",
title="The Jury Retires",
body="Six fixed-perspective jurors leave the public floor to vote from the record.",
turns=[_turn("Nemotron Jury", "juror panel", jury_panel, NEMOTRON_MODEL, 0.86)],
evidence=packet.evidence,
),
delay,
)
jury_votes_result = _required_role(
model_runner,
model_calls,
agent="Nemotron Jury",
role="juror vote generator",
model=NEMOTRON_MODEL,
case_summary=case_summary,
evidence_summary=evidence_summary,
task=_jury_task(),
provider=NEMOTRON_PROVIDER,
max_tokens=650,
)
votes = _parse_jury_votes(jury_votes_result, packet)
for vote in votes:
juror_result = ModelResult(
text=f"{vote.vote.replace('_', ' ').title()}. {vote.reason}",
call=jury_votes_result.call,
input_text=jury_votes_result.input_text,
)
yield _emit(
packet,
source_trace,
model_calls,
TrialEvent(
phase="deliberation",
title=f"Juror {vote.juror} Votes",
body=f"{vote.persona}. Evidence: {', '.join(vote.evidence_ids)}.",
turns=[_turn(vote.juror, "juror", juror_result, NEMOTRON_MODEL, 0.86)],
votes=[vote],
evidence=packet.evidence,
),
delay,
)
verdict = _verdict_from_votes(votes)
verdict_voice = _required_role(
model_runner,
model_calls,
agent=JUDGE_NAME,
role="verdict writer",
model=GPT_OSS_MODEL,
case_summary=case_summary,
evidence_summary=evidence_summary,
task=(
f"As {JUDGE_NAME}, announce the final legal finding after the jury vote with Stoic restraint. "
f"Finding: {verdict.finding}. "
f"Jury rationale: {verdict.rationale} Remedy: {verdict.remedy}. Include uncertainty without disclaiming the role."
),
provider=OPENAI_PROVIDER,
max_tokens=160,
)
yield _emit(
packet,
source_trace,
model_calls,
TrialEvent(
phase="verdict",
title="The Court Announces Judgment",
body=f"{verdict_voice.text}\n\n{verdict.rationale}\n\nRemedy: {verdict.remedy}",
verdict=verdict,
votes=votes,
evidence=packet.evidence,
turns=[_turn(JUDGE_NAME, "verdict writer", verdict_voice, GPT_OSS_MODEL, 0.88)],
),
delay,
)
def stream_trial_jsonl(
request: TrialRequest,
delay: float = 0.0,
model_runner: ModelRunner | None = None,
) -> Iterable[str]:
for event in stream_trial(request, delay, model_runner=model_runner):
yield json.dumps(event.model_dump(), ensure_ascii=True) + "\n"