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
+21 -2
View File
@@ -1,11 +1,17 @@
"""会话创建与历史查询接口"""
from fastapi import APIRouter, Depends, Request
from typing import Annotated
from fastapi import APIRouter, Depends, Query, Request
from app.common.response import ApiResponse, success_response
from app.core.auth import get_current_user
from app.core.exceptions import BizException
from app.schemas.conversation import ConversationCreated, ConversationDetail
from app.schemas.conversation import (
ConversationCreated,
ConversationDetail,
ConversationList,
)
from app.schemas.user import CurrentUser
router = APIRouter(prefix="/conversations", tags=["Conversation"])
@@ -21,6 +27,19 @@ async def create_conversation(
)
@router.get("")
async def list_conversations(
request: Request,
user: Annotated[CurrentUser, Depends(get_current_user)],
limit: Annotated[int, Query(ge=1, le=100)] = 20,
offset: Annotated[int, Query(ge=0)] = 0,
) -> ApiResponse[ConversationList]:
store = request.app.state.conversation_store
return success_response(
ConversationList(conversations=await store.list_by_user(user.id, limit, offset))
)
@router.get("/{conversation_id}")
async def get_conversation(
conversation_id: str,
+12
View File
@@ -25,3 +25,15 @@ class ConversationCreated(BaseModel):
class ConversationDetail(BaseModel):
conversation_id: str
turns: list[Turn]
class ConversationSummary(BaseModel):
"""历史会话列表项,只包含已产生对话的会话"""
conversation_id: str
title: str # 取自首轮用户输入,过长时截断
updated_at: datetime # 最近一轮对话的创建时间
class ConversationList(BaseModel):
conversations: list[ConversationSummary]
+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()
]
-1
View File
@@ -18,7 +18,6 @@ dependencies = [
"httpx>=0.28.1",
"mcp>=1.28.1",
"langchain-mcp-adapters>=0.3.0",
"deepagents>=0.7.21",
"langchain-core>=1.6.6",
"python-calamine>=0.8.2",
"pyarrow>=25.0.1",
Generated
-166
View File
@@ -38,24 +38,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
]
[[package]]
name = "anthropic"
version = "1.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "docstring-parser" },
{ name = "httpx2" },
{ name = "jiter" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ad/12/9a6ffa397b172adb040008d934a1dfd85d0e4cefc77489416db294ffc880/anthropic-1.11.0.tar.gz", hash = "sha256:3906fabac7ad7b5b46c6186040398fc7826885c77ce34e4dd7849de16fc8d0f8", size = 1390390, upload-time = "2026-09-30T22:58:21.098Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/6f/20edcd2956cac68042b1bcdd13ab3311447519f86b26929ff268dc74c650/anthropic-1.11.0-py3-none-any.whl", hash = "sha256:52f97b2c485cca7ac66058374f5073a3febb7e6849b16989c145d602a3efee21", size = 1681654, upload-time = "2026-09-30T22:58:22.925Z" },
]
[[package]]
name = "anyio"
version = "4.15.1"
@@ -87,15 +69,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
]
[[package]]
name = "bracex"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ac/01/5f394b8bcd6e5b92f73130990960423bbb19711f906bd9fe9ea5557c667c/bracex-3.0.1.tar.gz", hash = "sha256:4e38e32392e4a4780fe15d644bfc7c8514057cfc3861e060b11814ce829c25e4", size = 44019, upload-time = "2026-07-20T13:43:00.335Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/8f/6f7273a7adb8d73fc8d21ede4376a3e475e52f98435c6007f69100dec8ca/bracex-3.0.1-py3-none-any.whl", hash = "sha256:6523ad83aeb5098a4ee597cff0f964442ff74e460bd3fafaffab6a013ff2288c", size = 11940, upload-time = "2026-07-20T13:42:59.268Z" },
]
[[package]]
name = "certifi"
version = "2026.7.22"
@@ -437,24 +410,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/1d/1271f287ff7170ddafc2aad36260c4eec20ccd2fea70f38455e9d56d427b/cryptography-50.0.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:7b75de3c8b3be1cdb1052747c929440c3eea46c1bc2cb8a6e3a48388e9b7b452", size = 3805376, upload-time = "2026-09-30T15:29:58.729Z" },
]
[[package]]
name = "deepagents"
version = "0.7.21"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain" },
{ name = "langchain-anthropic" },
{ name = "langchain-core" },
{ name = "langchain-google-genai" },
{ name = "langsmith" },
{ name = "packaging" },
{ name = "wcmatch" },
]
sdist = { url = "https://files.pythonhosted.org/packages/59/6a/1dcd74589ece546a16280c33646c45439666aa40755f6e6c7bbb13ca86db/deepagents-0.7.21.tar.gz", hash = "sha256:49a594e9727c82e2354ed33bb4e656f94ec30715d1803de886792e8a6e2854e6", size = 313698, upload-time = "2026-09-30T19:28:11.383Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/8d/5ad6f7271abe3d3b584c05661bc2fb0e75be7847581659e5de83c44f4592/deepagents-0.7.21-py3-none-any.whl", hash = "sha256:c8e6a89c75c62c232f6197a21af5f1298fda7eb7f1bfbc321bbabf1fa70eefed", size = 341937, upload-time = "2026-09-30T19:28:10.09Z" },
]
[[package]]
name = "detect-installer"
version = "0.2.1"
@@ -482,15 +437,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
]
[[package]]
name = "docstring-parser"
version = "0.18.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" },
]
[[package]]
name = "duckdb"
version = "1.5.6"
@@ -752,54 +698,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/86/62c8477e775e1c035b817fd64964de3f8bd5743a53ca547c656484f5c356/fastar-0.12.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:70077a6d327ffa5d54f9d7bad6e6583c010530a5b120f5ad71a83b0a14d26ad9", size = 1025083, upload-time = "2026-08-20T09:11:23.724Z" },
]
[[package]]
name = "filetype"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" },
]
[[package]]
name = "google-auth"
version = "2.59.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "pyasn1-modules" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/e3/9f752a968e487fbc10b2c2a1dd561b62489f923972911009577bb88fe7bb/google_auth-2.59.1.tar.gz", hash = "sha256:ce50fc533ac02f489a2b183a0c156672c376ecb2091b1127bc7efba2975fff27", size = 384298, upload-time = "2026-09-30T17:26:25.738Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/38/311ac322f46ecd789f1342bb7adb0a14b940cf54b5db0e92d4f449aa6cf8/google_auth-2.59.1-py3-none-any.whl", hash = "sha256:89c3f931683a482ac97e61df7eb9da5e08a91703f3c752b2377d72cfb7d69e6a", size = 263519, upload-time = "2026-09-30T17:26:24.017Z" },
]
[package.optional-dependencies]
requests = [
{ name = "requests" },
]
[[package]]
name = "google-genai"
version = "2.27.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "google-auth", extra = ["requests"] },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "sniffio" },
{ name = "tenacity" },
{ name = "typing-extensions" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6e/28/22267bc20b240f8ee05302b3e8d5a5bd37f4ae236f468639390b4f988063/google_genai-2.27.0.tar.gz", hash = "sha256:7827330be282b3337c220f5d90c501b316c1ca0ee89358afb15f673dfcdab4fb", size = 705814, upload-time = "2026-10-01T22:41:28.049Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/a5/5255377cf8cdac7201f74ade6e70a17f0fe8fb0f65d06b0b857b2bf592ba/google_genai-2.27.0-py3-none-any.whl", hash = "sha256:44940e99d5d49432bb5e4fe3bd8ac9906e75162d051ca74af63b733fb5632382", size = 1167536, upload-time = "2026-10-01T22:41:26.071Z" },
]
[[package]]
name = "googleapis-common-protos"
version = "1.75.4"
@@ -1136,20 +1034,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/30/03/8cc027f9d297473de94f134546dae2bfca255161ebbda988522fc509e1d5/langchain-1.4.3-py3-none-any.whl", hash = "sha256:0334066939dd1c7551889727ed2d09a72dea7a6aed2193ffb61817fb658d8921", size = 164708, upload-time = "2026-09-28T20:17:22.978Z" },
]
[[package]]
name = "langchain-anthropic"
version = "1.7.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anthropic" },
{ name = "langchain-core" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/91/e6/45ead93ef6d9f244b502a6a32bf9a185b3da1cc7fa8003896a102dadac09/langchain_anthropic-1.7.5.tar.gz", hash = "sha256:6016f38e5c4bf12c16ecfa76abf133a978f459f4d2887b8e28435ae1ff00beb3", size = 764625, upload-time = "2026-09-29T15:22:07.106Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/a8/c507f2c1859b584843983e98bc71e0066c16a77e6c181921c928e0fdbcf7/langchain_anthropic-1.7.5-py3-none-any.whl", hash = "sha256:b21ef0e0c2d6175235a5b7eef4f5f89057df92d42dbfab66920a05605ef7173a", size = 65239, upload-time = "2026-09-29T15:22:05.832Z" },
]
[[package]]
name = "langchain-classic"
version = "1.0.8"
@@ -1189,21 +1073,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/90/10/8c00b4dc094886314c907ec7ead258dfee28948cf44175976cc4fba9aba8/langchain_core-1.6.6-py3-none-any.whl", hash = "sha256:d9a71e312ab7443701238750e04a57aefc037c4f1ec82837c8a1609d9a14a9b1", size = 572080, upload-time = "2026-09-29T14:52:53.213Z" },
]
[[package]]
name = "langchain-google-genai"
version = "4.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filetype" },
{ name = "google-genai" },
{ name = "langchain-core" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cf/4e/41798c80b574d958d189e049f13d64eb9246a66623465290f2cbfd641759/langchain_google_genai-4.4.0.tar.gz", hash = "sha256:7871beec56ac07b719f77c46997845db6ff2267b817bffb0f97877053b0895d7", size = 378415, upload-time = "2026-09-01T20:15:45.816Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/10/83bb535e78c2cf767a6193c2f3b00b2894c1581a36ef85911ebaa3b9a891/langchain_google_genai-4.4.0-py3-none-any.whl", hash = "sha256:8e23a1307bd2158590bbf9d99f1d658fc84a9d9ddb77fb1b372f8875a2bafbbf", size = 81617, upload-time = "2026-09-01T20:15:44.533Z" },
]
[[package]]
name = "langchain-mcp-adapters"
version = "0.3.2"
@@ -1543,7 +1412,6 @@ name = "my-v0-server"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "deepagents" },
{ name = "duckdb" },
{ name = "fastapi", extra = ["standard"] },
{ name = "httpx" },
@@ -1572,7 +1440,6 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "deepagents", specifier = ">=0.7.21" },
{ name = "duckdb", specifier = ">=1.2" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.141.1" },
{ name = "httpx", specifier = ">=0.28.1" },
@@ -2078,27 +1945,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/93/c0/37d4a7e8e2f7a6076283673d5298018ca26478b934c6ee369e10505ab32c/pyarrow-25.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4288f27577352d608ca08553b0865e4a9b3aa14820c5d95b53337218d609835b", size = 28753071, upload-time = "2026-08-10T12:40:46.623Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" },
]
[[package]]
name = "pyasn1-modules"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
@@ -3449,18 +3295,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/96/165c6e540bf765ca1115062017adf861d0816525f02464ba71313fe7cf92/watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec46a1ff4c1d81405a5fc645abdbff5e4e9388f7c9a6cdb392316a7c29b884f", size = 456919, upload-time = "2026-09-21T09:08:54.372Z" },
]
[[package]]
name = "wcmatch"
version = "11.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bracex" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/43/30e407989e313677dbb9d5f045f966549a7254834571e342eaa4b55cc67b/wcmatch-11.0.1.tar.gz", hash = "sha256:1ea2b4fa678b8ca268253798d5963935df39132d47c3e241c0a0732224005e7d", size = 144662, upload-time = "2026-08-14T15:20:40.477Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/77/7a02b0f05b3ffcdbef9719ce3ee0b508d6a29b58e95299f1580055671db3/wcmatch-11.0.1-py3-none-any.whl", hash = "sha256:fd149ecddb9f0a88ea780017d6dde17c994e494e7f7303d4e3c9d6251f978f4b", size = 43449, upload-time = "2026-08-14T15:20:39.379Z" },
]
[[package]]
name = "websockets"
version = "16.1.1"