📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-06-29 16:09:10 +00:00
parent 90c6c04c3f
commit f7f50d9fea
314 changed files with 31913 additions and 351 deletions
@@ -0,0 +1,274 @@
# Advanced RAG Cookbook
Build advanced RAG functionality with Weaviate.
Read first:
- Basic RAG cookbook, important to start from this base. MUST READ: [Basic RAG Cookbook](./basic_rag.md)
Docs to reference if needed:
- Search patterns and basics in Weaviate: https://docs.weaviate.io/weaviate/search/basics
- Filters in Weaviate: https://docs.weaviate.io/weaviate/search/filters
- Vector search: https://docs.weaviate.io/weaviate/search/similarity
- Keyword search: https://docs.weaviate.io/weaviate/search/bm25
- Hybrid search: https://docs.weaviate.io/weaviate/search/hybrid
- Image search: https://docs.weaviate.io/weaviate/search/image
## Core Rules
First implement the basic strategy from [here](./basic_rag.md). Then modify according to this guide.
- Use a virtual environment via `venv`
- Use `uv` for Python project/dependency management.
- Do not manually author `pyproject.toml` or `uv.lock`; let `uv` generate/update them.
- Use this install set: `uv add weaviate-client python-dotenv dspy weaviate-agents`
- Customise this cookbook to the users specification, ask them for details if not given.
Assume the user has data already to be used, do not create data unless asked to.
Instead of following this cookbook, you first must ask the user if they would prefer to use the Weaviate Query Agent. If so, all steps in this guide can be implemented with the query agent which does advanced RAG out of the box.
Query agent docs: https://docs.weaviate.io/agents/query/usage
## Env Rules
Mandatory:
- `WEAVIATE_URL`
- `WEAVIATE_API_KEY`
External provider keys:
- Fill only keys actually used by the target Weaviate collection setup.
## Advanced RAG overview
* Query re-writer: *Change user input text into a query text using an LLM*
* Query decomposition: *Change query into multiple sub-queries each re-written with an LLM*
* Filtering: *Use an LLM to define filters on the collection*
* Re-ranking: *Score the final results by a more advanced model*
* Prompt engineering: *Add chain of thought, Tree of thoughts, ReAct*
## Query Re-writer
```python
class QueryRewriter(dspy.Signature):
"""
Rewrite the user's query into a more relevant search term that is a more relevant search term for searching a database.
"""
input_query: str = dspy.InputField(description="The original user query")
rewritten_query: str = dspy.OutputField(
description=(
"A single search term that is more relevant to the user's query. "
"Include only relevant information, it does not need to be a full sentence or question "
)
)
```
Modify the `query_transformation` function:
```python
def query_transformation(query: str) -> list[str]:
lm = dspy.LM(subtask_model_name)
answer = dspy.Predict(QueryRewriter)
pred = answer(input_query=query, lm=lm)
return [pred.rewritten_query]
```
## Query Decomposition
```python
class QueryRewriter(dspy.Signature):
"""
Rewrite the user's query into a more relevant search terms that are more relevant search term for searching a database.
"""
input_query: str = dspy.InputField(description="The original user query")
rewritten_queries: list[str] = dspy.OutputField(
description=(
"A list of search terms that are more relevant to the user's query. "
"Each entry should include only relevant information, it does not need to be a full sentence or question "
"Split independent searches into different entries "
"Each entry should be relevant independently that capture a different required search aspect "
"Do not repeat similar search terms, each one should have a unique meaning "
"Be sparse, do not duplicate search terms "
)
)
def query_transformation(query: str) -> list[str]:
lm = dspy.LM(subtask_model_name)
answer = dspy.Predict(QueryRewriter)
pred = answer(input_query=query, lm=lm)
return pred.rewritten_queries
```
## LLM-created Filters
Filters can be specified by the user (for specific use-cases, perhaps), or you can get an LLM to write the filters also. Writing filters requires knowledge of the collection schema. This can be retrieved by advanced methods or a simple version can be used.
Simple version:
1. First create structured responses to format filters
```python
from pydantic import BaseModel, Field
from typing import Literal, Any
class SearchFilter(BaseModel):
field: str = Field(description="The field to be filtered on.")
operator: Literal["=", "!=", ">", "<"] = Field(description="The operator to be used in conjunction with the value. These are strict operators.")
value: Any = Field(description="The value to be used in conjunction with the operator.")
class Search(BaseModel):
filters: list[SearchFilter] = Field(description="The filters to be used in the vector database. This is an AND operation.")
class SearchCreation(dspy.Signature):
"""
Create filters and search parameters for a search query in a database.
"""
query: str = dspy.InputField()
schema: list[dict] = dspy.InputField(desc="Schema of the collection to be searched.")
data_sample: list[dict] = dspy.InputField(desc="A sample of the data in the collection to be searched.")
search: Search = dspy.OutputField(
desc=(
"Your filters and search parameters, this should be a valid JSON object. "
"This should be constructed so that it matches the goal of the user prompt."
)
)
```
This requires `schema` and `data_sample` as an input field to the LLM call `SearchCreation`.
2. Helper function to turn structured response into weaviate filter
```python
def _format_filters(search_filters: list[SearchFilter]):
filters = []
for search_filter in search_filters:
base_filter = Filter.by_property(search_filter.field)
if search_filter.operator == "=":
filter = base_filter.equal(search_filter.value)
elif search_filter.operator == "!=":
filter = base_filter.not_equal(search_filter.value)
elif search_filter.operator == ">":
filter = base_filter.greater_than(search_filter.value)
elif search_filter.operator == "<":
filter = base_filter.less_than(search_filter.value)
filters.append(filter)
return Filter.all_of(filters) if filters else None
```
3. Combine
```python
def create_filters(query: str):
# import client here
collection = client.collections.use("<collection_name>")
# Get collection schema (for field names etc.). can replace this with more advanced configuration (like aggregating for unique groups)
config = collection.config.get()
schema = [{"name": p.name, "type": p.data_type[:]} for p in config.properties]
# Get a sample of the data in the collection to be searched
data_sample = collection.query.fetch_objects(limit=5)
# Create search parameters
search_parameters = dspy.ChainOfThought(SearchCreation)
search_parameters_output = search_parameters(query=query, schema=schema, data_sample=data_sample, lm=dspy.LM(subtask_model_name))
return _format_filters(search_parameters_output.search.filters)
```
These filters can be passed into the `collection.query.near_text` (or equivalent search function).
## Re-ranking
Do not modify the user's collection unless requested to do so. Re-ranking requires configuring the collection with a re-ranker, for example:
```python
collection = client.collections.use("<collection_name>")
collection.config.update(
reranker_config=Reconfigure.Reranker.cohere()
)
```
(this would require a Cohere API key).
Modify the `retrieve` function
```python
from weaviate.classes.query import Rerank
def retrieve(query: str, limit: int | None = None, filters = []) -> list[dict]:
# ...existing code
response = collection.query.hybrid(
query=query,
limit=limit,
rerank=Rerank(
prop="content", # what field to re-rank on
query=query # what the search term for the re-ranker should be (same as original in this case)
),
filters=filters if filters else None
)
# ...existing code
```
## Prompt Engineering
This step depends on the LLM framework used. You can manually ask the LLM to include reasoning before giving its final answer, adding a reasoning sub-field to be completed before giving the final answer in structured response, or specify in DSPy to use chain-of-thought.
```python
class Generator(dspy.Signature):
"""
Answer the question based on the context.
Do not include any information from external sources, only use the information provided in the context.
If you cannot answer the question based on the information provided, say "I don't know".
"""
context: str | list[dict] = dspy.InputField(desc="The context to answer the question.")
query: str = dspy.InputField(desc="The question to answer.")
answer: str = dspy.OutputField(desc="The single answer to the question with no additional communication")
```
Modify the `generate` function:
```python
def generate(query: str, context: list[dict]) -> str:
lm = dspy.LM(generation_model_name)
answer = dspy.Predict(Generator)
pred = answer(context=context, query=query, lm=lm)
return pred.answer
```
Consider other prompt engineering techniques like ReAct (if necessary but likely overkill), few-shot learning (requires advanced specification), or otherwise.
## Query Agent
Skip this guide altogether and use the Weaviate Query Agent.
```python
from weaviate.agents.query import QueryAgent
# import client here
qa = QueryAgent(
client=client, collections=["Example_Communications_Raw"]
)
response = qa.search("<user query here>") # just search with no text response
response = qa.ask("<user query here>") # search with text response accessible via response.final_answer
```
## Customisation Points
**LLM framework**
This guide used DSPy. Follow the guidelines in [here](./basic_rag.md), but most likely you will need an LLM framework involving structured responses.
## Troubleshooting
- Weaviate startup host errors: ensure `WEAVIATE_URL` is full `https://...` URL.
- For any other issues, refer to the official library/package documentation and use web search extensively for troubleshooting.
## Done Criteria
- Create test scripts to check each function works independently with test data. Tear down tests after completion, or create a proper test suite with pytest (requires install)
- User has completed specification of the app.
@@ -0,0 +1,360 @@
# Agentic RAG Cookbook
Build RAG-powered AI agents with Weaviate.
Read first:
- Basic Agent cookbook, important to start from this base. MUST READ: [Basic Agent Cookbook](./basic_agent.md)
Docs to reference if needed:
- Search patterns and basics in Weaviate: https://docs.weaviate.io/weaviate/search/basics
- Filters in Weaviate: https://docs.weaviate.io/weaviate/search/filters
- Hybrid search: https://docs.weaviate.io/weaviate/search/hybrid
- Weaviate Query Agent: https://docs.weaviate.io/agents/query/usage
- Elysia: https://weaviate.github.io/elysia/
## Core Rules
First implement the basic agent from [here](./basic_agent.md). Then modify according to this guide.
- Use a virtual environment via `venv`
- Use `uv` for Python project/dependency management.
- Do not manually author `pyproject.toml` or `uv.lock`; let `uv` generate/update them.
- Use this install set: `uv add weaviate-client python-dotenv dspy`
- Add `weaviate-agents` if using the Query Agent: `uv add "weaviate-client[agents]"`
- Add `elysia-ai` if using Elysia: `uv add elysia-ai`
- Customise this cookbook to the users specification, ask them for details if not given.
Assume the user has data already to be used, do not create data unless asked to.
Instead of following this cookbook, you first must ask the user if they would prefer to use the Weaviate Query Agent or Elysia. If so, skip to the relevant section below.
- Query Agent docs: https://docs.weaviate.io/agents/query/usage
- Elysia docs: https://weaviate.github.io/elysia/
## Env Rules
Mandatory:
- An LLM provider API key (e.g. `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`)
- `WEAVIATE_URL`
- `WEAVIATE_API_KEY`
External provider keys:
- Fill only keys actually used by the target Weaviate collection setup.
## Agentic RAG Overview
* Naive RAG tool: *Basic retrieval as a single tool for the RouterAgent*
* Hierarchical RAG: *LLM-created filters and search parameters as a sub-agent tool*
* Vector DB memory: *Store and retrieve facts across sessions using Weaviate*
* Query Agent: *Pre-built agentic RAG service by Weaviate*
* Elysia: *Open source agentic framework with built-in query tool*
## Naive RAG Tool
A simple retrieval tool that the RouterAgent can call. Pass this as a tool to the RouterAgent from the [basic agent cookbook](./basic_agent.md).
```python
from weaviate import connect_to_weaviate_cloud
import os
def retrieve_data(query: str):
"""
Given a query (free text), return the most relevant documents from the vector database using hybrid search.
"""
client = connect_to_weaviate_cloud(
cluster_url=os.getenv("WEAVIATE_URL", ""),
auth_credentials=os.getenv("WEAVIATE_API_KEY", ""),
)
collection = client.collections.use("<collection_name>")
response = collection.query.hybrid(query=query, limit=5)
client.close()
return f"{[obj.properties for obj in response.objects]}"
```
Customise the search type (`hybrid`, `near_text`, `bm25`), `limit`, and return fields based on the use case.
## Hierarchical RAG (LLM-created Filters)
Instead of simple retrieval, use an LLM sub-agent to construct filters and search parameters. This makes the tool itself an agent.
1. Structured response models for filters:
```python
from pydantic import BaseModel, Field
from typing import Literal, Any
class SearchFilter(BaseModel):
field: str = Field(description="The field to be filtered on.")
operator: Literal["=", "!=", ">", "<"] = Field(description="The operator to be used in conjunction with the value.")
value: Any = Field(description="The value to be used in conjunction with the operator.")
class Search(BaseModel):
query: str = Field(description="The search query to be used in the vector database.")
filters: list[SearchFilter] = Field(description="The filters to be used in the vector database.")
limit: int = Field(description="The number of results to return from the vector database.")
class SearchCreation(dspy.Signature):
"""
Create a search query for a vector database.
"""
user_prompt: str = dspy.InputField()
schema: list[dict] = dspy.InputField(desc="Schema of the collection to be searched.")
search: Search = dspy.OutputField(
desc=(
"Your search query and filters, this should be a valid JSON object. "
"This should be constructed so that it matches the goal of the user prompt."
)
)
```
2. Helper function to convert structured filters to Weaviate filters:
```python
from weaviate.classes.query import Filter
def format_filters(search_filters: list[SearchFilter]):
filters = []
for search_filter in search_filters:
base_filter = Filter.by_property(search_filter.field)
if search_filter.operator == "=":
filters.append(base_filter.equal(search_filter.value))
elif search_filter.operator == "!=":
filters.append(base_filter.not_equal(search_filter.value))
elif search_filter.operator == ">":
filters.append(base_filter.greater_than(search_filter.value))
elif search_filter.operator == "<":
filters.append(base_filter.less_than(search_filter.value))
return Filter.all_of(filters) if filters else None
```
3. The hierarchical query tool (replaces the naive retrieval tool):
```python
def query_agent_tool(collection_name: str, user_prompt: str):
"""
Given a query (free text), return the most relevant documents from the vector database using hybrid search with LLM-generated filters.
"""
client = connect_to_weaviate_cloud(
cluster_url=os.getenv("WEAVIATE_URL", ""),
auth_credentials=os.getenv("WEAVIATE_API_KEY", ""),
)
collection = client.collections.use(collection_name)
config = collection.config.get()
schema = [{"name": p.name, "type": p.data_type[:]} for p in config.properties]
query_model = dspy.ChainOfThought(SearchCreation)
query_output = query_model(
user_prompt=user_prompt,
schema=schema,
lm=dspy.LM("<subtask_model_name>")
)
response = collection.query.hybrid(
query=query_output.search.query,
filters=format_filters(query_output.search.filters),
limit=query_output.search.limit
)
client.close()
return f"{[obj.properties for obj in response.objects]}"
```
Schema information is required for the LLM to construct filters. Fetch dynamically via `collection.config.get()` or provide manually if the schema is stable. Consider enriching the schema with sample data or enumerated values for better filter accuracy.
## Vector Database Memory
Store and retrieve facts across sessions using Weaviate. Only add this if cross-session persistence is required.
1. Memory creation signature:
```python
class MemoryCreation(dspy.Signature):
user_prompt: str = dspy.InputField()
assistant_response: str = dspy.InputField()
memory: str = dspy.OutputField(
description="A single string representing the most pertinent fact from the user/agent interaction."
)
```
2. Add `memories` as an input to `AgentResponse`:
```python
class AgentResponse(dspy.Signature):
# Input Fields
history: dspy.History = dspy.InputField()
user_prompt: str = dspy.InputField()
available_tools: str = dspy.InputField()
memories: list[str] = dspy.InputField(
desc="A list of memories from previous conversations, you can use these to inform your response."
)
# Output Fields
response: str = dspy.OutputField(
description="The response to the user's prompt whilst the tool is running. Update the user on the progress of their request (if a tool is picked), or the final response to the user (if no tool is picked)."
)
tool: str | None = dspy.OutputField(
description="The tool that needs to be used. Return None if no tool is needed."
)
tool_inputs: Dict[str, Any] | None = dspy.OutputField(
description=(
"The inputs for the tool. Return an empty dictionary (still include the field) if no inputs are needed. "
"The key is the name of the input, the value is the value of the input."
)
)
```
3. Add `create_memory` and `retrieve_memories` methods to `RouterAgent`:
```python
from weaviate import connect_to_weaviate_cloud
from weaviate.classes.config import Configure
class RouterAgent:
def __init__(self, model: str, memory_model: str | None = None, tools: List[Callable] = []):
self.tools: list[Callable] = tools
self.model = dspy.LM(model)
self.memory_model = dspy.LM(memory_model) if memory_model else dspy.LM(model)
self.agent = dspy.ChainOfThought(AgentResponse)
self.memory_agent = dspy.Predict(MemoryCreation)
self.conversation_history = dspy.History(messages=[])
self.weaviate_client = connect_to_weaviate_cloud(
cluster_url=os.getenv("WEAVIATE_URL", ""),
auth_credentials=os.getenv("WEAVIATE_API_KEY", ""),
)
# ... existing methods from basic_agent.md (add_conversation_history, get_tools_and_descriptions) ...
def create_memory(self, user_prompt: str, assistant_response: str, tool_result: str):
if tool_result:
assistant_response += "\n" + tool_result
result = self.memory_agent(
history=self.conversation_history,
user_prompt=user_prompt,
assistant_response=assistant_response,
lm=self.memory_model,
)
if not self.weaviate_client.collections.exists("Agent_Memory"):
self.weaviate_client.collections.create(
"Agent_Memory",
vector_config=Configure.Vectors.text2vec_weaviate()
)
collection = self.weaviate_client.collections.use("Agent_Memory")
collection.data.insert({"user_prompt": user_prompt, "memory": result.memory})
return result.memory
def retrieve_memories(self, user_prompt: str):
if not self.weaviate_client.collections.exists("Agent_Memory"):
return []
collection = self.weaviate_client.collections.use("Agent_Memory")
query = collection.query.near_text(query=user_prompt, limit=5)
return [memory.properties["memory"] for memory in query.objects]
```
Call `retrieve_memories` at the start of each interaction and pass results to the `memories` field of `AgentResponse`. Call `create_memory` after each interaction.
Consider using a cheaper model for memory creation (e.g. `memory_model="<cheap_model_name>"`).
## Weaviate Query Agent
Skip the custom implementation and use the pre-built Weaviate Query Agent for agentic RAG. Handles collection selection, filter construction, and query optimisation automatically.
```python
from weaviate.agents.query import QueryAgent
# import client here
qa = QueryAgent(
client=client, collections=["<collection_name>"]
)
response = qa.search("<user query here>") # retrieval only
response = qa.ask("<user query here>") # retrieval + text response via response.final_answer
```
The Query Agent is free up to 1000 requests per month. Docs: https://docs.weaviate.io/agents/query/usage
## Elysia
Elysia is an open source agentic framework with built-in query tools, decision trees, error handling, and automatic retry.
Setup:
```python
import elysia
from elysia.tools.text import FakeTextResponse as TextResponseTool
elysia.configure(
base_model="<model_name>",
base_provider="<provider>", # e.g. "anthropic", "openai"
logging_level="ERROR"
)
```
With custom tools:
```python
tree = elysia.Tree("empty", use_elysia_collections=False)
tree.add_tool(TextResponseTool)
@elysia.tool
async def your_tool(param: str):
"""Tool description."""
return {"result"}
tree.add_tool(your_tool)
response, _ = tree("user query here")
```
With built-in Weaviate query tool (requires preprocessing):
```python
from elysia import preprocess
preprocess("<collection_name>")
tree = elysia.Tree()
response, _ = tree(
"user query here",
collection_names=["<collection_name>"]
)
```
Elysia includes built-in error handling, self-healing, and automatic retry. Also available as a standalone app with a frontend UI: https://github.com/weaviate/elysia
## Customisation Points
**When to use which approach:**
| Use Case | Recommended Approach |
|----------|---------------------|
| Single collection, simple queries | Naive RAG tool |
| Need filters or operators | Hierarchical RAG or Query Agent |
| Multi-step tasks, multiple data sources | Sequential agent with agentic loop |
| Cross-session personalisation | Add Vector Database Memory layer |
| Production deployment with error handling | Use Elysia or Query Agent |
**Do not implement multi-agent architectures for simple retrieval tasks.**
**LLM framework**
This guide used DSPy. Follow the guidelines in [here](./basic_agent.md), but most likely you will need an LLM framework involving structured responses.
## Troubleshooting
- Weaviate startup host errors: ensure `WEAVIATE_URL` is full `https://...` URL.
- DSPy signature warnings about missing fields: these can occur when using followup agents without all fields; ensure optional fields are handled.
- For any other issues, refer to the official library/package documentation and use web search extensively for troubleshooting.
## Done Criteria
- Create test scripts to check each function works independently with test data. Tear down tests after completion, or create a proper test suite with pytest (requires install)
- User has completed specification of the app.
@@ -0,0 +1,428 @@
# Async Client Usage
Guide for using the Weaviate Python async client in production applications (FastAPI, async frameworks).
## 📚 Official Documentation Reference
**For agents:** If you encounter any issues not covered here, refer to the official Weaviate documentation:
- **Primary Reference**: [Weaviate Async API Documentation](https://docs.weaviate.io/weaviate/client-libraries/python/async)
- **Python Client Reference**: [Weaviate Python Client Docs](https://docs.weaviate.io/weaviate/client-libraries/python)
- **API Reference**: [ReadTheDocs - Python Client](https://weaviate-python-client.readthedocs.io/en/stable/)
- **Troubleshooting**: [Community Forum](https://forum.weaviate.io/) | [GitHub Issues](https://github.com/weaviate/weaviate-python-client/issues)
> **Note**: The async client (`WeaviateAsyncClient`) is available in `weaviate-client` v4.7.0+.
## Connection Methods
Three instantiation helpers are provided ([docs](https://docs.weaviate.io/weaviate/client-libraries/python/async#instantiation)):
### Weaviate Cloud (Recommended)
```python
import weaviate
from weaviate.classes.init import Auth
# Use the official helper function for Weaviate Cloud
client = weaviate.use_async_with_weaviate_cloud(
cluster_url="your-cluster.weaviate.cloud", # Accepts hostname with or without https://
auth_credentials=Auth.api_key("your-api-key"),
headers={ # Note: parameter is "headers" not "additional_headers"
"X-OpenAI-Api-Key": "sk-...",
"X-Anthropic-Api-Key": "sk-ant-...",
}
)
await client.connect() # Required! Async helpers don't auto-connect
```
**Reference**: [Weaviate Cloud Setup](https://docs.weaviate.io/weaviate/quickstart)
### Self-Hosted
```python
# For local instances
client = weaviate.use_async_with_local()
# For custom endpoints
client = weaviate.use_async_with_custom(
http_host="localhost",
http_port=8080,
http_secure=False,
grpc_host="localhost",
grpc_port=50051,
grpc_secure=False,
)
await client.connect()
```
**Reference**: [Connection Configuration](https://weaviate-python-client.readthedocs.io/en/stable/weaviate.html)
### Authentication
Multiple authentication modes are supported ([docs](https://docs.weaviate.io/weaviate/client-libraries/python#authentication)):
```python
from weaviate.classes.init import Auth
# API Key (most common for Weaviate Cloud)
auth = Auth.api_key("your-api-key")
# Bearer Token (with optional refresh token)
auth = Auth.bearer_token("access-token", refresh_token="refresh-token")
# Client Credentials (OIDC)
auth = Auth.client_credentials(client_secret="secret")
# Client Password (OIDC Resource Owner Password flow)
auth = Auth.client_password(username="user", password="pass")
# Usage
client = weaviate.use_async_with_weaviate_cloud(
cluster_url="your-cluster.weaviate.cloud",
auth_credentials=auth,
)
```
## Critical Patterns
### ⚠️ Connection Lifecycle
**Important**: Unlike synchronous helpers, async helpers **do not connect automatically** ([docs](https://docs.weaviate.io/weaviate/client-libraries/python/async#instantiation)). You must explicitly call `.connect()` and `.close()`:
```python
# ❌ Wrong - client not connected
client = weaviate.use_async_with_weaviate_cloud(...)
collections = await client.collections.list_all() # Will fail!
# ✅ Correct - explicit connect/close
client = weaviate.use_async_with_weaviate_cloud(...)
await client.connect()
collections = await client.collections.list_all()
await client.close()
```
### ⚠️ Sync vs Async Methods
**Key distinction** ([docs](https://docs.weaviate.io/weaviate/client-libraries/python/async#which-methods-are-async)): Methods involving server requests are async; local operations are synchronous.
```python
# Collection retrieval is SYNC (no await)
collection = client.collections.get("MyCollection")
# Operations on collections are ASYNC (need await)
config = await collection.config.get()
results = await collection.query.fetch_objects()
count = await collection.aggregate.over_all()
```
**Rule:** Getting the collection object is sync; calling methods on it is async.
### ⚠️ Bulk Operations
**Important Note** ([docs](https://docs.weaviate.io/weaviate/client-libraries/python/async#bulk-import-operations)): For large-scale data imports, use the **synchronous client** and its batch operations. The sync client's batch methods already handle concurrency internally and are optimized for bulk operations.
```python
# ✅ For bulk imports, prefer sync client
import weaviate
with weaviate.connect_to_weaviate_cloud(...) as client:
collection = client.collections.get("MyCollection")
# Batch insert handles concurrency automatically
with collection.batch.dynamic() as batch:
for item in large_dataset:
batch.add_object(properties=item)
```
Use the async client for:
- Web applications (FastAPI, Starlette)
- Concurrent request handling
- Interactive queries
Don't use the async client for:
- Bulk data imports (use sync client instead)
## Context Manager Pattern (Recommended)
**Best Practice** ([docs](https://docs.weaviate.io/weaviate/client-libraries/python/async#using-the-async-context-manager)): Use `async with` to automatically connect/disconnect:
```python
from contextlib import asynccontextmanager
from typing import AsyncGenerator
@asynccontextmanager
async def get_weaviate_client(
cluster_url: str,
api_key: str,
provider_headers: dict[str, str] | None = None,
) -> AsyncGenerator[weaviate.WeaviateAsyncClient, None]:
"""Connect to Weaviate Cloud with automatic cleanup."""
# Remove scheme if present
hostname = cluster_url.replace("https://", "").replace("http://", "")
client = weaviate.use_async_with_weaviate_cloud(
cluster_url=hostname,
auth_credentials=Auth.api_key(api_key),
headers=provider_headers,
)
try:
await client.connect()
yield client
finally:
await client.close()
# Usage
async def example():
async with get_weaviate_client(
cluster_url="your-cluster.weaviate.cloud",
api_key="your-key",
) as client:
collections = await client.collections.list_all()
```
> **Note**: When using the context manager, `.connect()` and `.close()` are called automatically.
## FastAPI Integration
**Use Case** ([docs](https://docs.weaviate.io/weaviate/client-libraries/python/async#use-cases)): The async client excels in web frameworks like FastAPI for handling concurrent requests.
Use lifespan management for shared client across requests:
```python
from fastapi import FastAPI
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: connect to Weaviate
app.state.weaviate = weaviate.use_async_with_weaviate_cloud(
cluster_url="your-cluster.weaviate.cloud",
auth_credentials=Auth.api_key("your-key"),
)
await app.state.weaviate.connect()
yield
# Shutdown: close connection
await app.state.weaviate.close()
app = FastAPI(lifespan=lifespan)
@app.get("/collections")
async def list_collections():
collections = await app.state.weaviate.collections.list_all()
return {"collections": list(collections.keys())}
```
**Community Discussion**: [FastAPI Best Practices](https://forum.weaviate.io/t/what-is-the-best-practice-to-use-v4-python-client-for-query-with-fastapi-or-other-async-python-framework/1245)
## Common Pitfalls
### 1. Parameter Name Confusion
```python
# ❌ Wrong - WeaviateAsyncClient() constructor uses different param
client = weaviate.use_async_with_weaviate_cloud(
additional_headers={...} # Wrong parameter name!
)
# ✅ Correct - use "headers" not "additional_headers"
client = weaviate.use_async_with_weaviate_cloud(
headers={...}
)
```
### 2. URL Format
Both formats work with helper functions:
```python
# ✅ Both accepted
client = weaviate.use_async_with_weaviate_cloud(
cluster_url="https://cluster.weaviate.cloud" # With scheme
)
client = weaviate.use_async_with_weaviate_cloud(
cluster_url="cluster.weaviate.cloud" # Without scheme
)
```
### 3. Sync vs Async Function Names
```python
# ❌ Wrong - sync client (cannot use await)
client = weaviate.connect_to_weaviate_cloud(...)
await client.connect() # TypeError!
# ✅ Correct - async client
client = weaviate.use_async_with_weaviate_cloud(...)
await client.connect()
```
**Naming pattern:**
- Sync: `connect_to_*` (e.g., `connect_to_weaviate_cloud`)
- Async: `use_async_with_*` (e.g., `use_async_with_weaviate_cloud`)
### 4. Port Configuration
```python
# ❌ Wrong - manual port config causes conflicts with Weaviate Cloud
client = WeaviateAsyncClient(
connection_params=ConnectionParams.from_url(
url="https://cluster.weaviate.cloud",
grpc_port=443, # Conflict!
)
)
# ✅ Correct - use helper function (handles ports automatically)
client = weaviate.use_async_with_weaviate_cloud(
cluster_url="cluster.weaviate.cloud"
)
```
**Rule:** For Weaviate Cloud, always use `use_async_with_weaviate_cloud()` — it handles HTTP (443) and gRPC (50051) ports correctly.
## Multi-Cluster Example
Managing connections to multiple Weaviate clusters:
```python
@asynccontextmanager
async def get_multi_cluster_clients(
clusters: dict[str, dict[str, str]]
) -> AsyncGenerator[dict[str, weaviate.WeaviateAsyncClient], None]:
"""Connect to multiple Weaviate clusters.
Args:
clusters: Dict of {cluster_id: {"url": "...", "api_key": "..."}}
"""
clients = {}
try:
# Connect to all clusters
for cluster_id, config in clusters.items():
client = weaviate.use_async_with_weaviate_cloud(
cluster_url=config["url"],
auth_credentials=Auth.api_key(config["api_key"]),
)
await client.connect()
clients[cluster_id] = client
yield clients
finally:
# Close all connections
for client in clients.values():
await client.close()
# Usage
async def example():
clusters = {
"prod": {"url": "prod.weaviate.cloud", "api_key": "key1"},
"dev": {"url": "dev.weaviate.cloud", "api_key": "key2"},
}
async with get_multi_cluster_clients(clusters) as clients:
prod_collections = await clients["prod"].collections.list_all()
dev_collections = await clients["dev"].collections.list_all()
```
## Environment Variables
See [Environment Requirements](environment_requirements.md) for provider API keys.
```python
import os
# Read from environment
cluster_url = os.environ["WEAVIATE_URL"]
api_key = os.environ["WEAVIATE_API_KEY"]
# Build provider headers
provider_headers = {}
if openai_key := os.getenv("OPENAI_API_KEY"):
provider_headers["X-OpenAI-Api-Key"] = openai_key
if anthropic_key := os.getenv("ANTHROPIC_API_KEY"):
provider_headers["X-Anthropic-Api-Key"] = anthropic_key
client = weaviate.use_async_with_weaviate_cloud(
cluster_url=cluster_url,
auth_credentials=Auth.api_key(api_key),
headers=provider_headers or None,
)
```
## Testing Async Code
```python
import pytest
@pytest.mark.asyncio
async def test_weaviate_connection():
async with get_weaviate_client(
cluster_url="test-cluster.weaviate.cloud",
api_key="test-key",
) as client:
collections = await client.collections.list_all()
assert isinstance(collections, dict)
```
## Quick Reference
| Task | Pattern | Await? |
| ---------------- | --------------------------------------------- | ------- |
| Create client | `weaviate.use_async_with_weaviate_cloud(...)` | No |
| Connect | `client.connect()` | **Yes** |
| Get collection | `client.collections.get("Name")` | No |
| List collections | `client.collections.list_all()` | **Yes** |
| Query data | `collection.query.fetch_objects()` | **Yes** |
| Get config | `collection.config.get()` | **Yes** |
| Aggregate | `collection.aggregate.over_all()` | **Yes** |
| Close | `client.close()` | **Yes** |
## Troubleshooting
### Common Issues
| Issue | Solution | Reference |
| ----------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------- |
| Connection hangs indefinitely | Use context manager or ensure proper `.close()` | [GitHub #753](https://github.com/weaviate/weaviate-python-client/issues/753) |
| Multi-worker conflicts (Gunicorn) | Use lifespan management, not startup hooks | [GitHub #1292](https://github.com/weaviate/weaviate-python-client/issues/1292) |
| `TypeError: object NoneType can't be used in 'await'` | Use `use_async_with_*` not `connect_to_*` | [Async API Docs](https://docs.weaviate.io/weaviate/client-libraries/python/async) |
| Port conflicts with Weaviate Cloud | Use helper functions, not manual `ConnectionParams` | See "Common Pitfalls #4" above |
### Getting Help
**For agents:** When encountering errors:
1. Check the [Common Pitfalls](#common-pitfalls) section above
2. Search [Community Forum](https://forum.weaviate.io/) for similar issues
3. Check [GitHub Issues](https://github.com/weaviate/weaviate-python-client/issues) for known bugs
4. Refer to [official async documentation](https://docs.weaviate.io/weaviate/client-libraries/python/async)
5. Review [Python client best practices](https://docs.weaviate.io/weaviate/client-libraries/python/notes-best-practices)
## Additional Resources
### Official Documentation
- **Primary**: [Weaviate Async API](https://docs.weaviate.io/weaviate/client-libraries/python/async)
- **Python Client**: [Main Documentation](https://docs.weaviate.io/weaviate/client-libraries/python)
- **API Reference**: [ReadTheDocs](https://weaviate-python-client.readthedocs.io/en/stable/)
- **Best Practices**: [Notes and Best Practices](https://docs.weaviate.io/weaviate/client-libraries/python/notes-best-practices)
### Framework Integration
- [FastAPI Lifespan Events](https://fastapi.tiangolo.com/advanced/events/)
- [Python Async Context Managers](https://docs.python.org/3/reference/datamodel.html#asynchronous-context-managers)
### Community
- [Weaviate Community Forum](https://forum.weaviate.io/)
- [Python Client GitHub](https://github.com/weaviate/weaviate-python-client)
- [Weaviate Blog](https://weaviate.io/blog)
@@ -0,0 +1,270 @@
# Basic Agent Cookbook
Build a tool-calling AI agent with structured outputs using DSPy.
For RAG tools, memory, and framework integrations, [see here](./agentic_rag.md).
Docs to reference if needed:
- DSPy signatures: https://dspy.ai/learn/programming/signatures/
- DSPy language models: https://dspy.ai/learn/programming/language_models/
- LiteLLM providers: https://docs.litellm.ai/docs/
## Core Rules
- Use a virtual environment via `venv`
- Use `uv` for Python project/dependency management.
- Do not manually author `pyproject.toml` or `uv.lock`; let `uv` generate/update them.
- Use this install set: `uv add dspy python-dotenv`
- Customise this cookbook to the users specification, ask them for details if not given.
## Env Rules
Mandatory:
- An LLM provider API key (e.g. `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`)
- `WEAVIATE_URL`
- `WEAVIATE_API_KEY`
Optional:
- Matching provider keys listed in `environment_requirements.md`
If the user explicitly requests a non-Weaviate agent, `WEAVIATE_URL` and `WEAVIATE_API_KEY` can be omitted.
## Agent Response Signature
The structured output that defines what the LLM returns when selecting tools.
```python
import dspy
from typing import Any, Dict
class AgentResponse(dspy.Signature):
# Input Fields
history: dspy.History = dspy.InputField()
user_prompt: str = dspy.InputField()
available_tools: str = dspy.InputField()
# Output Fields
response: str = dspy.OutputField(
description="The response to the user's prompt whilst the tool is running. Update the user on the progress of their request (if a tool is picked), or the final response to the user (if no tool is picked)."
)
tool: str | None = dspy.OutputField(
description="The tool that needs to be used. Return None if no tool is needed."
)
tool_inputs: Dict[str, Any] | None = dspy.OutputField(
description=(
"The inputs for the tool. Return an empty dictionary (still include the field) if no inputs are needed. "
"The key is the name of the input, the value is the value of the input."
)
)
```
Extend `AgentResponse` as needed: add `confidence: float` for certainty scoring, `requires_clarification: bool` for follow-up questions, or modify `description` strings to shape agent behaviour for a specific domain.
## Router Agent (Single Step)
Wraps the agent response into a class that manages conversation history and tool execution.
```python
from typing import Callable, List, Tuple
class RouterAgent:
def __init__(self, model: str, tools: List[Callable] = []):
self.tools: list[Callable] = tools
self.model = dspy.LM(model)
self.agent = dspy.ChainOfThought(AgentResponse)
self.conversation_history = dspy.History(messages=[])
def add_conversation_history(self, message: str, response: dspy.Prediction):
self.conversation_history.messages.append({"user_prompt": message, **response})
def get_tools_and_descriptions(self) -> str:
return "\n".join(
[
f"{tool.__name__}:\nDescription: {tool.__doc__ or ''}\nInputs: { {k: v for k, v in tool.__annotations__.items() if k != 'return'} }"
for tool in self.tools
]
)
def get_response(self, user_prompt: str) -> Tuple[str, str | None]:
result = self.agent(
history=self.conversation_history,
user_prompt=user_prompt,
available_tools=self.get_tools_and_descriptions(),
lm=self.model,
)
self.add_conversation_history(message=user_prompt, response=result)
if result.tool and result.tool.lower() not in ["null", "none"]:
tool_function = next(
(tool for tool in self.tools if tool.__name__ == result.tool), None
)
if tool_function is None:
raise ValueError(f"Tool {result.tool} not found")
tool_inputs = {k: v for k, v in result.tool_inputs.items() if k != "return"}
tool_result = tool_function(**tool_inputs)
else:
tool_result = None
return result.response, tool_result
```
Usage:
```python
router = RouterAgent(
model="<model_name>", # e.g. claude-sonnet-4-5, gpt-5.2, gemini-2.5-pro
tools=[your_tool_function]
)
response, tool_result = router.get_response("user query here")
```
## Tool Design
Tools are Python functions. The agent reads `__name__`, `__doc__`, and `__annotations__` to decide when to use them.
```python
def your_tool(param1: str, param2: int) -> str:
"""Clear description of what this tool does and when to use it."""
# tool logic here
return "result as string"
```
Key rules:
- Docstrings directly influence when the agent selects the tool. Be specific: "Get current weather conditions for a city" is better than "Get weather".
- Type hints guide what inputs the agent provides. Complex types like `filters: List[Dict]` may need additional description in the docstring.
- Return strings or string-serializable data.
## Sequential Multi-Step Agent
For tasks requiring multiple tool calls in succession, add a followup signature and loop.
Followup signature (receives `tool_output` from the previous step):
```python
class AgentFollowup(dspy.Signature):
# Input Fields
history: dspy.History = dspy.InputField()
user_prompt: str = dspy.InputField()
tool_output: str = dspy.InputField(description="The output of the previous tool.")
available_tools: str = dspy.InputField(
description="The available tools and their descriptions."
)
# Output Fields
response: str = dspy.OutputField(
description="The response to the user's prompt whilst the tool is running. Update the user on the progress of their request (if a tool is picked), or the final response to the user (if no tool is picked)."
)
tool: str | None = dspy.OutputField(
description="The tool that needs to be used. Return None if no tool is needed."
)
tool_inputs: Dict[str, Any] | None = dspy.OutputField(
description="The inputs for the tool. Return an empty dictionary (still include the field) if no inputs are needed. The key is the name of the input, the value is the value of the input.",
)
```
Modify `RouterAgent` to loop until the agent stops requesting tools:
```python
class RouterAgent:
def __init__(self, model: str, tools: List[Callable] = []):
self.tools: list[Callable] = tools
self.model = dspy.LM(model)
self.agent = dspy.ChainOfThought(AgentResponse)
self.followup_agent = dspy.ChainOfThought(AgentFollowup)
self.conversation_history = dspy.History(messages=[])
def add_conversation_history(self, message: str, response: dspy.Prediction):
self.conversation_history.messages.append({"user_prompt": message, **response})
def get_tools_and_descriptions(self) -> str:
return "\n".join(
[
f"{tool.__name__}:\nDescription: {tool.__doc__ or ''}\nInputs: { {k: v for k, v in tool.__annotations__.items() if k != 'return'} }"
for tool in self.tools
]
)
def get_response(self, user_prompt: str) -> str:
result = self.agent(
history=self.conversation_history,
user_prompt=user_prompt,
available_tools=self.get_tools_and_descriptions(),
lm=self.model,
)
self.add_conversation_history(message=user_prompt, response=result)
max_iter = 10
iter = 0
while result.tool is not None and result.tool.lower() not in ["null", "none"]:
iter += 1
if iter > max_iter:
break
tool_function = next(
(tool for tool in self.tools if tool.__name__ == result.tool), None
)
if tool_function is None:
raise ValueError(f"Tool {result.tool} not found")
tool_inputs = {k: v for k, v in result.tool_inputs.items() if k != "return"}
tool_result = tool_function(**tool_inputs)
result = self.followup_agent(
history=self.conversation_history,
tool_output=tool_result,
available_tools=self.get_tools_and_descriptions(),
lm=self.model,
)
self.add_conversation_history(message=tool_result, response=result)
return result.response
```
`max_iter` controls how many tool calls can occur before forced termination. Increase for complex multi-step tasks, decrease to limit costs and runaway loops.
## User-specific Customisations
If not specified ask the user about these points before implementing their respective strategies:
**LLM Framework**
You can use DSPy (works with all LiteLLM providers) or LiteLLM itself.
- DSPy: https://dspy.ai/learn/programming/language_models/
- LiteLLM: https://docs.litellm.ai/docs/
Alternatively, users can use a single model provider. What model provider will they use?
- OpenAI (https://platform.openai.com/docs/libraries)
- Anthropic (https://platform.claude.com/docs/)
- Google GenAI (https://ai.google.dev/gemini-api/docs/libraries)
- Other (such as locally hosted models), use best judgement
These may require additional installs.
**Model Selection**
What model(s) will the user use? Consider a mixed approach: capable model for main agent routing, cheaper model for auxiliary tasks like memory creation.
**Tools**
What tools does the user need? List their functions, inputs, and expected outputs. The agent is only as capable as its tools.
**Single-step vs Multi-step**
Does the user need a single tool call per query, or should the agent chain multiple tools in sequence? Only use multi-step if the use case requires it.
## Troubleshooting
- DSPy signature warnings about missing fields: ensure all input fields are passed or mark optional fields appropriately.
- Tool not found errors: ensure tool function names match exactly what the agent outputs.
- Agent loops indefinitely: lower `max_iter` or add more explicit termination conditions.
- For any other issues, refer to the official library/package documentation and use web search extensively for troubleshooting.
## Done Criteria
- Create test scripts to check each function works independently with test data. Tear down tests after completion, or create a proper test suite with pytest (requires install)
- User has completed specification of the app.
@@ -0,0 +1,219 @@
# Basic RAG Cookbook
Build basic RAG functionality with Weaviate.
For advanced strategies, [see here](./advanced_rag.md).
Docs to reference if needed:
- Search patterns and basics in Weaviate: https://docs.weaviate.io/weaviate/search/basics
- Filters in Weaviate: https://docs.weaviate.io/weaviate/search/filters
- Vector search: https://docs.weaviate.io/weaviate/search/similarity
- Keyword search: https://docs.weaviate.io/weaviate/search/bm25
- Hybrid search: https://docs.weaviate.io/weaviate/search/hybrid
- Image search: https://docs.weaviate.io/weaviate/search/image
## Core Rules
- Use a virtual environment via `venv`
- Use `uv` for Python project/dependency management.
- Do not manually author `pyproject.toml` or `uv.lock`; let `uv` generate/update them.
- Use this install set: `uv add weaviate-client python-dotenv dspy`
- Customise this cookbook to the users specification, ask them for details if not given.
Assume the user has data already to be used, do not create data unless asked to.
## Env Rules
Mandatory:
- `WEAVIATE_URL`
- `WEAVIATE_API_KEY`
External provider keys:
- Fill only keys actually used by the target Weaviate collection setup.
## Weaviate Client
```python
import os
from weaviate import connect_to_weaviate_cloud
client = connect_to_weaviate_cloud(
cluster_url=os.getenv("WEAVIATE_URL", ""),
auth_credentials=os.getenv("WEAVIATE_API_KEY", ""),
headers={
"X-OpenAI-Api-Key": os.getenv("OPENAI_API_KEY")
},
)
```
If the user's collections require vectorizer provider keys, set the matching keys listed in `environment_requirements.md`.
Clients must be closed after completion. Wrap in `try/finally` blocks with `client.close()` (and `client.connect()` to reconnect if needed).
## Multi-tenancy
Multi-tenancy should be checked via
```python
config = await collection.config.get()
config.multi_tenancy_config.enabled # bool
```
e.g.
```python
base_collection = client.collections.use(collection_name)
config = collection.config.get()
if config.multi_tenancy_config.enabled:
collection = base_collection.with_tenant("<tenant_name>")
else:
collection = base_collection
```
Tenant names can be obtained via
```python
all_tenants = list(collection.tenants.get().keys())
```
## Basic Retrieval
Use collections via
```python
collection = client.collections.use("<collection_name>")
```
Weaviate can use vector, keyword or hybrid search.
```python
collection.query.near_text # semantic (text)
collection.query.bm25 # keyword
collection.query.hybrid # blend of keyword and semantic
```
It can also do image search
```python
collection.query.near_image(
near_image = ... # base 64 representation of image or Path object to image
)
```
## Key Code Blocks
RAG should have 4 pieces of core functionality:
1. Pre-retrieval
2. Retrieval
3. Post-retrieval
4. Generation
These should all be separate functions and combined into a single function, leaving scope for later editing or for the user themselves to modify it, to keep it understandable.
## Pre-retrieval
Transform the user question into a vector-database style (list of) query(ies). Basic RAG will provide no extra query transformations.
```python
def query_transformation(query: str) -> list[str]:
return [query]
```
## Retrieval
```python
def retrieve(
query: str,
limit: int = 10, # optional
filters = [] # optional
# additional arguments if required can go here and passed down to the search strategy
) -> list[dict]:
# import client logic here
collection = client.collections.use("<collection_name>")
response = collection.query.near_text( # or hybrid, bm25, near_image
query=query,
limit=limit,
filters=filters if filters else None
)
return [
{
**obj.properties,
"uuid": obj.uuid
}
for obj in response.objects
]
```
## Post-Retrieval
Modify the output of `retrieve`. Basic RAG will provide no extra post-processing. But you can consider adding uniqueness checks, formatting to remove properties, or more.
```python
def process_retrieval_results(objects: list[dict]) -> list[dict]:
return objects
```
## Generation
This step depends on your LLM framework, [see below](#user-specific-customisations). Using DSPy:
```python
import dspy
def generate(query: str, context: list[dict]) -> str:
lm = dspy.LM("<model_name>") # e.g. gpt-5.2, gpt-5-mini, claude-sonnet-4-5, etc.
answer = dspy.Predict("context, query -> answer") # inputs: context, query. outputs: answer
pred = answer(context=context, query=query, lm=lm)
return pred.answer # answer is then an attribute of pred
```
## User-specific Customisations
If not specified ask the user about these points before implementing their respective strategies:
**LLM Framework**
You can use DSPy (works with all LiteLLM providers) or LiteLLM itself.
- DSPy: https://dspy.ai/learn/programming/language_models/
- LiteLLM: https://docs.litellm.ai/docs/
Alternatively, users can use a single model provider. What model provider will they use?
- OpenAI (https://platform.openai.com/docs/libraries)
- Anthropic (https://platform.claude.com/docs/)
- Google GenAI (https://ai.google.dev/gemini-api/docs/libraries)
- Other (such as locally hosted models), use best judgement
These may require additional installs.
**Collections**
Do collections already exist and what are they called? Does the user want to query multiple collections or just a single one? Does it need to be customisable?
What format is the data, images or text or something else? What vectoriser is the collection set up as? What API keys are needed?
**Search strategy**
Does the user want semantic, keyword or hybrid search?
Hybrid search has an `alpha` parameter, controlling tradeoff between keyword and semantic weights. `alpha=1` is pure semantic, `alpha=0` is pure keyword.
## Troubleshooting
- Weaviate startup host errors: ensure `WEAVIATE_URL` is full `https://...` URL.
- For any other issues, refer to the official library/package documentation and use web search extensively for troubleshooting.
## Done Criteria
- Create test scripts to check each function works independently with test data. Tear down tests after completion, or create a proper test suite with pytest (requires install)
- User has completed specification of the app.
@@ -0,0 +1,336 @@
# Build Data Explorer App
## Overview
Build a full-stack Data Explorer App for Weaviate Collections with FastAPI.
Read first:
- Search patterns and basics in Weaviate: https://docs.weaviate.io/weaviate/search/basics
- Filters in Weaviate: https://docs.weaviate.io/weaviate/search/filters
## Instructions
### Core Rules
- Use a virtual environment via `venv`
- Use `uv` for Python project/dependency management.
- Do not manually author `pyproject.toml` or `uv.lock`; let `uv` generate/update them.
- Use this backend install set:
- `uv add fastapi 'uvicorn[standard]' weaviate-client pydantic-settings python-dotenv`
- Depending on user request: consider combining this app with the [Query Agent Chatbot](./query_agent_chatbot.md).
- If the user explicitly only wants a data viewer/explorer, create this app independently
- If the user wants a fully featured chat and data explorer, combine the apps
- If no explicit instructions are given, ask the user their preference before continuing
- See the [Next Steps](#next-steps) section for more details
### Fast Setup Commands
Project bootstrap:
```bash
uv init data_explorer
cd data_explorer
uv venv
uv add fastapi 'uvicorn[standard]' weaviate-client pydantic-settings python-dotenv
```
### Workflow Contract
1. Build backend and frontend in one pass.
2. Create `.env` from the canonical template in `environment_requirements.md`, then add app-specific fields (for example, `CORS_ORIGINS`).
3. Before asking user to fill env, do non-secret local sanity checks that do not require real credentials (imports/compile/startup-shape checks).
4. Ask user to fill real env values:
- Mandatory: `WEAVIATE_URL`, `WEAVIATE_API_KEY`
- Optional: only provider keys required by their collection setup
5. After the user confirms, verify backend starts without errors and provide exact commands to run in the terminal.
Do not ask avoidable questions that you can resolve from context.
### Directory Structure
Use a modular layout like:
```text
data_explorer/
backend/
app/
main.py
config.py
lifespan.py
dependencies.py
routers/
services/
models/
.env # local file, never committed
```
Keep these boundaries:
- routers: HTTP only
- services: business/query-agent logic
- models: request/response schemas
- config/lifespan: wiring and startup/shutdown
### Backend Requirements
- FastAPI async app with lifespan.
- Async Weaviate client initialized in lifespan and closed on shutdown.
- Ensure no async blocking operations.
- Not a full CRUD implementation - this is only for viewing data in a Weaviate collection.
- Endpoints for:
- `GET /health`
- `GET /env_check`: returns what API keys are missing (if any) for verification on app start
- `GET /collections`: return available collections
- `GET /data/{collection_name}?xx=xx&yy=yy`: return data with optional arguments (more later), and pagination
- Pydantic settings should read from process environment; local `.env` loading is optional for local development.
- Conversation history mapping to Weaviate chat message format.
### Env Rules
Mandatory:
- `WEAVIATE_URL`
- `WEAVIATE_API_KEY`
External provider keys:
- Include every provider key needed by the target collections.
- Leave unused provider keys empty/commented.
CORS:
- Default `CORS_ORIGINS` should include:
- `http://localhost:3000`
- `http://127.0.0.1:3000`
- `http://localhost:5173`
- `http://127.0.0.1:5173`
### FastAPI standards
1. Do not use hardcoded status values, use `status` from FastAPI, for example:
```python
from fastapi import status
status.HTTP_200_OK # code 200
status.HTTP_404_NOT_FOUND # code 404
# and more
```
2. Use a Pydantic `BaseModel` for the `request` and `response_model` in all endpoints that require it. Ensure schema validation to mitigate user-error on the API.
3. Use path parameters and query parameters for GET endpoints instead of payloads, for example:
```python
@app.get("/items/{item_id}")
async def read_item(item_id: str):
return {"item_id": item_id}
```
```python
@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
return fake_items_db[skip : skip + limit]
```
4. Implement best practices for error-handling, do early returns and provide the correct status codes when necessary.
5. Use proper logging for API usage, not simple print statements.
### FastAPI endpoints
Basic structure of endpoints. Customise according to user preference or suitability. Do not follow exactly, this is a guideline only.
Ensure you also set up standard FastAPI procedures, such as global error handling, logging, dependencies. Set up an async client manager that connects on startup (via lifespan) and closes gracefully on app exit, use a dependency injection to add the client to the relevant endpoints.
#### GET /health
This is a standard health check. For example:
```python
from pydantic import BaseModel
class HealthResponse(BaseModel):
status: str
@app.get("/health", tags=["health"], response_model=HealthResponse)
async def health_check() -> HealthResponse:
logger.info("Health check requested")
return HealthResponse(status="healthy")
```
#### GET /env_check
Check what environment variables the backend has access to, used to verify the user's Weaviate configuration is correct. For example:
```python
import os
from pydantic import BaseModel
class EnvCheckResponse(BaseModel):
weaviate_url: bool
weaviate_api_key: bool
@app.get("/env_check", tags=["health"])
async def env_check() -> EnvCheckResponse:
logger.info("Environment check requested")
return EnvCheckResponse(
weaviate_url = os.getenv("WEAVIATE_URL") is not None,
weaviate_api_key = os.getenv("WEAVIATE_API_KEY") is not None,
)
```
### GET /collections
Check what collections are available. For example:
```python
from pydantic import BaseModel
from weaviate.client import WeaviateAsyncClient
class CollectionsResponse(BaseModel):
collections: list[str]
@app.get("/collections", tags=["collections"])
async def collections() -> CollectionsResponse:
# include client management to import async client here
logger.info("Collections requested")
collections = await client.collections.list_all()
return CollectionsResponse(
collections = list(collections.keys())
)
```
Tip: consider expanding this endpoint to include collection descriptions and configs. `await client.collections.list_all()` returns `dict[str, _CollectionConfigSimple]` where `_CollectionConfigSimple` contains attributes:
- `description`: `str`
- `properties`: `list[Property]` where `Property` has `.name`, `.description` and `.data_type` (accessed via `.data_type[:]` to get name of data type as string)
- `vector_config`: `dict[str, _NamedVectorConfig]` where `_NamedVectorConfig` has attribute `.vectorizer.vectorizer` (not a typo) which can be accessed via `.vectorizer.vectorizer[:]` to get the name of the vectoriser as a string.
Multi-tenancy should be checked via
```python
config = await collection.config.get()
config.multi_tenancy_config.enabled # bool
```
This is not available in the `_CollectionConfigSimple`, it must be fetched from `collection.config.get()`.
#### GET /data/{collection_name}
Retrieve data from a collection, using pagination, sorting and filters.
```python
from weaviate.collections import CollectionAsync
from fastapi import Query
from pydantic import BaseModel
from typing import Any
async def get_collection_data_types(collection: CollectionAsync) -> dict[str, str]:
config = await collection.config.get()
properties = config.properties
return {prop.name: prop.data_type[:] for prop in properties}
class GetDataResponse(BaseModel):
data_types: dict[str, str]
items: list[dict[str, Any]]
@router.post("/data/{collection_name}")
async def get_data(
collection_name: str,
page_size: int = Query(default=10, ge=1, le=100),
page_number: int = Query(default=1, ge=1),
query: str = Query(default=""),
sort_on: str = Query(default=None),
ascending: bool = Query(default=True),
) -> GetDataResponse:
# include client management to import async client here
collection = await client.collections.use(collection_name)
data_types = await async_get_collection_data_types(collection)
if query != "":
response = await collection.query.bm25(
query=query,
limit=page_size,
offset=page_size * (page_number - 1),
)
elif sort_on is not None:
response = await collection.query.fetch_objects(
sort=Sort.by_property(name=sort_on, ascending=ascending),
limit=page_size,
offset=page_size * (page_number - 1),
)
else:
response = await collection.query.fetch_objects(
limit=page_size,
offset=page_size * (page_number - 1),
)
return GetDataResponse(data_types = data_types, items = [obj.properties for obj in response.objects])
```
Tip: some collections can have multi-tenancy.
Consider adding the tenant as an optional query parameter to `get_data`, e.g.
```python
async def get_data(
... # existing args
tenant: str | None = Query(default=None)
):
base_collection = await client.collections.use(collection_name)
data_types = await async_get_collection_data_types(collection)
config = await collection.config.get()
if config.multi_tenancy_config.enabled and tenant and tenant.strip():
collection = base_collection.with_tenant(tenant)
else:
collection = base_collection
# ...existing code
```
### Post-Env Hand-Holding (Required)
After user says required env values are set, provide the terminal commands to run the backend:
```bash
cd data_explorer/backend
uv run uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
```
Then:
- Ask user to start terminal.
- Run smoke tests yourself against running services.
- Report pass/fail in plain language and fix blockers.
Do not offload detailed testing steps to the user unless they explicitly ask.
## Troubleshooting
- Weaviate startup host errors: ensure `WEAVIATE_URL` is full `https://...` URL.
- For any other issues, refer to the official library/package documentation using web search.
## Done Criteria
- Backend healthy.
- All endpoints work.
- User can run server in terminal with provided commands.
## Next Steps
This application is currently a data explorer backend. You may optionally offer to integrate it with the [Query Agent Chatbot](./query_agent_chatbot.md) based on user preference.
If the user chooses to combine these two applications, implement the integration as follows:
- Create or use a directory `/routes` which separate functions for query agent chat and data exploration. Import the routers in the `main.py` file
- If a frontend is requested, the frontend should have multiple pages/tabs depending on design choices so that data exploration and chat is separated
- Consider crossovers between functionalities, e.g. a chat button from the data viewer/collection viewer which takes the user to chat with that collection selected.
- Run quick tests to ensure the integration is seamless and the user can use both the chatbot and data explorer without any issues.
### Frontend
When the user explicitly asks for a frontend, use this reference as guideline:
- [Frontend Interface](frontend_interface.md): Build a Next.js frontend to interact with the Weaviate backend.
@@ -0,0 +1,78 @@
# Environment Requirements for Weaviate
Use this reference when building apps that connect to Weaviate and require external inference provider keys.
## Required Weaviate Auth
- `WEAVIATE_URL`
- `WEAVIATE_API_KEY`
## External Provider Env Vars and Headers
| Provider | Environment Variable(s) | Header(s) sent to Weaviate |
|----------|--------------------------|-----------------------------|
| Anthropic | `ANTHROPIC_API_KEY` | `X-Anthropic-Api-Key` |
| Anyscale | `ANYSCALE_API_KEY` | `X-Anyscale-Api-Key` |
| AWS | `AWS_ACCESS_KEY`, `AWS_SECRET_KEY` | `X-Aws-Access-Key`, `X-Aws-Secret-Key` |
| Cohere | `COHERE_API_KEY` | `X-Cohere-Api-Key` |
| Databricks | `DATABRICKS_TOKEN` | `X-Databricks-Token` |
| Friendli | `FRIENDLI_TOKEN` | `X-Friendli-Api-Key` |
| Google Vertex AI | `VERTEX_API_KEY` | `X-Goog-Vertex-Api-Key` |
| Google AI Studio | `STUDIO_API_KEY` | `X-Goog-Studio-Api-Key` |
| HuggingFace | `HUGGINGFACE_API_KEY` | `X-HuggingFace-Api-Key` |
| Jina AI | `JINAAI_API_KEY` | `X-JinaAI-Api-Key` |
| Mistral | `MISTRAL_API_KEY` | `X-Mistral-Api-Key` |
| NVIDIA | `NVIDIA_API_KEY` | `X-Nvidia-Api-Key` |
| OpenAI | `OPENAI_API_KEY` | `X-OpenAI-Api-Key` |
| Azure OpenAI | `AZURE_API_KEY` | `X-Azure-Api-Key` |
| Voyage AI | `VOYAGE_API_KEY` | `X-Voyage-Api-Key` |
| xAI | `XAI_API_KEY` | `X-Xai-Api-Key` |
## Usage Notes
- Set only the provider keys your collection configuration actually uses.
- If multiple providers are configured, include all corresponding headers.
## Canonical `.env` Template
Use this template in all cookbook apps. Then ask the user to fill only the values their app actually needs.
`WEAVIATE_URL` and `WEAVIATE_API_KEY` are mandatory for Weaviate-connected apps.
```dotenv
# Required for Weaviate cookbook apps (must be filled by user)
WEAVIATE_URL=
WEAVIATE_API_KEY=
# Common app-level settings (uncomment when needed by the selected cookbook)
# COLLECTIONS=
# CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173
# External provider keys (uncomment only what the target collection uses)
# ANTHROPIC_API_KEY=
# ANYSCALE_API_KEY=
# AWS_ACCESS_KEY=
# AWS_SECRET_KEY=
# AZURE_API_KEY=
# COHERE_API_KEY=
# DATABRICKS_TOKEN=
# FRIENDLI_TOKEN=
# HUGGINGFACE_API_KEY=
# JINAAI_API_KEY=
# MISTRAL_API_KEY=
# NVIDIA_API_KEY=
# OPENAI_API_KEY=
# STUDIO_API_KEY=
# VERTEX_API_KEY=
# VOYAGE_API_KEY=
# XAI_API_KEY=
```
## User Fill Guidance (Required)
1. Create a local `.env` file from this template.
2. Always ask the user to fill:
- `WEAVIATE_URL`
- `WEAVIATE_API_KEY`
3. Ask them to uncomment and fill only the provider keys their Weaviate collections require.
4. Keep `.env` local only and gitignored.
@@ -0,0 +1,104 @@
# Frontend Interface (Next.js + Weaviate Backend)
## Quick reference
| Item | Value |
| ------------ | ------------------------------------------------------------------------------------------- |
| **Stack** | Next.js (App Router), Tailwind v4, shadcn/ui, Framer Motion, react-icons, ai-sdk |
| **Node** | v25.3.0+ |
| **Backend** | `NEXT_PUBLIC_BACKEND_HOST` (default: `localhost:8000`) |
| **App type** | Single-page app; main view updates in place, no full-page navigations |
| **Layout** | shadcn Sidebar (left) + main content area; sidebar buttons switch the main view per feature |
---
## Setup (run in order)
### 1. Next.js
- **Command:** `npx create-next-app@latest . --yes` (run from repo root; may need `required_permissions: ["all"]` in sandbox)
- **Result:** TypeScript, ESLint, Tailwind v4, App Router, Turbopack, `@/*``./*`, no `src/`. App in `app/`, static in `public/`.
- **Scripts:** `dev` | `build` | `start` | `lint`. Dev server: http://localhost:3000.
- **Routes:** `app/layout.tsx`, `app/page.tsx`. Imports: `@/` = project root.
- **Ref:** [Next.js App Router Installation](https://nextjs.org/docs/app/getting-started/installation) — verify against current docs.
### 2. shadcn/ui
- **Requires:** Next.js + Tailwind v4 + App Router + `@/*`, no `src/`.
- **Init:** `npx shadcn@latest init -t next -y -b zinc --no-src-dir`
- **Add components:** `npx shadcn@latest add button -y` (e.g. `card`, `dialog`, `input`; `-o` overwrites).
- **Output:** `components.json`, `lib/utils.ts` (cn), `app/globals.css` (tw-animate, shadcn/tailwind.css, CSS vars). UI in `components/ui/<name>.tsx`. Import: `import { Button } from "@/components/ui/button"`.
- **Ref:** [shadcn Next.js](https://ui.shadcn.com/docs/installation/next) | [CLI](https://ui.shadcn.com/docs/cli).
### 3. Framer Motion
```bash
npm i framer-motion
```
- **Ref:** [Framer Motion](https://motion.dev/)
### 4. AI SDK (optional)
Note: Install only when create a conversational user interface for your chatbot application. It enables the streaming of chat messagesyou need to stream responses from the backend using useChat().
- **When:** Add this step only if the app needs a chat UI (e.g. query-agent or chatbot flows).
- **Stack:** Use the [Vercel AI SDK](https://ai-sdk.dev/docs/introduction) (`ai` + `@ai-sdk/*`). Use `useChat` and SDK UI primitives for the chat view.
- **Ref:** [AI SDK useChat](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) | [Next.js App Router setup](https://ai-sdk.dev/docs/getting-started/nextjs-app-router) — follow current docs for install and wiring.
```bash
npm i ai @ai-sdk/react zod
```
### 5. Environment
**Required:**
```bash
NEXT_PUBLIC_BACKEND_HOST="localhost:8000"
```
Use the actual backend host when not local.
---
## Rules (must follow)
### Stack and structure
- **UI:** Use **shadcn components only** for layout and interactive elements (buttons, cards, inputs, dialogs, etc.). Do not add another UI library.
- **Architecture:** **SPA** — one main page, update main view in place. Avoid full-page navigations unless necessary.
- **Icons:** Use **react-icons** only; prefer one set (e.g. `react-icons/fa` or `react-icons/hi`) for consistency.
- **Animation:** Use **Framer Motion** only. Do not add another animation library.
### Visual style
- **Goal:** Minimal, sleek, clean. No clutter, heavy borders, or noisy backgrounds.
- **Aesthetic:** “Liquid glass” — frosted, translucent; soft blur; light borders and shadows; depth without heaviness. Use `backdrop-blur`, semi-transparent fills, subtle gradients where they support this.
### Motion
- **Style:** Subtle, springy, purposeful (fade in, hover, enter/exit). Prefer spring physics over linear/ease-out.
### Layout
1. **Left:** shadcn **Sidebar** component.
2. **Right:** Main content area.
3. **Navigation:** One sidebar button per backend feature (e.g. data explorer, chat). Click switches the main view only.
### Responsiveness
- Layout and components must work on small and large screens.
---
## Docs (verify against current versions)
- [FastAPI](https://fastapi.tiangolo.com/) | [GitHub](https://github.com/fastapi/fastapi)
- [Node.js](https://nodejs.org/en)
- [Next.js](https://nextjs.org/docs)
- [Tailwind (Next.js)](https://tailwindcss.com/docs/installation/framework-guides/nextjs)
- [shadcn components](https://ui.shadcn.com/docs/components)
- [react-icons](https://react-icons.github.io/react-icons)
- [Framer Motion](https://motion.dev/)
- [AI SDK](https://ai-sdk.dev/docs/introduction)
@@ -0,0 +1,635 @@
# Multi-vector RAG: Building Multimodal Document Search Systems With Weaviate
## Overview
This cookbook provides instructions for implementing a Multimodal Retrieval-Augmented Generation (RAG) system over PDF document collections using Weaviate Embeddings multimodal model for embeddings and Ollama with a Vision Language Model (VLM) for generation.
Weaviate Embeddings handles all embedding generation server-side — no local GPU or model downloads required. Simply upload document images as base64 blobs and Weaviate generates multi-vector embeddings automatically.
### Architecture
A multimodal RAG system consists of two main pipelines:
**Ingestion Pipeline:**
- Documents (PDFs, images) are converted to page images
- Images are uploaded as base64 blobs to Weaviate
- Weaviate Embeddings generates multi-vector embeddings server-side using `ModernVBERT/colmodernvbert`
- Embeddings are stored in the vector index automatically
**Query Pipeline:**
- Text queries are sent to Weaviate, which embeds them server-side
- Relevant documents are retrieved using similarity search (MaxSim)
- Retrieved document images are passed to a Vision Language Model (VLM) running on Ollama with the query
- The VLM generates a natural language response based on visual and textual context
**Requirements:**
- Weaviate Cloud instance (Weaviate Embeddings is cloud-only)
- Python 3.11 or higher
- `uv` package manager ([installation guide](https://docs.astral.sh/uv/getting-started/installation/))
- [Ollama](https://ollama.com/) installed locally for VLM generation
## Workflow Instructions
### Step 1: Setup Project and Install Dependencies
#### Project Bootstrap
Initialize a new project with `uv`:
```bash
uv init multimodal-rag
cd multimodal-rag
uv venv
```
**Install uv if needed:**
```bash
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh -o /tmp/uv-install.sh
less /tmp/uv-install.sh
sh /tmp/uv-install.sh
# Or with pip
pip install uv
# Or with Homebrew
brew install uv
```
#### Install Core Dependencies
Install required libraries using `uv`:
```bash
uv add weaviate-client
```
**Package breakdown:**
- `weaviate-client`: Python client for Weaviate vector database (v4.x) — Weaviate Embeddings handles all embedding generation
#### Additional Dependencies (Install as Needed)
```bash
# For loading Hugging Face datasets
uv add datasets
# For PDF processing (pdf2image requires poppler to be installed!)
uv add pdf2image pillow
# For VLM generation via Ollama
uv add ollama
```
### Step 2: Prepare Your Document Dataset
#### Option A: Load Existing Dataset
If using a pre-existing dataset:
- Use Hugging Face `datasets` library
- Ensure dataset contains document images or can be converted to images
- Verify image format compatibility (JPEG, PNG)
#### Option B: Process Your Own Documents
For custom document collections:
1. Convert documents to images (if not already images)
- PDFs: Use `pdf2image` or similar libraries
- Office documents: Convert to PDF first, then to images
2. Organize with metadata (document ID, page number, title, etc.)
3. Store in a format suitable for batch processing
**Recommended structure:**
```python
{
"document_id": str,
"page_number": int,
"image": PIL.Image,
"metadata": dict # title, author, date, etc.
}
```
### Step 3: Configure Weaviate Collection
#### Weaviate Connection
```python
import os
import weaviate
from weaviate.classes.init import Auth
WEAVIATE_URL = os.getenv("WEAVIATE_URL")
WEAVIATE_API_KEY = os.getenv("WEAVIATE_API_KEY")
client = weaviate.connect_to_weaviate_cloud(
cluster_url=WEAVIATE_URL,
auth_credentials=Auth.api_key(WEAVIATE_API_KEY),
)
```
#### Create Collection Schema
Define a collection with `multi2vec_weaviate` vectorizer for automatic multimodal embeddings:
```python
from weaviate.classes.config import Configure, Property, DataType
collection_name = "PDFDocuments" # Use a descriptive name for your use case
collection = client.collections.create(
name=collection_name,
properties=[
Property(name="doc_page", data_type=DataType.BLOB),
Property(name="page_id", data_type=DataType.INT),
Property(name="document_id", data_type=DataType.TEXT),
Property(name="page_number", data_type=DataType.INT),
Property(name="title", data_type=DataType.TEXT),
# Add other metadata properties as needed
],
vector_config=[
Configure.MultiVectors.multi2vec_weaviate(
name="doc_vector"
image_field="doc_page",
model="ModernVBERT/colmodernvbert",
encoding=Configure.VectorIndex.MultiVector.Encoding.muvera(
ksim=4,
dprojections=16,
repetitions=20,
),
)
],
)
```
**Key Configuration Options:**
- **`doc_page`**: BLOB property that holds base64-encoded page images — the vectorizer reads this field
- **`image_field`**: Must match the BLOB property name (`"doc_page"`)
- **`model`**: `ModernVBERT/colmodernvbert` — 250M parameter late-interaction vision-language encoder, fine-tuned for visual document retrieval
- **MUVERA encoding**: Compresses multi-vectors into efficient single vectors while preserving retrieval quality
- `ksim`: Number of similar vectors to consider (default: 4)
- `dprojections`: Number of projection dimensions (default: 16)
- `repetitions`: Number of encoding repetitions (default: 20)
- **Properties**: Add all metadata you want to filter or display
**Without MUVERA encoding** (uses more memory but preserves full multi-vector representation):
```python
vector_config=[
Configure.MultiVectors.multi2vec_weaviate(
name="doc_vector",
image_field="doc_page",
model="ModernVBERT/colmodernvbert",
)
],
```
### Step 4: Index Documents
#### Convert Images to Base64
```python
import base64
from io import BytesIO
def image_to_base64(image):
"""Convert a PIL Image to a base64-encoded string.
Args:
image: PIL.Image object
Returns:
Base64-encoded string of the JPEG image
"""
buffer = BytesIO()
image.save(buffer, format="JPEG")
return base64.b64encode(buffer.getvalue()).decode("utf-8")
```
#### Batch Import
Weaviate Embeddings generates embeddings server-side during import — no local model needed:
```python
collection = client.collections.get(collection_name)
with collection.batch.dynamic() as batch:
for idx, document in enumerate(your_document_dataset):
# Convert image to base64
img_base64 = image_to_base64(document["image"])
# Add object to batch — Weaviate generates embeddings automatically
batch.add_object(
properties={
"doc_page": img_base64,
"page_id": document["page_id"],
"document_id": document["document_id"],
"page_number": document["page_number"],
"title": document.get("title", ""),
# Add other properties from your dataset
},
)
# Progress tracking
if idx % 25 == 0:
print(f"Indexed {idx+1}/{len(your_document_dataset)} documents")
# Clean up dataset if memory is limited
del your_document_dataset
print(f"Total documents indexed: {len(collection)}")
```
**Performance Tips:**
- **Batch size**: Weaviate automatically manages batch size with `dynamic()` mode
- **No local GPU needed**: Weaviate Embeddings runs server-side
- **Image format**: JPEG is recommended for smaller payload sizes
- **Large datasets**: Process in chunks, delete intermediate variables to free memory
### Step 5: Implement Retrieval
#### Basic Query Function
Weaviate handles query embedding automatically — just pass text:
```python
from weaviate.classes.query import MetadataQuery
def search_documents(query_text, limit=3):
"""Search for documents using Weaviate Embeddings multimodal model.
Args:
query_text: Natural language query string
limit: Number of results to return (default: 3)
Returns:
List of dicts with document properties, similarity scores, and base64 images
"""
collection = client.collections.get(collection_name)
# Search — Weaviate embeds the query server-side
# Include doc_page in return_properties to get the base64-encoded image blob
response = collection.query.near_text(
query=query_text,
limit=limit,
return_properties=["page_id", "document_id", "page_number", "title", "doc_page"],
return_metadata=MetadataQuery(distance=True),
)
# Process and format results
results = []
for i, obj in enumerate(response.objects):
props = obj.properties
results.append({
"rank": i + 1,
"page_id": props["page_id"],
"document_id": props["document_id"],
"page_number": props["page_number"],
"title": props["title"],
"distance": obj.metadata.distance,
"image_base64": props["doc_page"], # Already base64-encoded
})
return results
# Example usage
query = "How does DeepSeek-V2 compare against the LLaMA family of LLMs?"
results = search_documents(query, limit=3)
for result in results:
print(f"{result['rank']}) Distance: {result['distance']:.4f}, "
f"Title: \"{result['title']}\", Page: {result['page_number']}")
```
**Query Parameters:**
- **`limit`**: Number of results (1-10 recommended, consider VLM memory limits)
- **`return_metadata`**: Include `distance=True` to get similarity scores
- **Filters**: Add `filters=` for metadata filtering (see below)
**Accessing the image field in results:**
BLOB properties like `doc_page` are not returned by default when used as the `image_field` property of the `multi2vec_weaviate` vectorizer. You must request them explicitly via `return_properties` (as shown in `search_documents()` above). The returned blob is base64-encoded. The Ollama Python SDK's `images` key accepts raw `bytes` or path-like strings (not base64 strings), so decode with `base64.b64decode()` before passing to Ollama (as shown in `OllamaVLM.generate_answer()`).
#### Metadata Filtering
Add filters to narrow search scope by document properties:
```python
import weaviate.classes.config as wc
# Example: Filter by document ID
response = collection.query.near_text(
query="query text",
limit=5,
filters=wc.Filter.by_property("document_id").equal("paper_123"),
)
# Example: Filter by page range
response = collection.query.near_text(
query="query text",
limit=5,
filters=wc.Filter.by_property("page_number").less_than(10),
)
# Example: Combine multiple filters
from weaviate.classes.query import Filter
response = collection.query.near_text(
query="query text",
limit=5,
filters=(
Filter.by_property("document_id").equal("paper_123") &
Filter.by_property("page_number").less_than(10)
),
)
```
#### Hybrid Search
Combine vector search with BM25 keyword search:
```python
# Hybrid search: vector + keyword (Weaviate handles embedding)
response = collection.query.hybrid(
query="query text",
alpha=0.7, # 0.0=keyword only, 0.5=balanced, 1.0=vector only
limit=5,
)
```
**When to use hybrid search:**
- When exact keyword matches are important (e.g., searching for specific terms, IDs)
- To combine semantic understanding with exact text matching (BM25)
- Adjust `alpha` based on whether you prioritize semantic vs. keyword matching
### Step 6: Extend to Full RAG with a Vision Language Model
#### About Ollama
[Ollama](https://ollama.com/) makes it easy to run vision language models locally with a single command. No manual model downloads, GPU configuration, or dependency management required.
**Recommended VLM models for Ollama:**
- `qwen3-vl:4b`: ~4 GB, good for limited hardware
- `qwen3-vl:8b`: ~8 GB, better quality
- `qwen3-vl:32b`: ~32 GB, highest quality
- `gemma3`: Google's multimodal model, available in 4B/12B/27B sizes
- `llava`: LLaVA model, lightweight and fast
#### Install Ollama and Pull a Model
```bash
# Install Ollama (macOS/Linux)
curl -fsSL https://ollama.com/install.sh -o /tmp/ollama-install.sh
less /tmp/ollama-install.sh
sh /tmp/ollama-install.sh
# Or on macOS with Homebrew
brew install ollama
# Pull a vision language model
ollama pull qwen3-vl:4b
```
Verify the model is available:
```bash
ollama list
```
#### Implement Ollama VLM Wrapper
```python
import base64
import ollama
class OllamaVLM:
def __init__(self, model_name="qwen3-vl:4b"):
"""Initialize with an Ollama vision model name.
Args:
model_name: Ollama model tag (must support vision)
"""
self.model_name = model_name
def generate_answer(self, query, images_base64, max_tokens=128):
"""Generate text response based on query and retrieved document images.
Args:
query: String text query
images_base64: List of base64-encoded image strings (as returned by Weaviate)
max_tokens: Maximum tokens to generate (default: 128)
Returns:
Generated text answer as string
"""
# The Ollama SDK "images" key accepts bytes or path-like strings,
# so decode the base64 strings from Weaviate into raw bytes
images_bytes = [base64.b64decode(img) for img in images_base64]
response = ollama.chat(
model=self.model_name,
messages=[{
"role": "user",
"content": query,
"images": images_bytes,
}],
options={"num_predict": max_tokens},
)
return response["message"]["content"]
# Instantiate the VLM
vlm = OllamaVLM(model_name="qwen3-vl:4b")
```
#### Complete RAG Pipeline
```python
def multimodal_rag(query, num_documents=3, max_tokens=128):
"""Complete multimodal RAG pipeline using Weaviate Embeddings + Ollama VLM.
Args:
query: Natural language question
num_documents: Number of documents to retrieve (1-3 recommended)
max_tokens: Maximum tokens for VLM response
Returns:
Dict with query, answer, sources, and metadata
"""
# Step 1: Retrieve relevant documents (Weaviate handles embedding)
print(f"Searching for: {query}")
retrieved_docs = search_documents(query, limit=num_documents)
# Display retrieved sources
print(f"\nRetrieved {len(retrieved_docs)} documents:")
for doc in retrieved_docs:
print(f" - {doc['title']}, Page {doc['page_number']} "
f"(Distance: {doc['distance']:.4f})")
# Step 2: Extract base64 images from results
context_images = [doc["image_base64"] for doc in retrieved_docs]
# Step 3: Generate answer using Ollama VLM
print(f"\nGenerating answer...")
answer = vlm.generate_answer(query, context_images, max_tokens=max_tokens)
# Step 4: Return structured response
return {
"query": query,
"answer": answer,
"sources": retrieved_docs,
"num_sources": len(retrieved_docs)
}
# Example usage
query = "How does DeepSeek-V2 compare against the LLaMA family of LLMs?"
result = multimodal_rag(query, num_documents=1, max_tokens=128)
print(f"\nQuery: {result['query']}")
print(f"Answer: {result['answer']}")
print(f"\nBased on {result['num_sources']} source(s)")
```
#### Response Citation
Include source attribution in generated answers:
```python
def generate_with_citations(query, retrieved_docs, max_tokens=256):
"""Generate answer with source citations.
Args:
query: User question
retrieved_docs: List of documents from search_documents()
max_tokens: Maximum response length
Returns:
Answer string with embedded citations
"""
# Build source references
sources_text = "\n".join([
f"Source {i+1}: \"{doc['title']}\", Page {doc['page_number']}"
for i, doc in enumerate(retrieved_docs)
])
# Enhanced prompt with citation instructions
enhanced_query = f"""{query}
Available sources:
{sources_text}
Instructions: Answer the question based on the provided document images.
Cite sources in your answer using [Source N] notation."""
# Generate answer with citations
answer = vlm.generate_answer(
enhanced_query,
[doc["image_base64"] for doc in retrieved_docs],
max_tokens=max_tokens
)
return answer, retrieved_docs
# Example usage
query = "What is the architecture of GPT-4?"
answer, sources = generate_with_citations(query, search_documents(query, limit=3))
print(f"Answer: {answer}\n")
print("Sources:")
for src in sources:
print(f" - {src['title']}, Page {src['page_number']}")
```
## Troubleshooting
### Missing Environment Variables
```
Error: WEAVIATE_URL environment variable is not set
```
**Solution:** Set `WEAVIATE_URL` and `WEAVIATE_API_KEY` environment variables. See `environment_requirements.md`.
### Connection Errors
```
WeaviateConnectionError: Failed to connect to Weaviate
```
**Solution:** Verify `WEAVIATE_URL` is correct and your network can reach the Weaviate Cloud instance.
### Ollama Connection Error
```
ConnectionError: Failed to connect to Ollama
```
**Solution:** Make sure Ollama is running. Start it with:
```bash
ollama serve
```
### Ollama Model Not Found
```
ollama._types.ResponseError: model 'qwen3-vl:4b' not found
```
**Solution:** Pull the model first:
```bash
ollama pull qwen3-vl:4b
```
### Out of Memory (OOM) During VLM Generation
**Symptoms:** Out of memory errors when generating answers.
**Solutions:**
- Reduce `num_documents` — retrieve fewer documents (even 1 can work well)
- Reduce `max_tokens` — shorter responses use less memory
- Use a smaller model variant (`qwen3-vl:4b` instead of `8b`)
- Use API-based VLMs (GPT-4V, Claude, Gemini) to avoid local resource requirements entirely
### BLOB Property Not Returned in Query Results
**Symptom:** `doc_page` field is missing from query results.
**Solution:** BLOB properties used as `image_field` in `multi2vec_weaviate` are not returned by default. Specify them explicitly:
```python
response = collection.query.near_text(
query=query_text,
limit=limit,
return_properties=["page_id", "document_id", "page_number", "title", "doc_page"],
)
```
### Poppler Not Installed (PDF Processing)
```
Exception: Unable to get page count. Is poppler installed and in PATH?
```
**Solution:** Install poppler for `pdf2image`:
```bash
# macOS
brew install poppler
# Ubuntu/Debian
sudo apt-get install poppler-utils
```
### TypeError: unexpected keyword argument 'image_fields'
```
TypeError: _MultiVectors.multi2vec_weaviate() got an unexpected keyword argument 'image_fields'
```
**Cause:** The parameter is singular, not a list.
**Solution:** Use `image_field` (singular) instead of `image_fields`:
```python
Configure.MultiVectors.multi2vec_weaviate(
name="doc_vector",
image_field="doc_page",
...
)
```
## Done Criteria
The implementation is complete when:
- [ ] Project is initialized with `uv` and all dependencies are installed
- [ ] Document images are converted and uploaded to a Weaviate collection with `multi2vec_weaviate` vectorizer
- [ ] The collection uses `ModernVBERT/colmodernvbert` model with MUVERA encoding configured
- [ ] `search_documents()` returns ranked results with similarity scores for text queries
- [ ] Ollama with a vision language model generates natural language answers from retrieved document images
- [ ] The full `multimodal_rag()` pipeline retrieves documents and generates answers end-to-end
## Next Steps
- **Add metadata filtering** to narrow search scope by document ID, page range, or other properties
- **Implement hybrid search** combining vector similarity with BM25 keyword matching for better precision
- **Add response citations** using `generate_with_citations()` to attribute answers to source documents
- **Scale the dataset** by processing larger document collections with batch chunking and memory management
- **Swap in API-based VLMs** (GPT, Claude, Gemini) or other Ollama vision models (`gemma3`, `llava`) as alternatives
- **Evaluate retrieval quality** by testing queries against known-relevant documents and tuning MUVERA parameters
@@ -0,0 +1,75 @@
# Project Setup Contract (All Cookbooks)
Use this reference before generating any cookbook app.
## Goal
Set up a safe default project layout that prevents accidental secret leaks and keeps setup instructions consistent across all cookbooks.
## Required Order
1. Create project directory.
2. Initialize git immediately.
3. Create `.gitignore` before any local `.env` file.
4. Create `.env` from [environment_requirements.md](environment_requirements.md).
5. Ask user to fill required values (`WEAVIATE_URL`, `WEAVIATE_API_KEY`) and only the optional keys they need.
## Required Files
### `.gitignore`
```gitignore
# Python
__pycache__/
*.py[cod]
.venv/
# Node
node_modules/
.next/
out/
dist/
# Local env files (never commit secrets)
.env
.env.*
secrets/
# Common local artifacts
.DS_Store
```
### `.env`
- Use the canonical template as provided in [environment_requirements.md](environment_requirements.md).
- Keep real `.env` values local only.
## Git Baseline
Run these commands in every new cookbook app:
```bash
git init
git add .gitignore
git commit -m "initialize project baseline"
```
## Claude Safety Baseline (Recommended)
For projects developed with Claude Code, add deny rules for local secret files:
```json
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./**/.env)",
"Read(./**/.env.*)",
"Read(./secrets/**)"
]
}
}
```
Save this to `.claude/settings.json` at project root.
@@ -0,0 +1,163 @@
# Build Weaviate Query Agent Chatbot
## Overview
Build a full-stack Query Agent chatbot with minimal back-and-forth.
Read first:
- Weaviate Query Agent usage: https://docs.weaviate.io/agents/query/usage
## Instructions
### Core Rules
- Use `uv` for Python project/dependency management.
- Do not manually author `pyproject.toml` or `uv.lock`; let `uv` generate/update them.
- Use this backend install set:
- `uv add fastapi 'uvicorn[standard]' weaviate-client weaviate-agents pydantic-settings sse-starlette python-dotenv`
- If `uv` not available, create a `requirements.txt` for pip installation
- Depending on user request: consider combining this app with the Data Explorer.
- If the user explicitly only wants chatbot, create this app independently
- If the user wants a fully featured chat and data explorer, combine the apps
- If no explicit instructions are given, ask the user their preference before continuing
- See the [Next Steps](#next-steps) section for more details
### Fast Setup Commands
Project bootstrap:
```bash
uv init chatbot
cd chatbot
uv venv
uv add fastapi 'uvicorn[standard]' weaviate-client weaviate-agents pydantic-settings sse-starlette python-dotenv
```
### Workflow Contract
1. Build backend in one pass.
2. Create `.env` from the canonical template in `environment_requirements.md`, then add app-specific fields (for example, `COLLECTIONS`).
3. Before asking user to fill env, do non-secret local sanity checks that do not require real credentials (imports/compile/startup-shape checks).
4. Ask user to fill real env values:
- Mandatory: `WEAVIATE_URL`, `WEAVIATE_API_KEY`, `COLLECTIONS`
- Optional: only provider keys required by their collection setup
5. After the user confirms, verify backend starts without errors and provide exact commands to run it in terminal.
Do not ask avoidable questions that you can resolve from context.
### Directory Structure
Use a modular layout like:
```text
chatbot/
backend/
app/
main.py
config.py
lifespan.py
dependencies.py
routers/
services/
models/
.env # local file, never committed
```
Keep these boundaries:
- routers: HTTP only
- services: business/query-agent logic
- models: request/response schemas
- config/lifespan: wiring and startup/shutdown
### Backend Requirements
- FastAPI async app with lifespan.
- Async Weaviate client initialized in lifespan and closed on shutdown.
- Query Agent service layer (`ask` + `ask_stream`).
- For async FastAPI backends, use `AsyncQueryAgent` (not `QueryAgent`) so `await agent.ask(...)` and `async for ... in agent.ask_stream(...)` work correctly.
- Endpoints:
- `GET /health`
- `POST /chat`
- `POST /chat/stream` (SSE)
- Pydantic settings should read from process environment; local `.env` loading is optional for local development.
- Conversation history mapping to Weaviate chat message format.
### Source Handling
- For every ask response, normalize output into:
- `answer`: text from `response.final_answer` (fallback `""`)
- `sources`: list of `{ "collection": ..., "object_id": ... }` built from `response.sources`
- `source_count`: `len(sources)`
- `POST /chat` must return `answer`, `sources`, and `source_count`.
- `POST /chat/stream` must include the same fields in the final SSE event.
- If no sources are available, return `sources: []` and `source_count: 0`.
### Env Rules
Mandatory:
- `WEAVIATE_URL`
- `WEAVIATE_API_KEY`
- `COLLECTIONS`
External provider keys:
- Include every provider key needed by the target collections.
- Leave unused provider keys empty/commented.
CORS:
- Default `CORS_ORIGINS` should include:
- `http://localhost:3000`
- `http://127.0.0.1:3000`
- `http://localhost:5173`
- `http://127.0.0.1:5173`
### Post-Env Hand-Holding (Required)
After user says required env values are set, provide the terminal commands to run the backend:
```bash
cd chatbot/backend
uv run uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
```
Then:
- Ask user to start the terminal.
- Run smoke tests yourself against running services.
- Report pass/fail in plain language and fix blockers.
Do not offload detailed testing steps to the user unless they explicitly ask.
## Troubleshooting
- `OPTIONS /chat/stream 400`: fix CORS origin mismatch (`localhost` vs `127.0.0.1`).
- Weaviate startup host errors: ensure `WEAVIATE_URL` is full `https://...` URL.
- For any other issues, refer to the official library/package documentation using web search.
## Done Criteria
- Backend healthy.
- `/chat` works.
- `/chat/stream` streams progress/token/final.
- `/chat` and `/chat/stream` final include `sources` and `source_count`.
- User can run the server in the terminal with the provided commands.
## Next Steps
This application is currently a chatbot backend. You may optionally offer to integrate it with the [Data Explorer](./data_explorer.md) based on user preference.
If the user chooses to combine these two applications, implement the integration as follows:
- Create or use a directory `/routes` which separate functions for query agent chat and data exploration. Import the routers in the `main.py` file
- If a frontend is requested, the frontend should have multiple pages/tabs depending on design choices so that data exploration and chat is separated
- Consider crossovers between functionalities, e.g. a chat button from the data viewer/collection viewer which takes the user to chat with that collection selected.
- Run quick tests to ensure the integration is seamless and the user can use both the chatbot and data explorer without any issues.
### Frontend
When the user explicitly asks for a frontend, use this reference as guideline:
- [Frontend Interface](frontend_interface.md): Build a Next.js frontend to interact with the Weaviate backend.
- Render source citations from `sources` and `source_count` in the chat response UI.