Spaces:
Sleeping
Sleeping
Initialize Judge-GPT Space
#1
by AliIqbal05 - opened
This view is limited to 50 files because it contains too many changes. See the raw diff here.
- .gitattributes +26 -0
- .gitignore +10 -0
- README.md +96 -5
- app.py +2102 -0
- assets/ATTRIBUTION.md +10 -0
- assets/audio/ATTRIBUTION.md +51 -0
- assets/audio/Judgement.ogg +3 -0
- assets/audio/courtroom.ogg +3 -0
- assets/audio/crowd_shouting.ogg +3 -0
- assets/audio/paper_sound_1.mp3 +0 -0
- assets/audio/paper_sound_4.mp3 +0 -0
- assets/audio/select_001.ogg +0 -0
- assets/audio/steps_in_wood_floor.wav +3 -0
- assets/audio/wood_hammer_01.ogg +0 -0
- assets/audio/wood_hit_03.ogg +0 -0
- assets/background/CourtRoom.png +3 -0
- assets/book/README.md +14 -0
- assets/book/docket-book-closed-keyed.png +3 -0
- assets/book/docket-book-closed.png +3 -0
- assets/book/docket-book-open-keyed.png +3 -0
- assets/book/docket-book-open.png +3 -0
- assets/characters/cleopatra-vii.png +3 -0
- assets/characters/confucius.png +3 -0
- assets/characters/jensen-huang.png +3 -0
- assets/characters/john-stuart-mill.png +3 -0
- assets/characters/karl-marx.png +3 -0
- assets/characters/marcus-aurelius.png +3 -0
- assets/characters/niccolo-machiavelli.png +3 -0
- assets/characters/sources/cleopatra-vii-chroma.png +3 -0
- assets/characters/sources/confucius-chroma.png +3 -0
- assets/characters/sources/jensen-huang-chroma.png +3 -0
- assets/characters/sources/john-stuart-mill-chroma.png +3 -0
- assets/characters/sources/karl-marx-chroma.png +3 -0
- assets/characters/sources/marcus-aurelius-chroma.png +3 -0
- assets/characters/sources/niccolo-machiavelli-chroma.png +3 -0
- assets/courtroom-dickinson.jpg +3 -0
- assets/foreground/JudgeTable.png +3 -0
- assets/foreground/foregroundFence.png +3 -0
- data/README.md +5 -0
- data/agent_trace_sample.json +23 -0
- modal_app.py +193 -0
- requirements.txt +7 -0
- sovereign_bench/__init__.py +6 -0
- sovereign_bench/cases.py +141 -0
- sovereign_bench/engine.py +572 -0
- sovereign_bench/export.py +35 -0
- sovereign_bench/llm.py +209 -0
- sovereign_bench/models.py +86 -0
- sovereign_bench/retrieval.py +70 -0
- tests/test_cases.py +8 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,29 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/audio/courtroom.ogg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
assets/audio/crowd_shouting.ogg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
assets/audio/Judgement.ogg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
assets/audio/steps_in_wood_floor.wav filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
assets/background/CourtRoom.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
assets/book/docket-book-closed-keyed.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
assets/book/docket-book-closed.png filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
assets/book/docket-book-open-keyed.png filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
assets/book/docket-book-open.png filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
assets/characters/cleopatra-vii.png filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
assets/characters/confucius.png filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
assets/characters/jensen-huang.png filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
assets/characters/john-stuart-mill.png filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
assets/characters/karl-marx.png filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
assets/characters/marcus-aurelius.png filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
assets/characters/niccolo-machiavelli.png filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
assets/characters/sources/cleopatra-vii-chroma.png filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
assets/characters/sources/confucius-chroma.png filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
assets/characters/sources/jensen-huang-chroma.png filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
assets/characters/sources/john-stuart-mill-chroma.png filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
assets/characters/sources/karl-marx-chroma.png filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
assets/characters/sources/marcus-aurelius-chroma.png filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
assets/characters/sources/niccolo-machiavelli-chroma.png filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
assets/courtroom-dickinson.jpg filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
assets/foreground/foregroundFence.png filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
assets/foreground/JudgeTable.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
.env.*
|
| 3 |
+
!.env.example
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.py[cod]
|
| 6 |
+
.venv/
|
| 7 |
+
venv/
|
| 8 |
+
.modal.toml
|
| 9 |
+
.cache/
|
| 10 |
+
artifacts/
|
README.md
CHANGED
|
@@ -1,14 +1,105 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: yellow
|
| 5 |
colorTo: red
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
| 11 |
short_description: AI-native miniature trials under 32B.
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Judge-GPT
|
| 3 |
+
emoji: ⚖️
|
| 4 |
colorFrom: yellow
|
| 5 |
colorTo: red
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.17.3
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
short_description: AI-native miniature trials under 32B.
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Judge-GPT
|
| 15 |
+
|
| 16 |
+
Judge-GPT is a cinematic Gradio Space for the Build Small Hackathon's Thousand Token Wood track. It runs two-minute AI-native miniature trials where small-model agents act as advocates, judge, jurors, clerk, and evidence auditor.
|
| 17 |
+
|
| 18 |
+
The app is built to stay under the 32B named-model budget:
|
| 19 |
+
|
| 20 |
+
- `openai/gpt-oss-20b` for primary legal reasoning.
|
| 21 |
+
- `openbmb/AgentCPM-Explore` for clerk/stage/verdict style.
|
| 22 |
+
- `nvidia/Nemotron-Orchestrator-8B` for juror and evidence-auditor review.
|
| 23 |
+
|
| 24 |
+
Total named budget: 32B parameters.
|
| 25 |
+
|
| 26 |
+
## What the app can do
|
| 27 |
+
|
| 28 |
+
- Run cached trials for the Socrates and Barnaby demo cases without network search.
|
| 29 |
+
- Run the Live Search Tribunal path, which builds a search packet from a user query and stops if live material is too weak to support a trial.
|
| 30 |
+
- Add a hypothetical sidebar to shift the framing of a trial without editing cached case files.
|
| 31 |
+
- Switch trial pacing between swift, measured, and ceremonial speeds.
|
| 32 |
+
- Stage the courtroom with phase-specific visuals, agent puppets, evidence props, captions, and browser audio cues.
|
| 33 |
+
- Show the Mind Layer as a compact JSON trace of agent turns and phase metadata.
|
| 34 |
+
- Call a Modal streaming endpoint when `MODAL_TRIAL_URL` is configured. Endpoint or model failures stop the trial instead of substituting cached dialogue.
|
| 35 |
+
- Retain decree and agent-trace export helpers in `sovereign_bench/export.py` for future UI restoration.
|
| 36 |
+
|
| 37 |
+
## Limitations
|
| 38 |
+
|
| 39 |
+
- Judge-GPT is not legal advice and should not be used for real legal decisions.
|
| 40 |
+
- Live search snippets are not independently verified by the app.
|
| 41 |
+
- Output quality depends on Modal GPU availability, token limits, and the configured Hugging Face models.
|
| 42 |
+
- Model, Modal, or live retrieval failures stop the current trial rather than returning substitute courtroom dialogue.
|
| 43 |
+
- Trial results are not persisted across sessions.
|
| 44 |
+
- Export generation remains in the codebase, but the visible download UI is currently hidden.
|
| 45 |
+
|
| 46 |
+
## Run locally
|
| 47 |
+
|
| 48 |
+
```powershell
|
| 49 |
+
python -m pip install -r requirements.txt
|
| 50 |
+
python app.py
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## Modal backend
|
| 54 |
+
|
| 55 |
+
The Gradio app works locally without Modal. If `MODAL_TRIAL_URL` is set, the Space calls the Modal streaming endpoint and stops the trial if the endpoint is unavailable.
|
| 56 |
+
|
| 57 |
+
The deployed Modal endpoint runs each role prompt through a GPU-backed vLLM class on H100 by default. Traces mark successful GPU calls with `runtime: modal-gpu-vllm`, `provider: modal-gpu-vllm`, and `gpu: H100`. If a GPU/model load fails, the trial stops; the app does not substitute provider or cached dialogue.
|
| 58 |
+
|
| 59 |
+
```powershell
|
| 60 |
+
python -m modal deploy modal_app.py
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
Keep the deployed endpoint URL as a Hugging Face Space variable named `MODAL_TRIAL_URL`.
|
| 64 |
+
|
| 65 |
+
## Project targets
|
| 66 |
+
|
| 67 |
+
Workspace connected to:
|
| 68 |
+
|
| 69 |
+
- GitHub: `https://github.com/aliiqbal24/BuildSmallfinal.git`
|
| 70 |
+
- Modal profile: `ali-j-iqbal24`
|
| 71 |
+
- Hugging Face user: `AliIqbal05`
|
| 72 |
+
|
| 73 |
+
## Secrets
|
| 74 |
+
|
| 75 |
+
Credentials are not committed to this repo.
|
| 76 |
+
|
| 77 |
+
- Local Hugging Face CLI auth is stored in the Hugging Face cache.
|
| 78 |
+
- Modal auth is stored in the local Modal profile.
|
| 79 |
+
- Modal has a secret named `huggingface` with `HF_TOKEN`.
|
| 80 |
+
|
| 81 |
+
Use the Modal secret in functions like this:
|
| 82 |
+
|
| 83 |
+
```python
|
| 84 |
+
@app.function(secrets=[modal.Secret.from_name("huggingface")])
|
| 85 |
+
def run_model():
|
| 86 |
+
token = os.getenv("HF_TOKEN")
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## Developer guide
|
| 90 |
+
|
| 91 |
+
- `app.py`: Gradio UI, CSS, JavaScript audio hooks, HTML renderers, and Modal/local streaming switch.
|
| 92 |
+
- `sovereign_bench/engine.py`: trial phases, agent orchestration, verdict assembly, and trace construction.
|
| 93 |
+
- `sovereign_bench/llm.py`: Hugging Face calls, strict model error handling, and prompt building.
|
| 94 |
+
- `sovereign_bench/retrieval.py`: live search packet construction.
|
| 95 |
+
- `sovereign_bench/models.py`: Pydantic schemas for cases, evidence, events, turns, votes, and verdicts.
|
| 96 |
+
- `sovereign_bench/cases.py`: cached demo case packets.
|
| 97 |
+
- `sovereign_bench/export.py`: dormant decree and trace writers.
|
| 98 |
+
- `modal_app.py`: Modal deployment and GPU-backed streaming endpoint.
|
| 99 |
+
- `tests/`: engine, case, and rendering regression coverage.
|
| 100 |
+
|
| 101 |
+
## Verify Modal to Hugging Face
|
| 102 |
+
|
| 103 |
+
```powershell
|
| 104 |
+
python -m modal run modal_app.py
|
| 105 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,2102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from collections.abc import Iterable
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
import httpx
|
| 9 |
+
|
| 10 |
+
from sovereign_bench.engine import JUDGE_NAME, JUROR_PERSONAS, stream_trial
|
| 11 |
+
from sovereign_bench.models import TrialEvent, TrialRequest
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _load_env_file() -> None:
|
| 15 |
+
path = ".env"
|
| 16 |
+
if not os.path.exists(path):
|
| 17 |
+
return
|
| 18 |
+
with open(path, encoding="utf-8") as handle:
|
| 19 |
+
for line in handle:
|
| 20 |
+
stripped = line.strip()
|
| 21 |
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
| 22 |
+
continue
|
| 23 |
+
key, value = stripped.split("=", 1)
|
| 24 |
+
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
_load_env_file()
|
| 28 |
+
|
| 29 |
+
CASE_OPTIONS = {
|
| 30 |
+
"Trial of Socrates": "socrates",
|
| 31 |
+
"The People v. Barnaby Buttons": "barnaby",
|
| 32 |
+
"Live Search Tribunal": "live",
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
PHASE_GLYPHS = {
|
| 36 |
+
"pretrial": "00",
|
| 37 |
+
"intake": "01",
|
| 38 |
+
"claims": "02",
|
| 39 |
+
"opening": "03",
|
| 40 |
+
"evidence": "04",
|
| 41 |
+
"questions": "05",
|
| 42 |
+
"deliberation": "06",
|
| 43 |
+
"verdict": "07",
|
| 44 |
+
"appeal": "08",
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
AUDIO_PATHS = {
|
| 48 |
+
"score": "/gradio_api/file=assets/audio/courtroom.ogg",
|
| 49 |
+
"judgement": "/gradio_api/file=assets/audio/Judgement.ogg",
|
| 50 |
+
"crowd": "/gradio_api/file=assets/audio/crowd_shouting.ogg",
|
| 51 |
+
"gavel": "/gradio_api/file=assets/audio/wood_hammer_01.ogg",
|
| 52 |
+
"wood": "/gradio_api/file=assets/audio/wood_hit_03.ogg",
|
| 53 |
+
"steps": "/gradio_api/file=assets/audio/steps_in_wood_floor.wav",
|
| 54 |
+
"paper": "/gradio_api/file=assets/audio/paper_sound_1.mp3",
|
| 55 |
+
"paper_long": "/gradio_api/file=assets/audio/paper_sound_4.mp3",
|
| 56 |
+
"select": "/gradio_api/file=assets/audio/select_001.ogg",
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
CSS = """
|
| 60 |
+
:root {
|
| 61 |
+
--ink: #23170e;
|
| 62 |
+
--paper: #f3dfb7;
|
| 63 |
+
--paper-dark: #c79455;
|
| 64 |
+
--gold: #d9b060;
|
| 65 |
+
--mahogany: #4b2119;
|
| 66 |
+
--shadow: rgba(10, 5, 2, .6);
|
| 67 |
+
--red: #8f2e2d;
|
| 68 |
+
--green: #2f6f5e;
|
| 69 |
+
--blue: #254f7a;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
body,
|
| 73 |
+
.gradio-container {
|
| 74 |
+
margin: 0;
|
| 75 |
+
background: #141413 !important;
|
| 76 |
+
background-color: #141413 !important;
|
| 77 |
+
color: var(--ink);
|
| 78 |
+
font-family: Georgia, "Times New Roman", serif;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.gradio-container {
|
| 82 |
+
max-width: none !important;
|
| 83 |
+
padding: 0 !important;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.main,
|
| 87 |
+
.contain {
|
| 88 |
+
max-width: none !important;
|
| 89 |
+
padding: 0 !important;
|
| 90 |
+
background: transparent !important;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.gradio-container main,
|
| 94 |
+
.gradio-container .wrap,
|
| 95 |
+
.gradio-container .app,
|
| 96 |
+
.gradio-container .html-container {
|
| 97 |
+
background: transparent !important;
|
| 98 |
+
padding-left: 0 !important;
|
| 99 |
+
padding-right: 0 !important;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.docket-book-controls {
|
| 103 |
+
position: fixed;
|
| 104 |
+
left: 50%;
|
| 105 |
+
top: clamp(172px, 21vh, 212px);
|
| 106 |
+
z-index: 9999;
|
| 107 |
+
width: min(620px, calc(100vw - 160px));
|
| 108 |
+
max-width: none;
|
| 109 |
+
margin: 0;
|
| 110 |
+
padding: 0;
|
| 111 |
+
transform: translateX(-50%) rotate(-1deg);
|
| 112 |
+
border: 0 !important;
|
| 113 |
+
border-radius: 0 !important;
|
| 114 |
+
background: transparent !important;
|
| 115 |
+
box-shadow: none !important;
|
| 116 |
+
color: #321d10;
|
| 117 |
+
transition: opacity .32s ease, transform .65s ease;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
body.trial-has-started .docket-book-controls {
|
| 121 |
+
opacity: 0;
|
| 122 |
+
pointer-events: none;
|
| 123 |
+
transform: translateX(-50%) rotateX(56deg) rotate(-1deg) scale(.45);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.docket-book-controls::before {
|
| 127 |
+
content: none;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.docket-book-controls,
|
| 131 |
+
.docket-book-controls > *,
|
| 132 |
+
.docket-book-controls .form,
|
| 133 |
+
.docket-book-controls .block,
|
| 134 |
+
.docket-book-controls .gap,
|
| 135 |
+
.docket-book-controls .wrap {
|
| 136 |
+
background: transparent !important;
|
| 137 |
+
border: 0 !important;
|
| 138 |
+
box-shadow: none !important;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.docket-book-controls .docket-book-controls {
|
| 142 |
+
position: static !important;
|
| 143 |
+
left: auto !important;
|
| 144 |
+
top: auto !important;
|
| 145 |
+
width: 100% !important;
|
| 146 |
+
transform: none !important;
|
| 147 |
+
opacity: 1;
|
| 148 |
+
pointer-events: auto;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
body.trial-has-started .docket-book-controls .docket-book-controls {
|
| 152 |
+
pointer-events: none;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.book-control-heading {
|
| 156 |
+
margin: 0 0 6px;
|
| 157 |
+
color: #694019;
|
| 158 |
+
font: 900 12px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
|
| 159 |
+
letter-spacing: .08em;
|
| 160 |
+
text-transform: uppercase;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.docket-book-controls label,
|
| 164 |
+
.docket-book-controls span,
|
| 165 |
+
.docket-book-controls .prose {
|
| 166 |
+
color: #321d10 !important;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.docket-book-controls label {
|
| 170 |
+
font-size: 11px !important;
|
| 171 |
+
font-weight: 800 !important;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.docket-book-controls input,
|
| 175 |
+
.docket-book-controls textarea,
|
| 176 |
+
.docket-book-controls [role="combobox"],
|
| 177 |
+
.docket-book-controls .wrap-inner {
|
| 178 |
+
border-color: rgba(90, 50, 20, .24) !important;
|
| 179 |
+
border-radius: 4px !important;
|
| 180 |
+
background: rgba(255, 243, 207, .58) !important;
|
| 181 |
+
color: #241509 !important;
|
| 182 |
+
box-shadow: inset 0 1px 0 rgba(255,255,255,.24) !important;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.docket-book-controls textarea {
|
| 186 |
+
min-height: 42px !important;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.docket-book-controls button.primary {
|
| 190 |
+
min-height: 42px;
|
| 191 |
+
border: 1px solid rgba(44, 21, 10, .42) !important;
|
| 192 |
+
border-radius: 5px !important;
|
| 193 |
+
background: #1c130d !important;
|
| 194 |
+
color: #fff3d2 !important;
|
| 195 |
+
box-shadow: inset 0 1px 0 rgba(255,255,255,.12), 0 8px 18px rgba(40, 18, 9, .28);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.docket-book-controls .book-status p {
|
| 199 |
+
margin: 0 !important;
|
| 200 |
+
color: #5a3519 !important;
|
| 201 |
+
font-size: 12px;
|
| 202 |
+
line-height: 1.25;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.trial-options {
|
| 206 |
+
max-width: 1120px;
|
| 207 |
+
margin: 0 auto 14px;
|
| 208 |
+
border: 1px solid rgba(255, 226, 154, .18);
|
| 209 |
+
border-radius: 6px;
|
| 210 |
+
background: rgba(18, 9, 5, .78);
|
| 211 |
+
color: #f5dfb5;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.trial-options label,
|
| 215 |
+
.trial-options span,
|
| 216 |
+
.trial-options .prose {
|
| 217 |
+
color: #f5dfb5 !important;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.court-episode-stage {
|
| 221 |
+
--spot-x: 50%;
|
| 222 |
+
--spot-y: 36%;
|
| 223 |
+
position: relative;
|
| 224 |
+
min-height: min(880px, calc(100vh - 112px));
|
| 225 |
+
height: min(880px, calc(100vh - 112px));
|
| 226 |
+
margin: 0;
|
| 227 |
+
width: 100%;
|
| 228 |
+
max-width: none;
|
| 229 |
+
overflow: hidden;
|
| 230 |
+
isolation: auto;
|
| 231 |
+
color: #fff0d2;
|
| 232 |
+
border: 0;
|
| 233 |
+
border-radius: 0;
|
| 234 |
+
background: transparent;
|
| 235 |
+
box-shadow: none;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.court-episode-stage::before {
|
| 239 |
+
content: "";
|
| 240 |
+
display: none;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.court-episode-stage::after {
|
| 244 |
+
content: "";
|
| 245 |
+
display: none;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.court-episode-stage > * {
|
| 249 |
+
position: relative;
|
| 250 |
+
z-index: 4;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.episode-room {
|
| 254 |
+
position: absolute;
|
| 255 |
+
inset: 0;
|
| 256 |
+
z-index: 3;
|
| 257 |
+
background:
|
| 258 |
+
url('/gradio_api/file=assets/background/CourtRoom.png') center center / 100% 100% no-repeat,
|
| 259 |
+
#26120b;
|
| 260 |
+
filter: none;
|
| 261 |
+
transform: none;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.trial-started .episode-room,
|
| 265 |
+
.phase-intake .episode-room,
|
| 266 |
+
.phase-claims .episode-room,
|
| 267 |
+
.phase-opening .episode-room,
|
| 268 |
+
.phase-evidence .episode-room,
|
| 269 |
+
.phase-questions .episode-room,
|
| 270 |
+
.phase-deliberation .episode-room,
|
| 271 |
+
.phase-verdict .episode-room,
|
| 272 |
+
.phase-appeal .episode-room {
|
| 273 |
+
filter: none;
|
| 274 |
+
transform: none;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.phase-intake,
|
| 278 |
+
.phase-appeal { --spot-x: 50%; --spot-y: 30%; }
|
| 279 |
+
.phase-claims,
|
| 280 |
+
.phase-opening { --spot-x: 43%; --spot-y: 66%; }
|
| 281 |
+
.phase-evidence { --spot-x: 70%; --spot-y: 56%; }
|
| 282 |
+
.phase-questions,
|
| 283 |
+
.phase-verdict { --spot-x: 50%; --spot-y: 34%; }
|
| 284 |
+
.phase-deliberation { --spot-x: 79%; --spot-y: 60%; }
|
| 285 |
+
|
| 286 |
+
.episode-title {
|
| 287 |
+
position: absolute;
|
| 288 |
+
left: 26px;
|
| 289 |
+
top: 22px;
|
| 290 |
+
z-index: 9;
|
| 291 |
+
max-width: min(780px, calc(100% - 330px));
|
| 292 |
+
text-shadow: 0 3px 18px rgba(0, 0, 0, .75);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.episode-kicker,
|
| 296 |
+
.prop-label,
|
| 297 |
+
.caption-phase,
|
| 298 |
+
.tooltip-meta,
|
| 299 |
+
.drawer-kicker {
|
| 300 |
+
color: #f4d58f;
|
| 301 |
+
font: 800 11px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
|
| 302 |
+
letter-spacing: .06em;
|
| 303 |
+
text-transform: uppercase;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.episode-title h1 {
|
| 307 |
+
margin: 4px 0 6px;
|
| 308 |
+
max-width: 780px;
|
| 309 |
+
color: #fff4d7;
|
| 310 |
+
font-size: clamp(28px, 4.2vw, 58px);
|
| 311 |
+
line-height: .98;
|
| 312 |
+
letter-spacing: 0;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.episode-title p {
|
| 316 |
+
margin: 0;
|
| 317 |
+
max-width: 720px;
|
| 318 |
+
color: #f8dcaa;
|
| 319 |
+
font-size: 15px;
|
| 320 |
+
line-height: 1.38;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.audio-deck {
|
| 324 |
+
display: none;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.sound-toggle {
|
| 328 |
+
position: fixed;
|
| 329 |
+
left: 18px;
|
| 330 |
+
bottom: 18px;
|
| 331 |
+
z-index: 80;
|
| 332 |
+
width: 46px;
|
| 333 |
+
height: 46px;
|
| 334 |
+
border: 1px solid rgba(255, 226, 154, .48);
|
| 335 |
+
border-radius: 50%;
|
| 336 |
+
background: rgba(22, 11, 7, .82);
|
| 337 |
+
box-shadow: 0 12px 28px rgba(0, 0, 0, .42), inset 0 1px 0 rgba(255, 255, 255, .12);
|
| 338 |
+
cursor: pointer;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.sound-toggle:hover,
|
| 342 |
+
.sound-toggle:focus-visible {
|
| 343 |
+
outline: none;
|
| 344 |
+
border-color: rgba(255, 226, 154, .82);
|
| 345 |
+
background: rgba(41, 20, 12, .92);
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.sound-toggle .sound-icon {
|
| 349 |
+
position: absolute;
|
| 350 |
+
left: 13px;
|
| 351 |
+
top: 15px;
|
| 352 |
+
width: 10px;
|
| 353 |
+
height: 16px;
|
| 354 |
+
border-radius: 2px 0 0 2px;
|
| 355 |
+
background: #ffe5a6;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.sound-toggle .sound-icon::before {
|
| 359 |
+
content: "";
|
| 360 |
+
position: absolute;
|
| 361 |
+
left: 7px;
|
| 362 |
+
top: -3px;
|
| 363 |
+
width: 14px;
|
| 364 |
+
height: 22px;
|
| 365 |
+
border: 3px solid #ffe5a6;
|
| 366 |
+
border-left: 0;
|
| 367 |
+
border-radius: 0 18px 18px 0;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.sound-toggle .sound-icon::after {
|
| 371 |
+
content: "";
|
| 372 |
+
position: absolute;
|
| 373 |
+
left: 20px;
|
| 374 |
+
top: -9px;
|
| 375 |
+
width: 3px;
|
| 376 |
+
height: 34px;
|
| 377 |
+
border-radius: 4px;
|
| 378 |
+
background: #d64d45;
|
| 379 |
+
opacity: 0;
|
| 380 |
+
transform: rotate(42deg);
|
| 381 |
+
transform-origin: center;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.sound-toggle.muted .sound-icon::after {
|
| 385 |
+
opacity: 1;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.episode-book {
|
| 389 |
+
position: absolute;
|
| 390 |
+
left: 50%;
|
| 391 |
+
top: 12%;
|
| 392 |
+
z-index: 12;
|
| 393 |
+
width: min(760px, calc(100% - 32px));
|
| 394 |
+
aspect-ratio: 3 / 2;
|
| 395 |
+
transform: translateX(-50%) rotateX(0) rotateZ(-1deg);
|
| 396 |
+
transform-origin: center bottom;
|
| 397 |
+
color: #2b1b10;
|
| 398 |
+
filter: drop-shadow(0 34px 36px rgba(0, 0, 0, .48));
|
| 399 |
+
pointer-events: none;
|
| 400 |
+
transition: top .85s ease, width .85s ease, transform .85s ease, filter .85s ease, opacity .85s ease;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.book-art {
|
| 404 |
+
position: absolute;
|
| 405 |
+
inset: 0;
|
| 406 |
+
width: 100%;
|
| 407 |
+
height: 100%;
|
| 408 |
+
object-fit: contain;
|
| 409 |
+
pointer-events: none;
|
| 410 |
+
user-select: none;
|
| 411 |
+
transition: opacity .36s ease;
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
.book-art.closed-art {
|
| 415 |
+
opacity: 0;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.episode-book.closed {
|
| 419 |
+
top: 36%;
|
| 420 |
+
width: min(245px, 30vw);
|
| 421 |
+
transform: translateX(-50%) rotateX(56deg) rotateZ(1deg);
|
| 422 |
+
opacity: .92;
|
| 423 |
+
filter: drop-shadow(0 18px 18px rgba(0, 0, 0, .45));
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.episode-book.closed .open-art {
|
| 427 |
+
opacity: 0;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.episode-book.closed .closed-art {
|
| 431 |
+
opacity: 1;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.episode-book.closed .book-open-content {
|
| 435 |
+
opacity: 0;
|
| 436 |
+
pointer-events: none;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.book-open-content {
|
| 440 |
+
position: absolute;
|
| 441 |
+
inset: 17% 10% 13%;
|
| 442 |
+
z-index: 2;
|
| 443 |
+
display: grid;
|
| 444 |
+
grid-template-columns: 1fr 1fr;
|
| 445 |
+
gap: 72px;
|
| 446 |
+
padding: 0 28px;
|
| 447 |
+
transition: opacity .35s ease;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.book-open-content h2 {
|
| 451 |
+
margin: 0 0 10px;
|
| 452 |
+
color: #4c2a12;
|
| 453 |
+
font-size: 30px;
|
| 454 |
+
letter-spacing: 0;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
.book-open-content p,
|
| 458 |
+
.book-entry {
|
| 459 |
+
color: #3c2615;
|
| 460 |
+
font-size: 15px;
|
| 461 |
+
line-height: 1.34;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.book-entry {
|
| 465 |
+
margin: 11px 0;
|
| 466 |
+
padding-left: 12px;
|
| 467 |
+
border-left: 3px solid rgba(111, 61, 23, .36);
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.judge-dais {
|
| 471 |
+
position: absolute;
|
| 472 |
+
left: 50%;
|
| 473 |
+
top: 27%;
|
| 474 |
+
z-index: 6;
|
| 475 |
+
width: min(360px, 32vw);
|
| 476 |
+
min-width: 230px;
|
| 477 |
+
transform: translateX(-50%);
|
| 478 |
+
text-align: center;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
.bench-front {
|
| 482 |
+
display: none;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
.gavel {
|
| 486 |
+
display: none;
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
.gavel::before {
|
| 490 |
+
content: "";
|
| 491 |
+
display: none;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.phase-verdict .gavel {
|
| 495 |
+
animation: gavel-hit .55s ease-out both;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.counsel-table {
|
| 499 |
+
position: absolute;
|
| 500 |
+
bottom: 19%;
|
| 501 |
+
z-index: 6;
|
| 502 |
+
width: min(255px, 22vw);
|
| 503 |
+
height: 84px;
|
| 504 |
+
border: 0;
|
| 505 |
+
background: transparent;
|
| 506 |
+
box-shadow: none;
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
.counsel-table.left { left: 17%; }
|
| 510 |
+
.counsel-table.right { right: 17%; }
|
| 511 |
+
|
| 512 |
+
.trial-floor-mark {
|
| 513 |
+
display: none;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.witness-area {
|
| 517 |
+
position: absolute;
|
| 518 |
+
right: 8.5%;
|
| 519 |
+
bottom: 28%;
|
| 520 |
+
z-index: 6;
|
| 521 |
+
width: min(190px, 18vw);
|
| 522 |
+
height: 98px;
|
| 523 |
+
border: 0;
|
| 524 |
+
background: transparent;
|
| 525 |
+
box-shadow: none;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.jury-benches {
|
| 529 |
+
position: absolute;
|
| 530 |
+
top: 43%;
|
| 531 |
+
z-index: 7;
|
| 532 |
+
width: min(220px, 16vw);
|
| 533 |
+
min-width: 150px;
|
| 534 |
+
display: grid;
|
| 535 |
+
gap: 6px;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
.jury-benches.left {
|
| 539 |
+
left: 4.5%;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.jury-benches.right {
|
| 543 |
+
right: 4.5%;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
.jury-benches.left .jury-row {
|
| 547 |
+
transform: rotate(-7deg) skewY(-3deg);
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.jury-benches.right .jury-row {
|
| 551 |
+
transform: rotate(7deg) skewY(3deg);
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
.jury-rail {
|
| 555 |
+
display: none;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.jury-row {
|
| 559 |
+
display: grid;
|
| 560 |
+
grid-template-columns: repeat(3, 1fr);
|
| 561 |
+
gap: 8px;
|
| 562 |
+
align-items: end;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.gallery-benches {
|
| 566 |
+
display: none;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
.gallery-benches div {
|
| 570 |
+
display: none;
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.prop-label {
|
| 574 |
+
display: none;
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
.foreground-props {
|
| 578 |
+
position: absolute;
|
| 579 |
+
inset: 0;
|
| 580 |
+
z-index: 13;
|
| 581 |
+
pointer-events: none;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
.foreground-fence,
|
| 585 |
+
.judge-table-foreground {
|
| 586 |
+
position: absolute;
|
| 587 |
+
display: block;
|
| 588 |
+
max-width: none;
|
| 589 |
+
height: auto;
|
| 590 |
+
filter: none;
|
| 591 |
+
opacity: 1;
|
| 592 |
+
pointer-events: none;
|
| 593 |
+
user-select: none;
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
.foreground-fence {
|
| 597 |
+
bottom: -1.5%;
|
| 598 |
+
width: 47%;
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
.foreground-fence.fence-left {
|
| 602 |
+
left: 0;
|
| 603 |
+
transform: translateX(-2%);
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
.foreground-fence.fence-right {
|
| 607 |
+
right: 0;
|
| 608 |
+
transform: translateX(2%);
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
.judge-table-foreground {
|
| 612 |
+
left: 50%;
|
| 613 |
+
top: 35%;
|
| 614 |
+
z-index: 1;
|
| 615 |
+
width: 46%;
|
| 616 |
+
transform: translateX(-50%);
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
.puppet {
|
| 620 |
+
--skin: #c99257;
|
| 621 |
+
--robe: #282128;
|
| 622 |
+
--accent: #8a2f2f;
|
| 623 |
+
--portrait-width: 74px;
|
| 624 |
+
--portrait-top: -14px;
|
| 625 |
+
position: absolute;
|
| 626 |
+
z-index: 8;
|
| 627 |
+
width: 72px;
|
| 628 |
+
height: 128px;
|
| 629 |
+
transform: translate(-50%, -100%);
|
| 630 |
+
transform-origin: center bottom;
|
| 631 |
+
filter: none;
|
| 632 |
+
color: inherit;
|
| 633 |
+
text-decoration: none;
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
.puppet.small {
|
| 637 |
+
width: 50px;
|
| 638 |
+
height: 94px;
|
| 639 |
+
--portrait-width: 54px;
|
| 640 |
+
--portrait-top: -8px;
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
.puppet.active {
|
| 644 |
+
animation: puppet-breathe 1.45s ease-in-out infinite;
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
.puppet.walking {
|
| 648 |
+
animation: lawyer-walk 1.9s ease-in-out infinite;
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
.puppet.judge {
|
| 652 |
+
left: 50%;
|
| 653 |
+
top: 31%;
|
| 654 |
+
--skin: #c38a55;
|
| 655 |
+
--robe: #1b1b20;
|
| 656 |
+
--accent: #79242a;
|
| 657 |
+
--portrait-width: 96px;
|
| 658 |
+
--portrait-top: -28px;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
.puppet.clerk {
|
| 662 |
+
left: 43%;
|
| 663 |
+
top: 41%;
|
| 664 |
+
--skin: #b77b52;
|
| 665 |
+
--robe: #365548;
|
| 666 |
+
--accent: #2f6f5e;
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
.puppet.auric {
|
| 670 |
+
left: 24%;
|
| 671 |
+
top: 62%;
|
| 672 |
+
--skin: #c9975d;
|
| 673 |
+
--robe: #5b2719;
|
| 674 |
+
--accent: #a45c25;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
.speaker-auric .puppet.auric {
|
| 678 |
+
left: 43%;
|
| 679 |
+
top: 66%;
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
.puppet.sable {
|
| 683 |
+
left: 75%;
|
| 684 |
+
top: 62%;
|
| 685 |
+
--skin: #a86d4a;
|
| 686 |
+
--robe: #1d3045;
|
| 687 |
+
--accent: #254f7a;
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
.speaker-sable .puppet.sable {
|
| 691 |
+
left: 57%;
|
| 692 |
+
top: 66%;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
.puppet.auditor {
|
| 696 |
+
left: 71%;
|
| 697 |
+
top: 55%;
|
| 698 |
+
--skin: #c6a65b;
|
| 699 |
+
--robe: #4b3d1b;
|
| 700 |
+
--accent: #8d6b1f;
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
.puppet-portrait {
|
| 704 |
+
position: absolute;
|
| 705 |
+
left: 50%;
|
| 706 |
+
top: var(--portrait-top);
|
| 707 |
+
z-index: 3;
|
| 708 |
+
width: var(--portrait-width);
|
| 709 |
+
height: auto;
|
| 710 |
+
max-height: 118px;
|
| 711 |
+
transform: translateX(-50%);
|
| 712 |
+
object-fit: contain;
|
| 713 |
+
pointer-events: none;
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
.phase-evidence .puppet.auditor {
|
| 717 |
+
animation: evidence-focus 1.35s ease-in-out infinite;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.puppet::before {
|
| 721 |
+
content: "";
|
| 722 |
+
position: absolute;
|
| 723 |
+
left: 50%;
|
| 724 |
+
top: 0;
|
| 725 |
+
width: 44px;
|
| 726 |
+
height: 44px;
|
| 727 |
+
transform: translateX(-50%);
|
| 728 |
+
border: 2px solid rgba(255, 232, 174, .58);
|
| 729 |
+
border-radius: 50%;
|
| 730 |
+
background:
|
| 731 |
+
radial-gradient(circle at 34% 32%, rgba(255,255,255,.38), transparent 22%),
|
| 732 |
+
radial-gradient(circle at 36% 42%, #1b120c 0 2px, transparent 2.5px),
|
| 733 |
+
radial-gradient(circle at 62% 42%, #1b120c 0 2px, transparent 2.5px),
|
| 734 |
+
linear-gradient(180deg, var(--skin), #8b5638);
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.puppet::after {
|
| 738 |
+
content: "";
|
| 739 |
+
position: absolute;
|
| 740 |
+
left: 50%;
|
| 741 |
+
top: 48px;
|
| 742 |
+
width: 58px;
|
| 743 |
+
height: 70px;
|
| 744 |
+
transform: translateX(-50%);
|
| 745 |
+
border: 1px solid rgba(255, 232, 174, .22);
|
| 746 |
+
border-radius: 24px 24px 8px 8px;
|
| 747 |
+
background:
|
| 748 |
+
linear-gradient(90deg, transparent 46%, rgba(255, 226, 154, .14) 49%, transparent 52%),
|
| 749 |
+
linear-gradient(180deg, var(--accent), var(--robe) 52%, #130a07);
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
.puppet .mouth {
|
| 753 |
+
position: absolute;
|
| 754 |
+
left: 50%;
|
| 755 |
+
top: 27px;
|
| 756 |
+
z-index: 2;
|
| 757 |
+
width: 15px;
|
| 758 |
+
height: 7px;
|
| 759 |
+
transform: translateX(-50%);
|
| 760 |
+
border-bottom: 2px solid #28150c;
|
| 761 |
+
border-radius: 0 0 18px 18px;
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
.puppet.active .mouth,
|
| 765 |
+
.puppet.walking .mouth {
|
| 766 |
+
animation: speak-mouth .5s ease-in-out infinite;
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
.speech-bubble {
|
| 770 |
+
position: absolute;
|
| 771 |
+
left: 50%;
|
| 772 |
+
bottom: calc(100% + 12px);
|
| 773 |
+
z-index: 18;
|
| 774 |
+
width: 260px;
|
| 775 |
+
max-width: min(320px, calc(100vw - 32px));
|
| 776 |
+
transform: translateX(-50%);
|
| 777 |
+
padding: 10px 12px;
|
| 778 |
+
border: 1px solid rgba(255, 226, 154, .48);
|
| 779 |
+
border-radius: 6px;
|
| 780 |
+
background: rgba(255, 244, 215, .94);
|
| 781 |
+
color: #2d1b0d;
|
| 782 |
+
box-shadow: 0 14px 30px rgba(0, 0, 0, .34);
|
| 783 |
+
font-size: 12px;
|
| 784 |
+
font-weight: 700;
|
| 785 |
+
line-height: 1.3;
|
| 786 |
+
pointer-events: none;
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
.speech-bubble::after {
|
| 790 |
+
content: "";
|
| 791 |
+
position: absolute;
|
| 792 |
+
left: 50%;
|
| 793 |
+
bottom: -8px;
|
| 794 |
+
width: 14px;
|
| 795 |
+
height: 14px;
|
| 796 |
+
transform: translateX(-50%) rotate(45deg);
|
| 797 |
+
border-right: 1px solid rgba(255, 226, 154, .48);
|
| 798 |
+
border-bottom: 1px solid rgba(255, 226, 154, .48);
|
| 799 |
+
background: rgba(255, 244, 215, .94);
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
.tooltip {
|
| 803 |
+
position: absolute;
|
| 804 |
+
left: 50%;
|
| 805 |
+
bottom: calc(100% + 10px);
|
| 806 |
+
z-index: 20;
|
| 807 |
+
width: 320px;
|
| 808 |
+
max-width: min(360px, calc(100vw - 32px));
|
| 809 |
+
transform: translateX(-50%) translateY(6px);
|
| 810 |
+
opacity: 0;
|
| 811 |
+
pointer-events: none;
|
| 812 |
+
padding: 8px 10px;
|
| 813 |
+
border: 1px solid rgba(255, 226, 154, .34);
|
| 814 |
+
border-radius: 5px;
|
| 815 |
+
background: rgba(17, 9, 5, .88);
|
| 816 |
+
color: #fff0d2;
|
| 817 |
+
box-shadow: 0 12px 24px rgba(0,0,0,.36);
|
| 818 |
+
transition: opacity .18s ease, transform .18s ease;
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
.puppet:hover .tooltip,
|
| 822 |
+
.puppet:focus-within .tooltip,
|
| 823 |
+
.juror:hover .tooltip,
|
| 824 |
+
.juror:focus-within .tooltip {
|
| 825 |
+
opacity: 1;
|
| 826 |
+
transform: translateX(-50%) translateY(0);
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
.tooltip strong {
|
| 830 |
+
display: block;
|
| 831 |
+
color: #fff6df;
|
| 832 |
+
font-size: 13px;
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
.tooltip p {
|
| 836 |
+
margin: 6px 0 0;
|
| 837 |
+
color: #f5dfb5;
|
| 838 |
+
font-size: 11px;
|
| 839 |
+
line-height: 1.28;
|
| 840 |
+
white-space: normal;
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
.tooltip-meta {
|
| 844 |
+
margin-top: 3px;
|
| 845 |
+
color: #f4d58f;
|
| 846 |
+
font-size: 10px;
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
.tooltip-io-label,
|
| 850 |
+
.thread-label {
|
| 851 |
+
display: block;
|
| 852 |
+
margin-top: 7px;
|
| 853 |
+
color: #f4d58f;
|
| 854 |
+
font: 800 10px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
|
| 855 |
+
text-transform: uppercase;
|
| 856 |
+
}
|
| 857 |
+
|
| 858 |
+
.ai-thread-modal {
|
| 859 |
+
display: none;
|
| 860 |
+
position: fixed;
|
| 861 |
+
inset: max(18px, 4vh) max(18px, 5vw);
|
| 862 |
+
z-index: 120;
|
| 863 |
+
overflow: auto;
|
| 864 |
+
padding: 20px;
|
| 865 |
+
border: 1px solid rgba(255, 226, 154, .42);
|
| 866 |
+
border-radius: 8px;
|
| 867 |
+
background: rgba(18, 9, 5, .96);
|
| 868 |
+
color: #fff0d2;
|
| 869 |
+
box-shadow: 0 24px 70px rgba(0, 0, 0, .58);
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.ai-thread-modal:target {
|
| 873 |
+
display: block;
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
.thread-close {
|
| 877 |
+
position: sticky;
|
| 878 |
+
top: 0;
|
| 879 |
+
float: right;
|
| 880 |
+
padding: 7px 10px;
|
| 881 |
+
border: 1px solid rgba(255, 226, 154, .38);
|
| 882 |
+
border-radius: 4px;
|
| 883 |
+
background: rgba(255, 226, 154, .12);
|
| 884 |
+
color: #fff0d2;
|
| 885 |
+
text-decoration: none;
|
| 886 |
+
font: 800 11px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
|
| 887 |
+
}
|
| 888 |
+
|
| 889 |
+
.thread-title {
|
| 890 |
+
margin: 0 0 4px;
|
| 891 |
+
color: #fff6df;
|
| 892 |
+
font-size: 22px;
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
.thread-subtitle {
|
| 896 |
+
margin: 0 0 16px;
|
| 897 |
+
color: #f4d58f;
|
| 898 |
+
font: 800 12px/1.3 ui-monospace, SFMono-Regular, Consolas, monospace;
|
| 899 |
+
text-transform: uppercase;
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
.thread-turn {
|
| 903 |
+
margin: 0 0 18px;
|
| 904 |
+
padding-bottom: 16px;
|
| 905 |
+
border-bottom: 1px solid rgba(255, 226, 154, .16);
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
.thread-turn:last-child {
|
| 909 |
+
border-bottom: 0;
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
.thread-block {
|
| 913 |
+
margin: 7px 0 0;
|
| 914 |
+
white-space: pre-wrap;
|
| 915 |
+
color: #f8dfaa;
|
| 916 |
+
font: 12px/1.42 ui-monospace, SFMono-Regular, Consolas, monospace;
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
.juror {
|
| 920 |
+
--face: #c89259;
|
| 921 |
+
--juror-image: none;
|
| 922 |
+
position: relative;
|
| 923 |
+
height: 72px;
|
| 924 |
+
transform-origin: center bottom;
|
| 925 |
+
filter: none;
|
| 926 |
+
color: inherit;
|
| 927 |
+
text-decoration: none;
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
.juror.active {
|
| 931 |
+
animation: juror-react .82s ease-in-out infinite alternate;
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
.juror .speech-bubble {
|
| 935 |
+
bottom: calc(100% + 6px);
|
| 936 |
+
width: 230px;
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
.juror-face {
|
| 940 |
+
position: absolute;
|
| 941 |
+
left: 50%;
|
| 942 |
+
top: 0;
|
| 943 |
+
width: 30px;
|
| 944 |
+
height: 30px;
|
| 945 |
+
transform: translateX(-50%);
|
| 946 |
+
border: 2px solid rgba(255, 232, 174, .5);
|
| 947 |
+
border-radius: 50%;
|
| 948 |
+
background:
|
| 949 |
+
radial-gradient(circle at 35% 40%, #1d1109 0 2px, transparent 2.5px),
|
| 950 |
+
radial-gradient(circle at 64% 40%, #1d1109 0 2px, transparent 2.5px),
|
| 951 |
+
linear-gradient(180deg, var(--face), #835235);
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
.juror-face::after {
|
| 955 |
+
content: "";
|
| 956 |
+
position: absolute;
|
| 957 |
+
left: 10px;
|
| 958 |
+
bottom: 9px;
|
| 959 |
+
width: 14px;
|
| 960 |
+
height: 7px;
|
| 961 |
+
border-bottom: 2px solid #25140c;
|
| 962 |
+
border-radius: 0 0 18px 18px;
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
.juror-portrait {
|
| 966 |
+
position: absolute;
|
| 967 |
+
left: 50%;
|
| 968 |
+
top: -19px;
|
| 969 |
+
z-index: 3;
|
| 970 |
+
width: 58px;
|
| 971 |
+
height: 74px;
|
| 972 |
+
transform: translateX(-50%);
|
| 973 |
+
object-fit: contain;
|
| 974 |
+
pointer-events: none;
|
| 975 |
+
}
|
| 976 |
+
|
| 977 |
+
.juror-body {
|
| 978 |
+
position: absolute;
|
| 979 |
+
left: 50%;
|
| 980 |
+
top: 32px;
|
| 981 |
+
width: 36px;
|
| 982 |
+
height: 36px;
|
| 983 |
+
transform: translateX(-50%);
|
| 984 |
+
border-radius: 20px 20px 7px 7px;
|
| 985 |
+
border: 1px solid rgba(255, 232, 174, .18);
|
| 986 |
+
background: linear-gradient(180deg, #5b496f, #211726);
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
.phase-deliberation .juror:nth-child(odd) {
|
| 990 |
+
animation-delay: .18s;
|
| 991 |
+
}
|
| 992 |
+
|
| 993 |
+
.evidence-props {
|
| 994 |
+
position: absolute;
|
| 995 |
+
left: 55%;
|
| 996 |
+
right: 11%;
|
| 997 |
+
bottom: 36%;
|
| 998 |
+
z-index: 9;
|
| 999 |
+
display: flex;
|
| 1000 |
+
flex-wrap: wrap;
|
| 1001 |
+
gap: 8px;
|
| 1002 |
+
justify-content: center;
|
| 1003 |
+
pointer-events: auto;
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
.evidence-sheet {
|
| 1007 |
+
width: 96px;
|
| 1008 |
+
min-height: 72px;
|
| 1009 |
+
padding: 8px;
|
| 1010 |
+
transform: rotate(var(--tilt));
|
| 1011 |
+
border: 1px solid rgba(56, 32, 15, .22);
|
| 1012 |
+
border-radius: 3px;
|
| 1013 |
+
background:
|
| 1014 |
+
linear-gradient(135deg, transparent 0 84%, rgba(64, 38, 20, .18) 85%),
|
| 1015 |
+
#fff6df;
|
| 1016 |
+
color: #372212;
|
| 1017 |
+
box-shadow: 0 10px 20px rgba(0,0,0,.28);
|
| 1018 |
+
opacity: .18;
|
| 1019 |
+
transition: transform .25s ease, opacity .25s ease;
|
| 1020 |
+
}
|
| 1021 |
+
|
| 1022 |
+
.phase-evidence .evidence-sheet,
|
| 1023 |
+
.phase-questions .evidence-sheet,
|
| 1024 |
+
.phase-deliberation .evidence-sheet,
|
| 1025 |
+
.phase-verdict .evidence-sheet,
|
| 1026 |
+
.phase-appeal .evidence-sheet {
|
| 1027 |
+
opacity: .96;
|
| 1028 |
+
animation: paper-land .55s ease-out both;
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
.evidence-sheet:hover {
|
| 1032 |
+
transform: rotate(0) translateY(-8px) scale(1.08);
|
| 1033 |
+
z-index: 15;
|
| 1034 |
+
}
|
| 1035 |
+
|
| 1036 |
+
.evidence-sheet strong {
|
| 1037 |
+
display: block;
|
| 1038 |
+
margin-bottom: 4px;
|
| 1039 |
+
color: #254f7a;
|
| 1040 |
+
font: 800 12px/1 ui-monospace, SFMono-Regular, Consolas, monospace;
|
| 1041 |
+
}
|
| 1042 |
+
|
| 1043 |
+
.evidence-sheet span {
|
| 1044 |
+
display: block;
|
| 1045 |
+
font-size: 11px;
|
| 1046 |
+
line-height: 1.2;
|
| 1047 |
+
}
|
| 1048 |
+
|
| 1049 |
+
.trial-caption {
|
| 1050 |
+
position: absolute;
|
| 1051 |
+
left: 50%;
|
| 1052 |
+
bottom: 108px;
|
| 1053 |
+
z-index: 14;
|
| 1054 |
+
width: min(870px, calc(100% - 44px));
|
| 1055 |
+
transform: translateX(-50%);
|
| 1056 |
+
padding: 12px 16px 13px;
|
| 1057 |
+
border: 1px solid rgba(255, 226, 154, .34);
|
| 1058 |
+
border-radius: 6px;
|
| 1059 |
+
background: rgba(13, 7, 4, .78);
|
| 1060 |
+
backdrop-filter: blur(12px);
|
| 1061 |
+
box-shadow: 0 18px 36px rgba(0,0,0,.38);
|
| 1062 |
+
}
|
| 1063 |
+
|
| 1064 |
+
.caption-title {
|
| 1065 |
+
margin-top: 3px;
|
| 1066 |
+
color: #fff3d7;
|
| 1067 |
+
font-size: 20px;
|
| 1068 |
+
font-weight: 800;
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
.caption-body {
|
| 1072 |
+
margin-top: 5px;
|
| 1073 |
+
color: #f8dfaa;
|
| 1074 |
+
font-size: 14px;
|
| 1075 |
+
line-height: 1.36;
|
| 1076 |
+
white-space: pre-wrap;
|
| 1077 |
+
}
|
| 1078 |
+
|
| 1079 |
+
.decree-ribbon {
|
| 1080 |
+
position: absolute;
|
| 1081 |
+
right: 26px;
|
| 1082 |
+
top: 22px;
|
| 1083 |
+
z-index: 10;
|
| 1084 |
+
max-width: 230px;
|
| 1085 |
+
padding: 9px 11px;
|
| 1086 |
+
border: 1px solid rgba(255, 226, 154, .26);
|
| 1087 |
+
border-radius: 5px;
|
| 1088 |
+
background: rgba(18, 9, 5, .68);
|
| 1089 |
+
color: #ffe6ae;
|
| 1090 |
+
font: 800 11px/1.35 ui-monospace, SFMono-Regular, Consolas, monospace;
|
| 1091 |
+
text-transform: uppercase;
|
| 1092 |
+
}
|
| 1093 |
+
|
| 1094 |
+
.phase-verdict .judge-dais,
|
| 1095 |
+
.phase-questions .judge-dais {
|
| 1096 |
+
animation: bench-lean .9s ease-in-out infinite alternate;
|
| 1097 |
+
}
|
| 1098 |
+
|
| 1099 |
+
.phase-deliberation .jury-benches {
|
| 1100 |
+
animation: jury-murmur .7s ease-in-out infinite alternate;
|
| 1101 |
+
}
|
| 1102 |
+
|
| 1103 |
+
.stage-prop-link {
|
| 1104 |
+
cursor: help;
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
+
.drawer-shell {
|
| 1108 |
+
max-width: 1500px;
|
| 1109 |
+
margin: 12px auto 0;
|
| 1110 |
+
}
|
| 1111 |
+
|
| 1112 |
+
.drawer-grid {
|
| 1113 |
+
display: grid;
|
| 1114 |
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
| 1115 |
+
gap: 18px 28px;
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
.drawer-text-stack {
|
| 1119 |
+
color: var(--ink);
|
| 1120 |
+
line-height: 1.42;
|
| 1121 |
+
}
|
| 1122 |
+
|
| 1123 |
+
.drawer-text-block {
|
| 1124 |
+
color: var(--ink);
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
.drawer-text-block h4 {
|
| 1128 |
+
margin: 5px 0 7px;
|
| 1129 |
+
}
|
| 1130 |
+
|
| 1131 |
+
.drawer-text-block p,
|
| 1132 |
+
.drawer-empty {
|
| 1133 |
+
margin: 0 0 8px;
|
| 1134 |
+
line-height: 1.38;
|
| 1135 |
+
white-space: pre-wrap;
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
.vote-liable { color: var(--red); font-weight: 800; }
|
| 1139 |
+
.vote-not_liable { color: var(--green); font-weight: 800; }
|
| 1140 |
+
.vote-uncertain { color: var(--blue); font-weight: 800; }
|
| 1141 |
+
|
| 1142 |
+
.mind-text {
|
| 1143 |
+
max-height: 340px;
|
| 1144 |
+
overflow: auto;
|
| 1145 |
+
color: var(--ink);
|
| 1146 |
+
font: 12px/1.42 ui-monospace, SFMono-Regular, Consolas, monospace;
|
| 1147 |
+
white-space: pre-wrap;
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
@keyframes puppet-breathe {
|
| 1151 |
+
0%, 100% { transform: translate(-50%, -100%) translateY(0); }
|
| 1152 |
+
50% { transform: translate(-50%, -100%) translateY(-4px); }
|
| 1153 |
+
}
|
| 1154 |
+
|
| 1155 |
+
@keyframes lawyer-walk {
|
| 1156 |
+
0%, 100% { transform: translate(-50%, -100%) translateY(0) rotate(-1deg); }
|
| 1157 |
+
25% { transform: translate(-50%, -100%) translateY(-7px) rotate(2deg); }
|
| 1158 |
+
50% { transform: translate(-50%, -100%) translateY(0) rotate(1deg); }
|
| 1159 |
+
75% { transform: translate(-50%, -100%) translateY(-5px) rotate(-2deg); }
|
| 1160 |
+
}
|
| 1161 |
+
|
| 1162 |
+
@keyframes speak-mouth {
|
| 1163 |
+
0%, 100% { height: 5px; border-radius: 0 0 18px 18px; }
|
| 1164 |
+
50% { height: 10px; border-radius: 50%; border: 2px solid #28150c; }
|
| 1165 |
+
}
|
| 1166 |
+
|
| 1167 |
+
@keyframes juror-react {
|
| 1168 |
+
from { transform: translateY(0) rotate(-1deg); }
|
| 1169 |
+
to { transform: translateY(-5px) rotate(2deg); }
|
| 1170 |
+
}
|
| 1171 |
+
|
| 1172 |
+
@keyframes evidence-focus {
|
| 1173 |
+
0%, 100% { transform: translate(-50%, -100%) translateY(0) scale(1); }
|
| 1174 |
+
50% { transform: translate(-50%, -100%) translateY(-6px) scale(1.035); }
|
| 1175 |
+
}
|
| 1176 |
+
|
| 1177 |
+
@keyframes paper-land {
|
| 1178 |
+
from { transform: rotate(var(--tilt)) translateY(-18px); opacity: 0; }
|
| 1179 |
+
to { transform: rotate(var(--tilt)) translateY(0); opacity: .96; }
|
| 1180 |
+
}
|
| 1181 |
+
|
| 1182 |
+
@keyframes bench-lean {
|
| 1183 |
+
from { transform: translateX(-50%) translateY(0); }
|
| 1184 |
+
to { transform: translateX(-50%) translateY(-5px); }
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
@keyframes jury-murmur {
|
| 1188 |
+
from { transform: translateX(0); }
|
| 1189 |
+
to { transform: translateX(-3px); }
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
@keyframes gavel-hit {
|
| 1193 |
+
0% { transform: rotate(-18deg) translateY(0); }
|
| 1194 |
+
45% { transform: rotate(21deg) translateY(18px); }
|
| 1195 |
+
100% { transform: rotate(-18deg) translateY(0); }
|
| 1196 |
+
}
|
| 1197 |
+
|
| 1198 |
+
@media (max-width: 820px) {
|
| 1199 |
+
.docket-book-controls {
|
| 1200 |
+
position: fixed;
|
| 1201 |
+
top: 262px;
|
| 1202 |
+
width: calc(100vw - 52px);
|
| 1203 |
+
transform: translateX(-50%) rotate(-1deg);
|
| 1204 |
+
}
|
| 1205 |
+
|
| 1206 |
+
.court-episode-stage {
|
| 1207 |
+
height: 1280px;
|
| 1208 |
+
min-height: 1280px;
|
| 1209 |
+
}
|
| 1210 |
+
|
| 1211 |
+
.episode-room {
|
| 1212 |
+
background-position: center top;
|
| 1213 |
+
}
|
| 1214 |
+
|
| 1215 |
+
.episode-title {
|
| 1216 |
+
left: 16px;
|
| 1217 |
+
right: 16px;
|
| 1218 |
+
max-width: none;
|
| 1219 |
+
}
|
| 1220 |
+
|
| 1221 |
+
.decree-ribbon {
|
| 1222 |
+
top: 164px;
|
| 1223 |
+
left: 16px;
|
| 1224 |
+
right: auto;
|
| 1225 |
+
max-width: calc(100% - 32px);
|
| 1226 |
+
}
|
| 1227 |
+
|
| 1228 |
+
.episode-book {
|
| 1229 |
+
top: 220px;
|
| 1230 |
+
width: min(680px, calc(100% - 20px));
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
.episode-book.closed {
|
| 1234 |
+
top: 430px;
|
| 1235 |
+
width: 210px;
|
| 1236 |
+
}
|
| 1237 |
+
|
| 1238 |
+
.book-open-content {
|
| 1239 |
+
grid-template-columns: 1fr;
|
| 1240 |
+
gap: 10px;
|
| 1241 |
+
inset: 17% 12% 14%;
|
| 1242 |
+
padding: 0 18px;
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
.book-open-content h2 {
|
| 1246 |
+
font-size: 22px;
|
| 1247 |
+
margin-bottom: 5px;
|
| 1248 |
+
}
|
| 1249 |
+
|
| 1250 |
+
.book-open-content p,
|
| 1251 |
+
.book-entry {
|
| 1252 |
+
font-size: 12px;
|
| 1253 |
+
line-height: 1.22;
|
| 1254 |
+
}
|
| 1255 |
+
|
| 1256 |
+
.book-entry {
|
| 1257 |
+
margin: 5px 0;
|
| 1258 |
+
}
|
| 1259 |
+
|
| 1260 |
+
.judge-dais {
|
| 1261 |
+
top: 390px;
|
| 1262 |
+
width: 280px;
|
| 1263 |
+
}
|
| 1264 |
+
|
| 1265 |
+
.counsel-table.left {
|
| 1266 |
+
left: 7%;
|
| 1267 |
+
bottom: 470px;
|
| 1268 |
+
}
|
| 1269 |
+
|
| 1270 |
+
.counsel-table.right {
|
| 1271 |
+
right: 7%;
|
| 1272 |
+
bottom: 470px;
|
| 1273 |
+
}
|
| 1274 |
+
|
| 1275 |
+
.counsel-table {
|
| 1276 |
+
width: 154px;
|
| 1277 |
+
}
|
| 1278 |
+
|
| 1279 |
+
.puppet.auric {
|
| 1280 |
+
left: 20%;
|
| 1281 |
+
top: 650px;
|
| 1282 |
+
}
|
| 1283 |
+
|
| 1284 |
+
.puppet.sable {
|
| 1285 |
+
left: 80%;
|
| 1286 |
+
top: 650px;
|
| 1287 |
+
}
|
| 1288 |
+
|
| 1289 |
+
.speaker-auric .puppet.auric {
|
| 1290 |
+
left: 42%;
|
| 1291 |
+
top: 730px;
|
| 1292 |
+
}
|
| 1293 |
+
|
| 1294 |
+
.speaker-sable .puppet.sable {
|
| 1295 |
+
left: 58%;
|
| 1296 |
+
top: 730px;
|
| 1297 |
+
}
|
| 1298 |
+
|
| 1299 |
+
.puppet.clerk {
|
| 1300 |
+
left: 35%;
|
| 1301 |
+
top: 560px;
|
| 1302 |
+
}
|
| 1303 |
+
|
| 1304 |
+
.puppet.auditor {
|
| 1305 |
+
left: 78%;
|
| 1306 |
+
top: 540px;
|
| 1307 |
+
}
|
| 1308 |
+
|
| 1309 |
+
.witness-area {
|
| 1310 |
+
right: 5%;
|
| 1311 |
+
bottom: 580px;
|
| 1312 |
+
width: 138px;
|
| 1313 |
+
}
|
| 1314 |
+
|
| 1315 |
+
.jury-benches {
|
| 1316 |
+
top: 520px;
|
| 1317 |
+
width: 126px;
|
| 1318 |
+
min-width: 126px;
|
| 1319 |
+
}
|
| 1320 |
+
|
| 1321 |
+
.jury-benches.left {
|
| 1322 |
+
left: 5%;
|
| 1323 |
+
}
|
| 1324 |
+
|
| 1325 |
+
.jury-benches.right {
|
| 1326 |
+
right: 5%;
|
| 1327 |
+
}
|
| 1328 |
+
|
| 1329 |
+
.foreground-fence {
|
| 1330 |
+
bottom: -2px;
|
| 1331 |
+
width: 64%;
|
| 1332 |
+
}
|
| 1333 |
+
|
| 1334 |
+
.foreground-fence.fence-left {
|
| 1335 |
+
left: -17%;
|
| 1336 |
+
}
|
| 1337 |
+
|
| 1338 |
+
.foreground-fence.fence-right {
|
| 1339 |
+
right: -17%;
|
| 1340 |
+
}
|
| 1341 |
+
|
| 1342 |
+
.judge-table-foreground {
|
| 1343 |
+
top: 405px;
|
| 1344 |
+
width: 760px;
|
| 1345 |
+
}
|
| 1346 |
+
|
| 1347 |
+
.evidence-props {
|
| 1348 |
+
left: 8%;
|
| 1349 |
+
right: 8%;
|
| 1350 |
+
bottom: 410px;
|
| 1351 |
+
}
|
| 1352 |
+
|
| 1353 |
+
.trial-caption {
|
| 1354 |
+
bottom: 105px;
|
| 1355 |
+
}
|
| 1356 |
+
|
| 1357 |
+
.gallery-benches {
|
| 1358 |
+
bottom: 42px;
|
| 1359 |
+
grid-template-columns: repeat(3, 1fr);
|
| 1360 |
+
}
|
| 1361 |
+
}
|
| 1362 |
+
"""
|
| 1363 |
+
|
| 1364 |
+
APP_JS = f"""
|
| 1365 |
+
() => {{
|
| 1366 |
+
const paths = {json.dumps(AUDIO_PATHS)};
|
| 1367 |
+
const SCORE_BASE_VOLUME = 0.16;
|
| 1368 |
+
const SCORE_QUIET_VOLUME = 0.035;
|
| 1369 |
+
const SCORE_BREATH_INTERVAL_MS = 20000;
|
| 1370 |
+
const SCORE_BREATH_DURATION_MS = 5000;
|
| 1371 |
+
const make = (name, volume = 1, loop = false) => {{
|
| 1372 |
+
const audio = new Audio(paths[name]);
|
| 1373 |
+
audio.preload = 'auto';
|
| 1374 |
+
audio.volume = volume;
|
| 1375 |
+
audio.loop = loop;
|
| 1376 |
+
return audio;
|
| 1377 |
+
}};
|
| 1378 |
+
|
| 1379 |
+
if (!window.SovereignCourtAudio) {{
|
| 1380 |
+
const controller = {{
|
| 1381 |
+
unlocked: false,
|
| 1382 |
+
lastPhase: null,
|
| 1383 |
+
muted: false,
|
| 1384 |
+
scoreVolume: SCORE_BASE_VOLUME,
|
| 1385 |
+
crowdVolume: 0.0,
|
| 1386 |
+
fadeFrame: null,
|
| 1387 |
+
breathTimer: null,
|
| 1388 |
+
score: make('score', SCORE_BASE_VOLUME, true),
|
| 1389 |
+
crowd: make('crowd', 0.0, true),
|
| 1390 |
+
begin() {{
|
| 1391 |
+
this.unlocked = true;
|
| 1392 |
+
this.ensureLooping();
|
| 1393 |
+
this.startBreathing();
|
| 1394 |
+
this.play('select', 0.26);
|
| 1395 |
+
window.setTimeout(() => this.play('paper_long', 0.45), 120);
|
| 1396 |
+
window.setTimeout(() => this.play('gavel', 0.72), 520);
|
| 1397 |
+
this.observePhase();
|
| 1398 |
+
this.updateToggle();
|
| 1399 |
+
}},
|
| 1400 |
+
ensureLooping() {{
|
| 1401 |
+
if (!this.unlocked || this.muted) return;
|
| 1402 |
+
this.applyLoopVolumes();
|
| 1403 |
+
this.score.play().catch(() => {{}});
|
| 1404 |
+
this.crowd.play().catch(() => {{}});
|
| 1405 |
+
}},
|
| 1406 |
+
applyLoopVolumes() {{
|
| 1407 |
+
this.score.volume = this.muted ? 0 : this.scoreVolume;
|
| 1408 |
+
this.crowd.volume = this.muted ? 0 : this.crowdVolume;
|
| 1409 |
+
}},
|
| 1410 |
+
play(name, volume = 1) {{
|
| 1411 |
+
if (!this.unlocked || this.muted) return;
|
| 1412 |
+
const cue = make(name, volume, false);
|
| 1413 |
+
cue.play().catch(() => {{}});
|
| 1414 |
+
}},
|
| 1415 |
+
setCrowd(volume) {{
|
| 1416 |
+
this.crowdVolume = volume;
|
| 1417 |
+
this.applyLoopVolumes();
|
| 1418 |
+
}},
|
| 1419 |
+
fadeScore(toVolume, duration, onComplete) {{
|
| 1420 |
+
if (this.fadeFrame) window.cancelAnimationFrame(this.fadeFrame);
|
| 1421 |
+
const fromVolume = this.scoreVolume;
|
| 1422 |
+
const started = window.performance.now();
|
| 1423 |
+
const step = (now) => {{
|
| 1424 |
+
const progress = Math.min(1, (now - started) / duration);
|
| 1425 |
+
this.scoreVolume = fromVolume + ((toVolume - fromVolume) * progress);
|
| 1426 |
+
this.applyLoopVolumes();
|
| 1427 |
+
if (progress < 1) {{
|
| 1428 |
+
this.fadeFrame = window.requestAnimationFrame(step);
|
| 1429 |
+
}} else {{
|
| 1430 |
+
this.fadeFrame = null;
|
| 1431 |
+
if (onComplete) onComplete();
|
| 1432 |
+
}}
|
| 1433 |
+
}};
|
| 1434 |
+
this.fadeFrame = window.requestAnimationFrame(step);
|
| 1435 |
+
}},
|
| 1436 |
+
breatheScore() {{
|
| 1437 |
+
if (!this.unlocked) return;
|
| 1438 |
+
const halfDuration = SCORE_BREATH_DURATION_MS / 2;
|
| 1439 |
+
this.fadeScore(SCORE_QUIET_VOLUME, halfDuration, () => {{
|
| 1440 |
+
this.fadeScore(SCORE_BASE_VOLUME, halfDuration);
|
| 1441 |
+
}});
|
| 1442 |
+
}},
|
| 1443 |
+
startBreathing() {{
|
| 1444 |
+
if (this.breathTimer) return;
|
| 1445 |
+
this.breathTimer = window.setInterval(() => this.breatheScore(), SCORE_BREATH_INTERVAL_MS);
|
| 1446 |
+
}},
|
| 1447 |
+
toggleMuted() {{
|
| 1448 |
+
this.muted = !this.muted;
|
| 1449 |
+
if (this.muted) {{
|
| 1450 |
+
this.applyLoopVolumes();
|
| 1451 |
+
this.score.pause();
|
| 1452 |
+
this.crowd.pause();
|
| 1453 |
+
}} else {{
|
| 1454 |
+
this.ensureLooping();
|
| 1455 |
+
}}
|
| 1456 |
+
this.updateToggle();
|
| 1457 |
+
}},
|
| 1458 |
+
updateToggle() {{
|
| 1459 |
+
document.querySelectorAll('.sound-toggle').forEach((button) => {{
|
| 1460 |
+
button.classList.toggle('muted', this.muted);
|
| 1461 |
+
button.setAttribute('aria-pressed', String(this.muted));
|
| 1462 |
+
button.setAttribute('title', this.muted ? 'Sound off' : 'Sound on');
|
| 1463 |
+
}});
|
| 1464 |
+
}},
|
| 1465 |
+
cuePhase(phase) {{
|
| 1466 |
+
if (!this.unlocked || !phase || phase === this.lastPhase) return;
|
| 1467 |
+
this.lastPhase = phase;
|
| 1468 |
+
if (phase === 'intake') {{
|
| 1469 |
+
this.setCrowd(0.08);
|
| 1470 |
+
this.play('paper', 0.45);
|
| 1471 |
+
this.play('wood', 0.42);
|
| 1472 |
+
}} else if (phase === 'claims' || phase === 'opening') {{
|
| 1473 |
+
this.setCrowd(0.045);
|
| 1474 |
+
this.play('steps', 0.33);
|
| 1475 |
+
}} else if (phase === 'evidence') {{
|
| 1476 |
+
this.setCrowd(0.035);
|
| 1477 |
+
this.play('paper_long', 0.52);
|
| 1478 |
+
}} else if (phase === 'questions') {{
|
| 1479 |
+
this.setCrowd(0.02);
|
| 1480 |
+
this.play('wood', 0.34);
|
| 1481 |
+
}} else if (phase === 'deliberation') {{
|
| 1482 |
+
this.setCrowd(0.18);
|
| 1483 |
+
}} else if (phase === 'verdict') {{
|
| 1484 |
+
this.setCrowd(0.0);
|
| 1485 |
+
this.play('judgement', 0.66);
|
| 1486 |
+
window.setTimeout(() => this.play('gavel', 0.9), 650);
|
| 1487 |
+
}} else if (phase === 'appeal') {{
|
| 1488 |
+
this.setCrowd(0.035);
|
| 1489 |
+
this.play('paper_long', 0.5);
|
| 1490 |
+
}}
|
| 1491 |
+
}},
|
| 1492 |
+
observePhase() {{
|
| 1493 |
+
const stage = document.querySelector('.court-episode-stage');
|
| 1494 |
+
if (stage) this.cuePhase(stage.dataset.phase);
|
| 1495 |
+
this.updateToggle();
|
| 1496 |
+
}}
|
| 1497 |
+
}};
|
| 1498 |
+
|
| 1499 |
+
window.SovereignCourtAudio = controller;
|
| 1500 |
+
|
| 1501 |
+
const observer = new MutationObserver(() => controller.observePhase());
|
| 1502 |
+
observer.observe(document.body, {{ childList: true, subtree: true, attributes: true, attributeFilter: ['data-phase'] }});
|
| 1503 |
+
|
| 1504 |
+
document.addEventListener('click', (event) => {{
|
| 1505 |
+
const toggle = event.target.closest('.sound-toggle');
|
| 1506 |
+
if (toggle) {{
|
| 1507 |
+
event.preventDefault();
|
| 1508 |
+
controller.toggleMuted();
|
| 1509 |
+
return;
|
| 1510 |
+
}}
|
| 1511 |
+
if (event.target.closest('.docket-book-controls')) {{
|
| 1512 |
+
controller.play('select', 0.22);
|
| 1513 |
+
}}
|
| 1514 |
+
}}, true);
|
| 1515 |
+
}}
|
| 1516 |
+
}}
|
| 1517 |
+
"""
|
| 1518 |
+
|
| 1519 |
+
APP_HEAD = f"""
|
| 1520 |
+
<script>
|
| 1521 |
+
(function() {{
|
| 1522 |
+
const installCourtAudio = {APP_JS.strip()};
|
| 1523 |
+
if (document.readyState === 'loading') {{
|
| 1524 |
+
document.addEventListener('DOMContentLoaded', installCourtAudio, {{ once: true }});
|
| 1525 |
+
}} else {{
|
| 1526 |
+
installCourtAudio();
|
| 1527 |
+
}}
|
| 1528 |
+
}})();
|
| 1529 |
+
</script>
|
| 1530 |
+
"""
|
| 1531 |
+
|
| 1532 |
+
START_JS = """
|
| 1533 |
+
(case_label, search_query, hypothetical, speed, mind_layer) => {
|
| 1534 |
+
document.body.classList.add('trial-has-started');
|
| 1535 |
+
if (window.SovereignCourtAudio) {
|
| 1536 |
+
window.SovereignCourtAudio.begin();
|
| 1537 |
+
}
|
| 1538 |
+
return [case_label, search_query, hypothetical, speed, mind_layer];
|
| 1539 |
+
}
|
| 1540 |
+
"""
|
| 1541 |
+
|
| 1542 |
+
CHARACTERS = {
|
| 1543 |
+
JUDGE_NAME: {
|
| 1544 |
+
"class": "judge",
|
| 1545 |
+
"name": JUDGE_NAME,
|
| 1546 |
+
"role": "Stoic presiding judge",
|
| 1547 |
+
"model": "gpt-oss-20b",
|
| 1548 |
+
"image": "/gradio_api/file=assets/characters/marcus-aurelius.png",
|
| 1549 |
+
},
|
| 1550 |
+
"Clerk Meridian": {
|
| 1551 |
+
"class": "clerk",
|
| 1552 |
+
"name": "Clerk Meridian",
|
| 1553 |
+
"role": "Court clerk",
|
| 1554 |
+
"model": "AgentCPM-Explore",
|
| 1555 |
+
},
|
| 1556 |
+
"Advocate Auric": {
|
| 1557 |
+
"class": "auric",
|
| 1558 |
+
"name": "Advocate Auric",
|
| 1559 |
+
"role": "Claimant advocate",
|
| 1560 |
+
"model": "gpt-oss-20b",
|
| 1561 |
+
},
|
| 1562 |
+
"Counsel Sable": {
|
| 1563 |
+
"class": "sable",
|
| 1564 |
+
"name": "Counsel Sable",
|
| 1565 |
+
"role": "Respondent advocate",
|
| 1566 |
+
"model": "gpt-oss-20b",
|
| 1567 |
+
},
|
| 1568 |
+
"Auditor Prism": {
|
| 1569 |
+
"class": "auditor",
|
| 1570 |
+
"name": "Auditor Prism",
|
| 1571 |
+
"role": "Evidence auditor",
|
| 1572 |
+
"model": "Nemotron-Orchestrator-8B",
|
| 1573 |
+
},
|
| 1574 |
+
"Nemotron Jury": {
|
| 1575 |
+
"class": "jury",
|
| 1576 |
+
"name": "Nemotron Jury",
|
| 1577 |
+
"role": "Jury panel",
|
| 1578 |
+
"model": "Nemotron-Orchestrator-8B",
|
| 1579 |
+
},
|
| 1580 |
+
}
|
| 1581 |
+
|
| 1582 |
+
JUROR_FACES = {
|
| 1583 |
+
"Karl Marx": "#d0b79c",
|
| 1584 |
+
"John Stuart Mill": "#c99b72",
|
| 1585 |
+
"Confucius": "#c49a64",
|
| 1586 |
+
"Cleopatra VII": "#b98755",
|
| 1587 |
+
"Niccolo Machiavelli": "#b88963",
|
| 1588 |
+
"Jensen Huang": "#b37758",
|
| 1589 |
+
}
|
| 1590 |
+
|
| 1591 |
+
JUROR_IMAGES = {
|
| 1592 |
+
"Karl Marx": "/gradio_api/file=assets/characters/karl-marx.png",
|
| 1593 |
+
"John Stuart Mill": "/gradio_api/file=assets/characters/john-stuart-mill.png",
|
| 1594 |
+
"Confucius": "/gradio_api/file=assets/characters/confucius.png",
|
| 1595 |
+
"Cleopatra VII": "/gradio_api/file=assets/characters/cleopatra-vii.png",
|
| 1596 |
+
"Niccolo Machiavelli": "/gradio_api/file=assets/characters/niccolo-machiavelli.png",
|
| 1597 |
+
"Jensen Huang": "/gradio_api/file=assets/characters/jensen-huang.png",
|
| 1598 |
+
}
|
| 1599 |
+
|
| 1600 |
+
PHASE_AGENTS = {
|
| 1601 |
+
"pretrial": ["Clerk Meridian"],
|
| 1602 |
+
}
|
| 1603 |
+
|
| 1604 |
+
|
| 1605 |
+
def _remote_events(request: TrialRequest) -> Iterable[TrialEvent] | None:
|
| 1606 |
+
endpoint = os.getenv("MODAL_TRIAL_URL", "").strip()
|
| 1607 |
+
if not endpoint:
|
| 1608 |
+
return None
|
| 1609 |
+
|
| 1610 |
+
def iterator() -> Iterable[TrialEvent]:
|
| 1611 |
+
with httpx.stream("POST", endpoint, json=request.model_dump(), timeout=900.0) as response:
|
| 1612 |
+
response.raise_for_status()
|
| 1613 |
+
for line in response.iter_lines():
|
| 1614 |
+
if line:
|
| 1615 |
+
yield TrialEvent.model_validate_json(line)
|
| 1616 |
+
|
| 1617 |
+
return iterator()
|
| 1618 |
+
|
| 1619 |
+
|
| 1620 |
+
def get_events(request: TrialRequest) -> Iterable[TrialEvent]:
|
| 1621 |
+
remote = _remote_events(request)
|
| 1622 |
+
if remote is not None:
|
| 1623 |
+
yield from remote
|
| 1624 |
+
return
|
| 1625 |
+
delay = {"swift": 1.4, "measured": 2.4, "ceremonial": 3.4}[request.speed]
|
| 1626 |
+
yield from stream_trial(request, delay=delay)
|
| 1627 |
+
|
| 1628 |
+
|
| 1629 |
+
def _escape(value: str) -> str:
|
| 1630 |
+
return (
|
| 1631 |
+
value.replace("&", "&")
|
| 1632 |
+
.replace("<", "<")
|
| 1633 |
+
.replace(">", ">")
|
| 1634 |
+
.replace('"', """)
|
| 1635 |
+
)
|
| 1636 |
+
|
| 1637 |
+
|
| 1638 |
+
def _latest_packet_title(events: list[TrialEvent]) -> tuple[str, str]:
|
| 1639 |
+
if not events:
|
| 1640 |
+
return (
|
| 1641 |
+
"Judge-GPT",
|
| 1642 |
+
"The gallery doors open on an AI-native courtroom. Choose a case from the docket book and begin the proceeding.",
|
| 1643 |
+
)
|
| 1644 |
+
lines = events[0].body.splitlines()
|
| 1645 |
+
title = lines[0] if lines else "Judge-GPT"
|
| 1646 |
+
subtitle = lines[1] if len(lines) > 1 else events[0].title
|
| 1647 |
+
return title, subtitle
|
| 1648 |
+
|
| 1649 |
+
|
| 1650 |
+
def _active_agents_for(event: TrialEvent | None) -> set[str]:
|
| 1651 |
+
if event is None:
|
| 1652 |
+
return set(PHASE_AGENTS["pretrial"])
|
| 1653 |
+
if not event.turns:
|
| 1654 |
+
return set()
|
| 1655 |
+
return {event.turns[0].agent}
|
| 1656 |
+
|
| 1657 |
+
|
| 1658 |
+
def _active_speaker_for(event: TrialEvent | None) -> str:
|
| 1659 |
+
if event is None:
|
| 1660 |
+
return "Clerk Meridian"
|
| 1661 |
+
if not event.turns:
|
| 1662 |
+
return ""
|
| 1663 |
+
return event.turns[0].agent
|
| 1664 |
+
|
| 1665 |
+
|
| 1666 |
+
def _speaker_class_for(speaker: str) -> str:
|
| 1667 |
+
if not speaker:
|
| 1668 |
+
return ""
|
| 1669 |
+
if speaker in CHARACTERS:
|
| 1670 |
+
return f" speaker-{CHARACTERS[speaker]['class']}"
|
| 1671 |
+
return " speaker-" + "".join(ch.lower() if ch.isalnum() else "-" for ch in speaker).strip("-")
|
| 1672 |
+
|
| 1673 |
+
|
| 1674 |
+
def _latest_turn_text(event: TrialEvent | None, agent: str) -> str:
|
| 1675 |
+
if event is None:
|
| 1676 |
+
return ""
|
| 1677 |
+
turn = next((turn for turn in event.turns if turn.agent == agent), None)
|
| 1678 |
+
if turn is None:
|
| 1679 |
+
return ""
|
| 1680 |
+
return _short_text(turn.content, 210)
|
| 1681 |
+
|
| 1682 |
+
|
| 1683 |
+
def _thread_id(name: str) -> str:
|
| 1684 |
+
return "ai-thread-" + "".join(ch.lower() if ch.isalnum() else "-" for ch in name).strip("-")
|
| 1685 |
+
|
| 1686 |
+
|
| 1687 |
+
def _turns_for_agent(events: list[TrialEvent], agent: str) -> list[dict[str, str]]:
|
| 1688 |
+
turns = []
|
| 1689 |
+
for event in events:
|
| 1690 |
+
for turn in event.turns:
|
| 1691 |
+
if turn.agent == agent:
|
| 1692 |
+
turns.append(
|
| 1693 |
+
{
|
| 1694 |
+
"phase": event.phase,
|
| 1695 |
+
"title": event.title,
|
| 1696 |
+
"role": turn.role,
|
| 1697 |
+
"model": turn.model,
|
| 1698 |
+
"confidence": f"{turn.confidence:.2f}",
|
| 1699 |
+
"input": turn.input or "Prompt unavailable for this turn.",
|
| 1700 |
+
"output": turn.content or "No output captured yet.",
|
| 1701 |
+
}
|
| 1702 |
+
)
|
| 1703 |
+
return turns
|
| 1704 |
+
|
| 1705 |
+
|
| 1706 |
+
def _thread_for_character(events: list[TrialEvent], agent: str) -> list[dict[str, str]]:
|
| 1707 |
+
if agent in JUROR_FACES:
|
| 1708 |
+
turns = _turns_for_agent(events, agent)
|
| 1709 |
+
vote = next((vote for event in reversed(events) for vote in event.votes if vote.juror == agent), None)
|
| 1710 |
+
if not turns:
|
| 1711 |
+
turns = _turns_for_agent(events, "Nemotron Jury")
|
| 1712 |
+
if vote and turns:
|
| 1713 |
+
turns = [
|
| 1714 |
+
dict(
|
| 1715 |
+
turn,
|
| 1716 |
+
output=f"{turn['output']}\n\n{agent} persona: {vote.persona}\n{agent} vote: {vote.vote}\nReason: {vote.reason}",
|
| 1717 |
+
)
|
| 1718 |
+
for turn in turns
|
| 1719 |
+
]
|
| 1720 |
+
return turns
|
| 1721 |
+
return _turns_for_agent(events, agent)
|
| 1722 |
+
|
| 1723 |
+
|
| 1724 |
+
def _short_text(value: str, limit: int = 170) -> str:
|
| 1725 |
+
squashed = " ".join(value.split())
|
| 1726 |
+
return squashed if len(squashed) <= limit else squashed[: limit - 1].rstrip() + "..."
|
| 1727 |
+
|
| 1728 |
+
|
| 1729 |
+
def _tooltip(name: str, role: str, model: str, turns: list[dict[str, str]]) -> str:
|
| 1730 |
+
latest = turns[-1] if turns else None
|
| 1731 |
+
input_preview = _short_text(latest["input"] if latest else "Waiting for this model to receive its first prompt.")
|
| 1732 |
+
output_preview = _short_text(latest["output"] if latest else "No output has been emitted yet.")
|
| 1733 |
+
return (
|
| 1734 |
+
"<span class='tooltip'>"
|
| 1735 |
+
f"<strong>{_escape(name)}</strong>"
|
| 1736 |
+
f"{_escape(role)}"
|
| 1737 |
+
f"<span class='tooltip-meta'>{_escape(model)}</span>"
|
| 1738 |
+
"<span class='tooltip-io-label'>Input</span>"
|
| 1739 |
+
f"<p>{_escape(input_preview)}</p>"
|
| 1740 |
+
"<span class='tooltip-io-label'>Output</span>"
|
| 1741 |
+
f"<p>{_escape(output_preview)}</p>"
|
| 1742 |
+
"<span class='tooltip-meta'>Click to open full thread</span>"
|
| 1743 |
+
"</span>"
|
| 1744 |
+
)
|
| 1745 |
+
|
| 1746 |
+
|
| 1747 |
+
def _thread_modal(name: str, role: str, model: str, turns: list[dict[str, str]]) -> str:
|
| 1748 |
+
body = (
|
| 1749 |
+
"".join(
|
| 1750 |
+
"<section class='thread-turn'>"
|
| 1751 |
+
f"<div class='thread-subtitle'>{_escape(turn['phase'])} / {_escape(turn['role'])} / confidence {turn['confidence']}</div>"
|
| 1752 |
+
"<span class='thread-label'>Input</span>"
|
| 1753 |
+
f"<div class='thread-block'>{_escape(turn['input'])}</div>"
|
| 1754 |
+
"<span class='thread-label'>Output</span>"
|
| 1755 |
+
f"<div class='thread-block'>{_escape(turn['output'])}</div>"
|
| 1756 |
+
"</section>"
|
| 1757 |
+
for turn in turns
|
| 1758 |
+
)
|
| 1759 |
+
or "<section class='thread-turn'><div class='thread-block'>Waiting for this model thread to appear.</div></section>"
|
| 1760 |
+
)
|
| 1761 |
+
return (
|
| 1762 |
+
f"<aside id='{_escape(_thread_id(name))}' class='ai-thread-modal'>"
|
| 1763 |
+
"<a class='thread-close' href='#court-stage'>Close</a>"
|
| 1764 |
+
f"<h2 class='thread-title'>{_escape(name)}</h2>"
|
| 1765 |
+
f"<div class='thread-subtitle'>{_escape(role)} / {_escape(model)}</div>"
|
| 1766 |
+
f"{body}</aside>"
|
| 1767 |
+
)
|
| 1768 |
+
|
| 1769 |
+
|
| 1770 |
+
def _puppet(agent: str, active_agents: set[str], phase: str, events: list[TrialEvent], latest: TrialEvent | None) -> str:
|
| 1771 |
+
meta = CHARACTERS[agent]
|
| 1772 |
+
active = " active" if agent in active_agents else ""
|
| 1773 |
+
walking = " walking" if agent in {"Advocate Auric", "Counsel Sable"} and agent in active_agents else ""
|
| 1774 |
+
small = " small" if agent in {"Clerk Meridian", "Auditor Prism"} else ""
|
| 1775 |
+
turns = _thread_for_character(events, agent)
|
| 1776 |
+
bubble = ""
|
| 1777 |
+
if agent in active_agents:
|
| 1778 |
+
speech = _latest_turn_text(latest, agent)
|
| 1779 |
+
if speech:
|
| 1780 |
+
bubble = f"<span class='speech-bubble'>{_escape(speech)}</span>"
|
| 1781 |
+
portrait = ""
|
| 1782 |
+
if meta.get("image"):
|
| 1783 |
+
portrait = (
|
| 1784 |
+
f"<img class='puppet-portrait' src='{_escape(meta['image'])}' "
|
| 1785 |
+
f"alt='{_escape(meta['name'])} bust' onerror=\"this.style.display='none'\">"
|
| 1786 |
+
)
|
| 1787 |
+
return (
|
| 1788 |
+
f"<a class='puppet {meta['class']}{active}{walking}{small}' href='#{_escape(_thread_id(agent))}' aria-label='Open {_escape(agent)} model thread'>"
|
| 1789 |
+
f"{portrait}"
|
| 1790 |
+
"<span class='mouth'></span>"
|
| 1791 |
+
f"{bubble}"
|
| 1792 |
+
f"{_tooltip(meta['name'], meta['role'], meta['model'], turns)}"
|
| 1793 |
+
"</a>"
|
| 1794 |
+
)
|
| 1795 |
+
|
| 1796 |
+
|
| 1797 |
+
def _juror(name: str, active: bool, events: list[TrialEvent] | None = None, latest: TrialEvent | None = None) -> str:
|
| 1798 |
+
face = JUROR_FACES.get(name, "#c89259")
|
| 1799 |
+
image = JUROR_IMAGES.get(name, "")
|
| 1800 |
+
active_cls = " active" if active else ""
|
| 1801 |
+
turns = _thread_for_character(events or [], name)
|
| 1802 |
+
bubble = ""
|
| 1803 |
+
if active:
|
| 1804 |
+
vote = next((vote for vote in (latest.votes if latest else []) if vote.juror == name), None)
|
| 1805 |
+
speech = _latest_turn_text(latest, name)
|
| 1806 |
+
if vote:
|
| 1807 |
+
speech = f"{vote.vote.replace('_', ' ').title()}. {vote.reason}"
|
| 1808 |
+
if speech:
|
| 1809 |
+
bubble = f"<span class='speech-bubble'>{_escape(_short_text(speech, 190))}</span>"
|
| 1810 |
+
portrait = (
|
| 1811 |
+
f"<img class='juror-portrait' src='{_escape(image)}' alt='{_escape(name)} bust' "
|
| 1812 |
+
"onerror=\"this.style.display='none'\">"
|
| 1813 |
+
if image
|
| 1814 |
+
else ""
|
| 1815 |
+
)
|
| 1816 |
+
return (
|
| 1817 |
+
f"<a class='juror{active_cls}' href='#{_escape(_thread_id(name))}' style='--face: {face}' aria-label='Open {_escape(name)} model thread'>"
|
| 1818 |
+
f"{portrait}"
|
| 1819 |
+
"<span class='juror-face'></span><span class='juror-body'></span>"
|
| 1820 |
+
f"{bubble}"
|
| 1821 |
+
f"{_tooltip(name, 'HF-style juror', 'Nemotron panel', turns)}"
|
| 1822 |
+
"</a>"
|
| 1823 |
+
)
|
| 1824 |
+
|
| 1825 |
+
|
| 1826 |
+
def _book(open_book: bool) -> str:
|
| 1827 |
+
closed = "" if open_book else " closed"
|
| 1828 |
+
return (
|
| 1829 |
+
f"<div class='episode-book{closed}'>"
|
| 1830 |
+
"<img class='book-art open-art' src='/gradio_api/file=assets/book/docket-book-open.png' alt='Open docket book'>"
|
| 1831 |
+
"<img class='book-art closed-art' src='/gradio_api/file=assets/book/docket-book-closed.png' alt='Closed docket book'>"
|
| 1832 |
+
"</div>"
|
| 1833 |
+
)
|
| 1834 |
+
|
| 1835 |
+
|
| 1836 |
+
def _caption(event: TrialEvent | None, phase: str) -> tuple[str, str, str]:
|
| 1837 |
+
if event is None:
|
| 1838 |
+
return (
|
| 1839 |
+
"PRETRIAL",
|
| 1840 |
+
"The Courtroom Is Ready",
|
| 1841 |
+
"The docket is open, the room is dimmed, and the clerk is waiting for the first call.",
|
| 1842 |
+
)
|
| 1843 |
+
body = event.body.splitlines()[0] if event.body.splitlines() else event.body
|
| 1844 |
+
return (f"{PHASE_GLYPHS[phase]} / {phase.upper()}", event.title, body)
|
| 1845 |
+
|
| 1846 |
+
|
| 1847 |
+
def _evidence_props(events: list[TrialEvent]) -> str:
|
| 1848 |
+
evidence = next((event.evidence for event in reversed(events) if event.evidence), [])
|
| 1849 |
+
if not evidence:
|
| 1850 |
+
return ""
|
| 1851 |
+
tilts = ["-5deg", "3deg", "-2deg", "5deg"]
|
| 1852 |
+
sheets = []
|
| 1853 |
+
for idx, item in enumerate(evidence[:4]):
|
| 1854 |
+
sheets.append(
|
| 1855 |
+
f"<div class='evidence-sheet stage-prop-link' style='--tilt: {tilts[idx % len(tilts)]}' title='{_escape(item.note)}'>"
|
| 1856 |
+
f"<strong>{_escape(item.id)}</strong>"
|
| 1857 |
+
f"<span>{_escape(item.title)}</span></div>"
|
| 1858 |
+
)
|
| 1859 |
+
return f"<div class='evidence-props'>{''.join(sheets)}</div>"
|
| 1860 |
+
|
| 1861 |
+
|
| 1862 |
+
def _foreground_props() -> str:
|
| 1863 |
+
fence = "/gradio_api/file=assets/foreground/foregroundFence.png"
|
| 1864 |
+
judge_table = "/gradio_api/file=assets/foreground/JudgeTable.png"
|
| 1865 |
+
return (
|
| 1866 |
+
"<div class='foreground-props' aria-hidden='true'>"
|
| 1867 |
+
f"<img class='foreground-fence fence-left' src='{fence}' alt=''>"
|
| 1868 |
+
f"<img class='foreground-fence fence-right' src='{fence}' alt=''>"
|
| 1869 |
+
f"<img class='judge-table-foreground' src='{judge_table}' alt=''>"
|
| 1870 |
+
"</div>"
|
| 1871 |
+
)
|
| 1872 |
+
|
| 1873 |
+
|
| 1874 |
+
def _courtroom_juror_names(votes: list) -> list[str]:
|
| 1875 |
+
names = list(JUROR_FACES)
|
| 1876 |
+
names.extend(vote.juror for vote in votes if vote.juror not in names)
|
| 1877 |
+
return names[:6]
|
| 1878 |
+
|
| 1879 |
+
|
| 1880 |
+
def _latest_votes(events: list[TrialEvent]) -> list:
|
| 1881 |
+
by_juror = {}
|
| 1882 |
+
for event in events:
|
| 1883 |
+
for vote in event.votes:
|
| 1884 |
+
by_juror[vote.juror] = vote
|
| 1885 |
+
ordered = [by_juror[name] for name in JUROR_FACES if name in by_juror]
|
| 1886 |
+
ordered.extend(vote for juror, vote in by_juror.items() if juror not in JUROR_FACES)
|
| 1887 |
+
return ordered
|
| 1888 |
+
|
| 1889 |
+
|
| 1890 |
+
def render_court(events: list[TrialEvent], started: bool = False) -> str:
|
| 1891 |
+
latest = events[-1] if events else None
|
| 1892 |
+
phase = latest.phase if latest else "pretrial"
|
| 1893 |
+
title, subtitle = _latest_packet_title(events)
|
| 1894 |
+
active_agents = _active_agents_for(latest)
|
| 1895 |
+
active_speaker = _active_speaker_for(latest)
|
| 1896 |
+
speaker_cls = _speaker_class_for(active_speaker)
|
| 1897 |
+
caption_phase, caption_title, caption_body = _caption(latest, phase)
|
| 1898 |
+
latest_votes = _latest_votes(events)
|
| 1899 |
+
juror_names = _courtroom_juror_names(latest_votes)
|
| 1900 |
+
started_cls = " trial-started" if started or events else ""
|
| 1901 |
+
book_open = not started and not events
|
| 1902 |
+
puppets = "".join(
|
| 1903 |
+
_puppet(agent, active_agents, phase, events, latest)
|
| 1904 |
+
for agent in [JUDGE_NAME, "Clerk Meridian", "Advocate Auric", "Counsel Sable", "Auditor Prism"]
|
| 1905 |
+
)
|
| 1906 |
+
left_jurors = "".join(_juror(name, name == active_speaker, events, latest) for name in juror_names[:3])
|
| 1907 |
+
right_jurors = "".join(_juror(name, name == active_speaker, events, latest) for name in juror_names[3:6])
|
| 1908 |
+
evidence_props = _evidence_props(events)
|
| 1909 |
+
thread_modals = "".join(
|
| 1910 |
+
_thread_modal(meta["name"], meta["role"], meta["model"], _thread_for_character(events, agent))
|
| 1911 |
+
for agent, meta in CHARACTERS.items()
|
| 1912 |
+
) + "".join(
|
| 1913 |
+
_thread_modal(name, "HF-style juror", "Nemotron panel", _thread_for_character(events, name))
|
| 1914 |
+
for name in juror_names
|
| 1915 |
+
)
|
| 1916 |
+
return (
|
| 1917 |
+
f"<section id='court-stage' class='court-episode-stage phase-{_escape(phase)}{_escape(speaker_cls)}{started_cls}' data-phase='{_escape(phase)}'>"
|
| 1918 |
+
"<div class='episode-room'></div>"
|
| 1919 |
+
"<div class='audio-deck' aria-hidden='true'>"
|
| 1920 |
+
+ "".join(f"<audio preload='auto' src='{_escape(src)}'></audio>" for src in AUDIO_PATHS.values())
|
| 1921 |
+
+ "</div>"
|
| 1922 |
+
"<button class='sound-toggle' type='button' aria-label='Toggle sound' aria-pressed='false' title='Sound on'>"
|
| 1923 |
+
"<span class='sound-icon' aria-hidden='true'></span></button>"
|
| 1924 |
+
"<div class='episode-title'>"
|
| 1925 |
+
"<div class='episode-kicker'>Judge-GPT Virtual Courtroom</div>"
|
| 1926 |
+
f"<h1>{_escape(title)}</h1>"
|
| 1927 |
+
f"<p>{_escape(subtitle)}</p></div>"
|
| 1928 |
+
f"<div class='decree-ribbon'>Step {len(events) if events else 0}: {caption_title}<br>Hover characters for agent and model details</div>"
|
| 1929 |
+
f"{_book(book_open)}"
|
| 1930 |
+
f"<div class='judge-dais'><div class='prop-label'>{_escape(JUDGE_NAME)}</div><div class='bench-front'></div><span class='gavel'></span></div>"
|
| 1931 |
+
"<div class='counsel-table left'><div class='prop-label'>Claimant Table</div></div>"
|
| 1932 |
+
"<div class='counsel-table right'><div class='prop-label'>Respondent Table</div></div>"
|
| 1933 |
+
"<div class='trial-floor-mark'></div>"
|
| 1934 |
+
"<div class='witness-area'><div class='prop-label'>Evidence Stand</div></div>"
|
| 1935 |
+
"<div class='jury-benches left'><div class='prop-label'>Jury Box</div><div class='jury-row'>"
|
| 1936 |
+
f"{left_jurors}</div><div class='jury-rail'></div></div>"
|
| 1937 |
+
"<div class='jury-benches right'><div class='prop-label'>Jury Box</div><div class='jury-row'>"
|
| 1938 |
+
f"{right_jurors}</div><div class='jury-rail'></div></div>"
|
| 1939 |
+
f"{puppets}"
|
| 1940 |
+
f"{evidence_props}"
|
| 1941 |
+
f"{_foreground_props()}"
|
| 1942 |
+
"<div class='gallery-benches'><div></div><div></div><div></div><div></div><div></div><div></div></div>"
|
| 1943 |
+
"<div class='trial-caption'>"
|
| 1944 |
+
f"<div class='caption-phase'>Live Trial Feed / {_escape(caption_phase)}</div>"
|
| 1945 |
+
f"<div class='caption-title'>{_escape(caption_title)}</div>"
|
| 1946 |
+
f"<div class='caption-body'>{_escape(caption_body)}</div>"
|
| 1947 |
+
"</div>"
|
| 1948 |
+
f"{thread_modals}</section>"
|
| 1949 |
+
)
|
| 1950 |
+
|
| 1951 |
+
|
| 1952 |
+
def render_evidence(events: list[TrialEvent]) -> str:
|
| 1953 |
+
evidence = next((event.evidence for event in reversed(events) if event.evidence), [])
|
| 1954 |
+
if not evidence:
|
| 1955 |
+
return "<div class='drawer-empty'>The exhibit drawer is closed until the clerk opens the docket.</div>"
|
| 1956 |
+
return (
|
| 1957 |
+
"<div class='drawer-grid drawer-text-stack'>"
|
| 1958 |
+
+ "".join(
|
| 1959 |
+
"<section class='drawer-text-block'>"
|
| 1960 |
+
f"<div class='drawer-kicker'>{_escape(item.id)} / {item.reliability:.2f}</div>"
|
| 1961 |
+
f"<h4>{_escape(item.title)}</h4>"
|
| 1962 |
+
f"<p>{_escape(item.excerpt)}</p>"
|
| 1963 |
+
f"<p><strong>Direction:</strong> {_escape(item.supports)}</p>"
|
| 1964 |
+
f"<p>{_escape(item.note)}</p></section>"
|
| 1965 |
+
for item in evidence
|
| 1966 |
+
)
|
| 1967 |
+
+ "</div>"
|
| 1968 |
+
)
|
| 1969 |
+
|
| 1970 |
+
|
| 1971 |
+
def render_jurors(events: list[TrialEvent]) -> str:
|
| 1972 |
+
votes = _latest_votes(events)
|
| 1973 |
+
if not votes:
|
| 1974 |
+
sleepers = "".join(_juror(name, False) for name in JUROR_FACES)
|
| 1975 |
+
return (
|
| 1976 |
+
"<section class='drawer-text-block drawer-text-stack'><div class='drawer-kicker'>Jury Box</div>"
|
| 1977 |
+
f"<div class='jury-row'>{sleepers}</div>"
|
| 1978 |
+
"<p>The jurors are seated and silent.</p></section>"
|
| 1979 |
+
)
|
| 1980 |
+
return (
|
| 1981 |
+
"<div class='drawer-grid drawer-text-stack'>"
|
| 1982 |
+
+ "".join(
|
| 1983 |
+
"<section class='drawer-text-block'>"
|
| 1984 |
+
f"<div class='drawer-kicker'>{_escape(vote.juror)}</div>"
|
| 1985 |
+
f"<p><strong>Persona:</strong> {_escape(vote.persona)}</p>"
|
| 1986 |
+
f"<p class='vote-{vote.vote}'>{_escape(vote.vote.replace('_', ' '))}</p>"
|
| 1987 |
+
f"<p>{_escape(vote.reason)}</p>"
|
| 1988 |
+
f"<p><strong>Evidence:</strong> {_escape(', '.join(vote.evidence_ids))}</p></section>"
|
| 1989 |
+
for vote in votes
|
| 1990 |
+
)
|
| 1991 |
+
+ "</div>"
|
| 1992 |
+
)
|
| 1993 |
+
|
| 1994 |
+
|
| 1995 |
+
def render_mind(events: list[TrialEvent], enabled: bool) -> str:
|
| 1996 |
+
if not enabled:
|
| 1997 |
+
return "<div class='mind-text'>Mind Layer hidden.</div>"
|
| 1998 |
+
if not events:
|
| 1999 |
+
return "<div class='mind-text'>Awaiting trace.</div>"
|
| 2000 |
+
compact = [
|
| 2001 |
+
{
|
| 2002 |
+
"phase": event.phase,
|
| 2003 |
+
"title": event.title,
|
| 2004 |
+
"turns": [turn.model_dump() for turn in event.turns],
|
| 2005 |
+
"trace": event.trace,
|
| 2006 |
+
}
|
| 2007 |
+
for event in events
|
| 2008 |
+
]
|
| 2009 |
+
return f"<pre class='mind-text'>{_escape(json.dumps(compact, indent=2))}</pre>"
|
| 2010 |
+
|
| 2011 |
+
|
| 2012 |
+
def run_ui(case_label: str, search_query: str, hypothetical: str, speed: str, mind_layer: bool):
|
| 2013 |
+
request = TrialRequest(
|
| 2014 |
+
case_id=CASE_OPTIONS.get(case_label, "socrates"),
|
| 2015 |
+
search_query=search_query or "",
|
| 2016 |
+
hypothetical=hypothetical or "",
|
| 2017 |
+
speed=speed or "swift",
|
| 2018 |
+
mind_layer=bool(mind_layer),
|
| 2019 |
+
)
|
| 2020 |
+
events: list[TrialEvent] = []
|
| 2021 |
+
yield (
|
| 2022 |
+
render_court(events, started=True),
|
| 2023 |
+
render_evidence(events),
|
| 2024 |
+
render_jurors(events),
|
| 2025 |
+
render_mind(events, mind_layer),
|
| 2026 |
+
"The docket closes and the bailiff calls the room to order.",
|
| 2027 |
+
)
|
| 2028 |
+
try:
|
| 2029 |
+
for event in get_events(request):
|
| 2030 |
+
events.append(event)
|
| 2031 |
+
status = f"Step {len(events)}: {event.title}"
|
| 2032 |
+
yield (
|
| 2033 |
+
render_court(events, started=True),
|
| 2034 |
+
render_evidence(events),
|
| 2035 |
+
render_jurors(events),
|
| 2036 |
+
render_mind(events, mind_layer),
|
| 2037 |
+
status,
|
| 2038 |
+
)
|
| 2039 |
+
except Exception as exc:
|
| 2040 |
+
yield (
|
| 2041 |
+
render_court(events, started=True),
|
| 2042 |
+
render_evidence(events),
|
| 2043 |
+
render_jurors(events),
|
| 2044 |
+
render_mind(events, mind_layer),
|
| 2045 |
+
f"Model response required. Trial stopped: {exc}",
|
| 2046 |
+
)
|
| 2047 |
+
return
|
| 2048 |
+
yield (
|
| 2049 |
+
render_court(events, started=True),
|
| 2050 |
+
render_evidence(events),
|
| 2051 |
+
render_jurors(events),
|
| 2052 |
+
render_mind(events, mind_layer),
|
| 2053 |
+
"Verdict sealed.",
|
| 2054 |
+
)
|
| 2055 |
+
|
| 2056 |
+
|
| 2057 |
+
def build_app() -> gr.Blocks:
|
| 2058 |
+
with gr.Blocks(title="Judge-GPT") as demo:
|
| 2059 |
+
with gr.Group(elem_classes=["docket-book-controls"]):
|
| 2060 |
+
gr.HTML("<div class='book-control-heading'>DATA TRIAL:</div>")
|
| 2061 |
+
with gr.Row():
|
| 2062 |
+
case = gr.Dropdown(
|
| 2063 |
+
label="Case entry",
|
| 2064 |
+
choices=list(CASE_OPTIONS.keys()),
|
| 2065 |
+
value="Trial of Socrates",
|
| 2066 |
+
scale=2,
|
| 2067 |
+
)
|
| 2068 |
+
start = gr.Button("Begin Trial", variant="primary", scale=1)
|
| 2069 |
+
status = gr.Markdown("Ready.", elem_classes=["book-status"])
|
| 2070 |
+
courtroom = gr.HTML(render_court([]), label="Live courtroom")
|
| 2071 |
+
search = gr.State("")
|
| 2072 |
+
speed = gr.State("swift")
|
| 2073 |
+
mind = gr.State(True)
|
| 2074 |
+
with gr.Accordion("Advanced trial options", open=False, elem_classes=["trial-options"]):
|
| 2075 |
+
with gr.Row():
|
| 2076 |
+
hypo = gr.Textbox(label="Hypothetical sidebar", lines=1)
|
| 2077 |
+
with gr.Row(elem_classes=["drawer-shell"]):
|
| 2078 |
+
with gr.Column(scale=1):
|
| 2079 |
+
with gr.Tab("Evidence Drawer"):
|
| 2080 |
+
evidence = gr.HTML(render_evidence([]))
|
| 2081 |
+
with gr.Tab("Juror Panel"):
|
| 2082 |
+
jurors = gr.HTML(render_jurors([]))
|
| 2083 |
+
mind_html = gr.HTML(render_mind([], True), visible=False)
|
| 2084 |
+
start.click(
|
| 2085 |
+
run_ui,
|
| 2086 |
+
inputs=[case, search, hypo, speed, mind],
|
| 2087 |
+
outputs=[courtroom, evidence, jurors, mind_html, status],
|
| 2088 |
+
js=START_JS,
|
| 2089 |
+
)
|
| 2090 |
+
return demo
|
| 2091 |
+
|
| 2092 |
+
|
| 2093 |
+
demo = build_app()
|
| 2094 |
+
|
| 2095 |
+
if __name__ == "__main__":
|
| 2096 |
+
demo.queue().launch(
|
| 2097 |
+
show_error=True,
|
| 2098 |
+
allowed_paths=["assets"],
|
| 2099 |
+
css=CSS,
|
| 2100 |
+
head=APP_HEAD,
|
| 2101 |
+
theme=gr.themes.Soft(),
|
| 2102 |
+
)
|
assets/ATTRIBUTION.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Asset Attribution
|
| 2 |
+
|
| 3 |
+
## `courtroom-dickinson.jpg`
|
| 4 |
+
|
| 5 |
+
- Source: https://commons.wikimedia.org/wiki/File:Dickinson_Law_Courtroom.jpg
|
| 6 |
+
- Description: Penn State University, Dickinson School of Law courtroom
|
| 7 |
+
- Author: Jeremy Hess Photography
|
| 8 |
+
- License: Creative Commons CC0 1.0 Universal Public Domain Dedication
|
| 9 |
+
- Local use: cinematic courtroom background for Sovereign Bench
|
| 10 |
+
|
assets/audio/ATTRIBUTION.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Audio Attribution
|
| 2 |
+
|
| 3 |
+
All selected audio is stored locally in `assets/audio/` for the animated courtroom episode.
|
| 4 |
+
|
| 5 |
+
## Courtroom score and judgement sting
|
| 6 |
+
|
| 7 |
+
- Files: `courtroom.ogg`, `Judgement.ogg`
|
| 8 |
+
- Source: OpenGameArt, "Courtroom and Judgement"
|
| 9 |
+
- Author: Spring Spring
|
| 10 |
+
- License: CC0
|
| 11 |
+
- URL: https://opengameart.org/content/courtroom-and-judgement
|
| 12 |
+
|
| 13 |
+
## Courtroom chatter and crowd reaction
|
| 14 |
+
|
| 15 |
+
- File: `crowd_shouting.ogg`
|
| 16 |
+
- Source: OpenGameArt, "Crowd Shouting/Speaking Ambience"
|
| 17 |
+
- Author: StarNinjas
|
| 18 |
+
- License: CC0
|
| 19 |
+
- URL: https://opengameart.org/content/crowd-shoutingspeaking-ambience
|
| 20 |
+
|
| 21 |
+
## Gavel and wood hits
|
| 22 |
+
|
| 23 |
+
- Files: `wood_hammer_01.ogg`, `wood_hit_03.ogg`
|
| 24 |
+
- Source: OpenGameArt, "100 CC0 metal and wood SFX"
|
| 25 |
+
- Author: rubberduck
|
| 26 |
+
- License: CC0
|
| 27 |
+
- URL: https://opengameart.org/content/100-cc0-metal-and-wood-sfx
|
| 28 |
+
|
| 29 |
+
## Lawyer footsteps
|
| 30 |
+
|
| 31 |
+
- File: `steps_in_wood_floor.wav`
|
| 32 |
+
- Source: OpenGameArt, "Steps in wood floor"
|
| 33 |
+
- Author: mikeask
|
| 34 |
+
- License: CC0
|
| 35 |
+
- URL: https://opengameart.org/content/steps-in-wood-floor
|
| 36 |
+
|
| 37 |
+
## Book and paper movement
|
| 38 |
+
|
| 39 |
+
- Files: `paper_sound_1.mp3`, `paper_sound_4.mp3`
|
| 40 |
+
- Source: OpenGameArt, "Various Paper Sound Effects"
|
| 41 |
+
- Author: Luckius
|
| 42 |
+
- License: CC0
|
| 43 |
+
- URL: https://opengameart.org/content/various-paper-sound-effects
|
| 44 |
+
|
| 45 |
+
## Docket selection UI cue
|
| 46 |
+
|
| 47 |
+
- File: `select_001.ogg`
|
| 48 |
+
- Source: OpenGameArt, "Interface Sounds"
|
| 49 |
+
- Author: Kenney
|
| 50 |
+
- License: CC0
|
| 51 |
+
- URL: https://opengameart.org/content/interface-sounds
|
assets/audio/Judgement.ogg
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a0fb8647947708bcde1771a96cd6272653e90dd1d2823bae9581e28707fad35d
|
| 3 |
+
size 3164976
|
assets/audio/courtroom.ogg
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5ffd37d0a907c80324a51419544b5cdcb69781cc4642d9f79cff3e2ffee4a556
|
| 3 |
+
size 2666730
|
assets/audio/crowd_shouting.ogg
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a2c23a64c127c77717cbe4abb21f138bac9d9aa6cb3cc89d62bab5d0d96dd7ca
|
| 3 |
+
size 335526
|
assets/audio/paper_sound_1.mp3
ADDED
|
Binary file (12.3 kB). View file
|
|
|
assets/audio/paper_sound_4.mp3
ADDED
|
Binary file (53.5 kB). View file
|
|
|
assets/audio/select_001.ogg
ADDED
|
Binary file (5.47 kB). View file
|
|
|
assets/audio/steps_in_wood_floor.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aae50329e6b702c23a2e6e21f0eb673a0bca5aa9386fb9eb4ea6e9acdcb05536
|
| 3 |
+
size 613852
|
assets/audio/wood_hammer_01.ogg
ADDED
|
Binary file (14.2 kB). View file
|
|
|
assets/audio/wood_hit_03.ogg
ADDED
|
Binary file (16.6 kB). View file
|
|
|
assets/background/CourtRoom.png
ADDED
|
Git LFS Details
|
assets/book/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Docket Book Assets
|
| 2 |
+
|
| 3 |
+
These project-bound UI prop assets were generated with the built-in Codex image generation tool, then processed locally from a chroma-key background to transparent PNGs.
|
| 4 |
+
|
| 5 |
+
- `docket-book-open.png`: open docket book used before the trial starts.
|
| 6 |
+
- `docket-book-closed.png`: closed docket book used after the trial begins.
|
| 7 |
+
- `docket-book-open-keyed.png`: preserved chroma-key source.
|
| 8 |
+
- `docket-book-closed-keyed.png`: preserved chroma-key source.
|
| 9 |
+
|
| 10 |
+
Generation prompt summary:
|
| 11 |
+
|
| 12 |
+
- Antique legal docket book, warm parchment or dark leather, gold corner protectors, polished painterly game UI prop, centered with generous padding.
|
| 13 |
+
- No text, no logos, no watermark, no hands, no pen.
|
| 14 |
+
- Generated on a flat `#00ff00` background for local alpha extraction.
|
assets/book/docket-book-closed-keyed.png
ADDED
|
Git LFS Details
|
assets/book/docket-book-closed.png
ADDED
|
Git LFS Details
|
assets/book/docket-book-open-keyed.png
ADDED
|
Git LFS Details
|
assets/book/docket-book-open.png
ADDED
|
Git LFS Details
|
assets/characters/cleopatra-vii.png
ADDED
|
Git LFS Details
|
assets/characters/confucius.png
ADDED
|
Git LFS Details
|
assets/characters/jensen-huang.png
ADDED
|
Git LFS Details
|
assets/characters/john-stuart-mill.png
ADDED
|
Git LFS Details
|
assets/characters/karl-marx.png
ADDED
|
Git LFS Details
|
assets/characters/marcus-aurelius.png
ADDED
|
Git LFS Details
|
assets/characters/niccolo-machiavelli.png
ADDED
|
Git LFS Details
|
assets/characters/sources/cleopatra-vii-chroma.png
ADDED
|
Git LFS Details
|
assets/characters/sources/confucius-chroma.png
ADDED
|
Git LFS Details
|
assets/characters/sources/jensen-huang-chroma.png
ADDED
|
Git LFS Details
|
assets/characters/sources/john-stuart-mill-chroma.png
ADDED
|
Git LFS Details
|
assets/characters/sources/karl-marx-chroma.png
ADDED
|
Git LFS Details
|
assets/characters/sources/marcus-aurelius-chroma.png
ADDED
|
Git LFS Details
|
assets/characters/sources/niccolo-machiavelli-chroma.png
ADDED
|
Git LFS Details
|
assets/courtroom-dickinson.jpg
ADDED
|
Git LFS Details
|
assets/foreground/JudgeTable.png
ADDED
|
Git LFS Details
|
assets/foreground/foregroundFence.png
ADDED
|
Git LFS Details
|
data/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sovereign Bench Agent Trace Sample
|
| 2 |
+
|
| 3 |
+
This sample contains compact phase-level trace rows from the cached Barnaby Buttons trial. Runtime traces exported by the Gradio app include the full structured `TrialEvent` objects.
|
| 4 |
+
|
| 5 |
+
The trace is synthetic and intended for hackathon demonstration, reproducibility, and UI testing.
|
data/agent_trace_sample.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"phase": "intake",
|
| 4 |
+
"case_id": "barnaby",
|
| 5 |
+
"agent": "Clerk Meridian",
|
| 6 |
+
"model": "openbmb/AgentCPM-Explore",
|
| 7 |
+
"summary": "Opened The People v. Barnaby Buttons and recorded source provenance for cached demo reliability."
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"phase": "evidence",
|
| 11 |
+
"case_id": "barnaby",
|
| 12 |
+
"agent": "Auditor Prism",
|
| 13 |
+
"model": "nvidia/Nemotron-Orchestrator-8B",
|
| 14 |
+
"summary": "Scored ledger ink, crumb trail, calendar motive, and biscuit alibi as directional evidence with uncertainty."
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"phase": "verdict",
|
| 18 |
+
"case_id": "barnaby",
|
| 19 |
+
"agent": "Marcus Aurelius",
|
| 20 |
+
"model": "openai/gpt-oss-20b",
|
| 21 |
+
"summary": "Issued a narrow claimant finding with cited evidence IDs and an explicit uncertainty warning."
|
| 22 |
+
}
|
| 23 |
+
]
|
modal_app.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
|
| 4 |
+
import modal
|
| 5 |
+
|
| 6 |
+
from sovereign_bench.engine import stream_trial_jsonl
|
| 7 |
+
from sovereign_bench.llm import (
|
| 8 |
+
ModelCall,
|
| 9 |
+
ModelResult,
|
| 10 |
+
build_role_messages,
|
| 11 |
+
messages_hash,
|
| 12 |
+
)
|
| 13 |
+
from sovereign_bench.models import TrialRequest
|
| 14 |
+
|
| 15 |
+
app = modal.App("sovereign-bench")
|
| 16 |
+
GPU_NAME = "H100"
|
| 17 |
+
GPU_TIMEOUT_SECONDS = 20 * 60
|
| 18 |
+
HF_CACHE_DIR = "/root/.cache/huggingface"
|
| 19 |
+
|
| 20 |
+
image = (
|
| 21 |
+
modal.Image.debian_slim(python_version="3.12")
|
| 22 |
+
.pip_install("fastapi", "huggingface_hub", "httpx", "pydantic")
|
| 23 |
+
.add_local_dir("sovereign_bench", remote_path="/root/sovereign_bench")
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
model_cache = modal.Volume.from_name("sovereign-bench-model-cache", create_if_missing=True)
|
| 27 |
+
|
| 28 |
+
vllm_image = (
|
| 29 |
+
modal.Image.from_registry("nvidia/cuda:12.8.1-devel-ubuntu22.04", add_python="3.12")
|
| 30 |
+
.entrypoint([])
|
| 31 |
+
.uv_pip_install(
|
| 32 |
+
"vllm==0.18.1",
|
| 33 |
+
"huggingface_hub[hf_transfer]==0.36.0",
|
| 34 |
+
"transformers",
|
| 35 |
+
"httpx",
|
| 36 |
+
"pydantic",
|
| 37 |
+
)
|
| 38 |
+
.env(
|
| 39 |
+
{
|
| 40 |
+
"HF_HUB_ENABLE_HF_TRANSFER": "1",
|
| 41 |
+
"HF_HOME": HF_CACHE_DIR,
|
| 42 |
+
"VLLM_WORKER_MULTIPROC_METHOD": "spawn",
|
| 43 |
+
"VLLM_USE_FLASHINFER_MOE_MXFP4_MXFP8": "1",
|
| 44 |
+
}
|
| 45 |
+
)
|
| 46 |
+
.add_local_dir("sovereign_bench", remote_path="/root/sovereign_bench")
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@app.cls(
|
| 51 |
+
image=vllm_image,
|
| 52 |
+
gpu=GPU_NAME,
|
| 53 |
+
secrets=[modal.Secret.from_name("huggingface")],
|
| 54 |
+
volumes={HF_CACHE_DIR: model_cache},
|
| 55 |
+
timeout=GPU_TIMEOUT_SECONDS,
|
| 56 |
+
scaledown_window=10 * 60,
|
| 57 |
+
max_containers=3,
|
| 58 |
+
)
|
| 59 |
+
class VllmModel:
|
| 60 |
+
model_id: str = modal.parameter()
|
| 61 |
+
|
| 62 |
+
@modal.enter()
|
| 63 |
+
def load(self) -> None:
|
| 64 |
+
from vllm import LLM, SamplingParams
|
| 65 |
+
|
| 66 |
+
self.SamplingParams = SamplingParams
|
| 67 |
+
self.llm = LLM(
|
| 68 |
+
model=self.model_id,
|
| 69 |
+
trust_remote_code=True,
|
| 70 |
+
max_model_len=4096,
|
| 71 |
+
gpu_memory_utilization=0.9,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
@modal.method()
|
| 75 |
+
def generate(self, payload: dict) -> dict:
|
| 76 |
+
from sovereign_bench.llm import ModelCallError, clean_model_text
|
| 77 |
+
|
| 78 |
+
started = time.perf_counter()
|
| 79 |
+
messages = payload["messages"]
|
| 80 |
+
max_tokens = int(payload.get("max_tokens") or 120)
|
| 81 |
+
temperature = float(payload.get("temperature") or 0.45)
|
| 82 |
+
sampling_params = self.SamplingParams(
|
| 83 |
+
max_tokens=max_tokens,
|
| 84 |
+
temperature=temperature,
|
| 85 |
+
top_p=0.9,
|
| 86 |
+
)
|
| 87 |
+
retry_messages = messages + [
|
| 88 |
+
{
|
| 89 |
+
"role": "user",
|
| 90 |
+
"content": (
|
| 91 |
+
"Your previous response did not include visible courtroom dialogue. "
|
| 92 |
+
"Return only the final spoken dialogue now. Do not include <think>, analysis, reasoning, markdown, or notes. /no_think"
|
| 93 |
+
),
|
| 94 |
+
}
|
| 95 |
+
]
|
| 96 |
+
last_error: Exception | None = None
|
| 97 |
+
text = ""
|
| 98 |
+
for attempt_messages in (messages, retry_messages):
|
| 99 |
+
outputs = self.llm.chat(
|
| 100 |
+
[attempt_messages],
|
| 101 |
+
sampling_params=sampling_params,
|
| 102 |
+
use_tqdm=False,
|
| 103 |
+
chat_template_kwargs={"enable_thinking": False},
|
| 104 |
+
)
|
| 105 |
+
raw_text = outputs[0].outputs[0].text.strip()
|
| 106 |
+
try:
|
| 107 |
+
text = clean_model_text(raw_text)
|
| 108 |
+
break
|
| 109 |
+
except ModelCallError as exc:
|
| 110 |
+
last_error = exc
|
| 111 |
+
if not text and last_error:
|
| 112 |
+
raise last_error
|
| 113 |
+
return {
|
| 114 |
+
"text": text,
|
| 115 |
+
"latency_ms": int((time.perf_counter() - started) * 1000),
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def modal_gpu_enabled() -> bool:
|
| 120 |
+
return os.getenv("SOVEREIGN_DISABLE_MODAL_GPU", "").lower() not in {"1", "true", "yes"}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def modal_gpu_runner(**kwargs) -> ModelResult:
|
| 124 |
+
messages = build_role_messages(
|
| 125 |
+
agent=kwargs["agent"],
|
| 126 |
+
role=kwargs["role"],
|
| 127 |
+
case_summary=kwargs["case_summary"],
|
| 128 |
+
task=kwargs["task"],
|
| 129 |
+
evidence_summary=kwargs["evidence_summary"],
|
| 130 |
+
)
|
| 131 |
+
requested_model = kwargs["model"]
|
| 132 |
+
prompt_hash = messages_hash(messages)
|
| 133 |
+
|
| 134 |
+
if modal_gpu_enabled():
|
| 135 |
+
output = VllmModel(model_id=requested_model).generate.remote(
|
| 136 |
+
{
|
| 137 |
+
"messages": messages,
|
| 138 |
+
"max_tokens": kwargs.get("max_tokens", 120),
|
| 139 |
+
"temperature": 0.45,
|
| 140 |
+
}
|
| 141 |
+
)
|
| 142 |
+
return ModelResult(
|
| 143 |
+
text=output["text"],
|
| 144 |
+
input_text="\n\n".join(f"{item.get('role', 'user').upper()}:\n{item.get('content', '')}" for item in messages)
|
| 145 |
+
+ "\n\nASSISTANT:\n",
|
| 146 |
+
call=ModelCall(
|
| 147 |
+
model=requested_model,
|
| 148 |
+
provider="modal-gpu-vllm",
|
| 149 |
+
ok=True,
|
| 150 |
+
latency_ms=output["latency_ms"],
|
| 151 |
+
prompt_hash=prompt_hash,
|
| 152 |
+
requested_model=requested_model,
|
| 153 |
+
runtime="modal-gpu-vllm",
|
| 154 |
+
gpu=GPU_NAME,
|
| 155 |
+
),
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
raise RuntimeError("Modal GPU is disabled; no provider fallback is allowed.")
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
@app.function(image=image, secrets=[modal.Secret.from_name("huggingface")])
|
| 162 |
+
def check_huggingface_connection() -> str:
|
| 163 |
+
token = os.getenv("HF_TOKEN")
|
| 164 |
+
if not token:
|
| 165 |
+
return "HF_TOKEN is not available inside Modal."
|
| 166 |
+
|
| 167 |
+
from huggingface_hub import HfApi
|
| 168 |
+
|
| 169 |
+
user = HfApi(token=token).whoami()["name"]
|
| 170 |
+
return f"Connected to Hugging Face as {user}."
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
@app.function(
|
| 174 |
+
image=image,
|
| 175 |
+
secrets=[modal.Secret.from_name("huggingface")],
|
| 176 |
+
min_containers=1,
|
| 177 |
+
timeout=GPU_TIMEOUT_SECONDS,
|
| 178 |
+
)
|
| 179 |
+
@modal.fastapi_endpoint(method="POST", label="trial-stream")
|
| 180 |
+
def trial_stream(payload: dict):
|
| 181 |
+
from fastapi.responses import StreamingResponse
|
| 182 |
+
|
| 183 |
+
request = TrialRequest.model_validate(payload)
|
| 184 |
+
delay = {"swift": 0.02, "measured": 0.12, "ceremonial": 0.25}[request.speed]
|
| 185 |
+
return StreamingResponse(
|
| 186 |
+
stream_trial_jsonl(request, delay=delay, model_runner=modal_gpu_runner),
|
| 187 |
+
media_type="application/x-ndjson",
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
@app.local_entrypoint()
|
| 192 |
+
def main():
|
| 193 |
+
print(check_huggingface_connection.remote())
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
huggingface_hub
|
| 3 |
+
httpx
|
| 4 |
+
modal
|
| 5 |
+
pydantic
|
| 6 |
+
pytest
|
| 7 |
+
python-dotenv
|
sovereign_bench/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sovereign Bench trial engine package."""
|
| 2 |
+
|
| 3 |
+
from .engine import run_trial, stream_trial
|
| 4 |
+
from .models import TrialRequest
|
| 5 |
+
|
| 6 |
+
__all__ = ["TrialRequest", "run_trial", "stream_trial"]
|
sovereign_bench/cases.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from .models import CasePacket, EvidenceItem
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
SOCRATES = CasePacket(
|
| 7 |
+
id="socrates",
|
| 8 |
+
title="The Polis v. Socrates",
|
| 9 |
+
subtitle="A miniature retrial of impiety, civic anxiety, and troublesome questions.",
|
| 10 |
+
claimant="The Athenian Polis",
|
| 11 |
+
respondent="Socrates",
|
| 12 |
+
charge="Corrupting the youth and refusing the sanctioned gods of the city.",
|
| 13 |
+
setting="Athens, 399 BCE, reassembled inside a pocket tribunal.",
|
| 14 |
+
claimant_claim=(
|
| 15 |
+
"The city argues that Socrates trained young citizens to mock public authority "
|
| 16 |
+
"and placed private daimonion guidance above civic religion."
|
| 17 |
+
),
|
| 18 |
+
respondent_claim=(
|
| 19 |
+
"Socrates answers that cross-examination was a public service, not corruption, "
|
| 20 |
+
"and that unpopular inquiry should not be confused with civic sabotage."
|
| 21 |
+
),
|
| 22 |
+
source_note=(
|
| 23 |
+
"Cached public-domain style packet derived from Plato's Apology and Crito, "
|
| 24 |
+
"Xenophon's Apology, and common historical summaries. It is not a live scholarly edition."
|
| 25 |
+
),
|
| 26 |
+
evidence=[
|
| 27 |
+
EvidenceItem(
|
| 28 |
+
id="SOC-E1",
|
| 29 |
+
title="The Oracle Burden",
|
| 30 |
+
source="Plato, Apology tradition",
|
| 31 |
+
excerpt=(
|
| 32 |
+
"Socrates describes testing reputedly wise citizens after a Delphic oracle "
|
| 33 |
+
"report, creating public embarrassment but framing the act as duty."
|
| 34 |
+
),
|
| 35 |
+
supports="mixed",
|
| 36 |
+
reliability=0.78,
|
| 37 |
+
note="Shows both civic irritation and a claimed religious motivation.",
|
| 38 |
+
),
|
| 39 |
+
EvidenceItem(
|
| 40 |
+
id="SOC-E2",
|
| 41 |
+
title="Youthful Imitators",
|
| 42 |
+
source="Plato, Apology tradition",
|
| 43 |
+
excerpt=(
|
| 44 |
+
"Young men with leisure reportedly followed Socrates and copied his questioning, "
|
| 45 |
+
"which angered the questioned citizens."
|
| 46 |
+
),
|
| 47 |
+
supports="claimant",
|
| 48 |
+
reliability=0.68,
|
| 49 |
+
note="Supports social effect, but does not prove intentional corruption.",
|
| 50 |
+
),
|
| 51 |
+
EvidenceItem(
|
| 52 |
+
id="SOC-E3",
|
| 53 |
+
title="No Fee, No School",
|
| 54 |
+
source="Ancient defense tradition",
|
| 55 |
+
excerpt=(
|
| 56 |
+
"Socrates distinguishes himself from paid teachers and denies promising technical "
|
| 57 |
+
"instruction or private doctrine."
|
| 58 |
+
),
|
| 59 |
+
supports="respondent",
|
| 60 |
+
reliability=0.72,
|
| 61 |
+
note="Weakens the claim that he operated a formal corrupting academy.",
|
| 62 |
+
),
|
| 63 |
+
EvidenceItem(
|
| 64 |
+
id="SOC-E4",
|
| 65 |
+
title="The Daimonion",
|
| 66 |
+
source="Ancient biographical tradition",
|
| 67 |
+
excerpt=(
|
| 68 |
+
"Socrates reports a private divine sign that restrains him from certain actions, "
|
| 69 |
+
"which the court may read as piety or heterodoxy."
|
| 70 |
+
),
|
| 71 |
+
supports="mixed",
|
| 72 |
+
reliability=0.64,
|
| 73 |
+
note="Central ambiguity: private religious experience versus civic irreverence.",
|
| 74 |
+
),
|
| 75 |
+
],
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
BARNABY = CasePacket(
|
| 80 |
+
id="barnaby",
|
| 81 |
+
title="The People v. Barnaby Buttons",
|
| 82 |
+
subtitle="The last office mooncake, a tampered snack ledger, and crumbs shaped like intent.",
|
| 83 |
+
claimant="The Breakroom Commonwealth",
|
| 84 |
+
respondent="Barnaby Buttons",
|
| 85 |
+
charge="Theft of the final mooncake and alteration of the communal snack ledger.",
|
| 86 |
+
setting="A fluorescent office kitchen at 4:47 p.m., under the humming republic of the fridge.",
|
| 87 |
+
claimant_claim=(
|
| 88 |
+
"Barnaby removed the final mooncake, changed the snack ledger from '1 mooncake' "
|
| 89 |
+
"to '0 mooncakes', and left the team dessertless."
|
| 90 |
+
),
|
| 91 |
+
respondent_claim=(
|
| 92 |
+
"Barnaby says the mooncake was already abandoned, the ledger pen skipped naturally, "
|
| 93 |
+
"and the crumbs came from an unrelated biscuit."
|
| 94 |
+
),
|
| 95 |
+
source_note="Cached original whimsical packet made for reliable hackathon demos.",
|
| 96 |
+
evidence=[
|
| 97 |
+
EvidenceItem(
|
| 98 |
+
id="BTN-E1",
|
| 99 |
+
title="Ledger Ink Discontinuity",
|
| 100 |
+
source="Clerk's magnifying loupe",
|
| 101 |
+
excerpt="The zero in '0 mooncakes' uses a darker ink than the previous entries.",
|
| 102 |
+
supports="claimant",
|
| 103 |
+
reliability=0.82,
|
| 104 |
+
note="Strong tampering indicator, though pen swaps happen in offices.",
|
| 105 |
+
),
|
| 106 |
+
EvidenceItem(
|
| 107 |
+
id="BTN-E2",
|
| 108 |
+
title="Crumb Constellation",
|
| 109 |
+
source="Breakroom floor survey",
|
| 110 |
+
excerpt="Sesame crumbs form a trail from the pantry shelf to Barnaby's keyboard.",
|
| 111 |
+
supports="claimant",
|
| 112 |
+
reliability=0.71,
|
| 113 |
+
note="Suggestive route evidence, vulnerable to shared-desk contamination.",
|
| 114 |
+
),
|
| 115 |
+
EvidenceItem(
|
| 116 |
+
id="BTN-E3",
|
| 117 |
+
title="Calendar Entry",
|
| 118 |
+
source="Respondent's calendar",
|
| 119 |
+
excerpt="Barnaby had a 4:45 p.m. reminder titled 'Do not forget tea with lunar pastry'.",
|
| 120 |
+
supports="mixed",
|
| 121 |
+
reliability=0.76,
|
| 122 |
+
note="Shows desire and opportunity, but not necessarily theft.",
|
| 123 |
+
),
|
| 124 |
+
EvidenceItem(
|
| 125 |
+
id="BTN-E4",
|
| 126 |
+
title="Biscuit Alibi",
|
| 127 |
+
source="Vending machine receipt",
|
| 128 |
+
excerpt="A receipt shows Barnaby bought a sesame biscuit at 4:39 p.m.",
|
| 129 |
+
supports="respondent",
|
| 130 |
+
reliability=0.67,
|
| 131 |
+
note="Explains crumbs but not ledger alteration.",
|
| 132 |
+
),
|
| 133 |
+
],
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
CASES = {case.id: case for case in (SOCRATES, BARNABY)}
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def get_case(case_id: str) -> CasePacket:
|
| 141 |
+
return CASES.get(case_id, SOCRATES)
|
sovereign_bench/engine.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
import time
|
| 6 |
+
from collections import Counter
|
| 7 |
+
from collections.abc import Callable, Iterable
|
| 8 |
+
|
| 9 |
+
from pydantic import ValidationError
|
| 10 |
+
|
| 11 |
+
from .cases import get_case
|
| 12 |
+
from .llm import ModelCall, ModelResult, call_small_model
|
| 13 |
+
from .models import AgentTurn, CasePacket, JurorVote, TrialEvent, TrialRequest, Verdict
|
| 14 |
+
from .retrieval import build_live_case
|
| 15 |
+
|
| 16 |
+
GPT_OSS_MODEL = "openai/gpt-oss-20b"
|
| 17 |
+
OPENBMB_MODEL = "openbmb/AgentCPM-Explore"
|
| 18 |
+
NEMOTRON_MODEL = "nvidia/Nemotron-Orchestrator-8B"
|
| 19 |
+
OPENAI_PROVIDER = "auto"
|
| 20 |
+
OPENBMB_PROVIDER = "featherless-ai"
|
| 21 |
+
NEMOTRON_PROVIDER = "featherless-ai"
|
| 22 |
+
|
| 23 |
+
MODEL_BUDGET = [
|
| 24 |
+
("Presiding Advocate", GPT_OSS_MODEL, 20.0),
|
| 25 |
+
("Clerk of Style", OPENBMB_MODEL, 4.0),
|
| 26 |
+
("Juror/Auditor Ring", NEMOTRON_MODEL, 8.0),
|
| 27 |
+
]
|
| 28 |
+
TOTAL_PARAMS_B = sum(item[2] for item in MODEL_BUDGET)
|
| 29 |
+
|
| 30 |
+
JUDGE_NAME = "Marcus Aurelius"
|
| 31 |
+
JUDGE_PERSONA = "Stoic duty, restraint, public reason, and disciplined judgment"
|
| 32 |
+
|
| 33 |
+
JUROR_PERSONAS = {
|
| 34 |
+
"Karl Marx": "class power, material conditions, exploitation, institutional incentives",
|
| 35 |
+
"John Stuart Mill": "liberty, harm principle, utility, individual rights",
|
| 36 |
+
"Confucius": "social harmony, role duty, ritual order, moral cultivation",
|
| 37 |
+
"Cleopatra VII": "sovereign pragmatism, diplomacy, survival, legitimacy under pressure",
|
| 38 |
+
"Niccolo Machiavelli": "political realism, stability, power, consequences over ideals",
|
| 39 |
+
"Jensen Huang": "technological optimism, operator mindset, systems thinking, innovation tradeoffs",
|
| 40 |
+
}
|
| 41 |
+
JUROR_NAMES = list(JUROR_PERSONAS)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class RequiredModelError(RuntimeError):
|
| 45 |
+
"""Raised when a required courtroom model call cannot produce usable output."""
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
ModelRunner = Callable[..., ModelResult]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _turn(agent: str, role: str, result: ModelResult, model: str, confidence: float) -> AgentTurn:
|
| 52 |
+
return AgentTurn(
|
| 53 |
+
agent=agent,
|
| 54 |
+
role=role,
|
| 55 |
+
content=result.text,
|
| 56 |
+
model=model,
|
| 57 |
+
confidence=confidence,
|
| 58 |
+
input=getattr(result, "input_text", ""),
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _case_summary(packet: CasePacket) -> str:
|
| 63 |
+
return (
|
| 64 |
+
f"{packet.title}. Charge: {packet.charge}\n"
|
| 65 |
+
f"Claimant: {packet.claimant_claim}\n"
|
| 66 |
+
f"Respondent: {packet.respondent_claim}"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _evidence_summary(packet: CasePacket) -> str:
|
| 71 |
+
return "\n".join(
|
| 72 |
+
f"{item.id}: {item.title}; direction={item.supports}; reliability={item.reliability:.2f}; note={item.note}"
|
| 73 |
+
for item in packet.evidence
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _call_trace(calls: list[ModelCall]) -> list[dict]:
|
| 78 |
+
return [call.__dict__ for call in calls]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def resolve_case(request: TrialRequest) -> tuple[CasePacket, dict]:
|
| 82 |
+
if request.case_id == "live":
|
| 83 |
+
packet = build_live_case(request.search_query, request.hypothetical)
|
| 84 |
+
if packet:
|
| 85 |
+
return packet, {"mode": "live"}
|
| 86 |
+
raise RuntimeError("Live retrieval produced too little usable evidence; no fallback case will be substituted.")
|
| 87 |
+
return get_case(request.case_id), {"mode": "cached"}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _generate_role(model_runner: ModelRunner | None = None, **kwargs) -> ModelResult:
|
| 91 |
+
if model_runner is not None:
|
| 92 |
+
return model_runner(**kwargs)
|
| 93 |
+
return call_small_model(**kwargs)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _required_role(model_runner: ModelRunner | None, model_calls: list[ModelCall], **kwargs) -> ModelResult:
|
| 97 |
+
try:
|
| 98 |
+
result = _generate_role(model_runner, **kwargs)
|
| 99 |
+
except Exception as exc:
|
| 100 |
+
raise RequiredModelError(f"{kwargs.get('agent', 'Model')} unavailable: {exc}") from exc
|
| 101 |
+
model_calls.append(result.call)
|
| 102 |
+
if not result.call.ok:
|
| 103 |
+
error = result.call.error or "model call did not complete"
|
| 104 |
+
raise RequiredModelError(f"{kwargs.get('agent', 'Model')} unavailable: {error}")
|
| 105 |
+
if not result.text.strip():
|
| 106 |
+
raise RequiredModelError(f"{kwargs.get('agent', 'Model')} returned an empty response.")
|
| 107 |
+
return result
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _trace(packet: CasePacket, source_trace: dict, model_calls: list[ModelCall]) -> dict:
|
| 111 |
+
return {
|
| 112 |
+
"case_id": packet.id,
|
| 113 |
+
"model_budget_b": TOTAL_PARAMS_B,
|
| 114 |
+
"models": [{"role": role, "model": model, "params_b": params} for role, model, params in MODEL_BUDGET],
|
| 115 |
+
"model_calls": _call_trace(model_calls),
|
| 116 |
+
"live_model_call_count": sum(1 for call in model_calls if call.ok),
|
| 117 |
+
"attempted_model_call_count": len(model_calls),
|
| 118 |
+
**source_trace,
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _emit(
|
| 123 |
+
packet: CasePacket,
|
| 124 |
+
source_trace: dict,
|
| 125 |
+
model_calls: list[ModelCall],
|
| 126 |
+
event: TrialEvent,
|
| 127 |
+
delay: float,
|
| 128 |
+
) -> TrialEvent:
|
| 129 |
+
event.trace = _trace(packet, source_trace, model_calls)
|
| 130 |
+
if delay > 0:
|
| 131 |
+
time.sleep(delay)
|
| 132 |
+
return event
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _extract_json(text: str) -> object:
|
| 136 |
+
stripped = text.strip()
|
| 137 |
+
if stripped.startswith("```"):
|
| 138 |
+
stripped = re.sub(r"^```(?:json)?\s*", "", stripped, flags=re.I)
|
| 139 |
+
stripped = re.sub(r"\s*```$", "", stripped)
|
| 140 |
+
try:
|
| 141 |
+
return json.loads(stripped)
|
| 142 |
+
except json.JSONDecodeError:
|
| 143 |
+
match = re.search(r"(\{.*\}|\[.*\])", stripped, flags=re.S)
|
| 144 |
+
if not match:
|
| 145 |
+
raise
|
| 146 |
+
return json.loads(match.group(1))
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def _parse_jury_votes(result: ModelResult, packet: CasePacket) -> list[JurorVote]:
|
| 150 |
+
try:
|
| 151 |
+
data = _extract_json(result.text)
|
| 152 |
+
except json.JSONDecodeError as exc:
|
| 153 |
+
raise RequiredModelError(f"Nemotron Jury returned invalid JSON: {exc.msg}") from exc
|
| 154 |
+
|
| 155 |
+
raw_votes = data.get("votes") if isinstance(data, dict) else data
|
| 156 |
+
if not isinstance(raw_votes, list):
|
| 157 |
+
raise RequiredModelError("Nemotron Jury output must contain a votes list.")
|
| 158 |
+
if len(raw_votes) != len(JUROR_NAMES):
|
| 159 |
+
raise RequiredModelError("Nemotron Jury must return exactly six juror votes.")
|
| 160 |
+
|
| 161 |
+
known_evidence = {item.id for item in packet.evidence}
|
| 162 |
+
votes: list[JurorVote] = []
|
| 163 |
+
try:
|
| 164 |
+
for item in raw_votes:
|
| 165 |
+
vote = JurorVote.model_validate(item)
|
| 166 |
+
votes.append(vote)
|
| 167 |
+
except ValidationError as exc:
|
| 168 |
+
raise RequiredModelError(f"Nemotron Jury vote schema is invalid: {exc.errors()[0]['msg']}") from exc
|
| 169 |
+
|
| 170 |
+
if [vote.juror for vote in votes] != JUROR_NAMES:
|
| 171 |
+
raise RequiredModelError("Nemotron Jury must return votes in the fixed juror order.")
|
| 172 |
+
for vote in votes:
|
| 173 |
+
expected_persona = JUROR_PERSONAS[vote.juror]
|
| 174 |
+
if vote.persona.strip().lower() != expected_persona:
|
| 175 |
+
raise RequiredModelError(f"{vote.juror} persona must be '{expected_persona}'.")
|
| 176 |
+
if not vote.reason.strip():
|
| 177 |
+
raise RequiredModelError(f"{vote.juror} must include a rationale.")
|
| 178 |
+
if not vote.evidence_ids or any(evidence_id not in known_evidence for evidence_id in vote.evidence_ids):
|
| 179 |
+
raise RequiredModelError(f"{vote.juror} must cite known evidence IDs.")
|
| 180 |
+
return votes
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def _majority_finding(votes: list[JurorVote]) -> str:
|
| 184 |
+
counts = Counter(vote.vote for vote in votes)
|
| 185 |
+
top = counts.most_common()
|
| 186 |
+
if not top:
|
| 187 |
+
return "uncertain"
|
| 188 |
+
if len(top) > 1 and top[0][1] == top[1][1]:
|
| 189 |
+
return "mixed"
|
| 190 |
+
if top[0][0] == "uncertain":
|
| 191 |
+
return "uncertain"
|
| 192 |
+
return top[0][0]
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def _verdict_from_votes(votes: list[JurorVote]) -> Verdict:
|
| 196 |
+
finding = _majority_finding(votes)
|
| 197 |
+
evidence_ids = []
|
| 198 |
+
for vote in votes:
|
| 199 |
+
for evidence_id in vote.evidence_ids:
|
| 200 |
+
if evidence_id not in evidence_ids:
|
| 201 |
+
evidence_ids.append(evidence_id)
|
| 202 |
+
cited = evidence_ids[:4]
|
| 203 |
+
counts = Counter(vote.vote for vote in votes)
|
| 204 |
+
vote_line = ", ".join(f"{name}: {counts.get(name, 0)}" for name in ("liable", "not_liable", "uncertain"))
|
| 205 |
+
decree_by_finding = {
|
| 206 |
+
"liable": "The jury majority finds liability on the miniature record.",
|
| 207 |
+
"not_liable": "The jury majority does not find liability on the miniature record.",
|
| 208 |
+
"mixed": "The jury divides too closely for a clean finding.",
|
| 209 |
+
"uncertain": "The jury leaves the court with unresolved uncertainty.",
|
| 210 |
+
}
|
| 211 |
+
remedy_by_finding = {
|
| 212 |
+
"liable": "Enter symbolic censure and proportional repair.",
|
| 213 |
+
"not_liable": "Dismiss without prejudice to stronger proof.",
|
| 214 |
+
"mixed": "Record a divided result and preserve the exhibits for later review.",
|
| 215 |
+
"uncertain": "Withhold sanction and identify the proof gaps before any retrial.",
|
| 216 |
+
}
|
| 217 |
+
return Verdict(
|
| 218 |
+
finding=finding, # type: ignore[arg-type]
|
| 219 |
+
decree=decree_by_finding[finding],
|
| 220 |
+
rationale=f"Jury vote: {vote_line}. Cited evidence IDs: {', '.join(cited)}.",
|
| 221 |
+
evidence_ids=cited,
|
| 222 |
+
uncertainty=(
|
| 223 |
+
"Uncertainty remains visible: this is an AI-native miniature trial. Retrieved facts, cached "
|
| 224 |
+
"packets, and model inferences are separated in the trace and should not be treated as legal advice."
|
| 225 |
+
),
|
| 226 |
+
remedy=remedy_by_finding[finding],
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def _jury_task() -> str:
|
| 231 |
+
personas = "\n".join(f"- {name}: {persona}" for name, persona in JUROR_PERSONAS.items())
|
| 232 |
+
return (
|
| 233 |
+
"Return JSON only with a top-level 'votes' array. Create exactly one vote for each juror, in this order: "
|
| 234 |
+
f"{', '.join(JUROR_NAMES)}. Valid vote values are liable, not_liable, uncertain. Each item must contain "
|
| 235 |
+
"juror, persona, vote, reason, and evidence_ids. The persona value must exactly match the profile below. "
|
| 236 |
+
"Each reason should be one concise sentence and each evidence_ids list must cite evidence IDs from the record. "
|
| 237 |
+
"Vote through the named public-history worldview, not a generic juror role.\n"
|
| 238 |
+
f"{personas}"
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def run_trial(request: TrialRequest, model_runner: ModelRunner | None = None) -> list[TrialEvent]:
|
| 243 |
+
return list(stream_trial(request, delay=0.0, model_runner=model_runner))
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def stream_trial(
|
| 247 |
+
request: TrialRequest,
|
| 248 |
+
delay: float = 0.0,
|
| 249 |
+
model_runner: ModelRunner | None = None,
|
| 250 |
+
) -> Iterable[TrialEvent]:
|
| 251 |
+
packet, source_trace = resolve_case(request)
|
| 252 |
+
case_summary = _case_summary(packet)
|
| 253 |
+
evidence_summary = _evidence_summary(packet)
|
| 254 |
+
model_calls: list[ModelCall] = []
|
| 255 |
+
hypo = request.hypothetical.strip()
|
| 256 |
+
hypo_line = f"\n\nUser hypothetical admitted as a blue-ribbon sidebar: {hypo}" if hypo else ""
|
| 257 |
+
|
| 258 |
+
clerk = _required_role(
|
| 259 |
+
model_runner,
|
| 260 |
+
model_calls,
|
| 261 |
+
agent="Clerk Meridian",
|
| 262 |
+
role="clerk",
|
| 263 |
+
model=OPENBMB_MODEL,
|
| 264 |
+
case_summary=case_summary,
|
| 265 |
+
evidence_summary=evidence_summary,
|
| 266 |
+
task="Announce the case by name, identify the parties, and read the charge.",
|
| 267 |
+
provider=OPENBMB_PROVIDER,
|
| 268 |
+
max_tokens=110,
|
| 269 |
+
)
|
| 270 |
+
yield _emit(
|
| 271 |
+
packet,
|
| 272 |
+
source_trace,
|
| 273 |
+
model_calls,
|
| 274 |
+
TrialEvent(
|
| 275 |
+
phase="intake",
|
| 276 |
+
title="The Court Convenes",
|
| 277 |
+
body=f"{packet.title}\n{packet.subtitle}\n\nCharge: {packet.charge}{hypo_line}",
|
| 278 |
+
turns=[_turn("Clerk Meridian", "clerk", clerk, OPENBMB_MODEL, 0.88)],
|
| 279 |
+
evidence=packet.evidence,
|
| 280 |
+
),
|
| 281 |
+
delay,
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
judge_open = _required_role(
|
| 285 |
+
model_runner,
|
| 286 |
+
model_calls,
|
| 287 |
+
agent=JUDGE_NAME,
|
| 288 |
+
role="judge",
|
| 289 |
+
model=GPT_OSS_MODEL,
|
| 290 |
+
case_summary=case_summary,
|
| 291 |
+
evidence_summary=evidence_summary,
|
| 292 |
+
task=(
|
| 293 |
+
f"As {JUDGE_NAME}, a Stoic courtroom judge guided by {JUDGE_PERSONA}, explain the proceeding "
|
| 294 |
+
"and the burden of proof in one or two disciplined sentences."
|
| 295 |
+
),
|
| 296 |
+
provider=OPENAI_PROVIDER,
|
| 297 |
+
max_tokens=110,
|
| 298 |
+
)
|
| 299 |
+
yield _emit(
|
| 300 |
+
packet,
|
| 301 |
+
source_trace,
|
| 302 |
+
model_calls,
|
| 303 |
+
TrialEvent(
|
| 304 |
+
phase="intake",
|
| 305 |
+
title="The Burden Is Set",
|
| 306 |
+
body="The bench defines how the miniature court will weigh the record.",
|
| 307 |
+
turns=[_turn(JUDGE_NAME, "judge", judge_open, GPT_OSS_MODEL, 0.88)],
|
| 308 |
+
evidence=packet.evidence,
|
| 309 |
+
),
|
| 310 |
+
delay,
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
claimant_opening = _required_role(
|
| 314 |
+
model_runner,
|
| 315 |
+
model_calls,
|
| 316 |
+
agent="Advocate Auric",
|
| 317 |
+
role="claimant advocate",
|
| 318 |
+
model=GPT_OSS_MODEL,
|
| 319 |
+
case_summary=case_summary,
|
| 320 |
+
evidence_summary=evidence_summary,
|
| 321 |
+
task="Make the claimant's opening statement alone. Cite the strongest claimant-side exhibit.",
|
| 322 |
+
provider=OPENAI_PROVIDER,
|
| 323 |
+
max_tokens=130,
|
| 324 |
+
)
|
| 325 |
+
yield _emit(
|
| 326 |
+
packet,
|
| 327 |
+
source_trace,
|
| 328 |
+
model_calls,
|
| 329 |
+
TrialEvent(
|
| 330 |
+
phase="claims",
|
| 331 |
+
title="Claimant Opening",
|
| 332 |
+
body=packet.claimant_claim,
|
| 333 |
+
turns=[_turn("Advocate Auric", "claimant advocate", claimant_opening, GPT_OSS_MODEL, 0.88)],
|
| 334 |
+
evidence=packet.evidence,
|
| 335 |
+
),
|
| 336 |
+
delay,
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
respondent_opening = _required_role(
|
| 340 |
+
model_runner,
|
| 341 |
+
model_calls,
|
| 342 |
+
agent="Counsel Sable",
|
| 343 |
+
role="respondent advocate",
|
| 344 |
+
model=GPT_OSS_MODEL,
|
| 345 |
+
case_summary=case_summary,
|
| 346 |
+
evidence_summary=evidence_summary,
|
| 347 |
+
task="Make the respondent's opening statement alone. Emphasize uncertainty and cite a helpful exhibit.",
|
| 348 |
+
provider=OPENAI_PROVIDER,
|
| 349 |
+
max_tokens=130,
|
| 350 |
+
)
|
| 351 |
+
yield _emit(
|
| 352 |
+
packet,
|
| 353 |
+
source_trace,
|
| 354 |
+
model_calls,
|
| 355 |
+
TrialEvent(
|
| 356 |
+
phase="opening",
|
| 357 |
+
title="Respondent Opening",
|
| 358 |
+
body=packet.respondent_claim,
|
| 359 |
+
turns=[_turn("Counsel Sable", "respondent advocate", respondent_opening, GPT_OSS_MODEL, 0.88)],
|
| 360 |
+
evidence=packet.evidence,
|
| 361 |
+
),
|
| 362 |
+
delay,
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
auditor = _required_role(
|
| 366 |
+
model_runner,
|
| 367 |
+
model_calls,
|
| 368 |
+
agent="Auditor Prism",
|
| 369 |
+
role="evidence auditor",
|
| 370 |
+
model=NEMOTRON_MODEL,
|
| 371 |
+
case_summary=case_summary,
|
| 372 |
+
evidence_summary=evidence_summary,
|
| 373 |
+
task="Present the evidence record. Identify the strongest exhibit and the weakest inference.",
|
| 374 |
+
provider=NEMOTRON_PROVIDER,
|
| 375 |
+
max_tokens=150,
|
| 376 |
+
)
|
| 377 |
+
yield _emit(
|
| 378 |
+
packet,
|
| 379 |
+
source_trace,
|
| 380 |
+
model_calls,
|
| 381 |
+
TrialEvent(
|
| 382 |
+
phase="evidence",
|
| 383 |
+
title="The Record Is Audited",
|
| 384 |
+
body="\n".join(f"{item.id}: {item.title} | reliability {item.reliability:.2f} | {item.note}" for item in packet.evidence),
|
| 385 |
+
turns=[_turn("Auditor Prism", "evidence auditor", auditor, NEMOTRON_MODEL, 0.86)],
|
| 386 |
+
evidence=packet.evidence,
|
| 387 |
+
),
|
| 388 |
+
delay,
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
judge_question = _required_role(
|
| 392 |
+
model_runner,
|
| 393 |
+
model_calls,
|
| 394 |
+
agent=JUDGE_NAME,
|
| 395 |
+
role="judge",
|
| 396 |
+
model=GPT_OSS_MODEL,
|
| 397 |
+
case_summary=case_summary,
|
| 398 |
+
evidence_summary=evidence_summary,
|
| 399 |
+
task=(
|
| 400 |
+
f"As {JUDGE_NAME}, ask one sharp hinge question that would change the outcome if answered. "
|
| 401 |
+
"Use Stoic restraint and public reason."
|
| 402 |
+
),
|
| 403 |
+
provider=OPENAI_PROVIDER,
|
| 404 |
+
max_tokens=100,
|
| 405 |
+
)
|
| 406 |
+
yield _emit(
|
| 407 |
+
packet,
|
| 408 |
+
source_trace,
|
| 409 |
+
model_calls,
|
| 410 |
+
TrialEvent(
|
| 411 |
+
phase="questions",
|
| 412 |
+
title="The Hinge Question",
|
| 413 |
+
body="The bench asks the single question that could turn the record.",
|
| 414 |
+
turns=[_turn(JUDGE_NAME, "judge", judge_question, GPT_OSS_MODEL, 0.88)],
|
| 415 |
+
evidence=packet.evidence,
|
| 416 |
+
),
|
| 417 |
+
delay,
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
claimant_answer = _required_role(
|
| 421 |
+
model_runner,
|
| 422 |
+
model_calls,
|
| 423 |
+
agent="Advocate Auric",
|
| 424 |
+
role="claimant advocate",
|
| 425 |
+
model=GPT_OSS_MODEL,
|
| 426 |
+
case_summary=case_summary,
|
| 427 |
+
evidence_summary=evidence_summary,
|
| 428 |
+
task=f"Answer {JUDGE_NAME}'s hinge question for the claimant: {judge_question.text}",
|
| 429 |
+
provider=OPENAI_PROVIDER,
|
| 430 |
+
max_tokens=130,
|
| 431 |
+
)
|
| 432 |
+
yield _emit(
|
| 433 |
+
packet,
|
| 434 |
+
source_trace,
|
| 435 |
+
model_calls,
|
| 436 |
+
TrialEvent(
|
| 437 |
+
phase="questions",
|
| 438 |
+
title="Claimant Answers the Bench",
|
| 439 |
+
body="The claimant answers the hinge question.",
|
| 440 |
+
turns=[_turn("Advocate Auric", "claimant advocate", claimant_answer, GPT_OSS_MODEL, 0.88)],
|
| 441 |
+
evidence=packet.evidence,
|
| 442 |
+
),
|
| 443 |
+
delay,
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
+
respondent_answer = _required_role(
|
| 447 |
+
model_runner,
|
| 448 |
+
model_calls,
|
| 449 |
+
agent="Counsel Sable",
|
| 450 |
+
role="respondent advocate",
|
| 451 |
+
model=GPT_OSS_MODEL,
|
| 452 |
+
case_summary=case_summary,
|
| 453 |
+
evidence_summary=evidence_summary,
|
| 454 |
+
task=f"Answer {JUDGE_NAME}'s hinge question for the respondent: {judge_question.text}",
|
| 455 |
+
provider=OPENAI_PROVIDER,
|
| 456 |
+
max_tokens=130,
|
| 457 |
+
)
|
| 458 |
+
yield _emit(
|
| 459 |
+
packet,
|
| 460 |
+
source_trace,
|
| 461 |
+
model_calls,
|
| 462 |
+
TrialEvent(
|
| 463 |
+
phase="questions",
|
| 464 |
+
title="Respondent Answers the Bench",
|
| 465 |
+
body="The respondent answers the hinge question.",
|
| 466 |
+
turns=[_turn("Counsel Sable", "respondent advocate", respondent_answer, GPT_OSS_MODEL, 0.88)],
|
| 467 |
+
evidence=packet.evidence,
|
| 468 |
+
),
|
| 469 |
+
delay,
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
jury_panel = _required_role(
|
| 473 |
+
model_runner,
|
| 474 |
+
model_calls,
|
| 475 |
+
agent="Nemotron Jury",
|
| 476 |
+
role="juror panel",
|
| 477 |
+
model=NEMOTRON_MODEL,
|
| 478 |
+
case_summary=case_summary,
|
| 479 |
+
evidence_summary=evidence_summary,
|
| 480 |
+
task="Announce that the six named jurors retire to vote. Do not reveal the votes yet.",
|
| 481 |
+
provider=NEMOTRON_PROVIDER,
|
| 482 |
+
max_tokens=100,
|
| 483 |
+
)
|
| 484 |
+
yield _emit(
|
| 485 |
+
packet,
|
| 486 |
+
source_trace,
|
| 487 |
+
model_calls,
|
| 488 |
+
TrialEvent(
|
| 489 |
+
phase="deliberation",
|
| 490 |
+
title="The Jury Retires",
|
| 491 |
+
body="Six fixed-perspective jurors leave the public floor to vote from the record.",
|
| 492 |
+
turns=[_turn("Nemotron Jury", "juror panel", jury_panel, NEMOTRON_MODEL, 0.86)],
|
| 493 |
+
evidence=packet.evidence,
|
| 494 |
+
),
|
| 495 |
+
delay,
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
jury_votes_result = _required_role(
|
| 499 |
+
model_runner,
|
| 500 |
+
model_calls,
|
| 501 |
+
agent="Nemotron Jury",
|
| 502 |
+
role="juror vote generator",
|
| 503 |
+
model=NEMOTRON_MODEL,
|
| 504 |
+
case_summary=case_summary,
|
| 505 |
+
evidence_summary=evidence_summary,
|
| 506 |
+
task=_jury_task(),
|
| 507 |
+
provider=NEMOTRON_PROVIDER,
|
| 508 |
+
max_tokens=650,
|
| 509 |
+
)
|
| 510 |
+
votes = _parse_jury_votes(jury_votes_result, packet)
|
| 511 |
+
for vote in votes:
|
| 512 |
+
juror_result = ModelResult(
|
| 513 |
+
text=f"{vote.vote.replace('_', ' ').title()}. {vote.reason}",
|
| 514 |
+
call=jury_votes_result.call,
|
| 515 |
+
input_text=jury_votes_result.input_text,
|
| 516 |
+
)
|
| 517 |
+
yield _emit(
|
| 518 |
+
packet,
|
| 519 |
+
source_trace,
|
| 520 |
+
model_calls,
|
| 521 |
+
TrialEvent(
|
| 522 |
+
phase="deliberation",
|
| 523 |
+
title=f"Juror {vote.juror} Votes",
|
| 524 |
+
body=f"{vote.persona}. Evidence: {', '.join(vote.evidence_ids)}.",
|
| 525 |
+
turns=[_turn(vote.juror, "juror", juror_result, NEMOTRON_MODEL, 0.86)],
|
| 526 |
+
votes=[vote],
|
| 527 |
+
evidence=packet.evidence,
|
| 528 |
+
),
|
| 529 |
+
delay,
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
verdict = _verdict_from_votes(votes)
|
| 533 |
+
verdict_voice = _required_role(
|
| 534 |
+
model_runner,
|
| 535 |
+
model_calls,
|
| 536 |
+
agent=JUDGE_NAME,
|
| 537 |
+
role="verdict writer",
|
| 538 |
+
model=GPT_OSS_MODEL,
|
| 539 |
+
case_summary=case_summary,
|
| 540 |
+
evidence_summary=evidence_summary,
|
| 541 |
+
task=(
|
| 542 |
+
f"As {JUDGE_NAME}, announce the final legal finding after the jury vote with Stoic restraint. "
|
| 543 |
+
f"Finding: {verdict.finding}. "
|
| 544 |
+
f"Jury rationale: {verdict.rationale} Remedy: {verdict.remedy}. Include uncertainty without disclaiming the role."
|
| 545 |
+
),
|
| 546 |
+
provider=OPENAI_PROVIDER,
|
| 547 |
+
max_tokens=160,
|
| 548 |
+
)
|
| 549 |
+
yield _emit(
|
| 550 |
+
packet,
|
| 551 |
+
source_trace,
|
| 552 |
+
model_calls,
|
| 553 |
+
TrialEvent(
|
| 554 |
+
phase="verdict",
|
| 555 |
+
title="The Court Announces Judgment",
|
| 556 |
+
body=f"{verdict_voice.text}\n\n{verdict.rationale}\n\nRemedy: {verdict.remedy}",
|
| 557 |
+
verdict=verdict,
|
| 558 |
+
votes=votes,
|
| 559 |
+
evidence=packet.evidence,
|
| 560 |
+
turns=[_turn(JUDGE_NAME, "verdict writer", verdict_voice, GPT_OSS_MODEL, 0.88)],
|
| 561 |
+
),
|
| 562 |
+
delay,
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
def stream_trial_jsonl(
|
| 567 |
+
request: TrialRequest,
|
| 568 |
+
delay: float = 0.0,
|
| 569 |
+
model_runner: ModelRunner | None = None,
|
| 570 |
+
) -> Iterable[str]:
|
| 571 |
+
for event in stream_trial(request, delay, model_runner=model_runner):
|
| 572 |
+
yield json.dumps(event.model_dump(), ensure_ascii=True) + "\n"
|
sovereign_bench/export.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import tempfile
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from .models import TrialEvent
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def write_trace(events: list[TrialEvent]) -> str:
|
| 11 |
+
path = Path(tempfile.gettempdir()) / "sovereign_bench_trace.json"
|
| 12 |
+
path.write_text(
|
| 13 |
+
json.dumps([event.model_dump() for event in events], indent=2, ensure_ascii=True),
|
| 14 |
+
encoding="utf-8",
|
| 15 |
+
)
|
| 16 |
+
return str(path)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def write_decree(events: list[TrialEvent]) -> str:
|
| 20 |
+
verdict_event = next((event for event in events if event.verdict), events[-1])
|
| 21 |
+
verdict = verdict_event.verdict
|
| 22 |
+
path = Path(tempfile.gettempdir()) / "sovereign_bench_decree.md"
|
| 23 |
+
if verdict is None:
|
| 24 |
+
text = "# Sovereign Bench Decree\n\nNo verdict was recorded."
|
| 25 |
+
else:
|
| 26 |
+
text = (
|
| 27 |
+
"# Sovereign Bench Decree\n\n"
|
| 28 |
+
f"## Finding\n{verdict.finding}\n\n"
|
| 29 |
+
f"## Decree\n{verdict.decree}\n\n"
|
| 30 |
+
f"## Rationale\n{verdict.rationale}\n\n"
|
| 31 |
+
f"## Remedy\n{verdict.remedy}\n\n"
|
| 32 |
+
f"## Uncertainty\n{verdict.uncertainty}\n"
|
| 33 |
+
)
|
| 34 |
+
path.write_text(text, encoding="utf-8")
|
| 35 |
+
return str(path)
|
sovereign_bench/llm.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import re
|
| 5 |
+
import time
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from hashlib import sha256
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass
|
| 11 |
+
class ModelCall:
|
| 12 |
+
model: str
|
| 13 |
+
provider: str
|
| 14 |
+
ok: bool
|
| 15 |
+
latency_ms: int
|
| 16 |
+
prompt_hash: str
|
| 17 |
+
error: str | None = None
|
| 18 |
+
requested_model: str | None = None
|
| 19 |
+
runtime: str | None = None
|
| 20 |
+
gpu: str | None = None
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class ModelResult:
|
| 25 |
+
text: str
|
| 26 |
+
call: ModelCall
|
| 27 |
+
input_text: str = ""
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class ModelCallError(RuntimeError):
|
| 31 |
+
pass
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _short_error(exc: Exception) -> str:
|
| 35 |
+
message = str(exc).replace("\n", " ").strip()
|
| 36 |
+
return f"{exc.__class__.__name__}: {message[:220]}"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def messages_hash(messages: list[dict[str, str]]) -> str:
|
| 40 |
+
joined = "\n".join(f"{item.get('role', '')}:{item.get('content', '')}" for item in messages)
|
| 41 |
+
return sha256(joined.encode("utf-8")).hexdigest()[:16]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _prompt_from_messages(messages: list[dict[str, str]]) -> str:
|
| 45 |
+
return "\n\n".join(f"{item.get('role', 'user').upper()}:\n{item.get('content', '')}" for item in messages) + "\n\nASSISTANT:\n"
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _response_text(response: object) -> str:
|
| 49 |
+
choice = response.choices[0] # type: ignore[attr-defined]
|
| 50 |
+
message = choice.message
|
| 51 |
+
for attr in ("content", "reasoning_content", "reasoning"):
|
| 52 |
+
value = getattr(message, attr, None)
|
| 53 |
+
if isinstance(value, str) and value.strip():
|
| 54 |
+
return value.strip()
|
| 55 |
+
if isinstance(value, list):
|
| 56 |
+
pieces = []
|
| 57 |
+
for item in value:
|
| 58 |
+
text = getattr(item, "text", None) or (item.get("text") if isinstance(item, dict) else None)
|
| 59 |
+
if text:
|
| 60 |
+
pieces.append(str(text))
|
| 61 |
+
if pieces:
|
| 62 |
+
return " ".join(pieces).strip()
|
| 63 |
+
if hasattr(message, "model_dump"):
|
| 64 |
+
data = message.model_dump()
|
| 65 |
+
for key in ("content", "reasoning_content", "reasoning"):
|
| 66 |
+
value = data.get(key)
|
| 67 |
+
if isinstance(value, str) and value.strip():
|
| 68 |
+
return value.strip()
|
| 69 |
+
return ""
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def clean_model_text(text: str) -> str:
|
| 73 |
+
cleaned = re.sub(r"(?is)<think>.*?</think>", "", text).strip()
|
| 74 |
+
if re.search(r"(?i)<think>", cleaned):
|
| 75 |
+
raise ModelCallError("model returned unclosed hidden reasoning")
|
| 76 |
+
cleaned = re.sub(r"(?is)<analysis>.*?</analysis>", "", cleaned).strip()
|
| 77 |
+
cleaned = re.sub(r"(?is)<reasoning>.*?</reasoning>", "", cleaned).strip()
|
| 78 |
+
cleaned = cleaned.replace("</think>", "").strip()
|
| 79 |
+
if not cleaned:
|
| 80 |
+
raise ModelCallError("model returned no visible output")
|
| 81 |
+
return cleaned
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def model_enabled() -> bool:
|
| 85 |
+
return os.getenv("SOVEREIGN_DISABLE_LIVE_MODELS", "").lower() not in {"1", "true", "yes"}
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def call_hf_chat_model(
|
| 89 |
+
*,
|
| 90 |
+
model: str,
|
| 91 |
+
messages: list[dict[str, str]],
|
| 92 |
+
provider: str = "auto",
|
| 93 |
+
max_tokens: int = 140,
|
| 94 |
+
temperature: float = 0.45,
|
| 95 |
+
) -> ModelResult:
|
| 96 |
+
prompt_hash = messages_hash(messages)
|
| 97 |
+
started = time.perf_counter()
|
| 98 |
+
token = os.getenv("HF_TOKEN")
|
| 99 |
+
if not token or not model_enabled():
|
| 100 |
+
raise ModelCallError("HF_TOKEN missing or live models disabled")
|
| 101 |
+
|
| 102 |
+
try:
|
| 103 |
+
from huggingface_hub import InferenceClient
|
| 104 |
+
|
| 105 |
+
client = InferenceClient(model=model, provider=provider, token=token, timeout=45.0)
|
| 106 |
+
retry_messages = messages + [
|
| 107 |
+
{
|
| 108 |
+
"role": "user",
|
| 109 |
+
"content": (
|
| 110 |
+
"Your previous response did not include visible courtroom dialogue. "
|
| 111 |
+
"Return only the final spoken dialogue now. Do not include <think>, analysis, reasoning, markdown, or notes. /no_think"
|
| 112 |
+
),
|
| 113 |
+
}
|
| 114 |
+
]
|
| 115 |
+
last_error: Exception | None = None
|
| 116 |
+
text = ""
|
| 117 |
+
for attempt_messages in (messages, retry_messages):
|
| 118 |
+
try:
|
| 119 |
+
response = client.chat_completion(
|
| 120 |
+
messages=attempt_messages,
|
| 121 |
+
max_tokens=max_tokens,
|
| 122 |
+
temperature=temperature,
|
| 123 |
+
top_p=0.9,
|
| 124 |
+
)
|
| 125 |
+
raw_text = _response_text(response)
|
| 126 |
+
except Exception as chat_exc:
|
| 127 |
+
prompt = _prompt_from_messages(attempt_messages)
|
| 128 |
+
generated = client.text_generation(
|
| 129 |
+
prompt,
|
| 130 |
+
max_new_tokens=max_tokens,
|
| 131 |
+
temperature=temperature,
|
| 132 |
+
top_p=0.9,
|
| 133 |
+
return_full_text=False,
|
| 134 |
+
)
|
| 135 |
+
raw_text = str(generated).strip()
|
| 136 |
+
if not raw_text:
|
| 137 |
+
raise chat_exc
|
| 138 |
+
try:
|
| 139 |
+
text = clean_model_text(raw_text)
|
| 140 |
+
break
|
| 141 |
+
except ModelCallError as exc:
|
| 142 |
+
last_error = exc
|
| 143 |
+
if not text:
|
| 144 |
+
raise last_error or RuntimeError("empty model response")
|
| 145 |
+
return ModelResult(
|
| 146 |
+
text=text,
|
| 147 |
+
call=ModelCall(
|
| 148 |
+
model=model,
|
| 149 |
+
provider=provider,
|
| 150 |
+
ok=True,
|
| 151 |
+
latency_ms=int((time.perf_counter() - started) * 1000),
|
| 152 |
+
prompt_hash=prompt_hash,
|
| 153 |
+
),
|
| 154 |
+
)
|
| 155 |
+
except Exception as exc:
|
| 156 |
+
raise ModelCallError(
|
| 157 |
+
f"{model} via {provider} failed after {int((time.perf_counter() - started) * 1000)}ms: {_short_error(exc)}"
|
| 158 |
+
) from exc
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def call_small_model(
|
| 162 |
+
*,
|
| 163 |
+
agent: str,
|
| 164 |
+
role: str,
|
| 165 |
+
model: str,
|
| 166 |
+
case_summary: str,
|
| 167 |
+
task: str,
|
| 168 |
+
evidence_summary: str,
|
| 169 |
+
provider: str = "auto",
|
| 170 |
+
max_tokens: int = 120,
|
| 171 |
+
) -> ModelResult:
|
| 172 |
+
messages = build_role_messages(
|
| 173 |
+
agent=agent,
|
| 174 |
+
role=role,
|
| 175 |
+
case_summary=case_summary,
|
| 176 |
+
task=task,
|
| 177 |
+
evidence_summary=evidence_summary,
|
| 178 |
+
)
|
| 179 |
+
result = call_hf_chat_model(
|
| 180 |
+
model=model,
|
| 181 |
+
provider=provider,
|
| 182 |
+
messages=messages,
|
| 183 |
+
max_tokens=max_tokens,
|
| 184 |
+
)
|
| 185 |
+
result.input_text = _prompt_from_messages(messages)
|
| 186 |
+
return result
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def build_role_messages(
|
| 190 |
+
*,
|
| 191 |
+
agent: str,
|
| 192 |
+
role: str,
|
| 193 |
+
case_summary: str,
|
| 194 |
+
task: str,
|
| 195 |
+
evidence_summary: str,
|
| 196 |
+
) -> list[dict[str, str]]:
|
| 197 |
+
system = (
|
| 198 |
+
"You are one AI character in Sovereign Bench, a miniature virtual courtroom. "
|
| 199 |
+
"Write concise courtroom dialogue only. Cite evidence IDs when relevant. "
|
| 200 |
+
"Do not claim certainty beyond the record. Do not add markdown. "
|
| 201 |
+
"Return final spoken dialogue only; never reveal hidden reasoning, analysis, or <think> text. "
|
| 202 |
+
"Do not use thinking mode."
|
| 203 |
+
)
|
| 204 |
+
user = (
|
| 205 |
+
f"Agent: {agent}\nRole: {role}\nCase:\n{case_summary}\n\n"
|
| 206 |
+
f"Evidence:\n{evidence_summary}\n\nTask: {task}\n"
|
| 207 |
+
"Answer in 1-3 sentences, theatrical but clear.\n/no_think"
|
| 208 |
+
)
|
| 209 |
+
return [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
sovereign_bench/models.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Literal
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
TrialPhase = Literal[
|
| 9 |
+
"intake",
|
| 10 |
+
"claims",
|
| 11 |
+
"opening",
|
| 12 |
+
"evidence",
|
| 13 |
+
"questions",
|
| 14 |
+
"deliberation",
|
| 15 |
+
"verdict",
|
| 16 |
+
"appeal",
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class EvidenceItem(BaseModel):
|
| 21 |
+
id: str
|
| 22 |
+
title: str
|
| 23 |
+
source: str
|
| 24 |
+
excerpt: str
|
| 25 |
+
supports: Literal["claimant", "respondent", "mixed", "context"]
|
| 26 |
+
reliability: float = Field(ge=0.0, le=1.0)
|
| 27 |
+
note: str
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class CasePacket(BaseModel):
|
| 31 |
+
id: str
|
| 32 |
+
title: str
|
| 33 |
+
subtitle: str
|
| 34 |
+
claimant: str
|
| 35 |
+
respondent: str
|
| 36 |
+
charge: str
|
| 37 |
+
setting: str
|
| 38 |
+
claimant_claim: str
|
| 39 |
+
respondent_claim: str
|
| 40 |
+
source_note: str
|
| 41 |
+
evidence: list[EvidenceItem]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class TrialRequest(BaseModel):
|
| 45 |
+
case_id: str = "socrates"
|
| 46 |
+
search_query: str = ""
|
| 47 |
+
hypothetical: str = ""
|
| 48 |
+
speed: Literal["swift", "measured", "ceremonial"] = "swift"
|
| 49 |
+
mind_layer: bool = True
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class AgentTurn(BaseModel):
|
| 53 |
+
agent: str
|
| 54 |
+
role: str
|
| 55 |
+
content: str
|
| 56 |
+
model: str
|
| 57 |
+
confidence: float = Field(ge=0.0, le=1.0)
|
| 58 |
+
input: str = ""
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class JurorVote(BaseModel):
|
| 62 |
+
juror: str
|
| 63 |
+
persona: str = ""
|
| 64 |
+
vote: Literal["liable", "not_liable", "uncertain"]
|
| 65 |
+
reason: str
|
| 66 |
+
evidence_ids: list[str]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class Verdict(BaseModel):
|
| 70 |
+
finding: Literal["liable", "not_liable", "mixed", "uncertain"]
|
| 71 |
+
decree: str
|
| 72 |
+
rationale: str
|
| 73 |
+
evidence_ids: list[str]
|
| 74 |
+
uncertainty: str
|
| 75 |
+
remedy: str
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class TrialEvent(BaseModel):
|
| 79 |
+
phase: TrialPhase
|
| 80 |
+
title: str
|
| 81 |
+
body: str
|
| 82 |
+
turns: list[AgentTurn] = Field(default_factory=list)
|
| 83 |
+
evidence: list[EvidenceItem] = Field(default_factory=list)
|
| 84 |
+
votes: list[JurorVote] = Field(default_factory=list)
|
| 85 |
+
verdict: Verdict | None = None
|
| 86 |
+
trace: dict = Field(default_factory=dict)
|
sovereign_bench/retrieval.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from urllib.parse import quote_plus
|
| 5 |
+
|
| 6 |
+
import httpx
|
| 7 |
+
|
| 8 |
+
from .models import CasePacket, EvidenceItem
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _plain_text(html: str) -> str:
|
| 12 |
+
html = re.sub(r"(?is)<script.*?</script>|<style.*?</style>", " ", html)
|
| 13 |
+
html = re.sub(r"(?s)<[^>]+>", " ", html)
|
| 14 |
+
html = re.sub(r"\s+", " ", html)
|
| 15 |
+
return html.strip()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def build_live_case(query: str, hypothetical: str = "") -> CasePacket | None:
|
| 19 |
+
clean_query = " ".join(query.split())
|
| 20 |
+
if len(clean_query) < 8:
|
| 21 |
+
return None
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
url = f"https://r.jina.ai/http://r.jina.ai/http://duckduckgo.com/html/?q={quote_plus(clean_query)}"
|
| 25 |
+
response = httpx.get(url, timeout=8.0, follow_redirects=True)
|
| 26 |
+
text = _plain_text(response.text)
|
| 27 |
+
except Exception:
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
snippets = [
|
| 31 |
+
segment.strip()
|
| 32 |
+
for segment in re.split(r"(?<=[.!?])\s+", text)
|
| 33 |
+
if 80 <= len(segment.strip()) <= 320 and "http" not in segment[:20].lower()
|
| 34 |
+
]
|
| 35 |
+
unique: list[str] = []
|
| 36 |
+
for snippet in snippets:
|
| 37 |
+
if snippet.lower() not in {item.lower() for item in unique}:
|
| 38 |
+
unique.append(snippet)
|
| 39 |
+
if len(unique) == 4:
|
| 40 |
+
break
|
| 41 |
+
|
| 42 |
+
if len(unique) < 2:
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
evidence = [
|
| 46 |
+
EvidenceItem(
|
| 47 |
+
id=f"WEB-E{i}",
|
| 48 |
+
title=f"Retrieved fragment {i}",
|
| 49 |
+
source=f"Web retrieval for: {clean_query}",
|
| 50 |
+
excerpt=snippet,
|
| 51 |
+
supports="context" if i == 1 else "mixed",
|
| 52 |
+
reliability=max(0.45, 0.72 - (i * 0.06)),
|
| 53 |
+
note="Live retrieval fragment; the court treats it as context until corroborated.",
|
| 54 |
+
)
|
| 55 |
+
for i, snippet in enumerate(unique, start=1)
|
| 56 |
+
]
|
| 57 |
+
framing = hypothetical.strip() or "the parties dispute how the retrieved facts should be interpreted"
|
| 58 |
+
return CasePacket(
|
| 59 |
+
id="live",
|
| 60 |
+
title=f"Live Search Tribunal: {clean_query[:58]}",
|
| 61 |
+
subtitle="A search-fed miniature proceeding with uncertainty kept visible.",
|
| 62 |
+
claimant="The Search Record",
|
| 63 |
+
respondent="The Counter-Interpretation",
|
| 64 |
+
charge=f"Whether {framing}.",
|
| 65 |
+
setting="A temporary court assembled from retrieved public web fragments.",
|
| 66 |
+
claimant_claim="The retrieved record supports a coherent claim that should be credited.",
|
| 67 |
+
respondent_claim="The retrieved record is incomplete, ambiguous, or overread by the claimant.",
|
| 68 |
+
source_note="Live web retrieval via public search snippets. Treat as unverified context, not ground truth.",
|
| 69 |
+
evidence=evidence,
|
| 70 |
+
)
|
tests/test_cases.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sovereign_bench.cases import CASES
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_cached_cases_have_evidence():
|
| 5 |
+
assert {"socrates", "barnaby"} <= set(CASES)
|
| 6 |
+
for case in CASES.values():
|
| 7 |
+
assert len(case.evidence) >= 4
|
| 8 |
+
assert all(item.id and item.excerpt for item in case.evidence)
|