Justxd22 commited on
Commit
e7dcd7e
Β·
1 Parent(s): a591d42

Enhance game mechanics and UI: Update Gradio version, improve suspect card formatting, and add phone number handling for suspects

Browse files
Files changed (8) hide show
  1. GEMINI.md +1 -1
  2. app.py +123 -63
  3. game/game_engine.py +1 -0
  4. mcp/tools.py +82 -26
  5. prompts/murderer.txt +3 -0
  6. prompts/witness.txt +3 -0
  7. ui/components.py +60 -0
  8. 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 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.
 
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 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 = [
@@ -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 cards
 
46
  initial_chat, # Chatbot
47
- evidence_html, # Evidence board
48
  f"Round: {game.round}/5 | Points: {game.points}", # Stats
49
- gr.update(choices=[(s['name'], s['id']) for s in game.scenario["suspects"]], value=None), # Suspect dropdown
50
  gr.update(interactive=True), # Question input
51
  gr.update(interactive=True), # Question btn
52
- gr.update(interactive=True) # Tool btn
 
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
- # Update chat
69
- # Gradio 6 requires [{"role": "user", "content": "..."}, ...] format
70
 
71
- history.append({"role": "user", "content": f"**Detective to {suspect_id}:** {question}"})
72
- history.append({"role": "assistant", "content": f"**{suspect_id}:** {response}"})
73
 
74
  return history, "" # Clear input
75
 
76
- def use_tool_ui(session_id, tool_name, arg1, arg2, history):
77
  game = get_current_game(session_id)
78
  if not game:
79
- return history, "No game", "Error"
80
 
81
  # Construct kwargs based on tool
82
  kwargs = {}
83
  if tool_name == "get_location":
84
- kwargs = {"phone_number": arg1, "timestamp": arg2}
85
  elif tool_name == "get_footage":
86
- kwargs = {"location": arg1, "time_range": arg2}
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}\nResult: {result}"})
96
 
97
  # Update Evidence Board
98
- ev_html = f"<h3>Case: {game.scenario['title']}</h3><p>Victim: {game.scenario['victim']['name']}</p><hr><h4>Evidence Revealed:</h4><ul>"
99
- for item in game.evidence_revealed:
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, ev_html, stats
106
 
107
  def next_round_ui(session_id):
108
  game = get_current_game(session_id)
109
  if not game:
110
- return "No game", "Error"
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
- suspect_1 = gr.HTML(elem_classes="suspect-card")
134
- suspect_2 = gr.HTML(elem_classes="suspect-card")
135
- suspect_3 = gr.HTML(elem_classes="suspect-card")
136
- suspect_4 = gr.HTML(elem_classes="suspect-card")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- suspect_dropdown = gr.Dropdown(label="Select Suspect", choices=[])
149
- question_input = gr.Textbox(label="Question", placeholder="Where were you?", interactive=False)
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="Arg 1 (Phone/Loc/ID)")
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.HTML(
169
- elem_classes="evidence-board",
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=[difficulty_selector],
183
  outputs=[
184
  session_state,
185
- suspect_1, suspect_2, suspect_3, suspect_4,
 
186
  chatbot,
187
  evidence_board,
188
  game_stats,
189
- suspect_dropdown,
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, suspect_dropdown, question_input, chatbot],
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, arg2_input, chatbot],
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
- theme=gr.themes.Monochrome(),
216
- css=custom_css,
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
- if suspect["phone_number"] == phone_number:
 
 
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
- """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"],
@@ -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
- 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"):
 
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 {