Compare commits

1 Commits

Author SHA1 Message Date
e81aac2e29 Add semantic caching 2026-03-04 06:17:47 +00:00
23 changed files with 844 additions and 354 deletions

View File

@@ -6,4 +6,4 @@ __pycache__/
.env
agent.py
AGENTS.md
README.md

View File

@@ -1,43 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: "3.12"
- run: uv sync --frozen
- name: Ruff check
run: uv run ruff check
- name: Ruff format check
run: uv run ruff format --check
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: "3.12"
- run: uv sync --frozen
- name: Type check
run: uv run ty check
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: "3.12"
- run: uv sync --frozen
- name: Run tests
run: uv run pytest --cov

View File

@@ -1,3 +0,0 @@
Use `uv` for project management
Linter: `uv run ruff check`
Type-checking: `uv run ty check`

View File

@@ -1,14 +1,13 @@
FROM python:3.12-slim
FROM quay.ocp.banorte.com/golden/python-312:latest
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
WORKDIR /app
COPY pyproject.toml uv.lock README.md ./
RUN uv sync --no-dev --frozen --no-install-project
COPY pyproject.toml uv.lock ./
RUN uv sync --no-dev --frozen
COPY src/ src/
RUN uv sync --no-dev --frozen
ENV PATH="/app/.venv/bin:$PATH"

View File

@@ -13,6 +13,8 @@ dependencies = [
"mcp[cli]>=1.26.0",
"pydantic-settings>=2.9.1",
"pyyaml>=6.0",
"redis[hiredis]>=5.0.0,<7",
"redisvl>=0.6.0",
]
[project.scripts]
@@ -36,19 +38,3 @@ pythonpath = ["."]
[build-system]
requires = ["uv_build>=0.8.3,<0.9.0"]
build-backend = "uv_build"
[tool.ruff]
exclude = ["scripts", "tests"]
[tool.ty.src]
exclude = ["scripts", "tests"]
[tool.ruff.lint]
select = ['ALL']
ignore = [
'D203', # one-blank-line-before-class
'D213', # multi-line-summary-second-line
'COM812', # missing-trailing-comma
'ANN401', # dynamically-typed-any
'ERA001', # commented-out-code
]

View File

@@ -6,10 +6,10 @@ from .models import AppContext, SearchResult, SourceNamespace
from .utils.cache import LRUCache
__all__ = [
"AppContext",
"GoogleCloudFileStorage",
"GoogleCloudVectorSearch",
"LRUCache",
"SearchResult",
"SourceNamespace",
"SearchResult",
"AppContext",
"LRUCache",
]

View File

@@ -1,3 +1,4 @@
# ruff: noqa: INP001
"""MCP server for semantic search over Vertex AI Vector Search."""
import time
@@ -8,11 +9,7 @@ from .config import _args
from .logging import log_structured_entry
from .models import AppContext, SourceNamespace
from .server import lifespan
from .services.search import (
filter_search_results,
format_search_results,
generate_query_embedding,
)
from .services.search import filter_search_results, format_search_results, generate_query_embedding
mcp = FastMCP(
"knowledge-search",
@@ -47,7 +44,7 @@ async def knowledge_search(
log_structured_entry(
"knowledge_search request received",
"INFO",
{"query": query[:100]}, # Log first 100 chars of query
{"query": query[:100]} # Log first 100 chars of query
)
try:
@@ -64,9 +61,26 @@ async def knowledge_search(
log_structured_entry(
"Query embedding generated successfully",
"INFO",
{"time_ms": round((t_embed - t0) * 1000, 1)},
{"time_ms": round((t_embed - t0) * 1000, 1)}
)
# Check semantic cache before vector search
if app.semantic_cache is not None and source is None:
cached = await app.semantic_cache.check(embedding)
if cached is not None:
t_cache = time.perf_counter()
log_structured_entry(
"knowledge_search completed from cache",
"INFO",
{
"embedding_ms": f"{round((t_embed - t0) * 1000, 1)}ms",
"cache_check_ms": f"{round((t_cache - t_embed) * 1000, 1)}ms",
"total_ms": f"{round((t_cache - t0) * 1000, 1)}ms",
"cache_hit": True,
},
)
return cached
# Perform vector search
log_structured_entry("Performing vector search", "INFO")
try:
@@ -77,13 +91,17 @@ async def knowledge_search(
source=source,
)
t_search = time.perf_counter()
except Exception as e: # noqa: BLE001
except Exception as e:
log_structured_entry(
"Vector search failed",
"ERROR",
{"error": str(e), "error_type": type(e).__name__, "query": query[:100]},
{
"error": str(e),
"error_type": type(e).__name__,
"query": query[:100]
}
)
return f"Error performing vector search: {e!s}"
return f"Error performing vector search: {str(e)}"
# Apply similarity filtering
filtered_results = filter_search_results(search_results)
@@ -98,25 +116,38 @@ async def knowledge_search(
"source_filter": source.value if source is not None else None,
"results_count": len(filtered_results),
"chunks": [s["id"] for s in filtered_results],
},
"cache_hit": False,
}
)
# Format and return results
formatted = format_search_results(filtered_results)
if not filtered_results:
log_structured_entry(
"No results found for query", "INFO", {"query": query[:100]}
"No results found for query",
"INFO",
{"query": query[:100]}
)
return format_search_results(filtered_results)
# Store in semantic cache (only for unfiltered queries with results)
if app.semantic_cache is not None and source is None and filtered_results:
await app.semantic_cache.store(query, formatted, embedding)
except Exception as e: # noqa: BLE001
return formatted
except Exception as e:
# Catch-all for any unexpected errors
log_structured_entry(
"Unexpected error in knowledge_search",
"ERROR",
{"error": str(e), "error_type": type(e).__name__, "query": query[:100]},
{
"error": str(e),
"error_type": type(e).__name__,
"query": query[:100]
}
)
return f"Unexpected error during search: {e!s}"
return f"Unexpected error during search: {str(e)}"
def main() -> None:

View File

@@ -1,3 +1,4 @@
# ruff: noqa: INP001
"""Base client with shared aiohttp session management."""
import aiohttp

View File

@@ -1,3 +1,4 @@
# ruff: noqa: INP001
"""Google Cloud Storage client with caching."""
import asyncio
@@ -7,9 +8,8 @@ from typing import BinaryIO
import aiohttp
from gcloud.aio.storage import Storage
from knowledge_search_mcp.logging import log_structured_entry
from knowledge_search_mcp.utils.cache import LRUCache
from ..logging import log_structured_entry
from ..utils.cache import LRUCache
from .base import BaseGoogleCloudClient
HTTP_TOO_MANY_REQUESTS = 429
@@ -56,7 +56,7 @@ class GoogleCloudFileStorage(BaseGoogleCloudClient):
log_structured_entry(
"File retrieved from cache",
"INFO",
{"file": file_name, "bucket": self.bucket_name},
{"file": file_name, "bucket": self.bucket_name}
)
file_stream = io.BytesIO(cached_content)
file_stream.name = file_name
@@ -65,7 +65,7 @@ class GoogleCloudFileStorage(BaseGoogleCloudClient):
log_structured_entry(
"Starting file download from GCS",
"INFO",
{"file": file_name, "bucket": self.bucket_name},
{"file": file_name, "bucket": self.bucket_name}
)
storage_client = self._get_aio_storage()
@@ -87,18 +87,15 @@ class GoogleCloudFileStorage(BaseGoogleCloudClient):
"file": file_name,
"bucket": self.bucket_name,
"size_bytes": len(content),
"attempt": attempt + 1,
},
"attempt": attempt + 1
}
)
except TimeoutError as exc:
last_exception = exc
log_structured_entry(
(
f"Timeout downloading gs://{self.bucket_name}/{file_name} "
f"(attempt {attempt + 1}/{max_retries})"
),
f"Timeout downloading gs://{self.bucket_name}/{file_name} (attempt {attempt + 1}/{max_retries})",
"WARNING",
{"error": str(exc)},
{"error": str(exc)}
)
except aiohttp.ClientResponseError as exc:
last_exception = exc
@@ -107,18 +104,15 @@ class GoogleCloudFileStorage(BaseGoogleCloudClient):
or exc.status >= HTTP_SERVER_ERROR
):
log_structured_entry(
(
f"HTTP {exc.status} downloading gs://{self.bucket_name}/"
f"{file_name} (attempt {attempt + 1}/{max_retries})"
),
f"HTTP {exc.status} downloading gs://{self.bucket_name}/{file_name} (attempt {attempt + 1}/{max_retries})",
"WARNING",
{"status": exc.status, "message": str(exc)},
{"status": exc.status, "message": str(exc)}
)
else:
log_structured_entry(
f"Non-retryable HTTP error downloading gs://{self.bucket_name}/{file_name}",
"ERROR",
{"status": exc.status, "message": str(exc)},
{"status": exc.status, "message": str(exc)}
)
raise
else:
@@ -129,7 +123,7 @@ class GoogleCloudFileStorage(BaseGoogleCloudClient):
log_structured_entry(
"Retrying file download",
"INFO",
{"file": file_name, "delay_seconds": delay},
{"file": file_name, "delay_seconds": delay}
)
await asyncio.sleep(delay)
@@ -144,7 +138,7 @@ class GoogleCloudFileStorage(BaseGoogleCloudClient):
"file": file_name,
"bucket": self.bucket_name,
"max_retries": max_retries,
"last_error": str(last_exception),
},
"last_error": str(last_exception)
}
)
raise TimeoutError(msg) from last_exception

