opencode-skills-collection 3.1.9 → 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,701 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# dependencies = [
|
|
4
|
+
# "weaviate-client==4.19.2",
|
|
5
|
+
# "typer==0.21.0",
|
|
6
|
+
# "pdf2image>=1.17.0",
|
|
7
|
+
# "pillow>=10.0.0",
|
|
8
|
+
# ]
|
|
9
|
+
# ///
|
|
10
|
+
"""
|
|
11
|
+
Import data from CSV, JSON, JSONL, or PDF files to a Weaviate collection.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
uv run import.py data.csv --collection "CollectionName" [options]
|
|
15
|
+
uv run import.py document.pdf --collection "CollectionName" [options]
|
|
16
|
+
|
|
17
|
+
Environment Variables:
|
|
18
|
+
WEAVIATE_URL: Weaviate Cloud cluster URL
|
|
19
|
+
WEAVIATE_API_KEY: API key for authentication
|
|
20
|
+
+ Any provider API keys (OPENAI_API_KEY, COHERE_API_KEY, etc.) - auto-detected
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import base64
|
|
24
|
+
import csv
|
|
25
|
+
import itertools
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
from collections.abc import Iterator
|
|
30
|
+
from io import BytesIO
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
35
|
+
_DATETIME_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}$")
|
|
36
|
+
_RESERVED_FIELDS = {"id", "_additional"}
|
|
37
|
+
|
|
38
|
+
import typer
|
|
39
|
+
import weaviate
|
|
40
|
+
from weaviate.classes.config import Configure, DataType, Property
|
|
41
|
+
|
|
42
|
+
# Import shared connection utilities (local to this skill)
|
|
43
|
+
from weaviate_conn import get_client
|
|
44
|
+
|
|
45
|
+
# Types whose string values must never be JSON-parsed (already correct as strings)
|
|
46
|
+
_KEEP_AS_STRING = {DataType.TEXT, DataType.UUID, DataType.BLOB}
|
|
47
|
+
|
|
48
|
+
app = typer.Typer()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def detect_file_format(file_path: Path) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Detect file format based on extension.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
file_path: Path to the file
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
File format: "csv", "json", or "jsonl"
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If file format is not supported
|
|
63
|
+
"""
|
|
64
|
+
extension = file_path.suffix.lower()
|
|
65
|
+
|
|
66
|
+
if extension == ".csv":
|
|
67
|
+
return "csv"
|
|
68
|
+
elif extension == ".json":
|
|
69
|
+
return "json"
|
|
70
|
+
elif extension == ".jsonl":
|
|
71
|
+
return "jsonl"
|
|
72
|
+
elif extension == ".pdf":
|
|
73
|
+
return "pdf"
|
|
74
|
+
else:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Unsupported file format: {extension}. "
|
|
77
|
+
f"Supported formats: .csv, .json, .jsonl, .pdf"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def read_csv(
|
|
82
|
+
file_path: Path, mapping: dict[str, str] | None = None
|
|
83
|
+
) -> Iterator[dict[str, Any]]:
|
|
84
|
+
"""
|
|
85
|
+
Read data from CSV file with automatic dialect detection.
|
|
86
|
+
|
|
87
|
+
Yields rows one at a time — suitable for large files.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
file_path: Path to CSV file
|
|
91
|
+
mapping: Optional column name mapping
|
|
92
|
+
|
|
93
|
+
Yields:
|
|
94
|
+
Row dictionaries with data
|
|
95
|
+
"""
|
|
96
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
97
|
+
# Read a sample to detect the CSV dialect
|
|
98
|
+
sample = f.read(8192)
|
|
99
|
+
f.seek(0)
|
|
100
|
+
|
|
101
|
+
# Use Sniffer to detect the dialect (delimiter, quoting, etc.)
|
|
102
|
+
sniffer = csv.Sniffer()
|
|
103
|
+
try:
|
|
104
|
+
dialect = sniffer.sniff(sample)
|
|
105
|
+
except csv.Error:
|
|
106
|
+
dialect = csv.excel
|
|
107
|
+
|
|
108
|
+
reader = csv.DictReader(f, dialect=dialect)
|
|
109
|
+
|
|
110
|
+
# Warn if the header row looks like data (all-numeric or JSON-like values
|
|
111
|
+
# suggest the file has no header row and the first data row was misread as one).
|
|
112
|
+
if reader.fieldnames:
|
|
113
|
+
suspicious = [
|
|
114
|
+
k
|
|
115
|
+
for k in reader.fieldnames
|
|
116
|
+
if k
|
|
117
|
+
and (
|
|
118
|
+
k.lstrip("-").replace(".", "", 1).isdigit()
|
|
119
|
+
or k.startswith(("[", "{"))
|
|
120
|
+
)
|
|
121
|
+
]
|
|
122
|
+
if suspicious:
|
|
123
|
+
print(
|
|
124
|
+
f"Warning: CSV column names look like data values: {suspicious}. "
|
|
125
|
+
f"Ensure the first row is a header row with property names.",
|
|
126
|
+
file=sys.stderr,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
for row in reader:
|
|
130
|
+
# Apply mapping if provided
|
|
131
|
+
if mapping:
|
|
132
|
+
row = {mapping.get(k, k): v for k, v in row.items()}
|
|
133
|
+
yield row
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def read_json(
|
|
137
|
+
file_path: Path, mapping: dict[str, str] | None = None
|
|
138
|
+
) -> list[dict[str, Any]]:
|
|
139
|
+
"""
|
|
140
|
+
Read data from JSON file (expects array of objects).
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
file_path: Path to JSON file
|
|
144
|
+
mapping: Optional key name mapping
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
List of dictionaries with data
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
ValueError: If JSON is not an array
|
|
151
|
+
"""
|
|
152
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
153
|
+
data = json.load(f)
|
|
154
|
+
|
|
155
|
+
if not isinstance(data, list):
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"JSON file must contain an array of objects, got {type(data).__name__}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Apply mapping if provided
|
|
161
|
+
if mapping:
|
|
162
|
+
data = [{mapping.get(k, k): v for k, v in obj.items()} for obj in data]
|
|
163
|
+
|
|
164
|
+
return data
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def read_jsonl(
|
|
168
|
+
file_path: Path, mapping: dict[str, str] | None = None
|
|
169
|
+
) -> Iterator[dict[str, Any]]:
|
|
170
|
+
"""
|
|
171
|
+
Read data from JSONL file (one JSON object per line).
|
|
172
|
+
|
|
173
|
+
Yields objects one at a time — suitable for large files.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
file_path: Path to JSONL file
|
|
177
|
+
mapping: Optional key name mapping
|
|
178
|
+
|
|
179
|
+
Yields:
|
|
180
|
+
Object dictionaries with data
|
|
181
|
+
"""
|
|
182
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
183
|
+
for line_num, line in enumerate(f, 1):
|
|
184
|
+
line = line.strip()
|
|
185
|
+
if not line:
|
|
186
|
+
continue
|
|
187
|
+
try:
|
|
188
|
+
obj = json.loads(line)
|
|
189
|
+
# Apply mapping if provided
|
|
190
|
+
if mapping:
|
|
191
|
+
obj = {mapping.get(k, k): v for k, v in obj.items()}
|
|
192
|
+
yield obj
|
|
193
|
+
except json.JSONDecodeError as e:
|
|
194
|
+
raise ValueError(f"Invalid JSON on line {line_num}: {e}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def read_pdf(
|
|
198
|
+
file_path: Path, image_field: str = "doc_page"
|
|
199
|
+
) -> Iterator[dict[str, Any]]:
|
|
200
|
+
"""
|
|
201
|
+
Convert each page of a PDF to a base64-encoded JPEG and yield as objects.
|
|
202
|
+
|
|
203
|
+
Each page becomes one Weaviate object with the base64 image stored in
|
|
204
|
+
`image_field`, plus `page_number` and `file_name` metadata properties.
|
|
205
|
+
Page images are freed from memory after encoding.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
file_path: Path to the PDF file
|
|
209
|
+
image_field: Name of the BLOB property to store the base64 image
|
|
210
|
+
|
|
211
|
+
Yields:
|
|
212
|
+
Dicts with image_field, page_number, and file_name keys
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
RuntimeError: If poppler is not installed
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
from pdf2image import convert_from_path
|
|
219
|
+
|
|
220
|
+
pages = convert_from_path(str(file_path))
|
|
221
|
+
except Exception as e:
|
|
222
|
+
if "poppler" in str(e).lower() or "pdftoppm" in str(e).lower():
|
|
223
|
+
raise RuntimeError(
|
|
224
|
+
f"Poppler is not installed or not in PATH. "
|
|
225
|
+
f"Install it with:\n"
|
|
226
|
+
f" macOS: brew install poppler\n"
|
|
227
|
+
f" Ubuntu/Debian: sudo apt-get install poppler-utils\n"
|
|
228
|
+
f"Original error: {e}"
|
|
229
|
+
)
|
|
230
|
+
raise
|
|
231
|
+
|
|
232
|
+
for page_num, page_img in enumerate(pages, 1):
|
|
233
|
+
buffer = BytesIO()
|
|
234
|
+
page_img.save(buffer, format="JPEG")
|
|
235
|
+
img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
236
|
+
yield {
|
|
237
|
+
image_field: img_base64,
|
|
238
|
+
"page_number": page_num,
|
|
239
|
+
"file_name": file_path.stem,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def create_pdf_collection(
|
|
244
|
+
client: weaviate.WeaviateClient, name: str, image_field: str
|
|
245
|
+
) -> None:
|
|
246
|
+
"""
|
|
247
|
+
Create a Weaviate collection with the standard multimodal PDF schema.
|
|
248
|
+
|
|
249
|
+
Properties: image_field (BLOB), page_number (INT), file_name (TEXT)
|
|
250
|
+
Vectorizer: multi2vec_weaviate with ModernVBERT/colmodernvbert + MUVERA encoding
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
client: Connected Weaviate client
|
|
254
|
+
name: Collection name
|
|
255
|
+
image_field: Name of the BLOB property to store base64 page images
|
|
256
|
+
"""
|
|
257
|
+
client.collections.create(
|
|
258
|
+
name=name,
|
|
259
|
+
properties=[
|
|
260
|
+
Property(name=image_field, data_type=DataType.BLOB),
|
|
261
|
+
Property(name="page_number", data_type=DataType.INT),
|
|
262
|
+
Property(name="file_name", data_type=DataType.TEXT),
|
|
263
|
+
],
|
|
264
|
+
vector_config=[
|
|
265
|
+
Configure.MultiVectors.multi2vec_weaviate(
|
|
266
|
+
name="doc_vector",
|
|
267
|
+
image_field=image_field,
|
|
268
|
+
model="ModernVBERT/colmodernvbert",
|
|
269
|
+
encoding=Configure.VectorIndex.MultiVector.Encoding.muvera(
|
|
270
|
+
ksim=4,
|
|
271
|
+
dprojections=16,
|
|
272
|
+
repetitions=20,
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
],
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def convert_types(
|
|
280
|
+
obj: dict[str, Any],
|
|
281
|
+
prop_types: dict[str, DataType],
|
|
282
|
+
) -> dict[str, Any]:
|
|
283
|
+
"""
|
|
284
|
+
Prepare an object for insertion using the collection schema to guide conversion.
|
|
285
|
+
|
|
286
|
+
Non-string values (JSON/JSONL native types) pass through unchanged. String values
|
|
287
|
+
are cast to the type declared in prop_types. Fields not in the schema pass through
|
|
288
|
+
as-is. Reserved fields always pass through unchanged.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
obj: Raw object from the file
|
|
292
|
+
prop_types: Map of property name → DataType from the collection schema
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Object ready for batch insertion
|
|
296
|
+
"""
|
|
297
|
+
result = {}
|
|
298
|
+
for key, value in obj.items():
|
|
299
|
+
if value is None or value == "":
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
# Reserved fields pass through as-is (will be dropped or renamed by caller)
|
|
303
|
+
if key in _RESERVED_FIELDS:
|
|
304
|
+
result[key] = value
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
# String value: cast based on schema
|
|
308
|
+
target_type = prop_types.get(key)
|
|
309
|
+
|
|
310
|
+
# Non-string values already have the right native type, with one exception:
|
|
311
|
+
# date[] lists from JSON/JSONL may contain bare date strings needing RFC3339
|
|
312
|
+
if not isinstance(value, str):
|
|
313
|
+
if target_type == DataType.DATE_ARRAY and isinstance(value, list):
|
|
314
|
+
result[key] = [
|
|
315
|
+
f"{d}T00:00:00Z"
|
|
316
|
+
if isinstance(d, str) and _DATE_RE.match(d)
|
|
317
|
+
else d.replace(" ", "T") + "Z"
|
|
318
|
+
if isinstance(d, str) and _DATETIME_RE.match(d)
|
|
319
|
+
else d
|
|
320
|
+
for d in value
|
|
321
|
+
]
|
|
322
|
+
else:
|
|
323
|
+
result[key] = value
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
if target_type == DataType.INT:
|
|
327
|
+
try:
|
|
328
|
+
result[key] = int(value)
|
|
329
|
+
except (ValueError, TypeError):
|
|
330
|
+
result[key] = value
|
|
331
|
+
elif target_type == DataType.INT_ARRAY:
|
|
332
|
+
try:
|
|
333
|
+
result[key] = [int(x) for x in json.loads(value)]
|
|
334
|
+
except (ValueError, TypeError):
|
|
335
|
+
result[key] = value
|
|
336
|
+
elif target_type == DataType.NUMBER:
|
|
337
|
+
try:
|
|
338
|
+
result[key] = float(value)
|
|
339
|
+
except (ValueError, TypeError):
|
|
340
|
+
result[key] = value
|
|
341
|
+
elif target_type == DataType.NUMBER_ARRAY:
|
|
342
|
+
try:
|
|
343
|
+
result[key] = [float(x) for x in json.loads(value)]
|
|
344
|
+
except (ValueError, TypeError):
|
|
345
|
+
result[key] = value
|
|
346
|
+
elif target_type == DataType.BOOL:
|
|
347
|
+
if value.lower() in ("true", "false"):
|
|
348
|
+
result[key] = value.lower() == "true"
|
|
349
|
+
else:
|
|
350
|
+
result[key] = value
|
|
351
|
+
elif target_type == DataType.BOOL_ARRAY:
|
|
352
|
+
try:
|
|
353
|
+
parsed = json.loads(value)
|
|
354
|
+
result[key] = [
|
|
355
|
+
b if isinstance(b, bool) else str(b).lower() == "true"
|
|
356
|
+
for b in parsed
|
|
357
|
+
]
|
|
358
|
+
except (ValueError, TypeError):
|
|
359
|
+
result[key] = value
|
|
360
|
+
elif target_type == DataType.DATE:
|
|
361
|
+
if _DATE_RE.match(value):
|
|
362
|
+
result[key] = f"{value}T00:00:00Z"
|
|
363
|
+
elif _DATETIME_RE.match(value):
|
|
364
|
+
result[key] = value.replace(" ", "T") + "Z"
|
|
365
|
+
else:
|
|
366
|
+
result[key] = value
|
|
367
|
+
elif target_type == DataType.DATE_ARRAY:
|
|
368
|
+
try:
|
|
369
|
+
parsed = json.loads(value)
|
|
370
|
+
result[key] = [
|
|
371
|
+
f"{d}T00:00:00Z"
|
|
372
|
+
if isinstance(d, str) and _DATE_RE.match(d)
|
|
373
|
+
else d.replace(" ", "T") + "Z"
|
|
374
|
+
if isinstance(d, str) and _DATETIME_RE.match(d)
|
|
375
|
+
else d
|
|
376
|
+
for d in parsed
|
|
377
|
+
]
|
|
378
|
+
except (ValueError, TypeError):
|
|
379
|
+
result[key] = value
|
|
380
|
+
elif target_type is not None and target_type not in _KEEP_AS_STRING:
|
|
381
|
+
try:
|
|
382
|
+
result[key] = json.loads(value)
|
|
383
|
+
except (ValueError, TypeError):
|
|
384
|
+
result[key] = value
|
|
385
|
+
else:
|
|
386
|
+
# text, uuid, blob, or field not in schema — keep as string
|
|
387
|
+
result[key] = value
|
|
388
|
+
|
|
389
|
+
return result
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def import_objects(
|
|
393
|
+
coll: Any,
|
|
394
|
+
data: Iterator[dict[str, Any]],
|
|
395
|
+
prop_types: dict[str, DataType],
|
|
396
|
+
skip_set: set[str],
|
|
397
|
+
batch_size: int,
|
|
398
|
+
) -> tuple[int, int, int, list[str]]:
|
|
399
|
+
"""
|
|
400
|
+
Batch-insert objects from *data* into *coll*.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
(total_count, imported_count, failed_count, errors)
|
|
404
|
+
"""
|
|
405
|
+
total_count = 0
|
|
406
|
+
imported_count = 0
|
|
407
|
+
failed_count = 0
|
|
408
|
+
errors: list[str] = []
|
|
409
|
+
|
|
410
|
+
with coll.batch.dynamic() as batch:
|
|
411
|
+
for i, obj in enumerate(data, 1):
|
|
412
|
+
total_count += 1
|
|
413
|
+
try:
|
|
414
|
+
converted_obj = convert_types(obj, prop_types)
|
|
415
|
+
if skip_set:
|
|
416
|
+
converted_obj = {
|
|
417
|
+
k: v for k, v in converted_obj.items() if k not in skip_set
|
|
418
|
+
}
|
|
419
|
+
batch.add_object(properties=converted_obj)
|
|
420
|
+
imported_count += 1
|
|
421
|
+
|
|
422
|
+
if i % batch_size == 0:
|
|
423
|
+
print(f"Progress: {i} objects processed", file=sys.stderr)
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
failed_count += 1
|
|
427
|
+
error_msg = f"Object {i}: {str(e)}"
|
|
428
|
+
if len(errors) < 10:
|
|
429
|
+
errors.append(error_msg)
|
|
430
|
+
if len(errors) <= 5:
|
|
431
|
+
print(f"Warning: {error_msg}", file=sys.stderr)
|
|
432
|
+
|
|
433
|
+
# Check for server-side failures
|
|
434
|
+
server_failed = 0
|
|
435
|
+
for failed_obj in coll.batch.failed_objects:
|
|
436
|
+
server_failed += 1
|
|
437
|
+
if len(errors) < 10:
|
|
438
|
+
errors.append(f"Batch error: {failed_obj.message}")
|
|
439
|
+
|
|
440
|
+
failed_count += server_failed
|
|
441
|
+
return total_count, imported_count, failed_count, errors
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@app.command()
|
|
445
|
+
def main(
|
|
446
|
+
files: list[str] = typer.Argument(
|
|
447
|
+
..., help="One or more CSV, JSON, JSONL, or PDF files"
|
|
448
|
+
),
|
|
449
|
+
collection: str = typer.Option(
|
|
450
|
+
..., "--collection", "-c", help="Target collection name"
|
|
451
|
+
),
|
|
452
|
+
mapping: str = typer.Option(
|
|
453
|
+
None,
|
|
454
|
+
"--mapping",
|
|
455
|
+
"-m",
|
|
456
|
+
help="JSON object mapping file columns/keys to properties",
|
|
457
|
+
),
|
|
458
|
+
tenant: str = typer.Option(
|
|
459
|
+
None, "--tenant", "-t", help="Tenant name for multi-tenant collections"
|
|
460
|
+
),
|
|
461
|
+
batch_size: int = typer.Option(
|
|
462
|
+
100, "--batch-size", "-b", help="Number of objects per batch"
|
|
463
|
+
),
|
|
464
|
+
image_field: str = typer.Option(
|
|
465
|
+
"doc_page",
|
|
466
|
+
"--image-field",
|
|
467
|
+
"-i",
|
|
468
|
+
help="BLOB property name to store base64 page images (PDF imports only)",
|
|
469
|
+
),
|
|
470
|
+
skip_fields: str = typer.Option(
|
|
471
|
+
None,
|
|
472
|
+
"--skip-fields",
|
|
473
|
+
help="Comma-separated field names to exclude from import (e.g. 'id,created_at')",
|
|
474
|
+
),
|
|
475
|
+
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
476
|
+
):
|
|
477
|
+
"""Import data from CSV, JSON, JSONL, or PDF files to a Weaviate collection."""
|
|
478
|
+
try:
|
|
479
|
+
# Validate all file paths up front
|
|
480
|
+
file_paths: list[Path] = []
|
|
481
|
+
for f in files:
|
|
482
|
+
fp = Path(f)
|
|
483
|
+
if not fp.exists():
|
|
484
|
+
print(f"Error: File not found: {f}", file=sys.stderr)
|
|
485
|
+
raise typer.Exit(1)
|
|
486
|
+
file_paths.append(fp)
|
|
487
|
+
|
|
488
|
+
# Parse mapping if provided
|
|
489
|
+
mapping_dict = None
|
|
490
|
+
if mapping:
|
|
491
|
+
try:
|
|
492
|
+
mapping_dict = json.loads(mapping)
|
|
493
|
+
if not isinstance(mapping_dict, dict):
|
|
494
|
+
raise ValueError("Mapping must be a JSON object")
|
|
495
|
+
except json.JSONDecodeError as e:
|
|
496
|
+
print(f"Error: Invalid JSON in mapping: {e}", file=sys.stderr)
|
|
497
|
+
raise typer.Exit(1)
|
|
498
|
+
|
|
499
|
+
# Parse skip_fields
|
|
500
|
+
skip_set: set[str] = (
|
|
501
|
+
{f.strip() for f in skip_fields.split(",")} if skip_fields else set()
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# Validate batch size
|
|
505
|
+
if batch_size < 1:
|
|
506
|
+
print("Error: Batch size must be at least 1", file=sys.stderr)
|
|
507
|
+
raise typer.Exit(1)
|
|
508
|
+
|
|
509
|
+
# Detect formats. CSV/JSON/JSONL can be mixed freely; PDF cannot be mixed with them.
|
|
510
|
+
try:
|
|
511
|
+
fmt_by_path = {fp: detect_file_format(fp) for fp in file_paths}
|
|
512
|
+
except ValueError as e:
|
|
513
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
514
|
+
raise typer.Exit(1)
|
|
515
|
+
|
|
516
|
+
has_pdf = any(f == "pdf" for f in fmt_by_path.values())
|
|
517
|
+
has_non_pdf = any(f != "pdf" for f in fmt_by_path.values())
|
|
518
|
+
if has_pdf and has_non_pdf:
|
|
519
|
+
print(
|
|
520
|
+
"Error: PDF files cannot be mixed with CSV/JSON/JSONL files. "
|
|
521
|
+
"Import PDFs separately.",
|
|
522
|
+
file=sys.stderr,
|
|
523
|
+
)
|
|
524
|
+
raise typer.Exit(1)
|
|
525
|
+
|
|
526
|
+
# Connect to Weaviate once for all files
|
|
527
|
+
with get_client() as client:
|
|
528
|
+
# PDF: create collection if absent, append if it exists. CSV/JSON/JSONL: must already exist.
|
|
529
|
+
if has_pdf:
|
|
530
|
+
if not client.collections.exists(collection):
|
|
531
|
+
print(
|
|
532
|
+
f"Creating collection '{collection}' with multimodal PDF schema...",
|
|
533
|
+
file=sys.stderr,
|
|
534
|
+
)
|
|
535
|
+
create_pdf_collection(client, collection, image_field)
|
|
536
|
+
print(f"Collection '{collection}' created.", file=sys.stderr)
|
|
537
|
+
else:
|
|
538
|
+
print(
|
|
539
|
+
f"Collection '{collection}' exists — appending pages to it.",
|
|
540
|
+
file=sys.stderr,
|
|
541
|
+
)
|
|
542
|
+
else:
|
|
543
|
+
if not client.collections.exists(collection):
|
|
544
|
+
print(
|
|
545
|
+
f"Error: Collection '{collection}' does not exist. "
|
|
546
|
+
f"Read `weaviate` skill's `create_collection.md` reference to create it first.",
|
|
547
|
+
file=sys.stderr,
|
|
548
|
+
)
|
|
549
|
+
raise typer.Exit(1)
|
|
550
|
+
|
|
551
|
+
# Fetch schema once — used for multi-tenancy check and type-safe coercion
|
|
552
|
+
coll = client.collections.get(collection)
|
|
553
|
+
config = coll.config.get()
|
|
554
|
+
prop_types: dict[str, DataType] = {
|
|
555
|
+
p.name: p.data_type for p in config.properties
|
|
556
|
+
}
|
|
557
|
+
is_multi_tenant = (
|
|
558
|
+
config.multi_tenancy_config.enabled
|
|
559
|
+
if config.multi_tenancy_config
|
|
560
|
+
else False
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Validate tenant parameter
|
|
564
|
+
if is_multi_tenant and not tenant:
|
|
565
|
+
print(
|
|
566
|
+
f"Error: Collection '{collection}' is multi-tenant, "
|
|
567
|
+
f"--tenant parameter is required",
|
|
568
|
+
file=sys.stderr,
|
|
569
|
+
)
|
|
570
|
+
raise typer.Exit(1)
|
|
571
|
+
elif not is_multi_tenant and tenant:
|
|
572
|
+
print(
|
|
573
|
+
f"Warning: Collection '{collection}' is not multi-tenant, "
|
|
574
|
+
f"--tenant parameter will be ignored",
|
|
575
|
+
file=sys.stderr,
|
|
576
|
+
)
|
|
577
|
+
tenant = None
|
|
578
|
+
|
|
579
|
+
if tenant:
|
|
580
|
+
coll = coll.with_tenant(tenant)
|
|
581
|
+
print(f"Using tenant: {tenant}", file=sys.stderr)
|
|
582
|
+
|
|
583
|
+
# Process each file
|
|
584
|
+
grand_total = grand_imported = grand_failed = 0
|
|
585
|
+
all_errors: list[str] = []
|
|
586
|
+
file_results = []
|
|
587
|
+
|
|
588
|
+
for file_path in file_paths:
|
|
589
|
+
file_fmt = fmt_by_path[file_path]
|
|
590
|
+
print(
|
|
591
|
+
f"\n[{file_fmt.upper()}] {file_path}",
|
|
592
|
+
file=sys.stderr,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
if file_fmt == "csv":
|
|
597
|
+
data: Iterator[dict[str, Any]] = read_csv(
|
|
598
|
+
file_path, mapping_dict
|
|
599
|
+
)
|
|
600
|
+
elif file_fmt == "json":
|
|
601
|
+
data = iter(read_json(file_path, mapping_dict))
|
|
602
|
+
elif file_fmt == "jsonl":
|
|
603
|
+
data = read_jsonl(file_path, mapping_dict)
|
|
604
|
+
elif file_fmt == "pdf":
|
|
605
|
+
if mapping_dict:
|
|
606
|
+
print(
|
|
607
|
+
"Warning: --mapping is not supported for PDF imports and will be ignored.",
|
|
608
|
+
file=sys.stderr,
|
|
609
|
+
)
|
|
610
|
+
data = read_pdf(file_path, image_field)
|
|
611
|
+
except Exception as e:
|
|
612
|
+
print(f"Error reading file: {e}", file=sys.stderr)
|
|
613
|
+
raise typer.Exit(1)
|
|
614
|
+
|
|
615
|
+
# Peek: validate non-empty and warn on reserved fields
|
|
616
|
+
first = next(data, None)
|
|
617
|
+
if first is None:
|
|
618
|
+
print(
|
|
619
|
+
f"Warning: No data found in {file_path}, skipping.",
|
|
620
|
+
file=sys.stderr,
|
|
621
|
+
)
|
|
622
|
+
continue
|
|
623
|
+
if file_fmt != "pdf":
|
|
624
|
+
reserved_found = (set(first.keys()) & _RESERVED_FIELDS) - skip_set
|
|
625
|
+
if reserved_found:
|
|
626
|
+
print(
|
|
627
|
+
f"Warning: Reserved Weaviate field(s) detected in data: "
|
|
628
|
+
f"{', '.join(sorted(reserved_found))}. "
|
|
629
|
+
f"These will cause import failures. "
|
|
630
|
+
f"Use --skip-fields to exclude or --mapping to rename them.",
|
|
631
|
+
file=sys.stderr,
|
|
632
|
+
)
|
|
633
|
+
data = itertools.chain([first], data)
|
|
634
|
+
|
|
635
|
+
print(
|
|
636
|
+
f"Importing objects in batches of {batch_size}...", file=sys.stderr
|
|
637
|
+
)
|
|
638
|
+
total, imported, failed, errors = import_objects(
|
|
639
|
+
coll, data, prop_types, skip_set, batch_size
|
|
640
|
+
)
|
|
641
|
+
grand_total += total
|
|
642
|
+
grand_imported += imported
|
|
643
|
+
grand_failed += failed
|
|
644
|
+
all_errors.extend(errors)
|
|
645
|
+
file_results.append(
|
|
646
|
+
{
|
|
647
|
+
"file": str(file_path),
|
|
648
|
+
"format": file_fmt,
|
|
649
|
+
"total_objects": total,
|
|
650
|
+
"imported": total - failed,
|
|
651
|
+
"failed": failed,
|
|
652
|
+
**({"errors": errors[:10]} if errors else {}),
|
|
653
|
+
}
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
grand_success = grand_imported - grand_failed
|
|
657
|
+
|
|
658
|
+
result = {
|
|
659
|
+
"collection": collection,
|
|
660
|
+
"tenant": tenant,
|
|
661
|
+
"total_objects": grand_total,
|
|
662
|
+
"imported": grand_success,
|
|
663
|
+
"failed": grand_failed,
|
|
664
|
+
"files": file_results,
|
|
665
|
+
}
|
|
666
|
+
if has_pdf:
|
|
667
|
+
result["image_field"] = image_field
|
|
668
|
+
if all_errors:
|
|
669
|
+
result["errors"] = all_errors[:10]
|
|
670
|
+
|
|
671
|
+
if json_output:
|
|
672
|
+
print(json.dumps(result, indent=2))
|
|
673
|
+
else:
|
|
674
|
+
print(f"\n✓ Import completed!", file=sys.stderr)
|
|
675
|
+
print(f"\n**Collection:** {collection}")
|
|
676
|
+
if tenant:
|
|
677
|
+
print(f"**Tenant:** {tenant}")
|
|
678
|
+
if len(file_paths) > 1:
|
|
679
|
+
print(f"**Files Processed:** {len(file_results)}")
|
|
680
|
+
print(f"**Total Objects:** {grand_total}")
|
|
681
|
+
print(f"**Successfully Imported:** {grand_success}")
|
|
682
|
+
if grand_failed > 0:
|
|
683
|
+
print(f"**Failed:** {grand_failed}")
|
|
684
|
+
if all_errors:
|
|
685
|
+
print(f"\n**Sample Errors:**")
|
|
686
|
+
for error in all_errors[:5]:
|
|
687
|
+
print(f" - {error}")
|
|
688
|
+
|
|
689
|
+
if grand_failed > 0:
|
|
690
|
+
raise typer.Exit(1)
|
|
691
|
+
|
|
692
|
+
except weaviate.exceptions.WeaviateConnectionError as e:
|
|
693
|
+
print(f"Error: Connection failed - {e}", file=sys.stderr)
|
|
694
|
+
raise typer.Exit(1)
|
|
695
|
+
except Exception as e:
|
|
696
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
697
|
+
raise typer.Exit(1)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
if __name__ == "__main__":
|
|
701
|
+
app()
|