Compare commits

..

5 Commits

Author SHA1 Message Date
132ea1c04f Add semantic caching
Some checks failed
CI / lint (pull_request) Failing after 12s
CI / typecheck (pull_request) Successful in 13s
CI / test (pull_request) Failing after 27s
2026-03-05 22:10:46 +00:00
0cdf9cd44e Merge pull request 'Add CI' (#10) from push-ooqqtrvlvqxn into main
All checks were successful
CI / lint (push) Successful in 11s
CI / typecheck (push) Successful in 12s
CI / test (push) Successful in 25s
Reviewed-on: #10
2026-03-05 22:00:48 +00:00
d39b8a6ea7 Add CI
All checks were successful
CI / lint (pull_request) Successful in 10s
CI / typecheck (pull_request) Successful in 11s
CI / test (pull_request) Successful in 25s
2026-03-05 21:59:22 +00:00
86ed34887b Make cloud logging optional 2026-03-05 21:36:03 +00:00
694b060fa4 Merge pull request 'Refactor into package' (#8) from push-ymnnsrokkmwy into main
Reviewed-on: #8
2026-03-04 05:15:02 +00:00
21 changed files with 973 additions and 218 deletions

43
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,43 @@
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

3
CLAUDE.md Normal file
View File

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

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,3 +38,19 @@ 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",
"SourceNamespace",
"SearchResult",
"AppContext",
"LRUCache",
"SearchResult",
"SourceNamespace",
]

View File

@@ -1,4 +1,3 @@
# ruff: noqa: INP001
"""MCP server for semantic search over Vertex AI Vector Search."""
import time
@@ -9,7 +8,11 @@ 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",
@@ -44,7 +47,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:
@@ -61,9 +64,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:
@@ -74,17 +94,13 @@ async def knowledge_search(
source=source,
)
t_search = time.perf_counter()
except Exception as e:
except Exception as e: # noqa: BLE001
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: {str(e)}"
return f"Error performing vector search: {e!s}"
# Apply similarity filtering
filtered_results = filter_search_results(search_results)
@@ -98,32 +114,33 @@ async def knowledge_search(
"total_ms": f"{round((t_search - t0) * 1000, 1)}ms",
"source_filter": source.value if source is not None else None,
"results_count": len(filtered_results),
"chunks": [s["id"] for s in 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:
return formatted
except Exception as e: # noqa: BLE001
# 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: {str(e)}"
return f"Unexpected error during search: {e!s}"
def main() -> None:

View File

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

View File

@@ -1,4 +1,3 @@
# ruff: noqa: INP001
"""Google Cloud Storage client with caching."""
import asyncio
@@ -8,8 +7,9 @@ from typing import BinaryIO
import aiohttp
from gcloud.aio.storage import Storage
from ..logging import log_structured_entry
from ..utils.cache import LRUCache
from knowledge_search_mcp.logging import log_structured_entry
from knowledge_search_mcp.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,15 +87,18 @@ 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} (attempt {attempt + 1}/{max_retries})",
(
f"Timeout downloading gs://{self.bucket_name}/{file_name} "
f"(attempt {attempt + 1}/{max_retries})"
),
"WARNING",
{"error": str(exc)}
{"error": str(exc)},
)
except aiohttp.ClientResponseError as exc:
last_exception = exc
@@ -103,16 +106,19 @@ class GoogleCloudFileStorage(BaseGoogleCloudClient):
exc.status == HTTP_TOO_MANY_REQUESTS
or exc.status >= HTTP_SERVER_ERROR
):
log_structured_entry(
f"HTTP {exc.status} downloading gs://{self.bucket_name}/{file_name} (attempt {attempt + 1}/{max_retries})",
log_structured_entry(
(
f"HTTP {exc.status} downloading gs://{self.bucket_name}/"
f"{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:
@@ -123,7 +129,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)
@@ -138,7 +144,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,4 +1,3 @@
# ruff: noqa: INP001
"""Google Cloud Vector Search client."""
import asyncio
@@ -6,8 +5,9 @@ from collections.abc import Sequence
from gcloud.aio.auth import Token
from ..logging import log_structured_entry
from ..models import SearchResult, SourceNamespace
from knowledge_search_mcp.logging import log_structured_entry
from knowledge_search_mcp.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)
raise RuntimeError(msg) # noqa: TRY301
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,12 +206,9 @@ 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
return results # noqa: TRY300
except Exception as e:
log_structured_entry(
@@ -220,7 +217,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,7 +1,14 @@
"""Configuration management for the MCP server."""
import argparse
import os
import sys
import argparse
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, YamlConfigSettingsSource
from pydantic_settings import (
BaseSettings,
PydanticBaseSettingsSource,
YamlConfigSettingsSource,
)
def _parse_args() -> argparse.Namespace:
@@ -14,7 +21,7 @@ def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
return argparse.Namespace(
transport="stdio",
host="0.0.0.0",
host="0.0.0.0", # noqa: S104
port=8080,
config=os.environ.get("CONFIG_FILE", "config.yaml"),
)
@@ -25,7 +32,7 @@ def _parse_args() -> argparse.Namespace:
choices=["stdio", "sse", "streamable-http"],
default="stdio",
)
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--host", default="0.0.0.0") # noqa: S104
parser.add_argument("--port", type=int, default=8080)
parser.add_argument(
"--config",
@@ -36,6 +43,7 @@ def _parse_args() -> argparse.Namespace:
_args = _parse_args()
class Settings(BaseSettings):
"""Server configuration populated from env vars and a YAML config file."""
@@ -52,6 +60,14 @@ 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(
@@ -62,6 +78,7 @@ 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,
@@ -77,7 +94,7 @@ _cfg: Settings | None = None
def get_config() -> Settings:
"""Get or create the singleton Settings instance."""
global _cfg
global _cfg # noqa: PLW0603
if _cfg is None:
_cfg = Settings.model_validate({})
return _cfg
@@ -87,8 +104,8 @@ def get_config() -> Settings:
class _ConfigProxy:
"""Proxy object that lazily loads config on attribute access."""
def __getattr__(self, name: str):
def __getattr__(self, name: str) -> object:
return getattr(get_config(), name)
cfg = _ConfigProxy() # type: ignore[assignment]
cfg = _ConfigProxy()

View File

@@ -1,23 +1,22 @@
"""
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 Optional, Dict, Literal
from typing import 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
global _eval_log # noqa: PLW0603
if _eval_log is not None:
return _eval_log
@@ -27,30 +26,42 @@ def _get_logger() -> logging.Logger:
_eval_log = logger
return logger
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:
# Fallback to console if Cloud Logging is unavailable (local dev)
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
# 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)
logger.warning("Cloud Logging setup failed; using console. Error: %s", e)
_eval_log = logger
return logger
def log_structured_entry(message: str, severity: Literal["INFO", "WARNING", "ERROR"], custom_log: Optional[Dict] = None) -> None:
"""
Emit a JSON-structured log row.
def log_structured_entry(
message: str,
severity: Literal["INFO", "WARNING", "ERROR"],
custom_log: dict | None = 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,8 +1,7 @@
# ruff: noqa: INP001
"""Domain models for knowledge search MCP server."""
from dataclasses import dataclass
from enum import Enum
from enum import StrEnum
from typing import TYPE_CHECKING, TypedDict
if TYPE_CHECKING:
@@ -10,9 +9,10 @@ if TYPE_CHECKING:
from .clients.vector_search import GoogleCloudVectorSearch
from .config import Settings
from .services.semantic_cache import KnowledgeSemanticCache
class SourceNamespace(str, Enum):
class SourceNamespace(StrEnum):
"""Allowed values for the 'source' namespace filter."""
EDUCACION_FINANCIERA = "Educacion Financiera"
@@ -35,3 +35,4 @@ class AppContext:
vector_search: "GoogleCloudVectorSearch"
genai_client: "genai.Client"
settings: "Settings"
semantic_cache: "KnowledgeSemanticCache | None" = None

View File

@@ -1,4 +1,3 @@
# ruff: noqa: INP001
"""MCP server lifecycle management."""
from collections.abc import AsyncIterator
@@ -8,12 +7,13 @@ from google import genai
from mcp.server.fastmcp import FastMCP
from .clients.vector_search import GoogleCloudVectorSearch
from .config import Settings, cfg
from .config import get_config
from .logging import log_structured_entry
from .models import AppContext
from .services.semantic_cache import KnowledgeSemanticCache
from .services.validation import (
validate_genai_access,
validate_gcs_access,
validate_genai_access,
validate_vector_search_access,
)
@@ -21,15 +21,18 @@ 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": cfg.project_id,
"location": cfg.location,
"bucket": cfg.bucket,
"index_name": cfg.index_name,
}
"project_id": config_for_init.project_id,
"location": config_for_init.location,
"bucket": config_for_init.bucket,
"index_name": config_for_init.index_name,
},
)
vs: GoogleCloudVectorSearch | None = None
@@ -37,10 +40,10 @@ async def lifespan(_server: FastMCP) -> AsyncIterator[AppContext]:
# Initialize vector search client
log_structured_entry("Creating GoogleCloudVectorSearch client", "INFO")
vs = GoogleCloudVectorSearch(
project_id=cfg.project_id,
location=cfg.location,
bucket=cfg.bucket,
index_name=cfg.index_name,
project_id=config_for_init.project_id,
location=config_for_init.location,
bucket=config_for_init.bucket,
index_name=config_for_init.index_name,
)
# Configure endpoint
@@ -48,25 +51,28 @@ async def lifespan(_server: FastMCP) -> AsyncIterator[AppContext]:
"Configuring index endpoint",
"INFO",
{
"endpoint_name": cfg.endpoint_name,
"endpoint_domain": cfg.endpoint_domain,
}
"endpoint_name": config_for_init.endpoint_name,
"endpoint_domain": config_for_init.endpoint_domain,
},
)
vs.configure_index_endpoint(
name=cfg.endpoint_name,
public_domain=cfg.endpoint_domain,
name=config_for_init.endpoint_name,
public_domain=config_for_init.endpoint_domain,
)
# Initialize GenAI client
log_structured_entry(
"Creating GenAI client",
"INFO",
{"project_id": cfg.project_id, "location": cfg.location}
{
"project_id": config_for_init.project_id,
"location": config_for_init.location,
},
)
genai_client = genai.Client(
vertexai=True,
project=cfg.project_id,
location=cfg.location,
project=config_for_init.project_id,
location=config_for_init.location,
)
# Validate credentials and configuration by testing actual resources
@@ -76,32 +82,65 @@ async def lifespan(_server: FastMCP) -> AsyncIterator[AppContext]:
validation_errors = []
# Run all validations
genai_error = await validate_genai_access(genai_client, cfg)
config = get_config()
genai_error = await validate_genai_access(genai_client, config)
if genai_error:
validation_errors.append(genai_error)
gcs_error = await validate_gcs_access(vs, cfg)
gcs_error = await validate_gcs_access(vs, config)
if gcs_error:
validation_errors.append(gcs_error)
vs_error = await validate_vector_search_access(vs, cfg)
vs_error = await validate_vector_search_access(vs, config)
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")
log_structured_entry(
"All validations passed - MCP server initialization complete", "INFO"
)
# Initialize semantic cache if Redis is configured
semantic_cache = None
if config_for_init.redis_url:
try:
semantic_cache = KnowledgeSemanticCache(
redis_url=config_for_init.redis_url,
name=config_for_init.cache_name,
vector_dims=config_for_init.cache_vector_dims,
distance_threshold=config_for_init.cache_distance_threshold,
ttl=config_for_init.cache_ttl,
)
log_structured_entry(
"Semantic cache initialized",
"INFO",
{"redis_url": config_for_init.redis_url, "cache_name": config_for_init.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=cfg,
settings=config,
semantic_cache=semantic_cache,
)
except Exception as e:
@@ -111,7 +150,7 @@ async def lifespan(_server: FastMCP) -> AsyncIterator[AppContext]:
{
"error": str(e),
"error_type": type(e).__name__,
}
},
)
raise
finally:
@@ -121,9 +160,9 @@ async def lifespan(_server: FastMCP) -> AsyncIterator[AppContext]:
try:
await vs.close()
log_structured_entry("Closed aiohttp sessions", "INFO")
except Exception as e:
except Exception as e: # noqa: BLE001
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,13 +1,21 @@
"""Service modules for business logic."""
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
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,
)
__all__ = [
"filter_search_results",
"format_search_results",
"generate_query_embedding",
"validate_genai_access",
"validate_gcs_access",
"validate_genai_access",
"validate_vector_search_access",
]

