""" Medications command handler - bot-side hooks for medication management """ import asyncio import re from bot.command_registry import register_module import ai.parser as ai_parser async def _get_scheduled_time_from_context(message, med_name): """Fetch recent messages and extract scheduled time from medication reminder. Looks for bot messages in the last 5 messages that match the pattern: "Time to take {med_name} (...) ยท HH:MM" Returns the scheduled time string (e.g., "12:00") or None if not found. """ try: # Get last 5 messages from channel history async for msg in message.channel.history(limit=5): # Skip the current message if msg.id == message.id: continue # Check if this is a bot message if not msg.author.bot: continue content = msg.content # Look for reminder pattern: "Time to take {med_name} (...) ยท HH:MM" # Use case-insensitive match for medication name pattern = rf"Time to take {re.escape(med_name)}.*?ยท\s*(\d{{1,2}}:\d{{2}})" match = re.search(pattern, content, re.IGNORECASE) if match: return match.group(1) except Exception: pass return None async def handle_medication(message, session, parsed): action = parsed.get("action", "unknown") token = session["token"] user_uuid = session["user_uuid"] if action == "list": resp, status = api_request("get", "/api/medications", token) if status == 200: meds = resp if isinstance(resp, list) else [] if not meds: await message.channel.send("You don't have any medications yet.") else: lines = [ f"- **{m['name']}**: {m['dosage']} {m['unit']} ({m.get('frequency', 'n/a')})" for m in meds ] await message.channel.send("**Your medications:**\n" + "\n".join(lines)) else: await message.channel.send( f"Error: {resp.get('error', 'Failed to fetch medications')}" ) elif action == "add": name = parsed.get("name") dosage = parsed.get("dosage") unit = parsed.get("unit", "mg") frequency = parsed.get("frequency", "daily") times = parsed.get("times", ["08:00"]) days_of_week = parsed.get("days_of_week", []) interval_days = parsed.get("interval_days") needs_confirmation = parsed.get("needs_confirmation", False) confirmation_prompt = parsed.get("confirmation_prompt") if not name or not dosage: await message.channel.send("Please provide medication name and dosage.") return # Handle confirmation if needs_confirmation: # Store pending action in session for confirmation if "pending_confirmations" not in session: session["pending_confirmations"] = {} confirmation_id = f"med_add_{name}" session["pending_confirmations"][confirmation_id] = { "action": "add", "name": name, "dosage": dosage, "unit": unit, "frequency": frequency, "times": times, "days_of_week": days_of_week, "interval_days": interval_days, } schedule_desc = _format_schedule( frequency, times, days_of_week, interval_days ) await message.channel.send( f"{confirmation_prompt}\n\n" f"**Details:**\n" f"- Name: {name}\n" f"- Dosage: {dosage} {unit}\n" f"- Schedule: {schedule_desc}\n\n" f"Reply **yes** to confirm, or **no** to cancel." ) return await _add_medication( message, token, name, dosage, unit, frequency, times, days_of_week, interval_days, ) elif action == "take": med_id = parsed.get("medication_id") name = parsed.get("name") med_id, name, found = await _find_medication_by_name( message, token, med_id, name ) if not found: return if not med_id: await message.channel.send("Which medication did you take?") return # Try to get scheduled time from recent reminder context scheduled_time = await _get_scheduled_time_from_context(message, name) # Build request body with scheduled_time if found request_body = {} if scheduled_time: request_body["scheduled_time"] = scheduled_time resp, status = api_request( "post", f"/api/medications/{med_id}/take", token, request_body ) if status == 201: if scheduled_time: await message.channel.send( f"โœ… Logged **{name}** for {scheduled_time}! Great job staying on track." ) else: await message.channel.send( f"Logged **{name}**! Great job staying on track." ) else: await message.channel.send(f"Error: {resp.get('error', 'Failed to log')}") elif action == "skip": med_id = parsed.get("medication_id") name = parsed.get("name") reason = parsed.get("reason", "Skipped by user") med_id, name, found = await _find_medication_by_name( message, token, med_id, name ) if not found: return if not med_id: await message.channel.send("Which medication are you skipping?") return # Try to get scheduled time from recent reminder context scheduled_time = await _get_scheduled_time_from_context(message, name) # Build request body with scheduled_time if found request_body = {"reason": reason} if scheduled_time: request_body["scheduled_time"] = scheduled_time resp, status = api_request( "post", f"/api/medications/{med_id}/skip", token, request_body ) if status == 201: await message.channel.send(f"Skipped **{name}**. {reason}") else: await message.channel.send(f"Error: {resp.get('error', 'Failed to log')}") elif action == "today": resp, status = api_request("get", "/api/medications/today", token) if status == 200: meds = resp if isinstance(resp, list) else [] if not meds: await message.channel.send("No medications scheduled for today!") else: lines = [] for item in meds: med = item.get("medication", {}) times = item.get("scheduled_times", []) taken = item.get("taken_times", []) skipped = item.get("skipped_times", []) is_prn = item.get("is_prn", False) med_name = med.get("name", "Unknown") dosage = f"{med.get('dosage', '')} {med.get('unit', '')}".strip() if is_prn: status_icon = "๐Ÿ’Š" lines.append( f"{status_icon} **{med_name}** {dosage} (as needed)" ) elif times: for time in times: if time in taken: status_icon = "โœ…" elif time in skipped: status_icon = "โญ๏ธ" else: status_icon = "โฐ" lines.append( f"{status_icon} **{med_name}** {dosage} at {time}" ) else: lines.append(f"๐Ÿ’Š **{med_name}** {dosage}") await message.channel.send( "**Today's Medications:**\n" + "\n".join(lines) ) else: error_msg = "Failed to fetch today's schedule" await message.channel.send(f"Error: {resp.get('error', error_msg)}") elif action == "refills": resp, status = api_request("get", "/api/medications/refills-due", token) if status == 200: meds = resp if isinstance(resp, list) else [] if not meds: await message.channel.send("No refills due soon! ๐ŸŽ‰") else: lines = [] for med in meds: name = med.get("name", "Unknown") qty = med.get("quantity_remaining") refill_date = med.get("refill_date") if qty is not None and qty <= 7: lines.append(f"โš ๏ธ **{name}**: Only {qty} remaining") elif refill_date: lines.append(f"๐Ÿ“… **{name}**: Refill due by {refill_date}") await message.channel.send("**Refills Due:**\n" + "\n".join(lines)) else: await message.channel.send( f"Error: {resp.get('error', 'Failed to fetch refills')}" ) elif action == "snooze": med_id = parsed.get("medication_id") name = parsed.get("name") minutes = parsed.get("minutes", 15) med_id, name, found = await _find_medication_by_name( message, token, med_id, name ) if not found: return if not med_id: await message.channel.send("Which medication reminder should I snooze?") return resp, status = api_request( "post", f"/api/medications/{med_id}/snooze", token, {"minutes": minutes} ) if status == 200: await message.channel.send(f"โฐ Snoozed **{name}** for {minutes} minutes.") else: await message.channel.send( f"Error: {resp.get('error', 'Failed to snooze')}" ) elif action == "adherence": med_id = parsed.get("medication_id") name = parsed.get("name") if name and not med_id: # Look up by name first med_id, name, found = await _find_medication_by_name( message, token, None, name ) if not found: return if med_id: resp, status = api_request( "get", f"/api/medications/{med_id}/adherence", token ) else: resp, status = api_request("get", "/api/medications/adherence", token) if status == 200: if isinstance(resp, list): lines = [] for m in resp: adherence = m.get("adherence_percent") if adherence is not None: lines.append( f"- **{m['name']}**: {adherence}% adherence ({m.get('taken', 0)}/{m.get('expected', 0)} doses)" ) else: lines.append( f"- **{m['name']}**: PRN medication (no adherence tracking)" ) await message.channel.send("**Adherence Stats:**\n" + "\n".join(lines)) else: adherence = resp.get("adherence_percent") if adherence is not None: await message.channel.send( f"**{resp.get('name')}**:\n" f"- Adherence: {adherence}%\n" f"- Taken: {resp.get('taken', 0)}/{resp.get('expected', 0)} doses\n" f"- Skipped: {resp.get('skipped', 0)}" ) else: await message.channel.send( f"**{resp.get('name')}**: PRN medication (no adherence tracking)" ) else: await message.channel.send( f"Error: {resp.get('error', 'Failed to fetch adherence')}" ) elif action == "delete": med_id = parsed.get("medication_id") name = parsed.get("name") needs_confirmation = parsed.get("needs_confirmation", True) med_id, name, found = await _find_medication_by_name( message, token, med_id, name ) if not found: return if not med_id: await message.channel.send("Which medication should I delete?") return # Handle confirmation if needs_confirmation: if "pending_confirmations" not in session: session["pending_confirmations"] = {} confirmation_id = f"med_delete_{name}" session["pending_confirmations"][confirmation_id] = { "action": "delete", "interaction_type": "medication", "medication_id": med_id, "name": name, "needs_confirmation": False, # Skip confirmation next time } await message.channel.send( f"โš ๏ธ Are you sure you want to delete **{name}**?\n\n" f"This will also delete all logs for this medication.\n\n" f"Reply **yes** to confirm deletion, or **no** to cancel." ) return # Actually delete resp, status = api_request("delete", f"/api/medications/{med_id}", token) if status == 200: await message.channel.send(f"๐Ÿ—‘๏ธ Deleted **{name}** and all its logs.") else: await message.channel.send( f"Error: {resp.get('error', 'Failed to delete medication')}" ) else: await message.channel.send( f"Unknown action: {action}. Try: list, add, delete, take, skip, today, refills, snooze, or adherence." ) async def _add_medication( message, token, name, dosage, unit, frequency, times, days_of_week, interval_days ): """Helper to add a medication.""" data = { "name": name, "dosage": dosage, "unit": unit, "frequency": frequency, "times": times, } # Add optional fields if present if days_of_week: data["days_of_week"] = days_of_week if interval_days: data["interval_days"] = interval_days resp, status = api_request("post", "/api/medications", token, data) if status == 201: schedule_desc = _format_schedule(frequency, times, days_of_week, interval_days) await message.channel.send( f"โœ… Added **{name}** ({dosage} {unit}) - {schedule_desc}" ) else: error_msg = resp.get("error", "Failed to add medication") if "invalid input syntax" in error_msg.lower(): await message.channel.send( f"Error: I couldn't understand the schedule format. Try saying it differently, like 'every day at 9am' or 'on Monday and Wednesday at 8pm'." ) else: await message.channel.send(f"Error: {error_msg}") async def _find_medication_by_name(message, token, med_id, name): """Helper to find medication by name if ID not provided. Returns (med_id, name, found).""" if med_id: return med_id, name, True if not name: return None, None, True # Will prompt user later # Look up medication by name resp, status = api_request("get", "/api/medications", token) if status != 200: await message.channel.send("Error looking up medication. Please try again.") return None, None, False meds = resp if isinstance(resp, list) else [] # Try exact match first matching = [m for m in meds if m["name"].lower() == name.lower()] # Then try partial match if not matching: matching = [ m for m in meds if name.lower() in m["name"].lower() or m["name"].lower() in name.lower() ] if not matching: await message.channel.send( f"โŒ I couldn't find a medication called '{name}'.\n\nYour medications:\n" + "\n".join([f"- {m['name']}" for m in meds[:10]]) ) return None, None, False if len(matching) > 1: # Multiple matches - show list lines = [f"{i + 1}. {m['name']}" for i, m in enumerate(matching[:5])] await message.channel.send( f"๐Ÿ” I found multiple medications matching '{name}':\n" + "\n".join(lines) + "\n\nPlease specify which one (e.g., 'take 1' or say the full name)." ) return None, None, False return matching[0]["id"], matching[0]["name"], True def _format_schedule(frequency, times, days_of_week, interval_days): """Helper to format schedule description.""" if frequency == "daily": return f"daily at {', '.join(times)}" elif frequency == "twice_daily": return f"twice daily at {', '.join(times)}" elif frequency == "specific_days": days_str = ", ".join(days_of_week) if days_of_week else "certain days" return f"on {days_str} at {', '.join(times)}" elif frequency == "every_n_days": return f"every {interval_days} days at {', '.join(times)}" elif frequency == "as_needed": return "as needed" else: return f"{frequency} at {', '.join(times)}" def api_request(method, endpoint, token, data=None): import requests import os API_URL = os.getenv("API_URL", "http://app:5000") url = f"{API_URL}{endpoint}" headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"} try: resp = getattr(requests, method)(url, headers=headers, json=data, timeout=10) try: return resp.json(), resp.status_code except ValueError: return {}, resp.status_code except requests.RequestException: return {"error": "API unavailable"}, 503 def validate_medication_json(data): errors = [] if not isinstance(data, dict): return ["Response must be a JSON object"] if "error" in data: return [] if "action" not in data: errors.append("Missing required field: action") return errors register_module("medication", handle_medication) ai_parser.register_validator("medication", validate_medication_json)