265 lines
8.4 KiB
Python
265 lines
8.4 KiB
Python
|
|
"""
|
|||
|
|
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 []
|