""" DailyDigestGenerator Generates daily summary of user's day including conversations, tasks, key memories. Generated at 22:00 daily via scheduler. """ import json from dataclasses import dataclass, field from datetime import date, datetime, timedelta, UTC from typing import Any from sqlalchemy import select, and_ from sqlalchemy.ext.asyncio import AsyncSession from app.models.memory import MemorySummary, UserMemory from app.models.task import Task @dataclass class DailyDigestData: """Daily digest data structure.""" date: date summary: str key_points: list[dict] = field(default_factory=list) pending_questions: list[dict] = field(default_factory=list) suggestions: list[dict] = field(default_factory=list) class DailyDigestGenerator: """Generate daily summary for a user.""" MAX_KEY_POINTS = 5 MAX_SUGGESTIONS = 3 async def generate( self, db: AsyncSession, user_id: str, target_date: date | None = None, ) -> DailyDigestData: """Generate daily digest for user. 1. Get today's conversation summaries 2. Get high importance memories from today 3. Get today's tasks 4. Generate summary using LLM """ if target_date is None: target_date = datetime.now(UTC).date() start_of_day = datetime.combine(target_date, datetime.min.time()).replace(tzinfo=UTC) end_of_day = datetime.combine(target_date, datetime.max.time()).replace(tzinfo=UTC) # 1. Get conversation summaries from today summaries = await self._get_today_summaries(db, user_id, start_of_day, end_of_day) # 2. Get high importance memories from today high_importance_memories = await self._get_high_importance_memories( db, user_id, start_of_day, end_of_day ) # 3. Get today's tasks tasks = await self._get_today_tasks(db, user_id, start_of_day, end_of_day) # 4. Generate summary using LLM summary = await self._generate_summary( summaries=summaries, memories=high_importance_memories, tasks=tasks, ) # 5. Extract key points key_points = self._extract_key_points(high_importance_memories, summaries, tasks) # 6. Generate suggestions suggestions = self._generate_suggestions(high_importance_memories, tasks) return DailyDigestData( date=target_date, summary=summary, key_points=key_points, pending_questions=[], # Filled by LLM analysis suggestions=suggestions, ) async def _get_today_summaries( self, db: AsyncSession, user_id: str, start: datetime, end: datetime, ) -> list[MemorySummary]: """Get conversation summaries from today.""" result = await db.execute( select(MemorySummary) .where( MemorySummary.user_id == user_id, MemorySummary.summary_at >= start, MemorySummary.summary_at <= end, ) .order_by(MemorySummary.summary_at.desc()) ) return list(result.scalars().all()) async def _get_high_importance_memories( self, db: AsyncSession, user_id: str, start: datetime, end: datetime, ) -> list[UserMemory]: """Get high importance memories accessed or created today.""" result = await db.execute( select(UserMemory) .where( UserMemory.user_id == user_id, UserMemory.importance_level == "high", ((UserMemory.last_accessed_at >= start) | (UserMemory.last_accessed_at.is_(None))), ) .order_by(UserMemory.importance_score.desc()) .limit(5) ) return list(result.scalars().all()) async def _get_today_tasks( self, db: AsyncSession, user_id: str, start: datetime, end: datetime, ) -> list[Task]: """Get tasks updated today.""" result = await db.execute( select(Task) .where( Task.user_id == user_id, Task.updated_at >= start, Task.updated_at <= end, ) .order_by(Task.priority.desc()) ) return list(result.scalars().all()) async def _generate_summary( self, summaries: list[MemorySummary], memories: list[UserMemory], tasks: list[Task], ) -> str: """Generate daily summary text using LLM.""" from app.services.llm_service import get_llm from langchain_core.messages import HumanMessage, SystemMessage summary_texts = "\n".join(f"- {s.summary_text}" for s in summaries) memory_texts = "\n".join(f"- [{m.memory_type}] {m.content}" for m in memories[:3]) task_texts = "\n".join(f"- [{t.status}] {t.title}" for t in tasks[:5]) prompt = f"""今天的主要活动摘要: 对话摘要: {summary_texts or "无"} 高重要性记忆: {memory_texts or "无"} 任务: {task_texts or "无"} 请用1-2句话简洁总结今天的核心活动。不要过度发挥。""" try: llm = get_llm() response = await llm.invoke( [ SystemMessage( content="你是一个记忆助手。请简洁总结用户今天的核心活动,不超过50字。" ), HumanMessage(content=prompt), ] ) return response.content.strip() except Exception: # Fallback: count activities total = len(summaries) + len(memories) + len(tasks) if total == 0: return "今天没有明显的活动记录。" return f"今天共处理了 {total} 项活动({len(summaries)} 次对话、{len(memories)} 条重要记忆、{len(tasks)} 个任务)。" def _extract_key_points( self, memories: list[UserMemory], summaries: list[MemorySummary], tasks: list[Task], ) -> list[dict]: """Extract key points from today's activities.""" key_points = [] for m in memories[: self.MAX_KEY_POINTS]: key_points.append( { "content": m.content[:100], "importance": m.importance_score or 0.5, "source": "memory", } ) for t in tasks[:3]: if len(key_points) >= self.MAX_KEY_POINTS: break key_points.append( { "content": t.title, "importance": t.priority / 10.0 if t.priority else 0.5, "source": "task", } ) key_points.sort(key=lambda x: x["importance"], reverse=True) return key_points[: self.MAX_KEY_POINTS] def _generate_suggestions( self, memories: list[UserMemory], tasks: list[Task], ) -> list[dict]: """Generate suggestions based on today's activities.""" suggestions = [] # Suggest following up on high importance memories for m in memories[:2]: if len(suggestions) >= self.MAX_SUGGESTIONS: break suggestions.append( { "text": f"可以继续聊聊「{m.content[:20]}」相关的话题", "reason": "这是你关心的高优先级话题", } ) # Suggest incomplete high-priority tasks incomplete = [t for t in tasks if t.status != "done"] for t in incomplete[:2]: if len(suggestions) >= self.MAX_SUGGESTIONS: break suggestions.append( { "text": f"继续推进:{t.title}", "reason": "这是未完成的高优先级任务", } ) return suggestions[: self.MAX_SUGGESTIONS] async def get_recent_digests( self, db: AsyncSession, user_id: str, limit: int = 7, ) -> list[DailyDigestData]: """Get recent digests (stored as JSON in user metadata or separate table).""" # For simplicity, return empty list - digests are stored separately # In production, would query a daily_digests table return []