Fix presence tracking and med reminder bugs
This commit is contained in:
78
bot/bot.py
78
bot/bot.py
@@ -128,6 +128,7 @@ class JurySystem:
|
|||||||
async def retrieve(self, query_text, top_k=5):
|
async def retrieve(self, query_text, top_k=5):
|
||||||
"""Async retrieval — returns list of {metadata, score} dicts."""
|
"""Async retrieval — returns list of {metadata, score} dicts."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
return await asyncio.to_thread(self._retrieve_sync, query_text, top_k)
|
return await asyncio.to_thread(self._retrieve_sync, query_text, top_k)
|
||||||
|
|
||||||
async def query(self, query_text):
|
async def query(self, query_text):
|
||||||
@@ -147,6 +148,7 @@ If the answer is not in the context, say you don't know based on the provided te
|
|||||||
Be concise, compassionate, and practical."""
|
Be concise, compassionate, and practical."""
|
||||||
|
|
||||||
from ai.jury_council import generate_rag_answer
|
from ai.jury_council import generate_rag_answer
|
||||||
|
|
||||||
return await generate_rag_answer(query_text, context_text, system_prompt)
|
return await generate_rag_answer(query_text, context_text, system_prompt)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error querying DBT knowledge base: {e}"
|
return f"Error querying DBT knowledge base: {e}"
|
||||||
@@ -181,7 +183,9 @@ def apiRequest(method, endpoint, token=None, data=None, _retried=False):
|
|||||||
if resp.status_code == 401 and not _retried:
|
if resp.status_code == 401 and not _retried:
|
||||||
new_token = _try_refresh_token_for_session(token)
|
new_token = _try_refresh_token_for_session(token)
|
||||||
if new_token:
|
if new_token:
|
||||||
return apiRequest(method, endpoint, token=new_token, data=data, _retried=True)
|
return apiRequest(
|
||||||
|
method, endpoint, token=new_token, data=data, _retried=True
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
return resp.json(), resp.status_code
|
return resp.json(), resp.status_code
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -201,9 +205,12 @@ def _try_refresh_token_for_session(expired_token):
|
|||||||
if cached:
|
if cached:
|
||||||
refresh_token = cached.get("refresh_token")
|
refresh_token = cached.get("refresh_token")
|
||||||
if refresh_token:
|
if refresh_token:
|
||||||
result, status = apiRequest("post", "/api/refresh",
|
result, status = apiRequest(
|
||||||
data={"refresh_token": refresh_token},
|
"post",
|
||||||
_retried=True)
|
"/api/refresh",
|
||||||
|
data={"refresh_token": refresh_token},
|
||||||
|
_retried=True,
|
||||||
|
)
|
||||||
if status == 200 and "token" in result:
|
if status == 200 and "token" in result:
|
||||||
new_token = result["token"]
|
new_token = result["token"]
|
||||||
session["token"] = new_token
|
session["token"] = new_token
|
||||||
@@ -258,7 +265,8 @@ def negotiateToken(discord_id, username, password):
|
|||||||
# Try refresh token first (avoids sending password)
|
# Try refresh token first (avoids sending password)
|
||||||
if cached and cached.get("refresh_token"):
|
if cached and cached.get("refresh_token"):
|
||||||
result, status = apiRequest(
|
result, status = apiRequest(
|
||||||
"post", "/api/refresh",
|
"post",
|
||||||
|
"/api/refresh",
|
||||||
data={"refresh_token": cached["refresh_token"]},
|
data={"refresh_token": cached["refresh_token"]},
|
||||||
_retried=True,
|
_retried=True,
|
||||||
)
|
)
|
||||||
@@ -279,7 +287,9 @@ def negotiateToken(discord_id, username, password):
|
|||||||
and cached.get("hashed_password")
|
and cached.get("hashed_password")
|
||||||
and verifyPassword(password, cached.get("hashed_password"))
|
and verifyPassword(password, cached.get("hashed_password"))
|
||||||
):
|
):
|
||||||
result, status = apiRequest("post", "/api/login", data=login_data, _retried=True)
|
result, status = apiRequest(
|
||||||
|
"post", "/api/login", data=login_data, _retried=True
|
||||||
|
)
|
||||||
if status == 200 and "token" in result:
|
if status == 200 and "token" in result:
|
||||||
token = result["token"]
|
token = result["token"]
|
||||||
payload = decodeJwtPayload(token)
|
payload = decodeJwtPayload(token)
|
||||||
@@ -530,7 +540,9 @@ async def handleDBTQuery(message):
|
|||||||
if not jury_result.safe_questions:
|
if not jury_result.safe_questions:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
await message.channel.send("🔍 Searching knowledge base with approved questions...")
|
await message.channel.send(
|
||||||
|
"🔍 Searching knowledge base with approved questions..."
|
||||||
|
)
|
||||||
|
|
||||||
# Step 3: Multi-query retrieval — deduplicated by chunk ID
|
# Step 3: Multi-query retrieval — deduplicated by chunk ID
|
||||||
seen_ids = set()
|
seen_ids = set()
|
||||||
@@ -544,7 +556,9 @@ async def handleDBTQuery(message):
|
|||||||
context_chunks.append(r["metadata"]["text"])
|
context_chunks.append(r["metadata"]["text"])
|
||||||
|
|
||||||
if not context_chunks:
|
if not context_chunks:
|
||||||
await message.channel.send("⚠️ No relevant content found in the knowledge base.")
|
await message.channel.send(
|
||||||
|
"⚠️ No relevant content found in the knowledge base."
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
context = "\n\n---\n\n".join(context_chunks)
|
context = "\n\n---\n\n".join(context_chunks)
|
||||||
@@ -644,7 +658,8 @@ def _restore_sessions_from_cache():
|
|||||||
if not refresh_token:
|
if not refresh_token:
|
||||||
continue
|
continue
|
||||||
result, status = apiRequest(
|
result, status = apiRequest(
|
||||||
"post", "/api/refresh",
|
"post",
|
||||||
|
"/api/refresh",
|
||||||
data={"refresh_token": refresh_token},
|
data={"refresh_token": refresh_token},
|
||||||
_retried=True,
|
_retried=True,
|
||||||
)
|
)
|
||||||
@@ -705,15 +720,20 @@ async def update_presence_tracking():
|
|||||||
import core.adaptive_meds as adaptive_meds
|
import core.adaptive_meds as adaptive_meds
|
||||||
import core.postgres as postgres
|
import core.postgres as postgres
|
||||||
|
|
||||||
print(f"[DEBUG] Running presence tracking. Guilds: {len(client.guilds)}", flush=True)
|
print(
|
||||||
|
f"[DEBUG] Running presence tracking. Guilds: {len(client.guilds)}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
for guild in client.guilds:
|
for guild in client.guilds:
|
||||||
print(f"[DEBUG] Guild: {guild.name} ({guild.id}) - Members: {guild.member_count}")
|
print(
|
||||||
|
f"[DEBUG] Guild: {guild.name} ({guild.id}) - Members: {guild.member_count}"
|
||||||
|
)
|
||||||
|
|
||||||
# Get all users with presence tracking enabled
|
# Get all users with presence tracking enabled
|
||||||
settings = postgres.select(
|
settings = postgres.select(
|
||||||
"adaptive_med_settings", {"presence_tracking_enabled": True}
|
"adaptive_med_settings", {"presence_tracking_enabled": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"[DEBUG] Found {len(settings)} users with presence tracking enabled")
|
print(f"[DEBUG] Found {len(settings)} users with presence tracking enabled")
|
||||||
|
|
||||||
for setting in settings:
|
for setting in settings:
|
||||||
@@ -733,27 +753,46 @@ async def update_presence_tracking():
|
|||||||
# Get the member from a shared guild (needed for presence data)
|
# Get the member from a shared guild (needed for presence data)
|
||||||
try:
|
try:
|
||||||
member = None
|
member = None
|
||||||
target_id = int(discord_user_id)
|
try:
|
||||||
|
target_id = int(discord_user_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
print(
|
||||||
|
f"[DEBUG] Invalid Discord ID for user {user_uuid}: {discord_user_id}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
# Search through all guilds the bot is in
|
# Search through all guilds the bot is in
|
||||||
for guild in client.guilds:
|
for guild in client.guilds:
|
||||||
member = guild.get_member(target_id)
|
member = guild.get_member(target_id)
|
||||||
print(f"[DEBUG] Checked guild {guild.name}, member: {member}", flush=True)
|
print(
|
||||||
|
f"[DEBUG] Checked guild {guild.name}, member: {member}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
if member:
|
if member:
|
||||||
break
|
break
|
||||||
|
|
||||||
if not member:
|
if not member:
|
||||||
print(f"[DEBUG] User {discord_user_id} not found in any shared guild", flush=True)
|
print(
|
||||||
|
f"[DEBUG] User {discord_user_id} not found in any shared guild",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if user is online
|
# Check if user is online
|
||||||
is_online = member.status != discord.Status.offline
|
is_online = member.status != discord.Status.offline
|
||||||
print(f"[DEBUG] User status: {member.status}, is_online: {is_online}", flush=True)
|
print(
|
||||||
|
f"[DEBUG] User status: {member.status}, is_online: {is_online}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
# Get current presence from DB
|
# Get current presence from DB
|
||||||
presence = adaptive_meds.get_user_presence(user_uuid)
|
presence = adaptive_meds.get_user_presence(user_uuid)
|
||||||
was_online = presence.get("is_currently_online") if presence else False
|
was_online = presence.get("is_currently_online") if presence else False
|
||||||
print(f"[DEBUG] Previous state: {was_online}, Current: {is_online}", flush=True)
|
print(
|
||||||
|
f"[DEBUG] Previous state: {was_online}, Current: {is_online}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
# Update presence if changed
|
# Update presence if changed
|
||||||
if is_online != was_online:
|
if is_online != was_online:
|
||||||
@@ -788,6 +827,7 @@ async def presenceTrackingLoop():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] presenceTrackingLoop failed: {e}", flush=True)
|
print(f"[ERROR] presenceTrackingLoop failed: {e}", flush=True)
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ def should_send_nag(
|
|||||||
return False, "User offline"
|
return False, "User offline"
|
||||||
|
|
||||||
# Get today's schedule record for this specific time slot
|
# Get today's schedule record for this specific time slot
|
||||||
today = current_time.date()
|
today = user_today_for(user_uuid)
|
||||||
query = {"user_uuid": user_uuid, "medication_id": med_id, "adjustment_date": today}
|
query = {"user_uuid": user_uuid, "medication_id": med_id, "adjustment_date": today}
|
||||||
if scheduled_time is not None:
|
if scheduled_time is not None:
|
||||||
query["adjusted_time"] = scheduled_time
|
query["adjusted_time"] = scheduled_time
|
||||||
@@ -304,19 +304,56 @@ def should_send_nag(
|
|||||||
"medication_id": med_id,
|
"medication_id": med_id,
|
||||||
"user_uuid": user_uuid,
|
"user_uuid": user_uuid,
|
||||||
"action": "taken",
|
"action": "taken",
|
||||||
"scheduled_time": scheduled_time,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Filter to today's logs for this time slot
|
# Get medication times to calculate dose interval for proximity check
|
||||||
today_logs = [
|
med = postgres.select_one("medications", {"id": med_id})
|
||||||
log
|
dose_interval_minutes = 60 # default fallback
|
||||||
for log in logs
|
if med and med.get("times"):
|
||||||
if log.get("created_at") and log["created_at"].date() == today
|
times = med["times"]
|
||||||
]
|
if len(times) >= 2:
|
||||||
|
time_minutes = []
|
||||||
|
for t in times:
|
||||||
|
t = _normalize_time(t)
|
||||||
|
if t:
|
||||||
|
h, m = int(t[:2]), int(t[3:5])
|
||||||
|
time_minutes.append(h * 60 + m)
|
||||||
|
time_minutes.sort()
|
||||||
|
intervals = []
|
||||||
|
for i in range(1, len(time_minutes)):
|
||||||
|
intervals.append(time_minutes[i] - time_minutes[i - 1])
|
||||||
|
if intervals:
|
||||||
|
dose_interval_minutes = min(intervals)
|
||||||
|
|
||||||
if today_logs:
|
proximity_window = max(30, dose_interval_minutes // 2)
|
||||||
return False, "Already taken today"
|
|
||||||
|
# Filter to today's logs and check for this specific dose
|
||||||
|
for log in logs:
|
||||||
|
created_at = log.get("created_at")
|
||||||
|
if not created_at:
|
||||||
|
continue
|
||||||
|
if created_at.date() != today:
|
||||||
|
continue
|
||||||
|
|
||||||
|
log_scheduled_time = log.get("scheduled_time")
|
||||||
|
if log_scheduled_time:
|
||||||
|
log_scheduled_time = _normalize_time(log_scheduled_time)
|
||||||
|
if log_scheduled_time == scheduled_time:
|
||||||
|
return False, "Already taken today"
|
||||||
|
else:
|
||||||
|
if scheduled_time and created_at:
|
||||||
|
log_hour = created_at.hour
|
||||||
|
log_min = created_at.minute
|
||||||
|
sched_hour, sched_min = (
|
||||||
|
int(scheduled_time[:2]),
|
||||||
|
int(scheduled_time[3:5]),
|
||||||
|
)
|
||||||
|
diff_minutes = abs(
|
||||||
|
(log_hour * 60 + log_min) - (sched_hour * 60 + sched_min)
|
||||||
|
)
|
||||||
|
if diff_minutes <= proximity_window:
|
||||||
|
return False, "Already taken today"
|
||||||
|
|
||||||
return True, "Time to nag"
|
return True, "Time to nag"
|
||||||
|
|
||||||
|
|||||||
@@ -396,14 +396,24 @@ export default function SettingsPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{notif.discord_enabled && (
|
{notif.discord_enabled && (
|
||||||
<input
|
<div className="space-y-1">
|
||||||
type="text"
|
<input
|
||||||
placeholder="Your Discord user ID"
|
type="text"
|
||||||
value={notif.discord_user_id}
|
placeholder="Your Discord user ID (numbers only)"
|
||||||
onChange={(e) => setNotif({ ...notif, discord_user_id: e.target.value })}
|
value={notif.discord_user_id}
|
||||||
onBlur={() => updateNotif({ discord_user_id: notif.discord_user_id })}
|
onChange={(e) => {
|
||||||
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 bg-white dark:bg-gray-700"
|
const val = e.target.value;
|
||||||
/>
|
if (val === '' || /^\d+$/.test(val)) {
|
||||||
|
setNotif({ ...notif, discord_user_id: val });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => updateNotif({ discord_user_id: notif.discord_user_id })}
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 bg-white dark:bg-gray-700"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Enable Developer Mode in Discord, right-click your profile, and copy User ID
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -831,7 +841,7 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
value={newContact.contact_type}
|
value={newContact.contact_type}
|
||||||
onChange={(e) => setNewContact({ ...newContact, contact_type: e.target.value })}
|
onChange={(e) => setNewContact({ ...newContact, contact_type: e.target.value, contact_value: '' })}
|
||||||
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800"
|
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800"
|
||||||
>
|
>
|
||||||
<option value="discord">Discord</option>
|
<option value="discord">Discord</option>
|
||||||
@@ -840,9 +850,18 @@ export default function SettingsPage() {
|
|||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={newContact.contact_type === 'discord' ? 'Discord User ID' : newContact.contact_type === 'email' ? 'Email address' : 'Phone number'}
|
placeholder={newContact.contact_type === 'discord' ? 'Discord User ID (numbers only)' : newContact.contact_type === 'email' ? 'Email address' : 'Phone number'}
|
||||||
value={newContact.contact_value}
|
value={newContact.contact_value}
|
||||||
onChange={(e) => setNewContact({ ...newContact, contact_value: e.target.value })}
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (newContact.contact_type === 'discord') {
|
||||||
|
if (val === '' || /^\d+$/.test(val)) {
|
||||||
|
setNewContact({ ...newContact, contact_value: val });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setNewContact({ ...newContact, contact_value: val });
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800"
|
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
Reference in New Issue
Block a user