View File

@@ -1,11 +1,10 @@
# ruff: noqa: INP001
"""Search helper functions."""
from google import genai
from google.genai import types as genai_types
from ..logging import log_structured_entry
from ..models import SearchResult
from knowledge_search_mcp.logging import log_structured_entry
from knowledge_search_mcp.models import SearchResult
async def generate_query_embedding(
@@ -17,6 +16,7 @@ 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,9 +30,11 @@ 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)
except Exception as e:
return (embedding, None) # noqa: TRY300
except Exception as e: # noqa: BLE001
error_type = type(e).__name__
error_msg = str(e)
@@ -41,24 +43,15 @@ 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]
}
)
return ([], f"Error generating embedding: {error_msg}")
log_structured_entry(
"Failed to generate query embedding",
"ERROR",
{"error": error_msg, "error_type": error_type, "query": query[:100]},
)
return ([], f"Error generating embedding: {error_msg}")
def filter_search_results(
@@ -75,6 +68,7 @@ def filter_search_results(
Returns:
Filtered list of search results.
"""
if not results:
return []
@@ -82,14 +76,10 @@ def filter_search_results(
max_sim = max(r["distance"] for r in results)
cutoff = max_sim * top_percent
filtered = [
s
for s in results
if s["distance"] > cutoff and s["distance"] > min_similarity
return [
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.
@@ -99,6 +89,7 @@ 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,20 +1,26 @@
# 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 ..clients.vector_search import GoogleCloudVectorSearch
from ..config import Settings
from ..logging import log_structured_entry
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
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:
@@ -30,20 +36,26 @@ async def validate_genai_access(genai_client: genai.Client, cfg: Settings) -> st
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
except Exception as e:
msg = "Embedding validation returned empty response"
log_structured_entry(msg, "WARNING")
return msg # noqa: TRY300
except Exception as e: # noqa: BLE001
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: {str(e)}"
return f"GenAI: {e!s}"
async def validate_gcs_access(vs: GoogleCloudVectorSearch, cfg: Settings) -> str | None:
@@ -51,14 +63,11 @@ 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()
session = vs.storage._get_aio_session() # noqa: SLF001
token_obj = Token(
session=session,
scopes=["https://www.googleapis.com/auth/cloud-platform"],
@@ -70,102 +79,136 @@ 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 == 403:
if response.status == HTTP_FORBIDDEN:
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
elif response.status == 404:
if response.status == HTTP_NOT_FOUND:
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
elif not response.ok:
if 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}
)
return None
except Exception as e:
log_structured_entry(
"GCS bucket validation successful", "INFO", {"bucket": cfg.bucket}
)
return None
except Exception as e: # noqa: BLE001
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: {str(e)}"
return f"GCS: {e!s}"
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()
session = vs._get_aio_session()
headers = await vs._async_get_auth_headers() # noqa: SLF001
session = vs._get_aio_session() # noqa: SLF001
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 == 403:
msg = f"Access denied to endpoint '{cfg.endpoint_name}'. Check permissions."
if response.status == HTTP_FORBIDDEN:
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
elif response.status == 404:
msg = f"Endpoint '{cfg.endpoint_name}' not found. Check endpoint name and project."
if response.status == HTTP_NOT_FOUND:
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
elif not response.ok:
if not response.ok:
body = await response.text()
msg = f"Failed to access endpoint '{cfg.endpoint_name}': {response.status}"
msg = (
f"Failed to access endpoint '{cfg.endpoint_name}': "
f"{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}
)
return None
except Exception as e:
log_structured_entry(
"Vector search endpoint validation successful",
"INFO",
{"endpoint": cfg.endpoint_name},
)
return None
except Exception as e: # noqa: BLE001
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: {str(e)}"
return f"Vector Search: {e!s}"

View File

@@ -1,4 +1,3 @@
# 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"