From c903363f6efa3c5b543acd15d3204adb3eebc351 Mon Sep 17 00:00:00 2001 From: Chelsea Lee Date: Mon, 16 Feb 2026 05:53:40 -0600 Subject: [PATCH] feat(bot): comprehensive natural language command parsing - Enhanced AI prompts with time/frequency conversion rules and 20+ examples - Added smart medication name resolution and confirmation flows - New medication commands: today, refills, snooze, adherence - New routine commands: create_with_steps, add_steps, steps, schedule - Added active session awareness with shortcuts (done, skip, pause, resume) - Confirmation handling for destructive/create actions - Improved help message with natural language examples --- ai/ai_config.json | 4 +- bot/bot.py | 138 ++++++++++++- bot/commands/medications.py | 293 +++++++++++++++++++++------ bot/commands/routines.py | 391 ++++++++++++++++++++++++++++++------ 4 files changed, 700 insertions(+), 126 deletions(-) diff --git a/ai/ai_config.json b/ai/ai_config.json index 5130d34..d71deb4 100644 --- a/ai/ai_config.json +++ b/ai/ai_config.json @@ -3,8 +3,8 @@ "max_tokens": 8192, "prompts": { "command_parser": { - "system": "You are a helpful AI assistant that parses user commands into structured JSON. Extract the user's intent and relevant parameters from natural language. Return ONLY valid JSON, no explanations.\n\nBe flexible with language - handle typos, slang, and casual phrasing. Consider conversation context when available.\n\nKEY RULES:\n1. 'habit' = 'routine' - treat these as the same thing\n2. Extract medication/routine names from descriptions:\n - 'take a giant dab of THC' → action: 'take', name: 'THC'\n - 'smoke marijuana' as a routine → action: 'create', name: 'smoke marijuana'\n3. If user says 'I want to...' or describes creating something, infer action: 'create'\n4. If user says 'called X' or 'named X', extract X as the name\n5. If medication/routine isn't found and user said 'take'/'start', ask for the name, don't leave it blank\n6. For medications, frequency must be ONE OF: 'daily', 'twice_daily', 'specific_days', 'every_n_days', 'as_needed'\n7. Days of week must be in JSON array format: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']\n8. Times must be in JSON array format in 24hr format: ['08:00'], ['09:00', '21:00']\n\nAvailable interaction types:\n- 'routine': managing daily routines/habits (create|list|start|complete|skip|cancel|history|pause|resume)\n- 'medication': managing medications (add|list|take|skip|adherence)\n\nFor routines, extract: action, name, description (optional)\nFor medications, extract: action, name, dosage (number), unit, frequency (enum), times (array), days_of_week (array if specific_days)", - "user_template": "Parse this command into structured JSON.\n\nCurrent conversation context:\n{history_context}\n\nUser message: \"{user_input}\"\n\nReturn JSON with:\n{{\n \"interaction_type\": \"routine\" or \"medication\",\n \"action\": \"string\",\n \"name\": \"string\",\n \"description\": \"string\" (optional),\n \"dosage\": number (for meds),\n \"unit\": \"string\" (for meds: mg, mcg, g, ml, etc),\n \"frequency\": \"daily\" | \"twice_daily\" | \"specific_days\" | \"every_n_days\" | \"as_needed\",\n \"times\": [\"HH:MM\"] (for meds),\n \"days_of_week\": [\"mon\", \"tue\", ...] (only if frequency is specific_days),\n \"confidence\": number (0-1),\n \"needs_clarification\": \"string\" (if confidence < 0.8)\n}}\n\nEXAMPLES:\nRoutine examples:\n- 'take a giant dab of THC' → {{\"interaction_type\": \"medication\", \"action\": \"take\", \"name\": \"THC\", \"confidence\": 0.9}}\n- 'I want to create a habit called smoke dope' → {{\"interaction_type\": \"routine\", \"action\": \"create\", \"name\": \"smoke dope\", \"confidence\": 0.95}}\n- 'start my morning routine' → {{\"interaction_type\": \"routine\", \"action\": \"start\", \"name\": \"morning routine\", \"confidence\": 0.9}}\n\nMedication examples:\n- 'add lsd 50 mcg daily at 9am' → {{\"interaction_type\": \"medication\", \"action\": \"add\", \"name\": \"lsd\", \"dosage\": 50, \"unit\": \"mcg\", \"frequency\": \"daily\", \"times\": [\"09:00\"], \"confidence\": 0.95}}\n- 'add wellbutrin 150 mg twice daily' → {{\"interaction_type\": \"medication\", \"action\": \"add\", \"name\": \"wellbutrin\", \"dosage\": 150, \"unit\": \"mg\", \"frequency\": \"twice_daily\", \"times\": [\"08:00\", \"20:00\"], \"confidence\": 0.95}}\n- 'add vitamin d on tuesday and saturday' → {{\"interaction_type\": \"medication\", \"action\": \"add\", \"name\": \"vitamin d\", \"dosage\": 1, \"unit\": \"pill\", \"frequency\": \"specific_days\", \"times\": [\"08:00\"], \"days_of_week\": [\"tue\", \"sat\"], \"confidence\": 0.9}}\n- 'add aspirin every 3 days' → {{\"interaction_type\": \"medication\", \"action\": \"add\", \"name\": \"aspirin\", \"dosage\": 81, \"unit\": \"mg\", \"frequency\": \"every_n_days\", \"interval_days\": 3, \"times\": [\"08:00\"], \"confidence\": 0.9}}\n- 'which meds do I have?' → {{\"interaction_type\": \"medication\", \"action\": \"list\", \"confidence\": 0.95}}\n\nIf unclear, set needs_clarification explaining what's missing." + "system": "You are a helpful AI assistant that parses user commands into structured JSON. Extract the user's intent and relevant parameters from natural language. Return ONLY valid JSON, no explanations.\n\nBe flexible with language - handle typos, slang, and casual phrasing. Consider conversation context when available.\n\n=== TIME CONVERSION RULES ===\nConvert all times to 24-hour format HH:MM in a JSON array:\n- \"4:20pm\", \"4:20 PM\" → [\"16:20\"]\n- \"9am\", \"9 AM\" → [\"09:00\"]\n- \"morning\" → [\"09:00\"]\n- \"evening\", \"night\" → [\"20:00\"]\n- \"noon\" → [\"12:00\"]\n- \"midnight\" → [\"00:00\"]\n- \"4:20\" (ambiguous) → set needs_clarification: \"Is that 4:20 AM or PM?\"\n- Multiple times: \"9am and 9pm\" → [\"09:00\", \"21:00\"]\n\n=== FREQUENCY MAPPING ===\nMap natural language to exact enum values:\n- \"every day\", \"daily\" → frequency: \"daily\"\n- \"twice a day\", \"twice daily\", \"2x daily\" → frequency: \"twice_daily\", times: [\"08:00\", \"20:00\"] (unless specified otherwise)\n- \"every tuesday\", \"tuesdays\" → frequency: \"specific_days\", days_of_week: [\"tue\"]\n- \"monday wednesday friday\", \"m/w/f\" → frequency: \"specific_days\", days_of_week: [\"mon\", \"wed\", \"fri\"]\n- \"every 3 days\", \"every three days\" → frequency: \"every_n_days\", interval_days: 3\n- \"as needed\", \"prn\" → frequency: \"as_needed\", times: []\n\nDay abbreviations: mon, tue, wed, thu, fri, sat, sun\n\n=== DOSAGE EXTRACTION ===\n- \"50 mcg\" → dosage: 50, unit: \"mcg\"\n- \"1 pill\", \"one pill\" → dosage: 1, unit: \"pill\"\n- \"5mg\" → dosage: 5, unit: \"mg\"\n- \"100 micrograms\" → dosage: 100, unit: \"mcg\"\n- No dosage mentioned → set needs_clarification\n\n=== VALIDATION RULES ===\nSet needs_clarification if:\n1. Dosage is missing for 'add' action\n2. Time is ambiguous (e.g., just \"4:20\" without AM/PM)\n3. Frequency is unclear (e.g., \"sometimes\", \"often\")\n4. Name cannot be determined\n\n=== INTERACTION TYPES ===\n- \"routine\": habits, routines, activities with steps\n- \"medication\": medications, drugs, supplements, vitamins\n\nAvailable actions:\n- routine: create, create_with_steps, list, start, complete, skip, cancel, pause, resume, steps, schedule, stats, history\n- medication: add, list, take, skip, today, refills, snooze, adherence\n\n=== STEP EXTRACTION FOR ROUTINES ===\nWhen user mentions creating a routine WITH steps:\n- \"create morning routine with brush teeth, shower, eat\"\n → action: \"create_with_steps\", name: \"morning routine\", steps: [\"brush teeth\", \"shower\", \"eat\"], needs_confirmation: true\n- \"add steps to X: A, B, C\" → action: \"add_steps\", routine_name: \"X\", steps: [\"A\", \"B\", \"C\"]", + "user_template": "Parse this command into structured JSON.\n\nCurrent conversation context:\n{history_context}\n\nUser message: \"{user_input}\"\n\nReturn JSON with these exact fields:\n{{\n \"interaction_type\": \"routine\" | \"medication\",\n \"action\": \"string\",\n \"name\": \"string\" (med/routine name),\n \"routine_name\": \"string\" (for step-related actions),\n \"description\": \"string\" (optional),\n \"steps\": [\"step1\", \"step2\"] (for routine creation),\n \"dosage\": number (for meds),\n \"unit\": \"string\" (mg, mcg, pill, etc),\n \"frequency\": \"daily\" | \"twice_daily\" | \"specific_days\" | \"every_n_days\" | \"as_needed\",\n \"times\": [\"HH:MM\"],\n \"days_of_week\": [\"mon\", \"tue\", ...],\n \"interval_days\": number (for every_n_days),\n \"needs_confirmation\": boolean (true for destructive/create actions),\n \"confirmation_prompt\": \"string\" (what to ask user),\n \"confidence\": number (0-1),\n \"needs_clarification\": \"string\" (if confidence < 0.8 or missing required fields)\n}}\n\n=== EXAMPLES ===\n\nMedication examples:\n1. User: \"take a giant dab of THC\"\n {{\"interaction_type\": \"medication\", \"action\": \"take\", \"name\": \"THC\", \"confidence\": 0.9}}\n\n2. User: \"add lsd 50 mcg daily at 9am\"\n {{\"interaction_type\": \"medication\", \"action\": \"add\", \"name\": \"lsd\", \"dosage\": 50, \"unit\": \"mcg\", \"frequency\": \"daily\", \"times\": [\"09:00\"], \"confidence\": 0.95}}\n\n3. User: \"add wellbutrin 150 mg twice daily\"\n {{\"interaction_type\": \"medication\", \"action\": \"add\", \"name\": \"wellbutrin\", \"dosage\": 150, \"unit\": \"mg\", \"frequency\": \"twice_daily\", \"times\": [\"08:00\", \"20:00\"], \"confidence\": 0.95}}\n\n4. User: \"add vitamin d on tuesday and saturday at 8am\"\n {{\"interaction_type\": \"medication\", \"action\": \"add\", \"name\": \"vitamin d\", \"dosage\": 1, \"unit\": \"pill\", \"frequency\": \"specific_days\", \"times\": [\"08:00\"], \"days_of_week\": [\"tue\", \"sat\"], \"confidence\": 0.95}}\n\n5. User: \"add lsd every tuesday at 4:20\"\n {{\"interaction_type\": \"medication\", \"action\": \"add\", \"name\": \"lsd\", \"needs_clarification\": \"Is that 4:20 AM or PM?\", \"confidence\": 0.7}}\n\n6. User: \"which meds do I have?\"\n {{\"interaction_type\": \"medication\", \"action\": \"list\", \"confidence\": 0.95}}\n\n7. User: \"what's my schedule today?\"\n {{\"interaction_type\": \"medication\", \"action\": \"today\", \"confidence\": 0.9}}\n\n8. User: \"any refills due?\"\n {{\"interaction_type\": \"medication\", \"action\": \"refills\", \"confidence\": 0.9}}\n\n9. User: \"snooze my reminder for 30 minutes\"\n {{\"interaction_type\": \"medication\", \"action\": \"snooze\", \"minutes\": 30, \"confidence\": 0.9}}\n\nRoutine examples:\n10. User: \"create a morning routine\"\n {{\"interaction_type\": \"routine\", \"action\": \"create\", \"name\": \"morning routine\", \"needs_confirmation\": true, \"confirmation_prompt\": \"Create routine 'morning routine'?\", \"confidence\": 0.9}}\n\n11. User: \"create morning routine with brush teeth, shower, eat breakfast\"\n {{\"interaction_type\": \"routine\", \"action\": \"create_with_steps\", \"name\": \"morning routine\", \"steps\": [\"brush teeth\", \"shower\", \"eat breakfast\"], \"needs_confirmation\": true, \"confirmation_prompt\": \"Create 'morning routine' with 3 steps?\", \"confidence\": 0.95}}\n\n12. User: \"add steps to morning routine: meditate, journal\"\n {{\"interaction_type\": \"routine\", \"action\": \"add_steps\", \"routine_name\": \"morning routine\", \"steps\": [\"meditate\", \"journal\"], \"confidence\": 0.9}}\n\n13. User: \"what steps are in my morning routine?\"\n {{\"interaction_type\": \"routine\", \"action\": \"steps\", \"name\": \"morning routine\", \"confidence\": 0.9}}\n\n14. User: \"start morning routine\"\n {{\"interaction_type\": \"routine\", \"action\": \"start\", \"name\": \"morning routine\", \"confidence\": 0.9}}\n\n15. User: \"done\" (when in active session)\n {{\"interaction_type\": \"routine\", \"action\": \"complete\", \"confidence\": 0.9}}\n\n16. User: \"skip this step\"\n {{\"interaction_type\": \"routine\", \"action\": \"skip\", \"confidence\": 0.9}}\n\n17. User: \"schedule morning routine for monday wednesday friday at 7am\"\n {{\"interaction_type\": \"routine\", \"action\": \"schedule\", \"name\": \"morning routine\", \"days_of_week\": [\"mon\", \"wed\", \"fri\"], \"times\": [\"07:00\"], \"confidence\": 0.9}}\n\n18. User: \"show my routine stats\"\n {{\"interaction_type\": \"routine\", \"action\": \"stats\", \"confidence\": 0.9}}\n\n19. User: \"I want to create a habit called smoke dope\"\n {{\"interaction_type\": \"routine\", \"action\": \"create\", \"name\": \"smoke dope\", \"needs_confirmation\": true, \"confirmation_prompt\": \"Create routine 'smoke dope'?\", \"confidence\": 0.9}}\n\n20. User: \"add lsd\" (missing dosage)\n {{\"interaction_type\": \"medication\", \"action\": \"add\", \"name\": \"lsd\", \"needs_clarification\": \"What's the dosage for lsd?\", \"confidence\": 0.6}}\n\nIf the user describes something but key info is missing, set needs_clarification explaining what's needed." } }, "validation": { diff --git a/bot/bot.py b/bot/bot.py index a5990ec..de5aeb1 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -182,11 +182,119 @@ async def handleLoginStep(message): async def sendHelpMessage(message): - registered = list_registered() - help_msg = f"**Available Modules:**\n{chr(10).join(f'- {m}' for m in registered) if registered else '- No modules registered'}\n\nJust talk naturally and I'll help you out!" + help_msg = """**🤖 Synculous Bot - Natural Language Commands** + +Just talk to me naturally! Here are some examples: + +**💊 Medications:** +• "add lsd 50 mcg every tuesday at 4:20pm" +• "take my wellbutrin" +• "what meds do i have today?" +• "show my refills" +• "snooze my reminder for 30 minutes" +• "check adherence" + +**📋 Routines:** +• "create morning routine with brush teeth, shower, eat" +• "start my morning routine" +• "done" (complete current step) +• "skip" (skip current step) +• "pause/resume" (pause or continue) +• "what steps are in my routine?" +• "schedule workout for monday wednesday friday at 7am" +• "show my stats" + +**💡 Tips:** +• I understand natural language, typos, and slang +• If I'm unsure, I'll ask for clarification +• For important actions, I'll ask you to confirm with "yes" or "no" +• When you're in a routine, shortcuts like "done", "skip", "pause" work automatically""" await message.channel.send(help_msg) +async def checkActiveSession(session): + """Check if user has an active routine session and return details.""" + token = session.get("token") + if not token: + return None + + resp, status = apiRequest("get", "/api/sessions/active", token) + if status == 200 and "session" in resp: + return resp + return None + + +async def handleConfirmation(message, session): + """Handle yes/no confirmation responses. Returns True if handled.""" + discord_id = message.author.id + user_input = message.content.lower().strip() + + if "pending_confirmations" not in session: + return False + + # Check for any pending confirmations + pending = session["pending_confirmations"] + if not pending: + return False + + # Get the most recent pending confirmation + confirmation_id = list(pending.keys())[-1] + confirmation_data = pending[confirmation_id] + + if user_input in ("yes", "y", "yeah", "sure", "ok", "confirm"): + # Execute the confirmed action + del pending[confirmation_id] + + interaction_type = confirmation_data.get("interaction_type") + handler = get_handler(interaction_type) + + if handler: + # Create a fake parsed object for the handler + fake_parsed = confirmation_data.copy() + fake_parsed["needs_confirmation"] = False + await handler(message, session, fake_parsed) + return True + + elif user_input in ("no", "n", "nah", "cancel", "abort"): + del pending[confirmation_id] + await message.channel.send("❌ Cancelled.") + return True + + return False + + +async def handleActiveSessionShortcuts(message, session, active_session): + """Handle shortcuts like 'done', 'skip', 'next' when in active session.""" + user_input = message.content.lower().strip() + + # Map common shortcuts to actions + shortcuts = { + "done": ("routine", "complete"), + "finished": ("routine", "complete"), + "complete": ("routine", "complete"), + "next": ("routine", "complete"), + "skip": ("routine", "skip"), + "pass": ("routine", "skip"), + "pause": ("routine", "pause"), + "hold": ("routine", "pause"), + "resume": ("routine", "resume"), + "continue": ("routine", "resume"), + "stop": ("routine", "cancel"), + "quit": ("routine", "cancel"), + "abort": ("routine", "abort"), + } + + if user_input in shortcuts: + interaction_type, action = shortcuts[user_input] + handler = get_handler(interaction_type) + if handler: + fake_parsed = {"action": action} + await handler(message, session, fake_parsed) + return True + + return False + + async def routeCommand(message): discord_id = message.author.id session = user_sessions[discord_id] @@ -196,9 +304,33 @@ async def routeCommand(message): await sendHelpMessage(message) return + # Check for active session first + active_session = await checkActiveSession(session) + + # Handle confirmation responses + confirmation_handled = await handleConfirmation(message, session) + if confirmation_handled: + return + + # Handle shortcuts when in active session + if active_session: + shortcut_handled = await handleActiveSessionShortcuts(message, session, active_session) + if shortcut_handled: + return + async with message.channel.typing(): history = message_history.get(discord_id, []) - parsed = ai_parser.parse(message.content, "command_parser", history=history) + + # Add context about active session to help AI understand + context = "" + if active_session: + session_data = active_session.get("session", {}) + routine_name = session_data.get("routine_name", "a routine") + current_step = session_data.get("current_step_index", 0) + 1 + total_steps = active_session.get("total_steps", 0) + context = f"\n[Context: User is currently in active session for '{routine_name}', on step {current_step} of {total_steps}. They can say 'done', 'skip', 'pause', 'resume', or 'stop'.]" + + parsed = ai_parser.parse(message.content + context, "command_parser", history=history) if discord_id not in message_history: message_history[discord_id] = [] diff --git a/bot/commands/medications.py b/bot/commands/medications.py index 7df1fab..a11ea0e 100644 --- a/bot/commands/medications.py +++ b/bot/commands/medications.py @@ -32,58 +32,59 @@ async def handle_medication(message, session, parsed): 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 - 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 + # Handle confirmation + if needs_confirmation: + # Store pending action in session for confirmation + if "pending_confirmations" not in session: + session["pending_confirmations"] = {} - resp, status = api_request("post", "/api/medications", token, data) - if status == 201: - await message.channel.send(f"Added **{name}** to your medications!") - else: - await message.channel.send(f"Error: {resp.get('error', 'Failed to add medication')}") + 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") - if not med_id and name: - # Look up medication by name - resp, status = api_request("get", "/api/medications", token) - if status == 200: - meds = resp if isinstance(resp, list) else [] - # Find medication by name (case-insensitive partial match) - matching = [m for m in meds if name.lower() in m['name'].lower() or m['name'].lower() in name.lower()] - if matching: - med_id = matching[0]['id'] - name = matching[0]['name'] - else: - await message.channel.send(f"I couldn't find a medication called '{name}'. Use 'list' to see your medications.") - return - else: - await message.channel.send("Error looking up medication. Please try again.") - return - elif not med_id: + 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 resp, status = api_request("post", f"/api/medications/{med_id}/take", token, {}) if status == 201: - await message.channel.send("Logged it! Great job staying on track.") + 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')}") @@ -92,49 +93,225 @@ async def handle_medication(message, session, parsed): name = parsed.get("name") reason = parsed.get("reason", "Skipped by user") - if not med_id and name: - # Look up medication by name - resp, status = api_request("get", "/api/medications", token) - if status == 200: - meds = resp if isinstance(resp, list) else [] - # Find medication by name (case-insensitive partial match) - matching = [m for m in meds if name.lower() in m['name'].lower() or m['name'].lower() in name.lower()] - if matching: - med_id = matching[0]['id'] - name = matching[0]['name'] - else: - await message.channel.send(f"I couldn't find a medication called '{name}'. Use 'list' to see your medications.") - return - else: - await message.channel.send("Error looking up medication. Please try again.") - return - elif not med_id: + 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 resp, status = api_request("post", f"/api/medications/{med_id}/skip", token, {"reason": reason}) if status == 201: - await message.channel.send("OK, noted.") + 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: + await message.channel.send(f"Error: {resp.get('error', 'Failed to fetch today\'s schedule')}") + + 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 = [f"- {m['name']}: {m['adherence_percent']}% adherence" for m in resp] - await message.channel.send("**Adherence:**\n" + "\n".join(lines)) + 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: - await message.channel.send(f"**{resp.get('name')}**: {resp.get('adherence_percent')}% adherence") + 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')}") else: - await message.channel.send(f"Unknown action: {action}. Try: list, add, take, skip, or adherence.") + await message.channel.send(f"Unknown action: {action}. Try: list, add, 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): diff --git a/bot/commands/routines.py b/bot/commands/routines.py index 410a785..658f605 100644 --- a/bot/commands/routines.py +++ b/bot/commands/routines.py @@ -26,48 +26,183 @@ async def handle_routine(message, session, parsed): elif action == "create": name = parsed.get("name") description = parsed.get("description") + needs_confirmation = parsed.get("needs_confirmation", False) + confirmation_prompt = parsed.get("confirmation_prompt") if not name: await message.channel.send("Please provide a routine name.") return - data = {"name": name, "description": description or ""} - resp, status = api_request("post", "/api/routines", token, data) - if status == 201: - await message.channel.send(f"Created routine **{name}**!") + # Handle confirmation + if needs_confirmation: + if "pending_confirmations" not in session: + session["pending_confirmations"] = {} + + confirmation_id = f"routine_create_{name}" + session["pending_confirmations"][confirmation_id] = { + "action": "create", + "interaction_type": "routine", + "name": name, + "description": description or "" + } + + await message.channel.send( + f"{confirmation_prompt}\n\n" + f"Reply **yes** to create it, or **no** to cancel." + ) + return + + await _create_routine(message, token, name, description or "") + + elif action == "create_with_steps": + name = parsed.get("name") + steps = parsed.get("steps", []) + description = parsed.get("description", "") + needs_confirmation = parsed.get("needs_confirmation", True) + confirmation_prompt = parsed.get("confirmation_prompt") + + if not name: + await message.channel.send("Please provide a routine name.") + return + + if not steps: + await message.channel.send("What steps should this routine have?") + return + + # Handle confirmation + if needs_confirmation: + if "pending_confirmations" not in session: + session["pending_confirmations"] = {} + + confirmation_id = f"routine_create_{name}" + session["pending_confirmations"][confirmation_id] = { + "action": "create_with_steps", + "interaction_type": "routine", + "name": name, + "description": description, + "steps": steps + } + + steps_list = "\n".join([f"{i+1}. {step}" for i, step in enumerate(steps)]) + await message.channel.send( + f"{confirmation_prompt}\n\n" + f"**Steps:**\n{steps_list}\n\n" + f"Reply **yes** to create it, or **no** to cancel." + ) + return + + await _create_routine_with_steps(message, token, name, description, steps) + + elif action == "add_steps": + routine_name = parsed.get("routine_name") + steps = parsed.get("steps", []) + + if not routine_name or not steps: + await message.channel.send("Please specify the routine and steps to add.") + return + + # Find routine by name + routine_id, name, found = await _find_routine_by_name(message, token, None, routine_name) + if not found: + return + + if not routine_id: + await message.channel.send(f"Which routine should I add steps to?") + return + + # Add each step + success_count = 0 + for i, step_name in enumerate(steps): + step_data = { + "name": step_name, + "description": "", + "duration_seconds": None + } + resp, status = api_request("post", f"/api/routines/{routine_id}/steps", token, step_data) + if status == 201: + success_count += 1 + + if success_count == len(steps): + await message.channel.send(f"✅ Added {success_count} step(s) to **{name}**!") else: - await message.channel.send(f"Error: {resp.get('error', 'Failed to create routine')}") + await message.channel.send(f"Added {success_count} of {len(steps)} steps to **{name}**.") + + elif action == "steps": + routine_id = parsed.get("routine_id") + name = parsed.get("name") or parsed.get("routine_name") + + routine_id, name, found = await _find_routine_by_name(message, token, routine_id, name) + if not found: + return + + if not routine_id: + await message.channel.send("Which routine's steps should I show?") + return + + resp, status = api_request("get", f"/api/routines/{routine_id}/steps", token) + if status == 200: + steps = resp if isinstance(resp, list) else [] + if not steps: + await message.channel.send(f"**{name}** has no steps yet.") + else: + lines = [f"{i+1}. {s['name']}" for i, s in enumerate(steps)] + await message.channel.send(f"**Steps in {name}:**\n" + "\n".join(lines)) + else: + await message.channel.send(f"Error: {resp.get('error', 'Failed to fetch steps')}") + + elif action == "schedule": + routine_id = parsed.get("routine_id") + name = parsed.get("name") + days_of_week = parsed.get("days_of_week", []) + times = parsed.get("times", []) + + routine_id, name, found = await _find_routine_by_name(message, token, routine_id, name) + if not found: + return + + if not routine_id: + await message.channel.send("Which routine should I schedule?") + return + + if not days_of_week and not times: + await message.channel.send("When should this routine be scheduled? (e.g., 'Monday Wednesday Friday at 7am')") + return + + # Build schedule data + schedule_data = {} + if days_of_week: + schedule_data["days_of_week"] = days_of_week + if times: + schedule_data["times"] = times + + resp, status = api_request("put", f"/api/routines/{routine_id}/schedule", token, schedule_data) + if status == 200: + days_str = ', '.join(days_of_week) if days_of_week else 'daily' + times_str = ' at ' + ', '.join(times) if times else '' + await message.channel.send(f"📅 Scheduled **{name}** for {days_str}{times_str}") + else: + await message.channel.send(f"Error: {resp.get('error', 'Failed to schedule routine')}") elif action == "start": routine_id = parsed.get("routine_id") name = parsed.get("name") - if not routine_id and name: - # Look up routine by name - resp, status = api_request("get", "/api/routines", token) - if status == 200: - routines = resp if isinstance(resp, list) else [] - # Find routine by name (case-insensitive partial match) - matching = [r for r in routines if name.lower() in r['name'].lower() or r['name'].lower() in name.lower()] - if matching: - routine_id = matching[0]['id'] - name = matching[0]['name'] - else: - await message.channel.send(f"I couldn't find a routine called '{name}'. Use 'list' to see your routines.") - return - else: - await message.channel.send("Error looking up routine. Please try again.") - return - elif not routine_id: + routine_id, name, found = await _find_routine_by_name(message, token, routine_id, name) + if not found: + return + + if not routine_id: await message.channel.send("Which routine would you like to start?") return resp, status = api_request("post", f"/api/routines/{routine_id}/start", token) if status == 201: step = resp.get("current_step", {}) - await message.channel.send(f"Started! First step: **{step.get('name', 'Unknown')}**") + total_steps = resp.get("total_steps", 0) + current_step_num = resp.get("current_step_index", 0) + 1 + await message.channel.send(f"🚀 Started **{name}**!\n\nStep {current_step_num} of {total_steps}: **{step.get('name', 'Unknown')}**") elif status == 409: - await message.channel.send(f"You already have an active session: {resp.get('error', '')}") + await message.channel.send(f"⚠️ You already have an active session: {resp.get('error', '')}") else: await message.channel.send(f"Error: {resp.get('error', 'Failed to start routine')}") @@ -77,12 +212,16 @@ async def handle_routine(message, session, parsed): await message.channel.send("No active routine session.") return session_id = session_resp["session"]["id"] + current_step = session_resp.get("session", {}).get("current_step_index", 0) + 1 + total_steps = session_resp.get("total_steps", 0) + resp, status = api_request("post", f"/api/sessions/{session_id}/complete-step", token) if status == 200: if resp.get("next_step"): - await message.channel.send(f"Done! Next: **{resp['next_step'].get('name', 'Unknown')}**") + next_step_num = current_step + 1 + await message.channel.send(f"✅ Done!\n\nStep {next_step_num} of {total_steps}: **{resp['next_step'].get('name', 'Unknown')}**") else: - await message.channel.send("Completed all steps! Great job!") + await message.channel.send("🎉 Completed all steps! Great job!") else: await message.channel.send(f"Error: {resp.get('error', 'Failed to complete step')}") @@ -92,10 +231,14 @@ async def handle_routine(message, session, parsed): await message.channel.send("No active routine session.") return session_id = session_resp["session"]["id"] + current_step = session_resp.get("session", {}).get("current_step_index", 0) + 1 + total_steps = session_resp.get("total_steps", 0) + resp, status = api_request("post", f"/api/sessions/{session_id}/skip-step", token) if status == 200: if resp.get("next_step"): - await message.channel.send(f"Skipped! Next: **{resp['next_step'].get('name', 'Unknown')}**") + next_step_num = current_step + 1 + await message.channel.send(f"⏭️ Skipped!\n\nStep {next_step_num} of {total_steps}: **{resp['next_step'].get('name', 'Unknown')}**") else: await message.channel.send("All steps skipped! Routine ended.") else: @@ -109,23 +252,30 @@ async def handle_routine(message, session, parsed): session_id = session_resp["session"]["id"] resp, status = api_request("post", f"/api/sessions/{session_id}/cancel", token) if status == 200: - await message.channel.send("Routine cancelled.") + await message.channel.send("❌ Routine cancelled.") else: await message.channel.send(f"Error: {resp.get('error', 'Failed to cancel')}") elif action == "history": routine_id = parsed.get("routine_id") + name = parsed.get("name") + + routine_id, name, found = await _find_routine_by_name(message, token, routine_id, name) + if not found: + return + if not routine_id: await message.channel.send("Which routine's history?") return + resp, status = api_request("get", f"/api/routines/{routine_id}/history", token) if status == 200: sessions = resp if isinstance(resp, list) else [] if not sessions: await message.channel.send("No history yet.") else: - lines = [f"- {s.get('status', 'unknown')} on {s.get('created_at', '')}" for s in sessions[:5]] - await message.channel.send("**Recent sessions:**\n" + "\n".join(lines)) + lines = [f"- {s.get('status', 'unknown')} on {s.get('created_at', '')[:10]}" for s in sessions[:5]] + await message.channel.send(f"**Recent sessions for {name}:**\n" + "\n".join(lines)) else: await message.channel.send(f"Error: {resp.get('error', 'Failed to fetch history')}") @@ -137,7 +287,7 @@ async def handle_routine(message, session, parsed): session_id = session_resp["session"]["id"] resp, status = api_request("post", f"/api/sessions/{session_id}/pause", token) if status == 200: - await message.channel.send("Routine paused. Say 'resume' when ready to continue.") + await message.channel.send("⏸️ Routine paused. Say 'resume' when ready to continue.") else: await message.channel.send(f"Error: {resp.get('error', 'Failed to pause')}") @@ -148,7 +298,7 @@ async def handle_routine(message, session, parsed): return resp, status = api_request("post", f"/api/sessions/{session_resp['session']['id']}/resume", token) if status == 200: - await message.channel.send("Resumed! Let's keep going.") + await message.channel.send("▶️ Resumed! Let's keep going.") else: await message.channel.send(f"Error: {resp.get('error', 'Failed to resume')}") @@ -161,7 +311,7 @@ async def handle_routine(message, session, parsed): reason = parsed.get("reason", "Aborted by user") resp, status = api_request("post", f"/api/sessions/{session_id}/abort", token, {"reason": reason}) if status == 200: - await message.channel.send("Routine aborted. No worries, you can try again later!") + await message.channel.send("🛑 Routine aborted. No worries, you can try again later!") else: await message.channel.send(f"Error: {resp.get('error', 'Failed to abort')}") @@ -178,52 +328,89 @@ async def handle_routine(message, session, parsed): step_index = session_resp.get("session", {}).get("current_step_index", 0) resp, status = api_request("post", f"/api/sessions/{session_id}/note", token, {"step_index": step_index, "note": note}) if status == 201: - await message.channel.send(f"Note saved: {note}") + await message.channel.send(f"📝 Note saved: {note}") else: await message.channel.send(f"Error: {resp.get('error', 'Failed to save note')}") elif action == "stats": routine_id = parsed.get("routine_id") - if routine_id: - resp, status = api_request("get", f"/api/routines/{routine_id}/stats", token) - else: - await message.channel.send("Which routine's stats? (Please specify routine)") + name = parsed.get("name") + + # Check if user wants all stats or specific routine + if not routine_id and not name: + # Get all streaks + resp, status = api_request("get", "/api/routines/streaks", token) + if status == 200: + streaks = resp if isinstance(resp, list) else [] + if not streaks: + await message.channel.send("No routine stats yet. Complete some routines!") + else: + lines = [] + for s in streaks: + routine_name = s.get('routine_name', 'Unknown') + current = s.get('current_streak', 0) + longest = s.get('longest_streak', 0) + lines.append(f"- **{routine_name}**: {current} day streak (best: {longest})") + await message.channel.send("**Your Routine Stats:**\n" + "\n".join(lines)) + else: + await message.channel.send(f"Error: {resp.get('error', 'Failed to fetch stats')}") return + + routine_id, name, found = await _find_routine_by_name(message, token, routine_id, name) + if not found: + return + + if not routine_id: + await message.channel.send("Which routine's stats?") + return + + resp, status = api_request("get", f"/api/routines/{routine_id}/stats", token) if status == 200: + completion = resp.get('completion_rate_percent', 0) + completed = resp.get('completed', 0) + total = resp.get('total_sessions', 0) + avg_duration = resp.get('avg_duration_minutes', 0) + await message.channel.send( - f"**{resp.get('routine_name')} Stats:**\n" - f"- Completion rate: {resp.get('completion_rate_percent')}%\n" - f"- Completed: {resp.get('completed')}/{resp.get('total_sessions')}\n" - f"- Avg duration: {resp.get('avg_duration_minutes')} min" + f"**{resp.get('routine_name', name)} Stats:**\n" + f"- Completion rate: {completion}%\n" + f"- Completed: {completed}/{total} sessions\n" + f"- Avg duration: {avg_duration} min" ) else: await message.channel.send(f"Error: {resp.get('error', 'Failed to fetch stats')}") elif action == "streak": routine_id = parsed.get("routine_id") - if routine_id: - resp, status = api_request("get", f"/api/routines/{routine_id}/streak", token) - else: - resp, status = api_request("get", "/api/routines/streaks", token) - if status == 200: - streaks = resp if isinstance(resp, list) else [] - if not streaks: - await message.channel.send("No streaks yet. Complete routines to build streaks!") + name = parsed.get("name") + + if routine_id or name: + routine_id, name, found = await _find_routine_by_name(message, token, routine_id, name) + if not found: + return + if routine_id: + resp, status = api_request("get", f"/api/routines/{routine_id}/streak", token) + if status == 200: + await message.channel.send( + f"**{resp.get('routine_name', name)}** 🔥\n" + f"Current streak: {resp.get('current_streak', 0)} days\n" + f"Longest streak: {resp.get('longest_streak', 0)} days" + ) else: - lines = [f"- {s.get('routine_name')}: {s.get('current_streak')} day streak" for s in streaks] - await message.channel.send("**Your streaks:**\n" + "\n".join(lines)) - return - else: - await message.channel.send(f"Error: {resp.get('error', 'Failed to fetch streaks')}") + await message.channel.send(f"Error: {resp.get('error', 'Failed to fetch streak')}") return + + # Get all streaks + resp, status = api_request("get", "/api/routines/streaks", token) if status == 200: - await message.channel.send( - f"**{resp.get('routine_name')}**\n" - f"Current streak: {resp.get('current_streak')} days\n" - f"Longest streak: {resp.get('longest_streak')} days" - ) + streaks = resp if isinstance(resp, list) else [] + if not streaks: + await message.channel.send("No streaks yet. Complete routines to build streaks! 🔥") + else: + lines = [f"- {s.get('routine_name')}: {s.get('current_streak')} day streak 🔥" for s in streaks] + await message.channel.send("**Your Streaks:**\n" + "\n".join(lines)) else: - await message.channel.send(f"Error: {resp.get('error', 'Failed to fetch streak')}") + await message.channel.send(f"Error: {resp.get('error', 'Failed to fetch streaks')}") elif action == "templates": resp, status = api_request("get", "/api/templates", token) @@ -244,7 +431,7 @@ async def handle_routine(message, session, parsed): return resp, status = api_request("post", f"/api/templates/{template_id}/clone", token) if status == 201: - await message.channel.send(f"Cloned! Created routine: **{resp.get('name')}**") + await message.channel.send(f"✅ Cloned! Created routine: **{resp.get('name')}**") else: await message.channel.send(f"Error: {resp.get('error', 'Failed to clone template')}") @@ -267,7 +454,7 @@ async def handle_routine(message, session, parsed): return resp, status = api_request("post", f"/api/routines/{routine_id}/tags", token, {"tag_ids": [tag_id]}) if status == 200: - await message.channel.send(f"Added tag **{tag_name}** to routine!") + await message.channel.send(f"🏷️ Added tag **{tag_name}** to routine!") else: await message.channel.send(f"Error: {resp.get('error', 'Failed to add tag')}") @@ -275,6 +462,84 @@ async def handle_routine(message, session, parsed): await message.channel.send(f"Unknown action: {action}. Try: list, create, start, complete, skip, cancel, history, pause, resume, abort, note, stats, streak, templates, clone, or tag.") +async def _create_routine(message, token, name, description): + """Helper to create a routine.""" + data = {"name": name, "description": description} + resp, status = api_request("post", "/api/routines", token, data) + if status == 201: + await message.channel.send(f"✅ Created routine **{name}**!") + else: + await message.channel.send(f"Error: {resp.get('error', 'Failed to create routine')}") + + +async def _create_routine_with_steps(message, token, name, description, steps): + """Helper to create a routine with steps.""" + # First create the routine + data = {"name": name, "description": description} + resp, status = api_request("post", "/api/routines", token, data) + + if status != 201: + await message.channel.send(f"Error: {resp.get('error', 'Failed to create routine')}") + return + + routine_id = resp.get("id") + + # Add each step + success_count = 0 + for i, step_name in enumerate(steps): + step_data = { + "name": step_name, + "description": "", + "duration_seconds": None + } + step_resp, step_status = api_request("post", f"/api/routines/{routine_id}/steps", token, step_data) + if step_status == 201: + success_count += 1 + + await message.channel.send(f"✅ Created **{name}** with {success_count} step(s)!") + + +async def _find_routine_by_name(message, token, routine_id, name): + """Helper to find routine by name if ID not provided. Returns (routine_id, name, found).""" + if routine_id: + return routine_id, name, True + + if not name: + return None, None, True # Will prompt user later + + # Look up routine by name + resp, status = api_request("get", "/api/routines", token) + if status != 200: + await message.channel.send("Error looking up routine. Please try again.") + return None, None, False + + routines = resp if isinstance(resp, list) else [] + + # Try exact match first + matching = [r for r in routines if r['name'].lower() == name.lower()] + + # Then try partial match + if not matching: + matching = [r for r in routines if name.lower() in r['name'].lower() or r['name'].lower() in name.lower()] + + if not matching: + await message.channel.send(f"❌ I couldn't find a routine called '{name}'.\n\nYour routines:\n" + + "\n".join([f"- {r['name']}" for r in routines[:10]])) + return None, None, False + + if len(matching) > 1: + # Multiple matches - show list + lines = [f"{i+1}. {r['name']}" for i, r in enumerate(matching[:5])] + await message.channel.send( + f"🔍 I found multiple routines matching '{name}':\n" + + "\n".join(lines) + + "\n\nPlease specify which one (e.g., 'start 1' or say the full name)." + ) + return None, None, False + + return matching[0]['id'], matching[0]['name'], True + + def api_request(method, endpoint, token, data=None): import requests import os