Phase 5: Notifications — WebSocket, APScheduler, AI tool, health review
Backend: - Notification model + Alembic migration - Notification service: CRUD, mark read, unread count, pending scheduled - WebSocket manager singleton for real-time push - WebSocket endpoint /ws/notifications with JWT auth via query param - APScheduler integration: periodic notification sender (every 60s), daily proactive health review job (8 AM) - AI tool: schedule_notification (immediate or scheduled) - Health review worker: analyzes user memory via Claude, creates ai_generated notifications with WebSocket push Frontend: - Notification API client + Zustand store - WebSocket hook with auto-reconnect (exponential backoff) - Notification bell in header with unread count badge + dropdown - Notifications page with type badges, mark read, mark all read - WebSocket initialized in AppLayout for app-wide real-time updates - Enabled notifications nav in sidebar - English + Russian translations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
83
backend/app/workers/health_review.py
Normal file
83
backend/app/workers/health_review.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Daily job: proactive health review for all users with health data."""
|
||||
import asyncio
|
||||
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}"}],
|
||||
)
|
||||
|
||||
import json
|
||||
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()
|
||||
|
||||
await manager.send_to_user(user.id, {
|
||||
"type": "new_notification",
|
||||
"notification": {
|
||||
"id": str(notif.id),
|
||||
"title": notif.title,
|
||||
"body": notif.body,
|
||||
"type": notif.type,
|
||||
"created_at": notif.created_at.isoformat(),
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user