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