Add FastAPI backend with agent system
This commit is contained in:
165
backend/app/services/todo_service.py
Normal file
165
backend/app/services/todo_service.py
Normal file
@@ -0,0 +1,165 @@
|
||||
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 []
|
||||
Reference in New Issue
Block a user