@@ -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