Spaces:
Running
Running
Enhance game mechanics and UI: Update Gradio version, improve suspect card formatting, and add phone number handling for suspects
Browse files- GEMINI.md +1 -1
- app.py +123 -63
- game/game_engine.py +1 -0
- mcp/tools.py +82 -26
- prompts/murderer.txt +3 -0
- prompts/witness.txt +3 -0
- ui/components.py +60 -0
- ui/styles.css +12 -2
GEMINI.md
CHANGED
|
@@ -52,5 +52,5 @@ Since the project is a Gradio app:
|
|
| 52 |
|
| 53 |
### Key Directives
|
| 54 |
- **Follow the Plan:** All development should align with the specifications in `PLan.md`.
|
| 55 |
-
- **Gradio
|
| 56 |
- **MCP Integration:** Tools should be designed to simulate real investigative data retrieval.
|
|
|
|
| 52 |
|
| 53 |
### Key Directives
|
| 54 |
- **Follow the Plan:** All development should align with the specifications in `PLan.md`.
|
| 55 |
+
- **Gradio 6:** 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.
|
app.py
CHANGED
|
@@ -1,6 +1,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:
|
|
@@ -11,29 +12,47 @@ def get_current_game(session_id):
|
|
| 11 |
return None
|
| 12 |
return game_engine.get_game(session_id)
|
| 13 |
|
| 14 |
-
def
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 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 |
-
|
| 31 |
-
|
|
|
|
|
|
|
| 32 |
while len(cards) < 4:
|
| 33 |
cards.append("")
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
# Initial Evidence Board
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
# Initial Chat
|
| 39 |
initial_chat = [
|
|
@@ -42,16 +61,26 @@ def start_new_game_ui(difficulty):
|
|
| 42 |
|
| 43 |
return (
|
| 44 |
session_id,
|
| 45 |
-
cards[0], cards[1], cards[2], cards[3], # Suspect
|
|
|
|
| 46 |
initial_chat, # Chatbot
|
| 47 |
-
|
| 48 |
f"Round: {game.round}/5 | Points: {game.points}", # Stats
|
| 49 |
-
|
| 50 |
gr.update(interactive=True), # Question input
|
| 51 |
gr.update(interactive=True), # Question btn
|
| 52 |
-
gr.update(interactive=True)
|
|
|
|
| 53 |
)
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
def submit_question(session_id, suspect_id, question, history):
|
| 56 |
game = get_current_game(session_id)
|
| 57 |
if not game:
|
|
@@ -65,25 +94,25 @@ def submit_question(session_id, suspect_id, question, history):
|
|
| 65 |
|
| 66 |
response = game.question_suspect(suspect_id, question)
|
| 67 |
|
| 68 |
-
#
|
| 69 |
-
|
| 70 |
|
| 71 |
-
history.append({"role": "user", "content": f"**Detective to {
|
| 72 |
-
history.append({"role": "assistant", "content": f"**{
|
| 73 |
|
| 74 |
return history, "" # Clear input
|
| 75 |
|
| 76 |
-
def use_tool_ui(session_id, tool_name, arg1,
|
| 77 |
game = get_current_game(session_id)
|
| 78 |
if not game:
|
| 79 |
-
return history,
|
| 80 |
|
| 81 |
# Construct kwargs based on tool
|
| 82 |
kwargs = {}
|
| 83 |
if tool_name == "get_location":
|
| 84 |
-
kwargs = {"phone_number": arg1
|
| 85 |
elif tool_name == "get_footage":
|
| 86 |
-
kwargs = {"location": arg1
|
| 87 |
elif tool_name == "get_dna_test":
|
| 88 |
kwargs = {"evidence_id": arg1}
|
| 89 |
elif tool_name == "call_alibi":
|
|
@@ -91,23 +120,24 @@ def use_tool_ui(session_id, tool_name, arg1, arg2, history):
|
|
| 91 |
|
| 92 |
result = game.use_tool(tool_name, **kwargs)
|
| 93 |
|
|
|
|
|
|
|
|
|
|
| 94 |
# Update History
|
| 95 |
-
history.append({"role": "assistant", "content": f"π§ **System:** Used {tool_name}\
|
| 96 |
|
| 97 |
# Update Evidence Board
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
ev_html += f"<li>{str(item)}</li>"
|
| 101 |
-
ev_html += "</ul>"
|
| 102 |
|
| 103 |
stats = f"Round: {game.round}/5 | Points: {game.points}"
|
| 104 |
|
| 105 |
-
return history,
|
| 106 |
|
| 107 |
def next_round_ui(session_id):
|
| 108 |
game = get_current_game(session_id)
|
| 109 |
if not game:
|
| 110 |
-
return "No game"
|
| 111 |
|
| 112 |
not_over = game.advance_round()
|
| 113 |
stats = f"Round: {game.round}/5 | Points: {game.points}"
|
|
@@ -126,28 +156,56 @@ with gr.Blocks(title="Murder.Ai") as demo:
|
|
| 126 |
|
| 127 |
session_state = gr.State("")
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
with gr.Row():
|
| 130 |
# Left: Suspect Cards
|
| 131 |
with gr.Column(scale=1):
|
| 132 |
gr.Markdown("## Suspects")
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
# Center: Main Game Area
|
| 139 |
with gr.Column(scale=2):
|
| 140 |
game_stats = gr.Markdown("Round: 0/5 | Points: 10")
|
|
|
|
| 141 |
|
| 142 |
chatbot = gr.Chatbot(
|
| 143 |
label="Investigation Log",
|
| 144 |
height=500,
|
| 145 |
)
|
| 146 |
|
|
|
|
|
|
|
|
|
|
| 147 |
with gr.Row():
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
ask_btn = gr.Button("Ask", variant="secondary", interactive=False)
|
| 151 |
|
| 152 |
gr.Markdown("### Tools")
|
| 153 |
with gr.Row():
|
|
@@ -156,8 +214,7 @@ with gr.Blocks(title="Murder.Ai") as demo:
|
|
| 156 |
choices=["get_location", "get_footage", "get_dna_test", "call_alibi"],
|
| 157 |
value="get_location"
|
| 158 |
)
|
| 159 |
-
arg1_input = gr.Textbox(label="
|
| 160 |
-
arg2_input = gr.Textbox(label="Arg 2 (Time)")
|
| 161 |
use_tool_btn = gr.Button("Use Tool", interactive=False)
|
| 162 |
|
| 163 |
next_round_btn = gr.Button("βΆοΈ Next Round / End Game")
|
|
@@ -165,43 +222,45 @@ with gr.Blocks(title="Murder.Ai") as demo:
|
|
| 165 |
# Right: Evidence Board
|
| 166 |
with gr.Column(scale=1):
|
| 167 |
gr.Markdown("## Evidence Board")
|
| 168 |
-
evidence_board = gr.
|
| 169 |
-
|
| 170 |
-
value="<p>No active case.</p>"
|
| 171 |
)
|
| 172 |
-
|
| 173 |
-
# Start Controls
|
| 174 |
-
with gr.Row():
|
| 175 |
-
difficulty_selector = gr.Radio(["Easy", "Medium", "Hard"], label="Difficulty", value="Medium")
|
| 176 |
-
start_btn = gr.Button("π² GENERATE NEW CASE", variant="primary")
|
| 177 |
|
| 178 |
# --- EVENTS ---
|
| 179 |
|
| 180 |
start_btn.click(
|
| 181 |
fn=start_new_game_ui,
|
| 182 |
-
inputs=[
|
| 183 |
outputs=[
|
| 184 |
session_state,
|
| 185 |
-
|
|
|
|
| 186 |
chatbot,
|
| 187 |
evidence_board,
|
| 188 |
game_stats,
|
| 189 |
-
|
| 190 |
question_input,
|
| 191 |
ask_btn,
|
| 192 |
-
use_tool_btn
|
|
|
|
| 193 |
]
|
| 194 |
)
|
| 195 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
ask_btn.click(
|
| 197 |
fn=submit_question,
|
| 198 |
-
inputs=[session_state,
|
| 199 |
outputs=[chatbot, question_input]
|
| 200 |
)
|
| 201 |
|
| 202 |
use_tool_btn.click(
|
| 203 |
fn=use_tool_ui,
|
| 204 |
-
inputs=[session_state, tool_dropdown, arg1_input,
|
| 205 |
outputs=[chatbot, evidence_board, game_stats]
|
| 206 |
)
|
| 207 |
|
|
@@ -212,7 +271,8 @@ with gr.Blocks(title="Murder.Ai") as demo:
|
|
| 212 |
)
|
| 213 |
|
| 214 |
demo.launch(
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
allowed_paths=["."]
|
| 218 |
-
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import os
|
| 3 |
from game import game_engine
|
| 4 |
+
from ui.components import format_tool_result_markdown, format_suspect_card
|
| 5 |
|
| 6 |
# Load CSS
|
| 7 |
with open("ui/styles.css", "r") as f:
|
|
|
|
| 12 |
return None
|
| 13 |
return game_engine.get_game(session_id)
|
| 14 |
|
| 15 |
+
def start_new_game_ui(scenario_name):
|
| 16 |
+
"""Starts a new game based on scenario selection."""
|
| 17 |
+
|
| 18 |
+
difficulty = "medium" # Default
|
| 19 |
+
if "Coffee Shop" in scenario_name:
|
| 20 |
+
difficulty = "easy"
|
| 21 |
+
elif "Gallery" in scenario_name:
|
| 22 |
+
difficulty = "hard"
|
| 23 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
session_id, game = game_engine.start_game(difficulty)
|
| 25 |
|
| 26 |
+
# Generate Suspect Cards HTML and Button Updates
|
| 27 |
+
suspects = game.scenario["suspects"]
|
| 28 |
+
cards = [format_suspect_card(s) for s in suspects]
|
| 29 |
+
|
| 30 |
+
# Pad if fewer than 4
|
| 31 |
while len(cards) < 4:
|
| 32 |
cards.append("")
|
| 33 |
|
| 34 |
+
# Enable buttons for existing suspects
|
| 35 |
+
btn_updates = [gr.update(interactive=True, visible=True) for _ in suspects]
|
| 36 |
+
while len(btn_updates) < 4:
|
| 37 |
+
btn_updates.append(gr.update(interactive=False, visible=False))
|
| 38 |
+
|
| 39 |
# Initial Evidence Board
|
| 40 |
+
victim_info = game.scenario['victim']
|
| 41 |
+
# Basic extraction of details if available, otherwise generic placeholders (based on plan/typical structure)
|
| 42 |
+
found_at = victim_info.get('found_at', victim_info.get('location', 'Unknown Location'))
|
| 43 |
+
cause_of_death = victim_info.get('cause_of_death', 'Unknown')
|
| 44 |
+
|
| 45 |
+
evidence_md = f"""## π Case: {game.scenario['title']}
|
| 46 |
+
**Victim:** {victim_info['name']}
|
| 47 |
+
**Time of Death:** {victim_info['time_of_death']}
|
| 48 |
+
**Found At:** {found_at}
|
| 49 |
+
**Cause of Death:** {cause_of_death}
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
### π Evidence Log
|
| 54 |
+
*Evidence revealed during investigation will appear here.*
|
| 55 |
+
"""
|
| 56 |
|
| 57 |
# Initial Chat
|
| 58 |
initial_chat = [
|
|
|
|
| 61 |
|
| 62 |
return (
|
| 63 |
session_id,
|
| 64 |
+
cards[0], cards[1], cards[2], cards[3], # Suspect HTML
|
| 65 |
+
btn_updates[0], btn_updates[1], btn_updates[2], btn_updates[3], # Suspect Buttons
|
| 66 |
initial_chat, # Chatbot
|
| 67 |
+
evidence_md, # Evidence board
|
| 68 |
f"Round: {game.round}/5 | Points: {game.points}", # Stats
|
| 69 |
+
None, # Reset selected suspect ID
|
| 70 |
gr.update(interactive=True), # Question input
|
| 71 |
gr.update(interactive=True), # Question btn
|
| 72 |
+
gr.update(interactive=True), # Tool btn
|
| 73 |
+
"**Select a suspect to begin interrogation.**" # Status
|
| 74 |
)
|
| 75 |
|
| 76 |
+
def select_suspect_by_index(session_id, index):
|
| 77 |
+
game = get_current_game(session_id)
|
| 78 |
+
if not game or index >= len(game.scenario["suspects"]):
|
| 79 |
+
return None, "Error"
|
| 80 |
+
|
| 81 |
+
suspect = game.scenario["suspects"][index]
|
| 82 |
+
return suspect['id'], f"**Interrogating:** {suspect['name']}"
|
| 83 |
+
|
| 84 |
def submit_question(session_id, suspect_id, question, history):
|
| 85 |
game = get_current_game(session_id)
|
| 86 |
if not game:
|
|
|
|
| 94 |
|
| 95 |
response = game.question_suspect(suspect_id, question)
|
| 96 |
|
| 97 |
+
# Look up suspect name for nicer chat
|
| 98 |
+
suspect_name = next((s["name"] for s in game.scenario["suspects"] if s["id"] == suspect_id), suspect_id)
|
| 99 |
|
| 100 |
+
history.append({"role": "user", "content": f"**Detective to {suspect_name}:** {question}"})
|
| 101 |
+
history.append({"role": "assistant", "content": f"**{suspect_name}:** {response}"})
|
| 102 |
|
| 103 |
return history, "" # Clear input
|
| 104 |
|
| 105 |
+
def use_tool_ui(session_id, tool_name, arg1, history, current_evidence_md):
|
| 106 |
game = get_current_game(session_id)
|
| 107 |
if not game:
|
| 108 |
+
return history, current_evidence_md, "Error"
|
| 109 |
|
| 110 |
# Construct kwargs based on tool
|
| 111 |
kwargs = {}
|
| 112 |
if tool_name == "get_location":
|
| 113 |
+
kwargs = {"phone_number": arg1}
|
| 114 |
elif tool_name == "get_footage":
|
| 115 |
+
kwargs = {"location": arg1}
|
| 116 |
elif tool_name == "get_dna_test":
|
| 117 |
kwargs = {"evidence_id": arg1}
|
| 118 |
elif tool_name == "call_alibi":
|
|
|
|
| 120 |
|
| 121 |
result = game.use_tool(tool_name, **kwargs)
|
| 122 |
|
| 123 |
+
# Format the result
|
| 124 |
+
formatted_result = format_tool_result_markdown(tool_name, result)
|
| 125 |
+
|
| 126 |
# Update History
|
| 127 |
+
history.append({"role": "assistant", "content": f"π§ **System:** Used {tool_name}\nInput: {arg1}\n\n{formatted_result}"})
|
| 128 |
|
| 129 |
# Update Evidence Board
|
| 130 |
+
# We append the new formatted result to the markdown
|
| 131 |
+
new_evidence_md = current_evidence_md + f"\n\n---\n\n{formatted_result}"
|
|
|
|
|
|
|
| 132 |
|
| 133 |
stats = f"Round: {game.round}/5 | Points: {game.points}"
|
| 134 |
|
| 135 |
+
return history, new_evidence_md, stats
|
| 136 |
|
| 137 |
def next_round_ui(session_id):
|
| 138 |
game = get_current_game(session_id)
|
| 139 |
if not game:
|
| 140 |
+
return "No game"
|
| 141 |
|
| 142 |
not_over = game.advance_round()
|
| 143 |
stats = f"Round: {game.round}/5 | Points: {game.points}"
|
|
|
|
| 156 |
|
| 157 |
session_state = gr.State("")
|
| 158 |
|
| 159 |
+
# Top Controls
|
| 160 |
+
with gr.Row():
|
| 161 |
+
scenario_selector = gr.Dropdown(
|
| 162 |
+
choices=["The Silicon Valley Incident (Medium)", "The Coffee Shop Murder (Easy)", "The Gallery Heist (Hard)"],
|
| 163 |
+
value="The Silicon Valley Incident (Medium)",
|
| 164 |
+
label="Select Case Scenario"
|
| 165 |
+
)
|
| 166 |
+
start_btn = gr.Button("π LOAD CASE FILE", variant="primary")
|
| 167 |
+
|
| 168 |
with gr.Row():
|
| 169 |
# Left: Suspect Cards
|
| 170 |
with gr.Column(scale=1):
|
| 171 |
gr.Markdown("## Suspects")
|
| 172 |
+
|
| 173 |
+
# Suspect 1
|
| 174 |
+
with gr.Group():
|
| 175 |
+
s1_html = gr.HTML()
|
| 176 |
+
s1_btn = gr.Button("Interrogate", variant="secondary", visible=False)
|
| 177 |
+
|
| 178 |
+
# Suspect 2
|
| 179 |
+
with gr.Group():
|
| 180 |
+
s2_html = gr.HTML()
|
| 181 |
+
s2_btn = gr.Button("Interrogate", variant="secondary", visible=False)
|
| 182 |
+
|
| 183 |
+
# Suspect 3
|
| 184 |
+
with gr.Group():
|
| 185 |
+
s3_html = gr.HTML()
|
| 186 |
+
s3_btn = gr.Button("Interrogate", variant="secondary", visible=False)
|
| 187 |
+
|
| 188 |
+
# Suspect 4
|
| 189 |
+
with gr.Group():
|
| 190 |
+
s4_html = gr.HTML()
|
| 191 |
+
s4_btn = gr.Button("Interrogate", variant="secondary", visible=False)
|
| 192 |
|
| 193 |
# Center: Main Game Area
|
| 194 |
with gr.Column(scale=2):
|
| 195 |
game_stats = gr.Markdown("Round: 0/5 | Points: 10")
|
| 196 |
+
interrogation_status = gr.Markdown("**Select a suspect to begin interrogation.**")
|
| 197 |
|
| 198 |
chatbot = gr.Chatbot(
|
| 199 |
label="Investigation Log",
|
| 200 |
height=500,
|
| 201 |
)
|
| 202 |
|
| 203 |
+
# State variable to store selected suspect ID
|
| 204 |
+
selected_suspect_id = gr.State(value=None)
|
| 205 |
+
|
| 206 |
with gr.Row():
|
| 207 |
+
question_input = gr.Textbox(label="Question", placeholder="Where were you?", interactive=False, scale=4)
|
| 208 |
+
ask_btn = gr.Button("Ask", variant="secondary", interactive=False, scale=1)
|
|
|
|
| 209 |
|
| 210 |
gr.Markdown("### Tools")
|
| 211 |
with gr.Row():
|
|
|
|
| 214 |
choices=["get_location", "get_footage", "get_dna_test", "call_alibi"],
|
| 215 |
value="get_location"
|
| 216 |
)
|
| 217 |
+
arg1_input = gr.Textbox(label="Input (Phone / Location / ID)")
|
|
|
|
| 218 |
use_tool_btn = gr.Button("Use Tool", interactive=False)
|
| 219 |
|
| 220 |
next_round_btn = gr.Button("βΆοΈ Next Round / End Game")
|
|
|
|
| 222 |
# Right: Evidence Board
|
| 223 |
with gr.Column(scale=1):
|
| 224 |
gr.Markdown("## Evidence Board")
|
| 225 |
+
evidence_board = gr.Markdown(
|
| 226 |
+
value="Select a case to begin..."
|
|
|
|
| 227 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
# --- EVENTS ---
|
| 230 |
|
| 231 |
start_btn.click(
|
| 232 |
fn=start_new_game_ui,
|
| 233 |
+
inputs=[scenario_selector],
|
| 234 |
outputs=[
|
| 235 |
session_state,
|
| 236 |
+
s1_html, s2_html, s3_html, s4_html,
|
| 237 |
+
s1_btn, s2_btn, s3_btn, s4_btn,
|
| 238 |
chatbot,
|
| 239 |
evidence_board,
|
| 240 |
game_stats,
|
| 241 |
+
selected_suspect_id,
|
| 242 |
question_input,
|
| 243 |
ask_btn,
|
| 244 |
+
use_tool_btn,
|
| 245 |
+
interrogation_status
|
| 246 |
]
|
| 247 |
)
|
| 248 |
|
| 249 |
+
# Suspect Selection Events
|
| 250 |
+
s1_btn.click(fn=lambda s: select_suspect_by_index(s, 0), inputs=[session_state], outputs=[selected_suspect_id, interrogation_status])
|
| 251 |
+
s2_btn.click(fn=lambda s: select_suspect_by_index(s, 1), inputs=[session_state], outputs=[selected_suspect_id, interrogation_status])
|
| 252 |
+
s3_btn.click(fn=lambda s: select_suspect_by_index(s, 2), inputs=[session_state], outputs=[selected_suspect_id, interrogation_status])
|
| 253 |
+
s4_btn.click(fn=lambda s: select_suspect_by_index(s, 3), inputs=[session_state], outputs=[selected_suspect_id, interrogation_status])
|
| 254 |
+
|
| 255 |
ask_btn.click(
|
| 256 |
fn=submit_question,
|
| 257 |
+
inputs=[session_state, selected_suspect_id, question_input, chatbot],
|
| 258 |
outputs=[chatbot, question_input]
|
| 259 |
)
|
| 260 |
|
| 261 |
use_tool_btn.click(
|
| 262 |
fn=use_tool_ui,
|
| 263 |
+
inputs=[session_state, tool_dropdown, arg1_input, chatbot, evidence_board],
|
| 264 |
outputs=[chatbot, evidence_board, game_stats]
|
| 265 |
)
|
| 266 |
|
|
|
|
| 271 |
)
|
| 272 |
|
| 273 |
demo.launch(
|
| 274 |
+
server_name="0.0.0.0",
|
| 275 |
+
server_port=7860,
|
| 276 |
+
allowed_paths=["."],
|
| 277 |
+
css=custom_css
|
| 278 |
+
)
|
game/game_engine.py
CHANGED
|
@@ -40,6 +40,7 @@ class GameInstance:
|
|
| 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
|
|
|
|
| 40 |
"alibi_story": suspect["alibi_story"],
|
| 41 |
"bio": suspect["bio"],
|
| 42 |
"true_location": suspect["true_location"],
|
| 43 |
+
"phone_number": suspect.get("phone_number", "Unknown"), # Add phone number to context
|
| 44 |
|
| 45 |
# Murderer specific
|
| 46 |
"method": self.scenario["title"], # Placeholder, scenario doesn't explicitly list 'method' field in plan, using title/weapon logic implied
|
mcp/tools.py
CHANGED
|
@@ -1,9 +1,22 @@
|
|
| 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 |
-
|
|
|
|
|
|
|
| 7 |
return suspect["id"]
|
| 8 |
return None
|
| 9 |
|
|
@@ -14,40 +27,80 @@ def get_suspect_name(case_data, suspect_id):
|
|
| 14 |
return suspect["name"]
|
| 15 |
return "Unknown"
|
| 16 |
|
| 17 |
-
def get_location(case_data, phone_number: str, timestamp: str) -> dict:
|
| 18 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 30 |
-
|
| 31 |
-
if not location_data:
|
| 32 |
-
return {"error": f"No location data found for {phone_number} at {timestamp}."}
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
return {
|
| 35 |
-
"
|
| 36 |
-
"
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
if
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
return {
|
| 50 |
-
"location":
|
| 51 |
"time_range": time_range,
|
| 52 |
"visible_people": footage["visible_people"],
|
| 53 |
"quality": footage["quality"],
|
|
@@ -62,9 +115,6 @@ def get_dna_test(case_data, evidence_id: str) -> dict:
|
|
| 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 {
|
|
@@ -77,15 +127,21 @@ def get_dna_test(case_data, evidence_id: str) -> dict:
|
|
| 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 |
-
|
|
|
|
| 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"):
|
|
|
|
| 1 |
import time
|
| 2 |
+
import re
|
| 3 |
+
|
| 4 |
+
def normalize_phone(phone):
|
| 5 |
+
"""Strips non-digit characters from phone number for comparison."""
|
| 6 |
+
if not phone:
|
| 7 |
+
return ""
|
| 8 |
+
return re.sub(r"\D", "", phone)
|
| 9 |
|
| 10 |
def find_suspect_by_phone(case_data, phone_number):
|
| 11 |
+
"""Helper to find a suspect ID by their phone number (fuzzy match)."""
|
| 12 |
+
target_digits = normalize_phone(phone_number)
|
| 13 |
+
if not target_digits:
|
| 14 |
+
return None
|
| 15 |
+
|
| 16 |
for suspect in case_data["suspects"]:
|
| 17 |
+
suspect_digits = normalize_phone(suspect["phone_number"])
|
| 18 |
+
# Check if one ends with the other (to handle +1 vs no +1)
|
| 19 |
+
if target_digits.endswith(suspect_digits) or suspect_digits.endswith(target_digits):
|
| 20 |
return suspect["id"]
|
| 21 |
return None
|
| 22 |
|
|
|
|
| 27 |
return suspect["name"]
|
| 28 |
return "Unknown"
|
| 29 |
|
| 30 |
+
def get_location(case_data, phone_number: str, timestamp: str = None) -> dict:
|
| 31 |
+
"""
|
| 32 |
+
Query the case database for location data.
|
| 33 |
+
If timestamp is missing, searches for the time of death or returns all data.
|
| 34 |
+
"""
|
| 35 |
|
| 36 |
# Find which suspect has this phone number
|
| 37 |
suspect_id = find_suspect_by_phone(case_data, phone_number)
|
| 38 |
|
| 39 |
if not suspect_id:
|
| 40 |
+
return {"error": f"Phone number {phone_number} not associated with any suspect."}
|
| 41 |
|
|
|
|
|
|
|
| 42 |
phone_key = f"{suspect_id}_phone"
|
| 43 |
+
location_data_map = case_data.get("evidence", {}).get("location_data", {}).get(phone_key, {})
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
+
if not location_data_map:
|
| 46 |
+
return {"error": f"No location history found for {phone_number}."}
|
| 47 |
+
|
| 48 |
+
# If timestamp provided, look for exact match first
|
| 49 |
+
if timestamp:
|
| 50 |
+
location_data = location_data_map.get(timestamp)
|
| 51 |
+
if location_data:
|
| 52 |
+
return {
|
| 53 |
+
"timestamp": timestamp,
|
| 54 |
+
"coordinates": f"{location_data['lat']}, {location_data['lng']}",
|
| 55 |
+
"description": location_data['location'],
|
| 56 |
+
"accuracy": "Cell tower triangulation Β±50m"
|
| 57 |
+
}
|
| 58 |
+
else:
|
| 59 |
+
return {"error": f"No data for {timestamp}. Available times: {list(location_data_map.keys())}"}
|
| 60 |
+
|
| 61 |
+
# If no timestamp, return ALL data points found (or just the murder time one)
|
| 62 |
+
# Let's return the one closest to murder time (usually 8:47 PM in our main scenario)
|
| 63 |
+
# Or just return a list of all known locations.
|
| 64 |
+
|
| 65 |
+
results = []
|
| 66 |
+
for time_key, loc in location_data_map.items():
|
| 67 |
+
results.append(f"{time_key}: {loc['location']}")
|
| 68 |
+
|
| 69 |
return {
|
| 70 |
+
"info": "Location History Found",
|
| 71 |
+
"history": results
|
|
|
|
|
|
|
| 72 |
}
|
| 73 |
|
| 74 |
+
def get_footage(case_data, location: str, time_range: str = None) -> dict:
|
| 75 |
"""Query case database for camera footage."""
|
| 76 |
|
| 77 |
+
# Fuzzy match location keys
|
| 78 |
+
target_loc_key = None
|
| 79 |
+
footage_data = case_data.get("evidence", {}).get("footage_data", {})
|
| 80 |
+
|
| 81 |
+
for loc_key in footage_data.keys():
|
| 82 |
+
if location.lower() in loc_key.lower() or loc_key.lower() in location.lower():
|
| 83 |
+
target_loc_key = loc_key
|
| 84 |
+
break
|
| 85 |
+
|
| 86 |
+
if not target_loc_key:
|
| 87 |
+
return {"error": "No camera footage available at this location."}
|
| 88 |
+
|
| 89 |
+
# If time_range not provided, return the first one found or all
|
| 90 |
+
loc_footage = footage_data[target_loc_key]
|
| 91 |
|
| 92 |
+
if time_range:
|
| 93 |
+
footage = loc_footage.get(time_range)
|
| 94 |
+
if not footage:
|
| 95 |
+
return {"error": f"No footage for time {time_range}. Available: {list(loc_footage.keys())}"}
|
| 96 |
+
else:
|
| 97 |
+
# Return the first available clip info
|
| 98 |
+
first_key = list(loc_footage.keys())[0]
|
| 99 |
+
footage = loc_footage[first_key]
|
| 100 |
+
time_range = first_key # Update for return
|
| 101 |
|
| 102 |
return {
|
| 103 |
+
"location": target_loc_key,
|
| 104 |
"time_range": time_range,
|
| 105 |
"visible_people": footage["visible_people"],
|
| 106 |
"quality": footage["quality"],
|
|
|
|
| 115 |
if not dna:
|
| 116 |
return {"error": "Evidence not found or not testable."}
|
| 117 |
|
|
|
|
|
|
|
|
|
|
| 118 |
primary_match_name = get_suspect_name(case_data, dna.get("primary_match"))
|
| 119 |
|
| 120 |
return {
|
|
|
|
| 127 |
def call_alibi(case_data, phone_number: str) -> dict:
|
| 128 |
"""Call an alibi witness."""
|
| 129 |
|
| 130 |
+
# Find alibi in database - Fuzzy Match
|
| 131 |
alibi = None
|
| 132 |
+
target_digits = normalize_phone(phone_number)
|
| 133 |
+
|
| 134 |
+
if not target_digits:
|
| 135 |
+
return {"error": "Invalid phone number."}
|
| 136 |
+
|
| 137 |
for suspect_alibi in case_data.get("evidence", {}).get("alibis", {}).values():
|
| 138 |
+
contact_digits = normalize_phone(suspect_alibi.get("contact"))
|
| 139 |
+
if contact_digits and (target_digits.endswith(contact_digits) or contact_digits.endswith(target_digits)):
|
| 140 |
alibi = suspect_alibi
|
| 141 |
break
|
| 142 |
|
| 143 |
if not alibi:
|
| 144 |
+
return {"error": "Number not found or no alibi associated."}
|
| 145 |
|
| 146 |
# If alibi is truthful, confirm story
|
| 147 |
if alibi["truth"].startswith("Telling truth"):
|
prompts/murderer.txt
CHANGED
|
@@ -10,6 +10,9 @@ CASE DETAILS:
|
|
| 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)
|
|
|
|
| 10 |
- Location: {location}
|
| 11 |
- Motive: {motive}
|
| 12 |
|
| 13 |
+
YOUR DETAILS:
|
| 14 |
+
- Phone Number: {phone_number}
|
| 15 |
+
|
| 16 |
YOUR COVER STORY:
|
| 17 |
- You were "{alibi_story}"
|
| 18 |
- You have no alibi witnesses (unless specified otherwise)
|
prompts/witness.txt
CHANGED
|
@@ -5,6 +5,9 @@ TRUTH: You are INNOCENT. You did not commit the crime.
|
|
| 5 |
CASE DETAILS:
|
| 6 |
- Victim: {victim_name}
|
| 7 |
|
|
|
|
|
|
|
|
|
|
| 8 |
YOUR ALIBI:
|
| 9 |
- You were "{alibi_story}"
|
| 10 |
- You were at {true_location}
|
|
|
|
| 5 |
CASE DETAILS:
|
| 6 |
- Victim: {victim_name}
|
| 7 |
|
| 8 |
+
YOUR DETAILS:
|
| 9 |
+
- Phone Number: {phone_number}
|
| 10 |
+
|
| 11 |
YOUR ALIBI:
|
| 12 |
- You were "{alibi_story}"
|
| 13 |
- You were at {true_location}
|
ui/components.py
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def format_tool_result_markdown(tool_name, result):
|
| 2 |
+
"""Formats tool output as Markdown for the chat interface and evidence board."""
|
| 3 |
+
if "error" in result:
|
| 4 |
+
return f"β **Error:** {result['error']}"
|
| 5 |
+
|
| 6 |
+
output = ""
|
| 7 |
+
if tool_name == "get_location":
|
| 8 |
+
output += "### π Location Data\n"
|
| 9 |
+
if "history" in result:
|
| 10 |
+
output += "**Movement History:**\n"
|
| 11 |
+
for h in result['history']:
|
| 12 |
+
# Try to make history lines cleaner if they are just strings
|
| 13 |
+
output += f"- {h}\n"
|
| 14 |
+
elif "info" in result and result['info'] != "Location History Found":
|
| 15 |
+
# Only show info if it's an error or specific status, skip generic "Found"
|
| 16 |
+
output += f"**Status:** {result['info']}\n\n"
|
| 17 |
+
|
| 18 |
+
elif tool_name == "get_footage":
|
| 19 |
+
output += f"### πΉ CCTV Analysis\n"
|
| 20 |
+
output += f"- **Location:** {result.get('location', 'Unknown')}\n"
|
| 21 |
+
output += f"- **Time:** {result.get('time_range', 'N/A')}\n"
|
| 22 |
+
output += f"- **Quality:** {result.get('quality', 'N/A')}\n"
|
| 23 |
+
if "visible_people" in result:
|
| 24 |
+
people = ", ".join(result['visible_people'])
|
| 25 |
+
output += f"- **Visible:** {people}\n"
|
| 26 |
+
if "key_details" in result:
|
| 27 |
+
output += f"- **Key Details:** {result['key_details']}\n"
|
| 28 |
+
|
| 29 |
+
elif tool_name == "get_dna_test":
|
| 30 |
+
output += "### 𧬠DNA Analysis\n"
|
| 31 |
+
output += f"**Sample ID:** {result.get('evidence_id', 'Unknown')}\n"
|
| 32 |
+
if "result" in result:
|
| 33 |
+
output += f"**Result:** {result['result']}\n"
|
| 34 |
+
if "match" in result:
|
| 35 |
+
output += f"**Match:** {result['match']}\n"
|
| 36 |
+
|
| 37 |
+
elif tool_name == "call_alibi":
|
| 38 |
+
output += "### π Alibi Check\n"
|
| 39 |
+
if "status" in result:
|
| 40 |
+
output += f"**Status:** {result['status']}\n"
|
| 41 |
+
if "statement" in result:
|
| 42 |
+
output += f"**Statement:** \"{result['statement']}\"\n"
|
| 43 |
+
|
| 44 |
+
else:
|
| 45 |
+
# Fallback for unknown tools or simple strings
|
| 46 |
+
output = str(result)
|
| 47 |
+
|
| 48 |
+
return output
|
| 49 |
+
|
| 50 |
+
def format_suspect_card(suspect):
|
| 51 |
+
"""Generates HTML for a suspect card."""
|
| 52 |
+
return f"""
|
| 53 |
+
<div class="suspect-card">
|
| 54 |
+
<h3>{suspect['name']}</h3>
|
| 55 |
+
<p><strong>Role:</strong> {suspect['role']}</p>
|
| 56 |
+
<p><strong>Bio:</strong> {suspect['bio']}</p>
|
| 57 |
+
<p><strong>Phone:</strong> {suspect.get('phone_number', 'N/A')}</p>
|
| 58 |
+
<div style="margin-top: 5px; font-size: 0.8em; color: #666;">ID: {suspect['id']}</div>
|
| 59 |
+
</div>
|
| 60 |
+
"""
|
ui/styles.css
CHANGED
|
@@ -4,25 +4,35 @@
|
|
| 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 {
|
|
|
|
| 4 |
border-radius: 10px;
|
| 5 |
border: 2px solid #d0d0d0;
|
| 6 |
min-height: 300px;
|
| 7 |
+
color: #333 !important;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
.evidence-board p,
|
| 11 |
+
.evidence-board h3,
|
| 12 |
+
.evidence-board h4,
|
| 13 |
+
.evidence-board li,
|
| 14 |
+
.evidence-board ul {
|
| 15 |
+
color: #333 !important;
|
| 16 |
}
|
| 17 |
|
| 18 |
.suspect-card {
|
| 19 |
background: linear-gradient(135deg, #2b32b2 0%, #1488cc 100%);
|
| 20 |
border-radius: 15px;
|
| 21 |
padding: 15px;
|
| 22 |
+
color: white !important;
|
| 23 |
margin-bottom: 10px;
|
| 24 |
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 25 |
}
|
| 26 |
|
| 27 |
.suspect-card h3 {
|
| 28 |
margin-top: 0;
|
| 29 |
+
color: #fff !important;
|
| 30 |
}
|
| 31 |
|
| 32 |
.suspect-card p {
|
| 33 |
font-size: 0.9em;
|
| 34 |
opacity: 0.9;
|
| 35 |
+
color: #eee !important;
|
| 36 |
}
|
| 37 |
|
| 38 |
.chat-message {
|