Files
JARVIS/backend/app/services/todo_service.py

166 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 []