From a076ae2dbb4ec2e47e47f70492779c47c847d037 Mon Sep 17 00:00:00 2001 From: Cheney Zhang Date: Wed, 15 Apr 2026 12:51:41 +0000 Subject: [PATCH] feat: add Milvus vector index provider Wire up an external VectorIndex abstraction alongside the existing metadata store, and ship a Milvus / Milvus Lite / Zilliz Cloud implementation as the first provider. Users can now opt into Milvus with: database_config = { "metadata_store": {"provider": "inmemory"}, "vector_index": {"provider": "milvus"}, # ./milvus.db by default } The inmemory metadata backend mirrors create/update/delete into the vector index and routes vector_search_items through it, preserving scope filters (user_id, agent_id, ...) via Milvus dynamic fields. Salience ranking still runs locally because it relies on per-item reinforcement/recency factors. Scope is additive and non-breaking: default configs keep using the bruteforce/pgvector paths. pymilvus and milvus-lite are gated behind the new `[milvus]` optional-dependencies extra. Adds ADR 0003, docs/milvus.md, and a runnable example under examples/milvus_vector_index.py. --- CHANGELOG.md | 6 + README.md | 8 + docs/adr/0003-external-vector-index.md | 39 ++++ docs/architecture.md | 7 + docs/milvus.md | 83 +++++++ examples/milvus_vector_index.py | 72 ++++++ pyproject.toml | 15 +- src/memu/app/settings.py | 18 +- src/memu/database/factory.py | 17 +- src/memu/database/inmemory/__init__.py | 3 + src/memu/database/inmemory/repo.py | 13 +- .../inmemory/repositories/memory_item_repo.py | 55 ++++- src/memu/database/vector_index/__init__.py | 29 +++ src/memu/database/vector_index/interfaces.py | 39 ++++ src/memu/database/vector_index/milvus.py | 216 ++++++++++++++++++ tests/test_milvus_vector_index.py | 175 ++++++++++++++ uv.lock | 166 +++++++++++++- 17 files changed, 951 insertions(+), 10 deletions(-) create mode 100644 docs/adr/0003-external-vector-index.md create mode 100644 docs/milvus.md create mode 100644 examples/milvus_vector_index.py create mode 100644 src/memu/database/vector_index/__init__.py create mode 100644 src/memu/database/vector_index/interfaces.py create mode 100644 src/memu/database/vector_index/milvus.py create mode 100644 tests/test_milvus_vector_index.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 04f3d163..bf25d073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +* Add Milvus / Milvus Lite / Zilliz Cloud as an optional external vector index (`vector_index.provider = "milvus"`), wired to the in-memory metadata store. See `docs/milvus.md` and `docs/adr/0003-external-vector-index.md`. + ## [1.5.1](https://github.com/NevaMind-AI/memU/compare/v1.5.0...v1.5.1) (2026-03-23) diff --git a/README.md b/README.md index 2dedb745..cbf3ddae 100644 --- a/README.md +++ b/README.md @@ -315,6 +315,14 @@ Both examples demonstrate **proactive memory workflows**: See [`tests/test_inmemory.py`](tests/test_inmemory.py) and [`tests/test_postgres.py`](tests/test_postgres.py) for implementation details. +**Use Milvus / Milvus Lite / Zilliz Cloud as an external vector index:** +```bash +uv sync --extra milvus +export OPENAI_API_KEY=your_api_key +python examples/milvus_vector_index.py +``` +The `inmemory` metadata store is wired to Milvus today; the `sqlite` and `postgres` backends follow the same pattern and will be wired in a follow-up. See [`docs/milvus.md`](docs/milvus.md) for configuration details. + --- ### Custom LLM and Embedding Providers diff --git a/docs/adr/0003-external-vector-index.md b/docs/adr/0003-external-vector-index.md new file mode 100644 index 00000000..43658288 --- /dev/null +++ b/docs/adr/0003-external-vector-index.md @@ -0,0 +1,39 @@ +# ADR 0003: External Vector Index Alongside Metadata Store + +- Status: Accepted +- Date: 2026-04-15 + +## Context + +ADR 0002 locked in repository-based storage with backend-aware vector search: the `inmemory` and `sqlite` backends do brute-force cosine, and `postgres` can use pgvector. Two footprints are not well served by that design: + +- Multi-million-vector workloads, where brute-force is too slow and pgvector requires keeping Postgres hot enough to hold the index. +- Managed deployments that already standardise on a dedicated vector service (Milvus, Zilliz Cloud) and want memU to reuse it. + +`settings.py` already separates `metadata_store` from `vector_index` in configuration, but every shipped backend kept vectors inside its own tables. The separation was not realised in code. + +## Decision + +Introduce a `VectorIndex` protocol that memory repositories can delegate to when an external index is configured. The first implementation is `MilvusVectorIndex`, targeting Milvus Lite (zero-dep local), self-hosted Milvus, and Zilliz Cloud behind a single `uri`/`token` config. + +- `VectorIndex` lives in `memu/database/vector_index/` and exposes `upsert`, `delete`, `delete_many`, `search`, `close`. +- `build_vector_index` in the factory constructs the provider based on `VectorIndexConfig.provider`. +- Metadata backends receive the vector index through their builder and mirror mutations to it on create / update / delete / clear. +- Search routes through the vector index when present; salience ranking stays local because it needs per-item reinforcement and recency factors the index does not persist. + +Initial scope: the `inmemory` metadata backend is wired to `MilvusVectorIndex`. `sqlite` and `postgres` are planned follow-ups; their existing (brute-force / pgvector) paths remain the default so the change is additive. + +## Consequences + +Positive: + +- memU can scale vector similarity independently of the metadata store. +- Users on Zilliz Cloud or a shared Milvus cluster can reuse that infrastructure. +- Milvus Lite keeps the zero-setup local story (single file, no service) that `inmemory` already promises. + +Negative: + +- Two systems of record mean write amplification and a consistency window between the metadata store and the vector index. +- Scope fields are replicated into Milvus as dynamic fields, increasing storage vs. a single SQL column. +- `pymilvus` / `milvus-lite` are optional and gated behind the `milvus` extra; users picking this path accept that dependency. +- `milvus-lite <=2.5.1` still imports `pkg_resources`, so Python 3.13 users need `setuptools<81` until upstream switches to `importlib.metadata`. diff --git a/docs/architecture.md b/docs/architecture.md index fefbf54d..90f50c25 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -129,6 +129,13 @@ Storage is abstracted through a `Database` protocol with four repositories: For Postgres, startup runs migration bootstrap and attempts `CREATE EXTENSION IF NOT EXISTS vector` in `ddl_mode="create"`. +### External vector index (optional) + +`database_config.vector_index` can point at an external index that lives outside the metadata store. Providers: + +- `bruteforce` / `pgvector`: default, handled inside the metadata backend (see above). +- `milvus`: delegates similarity search to Milvus / Milvus Lite / Zilliz Cloud via the `VectorIndex` protocol in `memu/database/vector_index/`. Metadata backends mirror embedding writes through this interface on create/update/delete; searches route through it except for `ranking="salience"`, which still runs locally. See `docs/milvus.md` and `docs/adr/0003-external-vector-index.md`. + ### Scope model propagation `UserConfig.model` is merged into record/table models so scope fields (for example `user_id`) become first-class columns/attributes across resources, items, categories, and relations. diff --git a/docs/milvus.md b/docs/milvus.md new file mode 100644 index 00000000..3fdaeb4f --- /dev/null +++ b/docs/milvus.md @@ -0,0 +1,83 @@ +# Milvus Vector Index + +memU can route similarity search to [Milvus](https://milvus.io/) as an external vector index, decoupled from the metadata store. This is useful when: + +- **Scale**: you have more vectors than the brute-force fallback can handle at latency budget. +- **Managed deployments**: you want to offload vector infrastructure to Zilliz Cloud or a self-hosted Milvus cluster. +- **Zero-setup local dev**: Milvus Lite runs as a local file (`./milvus.db`) with no external service. + +## Install + +```bash +uv sync --extra milvus +# or: uv pip install "memu-py[milvus]" +``` + +> The `milvus` extra pulls `pymilvus`, `milvus-lite` and — on Python 3.13 — a `setuptools<81` pin to work around [milvus-lite's use of the removed `pkg_resources` API](https://github.com/milvus-io/milvus-lite/issues). + +## Quick Start (Milvus Lite) + +```python +from memu.app import MemoryService + +service = MemoryService( + llm_profiles={"default": {"api_key": "your-api-key"}}, + database_config={ + "metadata_store": {"provider": "inmemory"}, + "vector_index": {"provider": "milvus"}, + }, +) +``` + +With the default configuration the index is persisted to `./milvus.db` using Milvus Lite — no Docker, no separate process. + +## Targeting a Milvus Server + +```python +database_config = { + "metadata_store": {"provider": "sqlite", "dsn": "sqlite:///memu.db"}, + "vector_index": { + "provider": "milvus", + "uri": "http://localhost:19530", + "collection_name": "memu_prod", + }, +} +``` + +## Targeting Zilliz Cloud + +```python +database_config = { + "metadata_store": {"provider": "postgres", "dsn": os.environ["POSTGRES_DSN"]}, + "vector_index": { + "provider": "milvus", + "uri": os.environ["ZILLIZ_URI"], + "token": os.environ["ZILLIZ_TOKEN"], + "collection_name": "memu_prod", + }, +} +``` + +## Configuration + +| Field | Default | Notes | +| --- | --- | --- | +| `provider` | — | Must be `"milvus"` to enable this index. | +| `uri` | `"./milvus.db"` | File path runs Milvus Lite; `http(s)://host:port` targets a Milvus server; a Zilliz Cloud endpoint targets the managed service. | +| `token` | `None` | Auth token for Zilliz Cloud or a secured Milvus server. | +| `collection_name` | `"memu_memory_items"` | Name of the Milvus collection that holds memory vectors. | +| `dim` | `None` | Embedding dimension. Inferred from the first upsert when omitted. | + +## Supported Combinations + +The Milvus vector index can be paired with any metadata store. memU keeps the metadata record (summary, categories, scope, reinforcement stats, ...) in the metadata store and mirrors embeddings into Milvus on create / update / delete. + +- **Available now**: `inmemory` metadata store + Milvus vector index (feature-complete). +- **Planned**: `sqlite` and `postgres` metadata stores wired to Milvus. See `docs/adr/0003-external-vector-index.md` for the rollout plan. + +## How Search Works + +1. `create_item` / `update_item` mirror the embedding (and scope fields such as `user_id`, `agent_id`) into the configured Milvus collection. +2. `vector_search_items` forwards the query vector to Milvus using a COSINE AUTOINDEX. Scope filters (e.g. `where={"user_id": "u1"}`) are translated into Milvus boolean expressions on dynamic fields. +3. Milvus returns `(id, score)` pairs; the metadata store resolves them back to full memory records. +4. Salience ranking (`ranking="salience"`) still runs locally because it needs per-item reinforcement and recency factors that the vector index does not store. diff --git a/examples/milvus_vector_index.py b/examples/milvus_vector_index.py new file mode 100644 index 00000000..606f2933 --- /dev/null +++ b/examples/milvus_vector_index.py @@ -0,0 +1,72 @@ +"""Minimal end-to-end example: memU backed by a Milvus vector index. + +Uses the ``inmemory`` metadata store and Milvus Lite (a single ``milvus.db`` +file) so it runs with zero external services. Point ``uri`` at a Milvus server +URL or a Zilliz Cloud endpoint to scale to production. + +Run: + + uv sync --extra milvus + export OPENAI_API_KEY=sk-... + uv run python examples/milvus_vector_index.py +""" + +from __future__ import annotations + +import asyncio +import os +from pathlib import Path + +from memu.app import MemoryService + + +async def main() -> None: + api_key = os.environ.get("OPENAI_API_KEY") + if not api_key: + msg = "Set OPENAI_API_KEY before running this example." + raise SystemExit(msg) + + file_path = os.path.abspath("example/example_conversation.json") + if not Path(file_path).exists(): + msg = ( + f"Example conversation not found at {file_path}. " + "Run from the repository root so the 'example/' folder is visible." + ) + raise SystemExit(msg) + + service = MemoryService( + llm_profiles={"default": {"api_key": api_key}}, + database_config={ + "metadata_store": {"provider": "inmemory"}, + "vector_index": { + "provider": "milvus", + # Local Milvus Lite file. Swap for "http://host:19530" or a + # Zilliz Cloud endpoint to target a real deployment. + "uri": "./milvus.db", + "collection_name": "memu_example", + }, + }, + retrieve_config={"method": "rag"}, + ) + + print("[memU + Milvus] Memorizing example conversation...") + memory = await service.memorize( + resource_url=file_path, + modality="conversation", + user={"user_id": "demo-user"}, + ) + for cat in memory.get("categories", []): + print(f" - {cat.get('name')}: {(cat.get('summary') or '')[:80]}...") + + queries = [ + {"role": "user", "content": {"text": "What do you know about my preferences?"}}, + ] + result = await service.retrieve(queries=queries, where={"user_id": "demo-user"}) + + print("\n[memU + Milvus] Retrieved items:") + for item in result.get("items", [])[:5]: + print(f" - [{item.get('memory_type')}] {(item.get('summary') or '')[:100]}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index 82574482..a545e508 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,13 @@ test = [ [project.optional-dependencies] postgres = ["pgvector>=0.3.4", "sqlalchemy[postgresql-psycopgbinary]>=2.0.36"] +milvus = [ + "milvus-lite>=2.4.10", + "pymilvus>=2.5.0", + # milvus-lite <=2.5.1 still imports pkg_resources, which setuptools >=81 + # removed. Pin on Python 3.13 until a milvus-lite release drops the import. + "setuptools<81; python_version >= '3.13'", +] langgraph = ["langgraph>=0.0.10", "langchain-core>=0.1.0"] claude = ["claude-agent-sdk>=0.1.24"] @@ -81,7 +88,9 @@ memu-server = "memu.server.cli:main" [tool.deptry.per_rule_ignores] # Optional dependencies used in examples/ -DEP002 = ["claude-agent-sdk"] +# milvus-lite is imported indirectly by pymilvus when uri ends in .db; +# setuptools is a conditional compatibility pin for milvus-lite on Python 3.13. +DEP002 = ["claude-agent-sdk", "milvus-lite", "setuptools"] [tool.mypy] files = ["src", "tests"] @@ -109,6 +118,10 @@ ignore_missing_imports = true module = ["pgvector.*"] ignore_missing_imports = true +[[tool.mypy.overrides]] +module = ["pymilvus.*"] +ignore_missing_imports = true + [[tool.mypy.overrides]] module = ["memu.client.openai_wrapper"] disallow_untyped_defs = false diff --git a/src/memu/app/settings.py b/src/memu/app/settings.py index adcb4f16..b62e0966 100644 --- a/src/memu/app/settings.py +++ b/src/memu/app/settings.py @@ -303,8 +303,22 @@ class MetadataStoreConfig(BaseModel): class VectorIndexConfig(BaseModel): - provider: Annotated[Literal["bruteforce", "pgvector", "none"], Normalize] = "bruteforce" + provider: Annotated[Literal["bruteforce", "pgvector", "milvus", "none"], Normalize] = "bruteforce" dsn: str | None = Field(default=None, description="Postgres connection string when provider=pgvector.") + uri: str | None = Field( + default=None, + description=( + "Milvus URI. File path (e.g. './milvus.db') runs Milvus Lite, " + "'http://host:19530' targets a Milvus server, and a Zilliz Cloud " + "endpoint targets the managed service." + ), + ) + token: str | None = Field(default=None, description="Milvus/Zilliz auth token, if required.") + collection_name: str = Field(default="memu_memory_items", description="Milvus collection name.") + dim: int | None = Field( + default=None, + description="Embedding dimension. If None, inferred from the first upsert.", + ) class DatabaseConfig(BaseModel): @@ -319,3 +333,5 @@ def model_post_init(self, __context: Any) -> None: self.vector_index = VectorIndexConfig(provider="bruteforce") elif self.vector_index.provider == "pgvector" and self.vector_index.dsn is None: self.vector_index = self.vector_index.model_copy(update={"dsn": self.metadata_store.dsn}) + elif self.vector_index.provider == "milvus" and self.vector_index.uri is None: + self.vector_index = self.vector_index.model_copy(update={"uri": "./milvus.db"}) diff --git a/src/memu/database/factory.py b/src/memu/database/factory.py index dd1fcb50..fe1b7ce8 100644 --- a/src/memu/database/factory.py +++ b/src/memu/database/factory.py @@ -7,6 +7,7 @@ from memu.app.settings import DatabaseConfig from memu.database.inmemory import build_inmemory_database from memu.database.interfaces import Database +from memu.database.vector_index import build_vector_index if TYPE_CHECKING: pass @@ -20,14 +21,26 @@ def build_database( """ Initialize a database backend for the configured provider. - Supported providers: + Supported metadata providers: - "inmemory": In-memory storage (default, no persistence) - "postgres": PostgreSQL with optional pgvector support - "sqlite": SQLite file-based storage (lightweight, portable) + + Supported vector index providers (used together with a metadata provider): + - "bruteforce": In-memory cosine top-k (default) + - "pgvector": PostgreSQL pgvector column in the metadata backend + - "milvus": External Milvus / Milvus Lite / Zilliz Cloud index + - "none": Disable vector search entirely """ + vector_index = build_vector_index(config.vector_index) + provider = config.metadata_store.provider if provider == "inmemory": - return build_inmemory_database(config=config, user_model=user_model) + return build_inmemory_database( + config=config, + user_model=user_model, + vector_index=vector_index, + ) elif provider == "postgres": # Lazy import to avoid requiring pgvector when not using postgres from memu.database.postgres import build_postgres_database diff --git a/src/memu/database/inmemory/__init__.py b/src/memu/database/inmemory/__init__.py index fadbd111..d60a3cfa 100644 --- a/src/memu/database/inmemory/__init__.py +++ b/src/memu/database/inmemory/__init__.py @@ -5,12 +5,14 @@ from memu.app.settings import DatabaseConfig from memu.database.inmemory.models import build_inmemory_models from memu.database.inmemory.repo import InMemoryStore +from memu.database.vector_index.interfaces import VectorIndex def build_inmemory_database( *, config: DatabaseConfig, user_model: type[BaseModel], + vector_index: VectorIndex | None = None, ) -> InMemoryStore: resource_model, memory_category_model, memory_item_model, category_item_model = build_inmemory_models(user_model) return InMemoryStore( @@ -19,6 +21,7 @@ def build_inmemory_database( memory_item_model=memory_item_model, memory_category_model=memory_category_model, category_item_model=category_item_model, + vector_index=vector_index, ) diff --git a/src/memu/database/inmemory/repo.py b/src/memu/database/inmemory/repo.py index 44275f9f..d1ceaad6 100644 --- a/src/memu/database/inmemory/repo.py +++ b/src/memu/database/inmemory/repo.py @@ -15,6 +15,7 @@ from memu.database.interfaces import Database from memu.database.models import CategoryItem, MemoryCategory, MemoryItem, Resource from memu.database.repositories import MemoryCategoryRepo, ResourceRepo +from memu.database.vector_index.interfaces import VectorIndex class InMemoryStore(Database): @@ -27,6 +28,7 @@ def __init__( memory_category_model: type[Any] | None = None, category_item_model: type[Any] | None = None, state: InMemoryState | None = None, + vector_index: VectorIndex | None = None, ) -> None: self.scope_model = scope_model or BaseModel ( @@ -47,14 +49,21 @@ def __init__( memory_category_model = memory_category_model or default_memory_category_model or MemoryCategory category_item_model = category_item_model or default_category_item_model or CategoryItem + self._vector_index = vector_index + self.resource_repo: ResourceRepo = InMemoryResourceRepository(state=self.state, resource_model=resource_model) self.memory_category_repo: MemoryCategoryRepo = InMemoryMemoryCategoryRepository( state=self.state, memory_category_model=memory_category_model ) - self.memory_item_repo = InMemoryMemoryItemRepository(state=self.state, memory_item_model=memory_item_model) + self.memory_item_repo = InMemoryMemoryItemRepository( + state=self.state, + memory_item_model=memory_item_model, + vector_index=vector_index, + ) self.category_item_repo = InMemoryCategoryItemRepository( state=self.state, category_item_model=category_item_model ) def close(self) -> None: - return None + if self._vector_index is not None: + self._vector_index.close() diff --git a/src/memu/database/inmemory/repositories/memory_item_repo.py b/src/memu/database/inmemory/repositories/memory_item_repo.py index da28e14f..8480e14f 100644 --- a/src/memu/database/inmemory/repositories/memory_item_repo.py +++ b/src/memu/database/inmemory/repositories/memory_item_repo.py @@ -11,13 +11,39 @@ from memu.database.inmemory.vector import cosine_topk, cosine_topk_salience from memu.database.models import MemoryItem, MemoryType, compute_content_hash from memu.database.repositories.memory_item import MemoryItemRepo +from memu.database.vector_index.interfaces import VectorIndex + +# Fields that are part of the item record itself and must not be replicated +# into the external vector index as dynamic scope fields. +_NON_SCOPE_FIELDS = frozenset({ + "id", + "resource_id", + "memory_type", + "summary", + "embedding", + "extra", + "created_at", + "updated_at", +}) + + +def _extract_scope(item: MemoryItem) -> dict[str, Any]: + data = item.model_dump() if hasattr(item, "model_dump") else dict(item.__dict__) + return {k: v for k, v in data.items() if k not in _NON_SCOPE_FIELDS} class InMemoryMemoryItemRepository(MemoryItemRepo): - def __init__(self, *, state: InMemoryState, memory_item_model: type[MemoryItem]) -> None: + def __init__( + self, + *, + state: InMemoryState, + memory_item_model: type[MemoryItem], + vector_index: VectorIndex | None = None, + ) -> None: self._state = state self.memory_item_model = memory_item_model self.items: dict[str, MemoryItem] = self._state.items + self._vector_index = vector_index def list_items(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryItem]: if not where: @@ -54,9 +80,13 @@ def clear_items(self, where: Mapping[str, Any] | None = None) -> dict[str, Memor if not where: matches = self.items.copy() self.items.clear() + if self._vector_index is not None and matches: + self._vector_index.delete_many(matches.keys()) return matches matches = {mid: item for mid, item in self.items.items() if matches_where(item, where)} self.items = {mid: item for mid, item in self.items.items() if mid not in matches} + if self._vector_index is not None and matches: + self._vector_index.delete_many(matches.keys()) return matches def _find_by_hash(self, content_hash: str, user_data: dict[str, Any]) -> MemoryItem | None: @@ -117,6 +147,8 @@ def create_item( **user_data, ) self.items[mid] = it + if self._vector_index is not None and embedding: + self._vector_index.upsert(mid, embedding, scope=user_data) return it def create_item_reinforce( @@ -164,6 +196,8 @@ def create_item_reinforce( **user_data, ) self.items[mid] = it + if self._vector_index is not None and embedding: + self._vector_index.upsert(mid, embedding, scope=user_data) return it def vector_search_items( @@ -175,6 +209,12 @@ def vector_search_items( ranking: str = "similarity", recency_decay_days: float = 30.0, ) -> list[tuple[str, float]]: + # Salience ranking requires per-item reinforcement/recency factors that + # an external vector index cannot score directly, so fall through to the + # in-memory path for that mode. + if self._vector_index is not None and ranking != "salience": + return self._vector_index.search(query_vec, top_k, where=where) + pool = self.list_items(where) if ranking == "salience": @@ -219,6 +259,8 @@ def _parse_datetime(dt_str: str | None) -> pendulum.DateTime | None: def delete_item(self, item_id: str) -> None: if item_id in self.items: del self.items[item_id] + if self._vector_index is not None: + self._vector_index.delete(item_id) @override def update_item( @@ -256,7 +298,18 @@ def update_item( item.extra = current_extra self.items[item_id] = item + self._sync_vector_index_on_update(item_id, item, embedding) return item + def _sync_vector_index_on_update( + self, + item_id: str, + item: MemoryItem, + embedding: list[float] | None, + ) -> None: + if self._vector_index is None or embedding is None: + return + self._vector_index.upsert(item_id, embedding, scope=_extract_scope(item)) + __all__ = ["InMemoryMemoryItemRepository"] diff --git a/src/memu/database/vector_index/__init__.py b/src/memu/database/vector_index/__init__.py new file mode 100644 index 00000000..0f8e823a --- /dev/null +++ b/src/memu/database/vector_index/__init__.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from memu.app.settings import VectorIndexConfig +from memu.database.vector_index.interfaces import VectorIndex + + +def build_vector_index(config: VectorIndexConfig | None) -> VectorIndex | None: + """Construct an external VectorIndex instance if configured. + + Returns None for providers that do not require a separate index + (``bruteforce``, ``pgvector``, ``none``) because those are handled + inside the metadata backend itself. + """ + if config is None: + return None + provider = config.provider + if provider == "milvus": + from memu.database.vector_index.milvus import MilvusVectorIndex + + return MilvusVectorIndex( + uri=config.uri or "./milvus.db", + token=config.token, + collection_name=config.collection_name, + dim=config.dim, + ) + return None + + +__all__ = ["VectorIndex", "build_vector_index"] diff --git a/src/memu/database/vector_index/interfaces.py b/src/memu/database/vector_index/interfaces.py new file mode 100644 index 00000000..f42800d1 --- /dev/null +++ b/src/memu/database/vector_index/interfaces.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from typing import Any, Protocol, runtime_checkable + + +@runtime_checkable +class VectorIndex(Protocol): + """Backend-agnostic external vector index contract. + + Implementations keep vectors synchronized with the metadata store: + writes (``upsert`` / ``delete``) mirror mutations on memory items, + and ``search`` replaces the metadata backend's own top-k computation. + Filtering uses the same scope keys (``user_id``, ``agent_id``, ...) the + metadata store uses in its ``where`` clauses. + """ + + def upsert( + self, + item_id: str, + vector: list[float], + scope: Mapping[str, Any] | None = None, + ) -> None: ... + + def delete(self, item_id: str) -> None: ... + + def delete_many(self, item_ids: Iterable[str]) -> None: ... + + def search( + self, + query_vec: list[float], + top_k: int, + where: Mapping[str, Any] | None = None, + ) -> list[tuple[str, float]]: ... + + def close(self) -> None: ... + + +__all__ = ["VectorIndex"] diff --git a/src/memu/database/vector_index/milvus.py b/src/memu/database/vector_index/milvus.py new file mode 100644 index 00000000..f73c9acb --- /dev/null +++ b/src/memu/database/vector_index/milvus.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +import logging +from collections.abc import Iterable, Mapping +from threading import Lock +from typing import Any + +from memu.database.vector_index.interfaces import VectorIndex + +logger = logging.getLogger(__name__) + +_ID_FIELD = "id" +_VECTOR_FIELD = "vector" +_DEFAULT_ID_MAX_LENGTH = 128 + + +def _format_scalar(value: Any) -> str | None: + """Render a Python value as a Milvus boolean-expression literal. + + Returns ``None`` when the value cannot be used in a scalar filter + (e.g. nested dicts), in which case the calling search will fall back + to unfiltered retrieval for that key. + """ + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + if isinstance(value, str): + escaped = value.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + return None + + +def _build_filter_expr(where: Mapping[str, Any] | None) -> str: + if not where: + return "" + parts: list[str] = [] + for key, value in where.items(): + literal = _format_scalar(value) + if literal is None: + logger.debug("Skipping unsupported filter key %s=%r for Milvus", key, value) + continue + parts.append(f"{key} == {literal}") + return " and ".join(parts) + + +class MilvusVectorIndex(VectorIndex): + """Milvus implementation of the VectorIndex protocol. + + Uses ``MilvusClient`` for all operations. The collection is created on + first ``upsert`` once the embedding dimension is known (unless ``dim`` + is provided explicitly). Scope fields supplied via ``scope`` are stored + as dynamic fields so they can be used as filter expressions at search + time. + + The default ``uri="./milvus.db"`` runs Milvus Lite with zero external + dependencies; point ``uri`` at a Milvus server or Zilliz Cloud endpoint + for larger deployments. + """ + + def __init__( + self, + *, + uri: str = "./milvus.db", + token: str | None = None, + collection_name: str = "memu_memory_items", + dim: int | None = None, + ) -> None: + try: + from pymilvus import MilvusClient + except ImportError as e: + msg = ( + "pymilvus is required for the Milvus vector index. " + "Install it with: uv add pymilvus (or: pip install pymilvus)" + ) + raise ImportError(msg) from e + + self._uri = uri + self._token = token + self._collection_name = collection_name + self._dim = dim + self._client = MilvusClient(uri=uri, token=token) if token else MilvusClient(uri=uri) + self._lock = Lock() + self._ready = False + + if dim is not None: + self._ensure_collection(dim) + + def _ensure_collection(self, dim: int) -> None: + with self._lock: + if self._ready: + return + if self._client.has_collection(collection_name=self._collection_name): + self._ready = True + self._dim = dim + return + + from pymilvus import DataType + + schema = self._client.create_schema( + auto_id=False, + enable_dynamic_field=True, + ) + schema.add_field( + field_name=_ID_FIELD, + datatype=DataType.VARCHAR, + is_primary=True, + max_length=_DEFAULT_ID_MAX_LENGTH, + ) + schema.add_field( + field_name=_VECTOR_FIELD, + datatype=DataType.FLOAT_VECTOR, + dim=dim, + ) + + index_params = self._client.prepare_index_params() + index_params.add_index( + field_name=_VECTOR_FIELD, + index_type="AUTOINDEX", + metric_type="COSINE", + ) + + self._client.create_collection( + collection_name=self._collection_name, + schema=schema, + index_params=index_params, + ) + self._dim = dim + self._ready = True + + def upsert( + self, + item_id: str, + vector: list[float], + scope: Mapping[str, Any] | None = None, + ) -> None: + if not vector: + logger.debug("Skipping Milvus upsert for %s: empty vector", item_id) + return + self._ensure_collection(len(vector)) + + data: dict[str, Any] = {_ID_FIELD: item_id, _VECTOR_FIELD: list(vector)} + if scope: + for key, value in scope.items(): + if key in (_ID_FIELD, _VECTOR_FIELD): + continue + # Only persist scalar scope values; complex types are ignored + # since they cannot be used in Milvus filter expressions. + if isinstance(value, (str, int, float, bool)) or value is None: + data[key] = value + + self._client.upsert(collection_name=self._collection_name, data=[data]) + + def delete(self, item_id: str) -> None: + if not self._ready: + return + self._client.delete( + collection_name=self._collection_name, + filter=f'{_ID_FIELD} == "{item_id}"', + ) + + def delete_many(self, item_ids: Iterable[str]) -> None: + ids = list(item_ids) + if not ids or not self._ready: + return + quoted = ", ".join(f'"{i}"' for i in ids) + self._client.delete( + collection_name=self._collection_name, + filter=f"{_ID_FIELD} in [{quoted}]", + ) + + def search( + self, + query_vec: list[float], + top_k: int, + where: Mapping[str, Any] | None = None, + ) -> list[tuple[str, float]]: + if not self._ready or top_k <= 0 or not query_vec: + return [] + + expr = _build_filter_expr(where) + results = self._client.search( + collection_name=self._collection_name, + data=[list(query_vec)], + limit=top_k, + filter=expr or None, + output_fields=[_ID_FIELD], + search_params={"metric_type": "COSINE"}, + ) + if not results: + return [] + + hits = results[0] + scored: list[tuple[str, float]] = [] + for hit in hits: + # MilvusClient returns hits as dicts with ``id`` and ``distance`` keys. + # For COSINE, distance is cosine similarity (higher = better). + hit_id = hit.get("id") if isinstance(hit, dict) else getattr(hit, "id", None) + raw_score = hit.get("distance") if isinstance(hit, dict) else getattr(hit, "distance", 0.0) + if hit_id is None: + continue + scored.append((str(hit_id), float(raw_score if raw_score is not None else 0.0))) + return scored + + def close(self) -> None: + with self._lock: + close = getattr(self._client, "close", None) + if callable(close): + try: + close() + except Exception: + logger.debug("MilvusClient.close raised; ignoring", exc_info=True) + self._ready = False + + +__all__ = ["MilvusVectorIndex"] diff --git a/tests/test_milvus_vector_index.py b/tests/test_milvus_vector_index.py new file mode 100644 index 00000000..cf2fb352 --- /dev/null +++ b/tests/test_milvus_vector_index.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import math +from pathlib import Path + +import pytest +from pydantic import BaseModel + +pytest.importorskip("pymilvus") + +from memu.app.settings import DatabaseConfig +from memu.database.factory import build_database +from memu.database.vector_index.milvus import MilvusVectorIndex + + +class _UserScope(BaseModel): + user_id: str = "" + agent_id: str = "" + + +def _unit_vec(values: list[float]) -> list[float]: + norm = math.sqrt(sum(v * v for v in values)) or 1.0 + return [v / norm for v in values] + + +def _make_index(tmp_path: Path, name: str = "memu_test") -> MilvusVectorIndex: + return MilvusVectorIndex( + uri=str(tmp_path / "milvus.db"), + collection_name=name, + ) + + +def test_milvus_vector_index_upsert_and_search(tmp_path: Path) -> None: + index = _make_index(tmp_path) + try: + index.upsert("a", _unit_vec([1.0, 0.0, 0.0]), scope={"user_id": "u1"}) + index.upsert("b", _unit_vec([0.0, 1.0, 0.0]), scope={"user_id": "u1"}) + index.upsert("c", _unit_vec([0.0, 0.0, 1.0]), scope={"user_id": "u2"}) + + hits = index.search(_unit_vec([1.0, 0.0, 0.0]), top_k=2) + top_id, top_score = hits[0] + assert top_id == "a" + assert top_score > 0.99 + assert len(hits) == 2 + finally: + index.close() + + +def test_milvus_vector_index_scope_filter(tmp_path: Path) -> None: + index = _make_index(tmp_path, name="memu_scope") + try: + index.upsert("a", _unit_vec([1.0, 0.0, 0.0]), scope={"user_id": "u1"}) + index.upsert("b", _unit_vec([1.0, 0.1, 0.0]), scope={"user_id": "u2"}) + + hits = index.search(_unit_vec([1.0, 0.0, 0.0]), top_k=5, where={"user_id": "u2"}) + assert [hid for hid, _ in hits] == ["b"] + finally: + index.close() + + +def test_milvus_vector_index_delete_and_delete_many(tmp_path: Path) -> None: + index = _make_index(tmp_path, name="memu_delete") + try: + index.upsert("a", _unit_vec([1.0, 0.0, 0.0])) + index.upsert("b", _unit_vec([0.0, 1.0, 0.0])) + index.upsert("c", _unit_vec([0.0, 0.0, 1.0])) + + index.delete("a") + hits = index.search(_unit_vec([1.0, 0.0, 0.0]), top_k=5) + assert "a" not in {hid for hid, _ in hits} + + index.delete_many(["b", "c"]) + hits = index.search(_unit_vec([1.0, 0.0, 0.0]), top_k=5) + assert hits == [] + finally: + index.close() + + +def test_inmemory_backend_routes_search_through_milvus(tmp_path: Path) -> None: + config = DatabaseConfig.model_validate({ + "metadata_store": {"provider": "inmemory"}, + "vector_index": { + "provider": "milvus", + "uri": str(tmp_path / "milvus.db"), + "collection_name": "memu_e2e", + }, + }) + db = build_database(config=config, user_model=_UserScope) + try: + repo = db.memory_item_repo + + alpha = repo.create_item( + resource_id="r1", + memory_type="profile", + summary="alpha", + embedding=_unit_vec([1.0, 0.0, 0.0]), + user_data={"user_id": "u1", "agent_id": "a1"}, + ) + beta = repo.create_item( + resource_id="r1", + memory_type="profile", + summary="beta", + embedding=_unit_vec([0.0, 1.0, 0.0]), + user_data={"user_id": "u1", "agent_id": "a1"}, + ) + gamma = repo.create_item( + resource_id="r1", + memory_type="profile", + summary="gamma", + embedding=_unit_vec([0.0, 0.0, 1.0]), + user_data={"user_id": "u2", "agent_id": "a1"}, + ) + + # Search scoped to u1: should only return alpha/beta, alpha first. + hits = repo.vector_search_items( + query_vec=_unit_vec([1.0, 0.0, 0.0]), + top_k=5, + where={"user_id": "u1"}, + ) + ids = [hid for hid, _ in hits] + assert ids[0] == alpha.id + assert set(ids) == {alpha.id, beta.id} + assert gamma.id not in ids + + # Delete propagates to Milvus. + repo.delete_item(alpha.id) + hits = repo.vector_search_items( + query_vec=_unit_vec([1.0, 0.0, 0.0]), + top_k=5, + where={"user_id": "u1"}, + ) + assert alpha.id not in {hid for hid, _ in hits} + + # Update with a new embedding re-upserts. + repo.update_item(item_id=beta.id, embedding=_unit_vec([1.0, 0.0, 0.0])) + hits = repo.vector_search_items( + query_vec=_unit_vec([1.0, 0.0, 0.0]), + top_k=5, + where={"user_id": "u1"}, + ) + assert hits and hits[0][0] == beta.id + finally: + db.close() + + +def test_inmemory_backend_clear_items_propagates_to_milvus(tmp_path: Path) -> None: + config = DatabaseConfig.model_validate({ + "metadata_store": {"provider": "inmemory"}, + "vector_index": { + "provider": "milvus", + "uri": str(tmp_path / "milvus.db"), + "collection_name": "memu_clear", + }, + }) + db = build_database(config=config, user_model=_UserScope) + try: + repo = db.memory_item_repo + for i in range(3): + repo.create_item( + resource_id="r1", + memory_type="profile", + summary=f"item-{i}", + embedding=_unit_vec([1.0, float(i), 0.0]), + user_data={"user_id": "u1", "agent_id": "a1"}, + ) + + repo.clear_items(where={"user_id": "u1"}) + hits = repo.vector_search_items( + query_vec=_unit_vec([1.0, 0.0, 0.0]), + top_k=5, + where={"user_id": "u1"}, + ) + assert hits == [] + finally: + db.close() diff --git a/uv.lock b/uv.lock index 76e7b0c6..60046782 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,14 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.13" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] [[package]] name = "alembic" @@ -79,6 +87,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, ] +[[package]] +name = "cachetools" +version = "7.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, +] + [[package]] name = "certifi" version = "2025.10.5" @@ -458,6 +475,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, @@ -465,6 +483,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, @@ -472,6 +491,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, @@ -489,6 +509,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, ] +[[package]] +name = "grpcio" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -929,7 +980,7 @@ wheels = [ [[package]] name = "memu-py" -version = "1.5.0" +version = "1.5.1" source = { editable = "." } dependencies = [ { name = "alembic" }, @@ -952,6 +1003,11 @@ langgraph = [ { name = "langchain-core" }, { name = "langgraph" }, ] +milvus = [ + { name = "milvus-lite" }, + { name = "pymilvus" }, + { name = "setuptools" }, +] postgres = [ { name = "pgvector" }, { name = "sqlalchemy", extra = ["postgresql-psycopgbinary"] }, @@ -1003,15 +1059,18 @@ requires-dist = [ { name = "langchain-core", marker = "extra == 'langgraph'", specifier = ">=0.1.0" }, { name = "langgraph", marker = "extra == 'langgraph'", specifier = ">=0.0.10" }, { name = "lazyllm", specifier = ">=0.7.3" }, + { name = "milvus-lite", marker = "extra == 'milvus'", specifier = ">=2.4.10" }, { name = "numpy", specifier = ">=2.3.4" }, { name = "openai", specifier = ">=2.8.0" }, { name = "pendulum", specifier = ">=3.1.0" }, { name = "pgvector", marker = "extra == 'postgres'", specifier = ">=0.3.4" }, { name = "pydantic", specifier = ">=2.12.4" }, + { name = "pymilvus", marker = "extra == 'milvus'", specifier = ">=2.5.0" }, + { name = "setuptools", marker = "python_full_version >= '3.13' and extra == 'milvus'", specifier = "<81" }, { name = "sqlalchemy", extras = ["postgresql-psycopgbinary"], marker = "extra == 'postgres'", specifier = ">=2.0.36" }, { name = "sqlmodel", specifier = ">=0.0.27" }, ] -provides-extras = ["postgres", "langgraph", "claude"] +provides-extras = ["postgres", "milvus", "langgraph", "claude"] [package.metadata.requires-dev] dev = [ @@ -1058,6 +1117,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] +[[package]] +name = "milvus-lite" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/b2/acc5024c8e8b6a0b034670b8e8af306ebd633ede777dcbf557eac4785937/milvus_lite-2.5.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6b014453200ba977be37ba660cb2d021030375fa6a35bc53c2e1d92980a0c512", size = 27934713, upload-time = "2025-06-30T04:23:37.028Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2e/746f5bb1d6facd1e73eb4af6dd5efda11125b0f29d7908a097485ca6cad9/milvus_lite-2.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2e031088bf308afe5f8567850412d618cfb05a65238ed1a6117f60decccc95a", size = 24421451, upload-time = "2025-06-30T04:23:51.747Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/3d1fee5c16c7661cf53977067a34820f7269ed8ba99fe9cf35efc1700866/milvus_lite-2.5.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:a13277e9bacc6933dea172e42231f7e6135bd3bdb073dd2688ee180418abd8d9", size = 45337093, upload-time = "2025-06-30T04:24:06.706Z" }, + { url = "https://files.pythonhosted.org/packages/d3/82/41d9b80f09b82e066894d9b508af07b7b0fa325ce0322980674de49106a0/milvus_lite-2.5.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25ce13f4b8d46876dd2b7ac8563d7d8306da7ff3999bb0d14b116b30f71d706c", size = 55263911, upload-time = "2025-06-30T04:24:19.434Z" }, +] + [[package]] name = "mkdocs" version = "1.6.1" @@ -1381,6 +1454,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, + { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, + { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, + { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, + { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -1459,6 +1576,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, ] +[[package]] +name = "protobuf" +version = "7.34.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, + { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, +] + [[package]] name = "psutil" version = "6.1.1" @@ -1647,6 +1779,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, ] +[[package]] +name = "pymilvus" +version = "2.6.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "grpcio" }, + { name = "orjson" }, + { name = "pandas" }, + { name = "protobuf" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/d7/c5d1381248a33975ccc864a0f980f93270ecc35354de8646c8a16443cccb/pymilvus-2.6.12.tar.gz", hash = "sha256:8323e990dc305e607fef525498eb779e42940a69e0691dde009cd02d48845f7a", size = 1584521, upload-time = "2026-04-09T07:49:11.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/5d/44b0fa94c91503381e6f12298277f84f8e7b0bb00715ab89fc273c4d681e/pymilvus-2.6.12-py3-none-any.whl", hash = "sha256:69051b8b62712f157b2b50aeb7bde7fd7cdb5940aac0122094eb3cd58bc20f0d", size = 315183, upload-time = "2026-04-09T07:49:09.013Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -1996,6 +2147,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/bd/4168a751ddbbf43e86544b4de8b5c3b7be8d7167a2a5cb977d274e04f0a1/ruff-0.14.4-py3-none-win_arm64.whl", hash = "sha256:dd09c292479596b0e6fec8cd95c65c3a6dc68e9ad17b8f2382130f87ff6a75bb", size = 12663065, upload-time = "2025-11-06T22:07:42.603Z" }, ] +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + [[package]] name = "six" version = "1.17.0"