Add adaptive medication timing, Discord presence tracking, and nagging system

This commit is contained in:
2026-02-16 20:00:53 -06:00
parent 1ed187b0dd
commit d4fb41ae6b
6 changed files with 907 additions and 1 deletions

185
api/routes/adaptive_meds.py Normal file
View File

@@ -0,0 +1,185 @@
"""
api/routes/adaptive_meds.py - API endpoints for adaptive medication settings
"""
import flask
import jwt
import os
import core.postgres as postgres
import core.adaptive_meds as adaptive_meds
JWT_SECRET = os.getenv("JWT_SECRET")
def _get_user_uuid(request):
"""Extract and validate user UUID from JWT token."""
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return None
token = auth_header[7:]
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
return payload.get("sub")
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
def register(app):
@app.route("/api/adaptive-meds/settings", methods=["GET"])
def get_adaptive_settings():
"""Get user's adaptive medication settings."""
user_uuid = _get_user_uuid(flask.request)
if not user_uuid:
return flask.jsonify({"error": "Unauthorized"}), 401
settings = adaptive_meds.get_adaptive_settings(user_uuid)
if not settings:
# Return defaults
return flask.jsonify(
{
"adaptive_timing_enabled": False,
"adaptive_mode": "shift_all",
"presence_tracking_enabled": False,
"nagging_enabled": True,
"nag_interval_minutes": 15,
"max_nag_count": 4,
"quiet_hours_start": None,
"quiet_hours_end": None,
}
), 200
return flask.jsonify(
{
"adaptive_timing_enabled": settings.get(
"adaptive_timing_enabled", False
),
"adaptive_mode": settings.get("adaptive_mode", "shift_all"),
"presence_tracking_enabled": settings.get(
"presence_tracking_enabled", False
),
"nagging_enabled": settings.get("nagging_enabled", True),
"nag_interval_minutes": settings.get("nag_interval_minutes", 15),
"max_nag_count": settings.get("max_nag_count", 4),
"quiet_hours_start": settings.get("quiet_hours_start"),
"quiet_hours_end": settings.get("quiet_hours_end"),
}
), 200
@app.route("/api/adaptive-meds/settings", methods=["PUT"])
def update_adaptive_settings():
"""Update user's adaptive medication settings."""
user_uuid = _get_user_uuid(flask.request)
if not user_uuid:
return flask.jsonify({"error": "Unauthorized"}), 401
data = flask.request.get_json()
if not data:
return flask.jsonify({"error": "No data provided"}), 400
# Validate required fields if enabling adaptive timing
if data.get("adaptive_timing_enabled"):
if not data.get("adaptive_mode"):
return flask.jsonify(
{"error": "adaptive_mode is required when enabling adaptive timing"}
), 400
# Build update data
update_data = {
"adaptive_timing_enabled": data.get("adaptive_timing_enabled", False),
"adaptive_mode": data.get("adaptive_mode", "shift_all"),
"presence_tracking_enabled": data.get("presence_tracking_enabled", False),
"nagging_enabled": data.get("nagging_enabled", True),
"nag_interval_minutes": data.get("nag_interval_minutes", 15),
"max_nag_count": data.get("max_nag_count", 4),
"quiet_hours_start": data.get("quiet_hours_start"),
"quiet_hours_end": data.get("quiet_hours_end"),
}
# Check if settings exist
existing = adaptive_meds.get_adaptive_settings(user_uuid)
if existing:
postgres.update(
"adaptive_med_settings", update_data, {"user_uuid": user_uuid}
)
else:
update_data["user_uuid"] = user_uuid
postgres.insert("adaptive_med_settings", update_data)
return flask.jsonify({"success": True}), 200
@app.route("/api/adaptive-meds/presence", methods=["GET"])
def get_presence_status():
"""Get user's Discord presence status."""
user_uuid = _get_user_uuid(flask.request)
if not user_uuid:
return flask.jsonify({"error": "Unauthorized"}), 401
presence = adaptive_meds.get_user_presence(user_uuid)
if not presence:
return flask.jsonify(
{"is_online": False, "last_online_at": None, "typical_wake_time": None}
), 200
typical_wake = adaptive_meds.calculate_typical_wake_time(user_uuid)
return flask.jsonify(
{
"is_online": presence.get("is_currently_online", False),
"last_online_at": presence.get("last_online_at").isoformat()
if presence.get("last_online_at")
else None,
"last_offline_at": presence.get("last_offline_at").isoformat()
if presence.get("last_offline_at")
else None,
"typical_wake_time": typical_wake.strftime("%H:%M")
if typical_wake
else None,
}
), 200
@app.route("/api/adaptive-meds/schedule", methods=["GET"])
def get_today_schedule():
"""Get today's adaptive medication schedule."""
user_uuid = _get_user_uuid(flask.request)
if not user_uuid:
return flask.jsonify({"error": "Unauthorized"}), 401
from datetime import date
today = date.today()
# Get all medications for user
meds = postgres.select("medications", {"user_uuid": user_uuid, "active": True})
schedule_data = []
for med in meds:
med_id = med.get("id")
med_schedules = postgres.select(
"medication_schedules",
{
"user_uuid": user_uuid,
"medication_id": med_id,
"adjustment_date": today,
},
)
for sched in med_schedules:
schedule_data.append(
{
"medication_id": med_id,
"medication_name": med.get("name"),
"base_time": sched.get("base_time"),
"adjusted_time": sched.get("adjusted_time"),
"adjustment_minutes": sched.get("adjustment_minutes", 0),
"status": sched.get("status", "pending"),
"nag_count": sched.get("nag_count", 0),
}
)
return flask.jsonify(schedule_data), 200