View File

@@ -1,3 +1,4 @@
# ruff: noqa: INP001
"""Google Cloud Vector Search client."""
import asyncio
@@ -5,9 +6,8 @@ from collections.abc import Sequence
from gcloud.aio.auth import Token
from knowledge_search_mcp.logging import log_structured_entry
from knowledge_search_mcp.models import SearchResult, SourceNamespace
from ..logging import log_structured_entry
from ..models import SearchResult, SourceNamespace
from .base import BaseGoogleCloudClient
from .storage import GoogleCloudFileStorage
@@ -94,7 +94,7 @@ class GoogleCloudVectorSearch(BaseGoogleCloudClient):
log_structured_entry(
"Vector search query failed - endpoint not configured",
"ERROR",
{"error": msg},
{"error": msg}
)
raise RuntimeError(msg)
@@ -113,8 +113,8 @@ class GoogleCloudVectorSearch(BaseGoogleCloudClient):
"deployed_index_id": deployed_index_id,
"neighbor_count": limit,
"endpoint_id": endpoint_id,
"embedding_dimension": len(query),
},
"embedding_dimension": len(query)
}
)
datapoint: dict = {"feature_vector": list(query)}
@@ -149,10 +149,10 @@ class GoogleCloudVectorSearch(BaseGoogleCloudClient):
{
"status": response.status,
"response_body": body,
"deployed_index_id": deployed_index_id,
},
"deployed_index_id": deployed_index_id
}
)
raise RuntimeError(msg) # noqa: TRY301
raise RuntimeError(msg)
data = await response.json()
neighbors = data.get("nearestNeighbors", [{}])[0].get("neighbors", [])
@@ -161,15 +161,15 @@ class GoogleCloudVectorSearch(BaseGoogleCloudClient):
"INFO",
{
"neighbors_found": len(neighbors),
"deployed_index_id": deployed_index_id,
},
"deployed_index_id": deployed_index_id
}
)
if not neighbors:
log_structured_entry(
"No neighbors found in vector search",
"WARNING",
{"deployed_index_id": deployed_index_id},
{"deployed_index_id": deployed_index_id}
)
return []
@@ -185,7 +185,7 @@ class GoogleCloudVectorSearch(BaseGoogleCloudClient):
log_structured_entry(
"Fetching content for search results",
"INFO",
{"file_count": len(content_tasks)},
{"file_count": len(content_tasks)}
)
file_streams = await asyncio.gather(*content_tasks)
@@ -206,9 +206,12 @@ class GoogleCloudVectorSearch(BaseGoogleCloudClient):
log_structured_entry(
"Vector search completed successfully",
"INFO",
{"results_count": len(results), "deployed_index_id": deployed_index_id},
{
"results_count": len(results),
"deployed_index_id": deployed_index_id
}
)
return results # noqa: TRY300
return results
except Exception as e:
log_structured_entry(
@@ -217,7 +220,7 @@ class GoogleCloudVectorSearch(BaseGoogleCloudClient):
{
"error": str(e),
"error_type": type(e).__name__,
"deployed_index_id": deployed_index_id,
},
"deployed_index_id": deployed_index_id
}
)
raise

View File

@@ -1,14 +1,7 @@
"""Configuration management for the MCP server."""
import argparse
import os
import sys
from pydantic_settings import (
BaseSettings,
PydanticBaseSettingsSource,
YamlConfigSettingsSource,
)
import argparse
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, YamlConfigSettingsSource
def _parse_args() -> argparse.Namespace:
@@ -21,7 +14,7 @@ def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
return argparse.Namespace(
transport="stdio",
host="0.0.0.0", # noqa: S104
host="0.0.0.0",
port=8080,
config=os.environ.get("CONFIG_FILE", "config.yaml"),
)
@@ -32,7 +25,7 @@ def _parse_args() -> argparse.Namespace:
choices=["stdio", "sse", "streamable-http"],
default="stdio",
)
parser.add_argument("--host", default="0.0.0.0") # noqa: S104
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=8080)
parser.add_argument(
"--config",
@@ -43,7 +36,6 @@ def _parse_args() -> argparse.Namespace:
_args = _parse_args()
class Settings(BaseSettings):
"""Server configuration populated from env vars and a YAML config file."""
@@ -60,7 +52,13 @@ class Settings(BaseSettings):
search_limit: int = 10
log_name: str = "va_agent_evaluation_logs"
log_level: str = "INFO"
cloud_logging_enabled: bool = False
# Semantic cache (Redis)
redis_url: str | None = None
cache_name: str = "knowledge_search_cache"
cache_vector_dims: int = 3072
cache_distance_threshold: float = 0.12
cache_ttl: int | None = 3600
@classmethod
def settings_customise_sources(
@@ -71,7 +69,6 @@ class Settings(BaseSettings):
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
"""Customize the order of settings sources to include YAML config."""
return (
init_settings,
env_settings,
@@ -87,7 +84,7 @@ _cfg: Settings | None = None
def get_config() -> Settings:
"""Get or create the singleton Settings instance."""
global _cfg # noqa: PLW0603
global _cfg
if _cfg is None:
_cfg = Settings.model_validate({})
return _cfg
@@ -97,8 +94,8 @@ def get_config() -> Settings:
class _ConfigProxy:
"""Proxy object that lazily loads config on attribute access."""
def __getattr__(self, name: str) -> object:
def __getattr__(self, name: str):
return getattr(get_config(), name)
cfg = _ConfigProxy()
cfg = _ConfigProxy() # type: ignore[assignment]

View File

@@ -1,22 +1,23 @@
"""Centralized Cloud Logging setup.
Uses CloudLoggingHandler (background thread) so logging does not add latency.
"""
Centralized Cloud Logging setup.
Uses CloudLoggingHandler (background thread) so logging does not add latency
"""
import logging
from typing import Literal
from typing import Optional, Dict, Literal
import google.cloud.logging
from google.cloud.logging.handlers import CloudLoggingHandler
from .config import get_config
_eval_log: logging.Logger | None = None
def _get_logger() -> logging.Logger:
"""Get or create the singleton evaluation logger."""
global _eval_log # noqa: PLW0603
global _eval_log
if _eval_log is not None:
return _eval_log
@@ -26,42 +27,30 @@ def _get_logger() -> logging.Logger:
_eval_log = logger
return logger
if cfg.cloud_logging_enabled:
try:
client = google.cloud.logging.Client(project=cfg.project_id)
handler = CloudLoggingHandler(client, name=cfg.log_name) # async transport
logger.addHandler(handler)
logger.setLevel(getattr(logging, cfg.log_level.upper()))
except Exception as e: # noqa: BLE001
except Exception as e:
# Fallback to console if Cloud Logging is unavailable (local dev)
logging.basicConfig(level=getattr(logging, cfg.log_level.upper()))
logger = logging.getLogger(cfg.log_name)
logger.warning("Cloud Logging setup failed; using console. Error: %s", e)
else:
logging.basicConfig(level=getattr(logging, cfg.log_level.upper()))
logger = logging.getLogger(cfg.log_name)
_eval_log = logger
return logger
def log_structured_entry(
message: str,
severity: Literal["INFO", "WARNING", "ERROR"],
custom_log: dict | None = None,
) -> None:
"""Emit a JSON-structured log row.
def log_structured_entry(message: str, severity: Literal["INFO", "WARNING", "ERROR"], custom_log: Optional[Dict] = None) -> None:
"""
Emit a JSON-structured log row.
Args:
message: Short label for the row (e.g., "Final agent turn").
severity: "INFO" | "WARNING" | "ERROR"
custom_log: A dict with your structured payload.
"""
level = getattr(logging, severity.upper(), logging.INFO)
logger = _get_logger()
logger.log(
level,
message,
extra={"json_fields": {"message": message, "custom": custom_log or {}}},
)
logger.log(level, message, extra={"json_fields": {"message": message, "custom": custom_log or {}}})

View File

@@ -1,7 +1,8 @@
# ruff: noqa: INP001
"""Domain models for knowledge search MCP server."""
from dataclasses import dataclass
from enum import StrEnum
from enum import Enum
from typing import TYPE_CHECKING, TypedDict
if TYPE_CHECKING:
@@ -9,9 +10,10 @@ if TYPE_CHECKING:
from .clients.vector_search import GoogleCloudVectorSearch
from .config import Settings
from .services.semantic_cache import KnowledgeSemanticCache
class SourceNamespace(StrEnum):
class SourceNamespace(str, Enum):
"""Allowed values for the 'source' namespace filter."""
EDUCACION_FINANCIERA = "Educacion Financiera"
@@ -34,3 +36,4 @@ class AppContext:
vector_search: "GoogleCloudVectorSearch"
genai_client: "genai.Client"
settings: "Settings"
semantic_cache: "KnowledgeSemanticCache | None" = None

View File

@@ -1,3 +1,4 @@
# ruff: noqa: INP001
"""MCP server lifecycle management."""
from collections.abc import AsyncIterator
@@ -7,12 +8,13 @@ from google import genai
from mcp.server.fastmcp import FastMCP
from .clients.vector_search import GoogleCloudVectorSearch
from .config import get_config
from .config import Settings, cfg
from .logging import log_structured_entry
from .models import AppContext
from .services.semantic_cache import KnowledgeSemanticCache
from .services.validation import (
validate_gcs_access,
validate_genai_access,
validate_gcs_access,
validate_vector_search_access,
)
@@ -20,18 +22,15 @@ from .services.validation import (
@asynccontextmanager
async def lifespan(_server: FastMCP) -> AsyncIterator[AppContext]:
"""Create and configure the vector-search client for the server lifetime."""
# Get config with proper types for initialization
config_for_init = get_config()
log_structured_entry(
"Initializing MCP server",
"INFO",
{
"project_id": config_for_init.project_id,
"location": config_for_init.location,
"bucket": config_for_init.bucket,
"index_name": config_for_init.index_name,
},
"project_id": cfg.project_id,
"location": cfg.location,
"bucket": cfg.bucket,
"index_name": cfg.index_name,
}
)
vs: GoogleCloudVectorSearch | None = None
@@ -39,10 +38,10 @@ async def lifespan(_server: FastMCP) -> AsyncIterator[AppContext]:
# Initialize vector search client
log_structured_entry("Creating GoogleCloudVectorSearch client", "INFO")
vs = GoogleCloudVectorSearch(
project_id=config_for_init.project_id,
location=config_for_init.location,
bucket=config_for_init.bucket,
index_name=config_for_init.index_name,
project_id=cfg.project_id,
location=cfg.location,
bucket=cfg.bucket,
index_name=cfg.index_name,
)
# Configure endpoint
@@ -50,28 +49,25 @@ async def lifespan(_server: FastMCP) -> AsyncIterator[AppContext]:
"Configuring index endpoint",
"INFO",
{
"endpoint_name": config_for_init.endpoint_name,
"endpoint_domain": config_for_init.endpoint_domain,
},
"endpoint_name": cfg.endpoint_name,
"endpoint_domain": cfg.endpoint_domain,
}
)
vs.configure_index_endpoint(
name=config_for_init.endpoint_name,
public_domain=config_for_init.endpoint_domain,
name=cfg.endpoint_name,
public_domain=cfg.endpoint_domain,
)
# Initialize GenAI client
log_structured_entry(
"Creating GenAI client",
"INFO",
{
"project_id": config_for_init.project_id,
"location": config_for_init.location,
},
{"project_id": cfg.project_id, "location": cfg.location}
)
genai_client = genai.Client(
vertexai=True,
project=config_for_init.project_id,
location=config_for_init.location,
project=cfg.project_id,
location=cfg.location,
)
# Validate credentials and configuration by testing actual resources
@@ -81,41 +77,56 @@ async def lifespan(_server: FastMCP) -> AsyncIterator[AppContext]:
validation_errors = []
# Run all validations
config = get_config()
genai_error = await validate_genai_access(genai_client, config)
genai_error = await validate_genai_access(genai_client, cfg)
if genai_error:
validation_errors.append(genai_error)
gcs_error = await validate_gcs_access(vs, config)
gcs_error = await validate_gcs_access(vs, cfg)
if gcs_error:
validation_errors.append(gcs_error)
vs_error = await validate_vector_search_access(vs, config)
vs_error = await validate_vector_search_access(vs, cfg)
if vs_error:
validation_errors.append(vs_error)
# Summary of validations
if validation_errors:
log_structured_entry(
(
"MCP server started with validation errors - "
"service may not work correctly"
),
"MCP server started with validation errors - service may not work correctly",
"WARNING",
{
"validation_errors": validation_errors,
"error_count": len(validation_errors),
},
{"validation_errors": validation_errors, "error_count": len(validation_errors)}
)
else:
log_structured_entry("All validations passed - MCP server initialization complete", "INFO")
# Initialize semantic cache if Redis is configured
semantic_cache = None
if cfg.redis_url:
try:
semantic_cache = KnowledgeSemanticCache(
redis_url=cfg.redis_url,
name=cfg.cache_name,
vector_dims=cfg.cache_vector_dims,
distance_threshold=cfg.cache_distance_threshold,
ttl=cfg.cache_ttl,
)
log_structured_entry(
"All validations passed - MCP server initialization complete", "INFO"
"Semantic cache initialized",
"INFO",
{"redis_url": cfg.redis_url, "cache_name": cfg.cache_name},
)
except Exception as e:
log_structured_entry(
"Semantic cache initialization failed, continuing without cache",
"WARNING",
{"error": str(e), "error_type": type(e).__name__},
)
yield AppContext(
vector_search=vs,
genai_client=genai_client,
settings=config,
settings=cfg,
semantic_cache=semantic_cache,
)
except Exception as e:
@@ -125,7 +136,7 @@ async def lifespan(_server: FastMCP) -> AsyncIterator[AppContext]:
{
"error": str(e),
"error_type": type(e).__name__,
},
}
)
raise
finally:
@@ -135,9 +146,9 @@ async def lifespan(_server: FastMCP) -> AsyncIterator[AppContext]:
try:
await vs.close()
log_structured_entry("Closed aiohttp sessions", "INFO")
except Exception as e: # noqa: BLE001
except Exception as e:
log_structured_entry(
"Error closing aiohttp sessions",
"WARNING",
{"error": str(e), "error_type": type(e).__name__},
{"error": str(e), "error_type": type(e).__name__}
)

