Skip to content

Commit 7d2b265

Browse files
authored
Merge pull request #79 from MLAI-AUS-Inc/medhack-frontiers-skill
Medhack frontiers skill
2 parents 7d64a64 + fb1bdd0 commit 7d2b265

File tree

3 files changed

+379
-252
lines changed

3 files changed

+379
-252
lines changed

roo-standalone/roo/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ async def _medhack_daily_case_loop():
6060
client = mod.MedHackClient()
6161

6262
# Check if there's already a case for today
63-
current = client.get_current_case(today)
63+
current = await client.get_current_case(today)
6464
if current is None:
6565
# Start a new case
66-
new_case = client.start_new_case(today)
66+
new_case = await client.start_new_case(today)
6767
if new_case:
6868
difficulty = new_case.get("difficulty", "medium").upper()
6969
title = new_case.get("title", "")

roo-standalone/roo/skills/executor.py

Lines changed: 94 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -261,13 +261,13 @@ async def _execute_medhack(
261261
from ..slack_client import post_message
262262

263263
if requested_case_id is not None:
264-
new_case = client.start_specific_case(requested_case_id, today)
264+
new_case = await client.start_specific_case(requested_case_id, today, admin_slack_id=user_id)
265265
if not new_case:
266266
available = client.get_all_case_ids()
267267
return f"Case #{requested_case_id} not found. Available case IDs: {available}"
268268
else:
269269
# "next patient" — pick the next unplayed case
270-
new_case = client.start_new_case(today)
270+
new_case = await client.start_new_case(today, admin_slack_id=user_id)
271271
if not new_case:
272272
return "All cases have been played! No new cases available."
273273

@@ -328,17 +328,8 @@ async def _execute_medhack(
328328
is_event_q = any(k in text_lower for k in event_keywords)
329329
is_game_q = any(k in text_lower for k in game_keywords)
330330

331-
# If clearly a diagnosis guess, handle that first
332-
guess_patterns = ["i think it", "my diagnosis is", "my guess is", "is it ",
333-
"could it be", "i reckon it", "the diagnosis is",
334-
"i believe it", "it must be", "it's got to be",
335-
"does she have", "does he have", "do they have",
336-
"she has", "he has", "they have", "it could be",
337-
"maybe it's", "i'd say", "i'd guess",
338-
"has she got", "has he got", "have they got"]
339-
# Patterns that are ONLY for confirmation, not new guesses
331+
# Patterns for confirmation/lock-in only
340332
confirm_only_patterns = ["lock in", "lock it in", "final answer"]
341-
is_guess = any(p in text_lower for p in guess_patterns)
342333
is_lock_in = any(p in text_lower for p in confirm_only_patterns)
343334

344335
# Confirmation patterns for locking in a pending guess
@@ -348,25 +339,25 @@ async def _execute_medhack(
348339
cancel_patterns = ["no", "nah", "nope", "cancel", "never mind", "keep going",
349340
"not yet", "wait", "hold on", "keep digging"]
350341

351-
current_case = client.get_current_case(today)
342+
current_case = await client.get_current_case(today)
352343

353344
# --- Check for pending guess confirmation/cancellation ---
354-
pending_guess = client.get_pending_guess(user_id) if current_case else None
345+
pending_guess = (await client.get_pending_guess(user_id)) if current_case else None
355346
if pending_guess and current_case and not current_case.get("solved"):
356347
is_confirm = any(p in text_lower for p in confirm_patterns)
357348
is_cancel = any(p in text_lower for p in cancel_patterns)
358349

359350
if is_confirm:
360351
# Lock in the pending guess
361-
client.clear_pending_guess(user_id)
362-
result = client.check_guess(user_id, pending_guess, today)
352+
await client.clear_pending_guess(user_id)
353+
result = await client.check_guess(user_id, pending_guess, today)
363354
return await self._handle_guess_result(
364355
result, user_id, skill, text, client, today,
365356
thread_history, channel_id, pending_guess
366357
)
367358

368359
elif is_cancel:
369-
client.clear_pending_guess(user_id)
360+
await client.clear_pending_guess(user_id)
370361
return (
371362
f"<@{user_id}> No worries — guess cancelled. "
372363
f"Keep investigating and lock in your diagnosis when you're ready. "
@@ -376,70 +367,18 @@ async def _execute_medhack(
376367
# If they said something else while having a pending guess,
377368
# remind them (but also let the LLM respond to their question)
378369
# Clear the pending guess so it doesn't block future interactions
379-
client.clear_pending_guess(user_id)
370+
await client.clear_pending_guess(user_id)
380371

381-
# --- "Lock it in" with no pending guess: search thread history ---
382-
if is_lock_in and not is_guess and not pending_guess and current_case and not current_case.get("solved"):
383-
if client.is_user_locked_out(user_id, today):
372+
# --- "Lock it in" with no pending guess ---
373+
if is_lock_in and not pending_guess and current_case and not current_case.get("solved"):
374+
if await client.is_user_locked_out(user_id, today):
384375
return (
385376
f"<@{user_id}> Sorry mate, you've already used your guess for today's case. "
386377
"Come back tomorrow for a new one!"
387378
)
388-
# Try to find their most recent guess-like message in thread history
389-
extracted_guess = None
390-
if thread_history:
391-
for msg in reversed(thread_history) if isinstance(thread_history, list) else []:
392-
msg_user = msg.get("user", "")
393-
msg_text = msg.get("text", "").lower()
394-
if msg_user == user_id and msg_text != text_lower:
395-
# Check if this earlier message had a guess in it
396-
for p in guess_patterns:
397-
if p in msg_text:
398-
idx = msg_text.index(p) + len(p)
399-
candidate = msg.get("text", "")[idx:].strip().rstrip("?.!")
400-
if candidate:
401-
extracted_guess = candidate
402-
break
403-
if extracted_guess:
404-
break
405-
406-
if extracted_guess:
407-
# Lock it in immediately
408-
result = client.check_guess(user_id, extracted_guess, today)
409-
return await self._handle_guess_result(
410-
result, user_id, skill, text, client, today,
411-
thread_history, channel_id, extracted_guess
412-
)
413-
else:
414-
return (
415-
f"<@{user_id}> I'm not sure what diagnosis you want to lock in. "
416-
f"Please tell me your guess first, e.g. \"I think it's pneumonia\""
417-
)
418-
419-
# --- New guess attempt ---
420-
if is_guess and current_case and not current_case.get("solved"):
421-
if client.is_user_locked_out(user_id, today):
422-
return (
423-
f"<@{user_id}> Sorry mate, you've already used your guess for today's case. "
424-
"Come back tomorrow for a new one!"
425-
)
426-
427-
# Extract the guess from the text
428-
guess_text = text
429-
for prefix in guess_patterns:
430-
if prefix in text_lower:
431-
idx = text_lower.index(prefix) + len(prefix)
432-
candidate = text[idx:].strip().rstrip("?.!")
433-
if candidate:
434-
guess_text = candidate
435-
break
436-
437-
# Store as pending and ask for confirmation
438-
client.set_pending_guess(user_id, guess_text)
439379
return (
440-
f"<@{user_id}> You want to lock in *{guess_text}* as your final diagnosis?\n\n"
441-
f"_Remember: you only get *one guess* per case. "
442-
f"Reply *yes* to confirm or *no* to keep investigating._"
380+
f"<@{user_id}> I'm not sure what diagnosis you want to lock in. "
381+
f"Tell me your guess and I'll ask you to confirm before locking it in."
443382
)
444383

445384
# --- Repost the daily case (with image) ---
@@ -464,54 +403,52 @@ async def _execute_medhack(
464403
)
465404
return self._medhack_game_response(message, current_case.get("image_url", ""))
466405

467-
# --- Game interaction (not a guess) ---
468-
if is_game_q and current_case:
469-
if current_case.get("solved"):
470-
diagnosis_name = "already revealed"
471-
# Get the actual diagnosis for the solved message
472-
cases_data = client._load_cases()
473-
solved_case = next((c for c in cases_data if c["id"] == current_case["id"]), None)
474-
if solved_case:
475-
diagnosis_name = solved_case["diagnosis"]
476-
winners = current_case.get("winners", [])
477-
winner_mentions = ", ".join(f"<@{w}>" for w in winners)
478-
return (
479-
f"Today's case has been solved! The diagnosis was *{diagnosis_name}*.\n\n"
480-
f"Solved by: {winner_mentions}\n\n"
481-
f"Come back tomorrow for a new case!"
482-
)
406+
# --- Solved case ---
407+
if current_case and current_case.get("solved"):
408+
diagnosis_name = "already revealed"
409+
cases_data = client._load_cases()
410+
solved_case = next((c for c in cases_data if c["id"] == current_case["id"]), None)
411+
if solved_case:
412+
diagnosis_name = solved_case["diagnosis"]
413+
winners = current_case.get("winners", [])
414+
winner_mentions = ", ".join(f"<@{w}>" for w in winners)
415+
return (
416+
f"Today's case has been solved! The diagnosis was *{diagnosis_name}*.\n\n"
417+
f"Solved by: {winner_mentions}\n\n"
418+
f"Come back tomorrow for a new case!"
419+
)
483420

484-
# Block locked-out users from asking questions too
485-
if client.is_user_locked_out(user_id, today):
421+
# --- Locked out ---
422+
if current_case and await client.is_user_locked_out(user_id, today):
423+
return (
424+
f"<@{user_id}> Sorry mate, you've already used your guess for today's case "
425+
"so you can no longer interact with it. Come back tomorrow for a new one!"
426+
)
427+
428+
# --- Active unsolved case: use LLM to classify intent ---
429+
if current_case and not current_case.get("solved") and not is_event_q:
430+
classification = await self._classify_medhack_intent(text)
431+
432+
if classification.get("is_guess") and classification.get("diagnosis"):
433+
guess_text = classification["diagnosis"]
434+
await client.set_pending_guess(user_id, guess_text)
486435
return (
487-
f"<@{user_id}> Sorry mate, you've already used your guess for today's case "
488-
"so you can no longer interact with it. Come back tomorrow for a new one!"
436+
f"<@{user_id}> You want to lock in *{guess_text}* as your final diagnosis?\n\n"
437+
f"_Remember: you only get *one guess* per case. "
438+
f"Reply *yes* to confirm or *no* to keep investigating._"
489439
)
490440

491-
case_data = client.get_case_for_llm(today)
441+
# Not a guess — respond as PQM narrator
442+
case_data = await client.get_case_for_llm(today)
492443
llm_response = await self._medhack_llm_response(skill, text, case_data, thread_history)
493444
return f"<@{user_id}> {llm_response}"
494445

495-
if is_game_q and not current_case:
446+
if not current_case and is_game_q:
496447
return (
497448
"No active case right now! A new clinical case is posted each day. "
498449
"Keep an eye on this channel for the next one."
499450
)
500451

501-
# --- Default to game mode when there's an active unsolved case ---
502-
# If the user's question doesn't match game keywords but there IS an
503-
# active case and it's not clearly an event question, assume they're
504-
# talking to the patient.
505-
if current_case and not is_event_q and not current_case.get("solved"):
506-
if client.is_user_locked_out(user_id, today):
507-
return (
508-
f"<@{user_id}> Sorry mate, you've already used your guess for today's case "
509-
"so you can no longer interact with it. Come back tomorrow for a new one!"
510-
)
511-
case_data = client.get_case_for_llm(today)
512-
llm_response = await self._medhack_llm_response(skill, text, case_data, thread_history)
513-
return f"<@{user_id}> {llm_response}"
514-
515452
# --- Event info mode ---
516453
if is_event_q or (not is_game_q):
517454
event_info = client.load_event_info()
@@ -543,6 +480,51 @@ async def _execute_medhack(
543480

544481
return await self._execute_with_llm(skill, text, params, user_id, thread_history)
545482

483+
async def _classify_medhack_intent(self, text: str) -> dict:
484+
"""Use LLM to classify if a message is a diagnosis guess.
485+
486+
Returns dict with:
487+
is_guess (bool): whether the user is guessing a diagnosis
488+
diagnosis (str|None): the extracted diagnosis if is_guess
489+
"""
490+
prompt = f"""You are classifying messages in a medical diagnosis guessing game. Players interact with a simulated patient and can ask questions or guess the diagnosis.
491+
492+
A message is a DIAGNOSIS GUESS if the player is proposing what they think the medical diagnosis is. Examples:
493+
- "I think it's pneumonia" → guess: "pneumonia"
494+
- "Is it gastroenteritis?" → guess: "gastroenteritis"
495+
- "I guess Addison's disease" → guess: "Addison's disease"
496+
- "She has COPD" → guess: "COPD"
497+
- "Could it be lupus?" → guess: "lupus"
498+
- "My diagnosis is acute appendicitis" → guess: "acute appendicitis"
499+
- "gastroenteritis!" → guess: "gastroenteritis"
500+
501+
NOT a guess (these are clinical questions or requests):
502+
- "What are her vitals?"
503+
- "Can I see the blood results?"
504+
- "Does she have any allergies?" (asking about patient history, not guessing)
505+
- "Order a chest X-ray"
506+
- "Tell me about her symptoms"
507+
- "What medications is she on?"
508+
509+
Classify this message: "{text}"
510+
511+
Respond with ONLY valid JSON, no markdown:
512+
{{"is_guess": true, "diagnosis": "the diagnosis"}} or {{"is_guess": false, "diagnosis": null}}"""
513+
514+
openai_client = get_llm_client("openai")
515+
response = await openai_client.chat([
516+
{"role": "user", "content": prompt}
517+
], model="gpt-4o-mini", max_tokens=100)
518+
519+
import json as _json
520+
try:
521+
content = response.content.strip()
522+
if content.startswith("```"):
523+
content = content.split("\n", 1)[1].rsplit("```", 1)[0].strip()
524+
return _json.loads(content)
525+
except (ValueError, KeyError, IndexError):
526+
return {"is_guess": False, "diagnosis": None}
527+
546528
def _medhack_game_response(self, text: str, image_url: str = "") -> dict | str:
547529
"""Wrap a game response with image blocks when the case has an image_url."""
548530
if image_url:
@@ -629,7 +611,7 @@ async def _handle_guess_result(
629611

630612
else:
631613
# Wrong guess
632-
case_data = client.get_case_for_llm(today)
614+
case_data = await client.get_case_for_llm(today)
633615
llm_response = await self._medhack_llm_response(
634616
skill, text, case_data, thread_history,
635617
extra_instruction="The user just locked in an INCORRECT diagnosis guess. "

0 commit comments

Comments
 (0)