diff --git a/jiuwen_memory/foundation/store/index/simple_memory_index.py b/jiuwen_memory/foundation/store/index/simple_memory_index.py index a8976ea4..991d4e1a 100644 --- a/jiuwen_memory/foundation/store/index/simple_memory_index.py +++ b/jiuwen_memory/foundation/store/index/simple_memory_index.py @@ -666,9 +666,13 @@ async def _list_memories_via_vector_pushdown( if mem_types: type_order = {mt: i for i, mt in enumerate(mem_types)} - docs.sort(key=lambda d: (type_order.get(d.type, len(type_order)), -d.timestamp.timestamp())) + docs.sort(key=lambda d: ( + type_order.get(d.type, len(type_order)), + -d.timestamp.timestamp(), + d.id, + )) else: - docs.sort(key=lambda d: d.timestamp, reverse=True) + docs.sort(key=lambda d: (-d.timestamp.timestamp(), d.id)) return docs[offset:offset + limit] async def _list_memories_via_kv_scan( @@ -708,9 +712,13 @@ async def _list_memories_via_kv_scan( docs.append(doc) if mem_types: type_order = {mt: i for i, mt in enumerate(mem_types)} - docs.sort(key=lambda d: (type_order.get(d.type, len(type_order)), -d.timestamp.timestamp())) + docs.sort(key=lambda d: ( + type_order.get(d.type, len(type_order)), + -d.timestamp.timestamp(), + d.id, + )) else: - docs.sort(key=lambda d: d.timestamp, reverse=True) + docs.sort(key=lambda d: (-d.timestamp.timestamp(), d.id)) if filters is not None: docs = [d for d in docs if _apply_filter_group(d, filters)] return docs[offset:offset + limit] diff --git a/jiuwen_memory/memory_core/long_term_memory.py b/jiuwen_memory/memory_core/long_term_memory.py index 282f7c2f..4f25a8a5 100644 --- a/jiuwen_memory/memory_core/long_term_memory.py +++ b/jiuwen_memory/memory_core/long_term_memory.py @@ -2321,16 +2321,54 @@ async def get_user_mem_by_page(self, start_idx = page_size * (page_idx - 1) fetch_size = start_idx + page_size - search_data = await self.search_manager.list_user_mem(user_id=user_id, scope_id=scope_id, - nums=fetch_size, pages=1, - mem_type=None, - filters=filters) - mem_results = self._build_mem_info_list(search_data) - mem_results.extend(await self._list_middle_memories(user_id=user_id, scope_id=scope_id, - limit=fetch_size)) - mem_results.sort(key=self._mem_info_timestamp_sort_key) + + # Both indexed memories and middle-term messages are exposed by their + # stores newest-first. Sorting only an expanding newest-N prefix into + # chronological order makes the prefix shift on every page and causes + # already-returned records to appear again. Build one complete, + # deterministic snapshot first, then paginate that snapshot once. + mem_results = await self._list_all_indexed_memories( + user_id=user_id, + scope_id=scope_id, + filters=filters, + batch_size=max(fetch_size, 100), + ) + mem_results.extend(await self._list_all_middle_memories( + user_id=user_id, + scope_id=scope_id, + initial_limit=max(fetch_size, 100), + )) + mem_results.sort(key=self._mem_info_chronological_sort_key) return mem_results[start_idx:start_idx + page_size] + async def _list_all_indexed_memories( + self, + user_id: str, + scope_id: str, + filters: "FilterGroup | None", + batch_size: int, + ) -> list[MemInfo]: + """Read a stable snapshot of all non-middle memories in bounded pages.""" + page_size = max(batch_size, 1) + page_idx = 1 + results_by_id: dict[str, MemInfo] = {} + while True: + search_data = await self.search_manager.list_user_mem( + user_id=user_id, + scope_id=scope_id, + nums=page_size, + pages=page_idx, + mem_type=None, + filters=filters, + ) + batch = self._build_mem_info_list(search_data) + for mem_info in batch: + results_by_id.setdefault(mem_info.mem_id, mem_info) + if len(batch) < page_size: + break + page_idx += 1 + return list(results_by_id.values()) + @staticmethod def _build_mem_info_list(search_data: list[dict] | None) -> list[MemInfo]: if not search_data: @@ -2349,12 +2387,14 @@ def _build_mem_info_list(search_data: list[dict] | None) -> list[MemInfo]: return mem_results @staticmethod - def _mem_info_timestamp_sort_key(mem_info: MemInfo) -> float: + def _mem_info_chronological_sort_key(mem_info: MemInfo) -> tuple[float, str]: if mem_info.timestamp is None: - return 0.0 - if mem_info.timestamp.tzinfo is None: - return mem_info.timestamp.replace(tzinfo=timezone.utc).timestamp() - return mem_info.timestamp.timestamp() + timestamp = 0.0 + elif mem_info.timestamp.tzinfo is None: + timestamp = mem_info.timestamp.replace(tzinfo=timezone.utc).timestamp() + else: + timestamp = mem_info.timestamp.timestamp() + return timestamp, mem_info.mem_id async def _list_middle_memories_by_page( self, @@ -2365,9 +2405,35 @@ async def _list_middle_memories_by_page( ) -> list[MemInfo]: start_idx = page_size * (page_idx - 1) fetch_size = start_idx + page_size - mem_results = await self._list_middle_memories(user_id=user_id, scope_id=scope_id, limit=fetch_size) + mem_results = await self._list_all_middle_memories( + user_id=user_id, + scope_id=scope_id, + initial_limit=max(fetch_size, 100), + ) + mem_results.sort(key=self._mem_info_chronological_sort_key) return mem_results[start_idx:start_idx + page_size] + async def _list_all_middle_memories( + self, + user_id: str, + scope_id: str, + initial_limit: int, + ) -> list[MemInfo]: + """Read all middle memories by growing the newest-prefix request.""" + limit = max(initial_limit, 1) + while True: + mem_results = await self._list_middle_memories( + user_id=user_id, + scope_id=scope_id, + limit=limit, + ) + if len(mem_results) < limit: + results_by_id: dict[str, MemInfo] = {} + for mem_info in mem_results: + results_by_id.setdefault(mem_info.mem_id, mem_info) + return list(results_by_id.values()) + limit *= 2 + async def _list_middle_memories(self, user_id: str, scope_id: str, limit: int) -> list[MemInfo]: if limit <= 0 or not self.message_manager: return [] @@ -3032,4 +3098,4 @@ async def _create_semantic_store_with_embedding(self, scope_id: str) -> Semantic semantic_store.initialize_embedding_model(self._base_embed) else: pass - return semantic_store \ No newline at end of file + return semantic_store diff --git a/pyproject.toml b/pyproject.toml index 7d71bd65..fe1722ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ sqlite = ["aiosqlite>=0.22.1", "greenlet>=3.1.1"] redis = ["redis>=7.1.0"] mem0 = ["mem0ai>=1.0.0"] agentarts = ["agentarts-sdk>=0.1.2,<0.2"] -server = ["uvicorn>=0.30.0", "fastapi>=0.110.0", "python-dotenv>=1.2.1", "mcp>=1.2.0,<2.0.0"] +server = ["uvicorn>=0.30.0", "fastapi>=0.110.0", "python-dotenv>=1.2.1", "mcp>=1.14.1,<2.0.0"] all-storage = ["JiuwenMemory[sqlite,postgres,mysql,gaussdb,redis]"] all-vector = ["JiuwenMemory[chromadb,gaussvector,elasticsearch]"] file-index = ["sqlite-vec>=0.1.9", "watchdog>=4.0", "jieba>=0.42.1"] @@ -76,7 +76,7 @@ memory-mcp = "jiuwen_memory.server.mcp_server:main" [dependency-groups] test = [ "pytest>=8.3.5", "pytest-asyncio>=1.0.0", "pytest-html>=4.1.1", - "pytest-mock>=3.14.0", "pytest-cov>=7.0.0", "coverage>=7.7.1", + "pytest-mock>=3.14.0", "pytest-cov>=7.0.0", "coverage>=7.7.1", "mcp>=1.14.1,<2.0.0", ] lint = ["ruff>=0.11.2", "pylint>=3.0.0", "mypy>=1.12.0"] dev = [{ include-group = "test" }, { include-group = "lint" }] diff --git a/tests/unit_tests/core/memory/test_long_term_memory_middle_mutations.py b/tests/unit_tests/core/memory/test_long_term_memory_middle_mutations.py index 680d5bf5..209f8cda 100644 --- a/tests/unit_tests/core/memory/test_long_term_memory_middle_mutations.py +++ b/tests/unit_tests/core/memory/test_long_term_memory_middle_mutations.py @@ -469,8 +469,132 @@ async def test_get_user_mem_by_page_unknown_merges_long_and_middle_memory(): assert memory.search_manager.calls == [{ "user_id": "u1", "scope_id": "s1", - "nums": 10, + "nums": 100, "pages": 1, "mem_type": None, "filters": None, }] + + +@pytest.mark.asyncio +async def test_get_user_mem_by_page_unknown_has_no_cross_page_duplicates(): + oldest_first = [ + { + "id": f"long_{idx:03d}", + "mem": f"长期记忆 {idx}", + "mem_type": MemoryType.SEMANTIC_MEMORY.value, + "timestamp": datetime(2026, 7, 21, idx, 0, tzinfo=timezone.utc), + } + for idx in range(10) + ] + memory = LongTermMemory() + # Production indexes list newest-first. The old implementation fetched + # an expanding newest-N prefix, reversed it, and returned duplicate pages. + memory.search_manager = FakeSearchManager(list(reversed(oldest_first))) + memory.message_manager = FakeMessageManager() + + pages = [ + await memory.get_user_mem_by_page( + user_id="u1", + scope_id="s1", + page_size=3, + page_idx=page_idx, + memory_type=MemoryType.UNKNOWN, + ) + for page_idx in range(1, 6) + ] + + assert [[item.mem_id for item in page] for page in pages] == [ + ["long_000", "long_001", "long_002"], + ["long_003", "long_004", "long_005"], + ["long_006", "long_007", "long_008"], + ["long_009"], + [], + ] + all_ids = [item.mem_id for page in pages for item in page] + assert len(all_ids) == len(set(all_ids)) == 10 + + +@pytest.mark.asyncio +async def test_get_user_mem_by_page_unknown_orders_same_timestamp_by_mem_id(): + timestamp = datetime(2026, 7, 21, 12, 0, tzinfo=timezone.utc) + memory = LongTermMemory() + memory.search_manager = FakeSearchManager([ + { + "id": "b-long", + "mem": "长期记忆 B", + "mem_type": MemoryType.SEMANTIC_MEMORY.value, + "timestamp": timestamp, + }, + { + "id": "a-long", + "mem": "长期记忆 A", + "mem_type": MemoryType.SEMANTIC_MEMORY.value, + "timestamp": timestamp, + }, + ]) + memory.message_manager = FakeMessageManager() + memory.message_manager.messages = [ + (BaseMessage(role="user", content="中期记忆 D"), timestamp, "d-middle"), + (BaseMessage(role="user", content="中期记忆 C"), timestamp, "c-middle"), + ] + + pages = [ + await memory.get_user_mem_by_page( + user_id="u1", + scope_id="s1", + page_size=2, + page_idx=page_idx, + memory_type=MemoryType.UNKNOWN, + ) + for page_idx in (1, 2) + ] + + assert [[item.mem_id for item in page] for page in pages] == [ + ["a-long", "b-long"], + ["c-middle", "d-middle"], + ] + all_ids = [item.mem_id for page in pages for item in page] + assert len(all_ids) == len(set(all_ids)) == 4 + + +@pytest.mark.asyncio +async def test_get_user_mem_by_page_middle_has_no_cross_page_duplicates(): + class NewestPrefixMessageManager(FakeMessageManager): + async def get(self, user_id=None, scope_id=None, session_id=None, message_len=10): + return self.messages[-message_len:] + + oldest_first = [ + ( + BaseMessage(role="user", content=f"中期记忆 {idx}"), + datetime(2026, 7, 21, idx, 0, tzinfo=timezone.utc), + f"middle_{idx:03d}", + ) + for idx in range(10) + ] + memory = LongTermMemory() + memory.search_manager = FakeSearchManager([]) + memory.message_manager = NewestPrefixMessageManager() + # Production MessageManager returns the newest prefix in chronological + # order after reversing the database's DESC query. + memory.message_manager.messages = oldest_first + + pages = [ + await memory.get_user_mem_by_page( + user_id="u1", + scope_id="s1", + page_size=3, + page_idx=page_idx, + memory_type=MemoryType.MIDDLE_TERM_MEMORY, + ) + for page_idx in range(1, 5) + ] + + assert [[item.mem_id for item in page] for page in pages] == [ + ["middle_000", "middle_001", "middle_002"], + ["middle_003", "middle_004", "middle_005"], + ["middle_006", "middle_007", "middle_008"], + ["middle_009"], + ] + all_ids = [item.mem_id for page in pages for item in page] + assert len(all_ids) == len(set(all_ids)) == 10 diff --git a/uv.lock b/uv.lock index 6c73a303..66fe1b33 100644 --- a/uv.lock +++ b/uv.lock @@ -923,6 +923,17 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1" }, ] +[[package]] +name = "gmssl" +version = "3.2.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "pycryptodomex" }, +] +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/41/b1/01d707a2edfaad77715b2d27e5fbf14b6bcd34dd72ea179a5facfe4b1dd7/gmssl-3.2.2-py3-none-any.whl", hash = "sha256:59f069a91eb19ef59b9e7be4d436ed01c92ce064d3d7d45a8778fc07fd2cd068" }, +] + [[package]] name = "googleapis-common-protos" version = "1.75.0" @@ -1131,6 +1142,15 @@ http2 = [ { name = "h2" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc" }, +] + [[package]] name = "huaweicloudsdkagentidentity" version = "3.1.201" @@ -1247,6 +1267,12 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75" }, ] +[[package]] +name = "jieba" +version = "0.42.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2" } + [[package]] name = "jinja2" version = "3.1.6" @@ -1334,6 +1360,7 @@ dependencies = [ { name = "charset-normalizer" }, { name = "dashscope" }, { name = "filelock" }, + { name = "gmssl" }, { name = "httpx" }, { name = "loguru" }, { name = "oauthlib" }, @@ -1364,12 +1391,16 @@ all = [ { name = "elasticsearch", extra = ["async"] }, { name = "fastapi" }, { name = "greenlet" }, + { name = "jieba" }, + { name = "mcp" }, { name = "opentelemetry-proto" }, { name = "protobuf" }, { name = "psycopg2-binary" }, { name = "python-dotenv" }, { name = "redis" }, + { name = "sqlite-vec" }, { name = "uvicorn" }, + { name = "watchdog" }, ] all-storage = [ { name = "aiomysql" }, @@ -1394,6 +1425,11 @@ chromadb = [ elasticsearch = [ { name = "elasticsearch", extra = ["async"] }, ] +file-index = [ + { name = "jieba" }, + { name = "sqlite-vec" }, + { name = "watchdog" }, +] gaussdb = [ { name = "async-gaussdb" }, { name = "asyncpg" }, @@ -1415,6 +1451,7 @@ redis = [ ] server = [ { name = "fastapi" }, + { name = "mcp" }, { name = "python-dotenv" }, { name = "uvicorn" }, ] @@ -1426,6 +1463,7 @@ sqlite = [ [package.dev-dependencies] dev = [ { name = "coverage" }, + { name = "mcp" }, { name = "mypy" }, { name = "pylint" }, { name = "pytest" }, @@ -1442,6 +1480,7 @@ lint = [ ] test = [ { name = "coverage" }, + { name = "mcp" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -1467,12 +1506,15 @@ requires-dist = [ { name = "elasticsearch", extras = ["async"], marker = "extra == 'elasticsearch'", specifier = ">=8,<9" }, { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.110.0" }, { name = "filelock", specifier = ">=3.20.1" }, + { name = "gmssl", specifier = ">=3.2.2" }, { name = "greenlet", marker = "extra == 'sqlite'", specifier = ">=3.1.1" }, { name = "httpx", specifier = ">=0.24" }, - { name = "jiuwenmemory", extras = ["all-storage", "all-vector", "server"], marker = "extra == 'all'" }, + { name = "jieba", marker = "extra == 'file-index'", specifier = ">=0.42.1" }, + { name = "jiuwenmemory", extras = ["all-storage", "all-vector", "server", "file-index"], marker = "extra == 'all'" }, { name = "jiuwenmemory", extras = ["chromadb", "gaussvector", "elasticsearch"], marker = "extra == 'all-vector'" }, { name = "jiuwenmemory", extras = ["sqlite", "postgres", "mysql", "gaussdb", "redis"], marker = "extra == 'all-storage'" }, { name = "loguru", specifier = ">=0.7.3" }, + { name = "mcp", marker = "extra == 'server'", specifier = ">=1.14.1,<2.0.0" }, { name = "mem0ai", marker = "extra == 'mem0'", specifier = ">=1.0.0" }, { name = "oauthlib", specifier = ">=3.3.1" }, { name = "openai", specifier = ">=1.108.0" }, @@ -1489,16 +1531,19 @@ requires-dist = [ { name = "redis", marker = "extra == 'redis'", specifier = ">=7.1.0" }, { name = "requests", specifier = ">=2.32.3" }, { name = "sqlalchemy", specifier = ">=2.0.41" }, + { name = "sqlite-vec", marker = "extra == 'file-index'", specifier = ">=0.1.9" }, { name = "sqlmodel", specifier = ">=0.0.37" }, { name = "tenacity", specifier = ">=9.1.2" }, { name = "tqdm", specifier = ">=4.0" }, { name = "uvicorn", marker = "extra == 'server'", specifier = ">=0.30.0" }, + { name = "watchdog", marker = "extra == 'file-index'", specifier = ">=4.0" }, ] -provides-extras = ["chromadb", "gaussvector", "elasticsearch", "postgres", "mysql", "gaussdb", "sqlite", "redis", "mem0", "agentarts", "server", "all-storage", "all-vector", "default", "all"] +provides-extras = ["chromadb", "gaussvector", "elasticsearch", "postgres", "mysql", "gaussdb", "sqlite", "redis", "mem0", "agentarts", "server", "all-storage", "all-vector", "file-index", "default", "all"] [package.metadata.requires-dev] dev = [ { name = "coverage", specifier = ">=7.7.1" }, + { name = "mcp", specifier = ">=1.14.1,<2.0.0" }, { name = "mypy", specifier = ">=1.12.0" }, { name = "pylint", specifier = ">=3.0.0" }, { name = "pytest", specifier = ">=8.3.5" }, @@ -1515,6 +1560,7 @@ lint = [ ] test = [ { name = "coverage", specifier = ">=7.7.1" }, + { name = "mcp", specifier = ">=1.14.1,<2.0.0" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, @@ -1715,6 +1761,31 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" }, ] +[[package]] +name = "mcp" +version = "1.29.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -2622,6 +2693,36 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c" }, ] +[[package]] +name = "pycryptodomex" +version = "3.23.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c9/85/e24bf90972a30b0fcd16c73009add1d7d7cd9140c2498a68252028899e41/pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/2e/00/10edb04777069a42490a38c137099d4b17ba6e36a4e6e28bdc7470e9e853/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6b/3f/2872a9c2d3a27eac094f9ceaa5a8a483b774ae69018040ea3240d5b11154/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/70/af/774c2e2b4f6570fbf6a4972161adbb183aeeaa1863bde31e8706f123bf92/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa" }, + { url = "https://mirrors.aliyun.com/pypi/packages/de/a3/71065b24cb889d537954cedc3ae5466af00a2cabcff8e29b73be047e9a19/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c9/0b/ff6f43b7fbef4d302c8b981fe58467b8871902cdc3eb28896b52421422cc/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/02/de/9d4772c0506ab6da10b41159493657105d3f8bb5c53615d19452afc6b315/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314" }, + { url = "https://mirrors.aliyun.com/pypi/packages/28/ad/8b30efcd6341707a234e5eba5493700a17852ca1ac7a75daa7945fcf6427/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006" }, + { url = "https://mirrors.aliyun.com/pypi/packages/0f/02/16868e9f655b7670dbb0ac4f2844145cbc42251f916fc35c414ad2359849/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ca/18/4ca89ac737230b52ac8ffaca42f9c6f1fd07c81a6cd821e91af79db60632/pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328" }, + { url = "https://mirrors.aliyun.com/pypi/packages/73/34/13e01c322db027682e00986873eca803f11c56ade9ba5bbf3225841ea2d4/pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708" }, + { url = "https://mirrors.aliyun.com/pypi/packages/54/68/9504c8796b1805d58f4425002bcca20f12880e6fa4dc2fc9a668705c7a08/pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6" }, + { url = "https://mirrors.aliyun.com/pypi/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c8/a9/8862616a85cf450d2822dbd4fff1fcaba90877907a6ff5bc2672cafe42f8/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/46/9f/bda9c49a7c1842820de674ab36c79f4fbeeee03f8ff0e4f3546c3889076b/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/03/cc/870b9bf8ca92866ca0186534801cf8d20554ad2a76ca959538041b7a7cf4/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003" }, + { url = "https://mirrors.aliyun.com/pypi/packages/96/e3/ce9348236d8e669fea5dd82a90e86be48b9c341210f44e25443162aba187/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a5/e9/e869bcee87beb89040263c416a8a50204f7f7a83ac11897646c9e71e0daf/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51" }, +] + [[package]] name = "pydantic" version = "2.13.4" @@ -2741,6 +2842,11 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728" }, ] +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pylint" version = "4.0.6" @@ -2957,6 +3063,15 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a" }, ] +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23" }, +] + [[package]] name = "pytz" version = "2026.2" @@ -3333,6 +3448,18 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5" }, ] +[[package]] +name = "sqlite-vec" +version = "0.1.9" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786" }, + { url = "https://mirrors.aliyun.com/pypi/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32" }, +] + [[package]] name = "sqlmodel" version = "0.0.39" @@ -3347,6 +3474,19 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/cf/7d/b9813a582d4eb310be35e1fc7dfaae71207d7b62e9e53be314ebd251b53b/sqlmodel-0.0.39-py3-none-any.whl", hash = "sha256:90ebe92ce5cc11d7fff8dc7cb594790a102333c8fe7c14865254f6fc5c939795" }, ] +[[package]] +name = "sse-starlette" +version = "3.4.6" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/6c/10/a34c656829ffc1c4b22ef36d70d9ebb6b99c020e2aeb17cee5485099f028/sse_starlette-3.4.6.tar.gz", hash = "sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/49/36/e10c1d1b7ca881d2625db2ec28508578499187bb1c389952c398474e1834/sse_starlette-3.4.6-py3-none-any.whl", hash = "sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6" }, +] + [[package]] name = "starlette" version = "1.3.1" @@ -3557,6 +3697,33 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad" }, ] +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948" }, + { url = "https://mirrors.aliyun.com/pypi/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0" }, + { url = "https://mirrors.aliyun.com/pypi/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26" }, + { url = "https://mirrors.aliyun.com/pypi/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680" }, + { url = "https://mirrors.aliyun.com/pypi/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f" }, +] + [[package]] name = "watchfiles" version = "1.2.0"