View File

@@ -1,21 +1,13 @@
"""Service modules for business logic."""
from .search import (
filter_search_results,
format_search_results,
generate_query_embedding,
)
from .validation import (
validate_gcs_access,
validate_genai_access,
validate_vector_search_access,
)
from .search import filter_search_results, format_search_results, generate_query_embedding
from .validation import validate_genai_access, validate_gcs_access, validate_vector_search_access
__all__ = [
"filter_search_results",
"format_search_results",
"generate_query_embedding",
"validate_gcs_access",
"validate_genai_access",
"validate_gcs_access",
"validate_vector_search_access",
]

View File

@@ -1,10 +1,11 @@
# ruff: noqa: INP001
"""Search helper functions."""
from google import genai
from google.genai import types as genai_types
from knowledge_search_mcp.logging import log_structured_entry
from knowledge_search_mcp.models import SearchResult
from ..logging import log_structured_entry
from ..models import SearchResult
async def generate_query_embedding(
@@ -16,7 +17,6 @@ async def generate_query_embedding(
Returns:
Tuple of (embedding vector, error message). Error message is None on success.
"""
if not query or not query.strip():
return ([], "Error: Query cannot be empty")
@@ -30,11 +30,9 @@ async def generate_query_embedding(
task_type="RETRIEVAL_QUERY",
),
)
if not response.embeddings or not response.embeddings[0].values:
return ([], "Error: Failed to generate embedding - empty response")
embedding = response.embeddings[0].values
return (embedding, None) # noqa: TRY300
except Exception as e: # noqa: BLE001
return (embedding, None)
except Exception as e:
error_type = type(e).__name__
error_msg = str(e)
@@ -43,13 +41,22 @@ async def generate_query_embedding(
log_structured_entry(
"Rate limit exceeded while generating embedding",
"WARNING",
{"error": error_msg, "error_type": error_type, "query": query[:100]},
{
"error": error_msg,
"error_type": error_type,
"query": query[:100]
}
)
return ([], "Error: API rate limit exceeded. Please try again later.")
else:
log_structured_entry(
"Failed to generate query embedding",
"ERROR",
{"error": error_msg, "error_type": error_type, "query": query[:100]},
{
"error": error_msg,
"error_type": error_type,
"query": query[:100]
}
)
return ([], f"Error generating embedding: {error_msg}")
@@ -68,7 +75,6 @@ def filter_search_results(
Returns:
Filtered list of search results.
"""
if not results:
return []
@@ -76,10 +82,14 @@ def filter_search_results(
max_sim = max(r["distance"] for r in results)
cutoff = max_sim * top_percent
return [
s for s in results if s["distance"] > cutoff and s["distance"] > min_similarity
filtered = [
s
for s in results
if s["distance"] > cutoff and s["distance"] > min_similarity
]
return filtered
def format_search_results(results: list[SearchResult]) -> str:
"""Format search results as XML-like documents.
@@ -89,7 +99,6 @@ def format_search_results(results: list[SearchResult]) -> str:
Returns:
Formatted string with document tags.
"""
if not results:
return "No relevant documents found for your query."

View File

@@ -0,0 +1,97 @@
# ruff: noqa: INP001
"""Semantic cache backed by Redis for knowledge search results."""
from redisvl.extensions.cache.llm.semantic import SemanticCache
from redisvl.utils.vectorize.custom import CustomVectorizer
from ..logging import log_structured_entry
def _stub_embed(content: object) -> list[float]:
"""Stub vectorizer so SemanticCache creates an index with the right dims.
Never called at runtime — we always pass pre-computed vectors to
``acheck`` and ``astore``. Only invoked once by ``CustomVectorizer``
at init time to discover the dimensionality.
"""
return [0.0] * _stub_embed.dims # type: ignore[attr-defined]
class KnowledgeSemanticCache:
"""Thin wrapper around RedisVL SemanticCache with FLAT indexing."""
def __init__(
self,
redis_url: str,
name: str = "knowledge_search_cache",
vector_dims: int = 3072,
distance_threshold: float = 0.12,
ttl: int | None = 3600,
) -> None:
_stub_embed.dims = vector_dims # type: ignore[attr-defined]
vectorizer = CustomVectorizer(embed=_stub_embed)
self._cache = SemanticCache(
name=name,
distance_threshold=distance_threshold,
ttl=ttl,
redis_url=redis_url,
vectorizer=vectorizer,
overwrite=False,
)
self._name = name
async def check(
self,
embedding: list[float],
) -> str | None:
"""Return cached response for a semantically similar query, or None."""
try:
results = await self._cache.acheck(
vector=embedding,
num_results=1,
return_fields=["response", "prompt", "vector_distance"],
)
except Exception as e:
log_structured_entry(
"Semantic cache check failed, skipping cache",
"WARNING",
{"error": str(e), "error_type": type(e).__name__},
)
return None
if not results:
return None
hit = results[0]
log_structured_entry(
"Semantic cache hit",
"INFO",
{
"vector_distance": hit.get("vector_distance"),
"original_prompt": hit.get("prompt", "")[:100],
},
)
return hit.get("response")
async def store(
self,
query: str,
response: str,
embedding: list[float],
metadata: dict | None = None,
) -> None:
"""Store a query/response pair in the cache."""
try:
await self._cache.astore(
prompt=query,
response=response,
vector=embedding,
metadata=metadata,
)
except Exception as e:
log_structured_entry(
"Semantic cache store failed",
"WARNING",
{"error": str(e), "error_type": type(e).__name__},
)

View File

@@ -1,26 +1,20 @@
# ruff: noqa: INP001
"""Validation functions for Google Cloud services."""
from gcloud.aio.auth import Token
from google import genai
from google.genai import types as genai_types
from knowledge_search_mcp.clients.vector_search import GoogleCloudVectorSearch
from knowledge_search_mcp.config import Settings
from knowledge_search_mcp.logging import log_structured_entry
# HTTP status codes
HTTP_FORBIDDEN = 403
HTTP_NOT_FOUND = 404
from ..clients.vector_search import GoogleCloudVectorSearch
from ..config import Settings
from ..logging import log_structured_entry
async def validate_genai_access(
genai_client: genai.Client, cfg: Settings
) -> str | None:
async def validate_genai_access(genai_client: genai.Client, cfg: Settings) -> str | None:
"""Validate GenAI embedding access.
Returns:
Error message if validation fails, None if successful.
"""
log_structured_entry("Validating GenAI embedding access", "INFO")
try:
@@ -36,26 +30,20 @@ async def validate_genai_access(
log_structured_entry(
"GenAI embedding validation successful",
"INFO",
{
"embedding_dimension": len(embedding_values)
if embedding_values
else 0
},
{"embedding_dimension": len(embedding_values) if embedding_values else 0}
)
return None
else:
msg = "Embedding validation returned empty response"
log_structured_entry(msg, "WARNING")
return msg # noqa: TRY300
except Exception as e: # noqa: BLE001
return msg
except Exception as e:
log_structured_entry(
(
"Failed to validate GenAI embedding access - "
"service may not work correctly"
),
"Failed to validate GenAI embedding access - service may not work correctly",
"WARNING",
{"error": str(e), "error_type": type(e).__name__},
{"error": str(e), "error_type": type(e).__name__}
)
return f"GenAI: {e!s}"
return f"GenAI: {str(e)}"
async def validate_gcs_access(vs: GoogleCloudVectorSearch, cfg: Settings) -> str | None:
@@ -63,11 +51,14 @@ async def validate_gcs_access(vs: GoogleCloudVectorSearch, cfg: Settings) -> str
Returns:
Error message if validation fails, None if successful.
"""
log_structured_entry("Validating GCS bucket access", "INFO", {"bucket": cfg.bucket})
log_structured_entry(
"Validating GCS bucket access",
"INFO",
{"bucket": cfg.bucket}
)
try:
session = vs.storage._get_aio_session() # noqa: SLF001
session = vs.storage._get_aio_session()
token_obj = Token(
session=session,
scopes=["https://www.googleapis.com/auth/cloud-platform"],
@@ -79,136 +70,102 @@ async def validate_gcs_access(vs: GoogleCloudVectorSearch, cfg: Settings) -> str
f"https://storage.googleapis.com/storage/v1/b/{cfg.bucket}/o?maxResults=1",
headers=headers,
) as response:
if response.status == HTTP_FORBIDDEN:
if response.status == 403:
msg = f"Access denied to bucket '{cfg.bucket}'. Check permissions."
log_structured_entry(
(
"GCS bucket validation failed - access denied - "
"service may not work correctly"
),
"GCS bucket validation failed - access denied - service may not work correctly",
"WARNING",
{"bucket": cfg.bucket, "status": response.status},
{"bucket": cfg.bucket, "status": response.status}
)
return msg
if response.status == HTTP_NOT_FOUND:
elif response.status == 404:
msg = f"Bucket '{cfg.bucket}' not found. Check bucket name and project."
log_structured_entry(
(
"GCS bucket validation failed - not found - "
"service may not work correctly"
),
"GCS bucket validation failed - not found - service may not work correctly",
"WARNING",
{"bucket": cfg.bucket, "status": response.status},
{"bucket": cfg.bucket, "status": response.status}
)
return msg
if not response.ok:
elif not response.ok:
body = await response.text()
msg = f"Failed to access bucket '{cfg.bucket}': {response.status}"
log_structured_entry(
"GCS bucket validation failed - service may not work correctly",
"WARNING",
{"bucket": cfg.bucket, "status": response.status, "response": body},
{"bucket": cfg.bucket, "status": response.status, "response": body}
)
return msg
else:
log_structured_entry(
"GCS bucket validation successful", "INFO", {"bucket": cfg.bucket}
"GCS bucket validation successful",
"INFO",
{"bucket": cfg.bucket}
)
return None
except Exception as e: # noqa: BLE001
except Exception as e:
log_structured_entry(
"Failed to validate GCS bucket access - service may not work correctly",
"WARNING",
{"error": str(e), "error_type": type(e).__name__, "bucket": cfg.bucket},
{"error": str(e), "error_type": type(e).__name__, "bucket": cfg.bucket}
)
return f"GCS: {e!s}"
return f"GCS: {str(e)}"
async def validate_vector_search_access(
vs: GoogleCloudVectorSearch, cfg: Settings
) -> str | None:
async def validate_vector_search_access(vs: GoogleCloudVectorSearch, cfg: Settings) -> str | None:
"""Validate vector search endpoint access.
Returns:
Error message if validation fails, None if successful.
"""
log_structured_entry(
"Validating vector search endpoint access",
"INFO",
{"endpoint_name": cfg.endpoint_name},
{"endpoint_name": cfg.endpoint_name}
)
try:
headers = await vs._async_get_auth_headers() # noqa: SLF001
session = vs._get_aio_session() # noqa: SLF001
headers = await vs._async_get_auth_headers()
session = vs._get_aio_session()
endpoint_url = (
f"https://{cfg.location}-aiplatform.googleapis.com/v1/{cfg.endpoint_name}"
)
async with session.get(endpoint_url, headers=headers) as response:
if response.status == HTTP_FORBIDDEN:
msg = (
f"Access denied to endpoint '{cfg.endpoint_name}'. "
"Check permissions."
)
if response.status == 403:
msg = f"Access denied to endpoint '{cfg.endpoint_name}'. Check permissions."
log_structured_entry(
(
"Vector search endpoint validation failed - "
"access denied - service may not work correctly"
),
"Vector search endpoint validation failed - access denied - service may not work correctly",
"WARNING",
{"endpoint": cfg.endpoint_name, "status": response.status},
{"endpoint": cfg.endpoint_name, "status": response.status}
)
return msg
if response.status == HTTP_NOT_FOUND:
msg = (
f"Endpoint '{cfg.endpoint_name}' not found. "
"Check endpoint name and project."
)
elif response.status == 404:
msg = f"Endpoint '{cfg.endpoint_name}' not found. Check endpoint name and project."
log_structured_entry(
(
"Vector search endpoint validation failed - "
"not found - service may not work correctly"
),
"Vector search endpoint validation failed - not found - service may not work correctly",
"WARNING",
{"endpoint": cfg.endpoint_name, "status": response.status},
{"endpoint": cfg.endpoint_name, "status": response.status}
)
return msg
if not response.ok:
elif not response.ok:
body = await response.text()
msg = (
f"Failed to access endpoint '{cfg.endpoint_name}': "
f"{response.status}"
)
msg = f"Failed to access endpoint '{cfg.endpoint_name}': {response.status}"
log_structured_entry(
(
"Vector search endpoint validation failed - "
"service may not work correctly"
),
"Vector search endpoint validation failed - service may not work correctly",
"WARNING",
{
"endpoint": cfg.endpoint_name,
"status": response.status,
"response": body,
},
{"endpoint": cfg.endpoint_name, "status": response.status, "response": body}
)
return msg
else:
log_structured_entry(
"Vector search endpoint validation successful",
"INFO",
{"endpoint": cfg.endpoint_name},
{"endpoint": cfg.endpoint_name}
)
return None
except Exception as e: # noqa: BLE001
except Exception as e:
log_structured_entry(
(
"Failed to validate vector search endpoint access - "
"service may not work correctly"
),
"Failed to validate vector search endpoint access - service may not work correctly",
"WARNING",
{
"error": str(e),
"error_type": type(e).__name__,
"endpoint": cfg.endpoint_name,
},
{"error": str(e), "error_type": type(e).__name__, "endpoint": cfg.endpoint_name}
)
return f"Vector Search: {e!s}"
return f"Vector Search: {str(e)}"

View File

@@ -1,3 +1,4 @@
# ruff: noqa: INP001
"""LRU cache implementation."""
from collections import OrderedDict

View File

@@ -28,6 +28,9 @@ class TestKnowledgeSearch:
app.settings.deployed_index_id = "test-deployed-index"
app.settings.search_limit = 10
# No semantic cache by default
app.semantic_cache = None
return app
@pytest.fixture

View File

@@ -0,0 +1,272 @@
"""Tests for the semantic cache service and its integration."""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from knowledge_search_mcp.__main__ import knowledge_search
from knowledge_search_mcp.models import AppContext, SearchResult, SourceNamespace
from knowledge_search_mcp.services.semantic_cache import KnowledgeSemanticCache
class TestKnowledgeSemanticCache:
"""Unit tests for the KnowledgeSemanticCache wrapper."""
@patch("knowledge_search_mcp.services.semantic_cache.CustomVectorizer")
@patch("knowledge_search_mcp.services.semantic_cache.SemanticCache")
def test_init_creates_cache(self, mock_sc_cls, mock_vec_cls):
"""Test that __init__ creates the SemanticCache with correct params."""
mock_vectorizer = MagicMock()
mock_vec_cls.return_value = mock_vectorizer
KnowledgeSemanticCache(
redis_url="redis://localhost:6379",
name="test_cache",
vector_dims=3072,
distance_threshold=0.12,
ttl=3600,
)
mock_vec_cls.assert_called_once()
mock_sc_cls.assert_called_once_with(
name="test_cache",
distance_threshold=0.12,
ttl=3600,
redis_url="redis://localhost:6379",
vectorizer=mock_vectorizer,
overwrite=False,
)
@patch("knowledge_search_mcp.services.semantic_cache.CustomVectorizer")
@patch("knowledge_search_mcp.services.semantic_cache.SemanticCache")
async def test_check_returns_response_on_hit(self, mock_sc_cls, _mock_vec_cls):
"""Test cache check returns response when a similar vector is found."""
mock_inner = MagicMock()
mock_inner.acheck = AsyncMock(return_value=[
{"response": "cached answer", "prompt": "original q", "vector_distance": 0.05},
])
mock_sc_cls.return_value = mock_inner
cache = KnowledgeSemanticCache(redis_url="redis://localhost:6379")
result = await cache.check([0.1] * 3072)
assert result == "cached answer"
mock_inner.acheck.assert_awaited_once_with(
vector=[0.1] * 3072,
num_results=1,
)
@patch("knowledge_search_mcp.services.semantic_cache.CustomVectorizer")
@patch("knowledge_search_mcp.services.semantic_cache.SemanticCache")
async def test_check_returns_none_on_miss(self, mock_sc_cls, _mock_vec_cls):
"""Test cache check returns None when no similar vector is found."""
mock_inner = MagicMock()
mock_inner.acheck = AsyncMock(return_value=[])
mock_sc_cls.return_value = mock_inner
cache = KnowledgeSemanticCache(redis_url="redis://localhost:6379")
result = await cache.check([0.1] * 3072)
assert result is None
@patch("knowledge_search_mcp.services.semantic_cache.CustomVectorizer")
@patch("knowledge_search_mcp.services.semantic_cache.SemanticCache")
async def test_check_returns_none_on_error(self, mock_sc_cls, _mock_vec_cls):
"""Test cache check degrades gracefully on Redis errors."""
mock_inner = MagicMock()
mock_inner.acheck = AsyncMock(side_effect=ConnectionError("Redis down"))
mock_sc_cls.return_value = mock_inner
cache = KnowledgeSemanticCache(redis_url="redis://localhost:6379")
result = await cache.check([0.1] * 3072)
assert result is None
@patch("knowledge_search_mcp.services.semantic_cache.CustomVectorizer")
@patch("knowledge_search_mcp.services.semantic_cache.SemanticCache")
async def test_store_calls_astore(self, mock_sc_cls, _mock_vec_cls):
"""Test store delegates to SemanticCache.astore."""
mock_inner = MagicMock()
mock_inner.astore = AsyncMock()
mock_sc_cls.return_value = mock_inner
cache = KnowledgeSemanticCache(redis_url="redis://localhost:6379")
await cache.store("query", "response", [0.1] * 3072, {"key": "val"})
mock_inner.astore.assert_awaited_once_with(
prompt="query",
response="response",
vector=[0.1] * 3072,
metadata={"key": "val"},
)
@patch("knowledge_search_mcp.services.semantic_cache.CustomVectorizer")
@patch("knowledge_search_mcp.services.semantic_cache.SemanticCache")
async def test_store_does_not_raise_on_error(self, mock_sc_cls, _mock_vec_cls):
"""Test store degrades gracefully on Redis errors."""
mock_inner = MagicMock()
mock_inner.astore = AsyncMock(side_effect=ConnectionError("Redis down"))
mock_sc_cls.return_value = mock_inner
cache = KnowledgeSemanticCache(redis_url="redis://localhost:6379")
await cache.store("query", "response", [0.1] * 3072)
class TestKnowledgeSearchCacheIntegration:
"""Tests for cache integration in the knowledge_search tool."""
@pytest.fixture
def mock_cache(self):
"""Create a mock KnowledgeSemanticCache."""
cache = MagicMock(spec=KnowledgeSemanticCache)
cache.check = AsyncMock(return_value=None)
cache.store = AsyncMock()
return cache
@pytest.fixture
def mock_app_context(self, mock_cache):
"""Create a mock AppContext with semantic cache."""
app = MagicMock(spec=AppContext)
app.genai_client = MagicMock()
app.vector_search = MagicMock()
app.vector_search.async_run_query = AsyncMock()
app.settings = MagicMock()
app.settings.embedding_model = "gemini-embedding-001"
app.settings.deployed_index_id = "test-deployed-index"
app.settings.search_limit = 10
app.semantic_cache = mock_cache
return app
@pytest.fixture
def mock_context(self, mock_app_context):
"""Create a mock MCP Context."""
ctx = MagicMock()
ctx.request_context.lifespan_context = mock_app_context
return ctx
@pytest.fixture
def sample_embedding(self):
return [0.1] * 3072
@pytest.fixture
def sample_results(self) -> list[SearchResult]:
return [
{"id": "doc1", "distance": 0.95, "content": "Content 1"},
{"id": "doc2", "distance": 0.90, "content": "Content 2"},
]
@patch("knowledge_search_mcp.__main__.generate_query_embedding")
async def test_cache_hit_skips_vector_search(
self, mock_generate, mock_context, sample_embedding, mock_cache
):
"""On cache hit, vector search is never called."""
mock_generate.return_value = (sample_embedding, None)
mock_cache.check.return_value = "cached result"
result = await knowledge_search("test query", mock_context)
assert result == "cached result"
mock_cache.check.assert_awaited_once_with(sample_embedding)
mock_context.request_context.lifespan_context.vector_search.async_run_query.assert_not_called()
mock_cache.store.assert_not_awaited()
@patch("knowledge_search_mcp.__main__.generate_query_embedding")
@patch("knowledge_search_mcp.__main__.filter_search_results")
@patch("knowledge_search_mcp.__main__.format_search_results")
async def test_cache_miss_stores_result(
self,
mock_format,
mock_filter,
mock_generate,
mock_context,
sample_embedding,
sample_results,
mock_cache,
):
"""On cache miss, results are fetched and stored in cache."""
mock_generate.return_value = (sample_embedding, None)
mock_cache.check.return_value = None
mock_context.request_context.lifespan_context.vector_search.async_run_query.return_value = sample_results
mock_filter.return_value = sample_results
mock_format.return_value = "formatted results"
result = await knowledge_search("test query", mock_context)
assert result == "formatted results"
mock_cache.check.assert_awaited_once_with(sample_embedding)
mock_cache.store.assert_awaited_once_with(
"test query", "formatted results", sample_embedding,
)
@patch("knowledge_search_mcp.__main__.generate_query_embedding")
@patch("knowledge_search_mcp.__main__.filter_search_results")
@patch("knowledge_search_mcp.__main__.format_search_results")
async def test_cache_skipped_when_source_filter_set(
self,
mock_format,
mock_filter,
mock_generate,
mock_context,
sample_embedding,
sample_results,
mock_cache,
):
"""Cache is bypassed when a source filter is specified."""
mock_generate.return_value = (sample_embedding, None)
mock_context.request_context.lifespan_context.vector_search.async_run_query.return_value = sample_results
mock_filter.return_value = sample_results
mock_format.return_value = "formatted results"
result = await knowledge_search(
"test query", mock_context, source=SourceNamespace.EDUCACION_FINANCIERA,
)
assert result == "formatted results"
mock_cache.check.assert_not_awaited()
mock_cache.store.assert_not_awaited()
@patch("knowledge_search_mcp.__main__.generate_query_embedding")
@patch("knowledge_search_mcp.__main__.filter_search_results")
@patch("knowledge_search_mcp.__main__.format_search_results")
async def test_cache_not_stored_when_no_results(
self,
mock_format,
mock_filter,
mock_generate,
mock_context,
sample_embedding,
mock_cache,
):
"""Empty results are not stored in the cache."""
mock_generate.return_value = (sample_embedding, None)
mock_cache.check.return_value = None
mock_context.request_context.lifespan_context.vector_search.async_run_query.return_value = []
mock_filter.return_value = []
mock_format.return_value = "No relevant documents found for your query."
result = await knowledge_search("test query", mock_context)
assert result == "No relevant documents found for your query."
mock_cache.store.assert_not_awaited()
@patch("knowledge_search_mcp.__main__.generate_query_embedding")
@patch("knowledge_search_mcp.__main__.filter_search_results")
@patch("knowledge_search_mcp.__main__.format_search_results")
async def test_works_without_cache(
self,
mock_format,
mock_filter,
mock_generate,
mock_context,
sample_embedding,
sample_results,
):
"""Tool works normally when semantic_cache is None."""
mock_generate.return_value = (sample_embedding, None)
mock_context.request_context.lifespan_context.semantic_cache = None
mock_context.request_context.lifespan_context.vector_search.async_run_query.return_value = sample_results
mock_filter.return_value = sample_results
mock_format.return_value = "formatted results"
result = await knowledge_search("test query", mock_context)
assert result == "formatted results"

191
uv.lock generated
View File

@@ -1327,6 +1327,66 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "hiredis"
version = "3.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/82/d2817ce0653628e0a0cb128533f6af0dd6318a49f3f3a6a7bd1f2f2154af/hiredis-3.3.0.tar.gz", hash = "sha256:105596aad9249634361815c574351f1bd50455dc23b537c2940066c4a9dea685", size = 89048, upload-time = "2025-10-14T16:33:34.263Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/1c/ed28ae5d704f5c7e85b946fa327f30d269e6272c847fef7e91ba5fc86193/hiredis-3.3.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5b8e1d6a2277ec5b82af5dce11534d3ed5dffeb131fd9b210bc1940643b39b5f", size = 82026, upload-time = "2025-10-14T16:32:12.004Z" },
{ url = "https://files.pythonhosted.org/packages/f4/9b/79f30c5c40e248291023b7412bfdef4ad9a8a92d9e9285d65d600817dac7/hiredis-3.3.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c4981de4d335f996822419e8a8b3b87367fcef67dc5fb74d3bff4df9f6f17783", size = 46217, upload-time = "2025-10-14T16:32:13.133Z" },
{ url = "https://files.pythonhosted.org/packages/e7/c3/02b9ed430ad9087aadd8afcdf616717452d16271b701fa47edfe257b681e/hiredis-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1706480a683e328ae9ba5d704629dee2298e75016aa0207e7067b9c40cecc271", size = 41858, upload-time = "2025-10-14T16:32:13.98Z" },
{ url = "https://files.pythonhosted.org/packages/f1/98/b2a42878b82130a535c7aa20bc937ba2d07d72e9af3ad1ad93e837c419b5/hiredis-3.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a95cef9989736ac313639f8f545b76b60b797e44e65834aabbb54e4fad8d6c8", size = 170195, upload-time = "2025-10-14T16:32:14.728Z" },
{ url = "https://files.pythonhosted.org/packages/66/1d/9dcde7a75115d3601b016113d9b90300726fa8e48aacdd11bf01a453c145/hiredis-3.3.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca2802934557ccc28a954414c245ba7ad904718e9712cb67c05152cf6b9dd0a3", size = 181808, upload-time = "2025-10-14T16:32:15.622Z" },
{ url = "https://files.pythonhosted.org/packages/56/a1/60f6bda9b20b4e73c85f7f5f046bc2c154a5194fc94eb6861e1fd97ced52/hiredis-3.3.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fe730716775f61e76d75810a38ee4c349d3af3896450f1525f5a4034cf8f2ed7", size = 180578, upload-time = "2025-10-14T16:32:16.514Z" },
{ url = "https://files.pythonhosted.org/packages/d9/01/859d21de65085f323a701824e23ea3330a0ac05f8e184544d7aa5c26128d/hiredis-3.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:749faa69b1ce1f741f5eaf743435ac261a9262e2d2d66089192477e7708a9abc", size = 172508, upload-time = "2025-10-14T16:32:17.411Z" },
{ url = "https://files.pythonhosted.org/packages/99/a8/28fd526e554c80853d0fbf57ef2a3235f00e4ed34ce0e622e05d27d0f788/hiredis-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:95c9427f2ac3f1dd016a3da4e1161fa9d82f221346c8f3fdd6f3f77d4e28946c", size = 166341, upload-time = "2025-10-14T16:32:18.561Z" },
{ url = "https://files.pythonhosted.org/packages/f2/91/ded746b7d2914f557fbbf77be55e90d21f34ba758ae10db6591927c642c8/hiredis-3.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c863ee44fe7bff25e41f3a5105c936a63938b76299b802d758f40994ab340071", size = 176765, upload-time = "2025-10-14T16:32:19.491Z" },
{ url = "https://files.pythonhosted.org/packages/d6/4c/04aa46ff386532cb5f08ee495c2bf07303e93c0acf2fa13850e031347372/hiredis-3.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2213c7eb8ad5267434891f3241c7776e3bafd92b5933fc57d53d4456247dc542", size = 170312, upload-time = "2025-10-14T16:32:20.404Z" },
{ url = "https://files.pythonhosted.org/packages/90/6e/67f9d481c63f542a9cf4c9f0ea4e5717db0312fb6f37fb1f78f3a66de93c/hiredis-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a172bae3e2837d74530cd60b06b141005075db1b814d966755977c69bd882ce8", size = 167965, upload-time = "2025-10-14T16:32:21.259Z" },
{ url = "https://files.pythonhosted.org/packages/7a/df/dde65144d59c3c0d85e43255798f1fa0c48d413e668cfd92b3d9f87924ef/hiredis-3.3.0-cp312-cp312-win32.whl", hash = "sha256:cb91363b9fd6d41c80df9795e12fffbaf5c399819e6ae8120f414dedce6de068", size = 20533, upload-time = "2025-10-14T16:32:22.192Z" },
{ url = "https://files.pythonhosted.org/packages/f5/a9/55a4ac9c16fdf32e92e9e22c49f61affe5135e177ca19b014484e28950f7/hiredis-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:04ec150e95eea3de9ff8bac754978aa17b8bf30a86d4ab2689862020945396b0", size = 22379, upload-time = "2025-10-14T16:32:22.916Z" },
{ url = "https://files.pythonhosted.org/packages/6d/39/2b789ebadd1548ccb04a2c18fbc123746ad1a7e248b7f3f3cac618ca10a6/hiredis-3.3.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:b7048b4ec0d5dddc8ddd03da603de0c4b43ef2540bf6e4c54f47d23e3480a4fa", size = 82035, upload-time = "2025-10-14T16:32:23.715Z" },
{ url = "https://files.pythonhosted.org/packages/85/74/4066d9c1093be744158ede277f2a0a4e4cd0fefeaa525c79e2876e9e5c72/hiredis-3.3.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:e5f86ce5a779319c15567b79e0be806e8e92c18bb2ea9153e136312fafa4b7d6", size = 46219, upload-time = "2025-10-14T16:32:24.554Z" },
{ url = "https://files.pythonhosted.org/packages/fa/3f/f9e0f6d632f399d95b3635703e1558ffaa2de3aea4cfcbc2d7832606ba43/hiredis-3.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fbdb97a942e66016fff034df48a7a184e2b7dc69f14c4acd20772e156f20d04b", size = 41860, upload-time = "2025-10-14T16:32:25.356Z" },
{ url = "https://files.pythonhosted.org/packages/4a/c5/b7dde5ec390dabd1cabe7b364a509c66d4e26de783b0b64cf1618f7149fc/hiredis-3.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0fb4bea72fe45ff13e93ddd1352b43ff0749f9866263b5cca759a4c960c776f", size = 170094, upload-time = "2025-10-14T16:32:26.148Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d6/7f05c08ee74d41613be466935688068e07f7b6c55266784b5ace7b35b766/hiredis-3.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85b9baf98050e8f43c2826ab46aaf775090d608217baf7af7882596aef74e7f9", size = 181746, upload-time = "2025-10-14T16:32:27.844Z" },
{ url = "https://files.pythonhosted.org/packages/0e/d2/aaf9f8edab06fbf5b766e0cae3996324297c0516a91eb2ca3bd1959a0308/hiredis-3.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69079fb0f0ebb61ba63340b9c4bce9388ad016092ca157e5772eb2818209d930", size = 180465, upload-time = "2025-10-14T16:32:29.185Z" },
{ url = "https://files.pythonhosted.org/packages/8d/1e/93ded8b9b484519b211fc71746a231af98c98928e3ebebb9086ed20bb1ad/hiredis-3.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c17f77b79031ea4b0967d30255d2ae6e7df0603ee2426ad3274067f406938236", size = 172419, upload-time = "2025-10-14T16:32:30.059Z" },
{ url = "https://files.pythonhosted.org/packages/68/13/02880458e02bbfcedcaabb8f7510f9dda1c89d7c1921b1bb28c22bb38cbf/hiredis-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d14f745fc177bc05fc24bdf20e2b515e9a068d3d4cce90a0fb78d04c9c9d9a", size = 166400, upload-time = "2025-10-14T16:32:31.173Z" },
{ url = "https://files.pythonhosted.org/packages/11/60/896e03267670570f19f61dc65a2137fcb2b06e83ab0911d58eeec9f3cb88/hiredis-3.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ba063fdf1eff6377a0c409609cbe890389aefddfec109c2d20fcc19cfdafe9da", size = 176845, upload-time = "2025-10-14T16:32:32.12Z" },
{ url = "https://files.pythonhosted.org/packages/f1/90/a1d4bd0cdcf251fda72ac0bd932f547b48ad3420f89bb2ef91bf6a494534/hiredis-3.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1799cc66353ad066bfdd410135c951959da9f16bcb757c845aab2f21fc4ef099", size = 170365, upload-time = "2025-10-14T16:32:33.035Z" },
{ url = "https://files.pythonhosted.org/packages/f1/9a/7c98f7bb76bdb4a6a6003cf8209721f083e65d2eed2b514f4a5514bda665/hiredis-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2cbf71a121996ffac82436b6153290815b746afb010cac19b3290a1644381b07", size = 168022, upload-time = "2025-10-14T16:32:34.81Z" },
{ url = "https://files.pythonhosted.org/packages/0d/ca/672ee658ffe9525558615d955b554ecd36aa185acd4431ccc9701c655c9b/hiredis-3.3.0-cp313-cp313-win32.whl", hash = "sha256:a7cbbc6026bf03659f0b25e94bbf6e64f6c8c22f7b4bc52fe569d041de274194", size = 20533, upload-time = "2025-10-14T16:32:35.7Z" },
{ url = "https://files.pythonhosted.org/packages/20/93/511fd94f6a7b6d72a4cf9c2b159bf3d780585a9a1dca52715dd463825299/hiredis-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:a8def89dd19d4e2e4482b7412d453dec4a5898954d9a210d7d05f60576cedef6", size = 22387, upload-time = "2025-10-14T16:32:36.441Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b3/b948ee76a6b2bc7e45249861646f91f29704f743b52565cf64cee9c4658b/hiredis-3.3.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c135bda87211f7af9e2fd4e046ab433c576cd17b69e639a0f5bb2eed5e0e71a9", size = 82105, upload-time = "2025-10-14T16:32:37.204Z" },
{ url = "https://files.pythonhosted.org/packages/a2/9b/4210f4ebfb3ab4ada964b8de08190f54cbac147198fb463cd3c111cc13e0/hiredis-3.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f855c678230aed6fc29b962ce1cc67e5858a785ef3a3fd6b15dece0487a2e60", size = 46237, upload-time = "2025-10-14T16:32:38.07Z" },
{ url = "https://files.pythonhosted.org/packages/b3/7a/e38bfd7d04c05036b4ccc6f42b86b1032185cf6ae426e112a97551fece14/hiredis-3.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4059c78a930cbb33c391452ccce75b137d6f89e2eebf6273d75dafc5c2143c03", size = 41894, upload-time = "2025-10-14T16:32:38.929Z" },
{ url = "https://files.pythonhosted.org/packages/28/d3/eae43d9609c5d9a6effef0586ee47e13a0d84b44264b688d97a75cd17ee5/hiredis-3.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:334a3f1d14c253bb092e187736c3384203bd486b244e726319bbb3f7dffa4a20", size = 170486, upload-time = "2025-10-14T16:32:40.147Z" },
{ url = "https://files.pythonhosted.org/packages/c3/fd/34d664554880b27741ab2916d66207357563b1639e2648685f4c84cfb755/hiredis-3.3.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd137b147235447b3d067ec952c5b9b95ca54b71837e1b38dbb2ec03b89f24fc", size = 182031, upload-time = "2025-10-14T16:32:41.06Z" },
{ url = "https://files.pythonhosted.org/packages/08/a3/0c69fdde3f4155b9f7acc64ccffde46f312781469260061b3bbaa487fd34/hiredis-3.3.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f88f4f2aceb73329ece86a1cb0794fdbc8e6d614cb5ca2d1023c9b7eb432db8", size = 180542, upload-time = "2025-10-14T16:32:42.993Z" },
{ url = "https://files.pythonhosted.org/packages/68/7a/ad5da4d7bc241e57c5b0c4fe95aa75d1f2116e6e6c51577394d773216e01/hiredis-3.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:550f4d1538822fc75ebf8cf63adc396b23d4958bdbbad424521f2c0e3dfcb169", size = 172353, upload-time = "2025-10-14T16:32:43.965Z" },
{ url = "https://files.pythonhosted.org/packages/4b/dc/c46eace64eb047a5b31acd5e4b0dc6d2f0390a4a3f6d507442d9efa570ad/hiredis-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54b14211fbd5930fc696f6fcd1f1f364c660970d61af065a80e48a1fa5464dd6", size = 166435, upload-time = "2025-10-14T16:32:44.97Z" },
{ url = "https://files.pythonhosted.org/packages/4a/ac/ad13a714e27883a2e4113c980c94caf46b801b810de5622c40f8d3e8335f/hiredis-3.3.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9e96f63dbc489fc86f69951e9f83dadb9582271f64f6822c47dcffa6fac7e4a", size = 177218, upload-time = "2025-10-14T16:32:45.936Z" },
{ url = "https://files.pythonhosted.org/packages/c2/38/268fabd85b225271fe1ba82cb4a484fcc1bf922493ff2c74b400f1a6f339/hiredis-3.3.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:106e99885d46684d62ab3ec1d6b01573cc0e0083ac295b11aaa56870b536c7ec", size = 170477, upload-time = "2025-10-14T16:32:46.898Z" },
{ url = "https://files.pythonhosted.org/packages/20/6b/02bb8af810ea04247334ab7148acff7a61c08a8832830c6703f464be83a9/hiredis-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:087e2ef3206361281b1a658b5b4263572b6ba99465253e827796964208680459", size = 167915, upload-time = "2025-10-14T16:32:47.847Z" },
{ url = "https://files.pythonhosted.org/packages/83/94/901fa817e667b2e69957626395e6dee416e31609dca738f28e6b545ca6c2/hiredis-3.3.0-cp314-cp314-win32.whl", hash = "sha256:80638ebeab1cefda9420e9fedc7920e1ec7b4f0513a6b23d58c9d13c882f8065", size = 21165, upload-time = "2025-10-14T16:32:50.753Z" },
{ url = "https://files.pythonhosted.org/packages/b1/7e/4881b9c1d0b4cdaba11bd10e600e97863f977ea9d67c5988f7ec8cd363e5/hiredis-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a68aaf9ba024f4e28cf23df9196ff4e897bd7085872f3a30644dca07fa787816", size = 22996, upload-time = "2025-10-14T16:32:51.543Z" },
{ url = "https://files.pythonhosted.org/packages/a7/b6/d7e6c17da032665a954a89c1e6ee3bd12cb51cd78c37527842b03519981d/hiredis-3.3.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f7f80442a32ce51ee5d89aeb5a84ee56189a0e0e875f1a57bbf8d462555ae48f", size = 83034, upload-time = "2025-10-14T16:32:52.395Z" },
{ url = "https://files.pythonhosted.org/packages/27/6c/6751b698060cdd1b2d8427702cff367c9ed7a1705bcf3792eb5b896f149b/hiredis-3.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a1a67530da714954ed50579f4fe1ab0ddbac9c43643b1721c2cb226a50dde263", size = 46701, upload-time = "2025-10-14T16:32:53.572Z" },
{ url = "https://files.pythonhosted.org/packages/ce/8e/20a5cf2c83c7a7e08c76b9abab113f99f71cd57468a9c7909737ce6e9bf8/hiredis-3.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:616868352e47ab355559adca30f4f3859f9db895b4e7bc71e2323409a2add751", size = 42381, upload-time = "2025-10-14T16:32:54.762Z" },
{ url = "https://files.pythonhosted.org/packages/be/0a/547c29c06e8c9c337d0df3eec39da0cf1aad701daf8a9658dd37f25aca66/hiredis-3.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e799b79f3150083e9702fc37e6243c0bd47a443d6eae3f3077b0b3f510d6a145", size = 180313, upload-time = "2025-10-14T16:32:55.644Z" },
{ url = "https://files.pythonhosted.org/packages/89/8a/488de5469e3d0921a1c425045bf00e983d48b2111a90e47cf5769eaa536c/hiredis-3.3.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ef1dfb0d2c92c3701655e2927e6bbe10c499aba632c7ea57b6392516df3864b", size = 190488, upload-time = "2025-10-14T16:32:56.649Z" },
{ url = "https://files.pythonhosted.org/packages/b5/59/8493edc3eb9ae0dbea2b2230c2041a52bc03e390b02ffa3ac0bca2af9aea/hiredis-3.3.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c290da6bc2a57e854c7da9956cd65013483ede935677e84560da3b848f253596", size = 189210, upload-time = "2025-10-14T16:32:57.759Z" },
{ url = "https://files.pythonhosted.org/packages/f0/de/8c9a653922057b32fb1e2546ecd43ef44c9aa1a7cf460c87cae507eb2bc7/hiredis-3.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd8c438d9e1728f0085bf9b3c9484d19ec31f41002311464e75b69550c32ffa8", size = 180972, upload-time = "2025-10-14T16:32:58.737Z" },
{ url = "https://files.pythonhosted.org/packages/e4/a3/51e6e6afaef2990986d685ca6e254ffbd191f1635a59b2d06c9e5d10c8a2/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1bbc6b8a88bbe331e3ebf6685452cebca6dfe6d38a6d4efc5651d7e363ba28bd", size = 175315, upload-time = "2025-10-14T16:32:59.774Z" },
{ url = "https://files.pythonhosted.org/packages/96/54/e436312feb97601f70f8b39263b8da5ac4a5d18305ebdfb08ad7621f6119/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:55d8c18fe9a05496c5c04e6eccc695169d89bf358dff964bcad95696958ec05f", size = 185653, upload-time = "2025-10-14T16:33:00.749Z" },
{ url = "https://files.pythonhosted.org/packages/ed/a3/88e66030d066337c6c0f883a912c6d4b2d6d7173490fbbc113a6cbe414ff/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4ddc79afa76b805d364e202a754666cb3c4d9c85153cbfed522871ff55827838", size = 179032, upload-time = "2025-10-14T16:33:01.711Z" },
{ url = "https://files.pythonhosted.org/packages/bc/1f/fb7375467e9adaa371cd617c2984fefe44bdce73add4c70b8dd8cab1b33a/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e8a4b8540581dcd1b2b25827a54cfd538e0afeaa1a0e3ca87ad7126965981cc", size = 176127, upload-time = "2025-10-14T16:33:02.793Z" },
{ url = "https://files.pythonhosted.org/packages/66/14/0dc2b99209c400f3b8f24067273e9c3cb383d894e155830879108fb19e98/hiredis-3.3.0-cp314-cp314t-win32.whl", hash = "sha256:298593bb08487753b3afe6dc38bac2532e9bac8dcee8d992ef9977d539cc6776", size = 22024, upload-time = "2025-10-14T16:33:03.812Z" },
{ url = "https://files.pythonhosted.org/packages/b2/2f/8a0befeed8bbe142d5a6cf3b51e8cbe019c32a64a596b0ebcbc007a8f8f1/hiredis-3.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b442b6ab038a6f3b5109874d2514c4edf389d8d8b553f10f12654548808683bc", size = 23808, upload-time = "2025-10-14T16:33:04.965Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
@@ -1406,6 +1466,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "jsonpath-ng"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/32/58/250751940d75c8019659e15482d548a4aa3b6ce122c515102a4bfdac50e3/jsonpath_ng-1.8.0.tar.gz", hash = "sha256:54252968134b5e549ea5b872f1df1168bd7defe1a52fed5a358c194e1943ddc3", size = 74513, upload-time = "2026-02-24T14:42:06.182Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/99/33c7d78a3fb70d545fd5411ac67a651c81602cc09c9cf0df383733f068c5/jsonpath_ng-1.8.0-py3-none-any.whl", hash = "sha256:b8dde192f8af58d646fc031fac9c99fe4d00326afc4148f1f043c601a8cfe138", size = 67844, upload-time = "2026-02-28T00:53:19.637Z" },
]
[[package]]
name = "jsonschema"
version = "4.26.0"
@@ -1446,6 +1515,8 @@ dependencies = [
{ name = "mcp", extra = ["cli"] },
{ name = "pydantic-settings" },
{ name = "pyyaml" },
{ name = "redis", extra = ["hiredis"] },
{ name = "redisvl" },
]
[package.dev-dependencies]
@@ -1468,6 +1539,8 @@ requires-dist = [
{ name = "mcp", extras = ["cli"], specifier = ">=1.26.0" },
{ name = "pydantic-settings", specifier = ">=2.9.1" },
{ name = "pyyaml", specifier = ">=6.0" },
{ name = "redis", extras = ["hiredis"], specifier = ">=5.0.0,<7" },
{ name = "redisvl", specifier = ">=0.6.0" },
]
[package.metadata.requires-dev]
@@ -1607,6 +1680,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "ml-dtypes"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/15/76f86faa0902836cc133939732f7611ace68cf54148487a99c539c272dc8/ml_dtypes-0.4.1.tar.gz", hash = "sha256:fad5f2de464fd09127e49b7fd1252b9006fb43d2edc1ff112d390c324af5ca7a", size = 692594, upload-time = "2024-09-13T19:07:11.624Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/1a/99e924f12e4b62139fbac87419698c65f956d58de0dbfa7c028fa5b096aa/ml_dtypes-0.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:827d3ca2097085cf0355f8fdf092b888890bb1b1455f52801a2d7756f056f54b", size = 405077, upload-time = "2024-09-13T19:06:57.538Z" },
{ url = "https://files.pythonhosted.org/packages/8f/8c/7b610bd500617854c8cc6ed7c8cfb9d48d6a5c21a1437a36a4b9bc8a3598/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772426b08a6172a891274d581ce58ea2789cc8abc1c002a27223f314aaf894e7", size = 2181554, upload-time = "2024-09-13T19:06:59.196Z" },
{ url = "https://files.pythonhosted.org/packages/c7/c6/f89620cecc0581dc1839e218c4315171312e46c62a62da6ace204bda91c0/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126e7d679b8676d1a958f2651949fbfa182832c3cd08020d8facd94e4114f3e9", size = 2160488, upload-time = "2024-09-13T19:07:03.131Z" },
{ url = "https://files.pythonhosted.org/packages/ae/11/a742d3c31b2cc8557a48efdde53427fd5f9caa2fa3c9c27d826e78a66f51/ml_dtypes-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0fb650d5c582a9e72bb5bd96cfebb2cdb889d89daff621c8fbc60295eba66c", size = 127462, upload-time = "2024-09-13T19:07:04.916Z" },
]
[[package]]
name = "mmh3"
version = "5.2.0"
@@ -1786,6 +1874,67 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
]
[[package]]
name = "numpy"
version = "2.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" },
{ url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" },
{ url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" },
{ url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" },
{ url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" },
{ url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" },
{ url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" },
{ url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" },
{ url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" },
{ url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" },
{ url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" },
{ url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" },
{ url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" },
{ url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" },
{ url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" },
{ url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" },
{ url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" },
{ url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" },
{ url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" },
{ url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" },
{ url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" },
{ url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" },
{ url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" },
{ url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" },
{ url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" },
{ url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" },
{ url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" },
{ url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" },
{ url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" },
{ url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" },
{ url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" },
{ url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" },
{ url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" },
{ url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" },
{ url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" },
{ url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" },
{ url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" },
{ url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" },
{ url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" },
{ url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" },
{ url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" },
{ url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" },
{ url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" },
{ url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" },
{ url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" },
{ url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" },
{ url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" },
{ url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" },
{ url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" },
{ url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" },
]
[[package]]
name = "opentelemetry-api"
version = "1.38.0"
@@ -2348,6 +2497,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
]
[[package]]
name = "python-ulid"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/40/7e/0d6c82b5ccc71e7c833aed43d9e8468e1f2ff0be1b3f657a6fcafbb8433d/python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636", size = 93175, upload-time = "2025-08-18T16:09:26.305Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577, upload-time = "2025-08-18T16:09:25.047Z" },
]
[[package]]
name = "pywin32"
version = "311"
@@ -2410,6 +2568,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "redis"
version = "6.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" },
]
[package.optional-dependencies]
hiredis = [
{ name = "hiredis" },
]
[[package]]
name = "redisvl"
version = "0.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpath-ng" },
{ name = "ml-dtypes" },
{ name = "numpy" },
{ name = "pydantic" },
{ name = "python-ulid" },
{ name = "pyyaml" },
{ name = "redis" },
{ name = "tenacity" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/1a/f1f0ff963622c34a9e9a9f2a0c6ad82bfbd05c082ecc89e38e092e3e9069/redisvl-0.15.0.tar.gz", hash = "sha256:0e382e9b6cd8378dfe1515b18f92d125cfba905f6f3c5fe9b8904b3ca840d1ca", size = 861480, upload-time = "2026-02-27T14:02:33.366Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/23/5c5263a3cfc66957fa3bb154ef9441fbbcfb2f4eae910eb18e316db168b1/redisvl-0.15.0-py3-none-any.whl", hash = "sha256:aff716b9a9c4aef9c81de9a12d9939a0170ff3b3a1fe9d4164e94b131a754290", size = 197935, upload-time = "2026-02-27T14:02:31.262Z" },
]
[[package]]
name = "referencing"
version = "0.37.0"