View File

@@ -577,5 +577,88 @@ async def beforeBackgroundLoop():
await client.wait_until_ready()
# ==================== Discord Presence Tracking ====================
async def update_presence_tracking():
"""Track Discord presence for users with presence tracking enabled."""
try:
import core.adaptive_meds as adaptive_meds
import core.postgres as postgres
# Get all users with presence tracking enabled
settings = postgres.select(
"adaptive_med_settings", {"presence_tracking_enabled": True}
)
for setting in settings:
user_uuid = setting.get("user_uuid")
# Get user's Discord ID from notifications table
notif_settings = postgres.select("notifications", {"user_uuid": user_uuid})
if not notif_settings:
continue
discord_user_id = notif_settings[0].get("discord_user_id")
if not discord_user_id:
continue
# Get the user from Discord
try:
discord_user = await client.fetch_user(int(discord_user_id))
if not discord_user:
continue
# Check if user is online
is_online = discord_user.status != discord.Status.offline
# Get current presence from DB
presence = adaptive_meds.get_user_presence(user_uuid)
was_online = presence.get("is_currently_online") if presence else False
# Update presence if changed
if is_online != was_online:
adaptive_meds.update_user_presence(
user_uuid, discord_user_id, is_online
)
# Record the event
from datetime import datetime
event_type = "online" if is_online else "offline"
adaptive_meds.record_presence_event(
user_uuid, event_type, datetime.utcnow()
)
print(
f"Presence update: User {user_uuid} is now {'online' if is_online else 'offline'}"
)
except Exception as e:
print(f"Error tracking presence for user {user_uuid}: {e}")
except Exception as e:
print(f"Error in presence tracking loop: {e}")
@tasks.loop(seconds=30)
async def presenceTrackingLoop():
"""Track Discord presence every 30 seconds."""
await update_presence_tracking()
@presenceTrackingLoop.before_loop
async def beforePresenceTrackingLoop():
await client.wait_until_ready()
@client.event
async def on_ready():
print(f"Bot logged in as {client.user}")
loadCache()
backgroundLoop.start()
presenceTrackingLoop.start()
if __name__ == "__main__":
client.run(DISCORD_BOT_TOKEN)

12
bot/config.json Normal file
View File

@@ -0,0 +1,12 @@
{
"openrouter_api_key": "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",
"embedding_file": "dbt_knowledge.json",
"models": {
"generator": "moonshotai/kimi-k2.5",
"jury_clinical": "z-ai/glm-5",
"jury_safety": "deepseek/deepseek-v3.2",
"jury_empathy": "openai/gpt-4o-2024-08-06",
"jury_hallucination": "qwen/qwen3-235b-a22b-2507"
},
"system_prompt": "You are a DBT assistant. Answer based ONLY on the provided context."
}

View File

