Files
JARVIS/backend/app/services/memory/daily_digest.py

265 lines
8.4 KiB
Python
Raw Normal View History

"""
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 []