Skip to content

Pr 49 milvus#52

Merged
laxmanclo merged 5 commits into
mainfrom
pr-49-milvus
Apr 13, 2026
Merged

Pr 49 milvus#52
laxmanclo merged 5 commits into
mainfrom
pr-49-milvus

Conversation

@laxmanclo

@laxmanclo laxmanclo commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

I noticed that the cloud usage via API keys wasn't supported yet, so I went ahead and added support for Milvus / Zilliz Cloud authentication directly to this branch.

What I added:

Added milvus_token to VektoriConfig which gets passed directly down to both the async and sync MilvusClients.
Enabled CLI support via MILVUS_TOKEN or MILVUS_API_KEY environment variables.
Included unit tests to verify the factory passes the token correctly.
Updated the README.md with examples of how to connect to Zilliz Cloud using the new token configuration.

Summary by CodeRabbit

  • New Features

    • Added Milvus as a production-ready vector-store backend option
    • Extended Docker setup to support Milvus with automatic service orchestration
  • Documentation

    • Updated README with Milvus installation, configuration, and usage examples
    • Added example demonstrating Milvus integration with local models
  • Configuration

    • New optional dependency: vektori[milvus]
    • Support for Milvus authentication tokens via environment variables

snigenigmatic and others added 5 commits April 11, 2026 18:41
add MilvusBackend with facts/sentences/episodes collections and user partition-key isolation

wire Milvus into storage resolution (storage_backend='milvus' and :19530 URL heuristic)

add Milvus support to config/CLI/docs and Docker Compose local stack (etcd, minio, milvus)

add optional dependency extras for Milvus (pymilvus)

add unit + integration + e2e tests for Milvus backend behavior

harden Milvus operations for reliability (nullable string normalization, async loop fallback, read-after-write flush)

include Ollama+Milvus example as sanity-check demo (non-core to backend feature)
…nstant

- make _utcnow_iso return UTC-aware ISO timestamps

- catch RuntimeError for async loop mismatch and fall back to sync MilvusClient

- stop swallowing _delete failures in upsert fallback (warn on unsupported delete, re-raise others)

- replace hardcoded episode filter with _EPISODE_RECORD
  - Scope session markers by user_id to prevent cross-tenant collisions
  - Add user_id filter to session sentence queries in get_session
  - Use compare-and-swap retry loop in insert_fact_sources and
    insert_episode_fact to prevent last-writer-wins races
  - Remove turn_number equality constraint from expand_session_context
    so the window spans adjacent turns
  - Narrow _upsert_rows fallback to only trigger on unsupported-upsert
    errors instead of catching all exceptions
  - Validate sentences/embeddings length match before zip in
    upsert_sentences
  - Fix _call to catch pymilvus-wrapped RuntimeError on event loop
    mismatch
- Added `milvus_token` to `VektoriConfig` and `Vektori` client initialization
- Passed token to both `MilvusClient` and `AsyncMilvusClient`
- Added CLI support for `MILVUS_TOKEN` / `MILVUS_API_KEY`
- Updated README with Cloud connection examples
- Added unit tests for factory token parsing
@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds comprehensive Milvus vector database backend support to Vektori, including a full backend implementation with collection schema management, query/filtering logic, configuration wiring, Docker infrastructure, and extensive unit/integration/e2e test coverage alongside documentation and usage examples.

Changes

