Skip to content

feat: add turbopuffer vector store integration - #6627

Open
RogutKuba wants to merge 3 commits into
crewAIInc:mainfrom
turbopuffer:main
Open

feat: add turbopuffer vector store integration#6627
RogutKuba wants to merge 3 commits into
crewAIInc:mainfrom
turbopuffer:main

Conversation

@RogutKuba

Copy link
Copy Markdown

Summary

Adds support for turbopuffer as a vector store integration

  • crewai.rag.turbopuffer — client (sync + async), config, factory, types, utils
  • Docs + tests (client, utils, config, RagTool routing)

RogutKuba and others added 3 commits July 10, 2026 11:01
Add turbopuffer as a RAG vector store provider alongside ChromaDB and Qdrant:

- client/config/factory/types/utils implementing the BaseClient protocol
  (sync + async), with lazy namespace creation and $dist score normalization
- register the provider in the RAG factory, discriminated config union, and
  optional-import placeholders
- add the 'turbopuffer' optional dependency (extra); the default embedder uses
  fastembed all-MiniLM-L6-v2, loaded lazily on first embed so config construction
  never triggers a model download, and passing your own embedding_function skips
  fastembed entirely
- expose turbopuffer through the crewai-tools RagTool provider list
- document turbopuffer in the knowledge vector-store section
- tests for the client, utils, RagTool routing, and default-embedder laziness

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review: point to the regions list instead of leaving gcp-us-central1 unexplained.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tore

feat(rag): add turbopuffer vector store provider
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Turbopuffer as a RAG vector-store provider, including typed configuration, synchronous and asynchronous client operations, factory dispatch, CrewAI Tools integration, optional dependencies, documentation, and comprehensive tests.

Changes

Turbopuffer RAG provider

