Spaces:
Running
Running
| """12 Angry Agents - Main Gradio Application. | |
| AI-powered jury deliberation simulation where 11 AI agents + 1 human player | |
| debate criminal cases. Uses smolagents CodeAgent with LlamaIndex-powered tools. | |
| """ | |
| import asyncio | |
| from pathlib import Path | |
| import gradio as gr | |
| from dotenv import load_dotenv | |
| # Load environment variables | |
| load_dotenv() | |
| # Add project root to path for imports | |
| import sys | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| from core.game_state import GameState, GamePhase | |
| from core.orchestrator import OrchestratorAgent | |
| from case_db import CaseLoader, CriminalCase, CaseIndexFactory | |
| from agents import load_juror_configs, SmolagentJuror | |
| from ui.components import render_jury_box, render_vote_tally | |
| from services.mcp import MCPService, init_mcp_service, register_mcp_tools | |
| from services.tts import get_tts_service | |
| from services.narration import JudgeNarration | |
| # CSS file path for scalable styling | |
| CSS_PATH = Path(__file__).parent / "ui" / "static" / "styles.css" | |
| class JuryDeliberation: | |
| """Main game orchestrator - delegates to OrchestratorAgent.""" | |
| def __init__(self): | |
| self.case_loader = CaseLoader() | |
| self.juror_configs = load_juror_configs() | |
| self.juror_agents: dict[str, SmolagentJuror] = {} | |
| self.orchestrator: OrchestratorAgent | None = None | |
| self.current_case: CriminalCase | None = None | |
| self.mcp_service: MCPService | None = None | |
| self._case_index = None # Store reference for MCP service | |
| def game_state(self) -> GameState | None: | |
| """Get game state from orchestrator.""" | |
| return self.orchestrator.game_state if self.orchestrator else None | |
| async def initialize_game(self, case_id: str | None = None) -> tuple[str, str, str, list, str, bytes | None, str]: | |
| """Initialize a new game. | |
| Returns: | |
| Tuple of (case_summary, evidence_html, jury_box_html, chat_history, vote_html, audio_bytes, transcript) | |
| """ | |
| # Load case | |
| if case_id: | |
| self.current_case = self.case_loader.get_case(case_id) | |
| else: | |
| self.current_case = self.case_loader.get_random_case() | |
| if not self.current_case: | |
| return ( | |
| "No cases available. Please add case files to case_db/cases/", | |
| "", | |
| render_jury_box(self.juror_configs), | |
| [], | |
| "", | |
| None, | |
| "" | |
| ) | |
| # Create LlamaIndex for this case | |
| case_index = CaseIndexFactory.get_index(self.current_case) | |
| self._case_index = case_index # Store reference for MCP service | |
| # Initialize juror agents with case index (skip player seat 7) | |
| self.juror_agents = {} | |
| for config in self.juror_configs: | |
| if config.archetype != "player": | |
| try: | |
| agent = SmolagentJuror( | |
| config, | |
| case_index=case_index, | |
| case=self.current_case | |
| ) | |
| agent.set_initial_conviction(self.current_case) | |
| self.juror_agents[config.juror_id] = agent | |
| except Exception as e: | |
| print(f"Warning: Failed to initialize {config.name}: {e}") | |
| # Create orchestrator with agents | |
| self.orchestrator = OrchestratorAgent( | |
| juror_configs=self.juror_configs, | |
| juror_agents=self.juror_agents, | |
| case=self.current_case | |
| ) | |
| self.orchestrator.state.phase = GamePhase.PRESENTATION | |
| # Initialize MCP service for external agent participation | |
| self.mcp_service = MCPService( | |
| orchestrator=self.orchestrator, | |
| case_index=self._case_index, | |
| juror_configs=self.juror_configs, | |
| juror_agents=self.juror_agents | |
| ) | |
| init_mcp_service(self.mcp_service) | |
| # Format case info | |
| case_summary = f"""## {self.current_case.title} | |
| **Year:** {self.current_case.year} | **Jurisdiction:** {self.current_case.jurisdiction} | |
| **Charges:** {self.current_case.get_charges_text()} | |
| --- | |
| {self.current_case.summary} | |
| """ | |
| evidence_html = f"""### Evidence | |
| {self.current_case.get_evidence_summary()} | |
| ### Witnesses | |
| {self.current_case.get_witness_summary()} | |
| """ | |
| jury_html = render_jury_box(self.juror_configs, self.game_state) | |
| # Generate judge narration | |
| transcript = JudgeNarration.case_introduction(self.current_case) | |
| audio_bytes = None | |
| tts = get_tts_service() | |
| if tts.is_available: | |
| audio_bytes = await tts.asynthesize(transcript, style="formal", use_cache=True) | |
| chat_history = [ | |
| {"role": "assistant", "content": f"**Judge:** {transcript}"} | |
| ] | |
| vote_html = render_vote_tally(self.game_state) | |
| return case_summary, evidence_html, jury_html, chat_history, vote_html, audio_bytes, transcript | |
| def select_player_side(self, side: str) -> tuple[str, str, list]: | |
| """Player selects prosecution or defense side.""" | |
| if not self.orchestrator: | |
| return "", "", [] | |
| self.orchestrator.set_player_side(side) | |
| vote_html = render_vote_tally(self.game_state) | |
| jury_html = render_jury_box(self.juror_configs, self.game_state) | |
| side_text = "PROSECUTION (Guilty)" if side == "prosecute" else "DEFENSE (Not Guilty)" | |
| chat_update = [{"role": "user", "content": f"*You have chosen to argue for the {side_text}*"}] | |
| return vote_html, jury_html, chat_update | |
| async def run_deliberation_round(self, prev_tally: tuple | None = None) -> tuple[str, str, list[dict], bytes | None, str, tuple]: | |
| """Run a single round of AI juror deliberation using fair speaker queue. | |
| Args: | |
| prev_tally: Previous vote tally (guilty, not_guilty) for shift detection. | |
| Returns: | |
| Tuple of (vote_html, jury_html, new_messages, audio_bytes, transcript, new_tally) | |
| """ | |
| if not self.orchestrator or not self.current_case: | |
| return "", "", [], None, "", (0, 0) | |
| new_messages = [] | |
| # Use orchestrator's fair speaker selection | |
| results = await self.orchestrator.run_deliberation_round() | |
| # Format results for chat | |
| for result in results: | |
| turn = result.turn | |
| config = next(c for c in self.juror_configs if c.juror_id == turn.speaker_id) | |
| # Speaker's argument | |
| new_messages.append( | |
| {"role": "assistant", "content": f"**{config.emoji} {config.name}:** {turn.content}"} | |
| ) | |
| # Show reasoning steps if available (smolagents feature) | |
| if result.reasoning_steps: | |
| reasoning_text = "\n".join([f" {step}" for step in result.reasoning_steps]) | |
| new_messages.append({ | |
| "role": "assistant", | |
| "content": f"*{config.emoji} {config.name}'s reasoning:*\n```\n{reasoning_text}\n```" | |
| }) | |
| # Show tool calls if any | |
| if result.tool_calls: | |
| tools_text = ", ".join(result.tool_calls) | |
| new_messages.append({ | |
| "role": "assistant", | |
| "content": f"*Tools used: {tools_text}*" | |
| }) | |
| # Vote changes | |
| for juror_id, old_vote, new_vote in result.vote_changes: | |
| other_config = next(c for c in self.juror_configs if c.juror_id == juror_id) | |
| vote_text = "GUILTY" if new_vote == "guilty" else "NOT GUILTY" | |
| new_messages.append( | |
| {"role": "assistant", "content": f"*{other_config.emoji} {other_config.name} changes their vote to {vote_text}*"} | |
| ) | |
| vote_html = render_vote_tally(self.game_state) | |
| jury_html = render_jury_box(self.juror_configs, self.game_state) | |
| # Check for significant vote shift and generate narration | |
| new_guilty, new_not_guilty = self.game_state.get_vote_tally() | |
| new_tally = (new_guilty, new_not_guilty) | |
| audio_bytes = None | |
| transcript = "" | |
| if prev_tally: | |
| old_g, old_ng = prev_tally | |
| shift = abs(new_guilty - old_g) | |
| if shift >= 2: | |
| direction = "conviction" if new_guilty > old_g else "acquittal" | |
| transcript = JudgeNarration.vote_shift(shift, direction, new_guilty, new_not_guilty) | |
| if transcript: | |
| tts = get_tts_service() | |
| if tts.is_available: | |
| audio_bytes = await tts.asynthesize(transcript, style="dramatic", use_cache=False) | |
| new_messages.append({"role": "assistant", "content": f"**Judge:** {transcript}"}) | |
| return vote_html, jury_html, new_messages, audio_bytes, transcript, new_tally | |
| def make_player_argument( | |
| self, | |
| strategy: str, | |
| custom_text: str | None = None | |
| ) -> tuple[str, str, list[tuple]]: | |
| """Process player's argument using orchestrator.""" | |
| if not self.orchestrator or not self.current_case: | |
| return "", "", [] | |
| player_config = next(c for c in self.juror_configs if c.archetype == "player") | |
| # Create argument content | |
| if custom_text: | |
| content = custom_text | |
| else: | |
| strategy_templates = { | |
| "Challenge Evidence": "I'd like to point out some issues with the evidence presented...", | |
| "Question Witness Credibility": "Can we really trust the witness testimony here?", | |
| "Appeal to Reasonable Doubt": "Remember, we need proof beyond reasonable doubt. Do we have that?", | |
| "Present Alternative Theory": "What if there's another explanation for what happened?", | |
| "Call for Vote": "I think we should take a vote and see where everyone stands.", | |
| } | |
| content = strategy_templates.get(strategy, "I have some concerns about this case...") | |
| # Use orchestrator to process | |
| argument_type = strategy.lower().replace(" ", "_") | |
| result = self.orchestrator.process_player_argument(content, argument_type) | |
| new_messages = [{"role": "user", "content": f"**{player_config.emoji} You:** {content}"}] | |
| # Vote changes | |
| for juror_id, old_vote, new_vote in result.vote_changes: | |
| config = next(c for c in self.juror_configs if c.juror_id == juror_id) | |
| vote_text = "GUILTY" if new_vote == "guilty" else "NOT GUILTY" | |
| new_messages.append( | |
| {"role": "assistant", "content": f"*{config.emoji} {config.name} changes their vote to {vote_text}*"} | |
| ) | |
| vote_html = render_vote_tally(self.game_state) | |
| jury_html = render_jury_box(self.juror_configs, self.game_state) | |
| return vote_html, jury_html, new_messages | |
| # Create global game instance | |
| game = JuryDeliberation() | |
| def create_ui(): | |
| """Create the Gradio UI.""" | |
| with gr.Blocks() as demo: | |
| # State | |
| chat_history = gr.State([]) | |
| # Header | |
| gr.HTML("<h1 class='jury-title'>12 ANGRY AGENTS</h1>", elem_id="app-title") | |
| gr.HTML("<p class='jury-subtitle'>AI-Powered Jury Deliberation</p>", elem_id="app-subtitle") | |
| with gr.Row(elem_id="main-row"): | |
| # Left Column: Jury Box | |
| with gr.Column(scale=1, elem_id="jury-column"): | |
| gr.Markdown("### The Jury") | |
| jury_box = gr.HTML( | |
| render_jury_box(game.juror_configs), | |
| elem_id="jury-box-container" | |
| ) | |
| vote_tally = gr.HTML("", elem_id="vote-tally-container") | |
| # Judge narration section | |
| gr.Markdown("### Judge") | |
| with gr.Group(elem_id="judge-narration"): | |
| judge_audio = gr.Audio( | |
| label="", | |
| autoplay=True, | |
| elem_id="judge-audio", | |
| visible=True | |
| ) | |
| judge_transcript = gr.Textbox( | |
| label="", | |
| lines=2, | |
| interactive=False, | |
| elem_id="judge-transcript", | |
| show_label=False | |
| ) | |
| gr.Markdown("### Your Side") | |
| with gr.Row(elem_id="side-selection-row"): | |
| defend_btn = gr.Button( | |
| "DEFEND\n(Not Guilty)", | |
| variant="secondary", | |
| elem_id="defend-btn" | |
| ) | |
| prosecute_btn = gr.Button( | |
| "PROSECUTE\n(Guilty)", | |
| variant="primary", | |
| elem_id="prosecute-btn" | |
| ) | |
| # Center Column: Deliberation | |
| with gr.Column(scale=2, elem_id="deliberation-column"): | |
| gr.Markdown("### Deliberation Room") | |
| deliberation_chat = gr.Chatbot( | |
| label="Deliberation", | |
| height=400, | |
| show_label=False, | |
| elem_id="deliberation-chat", | |
| ) | |
| # Player input | |
| with gr.Row(elem_id="player-input-row"): | |
| strategy_select = gr.Dropdown( | |
| choices=[ | |
| "Challenge Evidence", | |
| "Question Witness Credibility", | |
| "Appeal to Reasonable Doubt", | |
| "Present Alternative Theory", | |
| "Call for Vote", | |
| ], | |
| label="Your Strategy", | |
| value="Challenge Evidence", | |
| elem_id="strategy-select", | |
| ) | |
| speak_btn = gr.Button( | |
| "Speak", | |
| variant="primary", | |
| elem_id="speak-btn", | |
| elem_classes=["primary-btn"] | |
| ) | |
| custom_text = gr.Textbox( | |
| label="Custom argument (optional)", | |
| placeholder="Type your own argument here...", | |
| max_lines=2, | |
| elem_id="custom-argument", | |
| ) | |
| with gr.Row(elem_id="action-buttons-row"): | |
| pass_btn = gr.Button("Pass Turn", elem_id="pass-btn") | |
| next_round_btn = gr.Button( | |
| "Next Round (AI Speaks)", | |
| variant="secondary", | |
| elem_id="next-round-btn" | |
| ) | |
| # Right Column: Case File | |
| with gr.Column(scale=1, elem_id="case-column", elem_classes=["case-file"]): | |
| gr.Markdown("### Case File") | |
| case_summary = gr.Markdown( | |
| "*Click 'Start New Case' to begin*", | |
| elem_id="case-summary" | |
| ) | |
| with gr.Accordion("Evidence & Witnesses", open=False, elem_id="evidence-accordion"): | |
| evidence_display = gr.Markdown("", elem_id="evidence-display") | |
| # Start Game Button | |
| start_btn = gr.Button( | |
| "Start New Case", | |
| variant="primary", | |
| size="lg", | |
| elem_id="start-btn", | |
| elem_classes=["primary-btn"] | |
| ) | |
| # State for tracking vote tally for shift detection | |
| vote_tally_state = gr.State(None) | |
| # Event handlers | |
| async def start_game(): | |
| case_md, evidence_md, jury_html, chat, vote_html, audio_bytes, transcript = await game.initialize_game() | |
| initial_tally = game.game_state.get_vote_tally() if game.game_state else (0, 0) | |
| return case_md, evidence_md, jury_html, chat, vote_html, audio_bytes, transcript, initial_tally | |
| start_btn.click( | |
| fn=start_game, | |
| inputs=[], | |
| outputs=[case_summary, evidence_display, jury_box, deliberation_chat, vote_tally, judge_audio, judge_transcript, vote_tally_state] | |
| ) | |
| def select_defend(chat): | |
| vote_html, jury_html, new_msgs = game.select_player_side("defend") | |
| return vote_html, jury_html, chat + new_msgs | |
| def select_prosecute(chat): | |
| vote_html, jury_html, new_msgs = game.select_player_side("prosecute") | |
| return vote_html, jury_html, chat + new_msgs | |
| defend_btn.click( | |
| fn=select_defend, | |
| inputs=[deliberation_chat], | |
| outputs=[vote_tally, jury_box, deliberation_chat] | |
| ) | |
| prosecute_btn.click( | |
| fn=select_prosecute, | |
| inputs=[deliberation_chat], | |
| outputs=[vote_tally, jury_box, deliberation_chat] | |
| ) | |
| async def run_ai_round(chat, prev_tally): | |
| vote_html, jury_html, new_msgs, audio_bytes, transcript, new_tally = await game.run_deliberation_round(prev_tally) | |
| return vote_html, jury_html, chat + new_msgs, audio_bytes, transcript, new_tally | |
| next_round_btn.click( | |
| fn=run_ai_round, | |
| inputs=[deliberation_chat, vote_tally_state], | |
| outputs=[vote_tally, jury_box, deliberation_chat, judge_audio, judge_transcript, vote_tally_state] | |
| ) | |
| def player_speak(strategy, custom, chat): | |
| text = custom if custom else None | |
| vote_html, jury_html, new_msgs = game.make_player_argument(strategy, text) | |
| return vote_html, jury_html, chat + new_msgs, "" | |
| speak_btn.click( | |
| fn=player_speak, | |
| inputs=[strategy_select, custom_text, deliberation_chat], | |
| outputs=[vote_tally, jury_box, deliberation_chat, custom_text] | |
| ) | |
| def pass_turn(chat): | |
| chat.append({"role": "user", "content": "*You pass your turn*"}) | |
| return chat | |
| pass_btn.click( | |
| fn=pass_turn, | |
| inputs=[deliberation_chat], | |
| outputs=[deliberation_chat] | |
| ) | |
| # Register MCP tools for external AI agent participation | |
| register_mcp_tools(demo) | |
| return demo | |
| # Main entry point | |
| if __name__ == "__main__": | |
| demo = create_ui() | |
| demo.launch( | |
| mcp_server=True, | |
| share=False, | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| css_paths=[CSS_PATH], | |
| theme=gr.themes.Base(), | |
| ) | |