Compare commits
5 Commits
d69c4e4f4a
...
push-omyxs
| Author | SHA1 | Date | |
|---|---|---|---|
| 132ea1c04f | |||
| 0cdf9cd44e | |||
| d39b8a6ea7 | |||
| 86ed34887b | |||
| 694b060fa4 |
43
.gitea/workflows/ci.yml
Normal file
43
.gitea/workflows/ci.yml
Normal 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
3
CLAUDE.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Use `uv` for project management
|
||||
Linter: `uv run ruff check`
|
||||
Type-checking: `uv run ty check`
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# ruff: noqa: INP001
|
||||
"""Base client with shared aiohttp session management."""
|
||||
|
||||
import aiohttp
|
||||
|
||||
@@ -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
|
||||
@@ -104,15 +107,18 @@ class GoogleCloudFileStorage(BaseGoogleCloudClient):
|
||||
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})",
|
||||
(
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
_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 {}}},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__},
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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,22 +43,13 @@ 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}")
|
||||
|
||||
@@ -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."
|
||||
|
||||
97
src/knowledge_search_mcp/services/semantic_cache.py
Normal file
97
src/knowledge_search_mcp/services/semantic_cache.py
Normal 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__},
|
||||
)
|
||||
@@ -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:
|
||||
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}
|
||||
"GCS bucket validation successful", "INFO", {"bucket": cfg.bucket}
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
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}
|
||||
{"endpoint": cfg.endpoint_name},
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
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}"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# ruff: noqa: INP001
|
||||
"""LRU cache implementation."""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
@@ -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
|
||||
|
||||
272
tests/test_semantic_cache.py
Normal file
272
tests/test_semantic_cache.py
Normal 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
191
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user