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.
Files changed (66) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +8 -1
  2. package/bundled-skills/browser-testing-with-devtools/SKILL.md +334 -0
  3. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  4. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  5. package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
  6. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  7. package/bundled-skills/docs/sources/sources.md +4 -0
  8. package/bundled-skills/docs/users/bundles.md +1 -1
  9. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  10. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  11. package/bundled-skills/docs/users/getting-started.md +1 -1
  12. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  13. package/bundled-skills/docs/users/usage.md +4 -4
  14. package/bundled-skills/docs/users/visual-guide.md +4 -4
  15. package/bundled-skills/drizzle-migration-conflict/SKILL.md +179 -0
  16. package/bundled-skills/drizzle-migration-conflict/references/ci-policy.md +87 -0
  17. package/bundled-skills/drizzle-migration-conflict/references/conflict-resolution.md +163 -0
  18. package/bundled-skills/drizzle-migration-conflict/references/report-template.md +69 -0
  19. package/bundled-skills/drizzle-migration-conflict/references/sources.md +51 -0
  20. package/bundled-skills/drizzle-migration-conflict/scripts/check_drizzle_migrations.py +721 -0
  21. package/bundled-skills/frontend-lighthouse/SKILL.md +348 -0
  22. package/bundled-skills/pre-release-review/SKILL.md +198 -0
  23. package/bundled-skills/pre-release-review/references/checklist.md +104 -0
  24. package/bundled-skills/pre-release-review/references/report-template.md +91 -0
  25. package/bundled-skills/re-create/SKILL.md +251 -0
  26. package/bundled-skills/weaviate/SKILL.md +132 -0
  27. package/bundled-skills/weaviate/references/ask.md +36 -0
  28. package/bundled-skills/weaviate/references/create_collection.md +152 -0
  29. package/bundled-skills/weaviate/references/environment_requirements.md +34 -0
  30. package/bundled-skills/weaviate/references/example_data.md +24 -0
  31. package/bundled-skills/weaviate/references/explore_collection.md +50 -0
  32. package/bundled-skills/weaviate/references/fetch_filter.md +88 -0
  33. package/bundled-skills/weaviate/references/get_collection.md +32 -0
  34. package/bundled-skills/weaviate/references/hybrid_search.md +47 -0
  35. package/bundled-skills/weaviate/references/import_data.md +160 -0
  36. package/bundled-skills/weaviate/references/keyword_search.md +38 -0
  37. package/bundled-skills/weaviate/references/list_collections.md +31 -0
  38. package/bundled-skills/weaviate/references/query_search.md +38 -0
  39. package/bundled-skills/weaviate/references/semantic_search.md +46 -0
  40. package/bundled-skills/weaviate/scripts/ask.py +106 -0
  41. package/bundled-skills/weaviate/scripts/create_collection.py +359 -0
  42. package/bundled-skills/weaviate/scripts/example_data.py +945 -0
  43. package/bundled-skills/weaviate/scripts/explore_collection.py +295 -0
  44. package/bundled-skills/weaviate/scripts/fetch_filter.py +261 -0
  45. package/bundled-skills/weaviate/scripts/get_collection.py +122 -0
  46. package/bundled-skills/weaviate/scripts/hybrid_search.py +157 -0
  47. package/bundled-skills/weaviate/scripts/import.py +701 -0
  48. package/bundled-skills/weaviate/scripts/keyword_search.py +142 -0
  49. package/bundled-skills/weaviate/scripts/list_collections.py +77 -0
  50. package/bundled-skills/weaviate/scripts/query_search.py +135 -0
  51. package/bundled-skills/weaviate/scripts/semantic_search.py +139 -0
  52. package/bundled-skills/weaviate/scripts/weaviate_conn.py +231 -0
  53. package/bundled-skills/weaviate-cookbooks/SKILL.md +67 -0
  54. package/bundled-skills/weaviate-cookbooks/references/advanced_rag.md +274 -0
  55. package/bundled-skills/weaviate-cookbooks/references/agentic_rag.md +360 -0
  56. package/bundled-skills/weaviate-cookbooks/references/async_client.md +428 -0
  57. package/bundled-skills/weaviate-cookbooks/references/basic_agent.md +270 -0
  58. package/bundled-skills/weaviate-cookbooks/references/basic_rag.md +219 -0
  59. package/bundled-skills/weaviate-cookbooks/references/data_explorer.md +336 -0
  60. package/bundled-skills/weaviate-cookbooks/references/environment_requirements.md +78 -0
  61. package/bundled-skills/weaviate-cookbooks/references/frontend_interface.md +104 -0
  62. package/bundled-skills/weaviate-cookbooks/references/pdf_multimodal_rag.md +635 -0
  63. package/bundled-skills/weaviate-cookbooks/references/project_setup.md +75 -0
  64. package/bundled-skills/weaviate-cookbooks/references/query_agent_chatbot.md +163 -0
  65. package/package.json +1 -1
  66. 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()