diff --git a/engine/config/.env.example b/engine/config/.env.example index bfcbe3f575..5431066640 100644 --- a/engine/config/.env.example +++ b/engine/config/.env.example @@ -8,6 +8,24 @@ STIRLING_FAST_MODEL=anthropic:claude-haiku-4-5 STIRLING_SMART_MODEL_MAX_TOKENS=8192 STIRLING_FAST_MODEL_MAX_TOKENS=2048 +# RAG Configuration — retrieval-augmented generation is always on. +# Embedding provider credentials are handled natively (e.g. VOYAGE_API_KEY for VoyageAI). +STIRLING_RAG_EMBEDDING_MODEL=voyageai:voyage-4 + +# Vector store backend: "sqlite" (embedded) or "pgvector" (external Postgres). +STIRLING_RAG_BACKEND=sqlite + +# Path to the sqlite-vec database file (used when backend=sqlite). +STIRLING_RAG_STORE_PATH=data/rag.db + +# Postgres DSN for pgvector (used when backend=pgvector). Leave empty when backend=sqlite. +# Example: postgresql://user:password@host:5432/dbname +STIRLING_RAG_PGVECTOR_DSN= + +STIRLING_RAG_CHUNK_SIZE=512 +STIRLING_RAG_CHUNK_OVERLAP=64 +STIRLING_RAG_TOP_K=5 + # PostHog analytics. Set STIRLING_POSTHOG_ENABLED=true and provide an API key to enable. STIRLING_POSTHOG_ENABLED=false STIRLING_POSTHOG_API_KEY=phc_VOdeYnlevc2T63m3myFGjeBlRcIusRgmhfx6XL5a1iz diff --git a/engine/pyproject.toml b/engine/pyproject.toml index ba10d05141..f1a10fe108 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -5,10 +5,14 @@ description = "AI Document Engine" requires-python = ">=3.13" dependencies = [ "fastapi>=0.116.0", + "pgvector>=0.3.6", + "psycopg[binary]>=3.2", "pydantic>=2.0.0", "pydantic-ai>=1.67.0", + "pydantic-ai-slim[voyageai]>=1.67.0", "pydantic-settings>=2.0.0", "python-dotenv>=1.2.1", + "sqlite-vec>=0.1.6", "uvicorn>=0.35.0", "opentelemetry-sdk>=1.39.0", "posthog>=3.0.0", @@ -16,6 +20,7 @@ dependencies = [ [dependency-groups] dev = [ + "anyio>=4.0.0", "datamodel-code-generator[ruff]>=0.26.0", "pytest>=8.0.0", "pyright>=1.1.408", diff --git a/engine/src/stirling/agents/pdf_questions.py b/engine/src/stirling/agents/pdf_questions.py index c8a63bd70b..60f7ea94c2 100644 --- a/engine/src/stirling/agents/pdf_questions.py +++ b/engine/src/stirling/agents/pdf_questions.py @@ -22,6 +22,7 @@ class PdfQuestionAgent: def __init__(self, runtime: AppRuntime) -> None: self.runtime = runtime + rag = runtime.rag_capability self.agent = Agent( model=runtime.smart_model, output_type=NativeOutput( @@ -36,6 +37,8 @@ class PdfQuestionAgent: "If the answer is not supported by the provided text, return not_found. " "When answering, include a short list of evidence snippets with their page numbers." ), + instructions=rag.instructions, + toolsets=[rag.toolset], model_settings=runtime.smart_model_settings, ) diff --git a/engine/src/stirling/api/app.py b/engine/src/stirling/api/app.py index 2afef6208c..bbaec84665 100644 --- a/engine/src/stirling/api/app.py +++ b/engine/src/stirling/api/app.py @@ -17,6 +17,7 @@ from stirling.api.routes import ( orchestrator_router, pdf_edit_router, pdf_question_router, + rag_router, ) from stirling.config import AppSettings, load_settings from stirling.contracts import HealthResponse @@ -58,6 +59,7 @@ app.include_router(pdf_edit_router) app.include_router(pdf_question_router) app.include_router(agent_draft_router) app.include_router(execution_router) +app.include_router(rag_router) app.include_router(ledger_router) diff --git a/engine/src/stirling/api/dependencies.py b/engine/src/stirling/api/dependencies.py index 7101f4274e..70e11a54a3 100644 --- a/engine/src/stirling/api/dependencies.py +++ b/engine/src/stirling/api/dependencies.py @@ -4,6 +4,7 @@ from fastapi import Request from stirling.agents import ExecutionPlanningAgent, OrchestratorAgent, PdfEditAgent, PdfQuestionAgent, UserSpecAgent from stirling.agents.ledger import MathAuditorAgent +from stirling.rag import RagService from stirling.services import AppRuntime @@ -31,5 +32,13 @@ def get_execution_planning_agent(request: Request) -> ExecutionPlanningAgent: return request.app.state.execution_planning_agent +def get_rag_service(request: Request) -> RagService: + return request.app.state.runtime.rag_service + + +def get_rag_embedding_model(request: Request) -> str: + return request.app.state.runtime.settings.rag_embedding_model + + def get_math_auditor_agent(request: Request) -> MathAuditorAgent: return request.app.state.math_auditor_agent diff --git a/engine/src/stirling/api/routes/__init__.py b/engine/src/stirling/api/routes/__init__.py index a4a0acdb94..d1e7db0b85 100644 --- a/engine/src/stirling/api/routes/__init__.py +++ b/engine/src/stirling/api/routes/__init__.py @@ -4,6 +4,7 @@ from .ledger import router as ledger_router from .orchestrator import router as orchestrator_router from .pdf_edit import router as pdf_edit_router from .pdf_questions import router as pdf_question_router +from .rag import router as rag_router __all__ = [ "agent_draft_router", @@ -12,4 +13,5 @@ __all__ = [ "orchestrator_router", "pdf_edit_router", "pdf_question_router", + "rag_router", ] diff --git a/engine/src/stirling/api/routes/rag.py b/engine/src/stirling/api/routes/rag.py new file mode 100644 index 0000000000..2eb9acfcc2 --- /dev/null +++ b/engine/src/stirling/api/routes/rag.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import APIRouter, Depends + +from stirling.api.dependencies import get_rag_embedding_model, get_rag_service +from stirling.contracts import ( + RagCollectionsResponse, + RagDeleteCollectionResponse, + RagIndexRequest, + RagIndexResponse, + RagSearchRequest, + RagSearchResponse, + RagSearchResultItem, + RagStatusResponse, +) +from stirling.rag import RagService + +router = APIRouter(prefix="/api/v1/rag", tags=["rag"]) + + +@router.get("/status", response_model=RagStatusResponse) +async def rag_status( + rag: Annotated[RagService, Depends(get_rag_service)], + embedding_model: Annotated[str, Depends(get_rag_embedding_model)], +) -> RagStatusResponse: + collections = await rag.list_collections() + return RagStatusResponse(embedding_model=embedding_model, collections=collections) + + +@router.post("/index", response_model=RagIndexResponse) +async def rag_index( + request: RagIndexRequest, + rag: Annotated[RagService, Depends(get_rag_service)], +) -> RagIndexResponse: + count = await rag.index_text( + collection=request.collection, + text=request.text, + source=request.source, + metadata=request.metadata, + ) + return RagIndexResponse(collection=request.collection, chunks_indexed=count) + + +@router.post("/search", response_model=RagSearchResponse) +async def rag_search( + request: RagSearchRequest, + rag: Annotated[RagService, Depends(get_rag_service)], +) -> RagSearchResponse: + results = await rag.search(query=request.query, collection=request.collection, top_k=request.top_k) + items = [ + RagSearchResultItem( + text=r.document.text, + source=r.document.metadata.get("source", ""), + chunk_id=r.document.metadata.get("chunk_index", ""), + score=r.score, + ) + for r in results + ] + return RagSearchResponse(query=request.query, results=items) + + +@router.get("/collections", response_model=RagCollectionsResponse) +async def rag_collections( + rag: Annotated[RagService, Depends(get_rag_service)], +) -> RagCollectionsResponse: + collections = await rag.list_collections() + return RagCollectionsResponse(collections=collections) + + +@router.delete("/collections/{name}", response_model=RagDeleteCollectionResponse) +async def rag_delete_collection( + name: str, + rag: Annotated[RagService, Depends(get_rag_service)], +) -> RagDeleteCollectionResponse: + await rag.delete_collection(name) + return RagDeleteCollectionResponse(status="deleted", collection=name) diff --git a/engine/src/stirling/config/__init__.py b/engine/src/stirling/config/__init__.py index cb0326172c..c5044a2148 100644 --- a/engine/src/stirling/config/__init__.py +++ b/engine/src/stirling/config/__init__.py @@ -1,8 +1,10 @@ """Configuration models and loaders for the Stirling AI service.""" -from .settings import AppSettings, load_settings +from .settings import ENGINE_ROOT, AppSettings, RagBackend, load_settings __all__ = [ + "ENGINE_ROOT", "AppSettings", + "RagBackend", "load_settings", ] diff --git a/engine/src/stirling/config/settings.py b/engine/src/stirling/config/settings.py index 0844c3ff42..2452b8ec8c 100644 --- a/engine/src/stirling/config/settings.py +++ b/engine/src/stirling/config/settings.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging import logging.handlers +from enum import StrEnum from functools import lru_cache from pathlib import Path @@ -13,6 +14,11 @@ ENGINE_ROOT = Path(__file__).resolve().parents[3] ENV_FILE = ENGINE_ROOT / ".env" +class RagBackend(StrEnum): + SQLITE = "sqlite" + PGVECTOR = "pgvector" + + class AppSettings(BaseSettings): model_config = SettingsConfigDict(env_file=ENV_FILE, extra="ignore", populate_by_name=True) @@ -21,6 +27,15 @@ class AppSettings(BaseSettings): smart_model_max_tokens: int = Field(validation_alias="STIRLING_SMART_MODEL_MAX_TOKENS") fast_model_max_tokens: int = Field(validation_alias="STIRLING_FAST_MODEL_MAX_TOKENS") + # RAG settings — always on; the backend picks between embedded sqlite-vec and external pgvector. + rag_backend: RagBackend = Field(validation_alias="STIRLING_RAG_BACKEND") + rag_embedding_model: str = Field(validation_alias="STIRLING_RAG_EMBEDDING_MODEL") + rag_store_path: Path = Field(validation_alias="STIRLING_RAG_STORE_PATH") + rag_pgvector_dsn: str = Field(validation_alias="STIRLING_RAG_PGVECTOR_DSN") + rag_chunk_size: int = Field(validation_alias="STIRLING_RAG_CHUNK_SIZE") + rag_chunk_overlap: int = Field(validation_alias="STIRLING_RAG_CHUNK_OVERLAP") + rag_default_top_k: int = Field(validation_alias="STIRLING_RAG_TOP_K") + log_level: str = Field(default="INFO", validation_alias="STIRLING_LOG_LEVEL") log_file: str = Field(default="", validation_alias="STIRLING_LOG_FILE") diff --git a/engine/src/stirling/contracts/__init__.py b/engine/src/stirling/contracts/__init__.py index 3f0b1fb87e..21633c075e 100644 --- a/engine/src/stirling/contracts/__init__.py +++ b/engine/src/stirling/contracts/__init__.py @@ -62,8 +62,20 @@ from .pdf_questions import ( PdfQuestionRequest, PdfQuestionResponse, ) +from .rag import ( + MAX_INDEX_TEXT_LENGTH, + RagCollectionsResponse, + RagDeleteCollectionResponse, + RagIndexRequest, + RagIndexResponse, + RagSearchRequest, + RagSearchResponse, + RagSearchResultItem, + RagStatusResponse, +) __all__ = [ + "MAX_INDEX_TEXT_LENGTH", "AgentDraft", "AgentDraftRequest", "AgentDraftResponse", @@ -106,6 +118,14 @@ __all__ = [ "PdfQuestionRequest", "PdfQuestionResponse", "PdfTextSelection", + "RagCollectionsResponse", + "RagDeleteCollectionResponse", + "RagIndexRequest", + "RagIndexResponse", + "RagSearchRequest", + "RagSearchResponse", + "RagSearchResultItem", + "RagStatusResponse", "Requisition", "Severity", "StepKind", diff --git a/engine/src/stirling/contracts/rag.py b/engine/src/stirling/contracts/rag.py new file mode 100644 index 0000000000..c4cea35af1 --- /dev/null +++ b/engine/src/stirling/contracts/rag.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from pydantic import Field + +from stirling.models import ApiModel + +MAX_INDEX_TEXT_LENGTH = 1_000_000 # 1MB text limit per index request + + +class RagStatusResponse(ApiModel): + embedding_model: str + collections: list[str] + + +class RagIndexRequest(ApiModel): + collection: str = Field(min_length=1) + text: str = Field(max_length=MAX_INDEX_TEXT_LENGTH) + source: str = "" + metadata: dict[str, str] = Field(default_factory=dict) + + +class RagIndexResponse(ApiModel): + collection: str + chunks_indexed: int + + +class RagSearchRequest(ApiModel): + query: str + collection: str | None = Field(default=None, min_length=1) + top_k: int = 5 + + +class RagSearchResultItem(ApiModel): + text: str + source: str + chunk_id: str + score: float + + +class RagSearchResponse(ApiModel): + query: str + results: list[RagSearchResultItem] + + +class RagCollectionsResponse(ApiModel): + collections: list[str] + + +class RagDeleteCollectionResponse(ApiModel): + status: str + collection: str diff --git a/engine/src/stirling/rag/README.md b/engine/src/stirling/rag/README.md new file mode 100644 index 0000000000..a4bdb6c452 --- /dev/null +++ b/engine/src/stirling/rag/README.md @@ -0,0 +1,81 @@ +# RAG Integration Guide + +## Adding RAG to an Agent + +```python +from pydantic_ai import Agent + +from stirling.services import AppRuntime + +class MyAgent: + def __init__(self, runtime: AppRuntime) -> None: + rag = runtime.rag_capability + self.agent = Agent( + model=runtime.smart_model, + system_prompt="Your prompt here...", + instructions=rag.instructions, + toolsets=[rag.toolset], + ) +``` + +That's it. The agent gets a `search_knowledge` tool it can call autonomously. + +## Scoping to Specific Collections + +Collections are named buckets of indexed documents — think folders. By default an agent searches everything in the store. Pass `collections=` to restrict it to only the docs indexed under those names. + +```python +from stirling.rag import RagCapability + +# Only searches docs indexed under "company-docs" — ignores everything else +scoped = RagCapability(runtime.rag_service, collections=["company-docs"], top_k=3) + +# Searches multiple collections +multi = RagCapability(runtime.rag_service, collections=["company-docs", "product-specs"]) + +# No collections arg = searches all collections in the store +everything = RagCapability(runtime.rag_service) +``` + +## Config (.env) + +``` +STIRLING_RAG_BACKEND=sqlite # or "pgvector" +STIRLING_RAG_EMBEDDING_MODEL=voyageai:voyage-4 +STIRLING_RAG_STORE_PATH=data/rag.db # used when backend=sqlite +STIRLING_RAG_PGVECTOR_DSN= # used when backend=pgvector +STIRLING_RAG_CHUNK_SIZE=512 +STIRLING_RAG_CHUNK_OVERLAP=64 +STIRLING_RAG_TOP_K=5 +VOYAGE_API_KEY=your-key +``` + +## Backends + +**`sqlite`** — Embedded sqlite-vec. Single `.db` file, zero ops. Ideal for dev and self-hosted deployments. + +**`pgvector`** — External PostgreSQL with the `vector` extension. Point `STIRLING_RAG_PGVECTOR_DSN` at your Postgres instance. + +Both backends implement the same `VectorStore` interface, so agents and the RAG service work identically regardless of which you pick. + +For a self-hosted embedding server (e.g. Ollama, TEI, vLLM) set the model string accordingly and point at the server via its native env var: + +``` +# Ollama running on another machine +STIRLING_RAG_EMBEDDING_MODEL=ollama:nomic-embed-text +OLLAMA_HOST=http://192.168.1.50:11434 + +# Any OpenAI-compatible embedding server +STIRLING_RAG_EMBEDDING_MODEL=openai:my-model +OPENAI_BASE_URL=http://192.168.1.50:8080/v1 +``` + +## API Endpoints + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| GET | `/api/v1/rag/status` | Report embedding model and existing collections | +| POST | `/api/v1/rag/index` | Index text into a collection | +| POST | `/api/v1/rag/search` | Search a collection | +| GET | `/api/v1/rag/collections` | List collections | +| DELETE | `/api/v1/rag/collections/{name}` | Delete a collection | diff --git a/engine/src/stirling/rag/__init__.py b/engine/src/stirling/rag/__init__.py new file mode 100644 index 0000000000..3abc1f8180 --- /dev/null +++ b/engine/src/stirling/rag/__init__.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from stirling.rag.capability import RagCapability +from stirling.rag.embedder import EmbeddingService +from stirling.rag.pgvector_store import PgVectorStore +from stirling.rag.service import RagService +from stirling.rag.sqlite_vec_store import SqliteVecStore +from stirling.rag.store import Document, SearchResult, VectorStore + +__all__ = [ + "Document", + "EmbeddingService", + "PgVectorStore", + "RagCapability", + "RagService", + "SearchResult", + "SqliteVecStore", + "VectorStore", +] diff --git a/engine/src/stirling/rag/capability.py b/engine/src/stirling/rag/capability.py new file mode 100644 index 0000000000..9950cf30ac --- /dev/null +++ b/engine/src/stirling/rag/capability.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Callable + +from pydantic_ai import FunctionToolset +from pydantic_ai.toolsets import AbstractToolset + +from stirling.rag.service import RagService + + +class RagCapability: + """Bundles RAG instructions and the ``search_knowledge`` toolset for agent injection. + + Agents consume this as:: + + rag = runtime.rag_capability + Agent( + ..., + instructions=rag.instructions, + toolsets=[rag.toolset], + ) + + When no collections are pinned, the instructions are generated dynamically at + run time so the agent sees the current list of collections in the store. + """ + + def __init__( + self, + rag_service: RagService, + collections: list[str] | None = None, + top_k: int = 5, + ) -> None: + self._rag_service = rag_service + self._collections = collections + self._top_k = top_k + toolset: FunctionToolset[None] = FunctionToolset() + toolset.add_function(self._search_knowledge, name="search_knowledge") + self._toolset = toolset + + @property + def instructions(self) -> str | Callable[[], Awaitable[str]]: + if self._collections: + return self._static_instructions_text(self._collections) + return self._dynamic_instructions + + @property + def toolset(self) -> AbstractToolset[None]: + return self._toolset + + @staticmethod + def _static_instructions_text(collections: list[str]) -> str: + collection_desc = f"collections: {', '.join(collections)}" + return ( + "You have access to a knowledge base search tool called 'search_knowledge'. " + f"It searches {collection_desc} for relevant information. " + "Use it when the provided context is insufficient to answer the user's question, " + "or when you think additional background information would improve your answer. " + "You do not have to use it if the answer is already clear from the provided text." + ) + + async def _dynamic_instructions(self) -> str: + collections = await self._rag_service.list_collections() + if collections: + names = ", ".join(collections) + collection_desc = f"the following knowledge base collections: {names}" + else: + collection_desc = "the knowledge base (currently empty — no collections indexed yet)" + return ( + "You have access to a knowledge base search tool called 'search_knowledge'. " + f"It searches {collection_desc} for relevant information. " + "Use it when the provided context is insufficient to answer the user's question, " + "or when you think additional background information would improve your answer. " + "You do not have to use it if the answer is already clear from the provided text." + ) + + async def _search_knowledge(self, query: str, max_results: int | None = None) -> str: + """Search the knowledge base for information relevant to the query. + + Args: + query: The search query describing what information you need. + max_results: Maximum number of results to return. + + Returns: + Formatted text with the most relevant knowledge base excerpts. + """ + k = max_results if max_results is not None else self._top_k + if self._collections: + all_results = [] + for col in self._collections: + col_results = await self._rag_service.search(query, collection=col, top_k=k) + all_results.extend(col_results) + all_results.sort(key=lambda r: r.score, reverse=True) + results = all_results[:k] + else: + results = await self._rag_service.search(query, top_k=k) + + if not results: + return "No relevant results found in the knowledge base." + + sections = [] + for i, result in enumerate(results, 1): + source = result.document.metadata.get("source", "unknown") + chunk_idx = result.document.metadata.get("chunk_index", "?") + score = f"{result.score:.3f}" + sections.append( + f"[Result {i} | source: {source}, chunk: {chunk_idx}, relevance: {score}]\n{result.document.text}" + ) + return "\n\n---\n\n".join(sections) diff --git a/engine/src/stirling/rag/chunker.py b/engine/src/stirling/rag/chunker.py new file mode 100644 index 0000000000..689365f6cb --- /dev/null +++ b/engine/src/stirling/rag/chunker.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +import re + +# TODO: replace with pydantic-ai's built-in chunking once +# https://github.com/pydantic/pydantic-ai/issues/3962 lands. + + +def chunk_text(text: str, chunk_size: int = 512, overlap: int = 64) -> list[str]: + """Split text into chunks of approximately chunk_size characters with overlap. + + Splits on paragraph then sentence boundaries to avoid cutting mid-thought. + Returns an empty list for empty/whitespace-only input. + """ + text = text.strip() + if not text: + return [] + + paragraphs = _split_paragraphs(text) + chunks: list[str] = [] + current: list[str] = [] + current_len = 0 + + for para in paragraphs: + para_len = len(para) + + if current_len + para_len <= chunk_size: + current.append(para) + current_len += para_len + continue + + # If the current buffer has content, flush it + if current: + chunks.append("\n\n".join(current)) + + # If this paragraph alone exceeds chunk_size, split it by sentences + if para_len > chunk_size: + sentence_chunks = _split_long_paragraph(para, chunk_size, overlap) + chunks.extend(sentence_chunks) + current = [] + current_len = 0 + else: + # Start new chunk with overlap from previous chunk + overlap_text = _get_overlap(chunks, overlap) if chunks else "" + if overlap_text: + current = [overlap_text, para] + current_len = len(overlap_text) + para_len + else: + current = [para] + current_len = para_len + + if current: + chunks.append("\n\n".join(current)) + + return [c.strip() for c in chunks if c.strip()] + + +def _split_paragraphs(text: str) -> list[str]: + """Split text into paragraphs on double newlines.""" + return [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()] + + +def _split_sentences(text: str) -> list[str]: + """Split text into sentences, keeping the delimiter attached.""" + parts = re.split(r"(?<=[.!?])\s+", text) + return [s.strip() for s in parts if s.strip()] + + +def _split_long_paragraph(paragraph: str, chunk_size: int, overlap: int) -> list[str]: + """Split a single long paragraph into sentence-boundary chunks.""" + sentences = _split_sentences(paragraph) + chunks: list[str] = [] + current: list[str] = [] + current_len = 0 + + for sentence in sentences: + sent_len = len(sentence) + + if current_len + sent_len <= chunk_size: + current.append(sentence) + current_len += sent_len + 1 # +1 for space + continue + + if current: + chunks.append(" ".join(current)) + + # If a single sentence exceeds chunk_size, force-split it + if sent_len > chunk_size: + for i in range(0, sent_len, chunk_size - overlap): + chunks.append(sentence[i : i + chunk_size]) + current = [] + current_len = 0 + else: + overlap_text = _get_overlap(chunks, overlap) if chunks else "" + if overlap_text: + current = [overlap_text, sentence] + current_len = len(overlap_text) + sent_len + 1 + else: + current = [sentence] + current_len = sent_len + + if current: + chunks.append(" ".join(current)) + + return chunks + + +def _get_overlap(chunks: list[str], overlap: int) -> str: + """Extract the last ~`overlap` characters from the most recent chunk, snapped to a word boundary.""" + if not chunks or overlap <= 0: + return "" + last = chunks[-1] + tail = last[-overlap:] if len(last) > overlap else last + # Snap to the nearest word boundary to avoid starting mid-word + space_idx = tail.find(" ") + if space_idx > 0: + tail = tail[space_idx + 1 :] + return tail diff --git a/engine/src/stirling/rag/embedder.py b/engine/src/stirling/rag/embedder.py new file mode 100644 index 0000000000..5df71bc808 --- /dev/null +++ b/engine/src/stirling/rag/embedder.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from pydantic_ai import Embedder + +from stirling.rag.chunker import chunk_text +from stirling.rag.store import Document + + +class EmbeddingService: + """Wraps Pydantic AI's Embedder to provide document chunking and embedding.""" + + def __init__(self, model_name: str, chunk_size: int = 512, chunk_overlap: int = 64) -> None: + self._embedder = Embedder(model_name) + self._chunk_size = chunk_size + self._chunk_overlap = chunk_overlap + + async def embed_query(self, text: str) -> list[float]: + """Embed a search query, optimised for retrieval.""" + result = await self._embedder.embed_query(text) + return list(result.embeddings[0]) + + async def embed_documents(self, texts: list[str]) -> list[list[float]]: + """Embed multiple document texts for indexing.""" + if not texts: + return [] + result = await self._embedder.embed_documents(texts) + return [list(emb) for emb in result.embeddings] + + def chunk_and_prepare( + self, + text: str, + source: str = "", + base_metadata: dict[str, str] | None = None, + ) -> list[Document]: + """Chunk text and return Document objects ready for embedding. + + Each chunk gets a unique ID based on source and chunk index. + """ + chunks = chunk_text(text, self._chunk_size, self._chunk_overlap) + documents: list[Document] = [] + for i, chunk in enumerate(chunks): + meta = dict(base_metadata) if base_metadata else {} + meta["source"] = source + meta["chunk_index"] = str(i) + doc_id = f"{source}:chunk:{i}" if source else f"chunk:{i}" + documents.append(Document(id=doc_id, text=chunk, metadata=meta)) + return documents diff --git a/engine/src/stirling/rag/pgvector_store.py b/engine/src/stirling/rag/pgvector_store.py new file mode 100644 index 0000000000..9eedffb47d --- /dev/null +++ b/engine/src/stirling/rag/pgvector_store.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import json + +import psycopg +from pgvector.psycopg import register_vector_async + +from stirling.rag.store import Document, SearchResult, VectorStore + + +class PgVectorStore(VectorStore): + """PostgreSQL + pgvector backed store. + + Connects to an external Postgres instance (DSN provided via config) and uses the + `vector` extension for similarity search. The schema is created on first use. + """ + + def __init__(self, dsn: str) -> None: + if not dsn: + raise ValueError("pgvector backend requires a non-empty DSN (STIRLING_RAG_PGVECTOR_DSN)") + self._dsn = dsn + self._initialized = False + + async def _connect(self) -> psycopg.AsyncConnection: + conn = await psycopg.AsyncConnection.connect(self._dsn) + await register_vector_async(conn) + return conn + + async def _ensure_schema(self) -> None: + if self._initialized: + return + async with await self._connect() as conn: + async with conn.cursor() as cur: + await cur.execute("CREATE EXTENSION IF NOT EXISTS vector") + await cur.execute( + """ + CREATE TABLE IF NOT EXISTS rag_documents ( + id TEXT NOT NULL, + collection TEXT NOT NULL, + text TEXT NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + embedding vector NOT NULL, + PRIMARY KEY (id, collection) + ) + """ + ) + await cur.execute("CREATE INDEX IF NOT EXISTS idx_rag_collection ON rag_documents(collection)") + await conn.commit() + self._initialized = True + + async def add_documents( + self, + collection: str, + documents: list[Document], + embeddings: list[list[float]], + ) -> None: + if len(documents) != len(embeddings): + raise ValueError(f"Got {len(documents)} documents but {len(embeddings)} embeddings") + if not documents: + return + + await self._ensure_schema() + async with await self._connect() as conn: + async with conn.cursor() as cur: + for doc, emb in zip(documents, embeddings): + await cur.execute( + """ + INSERT INTO rag_documents (id, collection, text, metadata, embedding) + VALUES (%s, %s, %s, %s::jsonb, %s) + ON CONFLICT (id, collection) + DO UPDATE SET + text = EXCLUDED.text, + metadata = EXCLUDED.metadata, + embedding = EXCLUDED.embedding + """, + (doc.id, collection, doc.text, json.dumps(doc.metadata), emb), + ) + await conn.commit() + + async def search( + self, + collection: str, + query_embedding: list[float], + top_k: int = 5, + ) -> list[SearchResult]: + await self._ensure_schema() + async with await self._connect() as conn: + async with conn.cursor() as cur: + await cur.execute( + """ + SELECT id, text, metadata, 1 - (embedding <=> %s) AS score + FROM rag_documents + WHERE collection = %s + ORDER BY embedding <=> %s + LIMIT %s + """, + (query_embedding, collection, query_embedding, top_k), + ) + rows = await cur.fetchall() + + return [ + SearchResult( + document=Document(id=r[0], text=r[1], metadata=r[2] or {}), + score=float(r[3]), + ) + for r in rows + ] + + async def delete_collection(self, collection: str) -> None: + await self._ensure_schema() + async with await self._connect() as conn: + async with conn.cursor() as cur: + await cur.execute("DELETE FROM rag_documents WHERE collection = %s", (collection,)) + await conn.commit() + + async def list_collections(self) -> list[str]: + await self._ensure_schema() + async with await self._connect() as conn: + async with conn.cursor() as cur: + await cur.execute("SELECT DISTINCT collection FROM rag_documents ORDER BY collection") + rows = await cur.fetchall() + return [r[0] for r in rows] + + async def has_collection(self, collection: str) -> bool: + await self._ensure_schema() + async with await self._connect() as conn: + async with conn.cursor() as cur: + await cur.execute( + "SELECT 1 FROM rag_documents WHERE collection = %s LIMIT 1", + (collection,), + ) + row = await cur.fetchone() + return row is not None diff --git a/engine/src/stirling/rag/service.py b/engine/src/stirling/rag/service.py new file mode 100644 index 0000000000..b8c9c4ab19 --- /dev/null +++ b/engine/src/stirling/rag/service.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import logging + +from stirling.rag.embedder import EmbeddingService +from stirling.rag.store import Document, SearchResult, VectorStore + +logger = logging.getLogger(__name__) + + +class RagService: + """Orchestrates embedding and vector storage for RAG workflows.""" + + def __init__(self, embedder: EmbeddingService, store: VectorStore, default_top_k: int = 5) -> None: + self._embedder = embedder + self._store = store + self._default_top_k = default_top_k + + async def index_text( + self, + collection: str, + text: str, + source: str = "", + metadata: dict[str, str] | None = None, + ) -> int: + """Chunk, embed, and store text. Returns the number of chunks indexed.""" + documents = self._embedder.chunk_and_prepare(text, source=source, base_metadata=metadata) + if not documents: + return 0 + embeddings = await self._embedder.embed_documents([doc.text for doc in documents]) + await self._store.add_documents(collection, documents, embeddings) + return len(documents) + + async def index_documents(self, collection: str, documents: list[Document]) -> int: + """Embed and store pre-chunked documents. Returns the number stored.""" + if not documents: + return 0 + embeddings = await self._embedder.embed_documents([doc.text for doc in documents]) + await self._store.add_documents(collection, documents, embeddings) + return len(documents) + + async def search( + self, + query: str, + collection: str | None = None, + top_k: int | None = None, + ) -> list[SearchResult]: + """Embed query and search across one or all collections. + + If collection is None, searches all available collections and merges results. + """ + k = top_k if top_k is not None else self._default_top_k + query_embedding = await self._embedder.embed_query(query) + + if collection is not None: + if not await self._store.has_collection(collection): + return [] + return await self._store.search(collection, query_embedding, k) + + # Search all collections, skipping any that error (e.g. dimension mismatch) + collections = await self._store.list_collections() + all_results: list[SearchResult] = [] + for col_name in collections: + try: + results = await self._store.search(col_name, query_embedding, k) + all_results.extend(results) + except Exception: # noqa: BLE001 — any backend error on one collection should not stop the others + logger.warning("Skipping collection %s during cross-collection search", col_name, exc_info=True) + + # Sort by score descending, return top_k across all collections + all_results.sort(key=lambda r: r.score, reverse=True) + return all_results[:k] + + async def delete_collection(self, collection: str) -> None: + """Remove a collection and all its documents.""" + await self._store.delete_collection(collection) + + async def list_collections(self) -> list[str]: + """List all available collections.""" + return await self._store.list_collections() diff --git a/engine/src/stirling/rag/sqlite_vec_store.py b/engine/src/stirling/rag/sqlite_vec_store.py new file mode 100644 index 0000000000..b3008dcf22 --- /dev/null +++ b/engine/src/stirling/rag/sqlite_vec_store.py @@ -0,0 +1,227 @@ +from __future__ import annotations + +import asyncio +import json +import math +import re +import sqlite3 +from pathlib import Path + +import sqlite_vec + +from stirling.rag.store import Document, SearchResult, VectorStore + + +class SqliteVecStore(VectorStore): + """sqlite-vec backed vector store. Single-file SQLite database, embedded, no server. + + Each collection gets its own `vec0` virtual table with a fixed embedding dimension + (detected on first insert). Document metadata lives in a regular table joined by rowid. + """ + + def __init__(self, db_path: str | Path) -> None: + is_memory = str(db_path) == ":memory:" + self._db_path: Path | None = None if is_memory else Path(db_path) + + if self._db_path is not None: + self._db_path.parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(str(self._db_path), check_same_thread=False) + else: + conn = sqlite3.connect(":memory:", check_same_thread=False) + + conn.enable_load_extension(True) + sqlite_vec.load(conn) + conn.enable_load_extension(False) + if self._db_path is not None: + conn.execute("PRAGMA journal_mode=WAL") + + self._conn = conn + self._lock = asyncio.Lock() + self._init_schema() + + @classmethod + def ephemeral(cls) -> SqliteVecStore: + """In-memory store for testing.""" + return cls(":memory:") + + def _init_schema(self) -> None: + self._conn.execute( + """ + CREATE TABLE IF NOT EXISTS collections ( + name TEXT PRIMARY KEY, + dim INTEGER NOT NULL, + table_name TEXT NOT NULL + ) + """ + ) + self._conn.execute( + """ + CREATE TABLE IF NOT EXISTS documents ( + id TEXT NOT NULL, + collection TEXT NOT NULL, + text TEXT NOT NULL, + metadata TEXT NOT NULL DEFAULT '{}', + vec_rowid INTEGER NOT NULL, + PRIMARY KEY (id, collection) + ) + """ + ) + self._conn.execute("CREATE INDEX IF NOT EXISTS idx_doc_collection ON documents(collection)") + self._conn.commit() + + @staticmethod + def _sanitize_table_name(collection: str) -> str: + safe = re.sub(r"[^a-zA-Z0-9_]", "_", collection) + return f"vec_{safe}" + + @staticmethod + def _normalize(vector: list[float]) -> list[float]: + norm = math.sqrt(sum(x * x for x in vector)) + if norm == 0: + return list(vector) + return [x / norm for x in vector] + + async def add_documents( + self, + collection: str, + documents: list[Document], + embeddings: list[list[float]], + ) -> None: + if len(documents) != len(embeddings): + raise ValueError(f"Got {len(documents)} documents but {len(embeddings)} embeddings") + if not documents: + return + + async with self._lock: + await asyncio.to_thread(self._sync_add, collection, documents, embeddings) + + def _sync_add( + self, + collection: str, + documents: list[Document], + embeddings: list[list[float]], + ) -> None: + dim = len(embeddings[0]) + row = self._conn.execute("SELECT dim, table_name FROM collections WHERE name = ?", (collection,)).fetchone() + if row is None: + table_name = self._sanitize_table_name(collection) + self._conn.execute(f"CREATE VIRTUAL TABLE IF NOT EXISTS {table_name} USING vec0(embedding float[{dim}])") + self._conn.execute( + "INSERT INTO collections(name, dim, table_name) VALUES (?, ?, ?)", + (collection, dim, table_name), + ) + else: + existing_dim, table_name = row + if existing_dim != dim: + raise ValueError(f"Collection {collection} has dim {existing_dim}, got embedding of dim {dim}") + + # Upsert: delete existing docs with matching IDs first + ids = [doc.id for doc in documents] + placeholders = ",".join("?" * len(ids)) + existing = self._conn.execute( + f"SELECT vec_rowid FROM documents WHERE collection = ? AND id IN ({placeholders})", + (collection, *ids), + ).fetchall() + if existing: + vec_rowids = [r[0] for r in existing] + row_placeholders = ",".join("?" * len(vec_rowids)) + self._conn.execute( + f"DELETE FROM {table_name} WHERE rowid IN ({row_placeholders})", + vec_rowids, + ) + self._conn.execute( + f"DELETE FROM documents WHERE collection = ? AND id IN ({placeholders})", + (collection, *ids), + ) + + for doc, emb in zip(documents, embeddings): + normalized = self._normalize(list(emb)) + cursor = self._conn.execute( + f"INSERT INTO {table_name}(embedding) VALUES (?)", + (sqlite_vec.serialize_float32(normalized),), + ) + vec_rowid = cursor.lastrowid + self._conn.execute( + "INSERT INTO documents(id, collection, text, metadata, vec_rowid) VALUES (?, ?, ?, ?, ?)", + (doc.id, collection, doc.text, json.dumps(doc.metadata), vec_rowid), + ) + self._conn.commit() + + async def search( + self, + collection: str, + query_embedding: list[float], + top_k: int = 5, + ) -> list[SearchResult]: + async with self._lock: + return await asyncio.to_thread(self._sync_search, collection, query_embedding, top_k) + + def _sync_search( + self, + collection: str, + query_embedding: list[float], + top_k: int, + ) -> list[SearchResult]: + row = self._conn.execute("SELECT table_name, dim FROM collections WHERE name = ?", (collection,)).fetchone() + if row is None: + return [] + table_name, dim = row + if len(query_embedding) != dim: + raise ValueError(f"Query embedding dim {len(query_embedding)} does not match collection dim {dim}") + + normalized = self._normalize(list(query_embedding)) + query_blob = sqlite_vec.serialize_float32(normalized) + + results = self._conn.execute( + f""" + SELECT d.id, d.text, d.metadata, v.distance + FROM {table_name} v + JOIN documents d ON d.vec_rowid = v.rowid AND d.collection = ? + WHERE v.embedding MATCH ? AND k = ? + ORDER BY v.distance + """, + (collection, query_blob, top_k), + ).fetchall() + + return [ + SearchResult( + document=Document( + id=r[0], + text=r[1], + metadata=json.loads(r[2]) if r[2] else {}, + ), + # For normalized vectors: cosine_sim = 1 - (L2^2 / 2) + score=max(0.0, 1.0 - (r[3] ** 2) / 2.0), + ) + for r in results + ] + + async def delete_collection(self, collection: str) -> None: + async with self._lock: + await asyncio.to_thread(self._sync_delete_collection, collection) + + def _sync_delete_collection(self, collection: str) -> None: + row = self._conn.execute("SELECT table_name FROM collections WHERE name = ?", (collection,)).fetchone() + if row is None: + return + table_name = row[0] + self._conn.execute(f"DROP TABLE IF EXISTS {table_name}") + self._conn.execute("DELETE FROM documents WHERE collection = ?", (collection,)) + self._conn.execute("DELETE FROM collections WHERE name = ?", (collection,)) + self._conn.commit() + + async def list_collections(self) -> list[str]: + async with self._lock: + return await asyncio.to_thread(self._sync_list_collections) + + def _sync_list_collections(self) -> list[str]: + rows = self._conn.execute("SELECT name FROM collections ORDER BY name").fetchall() + return [r[0] for r in rows] + + async def has_collection(self, collection: str) -> bool: + async with self._lock: + return await asyncio.to_thread(self._sync_has_collection, collection) + + def _sync_has_collection(self, collection: str) -> bool: + row = self._conn.execute("SELECT 1 FROM collections WHERE name = ?", (collection,)).fetchone() + return row is not None diff --git a/engine/src/stirling/rag/store.py b/engine/src/stirling/rag/store.py new file mode 100644 index 0000000000..1ad0dffbf5 --- /dev/null +++ b/engine/src/stirling/rag/store.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field + + +@dataclass +class Document: + """A chunk of text with metadata, ready for embedding and storage.""" + + id: str + text: str + metadata: dict[str, str] = field(default_factory=dict) + + +@dataclass +class SearchResult: + """A document returned from a vector search with its relevance score.""" + + document: Document + score: float + + +class VectorStore(ABC): + """Abstract interface for vector storage backends. + + Implementations must handle persistence, collection management, + and nearest-neighbor search over pre-computed embeddings. + """ + + @abstractmethod + async def add_documents( + self, + collection: str, + documents: list[Document], + embeddings: list[list[float]], + ) -> None: + """Store documents with their embeddings in the named collection.""" + + @abstractmethod + async def search( + self, + collection: str, + query_embedding: list[float], + top_k: int = 5, + ) -> list[SearchResult]: + """Return the top_k most similar documents from the collection.""" + + @abstractmethod + async def delete_collection(self, collection: str) -> None: + """Remove a collection and all its documents.""" + + @abstractmethod + async def list_collections(self) -> list[str]: + """Return names of all existing collections.""" + + @abstractmethod + async def has_collection(self, collection: str) -> bool: + """Check whether a collection exists.""" diff --git a/engine/src/stirling/services/runtime.py b/engine/src/stirling/services/runtime.py index 656e7d9512..3b91abbd79 100644 --- a/engine/src/stirling/services/runtime.py +++ b/engine/src/stirling/services/runtime.py @@ -1,11 +1,23 @@ from __future__ import annotations +import logging from dataclasses import dataclass +from typing import assert_never from pydantic_ai.models import Model, infer_model from pydantic_ai.settings import ModelSettings -from stirling.config import AppSettings +from stirling.config import ENGINE_ROOT, AppSettings, RagBackend +from stirling.rag import ( + EmbeddingService, + PgVectorStore, + RagCapability, + RagService, + SqliteVecStore, + VectorStore, +) + +logger = logging.getLogger(__name__) @dataclass(frozen=True) @@ -13,6 +25,8 @@ class AppRuntime: settings: AppSettings fast_model: Model smart_model: Model + rag_service: RagService + rag_capability: RagCapability @property def fast_model_settings(self) -> ModelSettings: @@ -39,13 +53,47 @@ def validate_structured_output_support(model: Model, model_name: str) -> None: raise ValueError(f"Unsupported model {model_name}. This model does not support structured outputs.") +def _build_vector_store(settings: AppSettings) -> VectorStore: + """Build the configured vector store backend.""" + if settings.rag_backend == RagBackend.SQLITE: + store_path = settings.rag_store_path + # Treat ":memory:" as a special in-process token; otherwise resolve against the engine root. + if str(store_path) != ":memory:" and not store_path.is_absolute(): + store_path = ENGINE_ROOT / store_path + logger.info("RAG backend=sqlite, db_path=%s", store_path) + return SqliteVecStore(db_path=store_path) + if settings.rag_backend == RagBackend.PGVECTOR: + logger.info("RAG backend=pgvector, dsn=") + return PgVectorStore(dsn=settings.rag_pgvector_dsn) + assert_never(settings.rag_backend) + + +def _build_rag(settings: AppSettings) -> tuple[RagService, RagCapability]: + """Build the RAG service and capability.""" + logger.info("RAG: embedding_model=%s", settings.rag_embedding_model) + embedder = EmbeddingService( + model_name=settings.rag_embedding_model, + chunk_size=settings.rag_chunk_size, + chunk_overlap=settings.rag_chunk_overlap, + ) + store = _build_vector_store(settings) + service = RagService(embedder=embedder, store=store, default_top_k=settings.rag_default_top_k) + capability = RagCapability(rag_service=service, top_k=settings.rag_default_top_k) + return service, capability + + def build_runtime(settings: AppSettings) -> AppRuntime: fast_model = infer_model(settings.fast_model_name) smart_model = infer_model(settings.smart_model_name) validate_structured_output_support(fast_model, settings.fast_model_name) validate_structured_output_support(smart_model, settings.smart_model_name) + + rag_service, rag_capability = _build_rag(settings) + return AppRuntime( settings=settings, fast_model=fast_model, smart_model=smart_model, + rag_service=rag_service, + rag_capability=rag_capability, ) diff --git a/engine/tests/conftest.py b/engine/tests/conftest.py index 309bbd0ac7..3e1809f769 100644 --- a/engine/tests/conftest.py +++ b/engine/tests/conftest.py @@ -1,10 +1,11 @@ from __future__ import annotations from collections.abc import Iterator +from pathlib import Path import pytest -from stirling.config import AppSettings, load_settings +from stirling.config import AppSettings, RagBackend, load_settings from stirling.services import build_runtime from stirling.services.runtime import AppRuntime @@ -22,6 +23,13 @@ def build_app_settings() -> AppSettings: fast_model_name="test", smart_model_max_tokens=8192, fast_model_max_tokens=2048, + rag_backend=RagBackend.SQLITE, + rag_embedding_model="voyageai:voyage-4", + rag_store_path=Path(":memory:"), + rag_pgvector_dsn="", + rag_chunk_size=512, + rag_chunk_overlap=64, + rag_default_top_k=5, posthog_enabled=False, posthog_api_key="", posthog_host="https://eu.i.posthog.com", diff --git a/engine/tests/ledger/test_routes.py b/engine/tests/ledger/test_routes.py index 6e36dd4712..ca322a6364 100644 --- a/engine/tests/ledger/test_routes.py +++ b/engine/tests/ledger/test_routes.py @@ -34,15 +34,9 @@ from stirling.contracts.ledger import ( class StubSettingsProvider: def __call__(self) -> AppSettings: - return AppSettings( - smart_model_name="test", - fast_model_name="test", - smart_model_max_tokens=8192, - fast_model_max_tokens=2048, - posthog_enabled=False, - posthog_api_key="", - posthog_host="https://eu.i.posthog.com", - ) + from conftest import build_app_settings + + return build_app_settings() class StubLedgerAgent: diff --git a/engine/tests/test_rag.py b/engine/tests/test_rag.py new file mode 100644 index 0000000000..da1d524653 --- /dev/null +++ b/engine/tests/test_rag.py @@ -0,0 +1,283 @@ +from __future__ import annotations + +import pytest + +from stirling.rag.capability import RagCapability +from stirling.rag.chunker import chunk_text +from stirling.rag.service import RagService +from stirling.rag.sqlite_vec_store import SqliteVecStore +from stirling.rag.store import Document, SearchResult + +# ── chunk_text ────────────────────────────────────────────────────────── + + +class TestChunkText: + def test_empty_input_returns_empty(self) -> None: + assert chunk_text("") == [] + assert chunk_text(" ") == [] + + def test_short_text_returns_single_chunk(self) -> None: + text = "Hello world." + chunks = chunk_text(text, chunk_size=100) + assert len(chunks) == 1 + assert chunks[0] == "Hello world." + + def test_splits_on_paragraph_boundaries(self) -> None: + text = "First paragraph.\n\nSecond paragraph.\n\nThird paragraph." + chunks = chunk_text(text, chunk_size=30, overlap=0) + # Each paragraph fits in 30 chars, so they should be split + assert len(chunks) >= 2 + assert "First paragraph." in chunks[0] + + def test_long_text_produces_multiple_chunks(self) -> None: + text = " ".join(["word"] * 200) + chunks = chunk_text(text, chunk_size=100, overlap=10) + assert len(chunks) > 1 + for chunk in chunks: + # Chunks may slightly exceed due to sentence boundary snapping + assert len(chunk) <= 200 # generous upper bound + + def test_overlap_produces_shared_content(self) -> None: + sentences = [f"Sentence number {i}." for i in range(20)] + text = " ".join(sentences) + chunks = chunk_text(text, chunk_size=100, overlap=30) + if len(chunks) >= 2: + # After word-boundary snapping, the second chunk should share + # some content with the tail of the first chunk + words_in_first_tail = chunks[0].split()[-3:] # last 3 words + overlap_text = " ".join(words_in_first_tail) + assert overlap_text in chunks[1], f"Expected overlap '{overlap_text}' in chunk[1]: '{chunks[1][:80]}...'" + + +# ── SqliteVecStore ────────────────────────────────────────────────────── + + +class TestSqliteVecStore: + """Each test gets its own ephemeral store to avoid cross-test dimension conflicts.""" + + @pytest.mark.anyio + async def test_add_and_search(self) -> None: + store = SqliteVecStore.ephemeral() + docs = [ + Document(id="1", text="Python is a programming language", metadata={"source": "test"}), + Document(id="2", text="Java is another programming language", metadata={"source": "test"}), + Document(id="3", text="The weather today is sunny", metadata={"source": "test"}), + ] + # Simple 3-dimensional embeddings for testing + embeddings = [ + [1.0, 0.0, 0.0], + [0.9, 0.1, 0.0], + [0.0, 0.0, 1.0], + ] + await store.add_documents("test-col", docs, embeddings) + + # Search with a query close to the programming-related docs + results = await store.search("test-col", [1.0, 0.05, 0.0], top_k=2) + assert len(results) == 2 + assert isinstance(results[0], SearchResult) + # The closest should be doc "1" (exact match on first dimension) + assert results[0].document.id == "1" + assert results[0].score > 0.5 + + @pytest.mark.anyio + async def test_list_and_has_collection(self) -> None: + store = SqliteVecStore.ephemeral() + docs = [Document(id="1", text="test", metadata={})] + await store.add_documents("my-collection", docs, [[1.0, 0.0]]) + + collections = await store.list_collections() + assert "my-collection" in collections + assert await store.has_collection("my-collection") is True + assert await store.has_collection("nonexistent") is False + + @pytest.mark.anyio + async def test_delete_collection(self) -> None: + store = SqliteVecStore.ephemeral() + docs = [Document(id="1", text="test", metadata={})] + await store.add_documents("to-delete", docs, [[1.0]]) + + assert await store.has_collection("to-delete") is True + await store.delete_collection("to-delete") + assert await store.has_collection("to-delete") is False + + @pytest.mark.anyio + async def test_search_empty_collection(self) -> None: + store = SqliteVecStore.ephemeral() + docs = [Document(id="1", text="test", metadata={})] + await store.add_documents("empty-test", docs, [[1.0, 0.0]]) + results = await store.search("empty-test", [1.0, 0.0], top_k=5) + assert len(results) == 1 + + @pytest.mark.anyio + async def test_mismatched_docs_embeddings_raises(self) -> None: + store = SqliteVecStore.ephemeral() + docs = [Document(id="1", text="test", metadata={})] + with pytest.raises(ValueError, match="documents.*embeddings"): + await store.add_documents("bad", docs, [[1.0], [2.0]]) + + +# ── RagService (with stub embedder) ──────────────────────────────────── + + +class StubEmbeddingService: + """A minimal stub that returns fixed-dimension embeddings for testing.""" + + def __init__(self, dim: int = 8) -> None: + self._dim = dim + + async def embed_query(self, text: str) -> list[float]: + # Deterministic embedding based on hash of text + h = hash(text) % 1000 + return [(h + i) / 1000.0 for i in range(self._dim)] + + async def embed_documents(self, texts: list[str]) -> list[list[float]]: + return [await self.embed_query(t) for t in texts] + + def chunk_and_prepare( + self, + text: str, + source: str = "", + base_metadata: dict[str, str] | None = None, + ) -> list[Document]: + from stirling.rag.chunker import chunk_text + + chunks = chunk_text(text, 100, 10) + docs = [] + for i, chunk in enumerate(chunks): + meta = dict(base_metadata) if base_metadata else {} + meta["source"] = source + meta["chunk_index"] = str(i) + doc_id = f"{source}:chunk:{i}" if source else f"chunk:{i}" + docs.append(Document(id=doc_id, text=chunk, metadata=meta)) + return docs + + +@pytest.fixture +def rag_service() -> RagService: + """Each RagService test gets its own fresh ephemeral store to avoid dimension conflicts.""" + store = SqliteVecStore.ephemeral() + return RagService(embedder=StubEmbeddingService(), store=store, default_top_k=3) # type: ignore[arg-type] + + +class TestRagService: + @pytest.mark.anyio + async def test_index_and_search(self, rag_service: RagService) -> None: + text = "Python is great for data science. It has many libraries like pandas and numpy." + count = await rag_service.index_text("docs", text, source="guide.pdf") + assert count > 0 + + results = await rag_service.search("Python libraries", collection="docs") + assert len(results) > 0 + assert results[0].document.text # non-empty text + + @pytest.mark.anyio + async def test_index_empty_text_returns_zero(self, rag_service: RagService) -> None: + count = await rag_service.index_text("docs", "", source="empty.pdf") + assert count == 0 + + @pytest.mark.anyio + async def test_search_nonexistent_collection_returns_empty(self, rag_service: RagService) -> None: + results = await rag_service.search("anything", collection="nonexistent") + assert results == [] + + @pytest.mark.anyio + async def test_search_all_collections(self, rag_service: RagService) -> None: + await rag_service.index_text("col-a", "Machine learning overview.", source="ml.pdf") + await rag_service.index_text("col-b", "Deep learning with neural networks.", source="dl.pdf") + + results = await rag_service.search("neural networks") + assert len(results) > 0 + + @pytest.mark.anyio + async def test_delete_collection(self, rag_service: RagService) -> None: + await rag_service.index_text("temp", "Temporary data.", source="tmp.pdf") + collections = await rag_service.list_collections() + assert "temp" in collections + + await rag_service.delete_collection("temp") + collections = await rag_service.list_collections() + assert "temp" not in collections + + +# ── RagCapability ────────────────────────────────────────────────────── + + +async def _invoke_search_knowledge(capability: RagCapability, query: str, max_results: int = 5) -> str: + """Extract and call the search_knowledge tool function from a RagCapability's toolset.""" + from pydantic_ai import FunctionToolset + + toolset = capability.toolset + assert isinstance(toolset, FunctionToolset) + tool = toolset.tools["search_knowledge"] + return await tool.function(query=query, max_results=max_results) # type: ignore[call-arg] — pyright can't infer the generic tool function's kwargs + + +class TestRagCapability: + def test_instructions_static_when_collections_pinned(self, rag_service: RagService) -> None: + cap = RagCapability(rag_service, collections=["docs", "manuals"]) + instructions = cap.instructions + assert isinstance(instructions, str) + assert "docs, manuals" in instructions + assert "search_knowledge" in instructions + + def test_instructions_dynamic_when_no_collections(self, rag_service: RagService) -> None: + cap = RagCapability(rag_service) + instructions = cap.instructions + assert callable(instructions) + + @pytest.mark.anyio + async def test_dynamic_instructions_list_available_collections(self, rag_service: RagService) -> None: + await rag_service.index_text("col-a", "Alpha content.", source="a.pdf") + await rag_service.index_text("col-b", "Beta content.", source="b.pdf") + cap = RagCapability(rag_service) + instructions_fn = cap.instructions + assert callable(instructions_fn) + text = await instructions_fn() + assert "col-a" in text + assert "col-b" in text + + @pytest.mark.anyio + async def test_dynamic_instructions_when_store_empty(self, rag_service: RagService) -> None: + cap = RagCapability(rag_service) + instructions_fn = cap.instructions + assert callable(instructions_fn) + text = await instructions_fn() + assert "empty" in text.lower() + + @pytest.mark.anyio + async def test_search_knowledge_returns_no_results_message_when_empty(self, rag_service: RagService) -> None: + cap = RagCapability(rag_service) + output = await _invoke_search_knowledge(cap, "anything") + assert output == "No relevant results found in the knowledge base." + + @pytest.mark.anyio + async def test_search_knowledge_formats_results_with_source_and_score(self, rag_service: RagService) -> None: + await rag_service.index_text("docs", "Python is a programming language.", source="guide.pdf") + cap = RagCapability(rag_service) + output = await _invoke_search_knowledge(cap, "Python") + assert "[Result 1" in output + assert "source: guide.pdf" in output + assert "chunk:" in output + assert "relevance:" in output + + @pytest.mark.anyio + async def test_search_knowledge_restricts_to_pinned_collections(self, rag_service: RagService) -> None: + await rag_service.index_text("pinned", "Pinned collection content.", source="pinned.pdf") + await rag_service.index_text("other", "Content in another collection.", source="other.pdf") + + cap = RagCapability(rag_service, collections=["pinned"]) + output = await _invoke_search_knowledge(cap, "content") + assert "pinned.pdf" in output + assert "other.pdf" not in output + + @pytest.mark.anyio + async def test_search_knowledge_respects_max_results(self, rag_service: RagService) -> None: + paragraphs = "\n\n".join(f"Paragraph {i} about topic." for i in range(10)) + await rag_service.index_text("bulk", paragraphs, source="bulk.pdf") + + cap = RagCapability(rag_service) + output = await _invoke_search_knowledge(cap, "topic", max_results=2) + # Only two results requested, so only Result 1 and Result 2 should appear + assert "[Result 1" in output + assert "[Result 2" in output + assert "[Result 3" not in output diff --git a/engine/tests/test_rag_routes.py b/engine/tests/test_rag_routes.py new file mode 100644 index 0000000000..6227159279 --- /dev/null +++ b/engine/tests/test_rag_routes.py @@ -0,0 +1,205 @@ +from __future__ import annotations + +from collections.abc import Iterator + +import pytest +from fastapi.testclient import TestClient + +from stirling.api import app +from stirling.api.dependencies import get_rag_embedding_model, get_rag_service +from stirling.rag import Document, RagService, SqliteVecStore + +TEST_EMBEDDING_MODEL = "test-embedder" + + +class StubEmbedder: + """Deterministic embeddings for route tests — no network, no provider needed.""" + + def __init__(self, dim: int = 8) -> None: + self._dim = dim + + async def embed_query(self, text: str) -> list[float]: + h = hash(text) % 1000 + return [(h + i) / 1000.0 for i in range(self._dim)] + + async def embed_documents(self, texts: list[str]) -> list[list[float]]: + return [await self.embed_query(t) for t in texts] + + def chunk_and_prepare( + self, + text: str, + source: str = "", + base_metadata: dict[str, str] | None = None, + ) -> list[Document]: + from stirling.rag.chunker import chunk_text + + chunks = chunk_text(text, 100, 10) + docs = [] + for i, chunk in enumerate(chunks): + meta = dict(base_metadata) if base_metadata else {} + meta["source"] = source + meta["chunk_index"] = str(i) + doc_id = f"{source}:chunk:{i}" if source else f"chunk:{i}" + docs.append(Document(id=doc_id, text=chunk, metadata=meta)) + return docs + + +def _build_service() -> RagService: + return RagService( + embedder=StubEmbedder(), # type: ignore[arg-type] + store=SqliteVecStore.ephemeral(), + default_top_k=3, + ) + + +@pytest.fixture +def client() -> Iterator[TestClient]: + service = _build_service() + app.dependency_overrides[get_rag_service] = lambda: service + app.dependency_overrides[get_rag_embedding_model] = lambda: TEST_EMBEDDING_MODEL + try: + yield TestClient(app) + finally: + app.dependency_overrides.pop(get_rag_service, None) + app.dependency_overrides.pop(get_rag_embedding_model, None) + + +# ── /status ───────────────────────────────────────────────────────────── + + +def test_status_reports_embedding_model_and_collections(client: TestClient) -> None: + client.post( + "/api/v1/rag/index", + json={"collection": "my-docs", "text": "Hello world.", "source": "a.pdf"}, + ) + response = client.get("/api/v1/rag/status") + assert response.status_code == 200 + body = response.json() + assert body["embeddingModel"] == TEST_EMBEDDING_MODEL + assert "my-docs" in body["collections"] + + +def test_status_when_empty(client: TestClient) -> None: + response = client.get("/api/v1/rag/status") + assert response.status_code == 200 + body = response.json() + assert body == {"embeddingModel": TEST_EMBEDDING_MODEL, "collections": []} + + +# ── /index ────────────────────────────────────────────────────────────── + + +def test_index_returns_chunk_count(client: TestClient) -> None: + response = client.post( + "/api/v1/rag/index", + json={"collection": "indexed", "text": "Short text.", "source": "doc.pdf"}, + ) + assert response.status_code == 200 + body = response.json() + assert body["collection"] == "indexed" + assert body["chunksIndexed"] >= 1 + + +def test_index_rejects_empty_collection_name(client: TestClient) -> None: + response = client.post( + "/api/v1/rag/index", + json={"collection": "", "text": "Text.", "source": "x.pdf"}, + ) + assert response.status_code == 422 + + +def test_index_rejects_oversized_text(client: TestClient) -> None: + huge = "x" * 1_000_001 # Just over the 1MB cap + response = client.post( + "/api/v1/rag/index", + json={"collection": "toobig", "text": huge}, + ) + assert response.status_code == 422 + + +# ── /search ───────────────────────────────────────────────────────────── + + +def test_search_returns_results(client: TestClient) -> None: + client.post( + "/api/v1/rag/index", + json={"collection": "search-test", "text": "Python is fun.", "source": "guide.pdf"}, + ) + response = client.post( + "/api/v1/rag/search", + json={"query": "Python", "collection": "search-test", "topK": 3}, + ) + assert response.status_code == 200 + body = response.json() + assert body["query"] == "Python" + assert len(body["results"]) >= 1 + first = body["results"][0] + assert first["source"] == "guide.pdf" + assert "score" in first + + +def test_search_rejects_empty_collection_name(client: TestClient) -> None: + response = client.post( + "/api/v1/rag/search", + json={"query": "anything", "collection": ""}, + ) + assert response.status_code == 422 + + +def test_search_without_collection_searches_all(client: TestClient) -> None: + client.post( + "/api/v1/rag/index", + json={"collection": "col-one", "text": "Alpha content.", "source": "one.pdf"}, + ) + client.post( + "/api/v1/rag/index", + json={"collection": "col-two", "text": "Beta content.", "source": "two.pdf"}, + ) + response = client.post( + "/api/v1/rag/search", + json={"query": "content"}, + ) + assert response.status_code == 200 + body = response.json() + assert len(body["results"]) >= 1 + + +# ── /collections ──────────────────────────────────────────────────────── + + +def test_collections_empty_when_no_data(client: TestClient) -> None: + response = client.get("/api/v1/rag/collections") + assert response.status_code == 200 + assert response.json() == {"collections": []} + + +def test_collections_lists_indexed(client: TestClient) -> None: + client.post( + "/api/v1/rag/index", + json={"collection": "list-me", "text": "Text.", "source": "x.pdf"}, + ) + response = client.get("/api/v1/rag/collections") + assert response.status_code == 200 + assert "list-me" in response.json()["collections"] + + +# ── DELETE /collections/{name} ────────────────────────────────────────── + + +def test_delete_collection_removes_it(client: TestClient) -> None: + client.post( + "/api/v1/rag/index", + json={"collection": "to-delete", "text": "Text.", "source": "x.pdf"}, + ) + response = client.delete("/api/v1/rag/collections/to-delete") + assert response.status_code == 200 + assert response.json() == {"status": "deleted", "collection": "to-delete"} + + listing = client.get("/api/v1/rag/collections").json() + assert "to-delete" not in listing["collections"] + + +def test_delete_nonexistent_collection_is_idempotent(client: TestClient) -> None: + response = client.delete("/api/v1/rag/collections/never-existed") + assert response.status_code == 200 + assert response.json() == {"status": "deleted", "collection": "never-existed"} diff --git a/engine/tests/test_stirling_contracts.py b/engine/tests/test_stirling_contracts.py index 3e4138a928..9e113015cb 100644 --- a/engine/tests/test_stirling_contracts.py +++ b/engine/tests/test_stirling_contracts.py @@ -73,11 +73,22 @@ def test_pdf_question_answer_defaults_evidence_list() -> None: def test_app_settings_accepts_model_configuration() -> None: + from pathlib import Path + + from stirling.config import RagBackend + settings = AppSettings( smart_model_name="claude-sonnet-4-5-20250929", fast_model_name="claude-haiku-4-5-20251001", smart_model_max_tokens=8192, fast_model_max_tokens=2048, + rag_backend=RagBackend.SQLITE, + rag_embedding_model="voyageai:voyage-4", + rag_store_path=Path(":memory:"), + rag_pgvector_dsn="", + rag_chunk_size=512, + rag_chunk_overlap=64, + rag_default_top_k=5, posthog_enabled=False, posthog_api_key="", posthog_host="https://eu.i.posthog.com", diff --git a/engine/uv.lock b/engine/uv.lock index 7d91203d39..c8f79e6372 100644 --- a/engine/uv.lock +++ b/engine/uv.lock @@ -107,6 +107,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, ] +[[package]] +name = "aiolimiter" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/23/b52debf471f7a1e42e362d959a3982bdcb4fe13a5d46e63d28868807a79c/aiolimiter-1.2.1.tar.gz", hash = "sha256:e02a37ea1a855d9e832252a105420ad4d15011505512a1a1d814647451b5cca9", size = 7185, upload-time = "2024-12-08T15:31:51.496Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/ba/df6e8e1045aebc4778d19b8a3a9bc1808adb1619ba94ca354d9ba17d86c3/aiolimiter-1.2.1-py3-none-any.whl", hash = "sha256:d3f249e9059a20badcb56b61601a83556133655c11d1eb3dd3e04ff069e5f3c7", size = 6711, upload-time = "2024-12-08T15:31:49.874Z" }, +] + [[package]] name = "aiosignal" version = "1.4.0" @@ -596,16 +605,21 @@ source = { editable = "." } dependencies = [ { name = "fastapi" }, { name = "opentelemetry-sdk" }, + { name = "pgvector" }, { name = "posthog" }, + { name = "psycopg", extra = ["binary"] }, { name = "pydantic" }, { name = "pydantic-ai" }, + { name = "pydantic-ai-slim", extra = ["voyageai"] }, { name = "pydantic-settings" }, { name = "python-dotenv" }, + { name = "sqlite-vec" }, { name = "uvicorn" }, ] [package.dev-dependencies] dev = [ + { name = "anyio" }, { name = "datamodel-code-generator", extra = ["ruff"] }, { name = "pyright" }, { name = "pytest" }, @@ -617,16 +631,21 @@ dev = [ requires-dist = [ { name = "fastapi", specifier = ">=0.116.0" }, { name = "opentelemetry-sdk", specifier = ">=1.39.0" }, + { name = "pgvector", specifier = ">=0.3.6" }, { name = "posthog", specifier = ">=3.0.0" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.2" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pydantic-ai", specifier = ">=1.67.0" }, + { name = "pydantic-ai-slim", extras = ["voyageai"], specifier = ">=1.67.0" }, { name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "python-dotenv", specifier = ">=1.2.1" }, + { name = "sqlite-vec", specifier = ">=0.1.6" }, { name = "uvicorn", specifier = ">=0.35.0" }, ] [package.metadata.requires-dev] dev = [ + { name = "anyio", specifier = ">=4.0.0" }, { name = "datamodel-code-generator", extras = ["ruff"], specifier = ">=0.26.0" }, { name = "pyright", specifier = ">=1.1.408" }, { name = "pytest", specifier = ">=8.0.0" }, @@ -738,6 +757,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/48/84b6dcba793178a44b9d99b4def6cd62f870dcfc5bb7b9153ac390135812/fastmcp-3.2.3-py3-none-any.whl", hash = "sha256:cc50af6eed1f62ed8b6ebf4987286d8d1d006f08d5bec739d5c7fb76160e0911", size = 707260, upload-time = "2026-04-09T22:05:01.225Z" }, ] +[[package]] +name = "ffmpeg-python" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "future", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543, upload-time = "2019-07-06T00:19:08.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/0c/56be52741f75bad4dc6555991fabd2e07b432d333da82c11ad701123888a/ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5", size = 25024, upload-time = "2019-07-06T00:19:07.215Z" }, +] + [[package]] name = "filelock" version = "3.25.2" @@ -829,6 +860,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] +[[package]] +name = "future" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, +] + [[package]] name = "genai-prices" version = "0.0.56" @@ -1223,6 +1263,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + [[package]] name = "jsonpath-python" version = "1.1.5" @@ -1232,6 +1284,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/50/1a313fb700526b134c71eb8a225d8b83be0385dbb0204337b4379c698cef/jsonpath_python-1.1.5-py3-none-any.whl", hash = "sha256:a60315404d70a65e76c9a782c84e50600480221d94a58af47b7b4d437351cb4b", size = 14090, upload-time = "2026-03-17T06:16:39.152Z" }, ] +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + [[package]] name = "jsonref" version = "1.1.0" @@ -1299,6 +1360,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] +[[package]] +name = "langchain-core" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch", marker = "python_full_version < '3.14'" }, + { name = "langsmith", marker = "python_full_version < '3.14'" }, + { name = "packaging", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "pyyaml", marker = "python_full_version < '3.14'" }, + { name = "tenacity", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, + { name = "uuid-utils", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/fe/20190232d9b513242899dbb0c2bb77e31b4d61e343743adbe90ebc2603d2/langchain_core-1.3.0.tar.gz", hash = "sha256:14a39f528bf459aa3aa40d0a7f7f1bae7520d435ef991ae14a4ceb74d8c49046", size = 860755, upload-time = "2026-04-17T14:51:38.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/e2/dbfa347aa072a6dc4cd38d6f9ebfc730b4c14c258c47f480f4c5c546f177/langchain_core-1.3.0-py3-none-any.whl", hash = "sha256:baf16ee028475df177b9ab8869a751c79406d64a6f12125b93802991b566cced", size = 515140, upload-time = "2026-04-17T14:51:36.274Z" }, +] + +[[package]] +name = "langchain-text-splitters" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/9f/6c545900fefb7b00ddfa3f16b80d61338a0ec68c31c5451eeeab99082760/langchain_text_splitters-1.1.2.tar.gz", hash = "sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627", size = 293580, upload-time = "2026-04-16T14:20:39.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/26/1ef06f56198d631296d646a6223de35bcc6cf9795ceb2442816bc963b84c/langchain_text_splitters-1.1.2-py3-none-any.whl", hash = "sha256:a2de0d799ff31886429fd6e2e0032df275b60ec817c19059a7b46181cc1c2f10", size = 35903, upload-time = "2026-04-16T14:20:38.243Z" }, +] + +[[package]] +name = "langsmith" +version = "0.7.32" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", marker = "python_full_version < '3.14'" }, + { name = "orjson", marker = "python_full_version < '3.14' and platform_python_implementation != 'PyPy'" }, + { name = "packaging", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "requests-toolbelt", marker = "python_full_version < '3.14'" }, + { name = "uuid-utils", marker = "python_full_version < '3.14'" }, + { name = "xxhash", marker = "python_full_version < '3.14'" }, + { name = "zstandard", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/b4/a0b4a501bee6b8a741ce29f8c48155b132118483cddc6f9247735ddb38fa/langsmith-0.7.32.tar.gz", hash = "sha256:b59b8e106d0e4c4842e158229296086e2aa7c561e3f602acda73d3ad0062e915", size = 1184518, upload-time = "2026-04-15T23:42:41.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/bc/148f98ac7dad73ac5e1b1c985290079cfeeb9ba13d760a24f25002beb2c9/langsmith-0.7.32-py3-none-any.whl", hash = "sha256:e1fde928990c4c52f47dc5132708cec674355d9101723d564183e965f383bf5f", size = 378272, upload-time = "2026-04-15T23:42:39.905Z" }, +] + [[package]] name = "logfire" version = "4.30.0" @@ -1568,6 +1680,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + [[package]] name = "openai" version = "2.29.0" @@ -1721,6 +1883,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" }, ] +[[package]] +name = "orjson" +version = "3.11.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, + { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, + { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, + { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, + { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, + { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, + { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, + { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -1748,6 +1948,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] +[[package]] +name = "pgvector" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/6c/6d8b4b03b958c02fa8687ec6063c49d952a189f8c91ebbe51e877dfab8f7/pgvector-0.4.2.tar.gz", hash = "sha256:322cac0c1dc5d41c9ecf782bd9991b7966685dee3a00bc873631391ed949513a", size = 31354, upload-time = "2025-12-05T01:07:17.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441, upload-time = "2025-12-05T01:07:16.536Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + [[package]] name = "platformdirs" version = "4.9.4" @@ -1879,6 +2149,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] +[[package]] +name = "psycopg" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/0a/cac9fdf1df16a269ba0e5f0f06cac61f826c94cadb39df028cdfe19d3a33/psycopg_binary-3.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d", size = 4590414, upload-time = "2026-02-18T16:50:01.441Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c0/d8f8508fbf440edbc0099b1abff33003cd80c9e66eb3a1e78834e3fb4fb9/psycopg_binary-3.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1", size = 4669021, upload-time = "2026-02-18T16:50:08.803Z" }, + { url = "https://files.pythonhosted.org/packages/04/05/097016b77e343b4568feddf12c72171fc513acef9a4214d21b9478569068/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925", size = 5467453, upload-time = "2026-02-18T16:50:14.985Z" }, + { url = "https://files.pythonhosted.org/packages/91/23/73244e5feb55b5ca109cede6e97f32ef45189f0fdac4c80d75c99862729d/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d", size = 5151135, upload-time = "2026-02-18T16:50:24.82Z" }, + { url = "https://files.pythonhosted.org/packages/11/49/5309473b9803b207682095201d8708bbc7842ddf3f192488a69204e36455/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1", size = 6737315, upload-time = "2026-02-18T16:50:35.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5d/03abe74ef34d460b33c4d9662bf6ec1dd38888324323c1a1752133c10377/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482", size = 4979783, upload-time = "2026-02-18T16:50:42.067Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/3fbf8e604e15f2f3752900434046c00c90bb8764305a1b81112bff30ba24/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12", size = 4509023, upload-time = "2026-02-18T16:50:50.116Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6b/1a06b43b7c7af756c80b67eac8bfaa51d77e68635a8a8d246e4f0bb7604a/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83", size = 4185874, upload-time = "2026-02-18T16:50:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d3/bf49e3dcaadba510170c8d111e5e69e5ae3f981c1554c5bb71c75ce354bb/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508", size = 3925668, upload-time = "2026-02-18T16:51:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/f8/92/0aac830ed6a944fe334404e1687a074e4215630725753f0e3e9a9a595b62/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1", size = 4234973, upload-time = "2026-02-18T16:51:09.097Z" }, + { url = "https://files.pythonhosted.org/packages/2e/96/102244653ee5a143ece5afe33f00f52fe64e389dfce8dbc87580c6d70d3d/psycopg_binary-3.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b", size = 3551342, upload-time = "2026-02-18T16:51:13.892Z" }, + { url = "https://files.pythonhosted.org/packages/a2/71/7a57e5b12275fe7e7d84d54113f0226080423a869118419c9106c083a21c/psycopg_binary-3.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2", size = 4607368, upload-time = "2026-02-18T16:51:19.295Z" }, + { url = "https://files.pythonhosted.org/packages/c7/04/cb834f120f2b2c10d4003515ef9ca9d688115b9431735e3936ae48549af8/psycopg_binary-3.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd", size = 4687047, upload-time = "2026-02-18T16:51:23.84Z" }, + { url = "https://files.pythonhosted.org/packages/40/e9/47a69692d3da9704468041aa5ed3ad6fc7f6bb1a5ae788d261a26bbca6c7/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430", size = 5487096, upload-time = "2026-02-18T16:51:29.645Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b6/0e0dd6a2f802864a4ae3dbadf4ec620f05e3904c7842b326aafc43e5f464/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b", size = 5168720, upload-time = "2026-02-18T16:51:36.499Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0d/977af38ac19a6b55d22dff508bd743fd7c1901e1b73657e7937c7cccb0a3/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead", size = 6762076, upload-time = "2026-02-18T16:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/34/40/912a39d48322cf86895c0eaf2d5b95cb899402443faefd4b09abbba6b6e1/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6", size = 4997623, upload-time = "2026-02-18T16:51:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/98/0c/c14d0e259c65dc7be854d926993f151077887391d5a081118907a9d89603/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e", size = 4532096, upload-time = "2026-02-18T16:51:51.421Z" }, + { url = "https://files.pythonhosted.org/packages/39/21/8b7c50a194cfca6ea0fd4d1f276158307785775426e90700ab2eba5cd623/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023", size = 4208884, upload-time = "2026-02-18T16:51:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/a4981bf42cf30ebba0424971d7ce70a222ae9b82594c42fc3f2105d7b525/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d", size = 3944542, upload-time = "2026-02-18T16:52:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/e9/b7c29b56aa0b85a4e0c4d89db691c1ceef08f46a356369144430c155a2f5/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856", size = 4254339, upload-time = "2026-02-18T16:52:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" }, +] + [[package]] name = "py-key-value-aio" version = "0.4.4" @@ -2045,6 +2361,9 @@ vertexai = [ { name = "google-auth" }, { name = "requests" }, ] +voyageai = [ + { name = "voyageai", marker = "python_full_version < '3.14'" }, +] xai = [ { name = "xai-sdk" }, ] @@ -2421,6 +2740,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + [[package]] name = "rich" version = "14.3.3" @@ -2590,6 +2921,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "sqlite-vec" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb", size = 131171, upload-time = "2026-03-31T08:02:31.717Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c", size = 165434, upload-time = "2026-03-31T08:02:32.712Z" }, + { url = "https://files.pythonhosted.org/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9", size = 160076, upload-time = "2026-03-31T08:02:33.796Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786", size = 163388, upload-time = "2026-03-31T08:02:34.888Z" }, + { url = "https://files.pythonhosted.org/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32", size = 292804, upload-time = "2026-03-31T08:02:36.035Z" }, +] + [[package]] name = "sse-starlette" version = "3.3.3" @@ -2790,6 +3133,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2026.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, +] + [[package]] name = "uncalled-for" version = "0.2.0" @@ -2808,6 +3160,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] +[[package]] +name = "uuid-utils" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, + { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, + { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, + { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, + { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, +] + [[package]] name = "uvicorn" version = "0.42.0" @@ -2821,6 +3195,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, ] +[[package]] +name = "voyageai" +version = "0.3.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "python_full_version < '3.14'" }, + { name = "aiolimiter", marker = "python_full_version < '3.14'" }, + { name = "ffmpeg-python", marker = "python_full_version < '3.14'" }, + { name = "langchain-text-splitters", marker = "python_full_version < '3.14'" }, + { name = "numpy", marker = "python_full_version < '3.14'" }, + { name = "pillow", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "tenacity", marker = "python_full_version < '3.14'" }, + { name = "tokenizers", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/16/1b46b3cd401e1717a68197c1fe336d7bb4e0a1833f8105e1738f5b1add05/voyageai-0.3.7.tar.gz", hash = "sha256:826cd97f97223f42b5babc5c459c9c80f3a8215ce5c0e007b0b276550f790d24", size = 26485, upload-time = "2025-12-16T18:43:05.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/64/89f6325666d6836979f94ac88b96fefc7527e02e61abc81359843585e088/voyageai-0.3.7-py3-none-any.whl", hash = "sha256:909f6c033001e5a3b3caf970525bf3614a1bfef9003cf3c3b68207dfdb53e86d", size = 34691, upload-time = "2025-12-16T18:43:04.073Z" }, +] + [[package]] name = "watchfiles" version = "1.1.1" @@ -2981,6 +3376,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/5d/62c394d46e56e43989b34977b490f84bd60ff715e8ea696880f63546b8be/xai_sdk-1.9.1-py3-none-any.whl", hash = "sha256:3f313f1238d847ec08401894c42bf91f034fdeae04fe279c974b0e2a9644573d", size = 247192, upload-time = "2026-03-19T22:57:45.447Z" }, ] +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, +] + [[package]] name = "yarl" version = "1.23.0" @@ -3075,3 +3538,43 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50e wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]