Blu3Orange commited on
Commit
a9f964a
·
1 Parent(s): f3e8cad

Add main application logic, memory management, and configuration for 12 Angry Agents

Browse files
app.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """12 Angry Agents - Main Gradio Application.
2
+
3
+ AI-powered jury deliberation simulation where 11 AI agents + 1 human player
4
+ debate criminal cases. A Judge narrator orchestrates the experience.
5
+ """
6
+
7
+ import asyncio
8
+ import os
9
+ import random
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import gradio as gr
14
+ from dotenv import load_dotenv
15
+
16
+ # Load environment variables
17
+ load_dotenv()
18
+
19
+ # Add project root to path for imports
20
+ import sys
21
+ sys.path.insert(0, str(Path(__file__).parent))
22
+
23
+ from core.game_state import GameState, GamePhase, DeliberationTurn
24
+ from core.models import JurorConfig
25
+ from core.conviction import calculate_conviction_change
26
+ from case_db import CaseLoader, CriminalCase
27
+ from agents import load_juror_configs, JurorAgent
28
+ from ui.components import render_jury_box, render_vote_tally
29
+
30
+
31
+ # Custom CSS for dark jury room theme
32
+ CUSTOM_CSS = """
33
+ .gradio-container {
34
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%) !important;
35
+ }
36
+ .jury-title {
37
+ text-align: center;
38
+ color: #e94560 !important;
39
+ font-family: 'Georgia', serif;
40
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
41
+ }
42
+ .phase-indicator {
43
+ background: #0f3460;
44
+ padding: 10px;
45
+ border-radius: 8px;
46
+ text-align: center;
47
+ color: #4ecdc4;
48
+ margin-bottom: 10px;
49
+ }
50
+ """
51
+
52
+
53
+ class JuryDeliberation:
54
+ """Main game orchestrator."""
55
+
56
+ def __init__(self):
57
+ self.case_loader = CaseLoader()
58
+ self.juror_configs = load_juror_configs()
59
+ self.juror_agents: dict[str, JurorAgent] = {}
60
+ self.game_state: GameState | None = None
61
+ self.current_case: CriminalCase | None = None
62
+
63
+ def initialize_game(self, case_id: str | None = None) -> tuple[str, str, str, list]:
64
+ """Initialize a new game.
65
+
66
+ Returns:
67
+ Tuple of (case_summary, evidence_html, jury_box_html, chat_history)
68
+ """
69
+ # Load case
70
+ if case_id:
71
+ self.current_case = self.case_loader.get_case(case_id)
72
+ else:
73
+ self.current_case = self.case_loader.get_random_case()
74
+
75
+ if not self.current_case:
76
+ # Use a default case if none found
77
+ return (
78
+ "No cases available. Please add case files to case_db/cases/",
79
+ "",
80
+ render_jury_box(self.juror_configs),
81
+ []
82
+ )
83
+
84
+ # Initialize game state
85
+ self.game_state = GameState(
86
+ case_id=self.current_case.case_id,
87
+ phase=GamePhase.PRESENTATION,
88
+ )
89
+
90
+ # Initialize juror agents (skip player seat 7)
91
+ self.juror_agents = {}
92
+ for config in self.juror_configs:
93
+ if config.archetype != "player":
94
+ try:
95
+ agent = JurorAgent(config)
96
+ # Set initial conviction
97
+ conviction = agent.set_initial_conviction(self.current_case)
98
+ # Set initial vote
99
+ self.game_state.votes[config.juror_id] = agent.get_vote()
100
+ self.game_state.conviction_scores[config.juror_id] = conviction
101
+ self.juror_agents[config.juror_id] = agent
102
+ except Exception as e:
103
+ print(f"Warning: Failed to initialize {config.name}: {e}")
104
+
105
+ # Format case info
106
+ case_summary = f"""## {self.current_case.title}
107
+
108
+ **Year:** {self.current_case.year} | **Jurisdiction:** {self.current_case.jurisdiction}
109
+
110
+ **Charges:** {self.current_case.get_charges_text()}
111
+
112
+ ---
113
+
114
+ {self.current_case.summary}
115
+ """
116
+
117
+ # Format evidence
118
+ evidence_html = f"""### Evidence
119
+
120
+ {self.current_case.get_evidence_summary()}
121
+
122
+ ### Witnesses
123
+
124
+ {self.current_case.get_witness_summary()}
125
+ """
126
+
127
+ # Render jury box
128
+ jury_html = render_jury_box(self.juror_configs, self.game_state)
129
+
130
+ # Initial chat with judge introduction
131
+ chat_history = [
132
+ (None, f"**\u2696\uFE0F Judge:** Members of the jury, you are here today to determine the fate of the defendant in the case of {self.current_case.title}. Please review the evidence carefully and deliberate with your fellow jurors. The burden of proof lies with the prosecution.")
133
+ ]
134
+
135
+ return case_summary, evidence_html, jury_html, chat_history
136
+
137
+ def select_player_side(self, side: str) -> tuple[str, str, list]:
138
+ """Player selects prosecution or defense side.
139
+
140
+ Returns:
141
+ Tuple of (vote_tally_html, jury_html, chat_history_update)
142
+ """
143
+ if not self.game_state:
144
+ return "", "", []
145
+
146
+ self.game_state.player_side = side
147
+ self.game_state.phase = GamePhase.DELIBERATION
148
+
149
+ # Set player's vote based on side
150
+ player_vote = "guilty" if side == "prosecute" else "not_guilty"
151
+ self.game_state.votes["juror_7"] = player_vote
152
+ self.game_state.conviction_scores["juror_7"] = 0.8 if side == "prosecute" else 0.2
153
+
154
+ vote_html = render_vote_tally(self.game_state)
155
+ jury_html = render_jury_box(self.juror_configs, self.game_state)
156
+
157
+ side_text = "PROSECUTION (Guilty)" if side == "prosecute" else "DEFENSE (Not Guilty)"
158
+ chat_update = [(f"*You have chosen to argue for the {side_text}*", None)]
159
+
160
+ return vote_html, jury_html, chat_update
161
+
162
+ async def run_deliberation_round(self) -> tuple[str, str, list[tuple]]:
163
+ """Run a single round of AI juror deliberation.
164
+
165
+ Returns:
166
+ Tuple of (vote_html, jury_html, new_chat_messages)
167
+ """
168
+ if not self.game_state or not self.current_case:
169
+ return "", "", []
170
+
171
+ self.game_state.round_number += 1
172
+ new_messages = []
173
+
174
+ # Select 1-3 random speakers (excluding player)
175
+ available_jurors = [
176
+ jid for jid in self.juror_agents.keys()
177
+ if jid != "juror_7"
178
+ ]
179
+ num_speakers = random.randint(1, min(3, len(available_jurors)))
180
+ speakers = random.sample(available_jurors, num_speakers)
181
+
182
+ # Each speaker makes an argument
183
+ for speaker_id in speakers:
184
+ agent = self.juror_agents[speaker_id]
185
+ config = next(c for c in self.juror_configs if c.juror_id == speaker_id)
186
+
187
+ try:
188
+ # Generate argument
189
+ turn = await agent.generate_argument(self.current_case, self.game_state)
190
+
191
+ # Add to chat
192
+ new_messages.append((
193
+ None,
194
+ f"**{config.emoji} {config.name}:** {turn.content}"
195
+ ))
196
+
197
+ # Other jurors react
198
+ for other_id, other_agent in self.juror_agents.items():
199
+ if other_id != speaker_id:
200
+ # Calculate impact based on archetype compatibility
201
+ base_impact = random.uniform(-0.15, 0.15)
202
+ impact = calculate_conviction_change(
203
+ other_agent.config,
204
+ other_agent.memory,
205
+ turn,
206
+ base_impact=base_impact
207
+ )
208
+ other_agent.receive_argument(turn, impact)
209
+
210
+ # Update vote if conviction changed significantly
211
+ new_vote = other_agent.get_vote()
212
+ if self.game_state.votes.get(other_id) != new_vote:
213
+ self.game_state.votes[other_id] = new_vote
214
+ vote_text = "GUILTY" if new_vote == "guilty" else "NOT GUILTY"
215
+ other_config = next(c for c in self.juror_configs if c.juror_id == other_id)
216
+ new_messages.append((
217
+ None,
218
+ f"*{other_config.emoji} {other_config.name} changes their vote to {vote_text}*"
219
+ ))
220
+
221
+ # Log turn
222
+ self.game_state.deliberation_log.append(turn)
223
+
224
+ except Exception as e:
225
+ print(f"Error in deliberation for {speaker_id}: {e}")
226
+
227
+ # Update displays
228
+ vote_html = render_vote_tally(self.game_state)
229
+ jury_html = render_jury_box(self.juror_configs, self.game_state)
230
+
231
+ return vote_html, jury_html, new_messages
232
+
233
+ def make_player_argument(
234
+ self,
235
+ strategy: str,
236
+ custom_text: str | None = None
237
+ ) -> tuple[str, str, list[tuple]]:
238
+ """Process player's argument.
239
+
240
+ Returns:
241
+ Tuple of (vote_html, jury_html, new_chat_messages)
242
+ """
243
+ if not self.game_state or not self.current_case:
244
+ return "", "", []
245
+
246
+ # Get player config
247
+ player_config = next(c for c in self.juror_configs if c.archetype == "player")
248
+
249
+ # Create argument content
250
+ if custom_text:
251
+ content = custom_text
252
+ else:
253
+ # Generate content based on strategy
254
+ strategy_templates = {
255
+ "Challenge Evidence": "I'd like to point out some issues with the evidence presented...",
256
+ "Question Witness Credibility": "Can we really trust the witness testimony here?",
257
+ "Appeal to Reasonable Doubt": "Remember, we need proof beyond reasonable doubt. Do we have that?",
258
+ "Present Alternative Theory": "What if there's another explanation for what happened?",
259
+ "Call for Vote": "I think we should take a vote and see where everyone stands.",
260
+ }
261
+ content = strategy_templates.get(strategy, "I have some concerns about this case...")
262
+
263
+ # Create turn
264
+ turn = DeliberationTurn(
265
+ round_number=self.game_state.round_number,
266
+ speaker_id="juror_7",
267
+ speaker_name="You",
268
+ argument_type=strategy.lower().replace(" ", "_"),
269
+ content=content,
270
+ )
271
+
272
+ new_messages = [(f"**{player_config.emoji} You:** {content}", None)]
273
+
274
+ # AI jurors react
275
+ for juror_id, agent in self.juror_agents.items():
276
+ base_impact = random.uniform(-0.1, 0.1)
277
+ # Player arguments have moderate base influence
278
+ base_impact *= 1.2
279
+ impact = calculate_conviction_change(
280
+ agent.config,
281
+ agent.memory,
282
+ turn,
283
+ base_impact=base_impact
284
+ )
285
+ agent.receive_argument(turn, impact)
286
+
287
+ # Check for vote changes
288
+ new_vote = agent.get_vote()
289
+ if self.game_state.votes.get(juror_id) != new_vote:
290
+ self.game_state.votes[juror_id] = new_vote
291
+ config = next(c for c in self.juror_configs if c.juror_id == juror_id)
292
+ vote_text = "GUILTY" if new_vote == "guilty" else "NOT GUILTY"
293
+ new_messages.append((
294
+ None,
295
+ f"*{config.emoji} {config.name} changes their vote to {vote_text}*"
296
+ ))
297
+
298
+ self.game_state.deliberation_log.append(turn)
299
+
300
+ vote_html = render_vote_tally(self.game_state)
301
+ jury_html = render_jury_box(self.juror_configs, self.game_state)
302
+
303
+ return vote_html, jury_html, new_messages
304
+
305
+
306
+ # Create global game instance
307
+ game = JuryDeliberation()
308
+
309
+
310
+ def create_ui():
311
+ """Create the Gradio UI."""
312
+
313
+ with gr.Blocks(css=CUSTOM_CSS, title="12 Angry Agents", theme=gr.themes.Base()) as demo:
314
+
315
+ # State
316
+ chat_history = gr.State([])
317
+
318
+ # Header
319
+ gr.HTML("<h1 class='jury-title'>12 ANGRY AGENTS</h1>")
320
+ gr.HTML("<p style='text-align: center; color: #888;'>AI-Powered Jury Deliberation</p>")
321
+
322
+ with gr.Row():
323
+ # Left Column: Jury Box
324
+ with gr.Column(scale=1):
325
+ gr.Markdown("### The Jury")
326
+ jury_box = gr.HTML(render_jury_box(game.juror_configs))
327
+ vote_tally = gr.HTML("")
328
+
329
+ gr.Markdown("### Your Side")
330
+ with gr.Row():
331
+ defend_btn = gr.Button("DEFEND\n(Not Guilty)", variant="secondary")
332
+ prosecute_btn = gr.Button("PROSECUTE\n(Guilty)", variant="primary")
333
+
334
+ # Center Column: Deliberation
335
+ with gr.Column(scale=2):
336
+ gr.Markdown("### Deliberation Room")
337
+ deliberation_chat = gr.Chatbot(
338
+ label="Deliberation",
339
+ height=400,
340
+ show_label=False,
341
+ )
342
+
343
+ # Player input
344
+ with gr.Row():
345
+ strategy_select = gr.Dropdown(
346
+ choices=[
347
+ "Challenge Evidence",
348
+ "Question Witness Credibility",
349
+ "Appeal to Reasonable Doubt",
350
+ "Present Alternative Theory",
351
+ "Call for Vote",
352
+ ],
353
+ label="Your Strategy",
354
+ value="Challenge Evidence",
355
+ )
356
+ speak_btn = gr.Button("Speak", variant="primary")
357
+
358
+ custom_text = gr.Textbox(
359
+ label="Custom argument (optional)",
360
+ placeholder="Type your own argument here...",
361
+ max_lines=2,
362
+ )
363
+
364
+ with gr.Row():
365
+ pass_btn = gr.Button("Pass Turn")
366
+ next_round_btn = gr.Button("Next Round (AI Speaks)", variant="secondary")
367
+
368
+ # Right Column: Case File
369
+ with gr.Column(scale=1):
370
+ gr.Markdown("### Case File")
371
+ case_summary = gr.Markdown("*Click 'Start New Case' to begin*")
372
+
373
+ with gr.Accordion("Evidence & Witnesses", open=False):
374
+ evidence_display = gr.Markdown("")
375
+
376
+ # Start Game Button
377
+ start_btn = gr.Button("Start New Case", variant="primary", size="lg")
378
+
379
+ # Event handlers
380
+ def start_game():
381
+ case_md, evidence_md, jury_html, chat = game.initialize_game()
382
+ return case_md, evidence_md, jury_html, chat, render_vote_tally(game.game_state)
383
+
384
+ start_btn.click(
385
+ fn=start_game,
386
+ inputs=[],
387
+ outputs=[case_summary, evidence_display, jury_box, deliberation_chat, vote_tally]
388
+ )
389
+
390
+ def select_defend(chat):
391
+ vote_html, jury_html, new_msgs = game.select_player_side("defend")
392
+ return vote_html, jury_html, chat + new_msgs
393
+
394
+ def select_prosecute(chat):
395
+ vote_html, jury_html, new_msgs = game.select_player_side("prosecute")
396
+ return vote_html, jury_html, chat + new_msgs
397
+
398
+ defend_btn.click(
399
+ fn=select_defend,
400
+ inputs=[deliberation_chat],
401
+ outputs=[vote_tally, jury_box, deliberation_chat]
402
+ )
403
+
404
+ prosecute_btn.click(
405
+ fn=select_prosecute,
406
+ inputs=[deliberation_chat],
407
+ outputs=[vote_tally, jury_box, deliberation_chat]
408
+ )
409
+
410
+ async def run_ai_round(chat):
411
+ vote_html, jury_html, new_msgs = await game.run_deliberation_round()
412
+ return vote_html, jury_html, chat + new_msgs
413
+
414
+ next_round_btn.click(
415
+ fn=run_ai_round,
416
+ inputs=[deliberation_chat],
417
+ outputs=[vote_tally, jury_box, deliberation_chat]
418
+ )
419
+
420
+ def player_speak(strategy, custom, chat):
421
+ text = custom if custom else None
422
+ vote_html, jury_html, new_msgs = game.make_player_argument(strategy, text)
423
+ return vote_html, jury_html, chat + new_msgs, ""
424
+
425
+ speak_btn.click(
426
+ fn=player_speak,
427
+ inputs=[strategy_select, custom_text, deliberation_chat],
428
+ outputs=[vote_tally, jury_box, deliberation_chat, custom_text]
429
+ )
430
+
431
+ def pass_turn(chat):
432
+ chat.append(("*You pass your turn*", None))
433
+ return chat
434
+
435
+ pass_btn.click(
436
+ fn=pass_turn,
437
+ inputs=[deliberation_chat],
438
+ outputs=[deliberation_chat]
439
+ )
440
+
441
+ # Enable MCP server
442
+ return demo
443
+
444
+
445
+ # Main entry point
446
+ if __name__ == "__main__":
447
+ demo = create_ui()
448
+ demo.launch(
449
+ mcp_server=True,
450
+ share=False,
451
+ server_name="0.0.0.0",
452
+ server_port=7860,
453
+ )
config/models.yaml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model configuration for 12 Angry Agents
2
+
3
+ default_model:
4
+ provider: "gemini"
5
+ model_id: "gemini-2.5-flash"
6
+ temperature: 0.7
7
+ max_tokens: 1024
8
+
9
+ # Optional per-agent overrides
10
+ model_overrides:
11
+ # Judge narration
12
+ judge:
13
+ provider: "gemini"
14
+ model_id: "gemini-2.5-flash"
15
+ temperature: 0.5
16
+
17
+ # Batch conviction updates
18
+ batch_updater:
19
+ provider: "gemini"
20
+ model_id: "gemini-2.5-flash"
21
+ temperature: 0.3
22
+
23
+ # Example: Give the contrarian philosopher a different model
24
+ # juror_5:
25
+ # provider: "anthropic"
26
+ # model_id: "claude-sonnet-4-20250514"
27
+ # temperature: 0.9
mcp/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """MCP server tools for 12 Angry Agents."""
2
+
3
+ # MCP tools will be defined in app.py via Gradio's mcp_server=True
4
+ # This module is reserved for future tool definitions
5
+
6
+ __all__ = []
memory/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Memory management for juror agents."""
2
+
3
+ from .summarizer import MemorySummarizer
4
+
5
+ __all__ = ["MemorySummarizer"]
memory/summarizer.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Memory summarization for context window management."""
2
+
3
+ from core.models import JurorMemory, ArgumentMemory
4
+
5
+
6
+ class MemorySummarizer:
7
+ """Compresses old deliberation history to manage context window."""
8
+
9
+ SUMMARY_INTERVAL = 5 # Summarize every 5 rounds
10
+ KEEP_RECENT = 3 # Keep last 3 turns in full detail
11
+
12
+ def should_summarize(self, round_num: int) -> bool:
13
+ """Check if we should summarize at this round."""
14
+ return round_num > 0 and round_num % self.SUMMARY_INTERVAL == 0
15
+
16
+ def summarize(self, memory: JurorMemory) -> None:
17
+ """Compress old turns in memory if needed.
18
+
19
+ Args:
20
+ memory: Juror memory to summarize.
21
+ """
22
+ if len(memory.arguments_heard) <= self.KEEP_RECENT:
23
+ return
24
+
25
+ # Split: recent (keep full) vs old (summarize)
26
+ old_turns = memory.arguments_heard[:-self.KEEP_RECENT]
27
+ recent_turns = memory.arguments_heard[-self.KEEP_RECENT:]
28
+
29
+ if not old_turns:
30
+ return
31
+
32
+ # Create simple text summary
33
+ summary_parts = []
34
+ for arg in old_turns:
35
+ summary_parts.append(
36
+ f"- {arg.speaker_id} made a {arg.argument_type} argument"
37
+ )
38
+
39
+ # Append to existing summary
40
+ if memory.deliberation_summary:
41
+ memory.deliberation_summary += "\n" + "\n".join(summary_parts)
42
+ else:
43
+ memory.deliberation_summary = "\n".join(summary_parts)
44
+
45
+ # Keep only recent turns
46
+ memory.arguments_heard = recent_turns
47
+
48
+ def get_memory_context(self, memory: JurorMemory) -> str:
49
+ """Get formatted memory context for prompts.
50
+
51
+ Args:
52
+ memory: Juror memory.
53
+
54
+ Returns:
55
+ Formatted string of memory context.
56
+ """
57
+ parts = []
58
+
59
+ if memory.deliberation_summary:
60
+ parts.append("## Earlier Discussion Summary")
61
+ parts.append(memory.deliberation_summary)
62
+
63
+ if memory.arguments_heard:
64
+ parts.append("## Recent Arguments")
65
+ for arg in memory.arguments_heard:
66
+ parts.append(f"- {arg.speaker_id} [{arg.argument_type}]: {arg.content_summary}")
67
+
68
+ return "\n".join(parts) if parts else "No deliberation history yet."
requirements.txt CHANGED
@@ -1,5 +1,6 @@
1
  # Core
2
- gradio==6.0.1
 
3
  pydantic>=2.0.0
4
  pydantic-settings>=2.0.0
5
 
 
1
  # Core
2
+ gradio[mcp]==6.0.1
3
+ pyyaml>=6.0.0
4
  pydantic>=2.0.0
5
  pydantic-settings>=2.0.0
6