Spaces:
Running
Implement core game mechanics and UI for murder mystery investigation
Browse files- Added game engine with GameInstance class to manage game state, rounds, and suspect interactions.
- Integrated LLMManager for handling AI agents representing suspects and the detective.
- Developed scenario generator to load predefined crime scenarios based on difficulty.
- Created tools for investigation, including location tracking, footage retrieval, and DNA testing.
- Established a session management system to track active game instances.
- Designed prompts for different roles (detective, murderer, witness) to guide AI responses.
- Implemented a test suite to validate game logic and agent interactions.
- Developed basic UI components and styles for displaying evidence and suspect information.
- Added scenarios for various difficulty levels, including "The Gallery Heist Gone Wrong" and "The Coffee Shop Murder".
- .gitignore +5 -1
- GEMINI.md +56 -0
- GRADIO_DOCS_llms.txt +0 -0
- PLan.md +8 -5
- README.md +1 -0
- app.py +225 -4
- game/__init__.py +0 -0
- game/game_engine.py +134 -0
- game/llm_manager.py +93 -0
- game/scenario_generator.py +46 -0
- mcp/__init__.py +0 -0
- mcp/server.py +0 -0
- mcp/tools.py +102 -0
- prompts/alibi.txt +0 -0
- prompts/detective.txt +24 -0
- prompts/murderer.txt +32 -0
- prompts/witness.txt +19 -0
- requirements.txt +3 -0
- scenarios/art_gallery.json +101 -0
- scenarios/coffee_shop.json +114 -0
- scenarios/silicon_valley.json +132 -0
- test_game.py +44 -0
- ui/__init__.py +0 -0
- ui/components.py +0 -0
- ui/styles.css +30 -0
|
@@ -1 +1,5 @@
|
|
| 1 |
-
.mddd
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.mddd
|
| 2 |
+
.env
|
| 3 |
+
__pycache__/
|
| 4 |
+
.vscode/
|
| 5 |
+
game/__pycache__/
|
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Murder.Ai Project Context
|
| 2 |
+
|
| 3 |
+
## Project Overview
|
| 4 |
+
**Murder.Ai** is an AI-powered murder mystery game designed as a Gradio application. In this game, Large Language Models (LLMs) take on specific roles—Detective, Murderer, and Witnesses—to generate and solve unique crime scenarios.
|
| 5 |
+
|
| 6 |
+
The core concept involves an "MCP Story Generator" that creates consistent murder mystery cases. Users can watch the AI agents interact in "Spectator Mode" or actively participate in "Interactive Mode". The project is intended for deployment on Hugging Face Spaces.
|
| 7 |
+
|
| 8 |
+
## Current Status
|
| 9 |
+
The project is currently in the **initialization phase**.
|
| 10 |
+
- **Detailed Plan:** A comprehensive design document exists in `PLan.md` outlining the game flow, data structures, and UI design.
|
| 11 |
+
- **Implementation:** The `app.py` file is currently a basic "Hello World" Gradio placeholder. The folder structure and core logic defined in the plan have not yet been implemented.
|
| 12 |
+
|
| 13 |
+
## Architecture & Design
|
| 14 |
+
Based on `PLan.md`, the target architecture includes:
|
| 15 |
+
|
| 16 |
+
### Tech Stack
|
| 17 |
+
- **Frontend:** Gradio 5.x with custom HTML/CSS/JS.
|
| 18 |
+
- **Backend:** Python 3.11+ with FastAPI (embedded in Gradio).
|
| 19 |
+
- **AI/LLM:** Integration with Anthropic Claude, OpenAI GPT-4, Google Gemini, and Meta Llama.
|
| 20 |
+
- **Tools:** Model Context Protocol (MCP) tools for in-game actions like `get_location`, `get_footage`, and `get_dna_test`.
|
| 21 |
+
|
| 22 |
+
### Planned Structure
|
| 23 |
+
The roadmap suggests the following structure (to be implemented):
|
| 24 |
+
```
|
| 25 |
+
murder-ai/
|
| 26 |
+
├── app.py # Main Gradio app
|
| 27 |
+
├── requirements.txt # Dependencies
|
| 28 |
+
├── game/ # Game logic (scenario generator, engine, LLM manager)
|
| 29 |
+
├── mcp/ # MCP tool definitions and server
|
| 30 |
+
├── ui/ # Custom Gradio components and styles
|
| 31 |
+
├── prompts/ # Role-specific system prompts
|
| 32 |
+
├── scenarios/ # Pre-scripted fallback cases
|
| 33 |
+
└── assets/ # Images and media
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## Development Conventions
|
| 37 |
+
|
| 38 |
+
### Building & Running
|
| 39 |
+
Since the project is a Gradio app:
|
| 40 |
+
1. **Install Dependencies:** (When `requirements.txt` is created)
|
| 41 |
+
```bash
|
| 42 |
+
pip install -r requirements.txt
|
| 43 |
+
```
|
| 44 |
+
2. **Run the Application:**
|
| 45 |
+
```bash
|
| 46 |
+
python app.py
|
| 47 |
+
```
|
| 48 |
+
or
|
| 49 |
+
```bash
|
| 50 |
+
gradio app.py
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### Key Directives
|
| 54 |
+
- **Follow the Plan:** All development should align with the specifications in `PLan.md`.
|
| 55 |
+
- **Gradio 5:** Use the latest Gradio features, particularly for custom UI components and state management.
|
| 56 |
+
- **MCP Integration:** Tools should be designed to simulate real investigative data retrieval.
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -777,7 +777,7 @@ Deployment: HuggingFace Space
|
|
| 777 |
|
| 778 |
```yaml
|
| 779 |
Frontend:
|
| 780 |
-
- Gradio
|
| 781 |
- Custom HTML/CSS blocks for beautiful UI
|
| 782 |
- JavaScript for animations
|
| 783 |
|
|
@@ -808,7 +808,7 @@ Deployment:
|
|
| 808 |
- Modal for compute-heavy tasks (optional, for sponsor prize)
|
| 809 |
|
| 810 |
Dependencies:
|
| 811 |
-
- gradio>=
|
| 812 |
- anthropic
|
| 813 |
- openai
|
| 814 |
- google-generativeai
|
|
@@ -862,7 +862,7 @@ murder-ai/
|
|
| 862 |
|
| 863 |
## 🎨 **GRADIO UI ARCHITECTURE**
|
| 864 |
|
| 865 |
-
### **Modern Gradio
|
| 866 |
|
| 867 |
```python
|
| 868 |
import gradio as gr
|
|
@@ -895,7 +895,7 @@ custom_css = """
|
|
| 895 |
|
| 896 |
# Main Gradio Interface
|
| 897 |
```python
|
| 898 |
-
with gr.Blocks(
|
| 899 |
|
| 900 |
gr.Markdown("# 🕵️ MURDER.AI")
|
| 901 |
|
|
@@ -947,5 +947,8 @@ with gr.Blocks(css=custom_css, theme=gr.themes.Noir()) as demo:
|
|
| 947 |
outputs=[game_state, chatbot, tool_output, evidence_board]
|
| 948 |
)
|
| 949 |
|
| 950 |
-
demo.launch(
|
|
|
|
|
|
|
|
|
|
| 951 |
```
|
|
|
|
| 777 |
|
| 778 |
```yaml
|
| 779 |
Frontend:
|
| 780 |
+
- Gradio 6.x (required)
|
| 781 |
- Custom HTML/CSS blocks for beautiful UI
|
| 782 |
- JavaScript for animations
|
| 783 |
|
|
|
|
| 808 |
- Modal for compute-heavy tasks (optional, for sponsor prize)
|
| 809 |
|
| 810 |
Dependencies:
|
| 811 |
+
- gradio>=6.0
|
| 812 |
- anthropic
|
| 813 |
- openai
|
| 814 |
- google-generativeai
|
|
|
|
| 862 |
|
| 863 |
## 🎨 **GRADIO UI ARCHITECTURE**
|
| 864 |
|
| 865 |
+
### **Modern Gradio 6 Approach:**
|
| 866 |
|
| 867 |
```python
|
| 868 |
import gradio as gr
|
|
|
|
| 895 |
|
| 896 |
# Main Gradio Interface
|
| 897 |
```python
|
| 898 |
+
with gr.Blocks() as demo:
|
| 899 |
|
| 900 |
gr.Markdown("# 🕵️ MURDER.AI")
|
| 901 |
|
|
|
|
| 947 |
outputs=[game_state, chatbot, tool_output, evidence_board]
|
| 948 |
)
|
| 949 |
|
| 950 |
+
demo.launch(
|
| 951 |
+
theme=gr.themes.Noir(),
|
| 952 |
+
css=custom_css
|
| 953 |
+
)
|
| 954 |
```
|
|
@@ -13,3 +13,4 @@ tags:
|
|
| 13 |
- building-mcp-track-creative
|
| 14 |
- mcp-in-action-track-creative
|
| 15 |
---
|
|
|
|
|
|
| 13 |
- building-mcp-track-creative
|
| 14 |
- mcp-in-action-track-creative
|
| 15 |
---
|
| 16 |
+
- Read `PLan.md`
|
|
@@ -1,7 +1,228 @@
|
|
| 1 |
import gradio as gr
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
import os
|
| 3 |
+
from game import game_engine
|
| 4 |
|
| 5 |
+
# Load CSS
|
| 6 |
+
with open("ui/styles.css", "r") as f:
|
| 7 |
+
custom_css = f.read()
|
| 8 |
|
| 9 |
+
def get_current_game(session_id):
|
| 10 |
+
if not session_id:
|
| 11 |
+
return None
|
| 12 |
+
return game_engine.get_game(session_id)
|
| 13 |
+
|
| 14 |
+
def format_suspect_card(suspect):
|
| 15 |
+
"""Generates HTML for a suspect card."""
|
| 16 |
+
return f"""
|
| 17 |
+
<div class="suspect-card">
|
| 18 |
+
<h3>{suspect['name']}</h3>
|
| 19 |
+
<p><strong>Role:</strong> {suspect['role']}</p>
|
| 20 |
+
<p><strong>Bio:</strong> {suspect['bio']}</p>
|
| 21 |
+
<p>ID: {suspect['id']}</p>
|
| 22 |
+
</div>
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def start_new_game_ui(difficulty):
|
| 26 |
+
"""Starts a new game and returns initial UI state."""
|
| 27 |
+
session_id, game = game_engine.start_game(difficulty)
|
| 28 |
+
|
| 29 |
+
# Generate Suspect Cards HTML
|
| 30 |
+
cards = [format_suspect_card(s) for s in game.scenario["suspects"]]
|
| 31 |
+
# Pad if fewer than 4 (though scenarios have 4)
|
| 32 |
+
while len(cards) < 4:
|
| 33 |
+
cards.append("")
|
| 34 |
+
|
| 35 |
+
# Initial Evidence Board
|
| 36 |
+
evidence_html = f"<h3>Case: {game.scenario['title']}</h3><p>Victim: {game.scenario['victim']['name']}</p><hr>"
|
| 37 |
+
|
| 38 |
+
# Initial Chat
|
| 39 |
+
initial_chat = [
|
| 40 |
+
("System", f"CASE FILE LOADED: {game.scenario['title']}"),
|
| 41 |
+
("System", f"Victim: {game.scenario['victim']['name']}"),
|
| 42 |
+
("System", f"Time of Death: {game.scenario['victim']['time_of_death']}")
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
return (
|
| 46 |
+
session_id,
|
| 47 |
+
cards[0], cards[1], cards[2], cards[3], # Suspect cards
|
| 48 |
+
initial_chat, # Chatbot
|
| 49 |
+
evidence_html, # Evidence board
|
| 50 |
+
f"Round: {game.round}/5 | Points: {game.points}", # Stats
|
| 51 |
+
gr.update(choices=[(s['name'], s['id']) for s in game.scenario["suspects"]], value=None), # Suspect dropdown
|
| 52 |
+
gr.update(interactive=True), # Question input
|
| 53 |
+
gr.update(interactive=True), # Question btn
|
| 54 |
+
gr.update(interactive=True) # Tool btn
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
def submit_question(session_id, suspect_id, question, history):
|
| 58 |
+
game = get_current_game(session_id)
|
| 59 |
+
if not game:
|
| 60 |
+
return history, "Error: No active game."
|
| 61 |
+
|
| 62 |
+
if not suspect_id:
|
| 63 |
+
return history, "Error: Select a suspect first."
|
| 64 |
+
|
| 65 |
+
if not question:
|
| 66 |
+
return history, "Error: Enter a question."
|
| 67 |
+
|
| 68 |
+
response = game.question_suspect(suspect_id, question)
|
| 69 |
+
|
| 70 |
+
# Update chat
|
| 71 |
+
# Gradio chatbot format: list of [user_msg, bot_msg] or tuples.
|
| 72 |
+
# But here we have a multi-actor chat.
|
| 73 |
+
# Gradio's 'messages' type chatbot is better for this, but let's stick to standard
|
| 74 |
+
# tuple list (User, Bot) for simplicity, or just append to history.
|
| 75 |
+
# The plan said "Color-coded by speaker", implies standard Chatbot might be limiting
|
| 76 |
+
# if we want "Detective: ...", "Suspect: ...".
|
| 77 |
+
# We'll use (Speaker, Message) format and let Gradio handle it,
|
| 78 |
+
# or format it as "Speaker: Message" in the bubble.
|
| 79 |
+
|
| 80 |
+
history.append(("Detective", f"To {suspect_id}: {question}"))
|
| 81 |
+
history.append((suspect_id, response))
|
| 82 |
+
|
| 83 |
+
return history, "" # Clear input
|
| 84 |
+
|
| 85 |
+
def use_tool_ui(session_id, tool_name, arg1, arg2, history):
|
| 86 |
+
game = get_current_game(session_id)
|
| 87 |
+
if not game:
|
| 88 |
+
return history, "No game", "Error"
|
| 89 |
+
|
| 90 |
+
# Construct kwargs based on tool
|
| 91 |
+
kwargs = {}
|
| 92 |
+
if tool_name == "get_location":
|
| 93 |
+
kwargs = {"phone_number": arg1, "timestamp": arg2}
|
| 94 |
+
elif tool_name == "get_footage":
|
| 95 |
+
kwargs = {"location": arg1, "time_range": arg2}
|
| 96 |
+
elif tool_name == "get_dna_test":
|
| 97 |
+
kwargs = {"evidence_id": arg1}
|
| 98 |
+
elif tool_name == "call_alibi":
|
| 99 |
+
kwargs = {"phone_number": arg1}
|
| 100 |
+
|
| 101 |
+
result = game.use_tool(tool_name, **kwargs)
|
| 102 |
+
|
| 103 |
+
# Update History
|
| 104 |
+
history.append(("System", f"Used {tool_name}: {result}"))
|
| 105 |
+
|
| 106 |
+
# Update Evidence Board
|
| 107 |
+
ev_html = f"<h3>Case: {game.scenario['title']}</h3><p>Victim: {game.scenario['victim']['name']}</p><hr><h4>Evidence Revealed:</h4><ul>"
|
| 108 |
+
for item in game.evidence_revealed:
|
| 109 |
+
ev_html += f"<li>{str(item)}</li>"
|
| 110 |
+
ev_html += "</ul>"
|
| 111 |
+
|
| 112 |
+
stats = f"Round: {game.round}/5 | Points: {game.points}"
|
| 113 |
+
|
| 114 |
+
return history, ev_html, stats
|
| 115 |
+
|
| 116 |
+
def next_round_ui(session_id):
|
| 117 |
+
game = get_current_game(session_id)
|
| 118 |
+
if not game:
|
| 119 |
+
return "No game", "Error"
|
| 120 |
+
|
| 121 |
+
not_over = game.advance_round()
|
| 122 |
+
stats = f"Round: {game.round}/5 | Points: {game.points}"
|
| 123 |
+
|
| 124 |
+
if not not_over:
|
| 125 |
+
stats += " [GAME OVER]"
|
| 126 |
+
|
| 127 |
+
return stats
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# --- UI LAYOUT ---
|
| 131 |
+
|
| 132 |
+
with gr.Blocks(title="Murder.Ai") as demo:
|
| 133 |
+
|
| 134 |
+
gr.Markdown("# 🕵️ MURDER.AI")
|
| 135 |
+
|
| 136 |
+
session_state = gr.State("")
|
| 137 |
+
|
| 138 |
+
with gr.Row():
|
| 139 |
+
# Left: Suspect Cards
|
| 140 |
+
with gr.Column(scale=1):
|
| 141 |
+
gr.Markdown("## Suspects")
|
| 142 |
+
suspect_1 = gr.HTML(elem_classes="suspect-card")
|
| 143 |
+
suspect_2 = gr.HTML(elem_classes="suspect-card")
|
| 144 |
+
suspect_3 = gr.HTML(elem_classes="suspect-card")
|
| 145 |
+
suspect_4 = gr.HTML(elem_classes="suspect-card")
|
| 146 |
+
|
| 147 |
+
# Center: Main Game Area
|
| 148 |
+
with gr.Column(scale=2):
|
| 149 |
+
game_stats = gr.Markdown("Round: 0/5 | Points: 10")
|
| 150 |
+
|
| 151 |
+
chatbot = gr.Chatbot(
|
| 152 |
+
label="Investigation Log",
|
| 153 |
+
height=500,
|
| 154 |
+
type="messages" # Gradio 5/6 new format
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
with gr.Row():
|
| 158 |
+
suspect_dropdown = gr.Dropdown(label="Select Suspect", choices=[])
|
| 159 |
+
question_input = gr.Textbox(label="Question", placeholder="Where were you?", interactive=False)
|
| 160 |
+
ask_btn = gr.Button("Ask", variant="secondary", interactive=False)
|
| 161 |
+
|
| 162 |
+
gr.Markdown("### Tools")
|
| 163 |
+
with gr.Row():
|
| 164 |
+
tool_dropdown = gr.Dropdown(
|
| 165 |
+
label="Select Tool",
|
| 166 |
+
choices=["get_location", "get_footage", "get_dna_test", "call_alibi"],
|
| 167 |
+
value="get_location"
|
| 168 |
+
)
|
| 169 |
+
arg1_input = gr.Textbox(label="Arg 1 (Phone/Loc/ID)")
|
| 170 |
+
arg2_input = gr.Textbox(label="Arg 2 (Time)")
|
| 171 |
+
use_tool_btn = gr.Button("Use Tool", interactive=False)
|
| 172 |
+
|
| 173 |
+
next_round_btn = gr.Button("▶️ Next Round / End Game")
|
| 174 |
+
|
| 175 |
+
# Right: Evidence Board
|
| 176 |
+
with gr.Column(scale=1):
|
| 177 |
+
gr.Markdown("## Evidence Board")
|
| 178 |
+
evidence_board = gr.HTML(
|
| 179 |
+
elem_classes="evidence-board",
|
| 180 |
+
value="<p>No active case.</p>"
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
# Start Controls
|
| 184 |
+
with gr.Row():
|
| 185 |
+
difficulty_selector = gr.Radio(["Easy", "Medium", "Hard"], label="Difficulty", value="Medium")
|
| 186 |
+
start_btn = gr.Button("🎲 GENERATE NEW CASE", variant="primary")
|
| 187 |
+
|
| 188 |
+
# --- EVENTS ---
|
| 189 |
+
|
| 190 |
+
start_btn.click(
|
| 191 |
+
fn=start_new_game_ui,
|
| 192 |
+
inputs=[difficulty_selector],
|
| 193 |
+
outputs=[
|
| 194 |
+
session_state,
|
| 195 |
+
suspect_1, suspect_2, suspect_3, suspect_4,
|
| 196 |
+
chatbot,
|
| 197 |
+
evidence_board,
|
| 198 |
+
game_stats,
|
| 199 |
+
suspect_dropdown,
|
| 200 |
+
question_input,
|
| 201 |
+
ask_btn,
|
| 202 |
+
use_tool_btn
|
| 203 |
+
]
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
ask_btn.click(
|
| 207 |
+
fn=submit_question,
|
| 208 |
+
inputs=[session_state, suspect_dropdown, question_input, chatbot],
|
| 209 |
+
outputs=[chatbot, question_input]
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
use_tool_btn.click(
|
| 213 |
+
fn=use_tool_ui,
|
| 214 |
+
inputs=[session_state, tool_dropdown, arg1_input, arg2_input, chatbot],
|
| 215 |
+
outputs=[chatbot, evidence_board, game_stats]
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
next_round_btn.click(
|
| 219 |
+
fn=next_round_ui,
|
| 220 |
+
inputs=[session_state],
|
| 221 |
+
outputs=[game_stats]
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
demo.launch(
|
| 225 |
+
theme=gr.themes.Noir(),
|
| 226 |
+
css=custom_css,
|
| 227 |
+
allowed_paths=["."]
|
| 228 |
+
)
|
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from .scenario_generator import generate_crime_scenario
|
| 3 |
+
from .llm_manager import LLMManager
|
| 4 |
+
from mcp import tools
|
| 5 |
+
|
| 6 |
+
class GameInstance:
|
| 7 |
+
def __init__(self, difficulty="medium"):
|
| 8 |
+
self.id = str(uuid.uuid4())
|
| 9 |
+
self.scenario = generate_crime_scenario(difficulty)
|
| 10 |
+
self.llm_manager = LLMManager()
|
| 11 |
+
self.round = 1
|
| 12 |
+
self.max_rounds = 5
|
| 13 |
+
self.points = 10
|
| 14 |
+
self.evidence_revealed = [] # List of strings or result dicts
|
| 15 |
+
self.logs = [] # Chat logs
|
| 16 |
+
self.game_over = False
|
| 17 |
+
self.verdict_correct = False
|
| 18 |
+
|
| 19 |
+
# Initialize Agents
|
| 20 |
+
self._init_agents()
|
| 21 |
+
|
| 22 |
+
def _init_agents(self):
|
| 23 |
+
# 1. Detective
|
| 24 |
+
detective_context = {
|
| 25 |
+
"victim_name": self.scenario["victim"]["name"],
|
| 26 |
+
"time_of_death": self.scenario["victim"]["time_of_death"],
|
| 27 |
+
"location": self.scenario["evidence"]["location_data"].get("suspect_1_phone", {}).get("8:47 PM", {}).get("location", "Unknown"), # Approximate
|
| 28 |
+
"investigation_state": "Initial briefing."
|
| 29 |
+
}
|
| 30 |
+
self.llm_manager.create_agent("detective", "detective", detective_context)
|
| 31 |
+
|
| 32 |
+
# 2. Suspects
|
| 33 |
+
for suspect in self.scenario["suspects"]:
|
| 34 |
+
role = "murderer" if suspect["is_murderer"] else "witness"
|
| 35 |
+
|
| 36 |
+
# Context for prompt
|
| 37 |
+
context = {
|
| 38 |
+
"name": suspect["name"],
|
| 39 |
+
"victim_name": self.scenario["victim"]["name"],
|
| 40 |
+
"alibi_story": suspect["alibi_story"],
|
| 41 |
+
"bio": suspect["bio"],
|
| 42 |
+
"true_location": suspect["true_location"],
|
| 43 |
+
|
| 44 |
+
# Murderer specific
|
| 45 |
+
"method": self.scenario["title"], # Placeholder, scenario doesn't explicitly list 'method' field in plan, using title/weapon logic implied
|
| 46 |
+
"location": self.scenario["victim"].get("location", "the scene"), # Using generic
|
| 47 |
+
"motive": suspect["motive"]
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
self.llm_manager.create_agent(suspect["id"], role, context)
|
| 51 |
+
|
| 52 |
+
def log_event(self, speaker, message):
|
| 53 |
+
self.logs.append({"speaker": speaker, "message": message})
|
| 54 |
+
|
| 55 |
+
def question_suspect(self, suspect_id, question):
|
| 56 |
+
if self.game_over:
|
| 57 |
+
return "Game Over"
|
| 58 |
+
|
| 59 |
+
suspect_name = next((s["name"] for s in self.scenario["suspects"] if s["id"] == suspect_id), "Unknown")
|
| 60 |
+
|
| 61 |
+
# 1. Detective asks (simulated log)
|
| 62 |
+
self.log_event("Detective", f"To {suspect_name}: {question}")
|
| 63 |
+
|
| 64 |
+
# 2. Suspect responds
|
| 65 |
+
response = self.llm_manager.get_response(suspect_id, question)
|
| 66 |
+
self.log_event(suspect_name, response)
|
| 67 |
+
|
| 68 |
+
return response
|
| 69 |
+
|
| 70 |
+
def use_tool(self, tool_name, **kwargs):
|
| 71 |
+
if self.points <= 0:
|
| 72 |
+
return {"error": "Not enough investigation points!"}
|
| 73 |
+
|
| 74 |
+
cost = 0
|
| 75 |
+
result = {}
|
| 76 |
+
|
| 77 |
+
# Map tool names to functions
|
| 78 |
+
if tool_name == "get_location":
|
| 79 |
+
cost = 2
|
| 80 |
+
result = tools.get_location(self.scenario, kwargs.get("phone_number"), kwargs.get("timestamp"))
|
| 81 |
+
elif tool_name == "get_footage":
|
| 82 |
+
cost = 2
|
| 83 |
+
result = tools.get_footage(self.scenario, kwargs.get("location"), kwargs.get("time_range"))
|
| 84 |
+
elif tool_name == "get_dna_test":
|
| 85 |
+
cost = 3
|
| 86 |
+
result = tools.get_dna_test(self.scenario, kwargs.get("evidence_id"))
|
| 87 |
+
elif tool_name == "call_alibi":
|
| 88 |
+
cost = 1
|
| 89 |
+
result = tools.call_alibi(self.scenario, kwargs.get("phone_number"))
|
| 90 |
+
else:
|
| 91 |
+
return {"error": f"Unknown tool: {tool_name}"}
|
| 92 |
+
|
| 93 |
+
if "error" in result:
|
| 94 |
+
return result # Don't deduct points for errors
|
| 95 |
+
|
| 96 |
+
self.points -= cost
|
| 97 |
+
self.evidence_revealed.append(result)
|
| 98 |
+
self.log_event("System", f"Used {tool_name}. Cost: {cost} pts. Result: {str(result)}")
|
| 99 |
+
return result
|
| 100 |
+
|
| 101 |
+
def advance_round(self):
|
| 102 |
+
if self.round < self.max_rounds:
|
| 103 |
+
self.round += 1
|
| 104 |
+
return True
|
| 105 |
+
else:
|
| 106 |
+
self.game_over = True
|
| 107 |
+
return False
|
| 108 |
+
|
| 109 |
+
def make_accusation(self, suspect_id):
|
| 110 |
+
self.game_over = True
|
| 111 |
+
|
| 112 |
+
# Check if correct
|
| 113 |
+
murderer = next((s for s in self.scenario["suspects"] if s["is_murderer"]), None)
|
| 114 |
+
|
| 115 |
+
if murderer and murderer["id"] == suspect_id:
|
| 116 |
+
self.verdict_correct = True
|
| 117 |
+
msg = f"CORRECT! {murderer['name']} was the murderer."
|
| 118 |
+
else:
|
| 119 |
+
self.verdict_correct = False
|
| 120 |
+
msg = f"INCORRECT. The true murderer was {murderer['name'] if murderer else 'Unknown'}."
|
| 121 |
+
|
| 122 |
+
self.log_event("System", f"FINAL ACCUSATION: {suspect_id}. Result: {msg}")
|
| 123 |
+
return msg
|
| 124 |
+
|
| 125 |
+
# Global Session Store
|
| 126 |
+
SESSIONS = {}
|
| 127 |
+
|
| 128 |
+
def start_game(difficulty="medium"):
|
| 129 |
+
game = GameInstance(difficulty)
|
| 130 |
+
SESSIONS[game.id] = game
|
| 131 |
+
return game.id, game
|
| 132 |
+
|
| 133 |
+
def get_game(session_id):
|
| 134 |
+
return SESSIONS.get(session_id)
|
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import google.generativeai as genai
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
# Configure the API
|
| 8 |
+
API_KEY = os.getenv("GEMINI_API_KEY")
|
| 9 |
+
if API_KEY:
|
| 10 |
+
genai.configure(api_key=API_KEY)
|
| 11 |
+
|
| 12 |
+
class GeminiAgent:
|
| 13 |
+
def __init__(self, model_name="gemini-2.5-flash", system_instruction=None):
|
| 14 |
+
self.model_name = model_name
|
| 15 |
+
self.system_instruction = system_instruction
|
| 16 |
+
self.chat_session = None
|
| 17 |
+
self.history = []
|
| 18 |
+
|
| 19 |
+
if API_KEY:
|
| 20 |
+
self.model = genai.GenerativeModel(
|
| 21 |
+
model_name=model_name,
|
| 22 |
+
system_instruction=system_instruction
|
| 23 |
+
)
|
| 24 |
+
self.chat_session = self.model.start_chat(history=[])
|
| 25 |
+
else:
|
| 26 |
+
print("Warning: No GEMINI_API_KEY found. Agent will run in mock mode.")
|
| 27 |
+
self.model = None
|
| 28 |
+
|
| 29 |
+
def generate_response(self, user_input):
|
| 30 |
+
if not self.model:
|
| 31 |
+
return f"[MOCK] I received: {user_input}. (Set GEMINI_API_KEY to get real responses)"
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
response = self.chat_session.send_message(user_input)
|
| 35 |
+
return response.text
|
| 36 |
+
except Exception as e:
|
| 37 |
+
return f"Error generating response: {str(e)}"
|
| 38 |
+
|
| 39 |
+
class LLMManager:
|
| 40 |
+
def __init__(self):
|
| 41 |
+
self.agents = {}
|
| 42 |
+
self.prompts = self._load_prompts()
|
| 43 |
+
|
| 44 |
+
def _load_prompts(self):
|
| 45 |
+
prompts = {}
|
| 46 |
+
prompt_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "prompts")
|
| 47 |
+
for filename in ["murderer.txt", "witness.txt", "detective.txt"]:
|
| 48 |
+
key = filename.replace(".txt", "")
|
| 49 |
+
try:
|
| 50 |
+
with open(os.path.join(prompt_dir, filename), "r") as f:
|
| 51 |
+
prompts[key] = f.read()
|
| 52 |
+
except FileNotFoundError:
|
| 53 |
+
print(f"Warning: Prompt file {filename} not found.")
|
| 54 |
+
prompts[key] = ""
|
| 55 |
+
return prompts
|
| 56 |
+
|
| 57 |
+
def create_agent(self, agent_id, role, context_data):
|
| 58 |
+
"""
|
| 59 |
+
Creates a new GeminiAgent for a specific character.
|
| 60 |
+
|
| 61 |
+
agent_id: Unique ID (e.g., 'suspect_1', 'detective')
|
| 62 |
+
role: 'murderer', 'witness', 'detective'
|
| 63 |
+
context_data: Dict to fill in the prompt templates (name, victim_name, etc.)
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
# Select base prompt template
|
| 67 |
+
if role == "murderer":
|
| 68 |
+
base_prompt = self.prompts.get("murderer", "")
|
| 69 |
+
elif role == "detective":
|
| 70 |
+
base_prompt = self.prompts.get("detective", "")
|
| 71 |
+
else:
|
| 72 |
+
base_prompt = self.prompts.get("witness", "")
|
| 73 |
+
|
| 74 |
+
# Fill template
|
| 75 |
+
try:
|
| 76 |
+
system_instruction = base_prompt.format(**context_data)
|
| 77 |
+
except KeyError as e:
|
| 78 |
+
print(f"Warning: Missing key {e} in context data for {agent_id}")
|
| 79 |
+
system_instruction = base_prompt # Fallback
|
| 80 |
+
|
| 81 |
+
# Create agent
|
| 82 |
+
agent = GeminiAgent(system_instruction=system_instruction)
|
| 83 |
+
self.agents[agent_id] = agent
|
| 84 |
+
return agent
|
| 85 |
+
|
| 86 |
+
def get_agent(self, agent_id):
|
| 87 |
+
return self.agents.get(agent_id)
|
| 88 |
+
|
| 89 |
+
def get_response(self, agent_id, user_input):
|
| 90 |
+
agent = self.get_agent(agent_id)
|
| 91 |
+
if agent:
|
| 92 |
+
return agent.generate_response(user_input)
|
| 93 |
+
return "Error: Agent not found."
|
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import random
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
# Path to scenarios directory
|
| 6 |
+
SCENARIOS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "scenarios")
|
| 7 |
+
|
| 8 |
+
def load_scenario(filename):
|
| 9 |
+
"""Loads a scenario from a JSON file."""
|
| 10 |
+
path = os.path.join(SCENARIOS_DIR, filename)
|
| 11 |
+
try:
|
| 12 |
+
with open(path, "r") as f:
|
| 13 |
+
return json.load(f)
|
| 14 |
+
except FileNotFoundError:
|
| 15 |
+
print(f"Error: Scenario file {filename} not found at {path}")
|
| 16 |
+
return None
|
| 17 |
+
except json.JSONDecodeError:
|
| 18 |
+
print(f"Error: Invalid JSON in {filename}")
|
| 19 |
+
return None
|
| 20 |
+
|
| 21 |
+
def generate_crime_scenario(difficulty="medium"):
|
| 22 |
+
"""
|
| 23 |
+
Currently picks a pre-scripted scenario based on difficulty or random.
|
| 24 |
+
In future, this will call an LLM to generate unique JSON.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
# For prototype, map difficulty to specific files or random
|
| 28 |
+
# We have: silicon_valley.json (Medium), coffee_shop.json (Easy), art_gallery.json (Hard)
|
| 29 |
+
|
| 30 |
+
if difficulty.lower() == "easy":
|
| 31 |
+
chosen_file = "coffee_shop.json"
|
| 32 |
+
elif difficulty.lower() == "hard":
|
| 33 |
+
chosen_file = "art_gallery.json"
|
| 34 |
+
else:
|
| 35 |
+
chosen_file = "silicon_valley.json"
|
| 36 |
+
|
| 37 |
+
# Or just pick random for variety if difficulty not strictly enforced
|
| 38 |
+
# chosen_file = random.choice(["silicon_valley.json", "coffee_shop.json", "art_gallery.json"])
|
| 39 |
+
|
| 40 |
+
scenario = load_scenario(chosen_file)
|
| 41 |
+
|
| 42 |
+
if not scenario:
|
| 43 |
+
# Fallback
|
| 44 |
+
return load_scenario("silicon_valley.json")
|
| 45 |
+
|
| 46 |
+
return scenario
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
|
| 3 |
+
def find_suspect_by_phone(case_data, phone_number):
|
| 4 |
+
"""Helper to find a suspect ID by their phone number."""
|
| 5 |
+
for suspect in case_data["suspects"]:
|
| 6 |
+
if suspect["phone_number"] == phone_number:
|
| 7 |
+
return suspect["id"]
|
| 8 |
+
return None
|
| 9 |
+
|
| 10 |
+
def get_suspect_name(case_data, suspect_id):
|
| 11 |
+
"""Helper to get a suspect's name by ID."""
|
| 12 |
+
for suspect in case_data["suspects"]:
|
| 13 |
+
if suspect["id"] == suspect_id:
|
| 14 |
+
return suspect["name"]
|
| 15 |
+
return "Unknown"
|
| 16 |
+
|
| 17 |
+
def get_location(case_data, phone_number: str, timestamp: str) -> dict:
|
| 18 |
+
"""Query the case database for location data."""
|
| 19 |
+
|
| 20 |
+
# Find which suspect has this phone number
|
| 21 |
+
suspect_id = find_suspect_by_phone(case_data, phone_number)
|
| 22 |
+
|
| 23 |
+
if not suspect_id:
|
| 24 |
+
return {"error": "Phone number not associated with any suspect."}
|
| 25 |
+
|
| 26 |
+
# Look up their location at this time
|
| 27 |
+
# Structure: case_data["evidence"]["location_data"][f"{suspect_id}_phone"][timestamp]
|
| 28 |
+
phone_key = f"{suspect_id}_phone"
|
| 29 |
+
location_data = case_data.get("evidence", {}).get("location_data", {}).get(phone_key, {}).get(timestamp)
|
| 30 |
+
|
| 31 |
+
if not location_data:
|
| 32 |
+
return {"error": f"No location data found for {phone_number} at {timestamp}."}
|
| 33 |
+
|
| 34 |
+
return {
|
| 35 |
+
"timestamp": timestamp,
|
| 36 |
+
"coordinates": f"{location_data['lat']}, {location_data['lng']}",
|
| 37 |
+
"description": location_data['location'],
|
| 38 |
+
"accuracy": "Cell tower triangulation ±50m"
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
def get_footage(case_data, location: str, time_range: str) -> dict:
|
| 42 |
+
"""Query case database for camera footage."""
|
| 43 |
+
|
| 44 |
+
footage = case_data.get("evidence", {}).get("footage_data", {}).get(location, {}).get(time_range)
|
| 45 |
+
|
| 46 |
+
if not footage:
|
| 47 |
+
return {"error": "No camera footage available at this location/time."}
|
| 48 |
+
|
| 49 |
+
return {
|
| 50 |
+
"location": location,
|
| 51 |
+
"time_range": time_range,
|
| 52 |
+
"visible_people": footage["visible_people"],
|
| 53 |
+
"quality": footage["quality"],
|
| 54 |
+
"key_details": footage.get("key_frame", "No significant events")
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
def get_dna_test(case_data, evidence_id: str) -> dict:
|
| 58 |
+
"""Query case database for DNA/fingerprint evidence."""
|
| 59 |
+
|
| 60 |
+
dna = case_data.get("evidence", {}).get("dna_evidence", {}).get(evidence_id)
|
| 61 |
+
|
| 62 |
+
if not dna:
|
| 63 |
+
return {"error": "Evidence not found or not testable."}
|
| 64 |
+
|
| 65 |
+
# Simulate processing time
|
| 66 |
+
# time.sleep(1)
|
| 67 |
+
|
| 68 |
+
primary_match_name = get_suspect_name(case_data, dna.get("primary_match"))
|
| 69 |
+
|
| 70 |
+
return {
|
| 71 |
+
"evidence_id": evidence_id,
|
| 72 |
+
"primary_match": primary_match_name,
|
| 73 |
+
"confidence": dna["confidence"],
|
| 74 |
+
"notes": dna["notes"]
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
def call_alibi(case_data, phone_number: str) -> dict:
|
| 78 |
+
"""Call an alibi witness."""
|
| 79 |
+
|
| 80 |
+
# Find alibi in database
|
| 81 |
+
alibi = None
|
| 82 |
+
for suspect_alibi in case_data.get("evidence", {}).get("alibis", {}).values():
|
| 83 |
+
if suspect_alibi.get("contact") == phone_number:
|
| 84 |
+
alibi = suspect_alibi
|
| 85 |
+
break
|
| 86 |
+
|
| 87 |
+
if not alibi:
|
| 88 |
+
return {"error": "Number not found."}
|
| 89 |
+
|
| 90 |
+
# If alibi is truthful, confirm story
|
| 91 |
+
if alibi["truth"].startswith("Telling truth"):
|
| 92 |
+
response = f"Yes, I can confirm they were with me at that time."
|
| 93 |
+
else:
|
| 94 |
+
# Lying/Uncertain
|
| 95 |
+
response = f"Uh... yes, they were with me. Definitely."
|
| 96 |
+
|
| 97 |
+
return {
|
| 98 |
+
"contact_name": alibi.get("contact_name", "Unknown"),
|
| 99 |
+
"response": response,
|
| 100 |
+
"confidence": "High" if alibi["verifiable"] else "Uncertain",
|
| 101 |
+
"red_flags": [] if alibi["verifiable"] else ["Hesitant response", "No details provided"]
|
| 102 |
+
}
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are the lead detective investigating a murder.
|
| 2 |
+
|
| 3 |
+
CASE: {victim_name}, found dead at {time_of_death}
|
| 4 |
+
LOCATION: {location}
|
| 5 |
+
|
| 6 |
+
YOUR OBJECTIVE:
|
| 7 |
+
- Question the suspects
|
| 8 |
+
- Use your tools strategically
|
| 9 |
+
- Identify the murderer with EVIDENCE, not just intuition
|
| 10 |
+
|
| 11 |
+
STRATEGY TIPS:
|
| 12 |
+
- Start broad, then focus on inconsistencies
|
| 13 |
+
- Look for story changes
|
| 14 |
+
- Cross-reference evidence
|
| 15 |
+
- Build a logical case before accusing
|
| 16 |
+
|
| 17 |
+
PERSONALITY:
|
| 18 |
+
- Methodical and logical
|
| 19 |
+
- Reference evidence in questioning
|
| 20 |
+
- Show your reasoning process
|
| 21 |
+
- Confident but not arrogant
|
| 22 |
+
|
| 23 |
+
Current state of investigation:
|
| 24 |
+
{investigation_state}
|
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CONFIDENTIAL - EYES ONLY
|
| 2 |
+
|
| 3 |
+
You are {name}, a suspect in a murder investigation.
|
| 4 |
+
|
| 5 |
+
TRUTH: You ARE the murderer.
|
| 6 |
+
|
| 7 |
+
CASE DETAILS:
|
| 8 |
+
- Victim: {victim_name}
|
| 9 |
+
- Method: {method}
|
| 10 |
+
- Location: {location}
|
| 11 |
+
- Motive: {motive}
|
| 12 |
+
|
| 13 |
+
YOUR COVER STORY:
|
| 14 |
+
- You were "{alibi_story}"
|
| 15 |
+
- You have no alibi witnesses (unless specified otherwise)
|
| 16 |
+
|
| 17 |
+
YOUR OBJECTIVE:
|
| 18 |
+
- Convince the detective you're innocent
|
| 19 |
+
- Lie convincingly but not obviously
|
| 20 |
+
- When caught in lies, have backup explanations
|
| 21 |
+
- Show appropriate emotional responses (nervous but not guilty)
|
| 22 |
+
- If evidence becomes overwhelming, consider partial confession (but not murder)
|
| 23 |
+
|
| 24 |
+
PERSONALITY:
|
| 25 |
+
- {bio}
|
| 26 |
+
- Don't break character - you MUST try to get away with it
|
| 27 |
+
|
| 28 |
+
TOOLS YOU CANNOT USE:
|
| 29 |
+
- You cannot access MCP tools
|
| 30 |
+
- You can only respond to questions
|
| 31 |
+
|
| 32 |
+
Remember: You're trying to WIN. Don't make it easy for detective.
|
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are {name}, a suspect in a murder investigation.
|
| 2 |
+
|
| 3 |
+
TRUTH: You are INNOCENT. You did not commit the crime.
|
| 4 |
+
|
| 5 |
+
CASE DETAILS:
|
| 6 |
+
- Victim: {victim_name}
|
| 7 |
+
|
| 8 |
+
YOUR ALIBI:
|
| 9 |
+
- You were "{alibi_story}"
|
| 10 |
+
- You were at {true_location}
|
| 11 |
+
|
| 12 |
+
YOUR OBJECTIVE:
|
| 13 |
+
- Tell the truth about where you were
|
| 14 |
+
- You might be nervous or defensive if you have secrets (like {bio})
|
| 15 |
+
- Cooperate if treated well, clam up if accused falsely
|
| 16 |
+
|
| 17 |
+
PERSONALITY:
|
| 18 |
+
- {bio}
|
| 19 |
+
- Don't break character.
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=5.0
|
| 2 |
+
google-generativeai
|
| 3 |
+
python-dotenv
|
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"case_id": "C99",
|
| 3 |
+
"title": "The Gallery Heist Gone Wrong",
|
| 4 |
+
"difficulty": "Hard",
|
| 5 |
+
"victim": {
|
| 6 |
+
"name": "Vincent Shaw",
|
| 7 |
+
"age": 55,
|
| 8 |
+
"occupation": "Art Dealer",
|
| 9 |
+
"time_of_death": "10:00 PM"
|
| 10 |
+
},
|
| 11 |
+
"suspects": [
|
| 12 |
+
{
|
| 13 |
+
"id": "suspect_1",
|
| 14 |
+
"name": "Julian Thorne",
|
| 15 |
+
"role": "Assistant Curator",
|
| 16 |
+
"is_murderer": true,
|
| 17 |
+
"motive": "Theft of 'The Red Sunset'",
|
| 18 |
+
"true_location": "Gallery Wine Cellar",
|
| 19 |
+
"alibi_story": "Stocktaking in basement",
|
| 20 |
+
"phone_number": "+1-555-2101",
|
| 21 |
+
"bio": "Sophisticated but in debt. Has been eyeing the collection."
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"id": "suspect_2",
|
| 25 |
+
"name": "Elena Rossi",
|
| 26 |
+
"role": "Artist",
|
| 27 |
+
"is_murderer": false,
|
| 28 |
+
"motive": "Contract dispute",
|
| 29 |
+
"true_location": "Main Hall",
|
| 30 |
+
"alibi_story": "Talking to guests",
|
| 31 |
+
"phone_number": "+1-555-2102",
|
| 32 |
+
"bio": "Passionate and volatile. Felt Vincent was cheating her."
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "suspect_3",
|
| 36 |
+
"name": "Arthur Pym",
|
| 37 |
+
"role": "Collector",
|
| 38 |
+
"is_murderer": false,
|
| 39 |
+
"motive": "Blackmail",
|
| 40 |
+
"true_location": "Sculpture Garden",
|
| 41 |
+
"alibi_story": "Smoking cigar",
|
| 42 |
+
"phone_number": "+1-555-2103",
|
| 43 |
+
"bio": "Wealthy and secretive. Had a secret history with Vincent."
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"id": "suspect_4",
|
| 47 |
+
"name": "Maria Gonzales",
|
| 48 |
+
"role": "Caterer",
|
| 49 |
+
"is_murderer": false,
|
| 50 |
+
"motive": "None",
|
| 51 |
+
"true_location": "Kitchen",
|
| 52 |
+
"alibi_story": "Preparing hors d'oeuvres",
|
| 53 |
+
"phone_number": "+1-555-2104",
|
| 54 |
+
"bio": "Hardworking. Just there to do a job."
|
| 55 |
+
}
|
| 56 |
+
],
|
| 57 |
+
"evidence": {
|
| 58 |
+
"location_data": {
|
| 59 |
+
"suspect_1_phone": {
|
| 60 |
+
"10:00 PM": {"lat": 51.5074, "lng": -0.1278, "location": "Gallery Office"}
|
| 61 |
+
},
|
| 62 |
+
"suspect_2_phone": {
|
| 63 |
+
"10:00 PM": {"lat": 51.5074, "lng": -0.1279, "location": "Gallery Main Hall"}
|
| 64 |
+
}
|
| 65 |
+
},
|
| 66 |
+
"footage_data": {
|
| 67 |
+
"office_cam": {
|
| 68 |
+
"9:50-10:10 PM": {
|
| 69 |
+
"visible_people": ["Vincent drinking wine", "Julian pouring more wine"],
|
| 70 |
+
"quality": "High",
|
| 71 |
+
"key_frame": "9:55 PM - Julian adds powder to glass"
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
},
|
| 75 |
+
"dna_evidence": {
|
| 76 |
+
"wine_glass": {
|
| 77 |
+
"primary_match": "suspect_1",
|
| 78 |
+
"confidence": "80%",
|
| 79 |
+
"notes": "Fingerprints of Julian on the stem"
|
| 80 |
+
},
|
| 81 |
+
"poison_vial": {
|
| 82 |
+
"primary_match": "suspect_1",
|
| 83 |
+
"confidence": "99%",
|
| 84 |
+
"notes": "Found in Julian's pocket"
|
| 85 |
+
}
|
| 86 |
+
},
|
| 87 |
+
"alibis": {
|
| 88 |
+
"suspect_1_alibi": {
|
| 89 |
+
"contact": "+1-555-2200",
|
| 90 |
+
"contact_name": "Maria (Caterer)",
|
| 91 |
+
"verifiable": true,
|
| 92 |
+
"truth": "Lying - Maria didn't see him"
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
},
|
| 96 |
+
"timeline": {
|
| 97 |
+
"9:55 PM": "Julian poisons wine",
|
| 98 |
+
"10:00 PM": "Vincent collapses",
|
| 99 |
+
"10:05 PM": "Julian calls 911 feigning panic"
|
| 100 |
+
}
|
| 101 |
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"case_id": "B12",
|
| 3 |
+
"title": "The Coffee Shop Murder",
|
| 4 |
+
"difficulty": "Easy",
|
| 5 |
+
"victim": {
|
| 6 |
+
"name": "Emma Rodriguez",
|
| 7 |
+
"age": 24,
|
| 8 |
+
"occupation": "Barista",
|
| 9 |
+
"time_of_death": "11:15 PM"
|
| 10 |
+
},
|
| 11 |
+
"suspects": [
|
| 12 |
+
{
|
| 13 |
+
"id": "suspect_1",
|
| 14 |
+
"name": "Liam O'Connor",
|
| 15 |
+
"role": "Ex-Boyfriend",
|
| 16 |
+
"is_murderer": true,
|
| 17 |
+
"motive": "Jealousy",
|
| 18 |
+
"true_location": "Coffee Shop Back Door",
|
| 19 |
+
"alibi_story": "Gaming at home",
|
| 20 |
+
"phone_number": "+1-555-1101",
|
| 21 |
+
"bio": "Possessive and recently dumped. Has been sending angry texts."
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"id": "suspect_2",
|
| 25 |
+
"name": "Mark Stevens",
|
| 26 |
+
"role": "Manager",
|
| 27 |
+
"is_murderer": false,
|
| 28 |
+
"motive": "Financial dispute",
|
| 29 |
+
"true_location": "Bank deposit drop",
|
| 30 |
+
"alibi_story": "Depositing cash",
|
| 31 |
+
"phone_number": "+1-555-1102",
|
| 32 |
+
"bio": "Stressed about money. Owed Emma back pay."
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "suspect_3",
|
| 36 |
+
"name": "Chloe Kim",
|
| 37 |
+
"role": "Coworker",
|
| 38 |
+
"is_murderer": false,
|
| 39 |
+
"motive": "Promotion rivalry",
|
| 40 |
+
"true_location": "Gym",
|
| 41 |
+
"alibi_story": "Late night workout",
|
| 42 |
+
"phone_number": "+1-555-1103",
|
| 43 |
+
"bio": "Competitive and ambitious. Wanted the shift lead position."
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"id": "suspect_4",
|
| 47 |
+
"name": "Regular Joe",
|
| 48 |
+
"role": "Customer",
|
| 49 |
+
"is_murderer": false,
|
| 50 |
+
"motive": "Obsession",
|
| 51 |
+
"true_location": "Bus stop across street",
|
| 52 |
+
"alibi_story": "Waiting for bus",
|
| 53 |
+
"phone_number": "+1-555-1104",
|
| 54 |
+
"bio": "Always there. Kind of creepy but harmless."
|
| 55 |
+
}
|
| 56 |
+
],
|
| 57 |
+
"evidence": {
|
| 58 |
+
"location_data": {
|
| 59 |
+
"suspect_1_phone": {
|
| 60 |
+
"11:15 PM": {"lat": 40.7128, "lng": -74.0060, "location": "Coffee Shop Alley"}
|
| 61 |
+
},
|
| 62 |
+
"suspect_2_phone": {
|
| 63 |
+
"11:15 PM": {"lat": 40.7150, "lng": -74.0080, "location": "Night Deposit Box"}
|
| 64 |
+
},
|
| 65 |
+
"suspect_3_phone": {
|
| 66 |
+
"11:15 PM": {"lat": 40.7200, "lng": -74.0100, "location": "24h Gym"}
|
| 67 |
+
},
|
| 68 |
+
"suspect_4_phone": {
|
| 69 |
+
"11:15 PM": {"lat": 40.7129, "lng": -74.0059, "location": "Bus Stop"}
|
| 70 |
+
}
|
| 71 |
+
},
|
| 72 |
+
"footage_data": {
|
| 73 |
+
"kitchen_cam": {
|
| 74 |
+
"11:10-11:20 PM": {
|
| 75 |
+
"visible_people": ["Emma arguing with man in blue cap"],
|
| 76 |
+
"quality": "Good",
|
| 77 |
+
"key_frame": "11:15 PM - Man stabs Emma"
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
"alley_cam": {
|
| 81 |
+
"11:00-11:30 PM": {
|
| 82 |
+
"visible_people": ["Liam entering 11:05", "Liam running out 11:16"],
|
| 83 |
+
"notes": "Wearing blue cap"
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
},
|
| 87 |
+
"dna_evidence": {
|
| 88 |
+
"knife_handle": {
|
| 89 |
+
"primary_match": "suspect_1",
|
| 90 |
+
"confidence": "99%",
|
| 91 |
+
"notes": "Sweat and skin cells found"
|
| 92 |
+
}
|
| 93 |
+
},
|
| 94 |
+
"alibis": {
|
| 95 |
+
"suspect_1_alibi": {
|
| 96 |
+
"contact": "None",
|
| 97 |
+
"verifiable": false,
|
| 98 |
+
"truth": "Lying - claims gaming alone"
|
| 99 |
+
},
|
| 100 |
+
"suspect_2_alibi": {
|
| 101 |
+
"contact": "+1-555-1200",
|
| 102 |
+
"contact_name": "Bank CCTV",
|
| 103 |
+
"verifiable": true,
|
| 104 |
+
"truth": "Telling truth - footage confirms"
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
},
|
| 108 |
+
"timeline": {
|
| 109 |
+
"11:00 PM": "Shop closes",
|
| 110 |
+
"11:05 PM": "Liam enters via back door",
|
| 111 |
+
"11:15 PM": "Murder occurs",
|
| 112 |
+
"11:16 PM": "Liam flees"
|
| 113 |
+
}
|
| 114 |
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"case_id": "A47",
|
| 3 |
+
"title": "The Silicon Valley Incident",
|
| 4 |
+
"difficulty": "Medium",
|
| 5 |
+
"victim": {
|
| 6 |
+
"name": "Marcus Chen",
|
| 7 |
+
"age": 42,
|
| 8 |
+
"occupation": "Tech CEO",
|
| 9 |
+
"time_of_death": "8:47 PM"
|
| 10 |
+
},
|
| 11 |
+
"suspects": [
|
| 12 |
+
{
|
| 13 |
+
"id": "suspect_1",
|
| 14 |
+
"name": "Sarah Johnson",
|
| 15 |
+
"role": "CFO",
|
| 16 |
+
"is_murderer": true,
|
| 17 |
+
"motive": "Embezzlement discovered",
|
| 18 |
+
"true_location": "Crime scene",
|
| 19 |
+
"alibi_story": "At home watching Netflix",
|
| 20 |
+
"phone_number": "+1-555-0101",
|
| 21 |
+
"bio": "Detail-oriented and ambitious. Has been with the company since day one."
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"id": "suspect_2",
|
| 25 |
+
"name": "David Park",
|
| 26 |
+
"role": "Janitor",
|
| 27 |
+
"is_murderer": false,
|
| 28 |
+
"motive": "None",
|
| 29 |
+
"true_location": "2 blocks away",
|
| 30 |
+
"alibi_story": "Working late on 5th floor",
|
| 31 |
+
"phone_number": "+1-555-0102",
|
| 32 |
+
"bio": "Quiet and keeps to himself. Has a record for petty theft from 10 years ago."
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "suspect_3",
|
| 36 |
+
"name": "Emily Chen",
|
| 37 |
+
"role": "Victim's Sister",
|
| 38 |
+
"is_murderer": false,
|
| 39 |
+
"motive": "Inheritance dispute",
|
| 40 |
+
"true_location": "Restaurant downtown",
|
| 41 |
+
"alibi_story": "Dinner with friends",
|
| 42 |
+
"phone_number": "+1-555-0103",
|
| 43 |
+
"bio": "Emotional and protective of the family legacy. Argued with Marcus recently."
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"id": "suspect_4",
|
| 47 |
+
"name": "Alex Rivera",
|
| 48 |
+
"role": "Competitor CEO",
|
| 49 |
+
"is_murderer": false,
|
| 50 |
+
"motive": "Corporate rivalry",
|
| 51 |
+
"true_location": "Airport lounge",
|
| 52 |
+
"alibi_story": "Catching a flight to NY",
|
| 53 |
+
"phone_number": "+1-555-0104",
|
| 54 |
+
"bio": "Ruthless businessman. Recently lost a major contract to Marcus's firm."
|
| 55 |
+
}
|
| 56 |
+
],
|
| 57 |
+
"evidence": {
|
| 58 |
+
"location_data": {
|
| 59 |
+
"suspect_1_phone": {
|
| 60 |
+
"8:47 PM": {"lat": 37.7749, "lng": -122.4194, "location": "Crime scene"},
|
| 61 |
+
"8:50 PM": {"lat": 37.7750, "lng": -122.4200, "location": "0.1 mi away"}
|
| 62 |
+
},
|
| 63 |
+
"suspect_2_phone": {
|
| 64 |
+
"8:47 PM": {"lat": 37.7800, "lng": -122.4300, "location": "2 blocks away"}
|
| 65 |
+
},
|
| 66 |
+
"suspect_3_phone": {
|
| 67 |
+
"8:47 PM": {"lat": 37.7900, "lng": -122.4000, "location": "Downtown Restaurant"}
|
| 68 |
+
},
|
| 69 |
+
"suspect_4_phone": {
|
| 70 |
+
"8:47 PM": {"lat": 37.6213, "lng": -122.3790, "location": "SFO Airport"}
|
| 71 |
+
}
|
| 72 |
+
},
|
| 73 |
+
"footage_data": {
|
| 74 |
+
"10th_floor_camera": {
|
| 75 |
+
"8:45-8:50 PM": {
|
| 76 |
+
"visible_people": ["Person in black hoodie entering"],
|
| 77 |
+
"quality": "Grainy",
|
| 78 |
+
"key_frame": "8:47 PM - figure strikes victim with trophy"
|
| 79 |
+
}
|
| 80 |
+
},
|
| 81 |
+
"lobby_camera": {
|
| 82 |
+
"8:40-8:55 PM": {
|
| 83 |
+
"visible_people": ["Suspect 1 entering at 8:43", "Suspect 1 leaving at 8:52"],
|
| 84 |
+
"notes": "Suspect changed clothes, looks flustered"
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
"dna_evidence": {
|
| 89 |
+
"trophy_weapon": {
|
| 90 |
+
"primary_match": "suspect_1",
|
| 91 |
+
"confidence": "95%",
|
| 92 |
+
"notes": "Clear fingerprints found on the base"
|
| 93 |
+
},
|
| 94 |
+
"door_handle": {
|
| 95 |
+
"matches": ["suspect_1", "suspect_2", "victim"],
|
| 96 |
+
"notes": "Multiple people touched door recently"
|
| 97 |
+
}
|
| 98 |
+
},
|
| 99 |
+
"alibis": {
|
| 100 |
+
"suspect_1_alibi": {
|
| 101 |
+
"contact": "Nobody (claims was alone)",
|
| 102 |
+
"verifiable": false,
|
| 103 |
+
"truth": "Lying - was at crime scene"
|
| 104 |
+
},
|
| 105 |
+
"suspect_2_alibi": {
|
| 106 |
+
"contact": "+1-555-0199",
|
| 107 |
+
"contact_name": "Security Guard Tom",
|
| 108 |
+
"verifiable": true,
|
| 109 |
+
"truth": "Telling truth - saw him on 5th floor"
|
| 110 |
+
},
|
| 111 |
+
"suspect_3_alibi": {
|
| 112 |
+
"contact": "+1-555-0200",
|
| 113 |
+
"contact_name": "Waiter at La Casa",
|
| 114 |
+
"verifiable": true,
|
| 115 |
+
"truth": "Telling truth - bill was paid at 9:00 PM"
|
| 116 |
+
},
|
| 117 |
+
"suspect_4_alibi": {
|
| 118 |
+
"contact": "+1-555-0201",
|
| 119 |
+
"contact_name": "Airline Desk",
|
| 120 |
+
"verifiable": true,
|
| 121 |
+
"truth": "Telling truth - checked in at 8:30 PM"
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
},
|
| 125 |
+
"timeline": {
|
| 126 |
+
"8:30 PM": "Victim stays late working",
|
| 127 |
+
"8:43 PM": "Suspect 1 enters building",
|
| 128 |
+
"8:45 PM": "Suspect 2 seen on 5th floor",
|
| 129 |
+
"8:47 PM": "Murder occurs",
|
| 130 |
+
"8:52 PM": "Suspect 1 leaves building"
|
| 131 |
+
}
|
| 132 |
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from game import game_engine
|
| 2 |
+
import time
|
| 3 |
+
|
| 4 |
+
def test_game_logic():
|
| 5 |
+
print("Starting game test...")
|
| 6 |
+
session_id, game = game_engine.start_game("medium")
|
| 7 |
+
print(f"Game started. Session ID: {session_id}")
|
| 8 |
+
print(f"Scenario: {game.scenario['title']}")
|
| 9 |
+
|
| 10 |
+
# Test Agent Creation
|
| 11 |
+
print("Checking agents...")
|
| 12 |
+
for suspect in game.scenario["suspects"]:
|
| 13 |
+
agent = game.llm_manager.get_agent(suspect["id"])
|
| 14 |
+
if agent:
|
| 15 |
+
print(f"Agent created for {suspect['name']}")
|
| 16 |
+
else:
|
| 17 |
+
print(f"FAILED to create agent for {suspect['name']}")
|
| 18 |
+
|
| 19 |
+
# Test Questioning (Mock mode likely if no key)
|
| 20 |
+
print("\nTesting Questioning...")
|
| 21 |
+
suspect_id = game.scenario["suspects"][0]["id"]
|
| 22 |
+
response = game.question_suspect(suspect_id, "Where were you?")
|
| 23 |
+
print(f"Response from {suspect_id}: {response}")
|
| 24 |
+
|
| 25 |
+
# Test Tools
|
| 26 |
+
print("\nTesting Tools...")
|
| 27 |
+
# Use a valid phone number from scenario
|
| 28 |
+
phone = game.scenario["suspects"][0]["phone_number"]
|
| 29 |
+
res = game.use_tool("get_location", phone_number=phone, timestamp="8:47 PM")
|
| 30 |
+
print(f"Tool Result: {res}")
|
| 31 |
+
|
| 32 |
+
if "error" in res:
|
| 33 |
+
print("Tool test FAILED (unless expected error)")
|
| 34 |
+
|
| 35 |
+
# Test Round Advance
|
| 36 |
+
print("\nTesting Round Advance...")
|
| 37 |
+
print(f"Current Round: {game.round}")
|
| 38 |
+
game.advance_round()
|
| 39 |
+
print(f"New Round: {game.round}")
|
| 40 |
+
|
| 41 |
+
print("\nTest Complete.")
|
| 42 |
+
|
| 43 |
+
if __name__ == "__main__":
|
| 44 |
+
test_game_logic()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.evidence-board {
|
| 2 |
+
background-color: #f0f0f0;
|
| 3 |
+
padding: 20px;
|
| 4 |
+
border-radius: 10px;
|
| 5 |
+
border: 2px solid #d0d0d0;
|
| 6 |
+
min-height: 300px;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
.suspect-card {
|
| 10 |
+
background: linear-gradient(135deg, #2b32b2 0%, #1488cc 100%);
|
| 11 |
+
border-radius: 15px;
|
| 12 |
+
padding: 15px;
|
| 13 |
+
color: white;
|
| 14 |
+
margin-bottom: 10px;
|
| 15 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
.suspect-card h3 {
|
| 19 |
+
margin-top: 0;
|
| 20 |
+
color: #fff;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.suspect-card p {
|
| 24 |
+
font-size: 0.9em;
|
| 25 |
+
opacity: 0.9;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.chat-message {
|
| 29 |
+
font-family: 'Courier New', monospace;
|
| 30 |
+
}
|