opencode-skills-collection 3.1.10 → 3.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-skills/.antigravity-install-manifest.json +8 -1
- package/bundled-skills/browser-testing-with-devtools/SKILL.md +334 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/sources/sources.md +4 -0
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/drizzle-migration-conflict/SKILL.md +179 -0
- package/bundled-skills/drizzle-migration-conflict/references/ci-policy.md +87 -0
- package/bundled-skills/drizzle-migration-conflict/references/conflict-resolution.md +163 -0
- package/bundled-skills/drizzle-migration-conflict/references/report-template.md +69 -0
- package/bundled-skills/drizzle-migration-conflict/references/sources.md +51 -0
- package/bundled-skills/drizzle-migration-conflict/scripts/check_drizzle_migrations.py +721 -0
- package/bundled-skills/frontend-lighthouse/SKILL.md +348 -0
- package/bundled-skills/pre-release-review/SKILL.md +198 -0
- package/bundled-skills/pre-release-review/references/checklist.md +104 -0
- package/bundled-skills/pre-release-review/references/report-template.md +91 -0
- package/bundled-skills/re-create/SKILL.md +251 -0
- package/bundled-skills/weaviate/SKILL.md +132 -0
- package/bundled-skills/weaviate/references/ask.md +36 -0
- package/bundled-skills/weaviate/references/create_collection.md +152 -0
- package/bundled-skills/weaviate/references/environment_requirements.md +34 -0
- package/bundled-skills/weaviate/references/example_data.md +24 -0
- package/bundled-skills/weaviate/references/explore_collection.md +50 -0
- package/bundled-skills/weaviate/references/fetch_filter.md +88 -0
- package/bundled-skills/weaviate/references/get_collection.md +32 -0
- package/bundled-skills/weaviate/references/hybrid_search.md +47 -0
- package/bundled-skills/weaviate/references/import_data.md +160 -0
- package/bundled-skills/weaviate/references/keyword_search.md +38 -0
- package/bundled-skills/weaviate/references/list_collections.md +31 -0
- package/bundled-skills/weaviate/references/query_search.md +38 -0
- package/bundled-skills/weaviate/references/semantic_search.md +46 -0
- package/bundled-skills/weaviate/scripts/ask.py +106 -0
- package/bundled-skills/weaviate/scripts/create_collection.py +359 -0
- package/bundled-skills/weaviate/scripts/example_data.py +945 -0
- package/bundled-skills/weaviate/scripts/explore_collection.py +295 -0
- package/bundled-skills/weaviate/scripts/fetch_filter.py +261 -0
- package/bundled-skills/weaviate/scripts/get_collection.py +122 -0
- package/bundled-skills/weaviate/scripts/hybrid_search.py +157 -0
- package/bundled-skills/weaviate/scripts/import.py +701 -0
- package/bundled-skills/weaviate/scripts/keyword_search.py +142 -0
- package/bundled-skills/weaviate/scripts/list_collections.py +77 -0
- package/bundled-skills/weaviate/scripts/query_search.py +135 -0
- package/bundled-skills/weaviate/scripts/semantic_search.py +139 -0
- package/bundled-skills/weaviate/scripts/weaviate_conn.py +231 -0
- package/bundled-skills/weaviate-cookbooks/SKILL.md +67 -0
- package/bundled-skills/weaviate-cookbooks/references/advanced_rag.md +274 -0
- package/bundled-skills/weaviate-cookbooks/references/agentic_rag.md +360 -0
- package/bundled-skills/weaviate-cookbooks/references/async_client.md +428 -0
- package/bundled-skills/weaviate-cookbooks/references/basic_agent.md +270 -0
- package/bundled-skills/weaviate-cookbooks/references/basic_rag.md +219 -0
- package/bundled-skills/weaviate-cookbooks/references/data_explorer.md +336 -0
- package/bundled-skills/weaviate-cookbooks/references/environment_requirements.md +78 -0
- package/bundled-skills/weaviate-cookbooks/references/frontend_interface.md +104 -0
- package/bundled-skills/weaviate-cookbooks/references/pdf_multimodal_rag.md +635 -0
- package/bundled-skills/weaviate-cookbooks/references/project_setup.md +75 -0
- package/bundled-skills/weaviate-cookbooks/references/query_agent_chatbot.md +163 -0
- package/package.json +1 -1
- package/skills_index.json +156 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
# Multi-vector RAG: Building Multimodal Document Search Systems With Weaviate
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
### Architecture
|
|
10
|
+
|
|
11
|
+
A multimodal RAG system consists of two main pipelines:
|
|
12
|
+
|
|
13
|
+
**Ingestion Pipeline:**
|
|
14
|
+
- Documents (PDFs, images) are converted to page images
|
|
15
|
+
- Images are uploaded as base64 blobs to Weaviate
|
|
16
|
+
- Weaviate Embeddings generates multi-vector embeddings server-side using `ModernVBERT/colmodernvbert`
|
|
17
|
+
- Embeddings are stored in the vector index automatically
|
|
18
|
+
|
|
19
|
+
**Query Pipeline:**
|
|
20
|
+
- Text queries are sent to Weaviate, which embeds them server-side
|
|
21
|
+
- Relevant documents are retrieved using similarity search (MaxSim)
|
|
22
|
+
- Retrieved document images are passed to a Vision Language Model (VLM) running on Ollama with the query
|
|
23
|
+
- The VLM generates a natural language response based on visual and textual context
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
**Requirements:**
|
|
28
|
+
- Weaviate Cloud instance (Weaviate Embeddings is cloud-only)
|
|
29
|
+
- Python 3.11 or higher
|
|
30
|
+
- `uv` package manager ([installation guide](https://docs.astral.sh/uv/getting-started/installation/))
|
|
31
|
+
- [Ollama](https://ollama.com/) installed locally for VLM generation
|
|
32
|
+
|
|
33
|
+
## Workflow Instructions
|
|
34
|
+
|
|
35
|
+
### Step 1: Setup Project and Install Dependencies
|
|
36
|
+
|
|
37
|
+
#### Project Bootstrap
|
|
38
|
+
|
|
39
|
+
Initialize a new project with `uv`:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv init multimodal-rag
|
|
43
|
+
cd multimodal-rag
|
|
44
|
+
uv venv
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Install uv if needed:**
|
|
48
|
+
```bash
|
|
49
|
+
# macOS/Linux
|
|
50
|
+
curl -LsSf https://astral.sh/uv/install.sh -o /tmp/uv-install.sh
|
|
51
|
+
less /tmp/uv-install.sh
|
|
52
|
+
sh /tmp/uv-install.sh
|
|
53
|
+
|
|
54
|
+
# Or with pip
|
|
55
|
+
pip install uv
|
|
56
|
+
|
|
57
|
+
# Or with Homebrew
|
|
58
|
+
brew install uv
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Install Core Dependencies
|
|
62
|
+
|
|
63
|
+
Install required libraries using `uv`:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv add weaviate-client
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Package breakdown:**
|
|
70
|
+
- `weaviate-client`: Python client for Weaviate vector database (v4.x) — Weaviate Embeddings handles all embedding generation
|
|
71
|
+
|
|
72
|
+
#### Additional Dependencies (Install as Needed)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# For loading Hugging Face datasets
|
|
76
|
+
uv add datasets
|
|
77
|
+
|
|
78
|
+
# For PDF processing (pdf2image requires poppler to be installed!)
|
|
79
|
+
uv add pdf2image pillow
|
|
80
|
+
|
|
81
|
+
# For VLM generation via Ollama
|
|
82
|
+
uv add ollama
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Step 2: Prepare Your Document Dataset
|
|
86
|
+
|
|
87
|
+
#### Option A: Load Existing Dataset
|
|
88
|
+
If using a pre-existing dataset:
|
|
89
|
+
- Use Hugging Face `datasets` library
|
|
90
|
+
- Ensure dataset contains document images or can be converted to images
|
|
91
|
+
- Verify image format compatibility (JPEG, PNG)
|
|
92
|
+
|
|
93
|
+
#### Option B: Process Your Own Documents
|
|
94
|
+
For custom document collections:
|
|
95
|
+
1. Convert documents to images (if not already images)
|
|
96
|
+
- PDFs: Use `pdf2image` or similar libraries
|
|
97
|
+
- Office documents: Convert to PDF first, then to images
|
|
98
|
+
2. Organize with metadata (document ID, page number, title, etc.)
|
|
99
|
+
3. Store in a format suitable for batch processing
|
|
100
|
+
|
|
101
|
+
**Recommended structure:**
|
|
102
|
+
```python
|
|
103
|
+
{
|
|
104
|
+
"document_id": str,
|
|
105
|
+
"page_number": int,
|
|
106
|
+
"image": PIL.Image,
|
|
107
|
+
"metadata": dict # title, author, date, etc.
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Step 3: Configure Weaviate Collection
|
|
112
|
+
|
|
113
|
+
#### Weaviate Connection
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import os
|
|
117
|
+
import weaviate
|
|
118
|
+
from weaviate.classes.init import Auth
|
|
119
|
+
|
|
120
|
+
WEAVIATE_URL = os.getenv("WEAVIATE_URL")
|
|
121
|
+
WEAVIATE_API_KEY = os.getenv("WEAVIATE_API_KEY")
|
|
122
|
+
|
|
123
|
+
client = weaviate.connect_to_weaviate_cloud(
|
|
124
|
+
cluster_url=WEAVIATE_URL,
|
|
125
|
+
auth_credentials=Auth.api_key(WEAVIATE_API_KEY),
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Create Collection Schema
|
|
130
|
+
|
|
131
|
+
Define a collection with `multi2vec_weaviate` vectorizer for automatic multimodal embeddings:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from weaviate.classes.config import Configure, Property, DataType
|
|
135
|
+
|
|
136
|
+
collection_name = "PDFDocuments" # Use a descriptive name for your use case
|
|
137
|
+
|
|
138
|
+
collection = client.collections.create(
|
|
139
|
+
name=collection_name,
|
|
140
|
+
properties=[
|
|
141
|
+
Property(name="doc_page", data_type=DataType.BLOB),
|
|
142
|
+
Property(name="page_id", data_type=DataType.INT),
|
|
143
|
+
Property(name="document_id", data_type=DataType.TEXT),
|
|
144
|
+
Property(name="page_number", data_type=DataType.INT),
|
|
145
|
+
Property(name="title", data_type=DataType.TEXT),
|
|
146
|
+
# Add other metadata properties as needed
|
|
147
|
+
],
|
|
148
|
+
vector_config=[
|
|
149
|
+
Configure.MultiVectors.multi2vec_weaviate(
|
|
150
|
+
name="doc_vector"
|
|
151
|
+
image_field="doc_page",
|
|
152
|
+
model="ModernVBERT/colmodernvbert",
|
|
153
|
+
encoding=Configure.VectorIndex.MultiVector.Encoding.muvera(
|
|
154
|
+
ksim=4,
|
|
155
|
+
dprojections=16,
|
|
156
|
+
repetitions=20,
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Key Configuration Options:**
|
|
164
|
+
- **`doc_page`**: BLOB property that holds base64-encoded page images — the vectorizer reads this field
|
|
165
|
+
- **`image_field`**: Must match the BLOB property name (`"doc_page"`)
|
|
166
|
+
- **`model`**: `ModernVBERT/colmodernvbert` — 250M parameter late-interaction vision-language encoder, fine-tuned for visual document retrieval
|
|
167
|
+
- **MUVERA encoding**: Compresses multi-vectors into efficient single vectors while preserving retrieval quality
|
|
168
|
+
- `ksim`: Number of similar vectors to consider (default: 4)
|
|
169
|
+
- `dprojections`: Number of projection dimensions (default: 16)
|
|
170
|
+
- `repetitions`: Number of encoding repetitions (default: 20)
|
|
171
|
+
- **Properties**: Add all metadata you want to filter or display
|
|
172
|
+
|
|
173
|
+
**Without MUVERA encoding** (uses more memory but preserves full multi-vector representation):
|
|
174
|
+
```python
|
|
175
|
+
vector_config=[
|
|
176
|
+
Configure.MultiVectors.multi2vec_weaviate(
|
|
177
|
+
name="doc_vector",
|
|
178
|
+
image_field="doc_page",
|
|
179
|
+
model="ModernVBERT/colmodernvbert",
|
|
180
|
+
)
|
|
181
|
+
],
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Step 4: Index Documents
|
|
185
|
+
|
|
186
|
+
#### Convert Images to Base64
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
import base64
|
|
190
|
+
from io import BytesIO
|
|
191
|
+
|
|
192
|
+
def image_to_base64(image):
|
|
193
|
+
"""Convert a PIL Image to a base64-encoded string.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
image: PIL.Image object
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Base64-encoded string of the JPEG image
|
|
200
|
+
"""
|
|
201
|
+
buffer = BytesIO()
|
|
202
|
+
image.save(buffer, format="JPEG")
|
|
203
|
+
return base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### Batch Import
|
|
207
|
+
|
|
208
|
+
Weaviate Embeddings generates embeddings server-side during import — no local model needed:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
collection = client.collections.get(collection_name)
|
|
212
|
+
|
|
213
|
+
with collection.batch.dynamic() as batch:
|
|
214
|
+
for idx, document in enumerate(your_document_dataset):
|
|
215
|
+
# Convert image to base64
|
|
216
|
+
img_base64 = image_to_base64(document["image"])
|
|
217
|
+
|
|
218
|
+
# Add object to batch — Weaviate generates embeddings automatically
|
|
219
|
+
batch.add_object(
|
|
220
|
+
properties={
|
|
221
|
+
"doc_page": img_base64,
|
|
222
|
+
"page_id": document["page_id"],
|
|
223
|
+
"document_id": document["document_id"],
|
|
224
|
+
"page_number": document["page_number"],
|
|
225
|
+
"title": document.get("title", ""),
|
|
226
|
+
# Add other properties from your dataset
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Progress tracking
|
|
231
|
+
if idx % 25 == 0:
|
|
232
|
+
print(f"Indexed {idx+1}/{len(your_document_dataset)} documents")
|
|
233
|
+
|
|
234
|
+
# Clean up dataset if memory is limited
|
|
235
|
+
del your_document_dataset
|
|
236
|
+
|
|
237
|
+
print(f"Total documents indexed: {len(collection)}")
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Performance Tips:**
|
|
241
|
+
- **Batch size**: Weaviate automatically manages batch size with `dynamic()` mode
|
|
242
|
+
- **No local GPU needed**: Weaviate Embeddings runs server-side
|
|
243
|
+
- **Image format**: JPEG is recommended for smaller payload sizes
|
|
244
|
+
- **Large datasets**: Process in chunks, delete intermediate variables to free memory
|
|
245
|
+
|
|
246
|
+
### Step 5: Implement Retrieval
|
|
247
|
+
|
|
248
|
+
#### Basic Query Function
|
|
249
|
+
|
|
250
|
+
Weaviate handles query embedding automatically — just pass text:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from weaviate.classes.query import MetadataQuery
|
|
254
|
+
|
|
255
|
+
def search_documents(query_text, limit=3):
|
|
256
|
+
"""Search for documents using Weaviate Embeddings multimodal model.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
query_text: Natural language query string
|
|
260
|
+
limit: Number of results to return (default: 3)
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
List of dicts with document properties, similarity scores, and base64 images
|
|
264
|
+
"""
|
|
265
|
+
collection = client.collections.get(collection_name)
|
|
266
|
+
|
|
267
|
+
# Search — Weaviate embeds the query server-side
|
|
268
|
+
# Include doc_page in return_properties to get the base64-encoded image blob
|
|
269
|
+
response = collection.query.near_text(
|
|
270
|
+
query=query_text,
|
|
271
|
+
limit=limit,
|
|
272
|
+
return_properties=["page_id", "document_id", "page_number", "title", "doc_page"],
|
|
273
|
+
return_metadata=MetadataQuery(distance=True),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Process and format results
|
|
277
|
+
results = []
|
|
278
|
+
for i, obj in enumerate(response.objects):
|
|
279
|
+
props = obj.properties
|
|
280
|
+
results.append({
|
|
281
|
+
"rank": i + 1,
|
|
282
|
+
"page_id": props["page_id"],
|
|
283
|
+
"document_id": props["document_id"],
|
|
284
|
+
"page_number": props["page_number"],
|
|
285
|
+
"title": props["title"],
|
|
286
|
+
"distance": obj.metadata.distance,
|
|
287
|
+
"image_base64": props["doc_page"], # Already base64-encoded
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
return results
|
|
291
|
+
|
|
292
|
+
# Example usage
|
|
293
|
+
query = "How does DeepSeek-V2 compare against the LLaMA family of LLMs?"
|
|
294
|
+
results = search_documents(query, limit=3)
|
|
295
|
+
|
|
296
|
+
for result in results:
|
|
297
|
+
print(f"{result['rank']}) Distance: {result['distance']:.4f}, "
|
|
298
|
+
f"Title: \"{result['title']}\", Page: {result['page_number']}")
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Query Parameters:**
|
|
302
|
+
- **`limit`**: Number of results (1-10 recommended, consider VLM memory limits)
|
|
303
|
+
- **`return_metadata`**: Include `distance=True` to get similarity scores
|
|
304
|
+
- **Filters**: Add `filters=` for metadata filtering (see below)
|
|
305
|
+
|
|
306
|
+
**Accessing the image field in results:**
|
|
307
|
+
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()`).
|
|
308
|
+
|
|
309
|
+
#### Metadata Filtering
|
|
310
|
+
|
|
311
|
+
Add filters to narrow search scope by document properties:
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
import weaviate.classes.config as wc
|
|
315
|
+
|
|
316
|
+
# Example: Filter by document ID
|
|
317
|
+
response = collection.query.near_text(
|
|
318
|
+
query="query text",
|
|
319
|
+
limit=5,
|
|
320
|
+
filters=wc.Filter.by_property("document_id").equal("paper_123"),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Example: Filter by page range
|
|
324
|
+
response = collection.query.near_text(
|
|
325
|
+
query="query text",
|
|
326
|
+
limit=5,
|
|
327
|
+
filters=wc.Filter.by_property("page_number").less_than(10),
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Example: Combine multiple filters
|
|
331
|
+
from weaviate.classes.query import Filter
|
|
332
|
+
|
|
333
|
+
response = collection.query.near_text(
|
|
334
|
+
query="query text",
|
|
335
|
+
limit=5,
|
|
336
|
+
filters=(
|
|
337
|
+
Filter.by_property("document_id").equal("paper_123") &
|
|
338
|
+
Filter.by_property("page_number").less_than(10)
|
|
339
|
+
),
|
|
340
|
+
)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
#### Hybrid Search
|
|
344
|
+
|
|
345
|
+
Combine vector search with BM25 keyword search:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
# Hybrid search: vector + keyword (Weaviate handles embedding)
|
|
349
|
+
response = collection.query.hybrid(
|
|
350
|
+
query="query text",
|
|
351
|
+
alpha=0.7, # 0.0=keyword only, 0.5=balanced, 1.0=vector only
|
|
352
|
+
limit=5,
|
|
353
|
+
)
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**When to use hybrid search:**
|
|
357
|
+
- When exact keyword matches are important (e.g., searching for specific terms, IDs)
|
|
358
|
+
- To combine semantic understanding with exact text matching (BM25)
|
|
359
|
+
- Adjust `alpha` based on whether you prioritize semantic vs. keyword matching
|
|
360
|
+
|
|
361
|
+
### Step 6: Extend to Full RAG with a Vision Language Model
|
|
362
|
+
|
|
363
|
+
#### About Ollama
|
|
364
|
+
|
|
365
|
+
[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.
|
|
366
|
+
|
|
367
|
+
**Recommended VLM models for Ollama:**
|
|
368
|
+
- `qwen3-vl:4b`: ~4 GB, good for limited hardware
|
|
369
|
+
- `qwen3-vl:8b`: ~8 GB, better quality
|
|
370
|
+
- `qwen3-vl:32b`: ~32 GB, highest quality
|
|
371
|
+
- `gemma3`: Google's multimodal model, available in 4B/12B/27B sizes
|
|
372
|
+
- `llava`: LLaVA model, lightweight and fast
|
|
373
|
+
|
|
374
|
+
#### Install Ollama and Pull a Model
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
# Install Ollama (macOS/Linux)
|
|
378
|
+
curl -fsSL https://ollama.com/install.sh -o /tmp/ollama-install.sh
|
|
379
|
+
less /tmp/ollama-install.sh
|
|
380
|
+
sh /tmp/ollama-install.sh
|
|
381
|
+
|
|
382
|
+
# Or on macOS with Homebrew
|
|
383
|
+
brew install ollama
|
|
384
|
+
|
|
385
|
+
# Pull a vision language model
|
|
386
|
+
ollama pull qwen3-vl:4b
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Verify the model is available:
|
|
390
|
+
```bash
|
|
391
|
+
ollama list
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
#### Implement Ollama VLM Wrapper
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
import base64
|
|
398
|
+
import ollama
|
|
399
|
+
|
|
400
|
+
class OllamaVLM:
|
|
401
|
+
def __init__(self, model_name="qwen3-vl:4b"):
|
|
402
|
+
"""Initialize with an Ollama vision model name.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
model_name: Ollama model tag (must support vision)
|
|
406
|
+
"""
|
|
407
|
+
self.model_name = model_name
|
|
408
|
+
|
|
409
|
+
def generate_answer(self, query, images_base64, max_tokens=128):
|
|
410
|
+
"""Generate text response based on query and retrieved document images.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
query: String text query
|
|
414
|
+
images_base64: List of base64-encoded image strings (as returned by Weaviate)
|
|
415
|
+
max_tokens: Maximum tokens to generate (default: 128)
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Generated text answer as string
|
|
419
|
+
"""
|
|
420
|
+
# The Ollama SDK "images" key accepts bytes or path-like strings,
|
|
421
|
+
# so decode the base64 strings from Weaviate into raw bytes
|
|
422
|
+
images_bytes = [base64.b64decode(img) for img in images_base64]
|
|
423
|
+
|
|
424
|
+
response = ollama.chat(
|
|
425
|
+
model=self.model_name,
|
|
426
|
+
messages=[{
|
|
427
|
+
"role": "user",
|
|
428
|
+
"content": query,
|
|
429
|
+
"images": images_bytes,
|
|
430
|
+
}],
|
|
431
|
+
options={"num_predict": max_tokens},
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
return response["message"]["content"]
|
|
435
|
+
|
|
436
|
+
# Instantiate the VLM
|
|
437
|
+
vlm = OllamaVLM(model_name="qwen3-vl:4b")
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
#### Complete RAG Pipeline
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
def multimodal_rag(query, num_documents=3, max_tokens=128):
|
|
444
|
+
"""Complete multimodal RAG pipeline using Weaviate Embeddings + Ollama VLM.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
query: Natural language question
|
|
448
|
+
num_documents: Number of documents to retrieve (1-3 recommended)
|
|
449
|
+
max_tokens: Maximum tokens for VLM response
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Dict with query, answer, sources, and metadata
|
|
453
|
+
"""
|
|
454
|
+
# Step 1: Retrieve relevant documents (Weaviate handles embedding)
|
|
455
|
+
print(f"Searching for: {query}")
|
|
456
|
+
retrieved_docs = search_documents(query, limit=num_documents)
|
|
457
|
+
|
|
458
|
+
# Display retrieved sources
|
|
459
|
+
print(f"\nRetrieved {len(retrieved_docs)} documents:")
|
|
460
|
+
for doc in retrieved_docs:
|
|
461
|
+
print(f" - {doc['title']}, Page {doc['page_number']} "
|
|
462
|
+
f"(Distance: {doc['distance']:.4f})")
|
|
463
|
+
|
|
464
|
+
# Step 2: Extract base64 images from results
|
|
465
|
+
context_images = [doc["image_base64"] for doc in retrieved_docs]
|
|
466
|
+
|
|
467
|
+
# Step 3: Generate answer using Ollama VLM
|
|
468
|
+
print(f"\nGenerating answer...")
|
|
469
|
+
answer = vlm.generate_answer(query, context_images, max_tokens=max_tokens)
|
|
470
|
+
|
|
471
|
+
# Step 4: Return structured response
|
|
472
|
+
return {
|
|
473
|
+
"query": query,
|
|
474
|
+
"answer": answer,
|
|
475
|
+
"sources": retrieved_docs,
|
|
476
|
+
"num_sources": len(retrieved_docs)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
# Example usage
|
|
480
|
+
query = "How does DeepSeek-V2 compare against the LLaMA family of LLMs?"
|
|
481
|
+
result = multimodal_rag(query, num_documents=1, max_tokens=128)
|
|
482
|
+
|
|
483
|
+
print(f"\nQuery: {result['query']}")
|
|
484
|
+
print(f"Answer: {result['answer']}")
|
|
485
|
+
print(f"\nBased on {result['num_sources']} source(s)")
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### Response Citation
|
|
489
|
+
|
|
490
|
+
Include source attribution in generated answers:
|
|
491
|
+
|
|
492
|
+
```python
|
|
493
|
+
def generate_with_citations(query, retrieved_docs, max_tokens=256):
|
|
494
|
+
"""Generate answer with source citations.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
query: User question
|
|
498
|
+
retrieved_docs: List of documents from search_documents()
|
|
499
|
+
max_tokens: Maximum response length
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Answer string with embedded citations
|
|
503
|
+
"""
|
|
504
|
+
# Build source references
|
|
505
|
+
sources_text = "\n".join([
|
|
506
|
+
f"Source {i+1}: \"{doc['title']}\", Page {doc['page_number']}"
|
|
507
|
+
for i, doc in enumerate(retrieved_docs)
|
|
508
|
+
])
|
|
509
|
+
|
|
510
|
+
# Enhanced prompt with citation instructions
|
|
511
|
+
enhanced_query = f"""{query}
|
|
512
|
+
|
|
513
|
+
Available sources:
|
|
514
|
+
{sources_text}
|
|
515
|
+
|
|
516
|
+
Instructions: Answer the question based on the provided document images.
|
|
517
|
+
Cite sources in your answer using [Source N] notation."""
|
|
518
|
+
|
|
519
|
+
# Generate answer with citations
|
|
520
|
+
answer = vlm.generate_answer(
|
|
521
|
+
enhanced_query,
|
|
522
|
+
[doc["image_base64"] for doc in retrieved_docs],
|
|
523
|
+
max_tokens=max_tokens
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return answer, retrieved_docs
|
|
527
|
+
|
|
528
|
+
# Example usage
|
|
529
|
+
query = "What is the architecture of GPT-4?"
|
|
530
|
+
answer, sources = generate_with_citations(query, search_documents(query, limit=3))
|
|
531
|
+
print(f"Answer: {answer}\n")
|
|
532
|
+
print("Sources:")
|
|
533
|
+
for src in sources:
|
|
534
|
+
print(f" - {src['title']}, Page {src['page_number']}")
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
## Troubleshooting
|
|
538
|
+
|
|
539
|
+
### Missing Environment Variables
|
|
540
|
+
```
|
|
541
|
+
Error: WEAVIATE_URL environment variable is not set
|
|
542
|
+
```
|
|
543
|
+
**Solution:** Set `WEAVIATE_URL` and `WEAVIATE_API_KEY` environment variables. See `environment_requirements.md`.
|
|
544
|
+
|
|
545
|
+
### Connection Errors
|
|
546
|
+
```
|
|
547
|
+
WeaviateConnectionError: Failed to connect to Weaviate
|
|
548
|
+
```
|
|
549
|
+
**Solution:** Verify `WEAVIATE_URL` is correct and your network can reach the Weaviate Cloud instance.
|
|
550
|
+
|
|
551
|
+
### Ollama Connection Error
|
|
552
|
+
```
|
|
553
|
+
ConnectionError: Failed to connect to Ollama
|
|
554
|
+
```
|
|
555
|
+
**Solution:** Make sure Ollama is running. Start it with:
|
|
556
|
+
```bash
|
|
557
|
+
ollama serve
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Ollama Model Not Found
|
|
561
|
+
```
|
|
562
|
+
ollama._types.ResponseError: model 'qwen3-vl:4b' not found
|
|
563
|
+
```
|
|
564
|
+
**Solution:** Pull the model first:
|
|
565
|
+
```bash
|
|
566
|
+
ollama pull qwen3-vl:4b
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Out of Memory (OOM) During VLM Generation
|
|
570
|
+
**Symptoms:** Out of memory errors when generating answers.
|
|
571
|
+
|
|
572
|
+
**Solutions:**
|
|
573
|
+
- Reduce `num_documents` — retrieve fewer documents (even 1 can work well)
|
|
574
|
+
- Reduce `max_tokens` — shorter responses use less memory
|
|
575
|
+
- Use a smaller model variant (`qwen3-vl:4b` instead of `8b`)
|
|
576
|
+
- Use API-based VLMs (GPT-4V, Claude, Gemini) to avoid local resource requirements entirely
|
|
577
|
+
|
|
578
|
+
### BLOB Property Not Returned in Query Results
|
|
579
|
+
**Symptom:** `doc_page` field is missing from query results.
|
|
580
|
+
|
|
581
|
+
**Solution:** BLOB properties used as `image_field` in `multi2vec_weaviate` are not returned by default. Specify them explicitly:
|
|
582
|
+
```python
|
|
583
|
+
response = collection.query.near_text(
|
|
584
|
+
query=query_text,
|
|
585
|
+
limit=limit,
|
|
586
|
+
return_properties=["page_id", "document_id", "page_number", "title", "doc_page"],
|
|
587
|
+
)
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Poppler Not Installed (PDF Processing)
|
|
591
|
+
```
|
|
592
|
+
Exception: Unable to get page count. Is poppler installed and in PATH?
|
|
593
|
+
```
|
|
594
|
+
**Solution:** Install poppler for `pdf2image`:
|
|
595
|
+
```bash
|
|
596
|
+
# macOS
|
|
597
|
+
brew install poppler
|
|
598
|
+
|
|
599
|
+
# Ubuntu/Debian
|
|
600
|
+
sudo apt-get install poppler-utils
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### TypeError: unexpected keyword argument 'image_fields'
|
|
604
|
+
```
|
|
605
|
+
TypeError: _MultiVectors.multi2vec_weaviate() got an unexpected keyword argument 'image_fields'
|
|
606
|
+
```
|
|
607
|
+
**Cause:** The parameter is singular, not a list.
|
|
608
|
+
|
|
609
|
+
**Solution:** Use `image_field` (singular) instead of `image_fields`:
|
|
610
|
+
```python
|
|
611
|
+
Configure.MultiVectors.multi2vec_weaviate(
|
|
612
|
+
name="doc_vector",
|
|
613
|
+
image_field="doc_page",
|
|
614
|
+
...
|
|
615
|
+
)
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
## Done Criteria
|
|
619
|
+
|
|
620
|
+
The implementation is complete when:
|
|
621
|
+
- [ ] Project is initialized with `uv` and all dependencies are installed
|
|
622
|
+
- [ ] Document images are converted and uploaded to a Weaviate collection with `multi2vec_weaviate` vectorizer
|
|
623
|
+
- [ ] The collection uses `ModernVBERT/colmodernvbert` model with MUVERA encoding configured
|
|
624
|
+
- [ ] `search_documents()` returns ranked results with similarity scores for text queries
|
|
625
|
+
- [ ] Ollama with a vision language model generates natural language answers from retrieved document images
|
|
626
|
+
- [ ] The full `multimodal_rag()` pipeline retrieves documents and generates answers end-to-end
|
|
627
|
+
|
|
628
|
+
## Next Steps
|
|
629
|
+
|
|
630
|
+
- **Add metadata filtering** to narrow search scope by document ID, page range, or other properties
|
|
631
|
+
- **Implement hybrid search** combining vector similarity with BM25 keyword matching for better precision
|
|
632
|
+
- **Add response citations** using `generate_with_citations()` to attribute answers to source documents
|
|
633
|
+
- **Scale the dataset** by processing larger document collections with batch chunking and memory management
|
|
634
|
+
- **Swap in API-based VLMs** (GPT, Claude, Gemini) or other Ollama vision models (`gemma3`, `llava`) as alternatives
|
|
635
|
+
- **Evaluate retrieval quality** by testing queries against known-relevant documents and tuning MUVERA parameters
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Project Setup Contract (All Cookbooks)
|
|
2
|
+
|
|
3
|
+
Use this reference before generating any cookbook app.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Set up a safe default project layout that prevents accidental secret leaks and keeps setup instructions consistent across all cookbooks.
|
|
8
|
+
|
|
9
|
+
## Required Order
|
|
10
|
+
|
|
11
|
+
1. Create project directory.
|
|
12
|
+
2. Initialize git immediately.
|
|
13
|
+
3. Create `.gitignore` before any local `.env` file.
|
|
14
|
+
4. Create `.env` from [environment_requirements.md](environment_requirements.md).
|
|
15
|
+
5. Ask user to fill required values (`WEAVIATE_URL`, `WEAVIATE_API_KEY`) and only the optional keys they need.
|
|
16
|
+
|
|
17
|
+
## Required Files
|
|
18
|
+
|
|
19
|
+
### `.gitignore`
|
|
20
|
+
|
|
21
|
+
```gitignore
|
|
22
|
+
# Python
|
|
23
|
+
__pycache__/
|
|
24
|
+
*.py[cod]
|
|
25
|
+
.venv/
|
|
26
|
+
|
|
27
|
+
# Node
|
|
28
|
+
node_modules/
|
|
29
|
+
.next/
|
|
30
|
+
out/
|
|
31
|
+
dist/
|
|
32
|
+
|
|
33
|
+
# Local env files (never commit secrets)
|
|
34
|
+
.env
|
|
35
|
+
.env.*
|
|
36
|
+
secrets/
|
|
37
|
+
|
|
38
|
+
# Common local artifacts
|
|
39
|
+
.DS_Store
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `.env`
|
|
43
|
+
|
|
44
|
+
- Use the canonical template as provided in [environment_requirements.md](environment_requirements.md).
|
|
45
|
+
- Keep real `.env` values local only.
|
|
46
|
+
|
|
47
|
+
## Git Baseline
|
|
48
|
+
|
|
49
|
+
Run these commands in every new cookbook app:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git init
|
|
53
|
+
git add .gitignore
|
|
54
|
+
git commit -m "initialize project baseline"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Claude Safety Baseline (Recommended)
|
|
58
|
+
|
|
59
|
+
For projects developed with Claude Code, add deny rules for local secret files:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"permissions": {
|
|
64
|
+
"deny": [
|
|
65
|
+
"Read(./.env)",
|
|
66
|
+
"Read(./.env.*)",
|
|
67
|
+
"Read(./**/.env)",
|
|
68
|
+
"Read(./**/.env.*)",
|
|
69
|
+
"Read(./secrets/**)"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Save this to `.claude/settings.json` at project root.
|