166 lines
5.3 KiB
Python
166 lines
5.3 KiB
Python
import json
|
||
import logging
|
||
from datetime import date, datetime, timedelta
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select
|
||
from app.models.todo import DailyTodo, TodoSource
|
||
from app.models.task import Task, TaskStatus
|
||
from app.models.conversation import Conversation, Message
|
||
from app.services.llm_service import get_llm
|
||
from langchain_core.messages import HumanMessage, SystemMessage
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
async def generate_daily_todos(user_id: str, db: AsyncSession) -> list[DailyTodo]:
|
||
"""
|
||
为用户生成今日待办:
|
||
1. 来自前一天未完成的看板任务(最多20条)
|
||
2. 来自前一天对话记录分析(最多3条)
|
||
"""
|
||
today = date.today()
|
||
yesterday = (today - timedelta(days=1)).isoformat()
|
||
|
||
todos: list[DailyTodo] = []
|
||
|
||
# 1. 从看板任务导入
|
||
kanban_todos = await _import_kanban_tasks(user_id, yesterday, db)
|
||
todos.extend(kanban_todos)
|
||
|
||
# 2. 从对话记录分析
|
||
chat_todos = await _analyze_chat_history(user_id, yesterday, db)
|
||
todos.extend(chat_todos)
|
||
|
||
return todos
|
||
|
||
|
||
async def _import_kanban_tasks(user_id: str, date_str: str, db: AsyncSession) -> list[DailyTodo]:
|
||
"""导入前一天创建的、未完成的看板任务"""
|
||
q = select(Task).where(
|
||
Task.user_id == user_id,
|
||
Task.status != TaskStatus.DONE,
|
||
).order_by(Task.created_at.desc()).limit(20)
|
||
|
||
tasks = (await db.execute(q)).scalars().all()
|
||
todos = []
|
||
|
||
for task in tasks:
|
||
todo = DailyTodo(
|
||
user_id=user_id,
|
||
title=task.title,
|
||
source=TodoSource.AI_KANBAN,
|
||
source_detail=f"看板:{task.title}",
|
||
source_ref_id=task.id,
|
||
todo_date=date.today().isoformat(),
|
||
)
|
||
db.add(todo)
|
||
todos.append(todo)
|
||
|
||
if todos:
|
||
await db.commit()
|
||
for todo in todos:
|
||
await db.refresh(todo)
|
||
|
||
return todos
|
||
|
||
|
||
async def _analyze_chat_history(user_id: str, date_str: str, db: AsyncSession) -> list[DailyTodo]:
|
||
"""分析前一天对话,提取待办事项"""
|
||
try:
|
||
# 查询前一天创建的对话
|
||
conv_q = select(Conversation).where(
|
||
Conversation.user_id == user_id,
|
||
).order_by(Conversation.created_at.desc()).limit(10)
|
||
convs = (await db.execute(conv_q)).scalars().all()
|
||
|
||
# 过滤出昨天的对话
|
||
yesterday_convs = []
|
||
for conv in convs:
|
||
created = conv.created_at
|
||
if hasattr(created, 'date'):
|
||
created_date = created.date() if hasattr(created, 'date') else created
|
||
else:
|
||
created_date = datetime.fromisoformat(str(created)).date()
|
||
|
||
if str(created_date) == date_str or (created + timedelta(hours=8)).strftime('%Y-%m-%d') == date_str:
|
||
yesterday_convs.append(conv)
|
||
|
||
if not yesterday_convs:
|
||
return []
|
||
|
||
# 收集消息内容(限制2000字)
|
||
messages_content = []
|
||
for conv in yesterday_convs:
|
||
msg_q = select(Message).where(
|
||
Message.conversation_id == conv.id
|
||
).order_by(Message.created_at.asc()).limit(50)
|
||
msgs = (await db.execute(msg_q)).scalars().all()
|
||
for msg in msgs:
|
||
if msg.content:
|
||
messages_content.append(f"[{msg.role}]: {msg.content[:500]}")
|
||
|
||
if not messages_content:
|
||
return []
|
||
|
||
full_text = "\n".join(messages_content)[:2000]
|
||
|
||
# 调用 LLM 分析
|
||
prompt = f"""你是一个任务规划助手。请分析以下对话记录,提取其中用户想要完成但尚未明确完成的事项。
|
||
|
||
要求:
|
||
- 最多提取 3 条
|
||
- 每条格式:{{"title": "事项描述(50字以内)", "reason": "来源说明(60字以内)"}}
|
||
- 只提取用户明确表达过需求但还未完成的事项
|
||
- 如果没有可提取的内容,返回空数组 []
|
||
|
||
对话记录:
|
||
{full_text}
|
||
|
||
返回 JSON 数组:"""
|
||
|
||
llm = get_llm()
|
||
response = await llm.invoke([
|
||
SystemMessage(content="你是一个任务规划助手。"),
|
||
HumanMessage(content=prompt),
|
||
])
|
||
content = response.content if hasattr(response, 'content') else str(response)
|
||
|
||
# 尝试解析 JSON
|
||
try:
|
||
# 提取 JSON 数组
|
||
start = content.find('[')
|
||
end = content.rfind(']') + 1
|
||
if start != -1 and end > start:
|
||
items = json.loads(content[start:end])
|
||
else:
|
||
items = []
|
||
except (json.JSONDecodeError, ValueError):
|
||
logger.warning(f"LLM 返回格式异常,跳过对话分析: {content[:200]}")
|
||
items = []
|
||
|
||
if not items:
|
||
return []
|
||
|
||
todos = []
|
||
for item in items[:3]:
|
||
todo = DailyTodo(
|
||
user_id=user_id,
|
||
title=item.get("title", "")[:500],
|
||
source=TodoSource.AI_CHAT,
|
||
source_detail=f"对话:{item.get('reason', '')[:60]}",
|
||
todo_date=date.today().isoformat(),
|
||
)
|
||
db.add(todo)
|
||
todos.append(todo)
|
||
|
||
if todos:
|
||
await db.commit()
|
||
for todo in todos:
|
||
await db.refresh(todo)
|
||
|
||
return todos
|
||
|
||
except Exception as e:
|
||
logger.error(f"对话分析失败: {e}")
|
||
return []
|