Cohort / File(s) Summary
Core Backend Implementation
vektori/storage/milvus.py
New MilvusBackend class (1667 lines) implementing StorageBackend interface with 3-collection architecture (sentences, facts, episodes), partition-key isolation via user_id, schema initialization with cosine similarity indexing, client fallback handling (async→sync), filtered query/search with distance translation, upsert deduplication/flush, embedder integration, and specialized query helpers for facts, episodes, sessions, edges, and GDPR user deletion.
Storage Factory Routing
vektori/storage/factory.py
Extended create_storage factory with milvus branch: detects storage_backend="milvus" and Milvus URL patterns (:19530, http), defaults to http://localhost:19530, routes config.milvus_token and embedding_dimension to new backend, and initializes via standard async flow.
Config & Client Wiring
vektori/config.py, vektori/client.py
Added milvus_token field to VektoriConfig for authentication; updated Vektori.__init__ to accept and forward milvus_token; added conditional set_sentence_embedder call during initialization for storages that support embedder injection.
CLI Support
vektori/cli.py
Extended storage backend help text to include milvus; updated database URL help with Milvus examples (http://host:19530, https://<cluster-endpoint>); wired milvus_token from MILVUS_TOKEN/MILVUS_API_KEY environment variables; reformatted and expanded config command docstring with Milvus example.
Unit & Integration Tests
tests/unit/test_milvus_backend.py, tests/unit/test_new_backends_factory.py, tests/integration/test_milvus_backend.py, tests/integration/test_milvus_e2e.py
Added comprehensive test coverage: unit tests for filter expressions, metadata conversion, deduplication logic, and "fail-closed" error handling; factory routing tests for config→backend selection and token propagation; integration tests (6 test cases) verifying sentences, facts, sessions, episodes workflows against live Milvus server; e2e tests exercising ingestion + retrieval pipeline with mocked embedder/LLM.
Documentation & Infrastructure
README.md, docker-compose.yml, pyproject.toml, examples/ollama_milvus_session_demo.py
Updated README with Milvus in backend list, added vektori[milvus] and vektori[neo4j,qdrant,milvus] install options, added Milvus configuration examples, and Docker compose section; added milvus, minio, etcd services to docker-compose with healthchecks and dependency ordering; extended pyproject extras with pymilvus>=2.5.3; added runnable example demonstrating local Vektori+Ollama+Milvus workflow with session/search operations.

Sequence Diagrams

sequenceDiagram
    actor User
    participant Vektori as Vektori Client
    participant Embedder
    participant MilvusBackend
    participant Milvus as Milvus Server
    
    User->>Vektori: add(messages, session_id, user_id)
    activate Vektori
    
    Vektori->>Embedder: embed(text) for each message
    Embedder-->>Vektori: embedding vectors
    
    Vektori->>MilvusBackend: upsert_sentences(sentences, embeddings, user_id)
    activate MilvusBackend
    
    MilvusBackend->>Milvus: upsert into sentences collection
    Milvus-->>MilvusBackend: ack
    
    MilvusBackend->>Milvus: flush (make visible)
    Milvus-->>MilvusBackend: flushed
    
    MilvusBackend-->>Vektori: count
    deactivate MilvusBackend
    
    Vektori-->>User: result (ok, sentence_ids)
    deactivate Vektori
Loading
sequenceDiagram
    actor User
    participant Vektori as Vektori Client
    participant Embedder
    participant MilvusBackend
    participant Milvus as Milvus Server
    
    User->>Vektori: search(query, depth=l1, user_id)
    activate Vektori
    
    Vektori->>Embedder: embed(query)
    Embedder-->>Vektori: query_embedding
    
    Vektori->>MilvusBackend: search_sentences(embedding, user_id, limit=10)
    activate MilvusBackend
    
    MilvusBackend->>Milvus: vector search (cosine) in sentences collection
    Milvus-->>MilvusBackend: results with distance scores
    
    MilvusBackend->>MilvusBackend: map distance to result dict
    MilvusBackend-->>Vektori: sentence_results
    deactivate MilvusBackend
    
    alt depth == l1
        Vektori->>MilvusBackend: search_facts(embedding, user_id)
        activate MilvusBackend
        MilvusBackend->>Milvus: vector search + filter (active, temporal constraints)
        Milvus-->>MilvusBackend: fact results
        MilvusBackend-->>Vektori: fact_results
        deactivate MilvusBackend
    end
    
    Vektori-->>User: {sentences, facts, episodes}
    deactivate Vektori
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related issues

  • vektori#44 — Implements the same Milvus backend feature, directly addressing the objective to add Milvus as a production storage backend.

Possibly related PRs

  • PR #48 — Adds a parallel new storage backend with similar structural changes (backend module, factory routing, config/client/CLI updates, docker-compose services, pyproject extras, tests, and documentation).
  • PR #9 — Refactors L1 "insights" layer to "episodes" API; this PR's MilvusBackend directly implements the episodes interface (insert\_episode, get\_episodes\_for\_facts, search\_episodes).
  • PR #3 — Core client/factory initialization refactor; this PR's changes to vektori/client.py and vektori/storage/factory.py extend the same plumbing for storage construction and embedder injection.

Poem

🐰 Hops around the Milvus collections with glee,
Three gardens for facts, sentences, and episodes—so free!
With partition-key fences to keep users apart,
Vector searches dance where the cosines impart,
Sessions bloom bright in this distributed heart!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Pr 49 milvus' is vague and uses informal naming conventions (PR number prefix); it does not clearly describe the feature being added (Milvus backend support). Consider a more descriptive title such as 'Add Milvus vector database backend support' or 'Implement Milvus storage backend with API token authentication'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-49-milvus

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@laxmanclo

Copy link
Copy Markdown
Contributor Author

@snigenigmatic Let me know what you think!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
README.md (1)

262-264: Consider documenting both Milvus env var aliases in the cloud example.

CLI accepts both MILVUS_TOKEN and MILVUS_API_KEY; showing both here would reduce setup confusion.

📝 Suggested doc tweak
-# Milvus Cloud
-MILVUS_TOKEN=your-api-key VEKTORI_STORAGE_BACKEND=milvus VEKTORI_DATABASE_URL=https://your-cluster-endpoint vektori add "I prefer dark mode" --user-id u1
+# Milvus Cloud (either env var works)
+MILVUS_TOKEN=your-api-key VEKTORI_STORAGE_BACKEND=milvus VEKTORI_DATABASE_URL=https://your-cluster-endpoint vektori add "I prefer dark mode" --user-id u1
+# or
+MILVUS_API_KEY=your-api-key VEKTORI_STORAGE_BACKEND=milvus VEKTORI_DATABASE_URL=https://your-cluster-endpoint vektori add "I prefer dark mode" --user-id u1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 262 - 264, Update the Milvus Cloud example to mention
both accepted environment variable names by showing both MILVUS_TOKEN and
MILVUS_API_KEY (e.g., list both alternatives or include both in the example) and
clarify that the CLI accepts either; update the example command that uses
vektori add "I prefer dark mode" --user-id u1 to include both env var aliases so
readers know either MILVUS_TOKEN or MILVUS_API_KEY will work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 53-54: Update the README pip install line so the “all backends”
extras actually include Postgres: change the extras list referenced by the
package string 'vektori[neo4j,qdrant,milvus]' to include Postgres (e.g. add
'postgres') so the install command reflects all backends; update the string
where 'vektori[neo4j,qdrant,milvus]' appears.

In `@vektori/storage/milvus.py`:
- Around line 922-926: The session-local sentence filters currently only
constrain session_id and is_active which allows cross-tenant reads when
session_ids collide; update the filter building in the Milvus sentence read
paths (the filter_expr construction used by the code around the shown diff and
the similar blocks at the other locations you flagged) to include tenant scoping
(e.g., user_id and/or agent_id) by passing the tenant context into the helpers
or resolving it from the seed row (use expand_session_context() to obtain tenant
fields when needed) and then add conditions like user_id == "{...}" and/or
agent_id == "{...}" to the f-string filters so all reads (including
find_sentence_containing() and L2 expansion callers) are restricted to the same
tenant.
- Around line 1357-1396: The override of insert_episode drops the metadata
parameter and always writes an empty dict; restore the metadata parameter on
insert_episode to match the abstract StorageBackend.insert_episode signature,
accept something like metadata: dict | None, and persist the caller-supplied
metadata by setting row["metadata"] = self._json_value(metadata or {}) before
inserting; keep existing behavior for missing metadata, and leave episode_id
generation, the _EPISODE_RECORD value, and lookups against
_episodes_col/_episode_output_fields unchanged.
- Around line 1143-1164: The find_fact_by_text implementation returns a full
fact dict but the storage contract (base.py) requires returning a fact id or
None; update find_fact_by_text to query only the id field from self._facts_col
(use self._get_one with a projection that requests only the id column instead of
self._fact_output_fields()), and then return the id string (e.g. row["id"] or
the actual id key used in your rows) or None if no row; keep the same filter
construction (record_type/_escape/agent_id) and do not call self._to_fact().
- Around line 1523-1529: upsert_session() is overwriting an existing marker's
agent_id with an empty string when called with agent_id=None; change the upsert
logic so it preserves the stored agent_id instead of replacing it: when building
the row for insertion/update (the dict containing "id", "text", "embedding",
"user_id", "agent_id", "session_id"), if agent_id is None retrieve the existing
marker by marker_id (or read the current agent_id if available) and use that
value, otherwise use the provided agent_id; alternatively only set "agent_id" in
the row when agent_id is not None to avoid erasing the stored agent_id.
- Around line 824-845: The dedup logic uses generate_content_hash (based on
session_id/turn/text) and _get_one against self._sentences_col to find existing
sentence rows by content_hash only, causing cross-tenant collisions and
incorrect increment of mentions and overwrites of tenant fields; update the
lookup in the block that calls _get_one (and any subsequent upsert path using
sid/mentions) to include tenant scope (sent["user_id"] and/or sent["agent_id"])
in the query predicate so existing is only returned for the same tenant, and
ensure the upsert/mentions increment also uses those tenant fields when choosing
sid and writing back.

---

Nitpick comments:
In `@README.md`:
- Around line 262-264: Update the Milvus Cloud example to mention both accepted
environment variable names by showing both MILVUS_TOKEN and MILVUS_API_KEY
(e.g., list both alternatives or include both in the example) and clarify that
the CLI accepts either; update the example command that uses vektori add "I
prefer dark mode" --user-id u1 to include both env var aliases so readers know
either MILVUS_TOKEN or MILVUS_API_KEY will work.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ce9c26cb-4818-46d2-bdeb-4b628ef0230a

📥 Commits

Reviewing files that changed from the base of the PR and between 2500816 and 402960c.

📒 Files selected for processing (13)
  • README.md
  • docker-compose.yml
  • examples/ollama_milvus_session_demo.py
  • pyproject.toml
  • tests/integration/test_milvus_backend.py
  • tests/integration/test_milvus_e2e.py
  • tests/unit/test_milvus_backend.py
  • tests/unit/test_new_backends_factory.py
  • vektori/cli.py
  • vektori/client.py
  • vektori/config.py
  • vektori/storage/factory.py
  • vektori/storage/milvus.py

Comment thread README.md
Comment on lines +53 to 54
pip install 'vektori[neo4j,qdrant,milvus]' # all backends
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix “all backends” install command to include Postgres dependencies.

Line 53 currently labels vektori[neo4j,qdrant,milvus] as “all backends”, but it skips Postgres extras.

📌 Suggested doc fix
-pip install 'vektori[neo4j,qdrant,milvus]'  # all backends
+pip install 'vektori[all]'                  # all backends (Postgres + Neo4j + Qdrant + Milvus)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pip install 'vektori[neo4j,qdrant,milvus]' # all backends
```
pip install 'vektori[all]' # all backends (Postgres + Neo4j + Qdrant + Milvus)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 53 - 54, Update the README pip install line so the
“all backends” extras actually include Postgres: change the extras list
referenced by the package string 'vektori[neo4j,qdrant,milvus]' to include
Postgres (e.g. add 'postgres') so the install command reflects all backends;
update the string where 'vektori[neo4j,qdrant,milvus]' appears.

Comment thread vektori/storage/milvus.py
Comment on lines +824 to +845
content_hash = generate_content_hash(
sent["session_id"],
f"{sent['turn_number']}_{sent['sentence_index']}",
sent["text"],
)

existing = await self._get_one(
self._sentences_col,
(
f'record_type == "{_SENTENCE_RECORD}" and '
f'content_hash == "{_escape(content_hash)}"'
),
self._sentence_output_fields(),
)

if existing:
sid = existing.get("id")
mentions = int(existing.get("mentions") or 1) + 1
else:
sid = sent["id"]
mentions = 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Scope sentence deduplication by tenant.

content_hash here is derived from session/turn/text only, and the lookup reuses any matching row without checking user_id or agent_id. Two tenants reusing the same session id and utterance will merge into one record, increment mentions, and overwrite the original tenant fields on upsert.

Suggested fix
             existing = await self._get_one(
                 self._sentences_col,
                 (
                     f'record_type == "{_SENTENCE_RECORD}" and '
+                    f'user_id == "{_escape(user_id)}" and '
+                    f'agent_id == "{_escape(agent_id or "")}" and '
                     f'content_hash == "{_escape(content_hash)}"'
                 ),
                 self._sentence_output_fields(),
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vektori/storage/milvus.py` around lines 824 - 845, The dedup logic uses
generate_content_hash (based on session_id/turn/text) and _get_one against
self._sentences_col to find existing sentence rows by content_hash only, causing
cross-tenant collisions and incorrect increment of mentions and overwrites of
tenant fields; update the lookup in the block that calls _get_one (and any
subsequent upsert path using sid/mentions) to include tenant scope
(sent["user_id"] and/or sent["agent_id"]) in the query predicate so existing is
only returned for the same tenant, and ensure the upsert/mentions increment also
uses those tenant fields when choosing sid and writing back.

Comment thread vektori/storage/milvus.py
Comment on lines +922 to +926
filter_expr = (
f'record_type == "{_SENTENCE_RECORD}" and '
f'session_id == "{_escape(session_id)}" and '
"is_active == true"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add tenant scoping to session-local sentence reads.

These filters only constrain session_id (and find_sentence_containing() also skips is_active). If two users reuse the same session id, quote matching and L2 expansion can pull another tenant's sentences into the current result set. expand_session_context() can at least add the seed row's user_id/agent_id; the other helpers need the same tenant context passed in or resolved before querying.

Also applies to: 967-971, 997-999, 1234-1240

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vektori/storage/milvus.py` around lines 922 - 926, The session-local sentence
filters currently only constrain session_id and is_active which allows
cross-tenant reads when session_ids collide; update the filter building in the
Milvus sentence read paths (the filter_expr construction used by the code around
the shown diff and the similar blocks at the other locations you flagged) to
include tenant scoping (e.g., user_id and/or agent_id) by passing the tenant
context into the helpers or resolving it from the seed row (use
expand_session_context() to obtain tenant fields when needed) and then add
conditions like user_id == "{...}" and/or agent_id == "{...}" to the f-string
filters so all reads (including find_sentence_containing() and L2 expansion
callers) are restricted to the same tenant.

Comment thread vektori/storage/milvus.py
Comment on lines +1143 to +1164
async def find_fact_by_text(
self,
user_id: str,
text: str,
agent_id: str | None = None,
) -> dict[str, Any] | None:
parts = [
f'record_type == "{_FACT_RECORD}"',
f'user_id == "{_escape(user_id)}"',
"is_active == true",
f'text == "{_escape(text)}"',
]
if agent_id is not None:
parts.append(f'agent_id == "{_escape(agent_id)}"')

row = await self._get_one(
self._facts_col,
" and ".join(parts),
self._fact_output_fields(),
)
return self._to_fact(row) if row else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Match find_fact_by_text() to the storage contract.

vektori/storage/base.py declares this method as returning a fact id or None, but this implementation returns a full fact dict. Any caller that forwards the result into id-based APIs will now get the wrong type back.

Suggested fix
     async def find_fact_by_text(
         self,
         user_id: str,
         text: str,
         agent_id: str | None = None,
-    ) -> dict[str, Any] | None:
+    ) -> str | None:
@@
-        return self._to_fact(row) if row else None
+        return str(row["id"]) if row else None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def find_fact_by_text(
self,
user_id: str,
text: str,
agent_id: str | None = None,
) -> dict[str, Any] | None:
parts = [
f'record_type == "{_FACT_RECORD}"',
f'user_id == "{_escape(user_id)}"',
"is_active == true",
f'text == "{_escape(text)}"',
]
if agent_id is not None:
parts.append(f'agent_id == "{_escape(agent_id)}"')
row = await self._get_one(
self._facts_col,
" and ".join(parts),
self._fact_output_fields(),
)
return self._to_fact(row) if row else None
async def find_fact_by_text(
self,
user_id: str,
text: str,
agent_id: str | None = None,
) -> str | None:
parts = [
f'record_type == "{_FACT_RECORD}"',
f'user_id == "{_escape(user_id)}"',
"is_active == true",
f'text == "{_escape(text)}"',
]
if agent_id is not None:
parts.append(f'agent_id == "{_escape(agent_id)}"')
row = await self._get_one(
self._facts_col,
" and ".join(parts),
self._fact_output_fields(),
)
return str(row["id"]) if row else None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vektori/storage/milvus.py` around lines 1143 - 1164, The find_fact_by_text
implementation returns a full fact dict but the storage contract (base.py)
requires returning a fact id or None; update find_fact_by_text to query only the
id field from self._facts_col (use self._get_one with a projection that requests
only the id column instead of self._fact_output_fields()), and then return the
id string (e.g. row["id"] or the actual id key used in your rows) or None if no
row; keep the same filter construction (record_type/_escape/agent_id) and do not
call self._to_fact().

Comment thread vektori/storage/milvus.py
Comment on lines +1357 to +1396
async def insert_episode(
self,
text: str,
embedding: list[float],
user_id: str,
agent_id: str | None = None,
session_id: str | None = None,
) -> str:
if self.embedding_dim and len(embedding) != self.embedding_dim:
raise ValueError(
f"Embedding has {len(embedding)} dimensions, expected {self.embedding_dim}"
)

agent_scope = agent_id or ""
episode_id = str(uuid.uuid5(uuid.NAMESPACE_OID, f"{user_id}::{agent_scope}::{text}"))
existing = await self._get_one(
self._episodes_col,
(
f'id == "{_escape(episode_id)}" and '
f'record_type == "{_EPISODE_RECORD}" and '
f'agent_id == "{_escape(agent_scope)}"'
),
self._episode_output_fields(),
)
if existing:
return episode_id

row = {
"id": episode_id,
"text": text,
"embedding": embedding,
"user_id": user_id,
"agent_id": agent_id or "",
"session_id": session_id or "",
"is_active": True,
"created_at": _utcnow_iso(),
"fact_ids": "",
"record_type": _EPISODE_RECORD,
"metadata": self._json_value({}),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restore the metadata parameter on insert_episode().

The abstract StorageBackend.insert_episode() signature includes metadata, but this override drops it and always persists {}. That breaks the backend contract and loses caller-supplied episode metadata.

Suggested fix
     async def insert_episode(
         self,
         text: str,
         embedding: list[float],
         user_id: str,
         agent_id: str | None = None,
         session_id: str | None = None,
+        metadata: dict[str, Any] | None = None,
     ) -> str:
@@
-            "metadata": self._json_value({}),
+            "metadata": self._json_value(metadata),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def insert_episode(
self,
text: str,
embedding: list[float],
user_id: str,
agent_id: str | None = None,
session_id: str | None = None,
) -> str:
if self.embedding_dim and len(embedding) != self.embedding_dim:
raise ValueError(
f"Embedding has {len(embedding)} dimensions, expected {self.embedding_dim}"
)
agent_scope = agent_id or ""
episode_id = str(uuid.uuid5(uuid.NAMESPACE_OID, f"{user_id}::{agent_scope}::{text}"))
existing = await self._get_one(
self._episodes_col,
(
f'id == "{_escape(episode_id)}" and '
f'record_type == "{_EPISODE_RECORD}" and '
f'agent_id == "{_escape(agent_scope)}"'
),
self._episode_output_fields(),
)
if existing:
return episode_id
row = {
"id": episode_id,
"text": text,
"embedding": embedding,
"user_id": user_id,
"agent_id": agent_id or "",
"session_id": session_id or "",
"is_active": True,
"created_at": _utcnow_iso(),
"fact_ids": "",
"record_type": _EPISODE_RECORD,
"metadata": self._json_value({}),
}
async def insert_episode(
self,
text: str,
embedding: list[float],
user_id: str,
agent_id: str | None = None,
session_id: str | None = None,
metadata: dict[str, Any] | None = None,
) -> str:
if self.embedding_dim and len(embedding) != self.embedding_dim:
raise ValueError(
f"Embedding has {len(embedding)} dimensions, expected {self.embedding_dim}"
)
agent_scope = agent_id or ""
episode_id = str(uuid.uuid5(uuid.NAMESPACE_OID, f"{user_id}::{agent_scope}::{text}"))
existing = await self._get_one(
self._episodes_col,
(
f'id == "{_escape(episode_id)}" and '
f'record_type == "{_EPISODE_RECORD}" and '
f'agent_id == "{_escape(agent_scope)}"'
),
self._episode_output_fields(),
)
if existing:
return episode_id
row = {
"id": episode_id,
"text": text,
"embedding": embedding,
"user_id": user_id,
"agent_id": agent_id or "",
"session_id": session_id or "",
"is_active": True,
"created_at": _utcnow_iso(),
"fact_ids": "",
"record_type": _EPISODE_RECORD,
"metadata": self._json_value(metadata),
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vektori/storage/milvus.py` around lines 1357 - 1396, The override of
insert_episode drops the metadata parameter and always writes an empty dict;
restore the metadata parameter on insert_episode to match the abstract
StorageBackend.insert_episode signature, accept something like metadata: dict |
None, and persist the caller-supplied metadata by setting row["metadata"] =
self._json_value(metadata or {}) before inserting; keep existing behavior for
missing metadata, and leave episode_id generation, the _EPISODE_RECORD value,
and lookups against _episodes_col/_episode_output_fields unchanged.

Comment thread vektori/storage/milvus.py
Comment on lines +1523 to +1529
row = {
"id": marker_id,
"text": "",
"embedding": [0.0] * self.embedding_dim,
"user_id": user_id,
"agent_id": agent_id or "",
"session_id": session_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't erase the stored agent_id on session updates.

When upsert_session() is called for an existing session with agent_id=None, this rewrites the marker with agent_id="". Subsequent agent-scoped session queries stop matching that session.

Suggested fix
         row = {
             "id": marker_id,
             "text": "",
             "embedding": [0.0] * self.embedding_dim,
             "user_id": user_id,
-            "agent_id": agent_id or "",
+            "agent_id": (
+                agent_id
+                if agent_id is not None
+                else (existing.get("agent_id", "") if existing else "")
+            ),
             "session_id": session_id,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vektori/storage/milvus.py` around lines 1523 - 1529, upsert_session() is
overwriting an existing marker's agent_id with an empty string when called with
agent_id=None; change the upsert logic so it preserves the stored agent_id
instead of replacing it: when building the row for insertion/update (the dict
containing "id", "text", "embedding", "user_id", "agent_id", "session_id"), if
agent_id is None retrieve the existing marker by marker_id (or read the current
agent_id if available) and use that value, otherwise use the provided agent_id;
alternatively only set "agent_id" in the row when agent_id is not None to avoid
erasing the stored agent_id.

@laxmanclo
laxmanclo merged commit 0aad7d1 into main Apr 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants