feat(memory): complete M.2-M.5 memory upgrade phases with tests
- M.2: ForgettingCurve, MemoryDecay, MemoryReinforcement (selective forgetting) - M.3: DailyDigestGenerator, ReminderScheduler, ProactiveInformer (proactive reminders) - M.4: MemoryExtractor with LLM-based memory extraction from conversations - M.5: MemoryRecallInjector with token budget control for prompt injection - All phases include comprehensive unit tests (109 tests passing) - Updated checklist.md to mark all tasks complete
This commit is contained in:
113
backend/app/services/memory/reminder_scheduler.py
Normal file
113
backend/app/services/memory/reminder_scheduler.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
ReminderScheduler
|
||||
|
||||
Schedules and manages user reminders.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import select, and_
|
||||
|
||||
from app.models.reminder import Reminder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
class ReminderScheduler:
|
||||
"""Schedule and manage user reminders."""
|
||||
|
||||
async def create_reminder(
|
||||
self,
|
||||
db: "AsyncSession",
|
||||
user_id: str,
|
||||
content: str,
|
||||
trigger_at: datetime,
|
||||
trigger_type: str = "time",
|
||||
context_memory_id: str | None = None,
|
||||
) -> Reminder:
|
||||
"""Create a new reminder."""
|
||||
reminder = Reminder(
|
||||
user_id=user_id,
|
||||
content=content,
|
||||
trigger_type=trigger_type,
|
||||
trigger_at=trigger_at,
|
||||
context_memory_id=context_memory_id,
|
||||
status="pending",
|
||||
)
|
||||
db.add(reminder)
|
||||
await db.commit()
|
||||
await db.refresh(reminder)
|
||||
return reminder
|
||||
|
||||
async def get_due_reminders(self, db: "AsyncSession", user_id: str) -> list[Reminder]:
|
||||
"""Get reminders that are due (status=pending, trigger_at <= now, not snoozed)."""
|
||||
now = datetime.now(UTC)
|
||||
result = await db.execute(
|
||||
select(Reminder)
|
||||
.where(
|
||||
Reminder.user_id == user_id,
|
||||
Reminder.status == "pending",
|
||||
Reminder.trigger_at <= now,
|
||||
((Reminder.snoozed_until.is_(None)) | (Reminder.snoozed_until <= now)),
|
||||
)
|
||||
.order_by(Reminder.trigger_at.asc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def snooze(
|
||||
self,
|
||||
db: "AsyncSession",
|
||||
reminder_id: int,
|
||||
minutes: int,
|
||||
) -> Reminder | None:
|
||||
"""Snooze reminder for N minutes."""
|
||||
result = await db.execute(select(Reminder).where(Reminder.id == reminder_id))
|
||||
reminder = result.scalar_one_or_none()
|
||||
if not reminder:
|
||||
return None
|
||||
|
||||
reminder.status = "snoozed"
|
||||
reminder.snoozed_until = datetime.now(UTC) + timedelta(minutes=minutes)
|
||||
await db.commit()
|
||||
await db.refresh(reminder)
|
||||
return reminder
|
||||
|
||||
async def dismiss(self, db: "AsyncSession", reminder_id: int) -> bool:
|
||||
"""Mark reminder as dismissed."""
|
||||
result = await db.execute(select(Reminder).where(Reminder.id == reminder_id))
|
||||
reminder = result.scalar_one_or_none()
|
||||
if not reminder:
|
||||
return False
|
||||
|
||||
reminder.status = "dismissed"
|
||||
await db.commit()
|
||||
return True
|
||||
|
||||
async def mark_sent(self, db: "AsyncSession", reminder_id: int) -> bool:
|
||||
"""Mark reminder as sent."""
|
||||
result = await db.execute(select(Reminder).where(Reminder.id == reminder_id))
|
||||
reminder = result.scalar_one_or_none()
|
||||
if not reminder:
|
||||
return False
|
||||
|
||||
reminder.status = "sent"
|
||||
await db.commit()
|
||||
return True
|
||||
|
||||
async def get_pending_reminders(
|
||||
self,
|
||||
db: "AsyncSession",
|
||||
user_id: str,
|
||||
) -> list[Reminder]:
|
||||
"""Get all pending reminders for a user."""
|
||||
result = await db.execute(
|
||||
select(Reminder)
|
||||
.where(
|
||||
Reminder.user_id == user_id,
|
||||
Reminder.status.in_(["pending", "snoozed"]),
|
||||
)
|
||||
.order_by(Reminder.trigger_at.asc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
Reference in New Issue
Block a user