Files
personal-ai-assistant/backend/app/workers/health_review.py
dolgolyov.alexei 04e3ae8319 Fix Phase 5 review issues: mark_all_read, datetime, WS payload, logging
Review fixes:
- mark_all_read now filters status IN (sent, delivered), preventing
  pending notifications from being incorrectly marked as read
- schedule_notification tool: force UTC on naive datetimes, reject
  past dates (send immediately instead)
- WebSocket notification payload now includes all fields matching
  NotificationResponse (user_id, channel, status, scheduled_at, etc.)
- Centralized _serialize_notification helper in notification_sender
- notification_sender: per-notification error handling with rollback
- health_review: moved import json to module top level

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:14:08 +03:00

79 lines
2.9 KiB
Python

"""Daily job: proactive health review for all users with health data."""
import asyncio
import json
import logging
from anthropic import AsyncAnthropic
from sqlalchemy import select
from app.config import settings
from app.database import async_session_factory
from app.models.user import User
from app.services.memory_service import get_critical_memories
from app.services.notification_service import create_notification
from app.services.ws_manager import manager
logger = logging.getLogger(__name__)
client = AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
async def run_daily_health_review():
"""Review each user's health profile and generate reminder notifications."""
if not settings.ANTHROPIC_API_KEY:
return
async with async_session_factory() as db:
result = await db.execute(select(User).where(User.is_active == True)) # noqa: E712
users = result.scalars().all()
for user in users:
try:
await _review_user(user)
await asyncio.sleep(1) # Rate limit
except Exception:
logger.exception(f"Health review failed for user {user.id}")
async def _review_user(user: User):
async with async_session_factory() as db:
memories = await get_critical_memories(db, user.id)
if not memories:
return
memory_text = "\n".join(f"- [{m.category}] {m.title}: {m.content}" for m in memories)
response = await client.messages.create(
model=settings.CLAUDE_MODEL,
max_tokens=500,
system="You are a health assistant. Based on the user's health profile, suggest any upcoming checkups, medication reviews, or health actions that should be reminded. Respond with a JSON array of objects with 'title' and 'body' fields. If no reminders are needed, return an empty array [].",
messages=[{"role": "user", "content": f"User health profile:\n{memory_text}"}],
)
try:
text = response.content[0].text.strip()
# Extract JSON from response
if "[" in text:
json_str = text[text.index("["):text.rindex("]") + 1]
reminders = json.loads(json_str)
else:
return
except (json.JSONDecodeError, ValueError):
return
for reminder in reminders[:5]: # Max 5 reminders per user per day
if "title" in reminder and "body" in reminder:
notif = await create_notification(
db,
user.id,
title=reminder["title"],
body=reminder["body"],
type="ai_generated",
)
await db.commit()
from app.workers.notification_sender import _serialize_notification
await manager.send_to_user(user.id, {
"type": "new_notification",
"notification": _serialize_notification(notif),
})