Layer / File(s) Summary
Provider contracts and configuration
lib/crewai/src/crewai/rag/config/..., lib/crewai/src/crewai/rag/turbopuffer/config.py, types.py, constants.py, lib/crewai/pyproject.toml
Adds Turbopuffer provider types, optional-import fallbacks, configuration validation, lazy fastembed loading, constants, and the turbopuffer dependency extra.
Turbopuffer client operations
lib/crewai/src/crewai/rag/turbopuffer/client.py, utils.py, factory.py
Implements sync and async collection management, document ingestion, vector search, metadata filtering, score normalization, deletion, and reset operations.
Framework and tool dispatch
lib/crewai/src/crewai/rag/factory.py, lib/crewai-tools/src/crewai_tools/tools/rag/..., docs/edge/en/concepts/knowledge.mdx
Routes Turbopuffer configurations through the RAG factory and RagTool, and documents provider setup and regional configuration.
Integration and unit validation
lib/crewai/tests/rag/turbopuffer/*, lib/crewai-tools/tests/tools/rag/rag_tool_test.py
Tests sync/async client behavior, embedding loading, utility transformations, filtering, reset flows, and RagTool initialization.

Sequence Diagram(s)

sequenceDiagram
  participant RAGConfig
  participant TurbopufferFactory
  participant TurbopufferClient
  participant TurbopufferNamespace
  RAGConfig->>TurbopufferFactory: create_client(config)
  TurbopufferFactory->>TurbopufferClient: construct configured wrapper
  TurbopufferClient->>TurbopufferClient: generate embedding
  TurbopufferClient->>TurbopufferNamespace: write or query vectors
  TurbopufferNamespace-->>TurbopufferClient: return rows and distances
  TurbopufferClient-->>RAGConfig: return SearchResult values
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding Turbopuffer vector store integration.
Description check ✅ Passed The description is directly related to the changeset and accurately summarizes the new Turbopuffer integration and related docs/tests.
Docstring Coverage ✅ Passed Docstring coverage is 98.06% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch main

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.

Warning

⚠️ This pull request shows signs of AI-generated slop (trivial_assertion). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py`:
- Line 247: Update RagTool._create_provider_config() to import TurbopufferConfig
through the existing optional-import fallback, using MissingTurbopufferConfig
when Turbopuffer is unavailable instead of raising ModuleNotFoundError. Match
the established ChromaDB/Qdrant optional embedding_function handling where
applicable.
- Around line 249-251: Update the embedding setup around build_embedder and
TurbopufferConfig to normalize custom embedding results into a flat list[float]
per document or query, unwrapping the single-vector [[...]] shape and validating
the result. Ensure Turbopuffer writes and rank_by queries receive flat vectors,
and add vector-shape assertions to test_rag_tool_with_custom_embeddings and the
corresponding second test.

In `@lib/crewai/src/crewai/rag/turbopuffer/utils.py`:
- Around line 87-99: Update the row construction in the metadata normalization
flow so metadata is merged before assigning the reserved fields, ensuring
metadata cannot overwrite “id”, “vector”, or CONTENT_KEY. Preserve the existing
list, dict, and empty-metadata handling, and add regression coverage for
collisions with each reserved field.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b8100642-0719-423c-8b68-8dc50cc56251

📥 Commits

Reviewing files that changed from the base of the PR and between b14d36b and 94805d5.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • docs/edge/en/concepts/knowledge.mdx
  • lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py
  • lib/crewai-tools/src/crewai_tools/tools/rag/types.py
  • lib/crewai-tools/tests/tools/rag/rag_tool_test.py
  • lib/crewai/pyproject.toml
  • lib/crewai/src/crewai/rag/config/optional_imports/base.py
  • lib/crewai/src/crewai/rag/config/optional_imports/protocols.py
  • lib/crewai/src/crewai/rag/config/optional_imports/providers.py
  • lib/crewai/src/crewai/rag/config/optional_imports/types.py
  • lib/crewai/src/crewai/rag/config/types.py
  • lib/crewai/src/crewai/rag/factory.py
  • lib/crewai/src/crewai/rag/turbopuffer/__init__.py
  • lib/crewai/src/crewai/rag/turbopuffer/client.py
  • lib/crewai/src/crewai/rag/turbopuffer/config.py
  • lib/crewai/src/crewai/rag/turbopuffer/constants.py
  • lib/crewai/src/crewai/rag/turbopuffer/factory.py
  • lib/crewai/src/crewai/rag/turbopuffer/types.py
  • lib/crewai/src/crewai/rag/turbopuffer/utils.py
  • lib/crewai/tests/rag/turbopuffer/__init__.py
  • lib/crewai/tests/rag/turbopuffer/test_client.py
  • lib/crewai/tests/rag/turbopuffer/test_config.py
  • lib/crewai/tests/rag/turbopuffer/test_utils.py

return QdrantConfig(**kwargs)

if provider == "turbopuffer":
from crewai.rag.turbopuffer.config import TurbopufferConfig

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate files:"
fd -a 'rag_tool.py|types.py' . | sed 's#^\./##' | head -80

echo
echo "Relevant class/function outline around rag_tool.py:"
if [ -f lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py ]; then
  wc -l lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py
  ast-grep outline lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py --view expanded || true
fi

echo
echo "Search Turbopuffer related code:"
rg -n "TurbopufferConfig|MissingTurbopufferConfig|from crewai\.rag\.turbopuffer|import crewai\\.rag\\.turbopuffer|MissingTurbopuffer" -S .

Repository: crewAIInc/crewAI

Length of output: 7843


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "rag_tool.py relevant section:"
sed -n '220,255p' lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py | cat -n

echo
echo "lib/crewai/src/crewai/rag/config/types.py relevant section:"
sed -n '1,90p' lib/crewai/src/crewai/rag/config/types.py | cat -n

echo
echo "lib/crewai/src/crewai/rag/config/optional_imports/providers.py relevant section:"
sed -n '1,70p' lib/crewai/src/crewai/rag/config/optional_imports/providers.py | cat -n

echo
echo "lib/crewai-tools/src/crewai_tools/tools/rag/types.py relevant section:"
sed -n '1,220p' lib/crewai-tools/src/crewai_tools/tools/rag/types.py | cat -n

echo
echo "Static call-graph check for _create_provider_config:"
rg -n "_create_provider_config|_validate_adapter|_resolve_adapter|RagToolConfig|TurbopufferConfig|from crewai\.rag\.turbopuffer\.config import" lib/crewai-tools/src lib/crewai/src/crewai -S

Repository: crewAIInc/crewAI

Length of output: 10318


Route Turbopuffer through the optional-import fallback.

RagTool._create_provider_config() imports TurbopufferConfig directly, so Turbopuffer support outside crewai.rag.config.types.py still raises ModuleNotFoundError instead of using the existing MissingTurbopufferConfig placeholder path. Use the same optional fallback here, matching ChromaDB/Qdrant’s optional embedding_function handling if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py` at line 247, Update
RagTool._create_provider_config() to import TurbopufferConfig through the
existing optional-import fallback, using MissingTurbopufferConfig when
Turbopuffer is unavailable instead of raising ModuleNotFoundError. Match the
established ChromaDB/Qdrant optional embedding_function handling where
applicable.

Comment on lines +249 to +251
kwargs = dict(provider_config)
if embedding_function is not None:
kwargs["embedding_function"] = embedding_function

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '(^|/)rag_tool\.py$|turbopuffer|test.*rag|rag.*test' || true

echo
echo "== target file outline/size =="
wc -l lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py 2>/dev/null || true
ast-grep outline lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py 2>/dev/null | sed -n '1,220p' || true

echo
echo "== relevant source lines =="
cat -n lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py | sed -n '220,285p'

Repository: crewAIInc/crewAI

Length of output: 6303


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== search build_embedder and embeddings usage in rag =="
rg -n "build_embedder|embedding_function|embeddings|Turbo|turbopuffer|rank_by|embedding_model" lib/crewai-tools/src/crewai_tools/tools/rag -S || true

echo
echo "== test files in rag area =="
git ls-files | rg 'test.*rag|rag.*test|rag_tool' | sed -n '1,120p'

echo
echo "== diff/stat for rag_tool.py if present =="
git diff --stat lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py || true
git diff -- lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py | sed -n '1,220p' || true

Repository: crewAIInc/crewAI

Length of output: 5771


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== rag_tool_test embeddings references =="
cat -n lib/crewai-tools/tests/tools/rag/rag_tool_test.py | sed -n '1,260p'

echo
echo "== turbopuffer config/client outlines =="
wc -l lib/crewai/src/crewai/rag/turbopuffer/*.py
for f in lib/crewai/src/crewai/rag/turbopuffer/{config,client,types,utils}.py; do
  echo "--- $f"
  ast-grep outline "$f" 2>/dev/null | sed -n '1,220p' || true
done

echo
echo "== turbopuffer source relevant snippets =="
for f in lib/crewai/src/crewai/rag/turbopuffer/{config,client,types,utils}.py; do
  echo "--- $f"
  rg -n "embeddings?|embedding|rank_by|vector|docs|insert" "$f" -C 3 || true
done

echo
echo "== turbopuffer tests relevant snippets =="
for f in lib/crewai/tests/rag/turbopuffer/{test_client,test_config,test_utils}.py; do
  echo "--- $f"
  rg -n "embeddings?|embedding|rank_by|vector|docs|insert" "$f" -C 3 || true
done

Repository: crewAIInc/crewAI

Length of output: 35185


🌐 Web query:

Turbopuffer Python SDK Namespace rank_by vector embedding flat list float

💡 Result:

In the Turbopuffer Python SDK, you perform a vector search by using the rank_by parameter within the namespace.query method [1][2]. The rank_by parameter accepts a tuple (or list) formatted as ("vector", "ANN", embedding), where the embedding is a flat list of floats representing your vector [1][3]. Example usage: ns = tpuf.namespace("your-namespace-name") result = ns.query( rank_by=("vector", "ANN", [0.1, 0.2, 0.3]), top_k=10, include_attributes=["name"]) Key details: - Embedding: The embedding should be a flat list of floats (e.g., [0.1, 0.2,...]) [1][2]. - ANN: Stands for "approximate nearest neighbor," which is the standard search mode for vector embedding queries [2]. - Namespace: The query is executed on a specific namespace instance, initialized via tpuf.namespace("name") [1][4]. - Data format: When writing data, vectors are represented as arrays of numbers or base64-encoded strings [5]. During a query, providing a flat list of floats for the rank_by parameter is the standard approach [1][3]. - Response: You can specify vector_encoding as "float" or "base64" to control how vectors are returned, with "float" being the default [2]. If "float" is selected, the vectors in the response are returned as arrays of numbers [2].

Citations:


Normalize embedding_model outputs for Turbopuffer.

build_embedder is passed into TurbopufferConfig unchanged, but the injected function in test_rag_tool_with_custom_embeddings returns [[...]]. Turbopuffer expects a flat list[float] for each document/query vector, so this can send nested vectors to writes and rank_by query calls. Unwrap/validate the single-vector shape and add a vector-shape assertion to both tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai-tools/src/crewai_tools/tools/rag/rag_tool.py` around lines 249 -
251, Update the embedding setup around build_embedder and TurbopufferConfig to
normalize custom embedding results into a flat list[float] per document or
query, unwrapping the single-vector [[...]] shape and validating the result.
Ensure Turbopuffer writes and rank_by queries receive flat vectors, and add
vector-shape assertions to test_rag_tool_with_custom_embeddings and the
corresponding second test.

Comment on lines +87 to +99
row: dict[str, Any] = {
"id": doc_id,
"vector": embedding,
CONTENT_KEY: doc["content"],
}

metadata = doc.get("metadata", {})
if isinstance(metadata, list):
metadata = metadata[0] if metadata else {}
elif not isinstance(metadata, dict):
metadata = dict(metadata) if metadata else {}

row.update(metadata)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Prevent metadata from overwriting reserved row fields.

Line 99 lets metadata replace id, vector, or content, so a valid record can be persisted with the wrong identity, embedding, or body. Merge metadata first, then write the reserved fields; add regression coverage for each collision.

Proposed fix
-    row: dict[str, Any] = {
-        "id": doc_id,
-        "vector": embedding,
-        CONTENT_KEY: doc["content"],
-    }
-
     metadata = doc.get("metadata", {})
     if isinstance(metadata, list):
         metadata = metadata[0] if metadata else {}
     elif not isinstance(metadata, dict):
         metadata = dict(metadata) if metadata else {}
 
-    row.update(metadata)
+    row = dict(metadata)
+    row.update(
+        {
+            "id": doc_id,
+            "vector": embedding,
+            CONTENT_KEY: doc["content"],
+        }
+    )
📝 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
row: dict[str, Any] = {
"id": doc_id,
"vector": embedding,
CONTENT_KEY: doc["content"],
}
metadata = doc.get("metadata", {})
if isinstance(metadata, list):
metadata = metadata[0] if metadata else {}
elif not isinstance(metadata, dict):
metadata = dict(metadata) if metadata else {}
row.update(metadata)
metadata = doc.get("metadata", {})
if isinstance(metadata, list):
metadata = metadata[0] if metadata else {}
elif not isinstance(metadata, dict):
metadata = dict(metadata) if metadata else {}
row = dict(metadata)
row.update(
{
"id": doc_id,
"vector": embedding,
CONTENT_KEY: doc["content"],
}
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/rag/turbopuffer/utils.py` around lines 87 - 99, Update
the row construction in the metadata normalization flow so metadata is merged
before assigning the reserved fields, ensuring metadata cannot overwrite “id”,
“vector”, or CONTENT_KEY. Preserve the existing list, dict, and empty-metadata
handling, and add regression coverage for collisions with each reserved field.

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.

1 participant