feat:新增对话历史列表

This commit is contained in:
2026-10-10 02:02:17 -07:00
parent b03c46dcb9
commit 7caf0d31da
6 changed files with 114 additions and 171 deletions
+40 -1
View File
@@ -5,7 +5,17 @@ from typing import Protocol
from uuid import uuid4
from app.config import settings
from app.schemas.conversation import Turn
from app.schemas.conversation import ConversationSummary, Turn
TITLE_MAX_LENGTH = 30
def make_title(prompt: str) -> str:
"""会话标题取首轮用户输入,合并空白后截断"""
title = " ".join(prompt.split())
if len(title) <= TITLE_MAX_LENGTH:
return title
return title[:TITLE_MAX_LENGTH] + "…"
class ConversationStore(Protocol):
@@ -23,6 +33,12 @@ class ConversationStore(Protocol):
async def save_turn(self, turn: Turn) -> None: ...
async def list_by_user(
self, user_id: int, limit: int, offset: int
) -> list[ConversationSummary]:
"""该用户已产生对话的会话,按最近更新时间倒序;没有对话的空会话不返回"""
...
def _now() -> datetime:
return datetime.now(UTC)
@@ -77,3 +93,26 @@ class InMemoryConversationStore:
if existing is not None:
turn = turn.model_copy(update={"created_at": existing.created_at})
self._turns[turn.conversation_id][turn.thread_id] = turn
async def list_by_user(
self, user_id: int, limit: int, offset: int
) -> list[ConversationSummary]:
summaries = []
for conversation_id, owner in self._owner.items():
turns = sorted(
self._turns.get(conversation_id, {}).values(),
key=lambda turn: turn.created_at,
)
if owner != user_id or not turns:
continue
summaries.append(
ConversationSummary(
conversation_id=conversation_id,
title=make_title(turns[0].prompt),
updated_at=turns[-1].created_at,
)
)
summaries.sort(
key=lambda item: (item.updated_at, item.conversation_id), reverse=True
)
return summaries[offset : offset + limit]
+41 -1
View File
@@ -8,7 +8,8 @@ from pymongo import ASCENDING, DESCENDING
from pymongo.asynchronous.database import AsyncDatabase
from app.config import settings
from app.schemas.conversation import Turn
from app.schemas.conversation import ConversationSummary, Turn
from app.services.conversation import make_title
# 单次清理的空会话上限,避免积压时一次请求扫描过多
PURGE_BATCH = 200
@@ -119,3 +120,42 @@ class MongoConversationStore:
{"$set": data, "$setOnInsert": {"created_at": created_at}},
upsert=True,
)
async def list_by_user(
self, user_id: int, limit: int, offset: int
) -> list[ConversationSummary]:
# $unwind 会丢弃没有任何轮次的空会话
pipeline = [
{"$match": {"user_id": user_id}},
{
"$lookup": {
"from": self._turns.name,
"localField": "_id",
"foreignField": "conversation_id",
"pipeline": [
{"$sort": {"created_at": 1, "_id": 1}},
{
"$group": {
"_id": None,
"first_prompt": {"$first": "$prompt"},
"updated_at": {"$last": "$created_at"},
}
},
],
"as": "stats",
}
},
{"$unwind": "$stats"},
{"$sort": {"stats.updated_at": -1, "_id": -1}},
{"$skip": offset},
{"$limit": limit},
]
cursor = await self._conversations.aggregate(pipeline)
return [
ConversationSummary(
conversation_id=item["_id"],
title=make_title(item["stats"]["first_prompt"]),
updated_at=item["stats"]["updated_at"],
)
for item in await cursor.to_list()
]