📦 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,106 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "weaviate-client==4.19.2",
# "weaviate-agents==1.2.0",
# "typer==0.21.0",
# ]
# ///
"""
Query Weaviate using Query Agent in Ask mode.
Usage:
uv run ask.py --query "your question" --collections "Collection1,Collection2" [--json]
Environment Variables:
WEAVIATE_URL: Weaviate Cloud cluster URL
WEAVIATE_API_KEY: API key for authentication
+ Any provider API keys (OPENAI_API_KEY, COHERE_API_KEY, etc.) - auto-detected
"""
import json
import sys
import typer
import weaviate
from weaviate.agents.query import QueryAgent
# Import shared connection utilities (local to this skill)
from weaviate_conn import get_client
app = typer.Typer()
def parse_collections(collections_str: str) -> list[str]:
"""Parse comma-separated collection names."""
collections = [c.strip() for c in collections_str.split(",") if c.strip()]
if not collections:
print("Error: At least one collection name required", file=sys.stderr)
raise typer.Exit(1)
return collections
@app.command()
def main(
query: str = typer.Option(..., "--query", "-q", help="Natural language question"),
collections: str = typer.Option(
..., "--collections", "-c", help="Comma-separated collection names"
),
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Query Weaviate using Query Agent in Ask mode (generates answer with sources)."""
collection_list = parse_collections(collections)
try:
with get_client() as client:
agent = QueryAgent(client=client, collections=collection_list)
print("Generating answer...", file=sys.stderr)
response = agent.ask(query)
print("Done.", file=sys.stderr)
# Extract data from response
answer = getattr(response, "final_answer", "") or ""
sources = []
if hasattr(response, "sources") and response.sources:
for src in response.sources:
sources.append(
{
"collection": getattr(src, "collection", None),
"object_id": getattr(src, "object_id", None),
}
)
result = {
"query": query,
"collections": collection_list,
"answer": answer,
"sources": sources,
"source_count": len(sources),
}
if json_output:
print(json.dumps(result, indent=2, default=str))
else:
# Markdown output for agent consumption
print(f"## Answer\n\n{answer}\n")
if sources:
print(f"## Sources ({len(sources)})\n")
print("| # | Collection | Object ID |")
print("|---|------------|-----------|")
for idx, src in enumerate(sources, 1):
print(
f"| {idx} | {src.get('collection', 'Unknown')} | `{src.get('object_id', 'N/A')}` |"
)
except weaviate.exceptions.WeaviateConnectionError as e:
print(f"Error: Connection failed - {e}", file=sys.stderr)
raise typer.Exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
if __name__ == "__main__":
app()
@@ -0,0 +1,359 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "weaviate-client==4.19.2",
# "typer==0.21.0",
# ]
# ///
"""
Create a Weaviate collection.
Usage:
uv run create_collection.py CollectionName --properties '[...]' [options]
Environment Variables:
WEAVIATE_URL: Weaviate Cloud cluster URL
WEAVIATE_API_KEY: API key for authentication
+ Any provider API keys (OPENAI_API_KEY, COHERE_API_KEY, etc.) - auto-detected
"""
import json
import sys
import typer
import weaviate
from weaviate.classes.config import (
Configure,
DataType,
Property,
Tokenization,
)
# Import shared connection utilities (local to this skill)
from weaviate_conn import get_client
app = typer.Typer()
# Data type string to enum mapping
DATA_TYPE_MAP = {
"text": DataType.TEXT,
"text[]": DataType.TEXT_ARRAY,
"boolean": DataType.BOOL,
"boolean[]": DataType.BOOL_ARRAY,
"bool": DataType.BOOL,
"bool[]": DataType.BOOL_ARRAY,
"int": DataType.INT,
"int[]": DataType.INT_ARRAY,
"number": DataType.NUMBER,
"number[]": DataType.NUMBER_ARRAY,
"date": DataType.DATE,
"date[]": DataType.DATE_ARRAY,
"uuid": DataType.UUID,
"uuid[]": DataType.UUID_ARRAY,
"geoCoordinates": DataType.GEO_COORDINATES,
"phoneNumber": DataType.PHONE_NUMBER,
"blob": DataType.BLOB,
"object": DataType.OBJECT,
"object[]": DataType.OBJECT_ARRAY,
}
# Types that support index_range_filters (enabled by default for better range query performance)
RANGE_FILTER_TYPES = {"int", "int[]", "number", "number[]", "date", "date[]"}
# Tokenization string to enum mapping
TOKENIZATION_MAP = {
"word": Tokenization.WORD,
"lowercase": Tokenization.LOWERCASE,
"whitespace": Tokenization.WHITESPACE,
"field": Tokenization.FIELD,
}
# Vectorizer string to config mapping
VECTORIZER_MAP = {
"text2vec_weaviate": lambda: Configure.Vectors.text2vec_weaviate(),
"text2vec_openai": lambda: Configure.Vectors.text2vec_openai(),
"text2vec_cohere": lambda: Configure.Vectors.text2vec_cohere(),
"text2vec_huggingface": lambda: Configure.Vectors.text2vec_huggingface(),
"text2vec_google_gemini": lambda: Configure.Vectors.text2vec_google_gemini(),
"text2vec_jinaai": lambda: Configure.Vectors.text2vec_jinaai(),
"text2vec_voyageai": lambda: Configure.Vectors.text2vec_voyageai(),
"text2vec_model2vec": lambda: Configure.Vectors.text2vec_model2vec(),
"text2vec_transformers": lambda: Configure.Vectors.text2vec_transformers(),
"text2vec_ollama": lambda: Configure.Vectors.text2vec_ollama(),
"multi2vec_clip": lambda: Configure.Vectors.multi2vec_clip(),
"multi2vec_bind": lambda: Configure.Vectors.multi2vec_bind(),
"none": lambda: Configure.Vectors.self_provided(),
}
def parse_property(prop_dict: dict) -> Property:
"""
Parse a property definition from a dictionary.
Args:
prop_dict: Dictionary with property definition
Returns:
Property instance
Raises:
ValueError: If property definition is invalid
"""
if "name" not in prop_dict:
raise ValueError("Property must have a 'name' field")
if "data_type" not in prop_dict:
raise ValueError(
f"Property '{prop_dict['name']}' must have a 'data_type' field"
)
name = prop_dict["name"]
data_type_str = prop_dict["data_type"].lower()
if data_type_str not in DATA_TYPE_MAP:
raise ValueError(
f"Invalid data_type '{prop_dict['data_type']}' for property '{name}'. "
f"Supported types: {', '.join(DATA_TYPE_MAP.keys())}"
)
data_type = DATA_TYPE_MAP[data_type_str]
# Build property kwargs
kwargs = {
"name": name,
"data_type": data_type,
}
# Add optional fields
if "description" in prop_dict:
kwargs["description"] = prop_dict["description"]
if "index_filterable" in prop_dict:
kwargs["index_filterable"] = bool(prop_dict["index_filterable"])
if "index_searchable" in prop_dict:
kwargs["index_searchable"] = bool(prop_dict["index_searchable"])
if "index_range_filters" in prop_dict:
kwargs["index_range_filters"] = bool(prop_dict["index_range_filters"])
# Handle tokenization for text types
if "tokenization" in prop_dict:
tokenization_str = prop_dict["tokenization"].lower()
if tokenization_str not in TOKENIZATION_MAP:
raise ValueError(
f"Invalid tokenization '{prop_dict['tokenization']}' for property '{name}'. "
f"Supported: {', '.join(TOKENIZATION_MAP.keys())}"
)
kwargs["tokenization"] = TOKENIZATION_MAP[tokenization_str]
# Handle nested properties for object types
if "nested_properties" in prop_dict:
if data_type not in [DataType.OBJECT, DataType.OBJECT_ARRAY]:
raise ValueError(
f"nested_properties can only be used with 'object' or 'object[]' data types "
f"(property '{name}' has type '{data_type_str}')"
)
kwargs["nested_properties"] = [
parse_property(nested_prop)
for nested_prop in prop_dict["nested_properties"]
]
return Property(**kwargs)
@app.command()
def main(
name: str = typer.Argument(..., help="Collection name (capitalize first letter)"),
properties: str = typer.Option(
...,
"--properties",
"-p",
help="JSON array of property definitions. Add a 'description' field to each property — the Query Agent uses these to understand your schema and construct accurate queries.",
),
description: str = typer.Option(
None,
"--description",
"-d",
help="Collection description. Weaviate agents read this to understand what the collection contains and decide which collection to query.",
),
vectorizer: str = typer.Option(
"text2vec_weaviate",
"--vectorizer",
"-v",
help=f"Vectorizer to use. Options: {', '.join(VECTORIZER_MAP.keys())}",
),
replication_factor: int = typer.Option(
None, "--replication-factor", "-r", help="Replication factor (default: 1)"
),
multi_tenancy: bool = typer.Option(
False, "--multi-tenancy", "-m", help="Enable multi-tenancy for data isolation"
),
auto_tenant_creation: bool = typer.Option(
False,
"--auto-tenant-creation",
"-a",
help="Auto-create tenants on insert (requires --multi-tenancy)",
),
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Create a new Weaviate collection with specified properties."""
try:
# Validate multi-tenancy options
if auto_tenant_creation and not multi_tenancy:
print(
"Error: --auto-tenant-creation requires --multi-tenancy to be enabled",
file=sys.stderr,
)
raise typer.Exit(1)
# Validate collection name (should start with uppercase)
if not name[0].isupper():
print(
f"Warning: Collection name '{name}' should start with an uppercase letter "
f"(GraphQL naming convention).",
file=sys.stderr,
)
name = name.capitalize()
print(f"Using '{name}' instead.", file=sys.stderr)
# Parse properties JSON
try:
properties_list = json.loads(properties)
if not isinstance(properties_list, list):
raise ValueError("Properties must be a JSON array")
if len(properties_list) == 0:
raise ValueError("Properties array cannot be empty")
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in properties: {e}", file=sys.stderr)
raise typer.Exit(1)
# Parse each property
try:
parsed_properties = [parse_property(prop) for prop in properties_list]
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
# Prepare collection config
collection_config = {
"name": name,
"properties": parsed_properties,
}
if description:
collection_config["description"] = description
# Add vectorizer if specified
if vectorizer:
vectorizer_lower = vectorizer.lower()
if vectorizer_lower not in VECTORIZER_MAP:
print(
f"Error: Invalid vectorizer '{vectorizer}'. "
f"Supported: {', '.join(VECTORIZER_MAP.keys())}",
file=sys.stderr,
)
raise typer.Exit(1)
collection_config["vector_config"] = VECTORIZER_MAP[vectorizer_lower]()
# Add replication config if specified
if replication_factor is not None:
if replication_factor < 1:
print("Error: Replication factor must be at least 1", file=sys.stderr)
raise typer.Exit(1)
collection_config["replication_config"] = Configure.replication(
factor=replication_factor
)
# Add multi-tenancy config if specified
if multi_tenancy:
collection_config["multi_tenancy_config"] = Configure.multi_tenancy(
enabled=True, auto_tenant_creation=auto_tenant_creation
)
with get_client() as client:
# Check if collection already exists
if client.collections.exists(name):
print(
f"Error: Collection '{name}' already exists. "
f"Delete it first or use a different name.",
file=sys.stderr,
)
raise typer.Exit(1)
print(f"Creating collection '{name}'...", file=sys.stderr)
client.collections.create(**collection_config)
# Verify creation by fetching the config
collection = client.collections.get(name)
config = collection.config.get()
result = {
"name": name,
"description": config.description,
"properties": [
{
"name": p.name,
"data_type": str(p.data_type),
"description": getattr(p, "description", None),
}
for p in config.properties
],
"multi_tenancy": {
"enabled": (
config.multi_tenancy_config.enabled
if config.multi_tenancy_config
else False
),
"auto_tenant_creation": (
config.multi_tenancy_config.auto_tenant_creation
if config.multi_tenancy_config
else False
),
},
"status": "created",
}
if json_output:
print(json.dumps(result, indent=2, default=str))
else:
print(f"\n✓ Collection '{name}' created successfully!\n")
if not result["description"]:
print(
"Tip: No collection description provided. "
"Weaviate agents read the collection description to understand what data it contains and decide which collection to query.",
file=sys.stderr,
)
props_without_desc = [
p["name"] for p in result["properties"] if not p.get("description")
]
if props_without_desc:
print(
f"Tip: {len(props_without_desc)} propert{'y has' if len(props_without_desc) == 1 else 'ies have'} no description. "
f"Adding descriptions helps the Query Agent understand your schema and construct accurate queries.",
file=sys.stderr,
)
print(f"**Description:** {config.description or 'N/A'}")
# Display multi-tenancy status
if result["multi_tenancy"]["enabled"]:
print(f"**Multi-Tenancy:** Enabled")
if result["multi_tenancy"]["auto_tenant_creation"]:
print(f"**Auto-Tenant Creation:** Enabled")
print(f"\n### Properties ({len(config.properties)})\n")
print("| Name | Data Type | Description |")
print("|------|-----------|-------------|")
for prop in result["properties"]:
desc = prop.get("description") or "-"
print(f"| {prop['name']} | {prop['data_type']} | {desc} |")
except weaviate.exceptions.WeaviateConnectionError as e:
print(f"Error: Connection failed - {e}", file=sys.stderr)
raise typer.Exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
if __name__ == "__main__":
app()
@@ -0,0 +1,945 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "weaviate-client==4.19.2",
# "weaviate-agents==1.2.0",
# "typer==0.21.0",
# "datasets>=4.5.0",
# ]
# ///
"""
Download an example dataset from the Hugging Face dataset hub.
Usage:
uv run example_data.py --domain "domain_name" --nrows "number_of_rows" --vectorizer "vectorizer_name"
Environment Variables:
WEAVIATE_URL: Weaviate Cloud cluster URL
WEAVIATE_API_KEY: API key for authentication
+ Any provider API keys (OPENAI_API_KEY, COHERE_API_KEY, etc.) - auto-detected
"""
import sys
import typer
import weaviate
from weaviate.client import WeaviateClient
import re
from weaviate.classes.config import Property, DataType, Configure
from datasets import load_dataset
from datetime import datetime, timezone
# Import shared connection utilities (local to this skill)
from weaviate_conn import get_client
app = typer.Typer()
# Vectorizer string to config mapping
VECTORIZER_MAP = {
"text2vec_weaviate": lambda: Configure.Vectors.text2vec_weaviate(),
"text2vec_openai": lambda: Configure.Vectors.text2vec_openai(),
"text2vec_cohere": lambda: Configure.Vectors.text2vec_cohere(),
"text2vec_huggingface": lambda: Configure.Vectors.text2vec_huggingface(),
"text2vec_google_gemini": lambda: Configure.Vectors.text2vec_google_gemini(),
"text2vec_jinaai": lambda: Configure.Vectors.text2vec_jinaai(),
"text2vec_voyageai": lambda: Configure.Vectors.text2vec_voyageai(),
"text2vec_model2vec": lambda: Configure.Vectors.text2vec_model2vec(),
"text2vec_transformers": lambda: Configure.Vectors.text2vec_transformers(),
"text2vec_ollama": lambda: Configure.Vectors.text2vec_ollama(),
"multi2vec_clip": lambda: Configure.Vectors.multi2vec_clip(),
"multi2vec_bind": lambda: Configure.Vectors.multi2vec_bind(),
"none": lambda: Configure.Vectors.self_provided(),
}
def _get_sentences(document: str) -> tuple[list[str], list[tuple[int, int]]]:
"""
Split document into sentences based on sentence_boundaries.
Maintains original order and preserves boundaries in chunks.
Returns sentences and their character spans (start, end) in the original document.
"""
sentence_boundaries: list[str] = [".", "?", "!"]
if not sentence_boundaries or not document:
return ([document], [(0, len(document))]) if document else ([], [])
escaped_boundaries = [re.escape(boundary) for boundary in sentence_boundaries]
pattern = r"(?<=" + "|".join(escaped_boundaries) + r")\s+"
sentences = []
spans = []
current_pos = 0
for match in re.finditer(pattern, document):
sentence_end = match.start()
sentence = document[current_pos:sentence_end].strip()
if sentence:
sentences.append(sentence)
spans.append((current_pos, sentence_end))
current_pos = match.end()
remaining = document[current_pos:].strip()
if remaining:
sentences.append(remaining)
spans.append((current_pos, len(document)))
filtered_sentences = []
filtered_spans = []
for sentence, span in zip(sentences, spans):
if sentence:
filtered_sentences.append(sentence)
filtered_spans.append(span)
return (
(filtered_sentences, filtered_spans)
if filtered_sentences
else ([document], [(0, len(document))])
)
def chunk_by_sentences(
document: str,
num_sentences: int,
overlap_sentences: int = 1,
) -> tuple[list[str], list[tuple[int, int]]]:
"""
Given a document (string), return the sentences as chunks and span annotations (start and end indices of chunks).
"""
if overlap_sentences >= num_sentences:
print(
f"Warning: overlap_sentences ({overlap_sentences}) is greater than num_sentences ({num_sentences}). Setting overlap to {num_sentences - 1}"
)
overlap_sentences = num_sentences - 1
sentences = _get_sentences(document)
span_annotations = []
chunks = []
i = 0
while i < len(sentences[0]):
# Get chunk of num_sentences sentences
chunk_sentences = sentences[1][i : i + num_sentences]
if not chunk_sentences:
break
# Get start and end char positions
start_char = chunk_sentences[0][0]
end_char = chunk_sentences[-1][1]
# Add chunk and its span annotation
chunks.append(document[start_char:end_char])
span_annotations.append((start_char, end_char))
# Move forward but account for overlap
i += num_sentences - overlap_sentences
return chunks, span_annotations
def create_ai_arxiv_collection(
client: WeaviateClient, vectorizer: str = "text2vec_weaviate", nrows: int = 1000
):
# check existence of collection
if client.collections.exists("AI_Arxiv"):
print(
f"Collection 'AI_Arxiv' already exists. Cannot create. Returning.",
file=sys.stderr,
)
return
print(f"Creating collection 'AI_Arxiv'...", file=sys.stderr)
collection = client.collections.create(
"AI_Arxiv",
description="AI and machine learning research papers from arXiv, chunked by sentences for semantic search.",
properties=[
Property(
name="paper_id",
data_type=DataType.TEXT,
index_searchable=False,
description="Unique arXiv paper identifier (e.g., '2301.07041')",
),
Property(
name="title",
data_type=DataType.TEXT,
description="Title of the research paper",
),
Property(
name="summary",
data_type=DataType.TEXT,
description="Abstract or summary of the research paper",
),
Property(
name="source",
data_type=DataType.TEXT,
index_searchable=False,
description="URL or source link to the original arXiv paper",
),
Property(
name="authors",
data_type=DataType.TEXT,
description="Comma-separated list of paper authors",
),
Property(
name="categories",
data_type=DataType.TEXT,
description="arXiv subject categories (e.g., 'cs.LG', 'stat.ML')",
),
Property(
name="comment",
data_type=DataType.TEXT,
description="Additional comments or notes from the authors",
),
Property(
name="primary_category",
data_type=DataType.TEXT,
description="Primary arXiv subject category for the paper",
),
Property(
name="published",
data_type=DataType.DATE,
index_range_filters=True,
description="Date the paper was first published on arXiv",
),
Property(
name="updated",
data_type=DataType.DATE,
index_range_filters=True,
description="Date the paper was last updated on arXiv",
),
Property(
name="chunk",
data_type=DataType.TEXT,
description="Text chunk from the paper body used for semantic search",
),
Property(
name="chunk_start",
data_type=DataType.NUMBER,
index_range_filters=True,
description="Character offset where this chunk starts in the original document",
),
Property(
name="chunk_end",
data_type=DataType.NUMBER,
index_range_filters=True,
description="Character offset where this chunk ends in the original document",
),
],
vector_config=VECTORIZER_MAP[vectorizer](),
inverted_index_config=Configure.inverted_index(index_null_state=True),
)
dataset = load_dataset("jamescalam/ai-arxiv2", split="train", keep_in_memory=True)
nrows = nrows or len(dataset)
with collection.batch.fixed_size(batch_size=100) as batch:
for i in range(min(nrows, len(dataset))):
item = dataset[i]
if i % int(min(nrows, len(dataset)) / 10) == 0:
print(
f"Importing {i}/{min(nrows, len(dataset))} objects... (AI_Arxiv)",
file=sys.stderr,
)
if item and isinstance(item, dict):
chunks, span_annotations = chunk_by_sentences(
document=item["content"], num_sentences=15, overlap_sentences=0
)
del item["content"]
item["paper_id"] = item["id"]
del item["id"]
del item["references"]
item["published"] = (
datetime.strptime("20231126", "%Y%m%d").replace(tzinfo=timezone.utc)
if item["published"]
else None
)
item["updated"] = (
datetime.strptime("20231126", "%Y%m%d").replace(tzinfo=timezone.utc)
if item["updated"]
else None
)
for chunk, span in zip(chunks, span_annotations):
item["chunk"] = chunk
item["chunk_start"] = span[0]
item["chunk_end"] = span[1]
batch.add_object(properties=item)
if batch.number_errors > 10:
print(
"Batch import stopped due to excessive errors. Returning.",
file=sys.stderr,
)
break
failed_objects = collection.batch.failed_objects
if failed_objects:
print(
f"Number of failed imports: {len(failed_objects)}",
file=sys.stderr,
)
print(f"First failed object: {failed_objects[0]}", file=sys.stderr)
return
print(
f"Created collection 'AI_Arxiv' with {len(collection)} objects.",
file=sys.stderr,
)
def create_income_tax_returns_collection(
client: WeaviateClient, vectorizer: str = "text2vec_weaviate", nrows: int = 1000
):
# check existence of collection
if client.collections.exists("Income_Tax_Returns"):
print(
f"Collection 'Income_Tax_Returns' already exists. Cannot create. Returning.",
file=sys.stderr,
)
return
print(f"Creating collection 'Income_Tax_Returns'...", file=sys.stderr)
collection = client.collections.create(
"Income_Tax_Returns",
description="Indian income tax return filings with taxpayer details, financials, and filing metadata.",
properties=[
Property(
name="pan",
data_type=DataType.TEXT,
index_searchable=False,
description="Permanent Account Number (PAN) — unique tax identifier for the taxpayer",
),
Property(
name="acknowledgement_number",
data_type=DataType.TEXT,
index_searchable=False,
description="Government-issued acknowledgement number for the filed return",
),
Property(
name="name",
data_type=DataType.TEXT,
description="Full legal name of the taxpayer",
),
Property(
name="address",
data_type=DataType.TEXT,
index_searchable=False,
description="Street address of the taxpayer",
),
Property(
name="area",
data_type=DataType.TEXT,
description="Area or locality name within the city",
),
Property(
name="city", data_type=DataType.TEXT, description="City of residence"
),
Property(
name="state",
data_type=DataType.TEXT,
description="State or province of residence",
),
Property(
name="pincode",
data_type=DataType.NUMBER,
index_range_filters=True,
description="Postal PIN code for the taxpayer's address",
),
Property(
name="state_code",
data_type=DataType.TEXT,
description="Two-character state code",
),
Property(
name="country_code",
data_type=DataType.TEXT,
description="ISO two-character country code (e.g., 'IN' for India)",
),
Property(
name="entity",
data_type=DataType.TEXT,
description="Type of taxpayer entity (e.g., 'Individual', 'Company', 'HUF')",
),
Property(
name="form",
data_type=DataType.TEXT,
description="Tax form type used for filing (e.g., 'ITR-1', 'ITR-2')",
),
Property(
name="assessment_year_start",
data_type=DataType.DATE,
index_range_filters=True,
description="Start date of the tax assessment year",
),
Property(
name="assessment_year_end",
data_type=DataType.DATE,
index_range_filters=True,
description="End date of the tax assessment year",
),
Property(
name="filing_datetime",
data_type=DataType.DATE,
index_range_filters=True,
description="Date and time when the return was filed",
),
Property(
name="late_filing",
data_type=DataType.BOOL,
description="Whether the return was filed after the due date",
),
Property(
name="signatory",
data_type=DataType.TEXT,
description="Name of the authorized signatory on the return",
),
Property(
name="loss",
data_type=DataType.NUMBER,
index_range_filters=True,
description="Total loss amount in Indian Rupees (INR)",
),
Property(
name="income",
data_type=DataType.NUMBER,
index_range_filters=True,
description="Total taxable income in Indian Rupees (INR)",
),
Property(
name="tax",
data_type=DataType.NUMBER,
index_range_filters=True,
description="Total tax payable in Indian Rupees (INR)",
),
Property(
name="cess",
data_type=DataType.NUMBER,
index_range_filters=True,
description="Health and education cess amount in Indian Rupees (INR)",
),
Property(
name="interest",
data_type=DataType.NUMBER,
index_range_filters=True,
description="Interest payable or receivable in Indian Rupees (INR)",
),
Property(
name="total_payable",
data_type=DataType.NUMBER,
index_range_filters=True,
description="Total amount payable including tax, cess, and interest in Indian Rupees (INR)",
),
],
vector_config=VECTORIZER_MAP[vectorizer](),
inverted_index_config=Configure.inverted_index(index_null_state=True),
)
dataset = load_dataset(
"AgamiAI/Indian-Income-Tax-Returns", split="train", keep_in_memory=True
)
nrows = nrows or len(dataset)
with collection.batch.fixed_size(batch_size=100) as batch:
for i in range(min(nrows, len(dataset))):
item = dataset[i]
if i % int(min(nrows, len(dataset)) / 10) == 0:
print(
f"Importing {i}/{min(nrows, len(dataset))} objects... (Income_Tax_Returns)",
file=sys.stderr,
)
if item and isinstance(item, dict):
batch.add_object(
properties={
"pan": item["pan"],
"acknowledgement_number": item["acknowledgement_number"],
"name": item["name"],
"address": item["address"],
"area": item["area"],
"city": item["city"],
"state": item["state"],
"pincode": item["pincode"],
"state_code": item["state_code"],
"country_code": item["country_code"],
"entity": item["entity"],
"form": item["form"],
"assessment_year_start": datetime.strptime(
item["assessment_year"][:4], "%Y"
).replace(tzinfo=timezone.utc),
"assessment_year_end": datetime.strptime(
item["assessment_year"][5:], "%y"
).replace(tzinfo=timezone.utc),
"filing_datetime": datetime.strptime(
item["filing_time"], "%d-%b-%Y %H:%M:%S"
).replace(tzinfo=timezone.utc),
"late_filing": item["late_filing"],
"signatory": item["signatory"],
"loss": (
item["financials"]["loss"]
if "loss" in item["financials"]
else None
),
"income": (
item["financials"]["income"]
if "income" in item["financials"]
else None
),
"tax": (
item["financials"]["tax"]
if "tax" in item["financials"]
else None
),
"cess": (
item["financials"]["cess"]
if "cess" in item["financials"]
else None
),
"interest": (
item["financials"]["interest"]
if "interest" in item["financials"]
else None
),
"total_payable": (
item["financials"]["total_payable"]
if "total_payable" in item["financials"]
else None
),
}
)
if batch.number_errors > 10:
print(
"Batch import stopped due to excessive errors. Returning.",
file=sys.stderr,
)
break
failed_objects = collection.batch.failed_objects
if failed_objects:
print(
f"Number of failed imports: {len(failed_objects)}",
file=sys.stderr,
)
print(f"First failed object: {failed_objects[0]}", file=sys.stderr)
return
print(
f"Created collection 'Income_Tax_Returns' with {len(collection)} objects.",
file=sys.stderr,
)
def create_product_catalog_collection(
client: WeaviateClient, vectorizer: str = "text2vec_weaviate", nrows: int = 1000
):
# check existence of collection
if client.collections.exists("Product_Catalog"):
print(
f"Collection 'Product_Catalog' already exists. Cannot create. Returning.",
file=sys.stderr,
)
return
print(f"Creating collection 'Product_Catalog'...", file=sys.stderr)
collection = client.collections.create(
"Product_Catalog",
description="E-commerce product catalog with pricing, brand, weight, and three-level category hierarchy.",
properties=[
Property(
name="product_name",
data_type=DataType.TEXT,
description="Name or title of the product",
),
Property(
name="size",
data_type=DataType.TEXT,
description="Size specification of the product (e.g., 'Small', '250g', '1L')",
),
Property(
name="pack_type",
data_type=DataType.TEXT,
description="Type of packaging (e.g., 'Box', 'Bag', 'Bottle')",
),
Property(
name="organic_status",
data_type=DataType.TEXT,
description="Organic certification status of the product (e.g., 'Organic', 'Conventional')",
),
Property(
name="weight_kg",
data_type=DataType.NUMBER,
index_range_filters=True,
description="Product weight in kilograms",
),
Property(
name="brand",
data_type=DataType.TEXT,
description="Brand name of the product",
),
Property(
name="price_usd",
data_type=DataType.NUMBER,
index_range_filters=True,
description="Product price in US dollars (USD)",
),
Property(
name="category",
data_type=DataType.TEXT,
description="Top-level product category (L1)",
),
Property(
name="subcategory",
data_type=DataType.TEXT,
description="Second-level product subcategory (L2)",
),
Property(
name="subsubcategory",
data_type=DataType.TEXT,
description="Third-level product subcategory (L3)",
),
],
vector_config=VECTORIZER_MAP[vectorizer](),
inverted_index_config=Configure.inverted_index(index_null_state=True),
)
dataset = load_dataset(
"pkghf/ecom-product-catalog", split="train", keep_in_memory=True
)
nrows = nrows or len(dataset)
with collection.batch.fixed_size(batch_size=100) as batch:
for i in range(min(nrows, len(dataset))):
item = dataset[i]
if i % int(min(nrows, len(dataset)) / 10) == 0:
print(
f"Importing {i}/{min(nrows, len(dataset))} objects... (Product_Catalog)",
file=sys.stderr,
)
if item and isinstance(item, dict):
batch.add_object(
properties={
"product_name": item["product_name"],
"size": item["size"],
"pack_type": item["pack_type"],
"organic_status": item["organic_status"],
"weight_kg": item["weight_kg"],
"brand": item["brand"],
"price_usd": item["price_usd"],
"category": item["L1"],
"subcategory": item["L2"],
"subsubcategory": item["L3"],
}
)
if batch.number_errors > 10:
print(
"Batch import stopped due to excessive errors. Returning.",
file=sys.stderr,
)
break
failed_objects = collection.batch.failed_objects
if failed_objects:
print(
f"Number of failed imports: {len(failed_objects)}",
file=sys.stderr,
)
print(f"First failed object: {failed_objects[0]}", file=sys.stderr)
return
print(
f"Created collection 'Product_Catalog' with {len(collection)} objects.",
file=sys.stderr,
)
def duration_to_days(duration_str: str) -> float | None:
"""Convert a duration string like '4 weeks', '2-4 weeks', '14 days' to a number of days.
For ranges like '2-4 weeks', returns the average (3 weeks = 21 days).
"""
unit_to_days = {
"day": 1,
"days": 1,
"week": 7,
"weeks": 7,
"month": 30,
"months": 30,
"year": 365,
"years": 365,
}
match = re.match(
r"(\d+)(?:\s*-\s*(\d+))?\s+(days?|weeks?|months?|years?)",
duration_str.strip(),
re.IGNORECASE,
)
if not match:
return None
low = float(match.group(1))
high = float(match.group(2)) if match.group(2) else low
unit = match.group(3).lower()
avg = (low + high) / 2
return avg * unit_to_days[unit]
def create_hair_medical_collection(
client: WeaviateClient, vectorizer: str = "text2vec_weaviate", nrows: int = 1000
):
# check existence of collection
if client.collections.exists("Hair_Medical"):
print(
f"Collection 'Hair_Medical' already exists. Cannot create. Returning.",
file=sys.stderr,
)
return
print(f"Creating collection 'Hair_Medical'...", file=sys.stderr)
collection = client.collections.create(
"Hair_Medical",
description="Hair disease diagnoses with associated symptoms, medications, side effects, severity, and treatment duration.",
properties=[
Property(
name="side_effects",
data_type=DataType.TEXT,
description="Known side effects of the prescribed medication",
),
Property(
name="avg_duration_days",
data_type=DataType.NUMBER,
index_range_filters=True,
description="Average treatment duration in days",
),
Property(
name="symptoms",
data_type=DataType.TEXT,
description="Symptoms associated with the hair disease",
),
Property(
name="medication_description",
data_type=DataType.TEXT,
description="Description and mechanism of action of the medication",
),
Property(
name="hair_disease",
data_type=DataType.TEXT,
description="Name of the hair disease or condition being treated",
),
Property(
name="medication",
data_type=DataType.TEXT,
description="Name of the prescribed medication",
),
Property(
name="disease_description",
data_type=DataType.TEXT,
description="Detailed description of the hair disease or condition",
),
Property(
name="disease_severity",
data_type=DataType.TEXT,
description="Severity level of the disease (e.g., 'Mild', 'Moderate', 'Severe')",
),
],
vector_config=VECTORIZER_MAP[vectorizer](),
inverted_index_config=Configure.inverted_index(index_null_state=True),
)
dataset = load_dataset("Amod/hair_medical_sit", split="train", keep_in_memory=True)
nrows = nrows or len(dataset)
with collection.batch.fixed_size(batch_size=100) as batch:
for i in range(min(nrows, len(dataset))):
item = dataset[i]
if i % int(min(nrows, len(dataset)) / 10) == 0:
print(
f"Importing {i}/{min(nrows, len(dataset))} objects... (Hair_Medical)",
file=sys.stderr,
)
if item and isinstance(item, dict):
batch.add_object(
properties={
"side_effects": item["Side Effects"],
"avg_duration_days": duration_to_days(item["Duration"]),
"symptoms": item["Symptoms"],
"medication_description": item["Medication Description"],
"hair_disease": item["Hair Disease"],
"medication": item["Medication"],
"disease_description": item["Disease Description"],
"disease_severity": item[" Severity of Disease"],
}
)
if batch.number_errors > 10:
print(
"Batch import stopped due to excessive errors. Returning.",
file=sys.stderr,
)
break
failed_objects = collection.batch.failed_objects
if failed_objects:
print(
f"Number of failed imports: {len(failed_objects)}",
file=sys.stderr,
)
print(f"First failed object: {failed_objects[0]}", file=sys.stderr)
return
print(
f"Created collection 'Hair_Medical' with {len(collection)} objects.",
file=sys.stderr,
)
def create_helpdesk_tickets_collection(
client: WeaviateClient, vectorizer: str = "text2vec_weaviate", nrows: int = 1000
):
# check existence of collection
if client.collections.exists("IT_Support_Tickets"):
print(
f"Collection 'IT_Support_Tickets' already exists. Cannot create. Returning.",
file=sys.stderr,
)
return
print(f"Creating collection 'IT_Support_Tickets'...", file=sys.stderr)
collection = client.collections.create(
"IT_Support_Tickets",
description="Synthetic IT helpdesk support tickets with subject, description, priority, category, and requester details.",
properties=[
Property(
name="ticket_id",
data_type=DataType.TEXT,
index_searchable=False,
description="Unique identifier for the support ticket",
),
Property(
name="subject",
data_type=DataType.TEXT,
description="Short subject line summarizing the IT issue",
),
Property(
name="description",
data_type=DataType.TEXT,
description="Detailed description of the IT support issue reported by the requester",
),
Property(
name="priority",
data_type=DataType.TEXT,
description="Priority level of the ticket (e.g., 'Low', 'Medium', 'High', 'Critical')",
),
Property(
name="category",
data_type=DataType.TEXT,
description="Category of the IT issue (e.g., 'Hardware', 'Software', 'Network', 'Access')",
),
Property(
name="createdAt",
data_type=DataType.DATE,
index_range_filters=True,
description="Date and time when the ticket was created",
),
Property(
name="requesterEmail",
data_type=DataType.TEXT,
description="Email address of the person who submitted the ticket",
),
],
vector_config=VECTORIZER_MAP[vectorizer](),
inverted_index_config=Configure.inverted_index(index_null_state=True),
)
dataset = load_dataset(
"Console-AI/IT-helpdesk-synthetic-tickets", split="train", keep_in_memory=True
)
nrows = nrows or len(dataset)
with collection.batch.fixed_size(batch_size=100) as batch:
for i in range(min(nrows, len(dataset))):
item = dataset[i]
if i % int(min(nrows, len(dataset)) / 10) == 0:
print(
f"Importing {i}/{min(nrows, len(dataset))} objects... (IT_Support_Tickets)",
file=sys.stderr,
)
if item and isinstance(item, dict):
batch.add_object(
properties={
"ticket_id": item["id"],
"subject": item["subject"],
"description": item["description"],
"priority": item["priority"],
"category": item["category"],
"createdAt": datetime.strptime(
item["createdAt"], "%Y-%m-%dT%H:%M:%S.%fZ"
).replace(tzinfo=timezone.utc),
"requesterEmail": item["requesterEmail"],
}
)
if batch.number_errors > 10:
print(
"Batch import stopped due to excessive errors. Returning.",
file=sys.stderr,
)
break
failed_objects = collection.batch.failed_objects
if failed_objects:
print(
f"Number of failed imports: {len(failed_objects)}",
file=sys.stderr,
)
print(f"First failed object: {failed_objects[0]}", file=sys.stderr)
return
print(
f"Created collection 'IT_Support_Tickets' with {len(collection)} objects.",
file=sys.stderr,
)
@app.command()
def main(
domain: str = typer.Option("academic", "--domain", "-d"),
nrows: int = typer.Option(None, "--nrows", "-n"),
vectorizer: str = typer.Option(
"text2vec_weaviate",
"--vectorizer",
"-v",
help=f"Vectorizer to use. Options: {', '.join(VECTORIZER_MAP.keys())}",
),
):
"""Download an example dataset from the Hugging Face dataset hub."""
with get_client() as client:
if domain == "academic":
create_ai_arxiv_collection(client, vectorizer, nrows)
elif domain == "finance":
create_income_tax_returns_collection(client, vectorizer, nrows)
elif domain == "ecommerce":
create_product_catalog_collection(client, vectorizer, nrows)
elif domain == "medical":
create_hair_medical_collection(client, vectorizer, nrows)
elif domain == "customer_support":
create_helpdesk_tickets_collection(client, vectorizer, nrows)
else:
print(f"Domain '{domain}' not supported. Returning.", file=sys.stderr)
raise typer.Exit(1)
if __name__ == "__main__":
app()
@@ -0,0 +1,295 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "weaviate-client==4.19.2",
# "typer==0.21.0",
# ]
# ///
"""
Explore a Weaviate collection's data: metrics, unique values (top occurrences), and sample objects.
Usage:
uv run explore_collection.py "CollectionName" [--limit 5] [--no-metrics] [--json]
Environment Variables:
WEAVIATE_URL: Weaviate Cloud cluster URL
WEAVIATE_API_KEY: API key for authentication
"""
import json
import sys
import typer
import weaviate
import weaviate.classes as wvc
from weaviate.classes.aggregate import Metrics
from weaviate.collections.classes.config import DataType
# Import shared connection utilities (local to this skill)
from weaviate_conn import get_client
app = typer.Typer()
def get_metrics_for_property(prop_name: str, data_type: DataType | str) -> Metrics:
"""
Return the appropriate Metrics object based on the property's data type.
"""
# Text
if data_type in [DataType.TEXT, DataType.TEXT_ARRAY]:
return Metrics(prop_name).text(
count=True,
top_occurrences_count=True,
top_occurrences_value=True,
limit=5,
)
# Integer
elif data_type in [DataType.INT, DataType.INT_ARRAY]:
return Metrics(prop_name).integer(
count=True,
minimum=True,
maximum=True,
mean=True,
median=True,
mode=True,
sum_=True,
)
# Number
elif data_type in [DataType.NUMBER, DataType.NUMBER_ARRAY]:
return Metrics(prop_name).number(
count=True,
minimum=True,
maximum=True,
mean=True,
median=True,
mode=True,
sum_=True,
)
# Boolean
elif data_type in [DataType.BOOL, DataType.BOOL_ARRAY]:
return Metrics(prop_name).boolean(
count=True,
percentage_true=True,
percentage_false=True,
total_true=True,
total_false=True,
)
# Date
elif data_type in [DataType.DATE, DataType.DATE_ARRAY]:
return Metrics(prop_name).date_(
count=True,
minimum=True,
maximum=True,
median=True,
mode=True,
)
return None
@app.command()
def main(
name: str = typer.Argument(..., help="Collection name"),
limit: int = typer.Option(
5, "--limit", "-l", help="Number of sample objects to show"
),
no_metrics: bool = typer.Option(
False, "--no-metrics", help="Skip calculating metrics (faster)"
),
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Explore data within a Weaviate collection."""
try:
with get_client() as client:
if not client.collections.exists(name):
print(f"Error: Collection '{name}' not found.", file=sys.stderr)
raise typer.Exit(1)
collection = client.collections.use(name)
config = collection.config.get()
# 1. Fetch Aggregation Metrics
metrics_data = {}
total_count = 0
if not no_metrics:
if not json_output:
print("Calculating metrics...", file=sys.stderr)
return_metrics = []
# Add metrics for each property based on type
for prop in config.properties:
m = get_metrics_for_property(prop.name, prop.data_type)
if m:
return_metrics.append(m)
try:
# Always ask for total_count
if return_metrics:
agg_response = collection.aggregate.over_all(
total_count=True, return_metrics=return_metrics
)
else:
# Fallback if no properties to aggregate
agg_response = collection.aggregate.over_all(total_count=True)
total_count = agg_response.total_count
for prop_name, agg_res in agg_response.properties.items():
prop_metrics = {}
# Helpers to extract common fields safely
def extract_fields(obj, fields):
for f in fields:
val = getattr(obj, f, None)
if val is not None:
prop_metrics[f] = val
# Identify type of result by checking attributes
if hasattr(agg_res, "top_occurrences"):
# Text
extract_fields(agg_res, ["count"])
if agg_res.top_occurrences:
prop_metrics["top_occurrences"] = [
{"value": to.value, "count": to.count}
for to in agg_res.top_occurrences
]
elif hasattr(agg_res, "mean"):
# Number/Int
extract_fields(
agg_res,
[
"count",
"minimum",
"maximum",
"mean",
"median",
"mode",
"sum_",
],
)
elif hasattr(agg_res, "percentage_true"):
# Boolean
extract_fields(
agg_res,
[
"count",
"total_true",
"total_false",
"percentage_true",
"percentage_false",
],
)
elif hasattr(agg_res, "minimum") and not hasattr(
agg_res, "mean"
):
# Date (has min/max but no mean)
extract_fields(
agg_res,
["count", "minimum", "maximum", "median", "mode"],
)
if prop_metrics:
metrics_data[prop_name] = prop_metrics
except Exception as e:
if not json_output:
print(f"Warning: Aggregation failed: {e}", file=sys.stderr)
metrics_data["error"] = str(e)
else:
# Just get total count if metrics skipped
try:
agg_response = collection.aggregate.over_all(total_count=True)
total_count = agg_response.total_count
except Exception:
pass
# 2. Fetch Sample Objects
if limit > 0:
if not json_output:
print(f"Fetching {limit} sample objects...", file=sys.stderr)
# Fetch objects with all properties
objects_resp = collection.query.fetch_objects(limit=limit)
sample_objects = []
for obj in objects_resp.objects:
sample_objects.append(
{"uuid": str(obj.uuid), "properties": obj.properties}
)
else:
sample_objects = []
# 3. Output
result = {
"collection": name,
"total_count": total_count,
"metrics": metrics_data,
"sample_objects": sample_objects,
}
if json_output:
print(json.dumps(result, indent=2, default=str))
else:
# Markdown Output
print(f"## Collection Explorer: {name}\n")
print(f"**Total Objects:** {total_count}")
if metrics_data:
print("\n### Property Metrics\n")
prop_types = {p.name: p.data_type.value for p in config.properties}
for prop_name, data in metrics_data.items():
p_type = prop_types.get(prop_name, "unknown")
print(f"**{prop_name}** ({p_type})")
for k, v in data.items():
if k == "top_occurrences":
print(f"- Top Values:")
for item in v:
# Escape pipes and newlines in values
val_str = (
str(item["value"])
.replace("\n", " ")
.replace("|", "\\|")
)
print(f" - {val_str} ({item['count']})")
else:
label = k.replace("_", " ").capitalize()
print(f"- {label}: {v}")
print("")
if sample_objects:
print(f"### Sample Objects (Limit: {limit})\n")
all_props = set()
for obj in sample_objects:
all_props.update(obj["properties"].keys())
sorted_props = sorted(list(all_props))
headers = ["#", "UUID"] + sorted_props
header_row = "| " + " | ".join(headers) + " |"
separator_row = "| " + " | ".join(["---"] * len(headers)) + " |"
print(header_row)
print(separator_row)
for idx, obj in enumerate(sample_objects, 1):
row_data = [str(idx), str(obj["uuid"])]
props = obj["properties"]
for prop in sorted_props:
val = props.get(prop, "-")
val_str = str(val).replace("\n", " ").replace("|", "\\|")
if len(val_str) > 100:
val_str = val_str[:97] + "..."
row_data.append(val_str)
print("| " + " | ".join(row_data) + " |")
print()
except weaviate.exceptions.WeaviateConnectionError as e:
print(f"Error: Connection failed - {e}", file=sys.stderr)
raise typer.Exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
if __name__ == "__main__":
app()
@@ -0,0 +1,261 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "weaviate-client==4.19.2",
# "typer==0.21.0",
# ]
# ///
"""
Fetch and filter objects from a Weaviate collection.
Usage:
# Fetch random 10 objects
uv run fetch_filter.py "JeopardyQuestion"
# Fetch by ID
uv run fetch_filter.py "JeopardyQuestion" --id "uuid-string"
# Filter with simple JSON
uv run fetch_filter.py "JeopardyQuestion" --filters '[{"property": "round", "operator": "equal", "value": "Double Jeopardy!"}]'
Environment Variables:
WEAVIATE_URL: Weaviate Cloud cluster URL
WEAVIATE_API_KEY: API key for authentication
"""
import json
import sys
from typing import Any, List, Optional
import typer
import weaviate
from weaviate.classes.query import Filter
# Import shared connection utilities (local to this skill)
from weaviate_conn import get_client
app = typer.Typer()
def parse_filter_item(item: Any) -> Optional[Filter]:
"""
Recursively parse a single filter item (dict or list).
Supported structures:
1. List of filters (implicit AND): [filter1, filter2]
2. Explicit Logical Operators:
{"operator": "and", "filters": [...]}
{"operator": "or", "filters": [...]}
3. Property Filter:
{"property": "name", "operator": "equal", "value": "foo"}
"""
if isinstance(item, list):
# Implicit AND for lists
sub_filters = [parse_filter_item(x) for x in item]
# Filter out Nones
sub_filters = [f for f in sub_filters if f is not None]
if not sub_filters:
return None
return Filter.all_of(sub_filters)
if not isinstance(item, dict):
return None
# Check for logical operators
op = item.get("operator")
if op == "and":
sub_items = item.get("filters", [])
sub_filters = [parse_filter_item(x) for x in sub_items]
sub_filters = [f for f in sub_filters if f is not None]
return Filter.all_of(sub_filters) if sub_filters else None
if op == "or":
sub_items = item.get("filters", [])
sub_filters = [parse_filter_item(x) for x in sub_items]
sub_filters = [f for f in sub_filters if f is not None]
return Filter.any_of(sub_filters) if sub_filters else None
# Property Filter
prop = item.get("property")
val = item.get("value")
if not prop or not op:
return None
current_filter = Filter.by_property(prop)
# Map operator string to method
if op == "equal":
return current_filter.equal(val)
elif op == "not_equal":
return current_filter.not_equal(val)
elif op == "less_than":
return current_filter.less_than(val)
elif op == "less_or_equal":
return current_filter.less_or_equal(val)
elif op == "greater_than":
return current_filter.greater_than(val)
elif op == "greater_or_equal":
return current_filter.greater_or_equal(val)
elif op == "like":
return current_filter.like(val)
elif op == "contains_any":
if not isinstance(val, list):
print(
f"Error: Value for 'contains_any' must be a list, got {type(val)}",
file=sys.stderr,
)
raise typer.Exit(1)
return current_filter.contains_any(val)
elif op == "contains_all":
if not isinstance(val, list):
print(
f"Error: Value for 'contains_all' must be a list, got {type(val)}",
file=sys.stderr,
)
raise typer.Exit(1)
return current_filter.contains_all(val)
elif op == "is_none":
return current_filter.is_none(bool(val))
else:
print(
f"Warning: Unknown operator '{op}' for property '{prop}'. Skipping.",
file=sys.stderr,
)
return None
def parse_filters(filter_json: str) -> Optional[Filter]:
"""
Parse a JSON string of filters into a Weaviate Filter object.
Supports complex nesting with AND/OR.
"""
if not filter_json:
return None
try:
data = json.loads(filter_json)
except json.JSONDecodeError as e:
print(f"Error parsing filters JSON: {e}", file=sys.stderr)
raise typer.Exit(1)
return parse_filter_item(data)
@app.command()
def main(
collection_name: str = typer.Argument(..., help="Collection name"),
obj_id: str = typer.Option(None, "--id", help="Fetch specific object by UUID"),
filters: str = typer.Option(None, "--filters", "-f", help="JSON string of filters"),
limit: int = typer.Option(10, "--limit", "-l", help="Number of objects to fetch"),
properties: str = typer.Option(
None,
"--properties",
"-p",
help="Comma-separated properties to include (default: all)",
),
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Fetch objects with optional filtering."""
try:
with get_client() as client:
if not client.collections.exists(collection_name):
print(
f"Error: Collection '{collection_name}' not found.", file=sys.stderr
)
raise typer.Exit(1)
collection = client.collections.use(collection_name)
# Determine return properties
return_properties = None
if properties:
return_properties = [
p.strip() for p in properties.split(",") if p.strip()
]
results = []
if obj_id:
# Fetch single object by ID
if not json_output:
print(f"Fetching object {obj_id}...", file=sys.stderr)
obj = collection.query.fetch_object_by_id(obj_id)
if obj:
results.append(obj)
else:
print(f"Error: Object {obj_id} not found.", file=sys.stderr)
raise typer.Exit(1)
else:
# Fetch multiple with filters
weaviate_filter = parse_filters(filters)
if not json_output:
print(
f"Fetching objects from '{collection_name}'...", file=sys.stderr
)
response = collection.query.fetch_objects(
filters=weaviate_filter,
limit=limit,
return_properties=return_properties,
)
results = list(response.objects)
# Output Formatting
output_data = []
for obj in results:
item = {
"uuid": str(obj.uuid),
"properties": obj.properties,
"metadata": {
"creation_time": str(obj.metadata.creation_time)
if obj.metadata.creation_time
else None,
},
}
output_data.append(item)
if json_output:
print(json.dumps(output_data, indent=2, default=str))
else:
if not results:
print("No objects found.")
else:
print(f"## Found {len(results)} Objects\n")
# Gather all property keys for the table headers
all_keys = set()
for item in output_data:
all_keys.update(item["properties"].keys())
sorted_keys = sorted(list(all_keys))
# Table Header
headers = ["UUID"] + sorted_keys
print("| " + " | ".join(headers) + " |")
print("| " + " | ".join(["---"] * len(headers)) + " |")
for item in output_data:
row = [str(item["uuid"])]
for k in sorted_keys:
val = item["properties"].get(k, "-")
val_str = str(val).replace("\n", " ").replace("|", "\\|")
if len(val_str) > 100:
val_str = val_str[:97] + "..."
row.append(val_str)
print("| " + " | ".join(row) + " |")
except weaviate.exceptions.WeaviateConnectionError as e:
print(f"Error: Connection failed - {e}", file=sys.stderr)
raise typer.Exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
if __name__ == "__main__":
app()
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "weaviate-client==4.19.2",
# "typer==0.21.0",
# ]
# ///
"""
Get details of a specific Weaviate collection.
Usage:
uv run get_collection.py --name "CollectionName" [--json]
Environment Variables:
WEAVIATE_URL: Weaviate Cloud cluster URL
WEAVIATE_API_KEY: API key for authentication
"""
import json
import sys
import typer
import weaviate
# Import shared connection utilities (local to this skill)
from weaviate_conn import get_client
app = typer.Typer()
@app.command()
def main(
name: str = typer.Option(..., "--name", "-n", help="Collection name"),
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Get detailed configuration of a Weaviate collection."""
try:
with get_client() as client:
if not client.collections.exists(name):
print(f"Error: Collection '{name}' not found.", file=sys.stderr)
raise typer.Exit(1)
print("Fetching collection details...", file=sys.stderr)
collection = client.collections.use(name)
config = collection.config.get()
# Extract vectorizer config
vectorizer_config = None
if hasattr(config, "vectorizer_config") and config.vectorizer_config:
vc = config.vectorizer_config
if hasattr(vc, "vectorizer"):
vectorizer_config = {
"vectorizer": str(vc.vectorizer.value)
if hasattr(vc.vectorizer, "value")
else str(vc.vectorizer),
"model": getattr(vc, "model", None),
}
# Extract properties
properties = []
if hasattr(config, "properties") and config.properties:
for p in config.properties:
prop_info = {
"name": p.name,
"data_type": str(p.data_type),
"description": getattr(p, "description", None),
}
properties.append(prop_info)
result = {
"name": name,
"description": config.description,
"vectorizer_config": vectorizer_config,
"properties": properties,
"replication_factor": getattr(config.replication_config, "factor", None)
if hasattr(config, "replication_config")
else None,
"multi_tenancy_enabled": getattr(
config.multi_tenancy_config, "enabled", False
)
if hasattr(config, "multi_tenancy_config")
else False,
}
if json_output:
print(json.dumps(result, indent=2, default=str))
else:
print(f"## Collection: {name}\n")
print(f"**Description:** {config.description or 'N/A'}")
if vectorizer_config:
print(
f"**Vectorizer:** {vectorizer_config.get('vectorizer', 'N/A')}"
)
if vectorizer_config.get("model"):
print(f"**Model:** {vectorizer_config['model']}")
print(
f"**Replication Factor:** {result['replication_factor'] or 'N/A'}"
)
print(
f"**Multi-Tenancy:** {'Enabled' if result['multi_tenancy_enabled'] else 'Disabled'}"
)
if properties:
print(f"\n### Properties ({len(properties)})\n")
print("| Name | Data Type | Description |")
print("|------|-----------|-------------|")
for prop in properties:
desc = prop.get("description") or "-"
print(f"| {prop['name']} | {prop['data_type']} | {desc} |")
except weaviate.exceptions.WeaviateConnectionError as e:
print(f"Error: Connection failed - {e}", file=sys.stderr)
raise typer.Exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
if __name__ == "__main__":
app()
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "weaviate-client==4.19.2",
# "typer==0.21.0",
# ]
# ///
"""
Hybrid search on a Weaviate collection (combines vector and keyword search).
Usage:
uv run hybrid_search.py --query "your query" --collection "CollectionName" [--alpha 0.5] [--limit 10] [--json]
Environment Variables:
WEAVIATE_URL: Weaviate Cloud cluster URL
WEAVIATE_API_KEY: API key for authentication
+ Any provider API keys (OPENAI_API_KEY, COHERE_API_KEY, etc.) - auto-detected
"""
import json
import sys
import typer
import weaviate
from weaviate.classes.query import MetadataQuery
# Import shared connection utilities (local to this skill)
from weaviate_conn import get_client
app = typer.Typer()
def parse_properties(properties_str: str | None) -> list[str] | None:
"""Parse comma-separated property names."""
if not properties_str:
return None
return [p.strip() for p in properties_str.split(",") if p.strip()]
@app.command()
def main(
query: str = typer.Option(..., "--query", "-q", help="Search query text"),
collection: str = typer.Option(..., "--collection", "-c", help="Collection name"),
alpha: float = typer.Option(
0.7,
"--alpha",
"-a",
help="Balance: 1.0=vector only, 0.0=keyword only (default: 0.7)",
),
limit: int = typer.Option(10, "--limit", "-l", help="Maximum results to return"),
properties: str = typer.Option(
None, "--properties", "-p", help="Comma-separated properties to search"
),
target_vector: str = typer.Option(
None,
"--target-vector",
"-t",
help="Target vector name for named vector collections",
),
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Perform hybrid search (vector + keyword) on a Weaviate collection."""
query_properties = parse_properties(properties)
try:
with get_client() as client:
if not client.collections.exists(collection):
print(f"Error: Collection '{collection}' not found.", file=sys.stderr)
raise typer.Exit(1)
coll = client.collections.use(collection)
print("Searching...", file=sys.stderr)
response = coll.query.hybrid(
query=query,
alpha=alpha,
limit=limit,
query_properties=query_properties,
target_vector=target_vector,
return_metadata=MetadataQuery(score=True, explain_score=True),
)
print("Done.", file=sys.stderr)
objects = []
for obj in response.objects:
obj_data = {
"uuid": str(obj.uuid),
"properties": dict(obj.properties),
"score": obj.metadata.score if obj.metadata else None,
"explain_score": obj.metadata.explain_score
if obj.metadata
else None,
}
objects.append(obj_data)
result = {
"query": query,
"collection": collection,
"alpha": alpha,
"limit": limit,
"target_vector": target_vector,
"objects": objects,
"object_count": len(objects),
}
if json_output:
print(json.dumps(result, indent=2, default=str))
else:
print(f"## Hybrid Search Results\n")
print(f"**Query:** {query}")
print(f"**Collection:** {collection}")
print(f"**Alpha:** {alpha} (1=vector, 0=keyword)")
print(f"**Found:** {len(objects)} objects\n")
if objects:
all_props = set()
for obj in objects:
all_props.update(obj.get("properties", {}).keys())
sorted_props = sorted(list(all_props))
headers = ["#", "UUID", "Score"] + sorted_props
header_row = "| " + " | ".join(headers) + " |"
separator_row = "| " + " | ".join(["---"] * len(headers)) + " |"
print(header_row)
print(separator_row)
for idx, obj in enumerate(objects, 1):
score = obj.get("score")
score_str = f"{score:.4f}" if score is not None else "N/A"
row_data = [
str(idx),
str(obj.get("uuid", "N/A")),
score_str,
]
props = obj.get("properties", {})
for prop in sorted_props:
val = props.get(prop, "-")
val_str = str(val).replace("\n", " ").replace("|", "\\|")
row_data.append(val_str)
print("| " + " | ".join(row_data) + " |")
print()
else:
print("No objects found matching the query.\n")
except weaviate.exceptions.WeaviateConnectionError as e:
print(f"Error: Connection failed - {e}", file=sys.stderr)
raise typer.Exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
if __name__ == "__main__":
app()
@@ -0,0 +1,701 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "weaviate-client==4.19.2",
# "typer==0.21.0",
# "pdf2image>=1.17.0",
# "pillow>=10.0.0",
# ]
# ///
"""
Import data from CSV, JSON, JSONL, or PDF files to a Weaviate collection.
Usage:
uv run import.py data.csv --collection "CollectionName" [options]
uv run import.py document.pdf --collection "CollectionName" [options]
Environment Variables:
WEAVIATE_URL: Weaviate Cloud cluster URL
WEAVIATE_API_KEY: API key for authentication
+ Any provider API keys (OPENAI_API_KEY, COHERE_API_KEY, etc.) - auto-detected
"""
import base64
import csv
import itertools
import json
import re
import sys
from collections.abc import Iterator
from io import BytesIO
from pathlib import Path
from typing import Any
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
_DATETIME_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}$")
_RESERVED_FIELDS = {"id", "_additional"}
import typer
import weaviate
from weaviate.classes.config import Configure, DataType, Property
# Import shared connection utilities (local to this skill)
from weaviate_conn import get_client
# Types whose string values must never be JSON-parsed (already correct as strings)
_KEEP_AS_STRING = {DataType.TEXT, DataType.UUID, DataType.BLOB}
app = typer.Typer()
def detect_file_format(file_path: Path) -> str:
"""
Detect file format based on extension.
Args:
file_path: Path to the file
Returns:
File format: "csv", "json", or "jsonl"
Raises:
ValueError: If file format is not supported
"""
extension = file_path.suffix.lower()
if extension == ".csv":
return "csv"
elif extension == ".json":
return "json"
elif extension == ".jsonl":
return "jsonl"
elif extension == ".pdf":
return "pdf"
else:
raise ValueError(
f"Unsupported file format: {extension}. "
f"Supported formats: .csv, .json, .jsonl, .pdf"
)
def read_csv(
file_path: Path, mapping: dict[str, str] | None = None
) -> Iterator[dict[str, Any]]:
"""
Read data from CSV file with automatic dialect detection.
Yields rows one at a time — suitable for large files.
Args:
file_path: Path to CSV file
mapping: Optional column name mapping
Yields:
Row dictionaries with data
"""
with open(file_path, "r", encoding="utf-8") as f:
# Read a sample to detect the CSV dialect
sample = f.read(8192)
f.seek(0)
# Use Sniffer to detect the dialect (delimiter, quoting, etc.)
sniffer = csv.Sniffer()
try:
dialect = sniffer.sniff(sample)
except csv.Error:
dialect = csv.excel
reader = csv.DictReader(f, dialect=dialect)
# Warn if the header row looks like data (all-numeric or JSON-like values
# suggest the file has no header row and the first data row was misread as one).
if reader.fieldnames:
suspicious = [
k
for k in reader.fieldnames
if k
and (
k.lstrip("-").replace(".", "", 1).isdigit()
or k.startswith(("[", "{"))
)
]
if suspicious:
print(
f"Warning: CSV column names look like data values: {suspicious}. "
f"Ensure the first row is a header row with property names.",
file=sys.stderr,
)
for row in reader:
# Apply mapping if provided
if mapping:
row = {mapping.get(k, k): v for k, v in row.items()}
yield row
def read_json(
file_path: Path, mapping: dict[str, str] | None = None
) -> list[dict[str, Any]]:
"""
Read data from JSON file (expects array of objects).
Args:
file_path: Path to JSON file
mapping: Optional key name mapping
Returns:
List of dictionaries with data
Raises:
ValueError: If JSON is not an array
"""
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, list):
raise ValueError(
f"JSON file must contain an array of objects, got {type(data).__name__}"
)
# Apply mapping if provided
if mapping:
data = [{mapping.get(k, k): v for k, v in obj.items()} for obj in data]
return data
def read_jsonl(
file_path: Path, mapping: dict[str, str] | None = None
) -> Iterator[dict[str, Any]]:
"""
Read data from JSONL file (one JSON object per line).
Yields objects one at a time — suitable for large files.
Args:
file_path: Path to JSONL file
mapping: Optional key name mapping
Yields:
Object dictionaries with data
"""
with open(file_path, "r", encoding="utf-8") as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
# Apply mapping if provided
if mapping:
obj = {mapping.get(k, k): v for k, v in obj.items()}
yield obj
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON on line {line_num}: {e}")
def read_pdf(
file_path: Path, image_field: str = "doc_page"
) -> Iterator[dict[str, Any]]:
"""
Convert each page of a PDF to a base64-encoded JPEG and yield as objects.
Each page becomes one Weaviate object with the base64 image stored in
`image_field`, plus `page_number` and `file_name` metadata properties.
Page images are freed from memory after encoding.
Args:
file_path: Path to the PDF file
image_field: Name of the BLOB property to store the base64 image
Yields:
Dicts with image_field, page_number, and file_name keys
Raises:
RuntimeError: If poppler is not installed
"""
try:
from pdf2image import convert_from_path
pages = convert_from_path(str(file_path))
except Exception as e:
if "poppler" in str(e).lower() or "pdftoppm" in str(e).lower():
raise RuntimeError(
f"Poppler is not installed or not in PATH. "
f"Install it with:\n"
f" macOS: brew install poppler\n"
f" Ubuntu/Debian: sudo apt-get install poppler-utils\n"
f"Original error: {e}"
)
raise
for page_num, page_img in enumerate(pages, 1):
buffer = BytesIO()
page_img.save(buffer, format="JPEG")
img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
yield {
image_field: img_base64,
"page_number": page_num,
"file_name": file_path.stem,
}
def create_pdf_collection(
client: weaviate.WeaviateClient, name: str, image_field: str
) -> None:
"""
Create a Weaviate collection with the standard multimodal PDF schema.
Properties: image_field (BLOB), page_number (INT), file_name (TEXT)
Vectorizer: multi2vec_weaviate with ModernVBERT/colmodernvbert + MUVERA encoding
Args:
client: Connected Weaviate client
name: Collection name
image_field: Name of the BLOB property to store base64 page images
"""
client.collections.create(
name=name,
properties=[
Property(name=image_field, data_type=DataType.BLOB),
Property(name="page_number", data_type=DataType.INT),
Property(name="file_name", data_type=DataType.TEXT),
],
vector_config=[
Configure.MultiVectors.multi2vec_weaviate(
name="doc_vector",
image_field=image_field,
model="ModernVBERT/colmodernvbert",
encoding=Configure.VectorIndex.MultiVector.Encoding.muvera(
ksim=4,
dprojections=16,
repetitions=20,
),
)
],
)
def convert_types(
obj: dict[str, Any],
prop_types: dict[str, DataType],
) -> dict[str, Any]:
"""
Prepare an object for insertion using the collection schema to guide conversion.
Non-string values (JSON/JSONL native types) pass through unchanged. String values
are cast to the type declared in prop_types. Fields not in the schema pass through
as-is. Reserved fields always pass through unchanged.
Args:
obj: Raw object from the file
prop_types: Map of property name → DataType from the collection schema
Returns:
Object ready for batch insertion
"""
result = {}
for key, value in obj.items():
if value is None or value == "":
continue
# Reserved fields pass through as-is (will be dropped or renamed by caller)
if key in _RESERVED_FIELDS:
result[key] = value
continue
# String value: cast based on schema
target_type = prop_types.get(key)
# Non-string values already have the right native type, with one exception:
# date[] lists from JSON/JSONL may contain bare date strings needing RFC3339
if not isinstance(value, str):
if target_type == DataType.DATE_ARRAY and isinstance(value, list):
result[key] = [
f"{d}T00:00:00Z"
if isinstance(d, str) and _DATE_RE.match(d)
else d.replace(" ", "T") + "Z"
if isinstance(d, str) and _DATETIME_RE.match(d)
else d
for d in value
]
else:
result[key] = value
continue
if target_type == DataType.INT:
try:
result[key] = int(value)
except (ValueError, TypeError):
result[key] = value
elif target_type == DataType.INT_ARRAY:
try:
result[key] = [int(x) for x in json.loads(value)]
except (ValueError, TypeError):
result[key] = value
elif target_type == DataType.NUMBER:
try:
result[key] = float(value)
except (ValueError, TypeError):
result[key] = value
elif target_type == DataType.NUMBER_ARRAY:
try:
result[key] = [float(x) for x in json.loads(value)]
except (ValueError, TypeError):
result[key] = value
elif target_type == DataType.BOOL:
if value.lower() in ("true", "false"):
result[key] = value.lower() == "true"
else:
result[key] = value
elif target_type == DataType.BOOL_ARRAY:
try:
parsed = json.loads(value)
result[key] = [
b if isinstance(b, bool) else str(b).lower() == "true"
for b in parsed
]
except (ValueError, TypeError):
result[key] = value
elif target_type == DataType.DATE:
if _DATE_RE.match(value):
result[key] = f"{value}T00:00:00Z"
elif _DATETIME_RE.match(value):
result[key] = value.replace(" ", "T") + "Z"
else:
result[key] = value
elif target_type == DataType.DATE_ARRAY:
try:
parsed = json.loads(value)
result[key] = [
f"{d}T00:00:00Z"
if isinstance(d, str) and _DATE_RE.match(d)
else d.replace(" ", "T") + "Z"
if isinstance(d, str) and _DATETIME_RE.match(d)
else d
for d in parsed
]
except (ValueError, TypeError):
result[key] = value
elif target_type is not None and target_type not in _KEEP_AS_STRING:
try:
result[key] = json.loads(value)
except (ValueError, TypeError):
result[key] = value
else:
# text, uuid, blob, or field not in schema — keep as string
result[key] = value
return result
def import_objects(
coll: Any,
data: Iterator[dict[str, Any]],
prop_types: dict[str, DataType],
skip_set: set[str],
batch_size: int,
) -> tuple[int, int, int, list[str]]:
"""
Batch-insert objects from *data* into *coll*.
Returns:
(total_count, imported_count, failed_count, errors)
"""
total_count = 0
imported_count = 0
failed_count = 0
errors: list[str] = []
with coll.batch.dynamic() as batch:
for i, obj in enumerate(data, 1):
total_count += 1
try:
converted_obj = convert_types(obj, prop_types)
if skip_set:
converted_obj = {
k: v for k, v in converted_obj.items() if k not in skip_set
}
batch.add_object(properties=converted_obj)
imported_count += 1
if i % batch_size == 0:
print(f"Progress: {i} objects processed", file=sys.stderr)
except Exception as e:
failed_count += 1
error_msg = f"Object {i}: {str(e)}"
if len(errors) < 10:
errors.append(error_msg)
if len(errors) <= 5:
print(f"Warning: {error_msg}", file=sys.stderr)
# Check for server-side failures
server_failed = 0
for failed_obj in coll.batch.failed_objects:
server_failed += 1
if len(errors) < 10:
errors.append(f"Batch error: {failed_obj.message}")
failed_count += server_failed
return total_count, imported_count, failed_count, errors
@app.command()
def main(
files: list[str] = typer.Argument(
..., help="One or more CSV, JSON, JSONL, or PDF files"
),
collection: str = typer.Option(
..., "--collection", "-c", help="Target collection name"
),
mapping: str = typer.Option(
None,
"--mapping",
"-m",
help="JSON object mapping file columns/keys to properties",
),
tenant: str = typer.Option(
None, "--tenant", "-t", help="Tenant name for multi-tenant collections"
),
batch_size: int = typer.Option(
100, "--batch-size", "-b", help="Number of objects per batch"
),
image_field: str = typer.Option(
"doc_page",
"--image-field",
"-i",
help="BLOB property name to store base64 page images (PDF imports only)",
),
skip_fields: str = typer.Option(
None,
"--skip-fields",
help="Comma-separated field names to exclude from import (e.g. 'id,created_at')",
),
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Import data from CSV, JSON, JSONL, or PDF files to a Weaviate collection."""
try:
# Validate all file paths up front
file_paths: list[Path] = []
for f in files:
fp = Path(f)
if not fp.exists():
print(f"Error: File not found: {f}", file=sys.stderr)
raise typer.Exit(1)
file_paths.append(fp)
# Parse mapping if provided
mapping_dict = None
if mapping:
try:
mapping_dict = json.loads(mapping)
if not isinstance(mapping_dict, dict):
raise ValueError("Mapping must be a JSON object")
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in mapping: {e}", file=sys.stderr)
raise typer.Exit(1)
# Parse skip_fields
skip_set: set[str] = (
{f.strip() for f in skip_fields.split(",")} if skip_fields else set()
)
# Validate batch size
if batch_size < 1:
print("Error: Batch size must be at least 1", file=sys.stderr)
raise typer.Exit(1)
# Detect formats. CSV/JSON/JSONL can be mixed freely; PDF cannot be mixed with them.
try:
fmt_by_path = {fp: detect_file_format(fp) for fp in file_paths}
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
has_pdf = any(f == "pdf" for f in fmt_by_path.values())
has_non_pdf = any(f != "pdf" for f in fmt_by_path.values())
if has_pdf and has_non_pdf:
print(
"Error: PDF files cannot be mixed with CSV/JSON/JSONL files. "
"Import PDFs separately.",
file=sys.stderr,
)
raise typer.Exit(1)
# Connect to Weaviate once for all files
with get_client() as client:
# PDF: create collection if absent, append if it exists. CSV/JSON/JSONL: must already exist.
if has_pdf:
if not client.collections.exists(collection):
print(
f"Creating collection '{collection}' with multimodal PDF schema...",
file=sys.stderr,
)
create_pdf_collection(client, collection, image_field)
print(f"Collection '{collection}' created.", file=sys.stderr)
else:
print(
f"Collection '{collection}' exists — appending pages to it.",
file=sys.stderr,
)
else:
if not client.collections.exists(collection):
print(
f"Error: Collection '{collection}' does not exist. "
f"Read `weaviate` skill's `create_collection.md` reference to create it first.",
file=sys.stderr,
)
raise typer.Exit(1)
# Fetch schema once — used for multi-tenancy check and type-safe coercion
coll = client.collections.get(collection)
config = coll.config.get()
prop_types: dict[str, DataType] = {
p.name: p.data_type for p in config.properties
}
is_multi_tenant = (
config.multi_tenancy_config.enabled
if config.multi_tenancy_config
else False
)
# Validate tenant parameter
if is_multi_tenant and not tenant:
print(
f"Error: Collection '{collection}' is multi-tenant, "
f"--tenant parameter is required",
file=sys.stderr,
)
raise typer.Exit(1)
elif not is_multi_tenant and tenant:
print(
f"Warning: Collection '{collection}' is not multi-tenant, "
f"--tenant parameter will be ignored",
file=sys.stderr,
)
tenant = None
if tenant:
coll = coll.with_tenant(tenant)
print(f"Using tenant: {tenant}", file=sys.stderr)
# Process each file
grand_total = grand_imported = grand_failed = 0
all_errors: list[str] = []
file_results = []
for file_path in file_paths:
file_fmt = fmt_by_path[file_path]
print(
f"\n[{file_fmt.upper()}] {file_path}",
file=sys.stderr,
)
try:
if file_fmt == "csv":
data: Iterator[dict[str, Any]] = read_csv(
file_path, mapping_dict
)
elif file_fmt == "json":
data = iter(read_json(file_path, mapping_dict))
elif file_fmt == "jsonl":
data = read_jsonl(file_path, mapping_dict)
elif file_fmt == "pdf":
if mapping_dict:
print(
"Warning: --mapping is not supported for PDF imports and will be ignored.",
file=sys.stderr,
)
data = read_pdf(file_path, image_field)
except Exception as e:
print(f"Error reading file: {e}", file=sys.stderr)
raise typer.Exit(1)
# Peek: validate non-empty and warn on reserved fields
first = next(data, None)
if first is None:
print(
f"Warning: No data found in {file_path}, skipping.",
file=sys.stderr,
)
continue
if file_fmt != "pdf":
reserved_found = (set(first.keys()) & _RESERVED_FIELDS) - skip_set
if reserved_found:
print(
f"Warning: Reserved Weaviate field(s) detected in data: "
f"{', '.join(sorted(reserved_found))}. "
f"These will cause import failures. "
f"Use --skip-fields to exclude or --mapping to rename them.",
file=sys.stderr,
)
data = itertools.chain([first], data)
print(
f"Importing objects in batches of {batch_size}...", file=sys.stderr
)
total, imported, failed, errors = import_objects(
coll, data, prop_types, skip_set, batch_size
)
grand_total += total
grand_imported += imported
grand_failed += failed
all_errors.extend(errors)
file_results.append(
{
"file": str(file_path),
"format": file_fmt,
"total_objects": total,
"imported": total - failed,
"failed": failed,
**({"errors": errors[:10]} if errors else {}),
}
)
grand_success = grand_imported - grand_failed
result = {
"collection": collection,
"tenant": tenant,
"total_objects": grand_total,
"imported": grand_success,
"failed": grand_failed,
"files": file_results,
}
if has_pdf:
result["image_field"] = image_field
if all_errors:
result["errors"] = all_errors[:10]
if json_output:
print(json.dumps(result, indent=2))
else:
print(f"\n✓ Import completed!", file=sys.stderr)
print(f"\n**Collection:** {collection}")
if tenant:
print(f"**Tenant:** {tenant}")
if len(file_paths) > 1:
print(f"**Files Processed:** {len(file_results)}")
print(f"**Total Objects:** {grand_total}")
print(f"**Successfully Imported:** {grand_success}")
if grand_failed > 0:
print(f"**Failed:** {grand_failed}")
if all_errors:
print(f"\n**Sample Errors:**")
for error in all_errors[:5]:
print(f" - {error}")
if grand_failed > 0:
raise typer.Exit(1)
except weaviate.exceptions.WeaviateConnectionError as e:
print(f"Error: Connection failed - {e}", file=sys.stderr)
raise typer.Exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
if __name__ == "__main__":
app()
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "weaviate-client==4.19.2",
# "typer==0.21.0",
# ]
# ///
"""
Keyword (BM25) search on a Weaviate collection.
Usage:
uv run keyword_search.py --query "your query" --collection "CollectionName" [--limit 10] [--json]
Environment Variables:
WEAVIATE_URL: Weaviate Cloud cluster URL
WEAVIATE_API_KEY: API key for authentication
"""
import json
import sys
import typer
import weaviate
from weaviate.classes.query import MetadataQuery
# Import shared connection utilities (local to this skill)
from weaviate_conn import get_client
app = typer.Typer()
def parse_properties(properties_str: str | None) -> list[str] | None:
"""Parse comma-separated property names with optional boost."""
if not properties_str:
return None
return [p.strip() for p in properties_str.split(",") if p.strip()]
@app.command()
def main(
query: str = typer.Option(..., "--query", "-q", help="Keyword search query"),
collection: str = typer.Option(..., "--collection", "-c", help="Collection name"),
limit: int = typer.Option(10, "--limit", "-l", help="Maximum results to return"),
properties: str = typer.Option(
None,
"--properties",
"-p",
help="Properties to search with optional boost (e.g., 'title^2,content')",
),
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Perform keyword (BM25) search on a Weaviate collection."""
query_properties = parse_properties(properties)
try:
with get_client() as client:
if not client.collections.exists(collection):
print(f"Error: Collection '{collection}' not found.", file=sys.stderr)
raise typer.Exit(1)
coll = client.collections.use(collection)
print("Searching...", file=sys.stderr)
response = coll.query.bm25(
query=query,
limit=limit,
query_properties=query_properties,
return_metadata=MetadataQuery(score=True),
)
print("Done.", file=sys.stderr)
objects = []
for obj in response.objects:
obj_data = {
"uuid": str(obj.uuid),
"properties": dict(obj.properties),
"score": obj.metadata.score if obj.metadata else None,
}
objects.append(obj_data)
result = {
"query": query,
"collection": collection,
"limit": limit,
"query_properties": query_properties,
"objects": objects,
"object_count": len(objects),
}
if json_output:
print(json.dumps(result, indent=2, default=str))
else:
print(f"## Keyword Search Results\n")
print(f"**Query:** {query}")
print(f"**Collection:** {collection}")
if query_properties:
print(f"**Properties:** {', '.join(query_properties)}")
print(f"**Found:** {len(objects)} objects\n")
if objects:
all_props = set()
for obj in objects:
all_props.update(obj.get("properties", {}).keys())
sorted_props = sorted(list(all_props))
headers = ["#", "UUID", "Score"] + sorted_props
header_row = "| " + " | ".join(headers) + " |"
separator_row = "| " + " | ".join(["---"] * len(headers)) + " |"
print(header_row)
print(separator_row)
for idx, obj in enumerate(objects, 1):
score = obj.get("score")
score_str = f"{score:.4f}" if score is not None else "N/A"
row_data = [
str(idx),
str(obj.get("uuid", "N/A")),
score_str,
]
props = obj.get("properties", {})
for prop in sorted_props:
val = props.get(prop, "-")
val_str = str(val).replace("\n", " ").replace("|", "\\|")
row_data.append(val_str)
print("| " + " | ".join(row_data) + " |")
print()
else:
print("No objects found matching the query.\n")
except weaviate.exceptions.WeaviateConnectionError as e:
print(f"Error: Connection failed - {e}", file=sys.stderr)
raise typer.Exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
if __name__ == "__main__":
app()
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "weaviate-client==4.19.2",
# "typer==0.21.0",
# ]
# ///
"""
List all Weaviate collections.
Usage:
uv run list_collections.py [--json]
Environment Variables:
WEAVIATE_URL: Weaviate Cloud cluster URL
WEAVIATE_API_KEY: API key for authentication
"""
import json
import sys
import typer
import weaviate
# Import shared connection utilities (local to this skill)
from weaviate_conn import get_client
app = typer.Typer()
@app.command()
def main(
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""List all Weaviate collections."""
try:
with get_client() as client:
print("Fetching collections...", file=sys.stderr)
collections = client.collections.list_all(simple=False)
print(f"Found {len(collections)} collections.", file=sys.stderr)
if json_output:
result = []
for name, config in collections.items():
result.append(
{
"name": name,
"description": config.description,
"properties": [
{"name": p.name, "data_type": str(p.data_type)}
for p in config.properties
],
}
)
print(json.dumps(result, indent=2, default=str))
else:
if not collections:
print("No collections found.")
else:
print("## Collections\n")
print("| Name | Description | Properties |")
print("|------|-------------|------------|")
for name, config in collections.items():
props = ", ".join([p.name for p in config.properties])
desc = config.description or "N/A"
print(f"| {name} | {desc} | {props} |")
except weaviate.exceptions.WeaviateConnectionError as e:
print(f"Error: Connection failed - {e}", file=sys.stderr)
raise typer.Exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
if __name__ == "__main__":
app()
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "weaviate-client==4.19.2",
# "weaviate-agents==1.2.0",
# "typer==0.21.0",
# ]
# ///
"""
Query Weaviate using Query Agent in Search mode.
Usage:
uv run search.py --query "your query" --collections "Collection1,Collection2" [--limit 10] [--json]
Environment Variables:
WEAVIATE_URL: Weaviate Cloud cluster URL
WEAVIATE_API_KEY: API key for authentication
+ Any provider API keys (OPENAI_API_KEY, COHERE_API_KEY, etc.) - auto-detected
"""
import json
import sys
import typer
import weaviate
from weaviate.agents.query import QueryAgent
# Import shared connection utilities (local to this skill)
from weaviate_conn import get_client
app = typer.Typer()
def parse_collections(collections_str: str) -> list[str]:
"""Parse comma-separated collection names."""
collections = [c.strip() for c in collections_str.split(",") if c.strip()]
if not collections:
print("Error: At least one collection name required", file=sys.stderr)
raise typer.Exit(1)
return collections
@app.command()
def main(
query: str = typer.Option(
..., "--query", "-q", help="Natural language search query"
),
collections: str = typer.Option(
..., "--collections", "-c", help="Comma-separated collection names"
),
limit: int = typer.Option(10, "--limit", "-l", help="Maximum results to return"),
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Query Weaviate using Query Agent in Search mode (retrieves raw objects)."""
collection_list = parse_collections(collections)
try:
with get_client() as client:
agent = QueryAgent(client=client, collections=collection_list)
print("Searching...", file=sys.stderr)
response = agent.search(query, limit=limit)
print("Done.", file=sys.stderr)
# Extract objects from search results
objects = []
if hasattr(response, "search_results") and response.search_results:
search_results = response.search_results
if hasattr(search_results, "objects") and search_results.objects:
for obj in search_results.objects:
obj_data = {
"uuid": str(getattr(obj, "uuid", "")),
"collection": getattr(obj, "collection", None),
"properties": dict(getattr(obj, "properties", {})),
}
objects.append(obj_data)
result = {
"query": query,
"collections": collection_list,
"limit": limit,
"objects": objects,
"object_count": len(objects),
}
if json_output:
print(json.dumps(result, indent=2, default=str))
else:
print(f"## Search Results\n")
print(f"**Query:** {query}")
print(f"**Collections:** {', '.join(collection_list)}")
print(f"**Found:** {len(objects)} objects\n")
if objects:
# Collect all property keys
all_props = set()
for obj in objects:
all_props.update(obj.get("properties", {}).keys())
sorted_props = sorted(list(all_props))
headers = ["#", "UUID", "Collection"] + sorted_props
header_row = "| " + " | ".join(headers) + " |"
separator_row = "| " + " | ".join(["---"] * len(headers)) + " |"
print(header_row)
print(separator_row)
for idx, obj in enumerate(objects, 1):
row_data = [
str(idx),
str(obj.get("uuid", "N/A")),
str(obj.get("collection", "N/A")),
]
props = obj.get("properties", {})
for prop in sorted_props:
val = props.get(prop, "-")
val_str = str(val).replace("\n", " ").replace("|", "\\|")
row_data.append(val_str)
print("| " + " | ".join(row_data) + " |")
print()
else:
print("No objects found matching the query.\n")
except weaviate.exceptions.WeaviateConnectionError as e:
print(f"Error: Connection failed - {e}", file=sys.stderr)
raise typer.Exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
if __name__ == "__main__":
app()
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "weaviate-client==4.19.2",
# "typer==0.21.0",
# ]
# ///
"""
Semantic (vector) search on a Weaviate collection.
Usage:
uv run semantic_search.py --query "your query" --collection "CollectionName" [--limit 10] [--json]
Environment Variables:
WEAVIATE_URL: Weaviate Cloud cluster URL
WEAVIATE_API_KEY: API key for authentication
+ Any provider API keys (OPENAI_API_KEY, COHERE_API_KEY, etc.) - auto-detected
"""
import json
import sys
import typer
import weaviate
from weaviate.classes.query import MetadataQuery
# Import shared connection utilities (local to this skill)
from weaviate_conn import get_client
app = typer.Typer()
@app.command()
def main(
query: str = typer.Option(..., "--query", "-q", help="Search query text"),
collection: str = typer.Option(..., "--collection", "-c", help="Collection name"),
limit: int = typer.Option(10, "--limit", "-l", help="Maximum results to return"),
distance: float = typer.Option(
None, "--distance", "-d", help="Maximum distance threshold"
),
target_vector: str = typer.Option(
None,
"--target-vector",
"-t",
help="Target vector name for named vector collections",
),
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Perform semantic (vector similarity) search on a Weaviate collection."""
try:
with get_client() as client:
if not client.collections.exists(collection):
print(f"Error: Collection '{collection}' not found.", file=sys.stderr)
raise typer.Exit(1)
coll = client.collections.use(collection)
print("Searching...", file=sys.stderr)
response = coll.query.near_text(
query=query,
limit=limit,
distance=distance,
target_vector=target_vector,
return_metadata=MetadataQuery(distance=True),
)
print("Done.", file=sys.stderr)
objects = []
for obj in response.objects:
obj_data = {
"uuid": str(obj.uuid),
"properties": dict(obj.properties),
"distance": obj.metadata.distance if obj.metadata else None,
}
objects.append(obj_data)
result = {
"query": query,
"collection": collection,
"limit": limit,
"distance_threshold": distance,
"target_vector": target_vector,
"objects": objects,
"object_count": len(objects),
}
if json_output:
print(json.dumps(result, indent=2, default=str))
else:
print(f"## Semantic Search Results\n")
print(f"**Query:** {query}")
print(f"**Collection:** {collection}")
if distance:
print(f"**Max Distance:** {distance}")
print(f"**Found:** {len(objects)} objects\n")
if objects:
all_props = set()
for obj in objects:
all_props.update(obj.get("properties", {}).keys())
sorted_props = sorted(list(all_props))
headers = ["#", "UUID", "Distance"] + sorted_props
header_row = "| " + " | ".join(headers) + " |"
separator_row = "| " + " | ".join(["---"] * len(headers)) + " |"
print(header_row)
print(separator_row)
for idx, obj in enumerate(objects, 1):
dist = obj.get("distance")
dist_str = f"{dist:.4f}" if dist is not None else "N/A"
row_data = [
str(idx),
str(obj.get("uuid", "N/A")),
dist_str,
]
props = obj.get("properties", {})
for prop in sorted_props:
val = props.get(prop, "-")
val_str = str(val).replace("\n", " ").replace("|", "\\|")
row_data.append(val_str)
print("| " + " | ".join(row_data) + " |")
print()
else:
print("No objects found matching the query.\n")
except weaviate.exceptions.WeaviateConnectionError as e:
print(f"Error: Connection failed - {e}", file=sys.stderr)
raise typer.Exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
raise typer.Exit(1)
if __name__ == "__main__":
app()
@@ -0,0 +1,241 @@
"""
Shared Weaviate connection utilities.
This module handles:
- Environment variable validation
- API key to header mapping for all supported providers
- Client connection with automatic header configuration
Usage in scripts:
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "lib"))
from weaviate_conn import get_client, get_headers, validate_env
"""
import os
import sys
from contextlib import contextmanager
from typing import Generator
import weaviate
from weaviate.classes.init import Auth
from weaviate.client import WeaviateClient
from weaviate.classes.init import AdditionalConfig, Timeout
# Canonical environment variable to Weaviate header mapping
API_KEY_MAP = {
"ANTHROPIC_API_KEY": "X-Anthropic-Api-Key",
"ANYSCALE_API_KEY": "X-Anyscale-Api-Key",
"AWS_ACCESS_KEY": "X-Aws-Access-Key",
"AWS_SECRET_KEY": "X-Aws-Secret-Key",
"COHERE_API_KEY": "X-Cohere-Api-Key",
"DATABRICKS_TOKEN": "X-Databricks-Token",
"FRIENDLI_TOKEN": "X-Friendli-Api-Key",
"VERTEX_API_KEY": "X-Goog-Vertex-Api-Key",
"STUDIO_API_KEY": "X-Goog-Studio-Api-Key",
"HUGGINGFACE_API_KEY": "X-HuggingFace-Api-Key",
"JINAAI_API_KEY": "X-JinaAI-Api-Key",
"MISTRAL_API_KEY": "X-Mistral-Api-Key",
"NVIDIA_API_KEY": "X-Nvidia-Api-Key",
"OPENAI_API_KEY": "X-OpenAI-Api-Key",
"AZURE_API_KEY": "X-Azure-Api-Key",
"VOYAGE_API_KEY": "X-Voyage-Api-Key",
"XAI_API_KEY": "X-Xai-Api-Key",
}
def _collect_headers_and_providers() -> tuple[dict[str, str], list[str]]:
"""
Scan env once to build Weaviate headers and detected key names.
Returns:
Tuple of (headers, detected_env_var_names)
"""
headers: dict[str, str] = {}
detected_providers: list[str] = []
for env_var, header_name in API_KEY_MAP.items():
value = os.environ.get(env_var, "").strip()
if not value:
continue
detected_providers.append(env_var)
headers[header_name] = value
return headers, detected_providers
def validate_env(require_weaviate: bool = True) -> tuple[str, str]:
"""
Validate required Weaviate environment variables.
Args:
require_weaviate: If True, exit with error if WEAVIATE_URL/API_KEY not set
Returns:
Tuple of (weaviate_url, weaviate_api_key)
Raises:
SystemExit: If required variables are missing
"""
url = os.environ.get("WEAVIATE_URL", "").strip()
api_key = os.environ.get("WEAVIATE_API_KEY", "").strip()
if require_weaviate:
if not url:
print("Error: WEAVIATE_URL environment variable not set", file=sys.stderr)
sys.exit(1)
if not api_key:
print(
"Error: WEAVIATE_API_KEY environment variable not set", file=sys.stderr
)
sys.exit(1)
return url, api_key
def get_headers() -> dict[str, str] | None:
"""
Build headers dict from all available API keys in environment.
Scans environment for all known API key variables and builds
the appropriate headers dict for Weaviate client connection.
Returns:
Dict of headers if any API keys found, None otherwise
"""
headers, _ = _collect_headers_and_providers()
return headers if headers else None
def get_detected_providers() -> list[str]:
"""
Get list of detected API key environment variable names.
Returns:
List of env var names (e.g., ["OPENAI_API_KEY", "COHERE_API_KEY"])
"""
_, detected_providers = _collect_headers_and_providers()
return sorted(detected_providers)
def _detected_provider_summary(detected_providers: list[str] | None) -> str | None:
"""Return a safe verbose summary without exposing credential env var names."""
if not detected_providers:
return None
provider_count = len(detected_providers)
label = "provider" if provider_count == 1 else "providers"
return f"Detected {provider_count} {label}."
@contextmanager
def get_client(
url: str | None = None,
api_key: str | None = None,
headers: dict[str, str] | None = None,
verbose: bool = True,
) -> Generator[WeaviateClient, None, None]:
"""
Context manager for Weaviate client connection.
Auto-detects credentials from environment if not provided.
Auto-builds headers from all available API keys if not provided.
Args:
url: Weaviate cluster URL (default: from WEAVIATE_URL env var)
api_key: Weaviate API key (default: from WEAVIATE_API_KEY env var)
headers: Custom headers dict (default: auto-detected from env vars)
verbose: Print connection status to stderr
Yields:
Connected WeaviateClient instance
Example:
with get_client() as client:
collections = client.collections.list_all()
"""
# Get credentials from env if not provided
if url is None or api_key is None:
env_url, env_api_key = validate_env()
url = url or env_url
api_key = api_key or env_api_key
# Auto-detect headers if not provided
if headers is None:
headers, detected_providers = _collect_headers_and_providers()
headers = headers or None
else:
detected_providers = None
if verbose:
provider_summary = _detected_provider_summary(detected_providers)
if provider_summary:
print(provider_summary, file=sys.stderr)
print("Connecting to Weaviate...", file=sys.stderr)
client = weaviate.connect_to_weaviate_cloud(
cluster_url=url,
auth_credentials=Auth.api_key(api_key),
headers=headers,
additional_config=AdditionalConfig(
timeout=Timeout(init=30, query=60, insert=120)
),
)
try:
if verbose:
print("Connected.", file=sys.stderr)
yield client
finally:
client.close()
def connect_client(
url: str | None = None,
api_key: str | None = None,
headers: dict[str, str] | None = None,
verbose: bool = True,
) -> WeaviateClient:
"""
Get a Weaviate client connection (non-context manager version).
IMPORTANT: Caller is responsible for calling client.close()
Args:
url: Weaviate cluster URL (default: from WEAVIATE_URL env var)
api_key: Weaviate API key (default: from WEAVIATE_API_KEY env var)
headers: Custom headers dict (default: auto-detected from env vars)
verbose: Print connection status to stderr
Returns:
Connected WeaviateClient instance
"""
if url is None or api_key is None:
env_url, env_api_key = validate_env()
url = url or env_url
api_key = api_key or env_api_key
if headers is None:
headers, detected_providers = _collect_headers_and_providers()
headers = headers or None
else:
detected_providers = None
if verbose:
provider_summary = _detected_provider_summary(detected_providers)
if provider_summary:
print(provider_summary, file=sys.stderr)
print("Connecting to Weaviate...", file=sys.stderr)
client = weaviate.connect_to_weaviate_cloud(
cluster_url=url,
auth_credentials=Auth.api_key(api_key),
headers=headers,
)
if verbose:
print("Connected.", file=sys.stderr)
return client