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