📦 deps(thirdparty): update snapshots
This commit is contained in:
+106
@@ -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()
|
||||
+359
@@ -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()
|
||||
+945
@@ -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()
|
||||
+295
@@ -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()
|
||||
+261
@@ -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()
|
||||
+122
@@ -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()
|
||||
+157
@@ -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()
|
||||
+701
@@ -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()
|
||||
+142
@@ -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()
|
||||
+77
@@ -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()
|
||||
+135
@@ -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()
|
||||
+139
@@ -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()
|
||||
+241
@@ -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
|
||||
Reference in New Issue
Block a user