@@ -200,3 +200,53 @@ CREATE TABLE IF NOT EXISTS med_logs (
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ── Adaptive Medication Settings ─────────────────────────────
CREATE TABLE IF NOT EXISTS adaptive_med_settings (
id UUID PRIMARY KEY,
user_uuid UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE,
adaptive_timing_enabled BOOLEAN DEFAULT FALSE,
adaptive_mode VARCHAR(20) DEFAULT 'shift_all',
presence_tracking_enabled BOOLEAN DEFAULT FALSE,
nagging_enabled BOOLEAN DEFAULT TRUE,
nag_interval_minutes INTEGER DEFAULT 15,
max_nag_count INTEGER DEFAULT 4,
quiet_hours_start TIME,
quiet_hours_end TIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ── User Discord Presence Tracking ────────────────────────────
CREATE TABLE IF NOT EXISTS user_presence (
id UUID PRIMARY KEY,
user_uuid UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE,
discord_user_id VARCHAR(255),
last_online_at TIMESTAMP,
last_offline_at TIMESTAMP,
is_currently_online BOOLEAN DEFAULT FALSE,
typical_wake_time TIME,
presence_history JSONB DEFAULT '[]',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ── Adaptive Medication Schedules (Daily Tracking) ───────────
CREATE TABLE IF NOT EXISTS medication_schedules (
id UUID PRIMARY KEY,
medication_id UUID REFERENCES medications(id) ON DELETE CASCADE,
user_uuid UUID REFERENCES users(id) ON DELETE CASCADE,
base_time TIME NOT NULL,
adjusted_time TIME,
adjustment_date DATE NOT NULL,
adjustment_minutes INTEGER DEFAULT 0,
nag_count INTEGER DEFAULT 0,
last_nag_at TIMESTAMP,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_med_schedules_user_date ON medication_schedules(user_uuid, adjustment_date);
CREATE INDEX IF NOT EXISTS idx_med_schedules_status ON medication_schedules(status);

358
core/adaptive_meds.py Normal file
View File

@@ -0,0 +1,358 @@
"""
core/adaptive_meds.py - Adaptive medication timing and nagging logic
This module handles:
- Discord presence tracking for wake detection
- Adaptive medication schedule calculations
- Nagging logic for missed medications
- Quiet hours enforcement
"""
import json
from datetime import datetime, timedelta, time
from typing import Optional, Dict, List, Tuple
import core.postgres as postgres
from core.tz import user_now
def get_adaptive_settings(user_uuid: str) -> Optional[Dict]:
"""Get user's adaptive medication settings."""
rows = postgres.select("adaptive_med_settings", {"user_uuid": user_uuid})
if rows:
return rows[0]
return None
def get_user_presence(user_uuid: str) -> Optional[Dict]:
"""Get user's Discord presence data."""
rows = postgres.select("user_presence", {"user_uuid": user_uuid})
if rows:
return rows[0]
return None
def update_user_presence(user_uuid: str, discord_user_id: str, is_online: bool):
"""Update user's presence status."""
now = datetime.utcnow()
presence = get_user_presence(user_uuid)
if presence:
# Update existing record
updates = {"is_currently_online": is_online, "updated_at": now}
if is_online:
updates["last_online_at"] = now
else:
updates["last_offline_at"] = now
postgres.update("user_presence", updates, {"user_uuid": user_uuid})
else:
# Create new record
data = {
"user_uuid": user_uuid,
"discord_user_id": discord_user_id,
"is_currently_online": is_online,
"last_online_at": now if is_online else None,
"last_offline_at": now if not is_online else None,
"presence_history": json.dumps([]),
"updated_at": now,
}
postgres.insert("user_presence", data)
def record_presence_event(user_uuid: str, event_type: str, timestamp: datetime):
"""Record a presence event in the history."""
presence = get_user_presence(user_uuid)
if not presence:
return
history = json.loads(presence.get("presence_history", "[]"))
# Add new event
history.append({"type": event_type, "timestamp": timestamp.isoformat()})
# Keep only last 7 days of history (up to 100 events)
history = history[-100:]
postgres.update(
"user_presence",
{"presence_history": json.dumps(history)},
{"user_uuid": user_uuid},
)
def calculate_typical_wake_time(user_uuid: str) -> Optional[time]:
"""Calculate user's typical wake time based on presence history."""
presence = get_user_presence(user_uuid)
if not presence:
return None
history = json.loads(presence.get("presence_history", "[]"))
if len(history) < 3:
return None
# Get all "online" events
wake_times = []
for event in history:
if event["type"] == "online":
ts = datetime.fromisoformat(event["timestamp"])
wake_times.append(ts.time())
if not wake_times:
return None
# Calculate average wake time (convert to minutes since midnight)
total_minutes = sum(t.hour * 60 + t.minute for t in wake_times)
avg_minutes = total_minutes // len(wake_times)
return time(avg_minutes // 60, avg_minutes % 60)
def detect_wake_event(user_uuid: str, current_time: datetime) -> Optional[datetime]:
"""Detect if user just woke up based on presence change."""
presence = get_user_presence(user_uuid)
if not presence:
return None
# Check if they just came online
if presence.get("is_currently_online"):
last_online = presence.get("last_online_at")
last_offline = presence.get("last_offline_at")
if last_online and last_offline:
offline_duration = last_online - last_offline
# If they were offline for more than 30 minutes, consider it a wake event
if offline_duration.total_seconds() > 1800: # 30 minutes
return last_online
return None
def is_quiet_hours(user_uuid: str, check_time: datetime) -> bool:
"""Check if current time is within user's quiet hours."""
settings = get_adaptive_settings(user_uuid)
if not settings:
return False
quiet_start = settings.get("quiet_hours_start")
quiet_end = settings.get("quiet_hours_end")
if not quiet_start or not quiet_end:
return False
current_time = check_time.time()
# Handle quiet hours that span midnight
if quiet_start > quiet_end:
return current_time >= quiet_start or current_time <= quiet_end
else:
return quiet_start <= current_time <= quiet_end
def calculate_adjusted_times(
user_uuid: str, base_times: List[str], wake_time: Optional[datetime] = None
) -> List[Tuple[str, int]]:
"""
Calculate adjusted medication times based on wake time.
Args:
user_uuid: User's UUID
base_times: List of base times in "HH:MM" format
wake_time: Optional wake time to use for adjustment
Returns:
List of (adjusted_time_str, offset_minutes) tuples
"""
settings = get_adaptive_settings(user_uuid)
if not settings or not settings.get("adaptive_timing_enabled"):
# Return base times with 0 offset
return [(t, 0) for t in base_times]
# Get user's timezone
prefs = postgres.select("user_preferences", {"user_uuid": user_uuid})
offset_minutes = prefs[0].get("timezone_offset", 0) if prefs else 0
# Get current time in user's timezone
user_current_time = user_now(offset_minutes)
today = user_current_time.date()
# Determine wake time
if wake_time is None:
# Try to get from presence detection
wake_time = detect_wake_event(user_uuid, user_current_time)
if wake_time is None:
# Use typical wake time if available
typical_wake = calculate_typical_wake_time(user_uuid)
if typical_wake:
wake_time = datetime.combine(today, typical_wake)
if wake_time is None:
# Default wake time (8 AM)
wake_time = datetime.combine(today, time(8, 0))
# Calculate offset from first med time
if not base_times:
return []
first_med_time = datetime.strptime(base_times[0], "%H:%M").time()
first_med_datetime = datetime.combine(today, first_med_time)
# Calculate how late they are
if wake_time.time() > first_med_time:
# They woke up after their first med time
offset_minutes = int((wake_time - first_med_datetime).total_seconds() / 60)
else:
offset_minutes = 0
# Adjust all times
adjusted = []
for base_time_str in base_times:
base_time = datetime.strptime(base_time_str, "%H:%M").time()
base_datetime = datetime.combine(today, base_time)
# Add offset
adjusted_datetime = base_datetime + timedelta(minutes=offset_minutes)
adjusted_time_str = adjusted_datetime.strftime("%H:%M")
adjusted.append((adjusted_time_str, offset_minutes))
return adjusted
def should_send_nag(
user_uuid: str, med_id: str, scheduled_time: str, current_time: datetime
) -> Tuple[bool, str]:
"""
Determine if we should send a nag notification.
Returns:
(should_nag: bool, reason: str)
"""
settings = get_adaptive_settings(user_uuid)
if not settings:
return False, "No settings"
if not settings.get("nagging_enabled"):
return False, "Nagging disabled"
# Check quiet hours
if is_quiet_hours(user_uuid, current_time):
return False, "Quiet hours"
# Check if user is online (don't nag if offline unless presence tracking disabled)
presence = get_user_presence(user_uuid)
if presence and settings.get("presence_tracking_enabled"):
if not presence.get("is_currently_online"):
return False, "User offline"
# Get today's schedule record
today = current_time.date()
schedules = postgres.select(
"medication_schedules",
{"user_uuid": user_uuid, "medication_id": med_id, "adjustment_date": today},
)
if not schedules:
return False, "No schedule found"
schedule = schedules[0]
nag_count = schedule.get("nag_count", 0)
max_nags = settings.get("max_nag_count", 4)
if nag_count >= max_nags:
return False, f"Max nags reached ({max_nags})"
# Check if it's time to nag
last_nag = schedule.get("last_nag_at")
nag_interval = settings.get("nag_interval_minutes", 15)
if last_nag:
time_since_last_nag = (current_time - last_nag).total_seconds() / 60
if time_since_last_nag < nag_interval:
return False, f"Too soon ({int(time_since_last_nag)} < {nag_interval} min)"
# Check if medication was already taken today
logs = postgres.select(
"med_logs", {"medication_id": med_id, "user_uuid": user_uuid, "action": "taken"}
)
# Filter to today's logs
today_logs = [
log
for log in logs
if log.get("created_at") and log["created_at"].date() == today
]
if today_logs:
return False, "Already taken today"
return True, "Time to nag"
def record_nag_sent(user_uuid: str, med_id: str, scheduled_time: str):
"""Record that a nag was sent."""
today = datetime.utcnow().date()
schedules = postgres.select(
"medication_schedules",
{"user_uuid": user_uuid, "medication_id": med_id, "adjustment_date": today},
)
if schedules:
schedule = schedules[0]
new_nag_count = schedule.get("nag_count", 0) + 1
postgres.update(
"medication_schedules",
{"nag_count": new_nag_count, "last_nag_at": datetime.utcnow()},
{"id": schedule["id"]},
)
def create_daily_schedule(user_uuid: str, med_id: str, base_times: List[str]):
"""Create today's medication schedule with adaptive adjustments."""
today = datetime.utcnow().date()
# Check if schedule already exists
existing = postgres.select(
"medication_schedules",
{"user_uuid": user_uuid, "medication_id": med_id, "adjustment_date": today},
)
if existing:
return
# Calculate adjusted times
adjusted_times = calculate_adjusted_times(user_uuid, base_times)
# Create schedule records for each time
for base_time, (adjusted_time, offset) in zip(base_times, adjusted_times):
data = {
"user_uuid": user_uuid,
"medication_id": med_id,
"base_time": base_time,
"adjusted_time": adjusted_time,
"adjustment_date": today,
"adjustment_minutes": offset,
"nag_count": 0,
"status": "pending",
}
postgres.insert("medication_schedules", data)
def mark_med_taken(user_uuid: str, med_id: str, scheduled_time: str):
"""Mark a medication as taken."""
today = datetime.utcnow().date()
postgres.update(
"medication_schedules",
{"status": "taken"},
{
"user_uuid": user_uuid,
"medication_id": med_id,
"adjustment_date": today,
"adjusted_time": scheduled_time,
},
)

View File

@@ -11,6 +11,7 @@ from datetime import datetime, timezone, timedelta
import core.postgres as postgres
import core.notifications as notifications
import core.adaptive_meds as adaptive_meds
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@@ -155,9 +156,226 @@ def check_refills():
logger.error(f"Error checking refills: {e}")
def create_daily_adaptive_schedules():
"""Create today's medication schedules with adaptive timing."""
try:
from datetime import date as date_type
meds = postgres.select("medications", where={"active": True})
for med in meds:
user_uuid = med.get("user_uuid")
med_id = med.get("id")
times = med.get("times", [])
if not times:
continue
# Create daily schedule with adaptive adjustments
adaptive_meds.create_daily_schedule(user_uuid, med_id, times)
except Exception as e:
logger.error(f"Error creating daily adaptive schedules: {e}")
def check_adaptive_medication_reminders():
"""Check for medications due now with adaptive timing."""
try:
from datetime import date as date_type
meds = postgres.select("medications", where={"active": True})
# Group by user
user_meds = {}
for med in meds:
uid = med.get("user_uuid")
if uid not in user_meds:
user_meds[uid] = []
user_meds[uid].append(med)
for user_uuid, user_med_list in user_meds.items():
now = _user_now_for(user_uuid)
current_time = now.strftime("%H:%M")
today = now.date()
# Check if adaptive timing is enabled
settings = adaptive_meds.get_adaptive_settings(user_uuid)
adaptive_enabled = settings and settings.get("adaptive_timing_enabled")
for med in user_med_list:
freq = med.get("frequency", "daily")
if freq == "as_needed":
continue
# Day-of-week check
if freq == "specific_days":
current_day = now.strftime("%a").lower()
med_days = med.get("days_of_week", [])
if current_day not in med_days:
continue
# Interval check
if freq == "every_n_days":
start = med.get("start_date")
interval = med.get("interval_days")
if start and interval:
start_d = (
start
if isinstance(start, date_type)
else datetime.strptime(str(start), "%Y-%m-%d").date()
)
if (today - start_d).days < 0 or (
today - start_d
).days % interval != 0:
continue
else:
continue
# Get today's schedule
schedules = postgres.select(
"medication_schedules",
where={
"user_uuid": user_uuid,
"medication_id": med["id"],
"adjustment_date": today,
"status": "pending",
},
)
for sched in schedules:
# Check if it's time to take this med
if adaptive_enabled:
# Use adjusted time
check_time = sched.get("adjusted_time")
else:
# Use base time
check_time = sched.get("base_time")
if check_time != current_time:
continue
# Check if already taken
logs = postgres.select(
"med_logs",
where={
"medication_id": med["id"],
"user_uuid": user_uuid,
"action": "taken",
},
)
already_taken = any(
str(log.get("created_at", ""))[:10] == today.isoformat()
for log in logs
)
if already_taken:
continue
# Send notification
user_settings = notifications.getNotificationSettings(user_uuid)
if user_settings:
offset = sched.get("adjustment_minutes", 0)
if offset > 0:
msg = f"⏰ Time to take {med['name']} ({med['dosage']} {med['unit']}) · {check_time} (adjusted +{offset}min)"
else:
msg = f"⏰ Time to take {med['name']} ({med['dosage']} {med['unit']}) · {check_time}"
notifications._sendToEnabledChannels(
user_settings, msg, user_uuid=user_uuid
)
except Exception as e:
logger.error(f"Error checking adaptive medication reminders: {e}")
def check_nagging():
"""Check for missed medications and send nag notifications."""
try:
from datetime import date as date_type
# Get all active medications
meds = postgres.select("medications", where={"active": True})
for med in meds:
user_uuid = med.get("user_uuid")
med_id = med.get("id")
# Get user's settings
settings = adaptive_meds.get_adaptive_settings(user_uuid)
if not settings or not settings.get("nagging_enabled"):
continue
now = datetime.utcnow()
today = now.date()
# Get today's schedules
schedules = postgres.select(
"medication_schedules",
where={
"user_uuid": user_uuid,
"medication_id": med_id,
"adjustment_date": today,
"status": "pending",
},
)
for sched in schedules:
# Check if we should nag
should_nag, reason = adaptive_meds.should_send_nag(
user_uuid, med_id, sched.get("adjusted_time"), now
)
if not should_nag:
continue
# Get the time to display
adaptive_enabled = settings.get("adaptive_timing_enabled")
if adaptive_enabled:
display_time = sched.get("adjusted_time")
else:
display_time = sched.get("base_time")
# Send nag notification
user_settings = notifications.getNotificationSettings(user_uuid)
if user_settings:
nag_count = sched.get("nag_count", 0) + 1
max_nags = settings.get("max_nag_count", 4)
msg = f"🔔 {med['name']} reminder {nag_count}/{max_nags}: You missed your {display_time} dose. Please take it now!"
notifications._sendToEnabledChannels(
user_settings, msg, user_uuid=user_uuid
)
# Record that we sent a nag
adaptive_meds.record_nag_sent(
user_uuid, med_id, sched.get("adjusted_time")
)
logger.info(
f"Sent nag {nag_count}/{max_nags} for {med['name']} to user {user_uuid}"
)
except Exception as e:
logger.error(f"Error checking nags: {e}")
def poll_callback():
"""Called every POLL_INTERVAL seconds."""
check_medication_reminders()
# Create daily schedules at midnight
now = datetime.utcnow()
if now.hour == 0 and now.minute < POLL_INTERVAL / 60:
create_daily_adaptive_schedules()
# Check reminders with adaptive timing
check_adaptive_medication_reminders()
# Check for nags
check_nagging()
# Original checks
check_routine_reminders()
check_refills()