diff --git a/Dockerfile b/Dockerfile index d2199e7..18f3a77 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,5 +12,6 @@ COPY . . RUN uv sync --locked --no-dev ENV PYTHONUNBUFFERED=1 +ENV VIRTUAL_ENV=/app/.venv ENTRYPOINT ["/app/.venv/bin/nextcloud-mcp-server", "--host", "0.0.0.0"] diff --git a/docker-compose.yml b/docker-compose.yml index 15161c4..066e56c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,6 +74,8 @@ services: depends_on: app: condition: service_healthy + qdrant: + condition: service_healthy ports: - 127.0.0.1:8000:8000 environment: @@ -81,6 +83,21 @@ services: - NEXTCLOUD_USERNAME=admin - NEXTCLOUD_PASSWORD=admin + # Vector sync configuration (ADR-007) + - VECTOR_SYNC_ENABLED=true + - VECTOR_SYNC_SCAN_INTERVAL=3600 + - VECTOR_SYNC_PROCESSOR_WORKERS=3 + + # Qdrant configuration + - QDRANT_URL=http://qdrant:6333 + - QDRANT_API_KEY=${QDRANT_API_KEY:-my_secret_api_key} + - QDRANT_COLLECTION=nextcloud_content + + # Ollama configuration + - OLLAMA_BASE_URL=https://ollama.internal.coutinho.io:443 + - OLLAMA_EMBEDDING_MODEL=nomic-embed-text + - OLLAMA_VERIFY_SSL=true + mcp-oauth: build: . command: ["--transport", "streamable-http", "--oauth", "--port", "8001", "--oauth-token-type", "jwt"] @@ -183,6 +200,22 @@ services: - keycloak-tokens:/app/data - keycloak-oauth-storage:/app/.oauth + qdrant: + image: qdrant/qdrant:latest + restart: always + ports: + - 127.0.0.1:6333:6333 # REST API + - 127.0.0.1:6334:6334 # gRPC (optional) + volumes: + - qdrant-data:/qdrant/storage + environment: + - QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY:-my_secret_api_key} + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:6333/readyz || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + volumes: nextcloud: db: @@ -190,3 +223,4 @@ volumes: oauth-tokens: keycloak-tokens: keycloak-oauth-storage: + qdrant-data: diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py index 197301c..d0a63e5 100644 --- a/nextcloud_mcp_server/app.py +++ b/nextcloud_mcp_server/app.py @@ -1,3 +1,4 @@ +import asyncio import logging import os from collections.abc import AsyncIterator @@ -8,6 +9,7 @@ from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from nextcloud_mcp_server.auth.refresh_token_storage import RefreshTokenStorage +import anyio import click import httpx import uvicorn @@ -32,6 +34,7 @@ from nextcloud_mcp_server.client import NextcloudClient from nextcloud_mcp_server.config import ( LOGGING_CONFIG, get_document_processor_config, + get_settings, setup_logging, ) from nextcloud_mcp_server.context import get_client as get_nextcloud_client @@ -47,6 +50,7 @@ from nextcloud_mcp_server.server import ( configure_webdav_tools, ) from nextcloud_mcp_server.server.oauth_tools import register_oauth_tools +from nextcloud_mcp_server.vector import processor_task, scanner_task logger = logging.getLogger(__name__) @@ -206,6 +210,9 @@ class AppContext: """Application context for BasicAuth mode.""" client: NextcloudClient + document_queue: Optional[asyncio.Queue] = None + shutdown_event: Optional[anyio.Event] = None + scanner_wake_event: Optional[anyio.Event] = None @dataclass @@ -369,6 +376,9 @@ async def app_lifespan_basic(server: FastMCP) -> AsyncIterator[AppContext]: Creates a single Nextcloud client with basic authentication that is shared across all requests. + + If vector sync is enabled (VECTOR_SYNC_ENABLED=true), also starts + background tasks for automatic document indexing (ADR-007). """ logger.info("Starting MCP server in BasicAuth mode") logger.info("Creating Nextcloud client with BasicAuth") @@ -379,11 +389,74 @@ async def app_lifespan_basic(server: FastMCP) -> AsyncIterator[AppContext]: # Initialize document processors initialize_document_processors() - try: - yield AppContext(client=client) - finally: - logger.info("Shutting down BasicAuth mode") - await client.close() + settings = get_settings() + + # Check if vector sync is enabled + if settings.vector_sync_enabled: + logger.info("Vector sync enabled - starting background tasks") + + # Get username from environment for BasicAuth mode + username = os.getenv("NEXTCLOUD_USERNAME") + if not username: + raise ValueError( + "NEXTCLOUD_USERNAME is required for vector sync in BasicAuth mode" + ) + + # Initialize shared state + document_queue = asyncio.Queue(maxsize=settings.vector_sync_queue_max_size) + shutdown_event = anyio.Event() + scanner_wake_event = anyio.Event() + + # Start background tasks using anyio TaskGroup + async with anyio.create_task_group() as tg: + # Start scanner task + tg.start_soon( + scanner_task, + document_queue, + shutdown_event, + scanner_wake_event, + client, + username, + ) + + # Start processor pool + for i in range(settings.vector_sync_processor_workers): + tg.start_soon( + processor_task, + i, + document_queue, + shutdown_event, + client, + username, + ) + + logger.info( + f"Background sync tasks started: 1 scanner + {settings.vector_sync_processor_workers} processors" + ) + + # Yield with background tasks running + try: + yield AppContext( + client=client, + document_queue=document_queue, + shutdown_event=shutdown_event, + scanner_wake_event=scanner_wake_event, + ) + finally: + # Shutdown signal + logger.info("Shutting down background sync tasks") + shutdown_event.set() + + # TaskGroup automatically cancels all tasks on exit + logger.info("Background sync tasks stopped") + await client.close() + else: + # No vector sync - simple lifecycle + try: + yield AppContext(client=client) + finally: + logger.info("Shutting down BasicAuth mode") + await client.close() async def setup_oauth_config(): @@ -946,7 +1019,7 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): """Readiness probe endpoint. Returns 200 OK if the application is ready to serve traffic. - Checks that required configuration is present. + Checks that required configuration is present and Qdrant if vector sync enabled. """ checks = {} is_ready = True @@ -976,6 +1049,24 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): checks["auth_configured"] = "error: credentials not set" is_ready = False + # Check Qdrant status if vector sync is enabled + vector_sync_enabled = ( + os.getenv("VECTOR_SYNC_ENABLED", "false").lower() == "true" + ) + if vector_sync_enabled: + try: + qdrant_url = os.getenv("QDRANT_URL", "http://qdrant:6333") + async with httpx.AsyncClient(timeout=2.0) as client: + response = await client.get(f"{qdrant_url}/readyz") + if response.status_code == 200: + checks["qdrant"] = "ok" + else: + checks["qdrant"] = f"error: status {response.status_code}" + is_ready = False + except Exception as e: + checks["qdrant"] = f"error: {str(e)}" + is_ready = False + status_code = 200 if is_ready else 503 return JSONResponse( { diff --git a/nextcloud_mcp_server/auth/client_registration.py b/nextcloud_mcp_server/auth/client_registration.py index 44451a9..f4e3797 100644 --- a/nextcloud_mcp_server/auth/client_registration.py +++ b/nextcloud_mcp_server/auth/client_registration.py @@ -79,19 +79,22 @@ async def register_client( client_name: str = "Nextcloud MCP Server", redirect_uris: list[str] | None = None, scopes: str = "openid profile email", - token_type: str = "Bearer", + token_type: str | None = "Bearer", resource_url: str | None = None, ) -> ClientInfo: """ - Register a new OAuth client with Nextcloud OIDC using dynamic client registration. + Register a new OAuth client using RFC 7591 Dynamic Client Registration. + + This function supports both Nextcloud OIDC and standard OIDC providers like Keycloak. Args: - nextcloud_url: Base URL of the Nextcloud instance + nextcloud_url: Base URL of the OIDC provider registration_endpoint: Full URL to the registration endpoint client_name: Name of the client application redirect_uris: List of redirect URIs (default: http://localhost:8000/oauth/callback) scopes: Space-separated list of scopes to request - token_type: Type of access tokens to issue (default: "Bearer", also supports "JWT") + token_type: Type of access tokens (default: "Bearer", supports "JWT" for Nextcloud). + Set to None to omit this field (required for Keycloak and other standard providers). resource_url: OAuth 2.0 Protected Resource URL (RFC 9728) - used for token introspection authorization Returns: @@ -100,6 +103,11 @@ async def register_client( Raises: httpx.HTTPStatusError: If registration fails ValueError: If response is invalid + + Note: + The token_type parameter is a Nextcloud-specific extension and is not part of RFC 7591. + Standard OIDC providers like Keycloak do not accept this field and will return a 400 error + if it's included. Set token_type=None when registering with Keycloak or other standard providers. """ if redirect_uris is None: redirect_uris = ["http://localhost:8000/oauth/callback"] @@ -111,9 +119,12 @@ async def register_client( "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "scope": scopes, - "token_type": token_type, } + # Add token_type if provided (Nextcloud-specific, not RFC 7591 standard) + if token_type is not None: + client_metadata["token_type"] = token_type + # Add resource_url if provided (RFC 9728) if resource_url: client_metadata["resource_url"] = resource_url diff --git a/nextcloud_mcp_server/config.py b/nextcloud_mcp_server/config.py index 73d86e4..da05108 100644 --- a/nextcloud_mcp_server/config.py +++ b/nextcloud_mcp_server/config.py @@ -156,6 +156,22 @@ class Settings: token_encryption_key: Optional[str] = None token_storage_db: Optional[str] = None + # Vector sync settings (ADR-007) + vector_sync_enabled: bool = False + vector_sync_scan_interval: int = 3600 # seconds + vector_sync_processor_workers: int = 3 + vector_sync_queue_max_size: int = 10000 + + # Qdrant settings + qdrant_url: str = "http://qdrant:6333" + qdrant_api_key: Optional[str] = None + qdrant_collection: str = "nextcloud_content" + + # Ollama settings (for embeddings) + ollama_base_url: Optional[str] = None + ollama_embedding_model: str = "nomic-embed-text" + ollama_verify_ssl: bool = True + def get_settings() -> Settings: """Get application settings from environment variables. @@ -192,4 +208,23 @@ def get_settings() -> Settings: # Token settings token_encryption_key=os.getenv("TOKEN_ENCRYPTION_KEY"), token_storage_db=os.getenv("TOKEN_STORAGE_DB", "/tmp/tokens.db"), + # Vector sync settings (ADR-007) + vector_sync_enabled=( + os.getenv("VECTOR_SYNC_ENABLED", "false").lower() == "true" + ), + vector_sync_scan_interval=int(os.getenv("VECTOR_SYNC_SCAN_INTERVAL", "3600")), + vector_sync_processor_workers=int( + os.getenv("VECTOR_SYNC_PROCESSOR_WORKERS", "3") + ), + vector_sync_queue_max_size=int( + os.getenv("VECTOR_SYNC_QUEUE_MAX_SIZE", "10000") + ), + # Qdrant settings + qdrant_url=os.getenv("QDRANT_URL", "http://qdrant:6333"), + qdrant_api_key=os.getenv("QDRANT_API_KEY"), + qdrant_collection=os.getenv("QDRANT_COLLECTION", "nextcloud_content"), + # Ollama settings + ollama_base_url=os.getenv("OLLAMA_BASE_URL"), + ollama_embedding_model=os.getenv("OLLAMA_EMBEDDING_MODEL", "nomic-embed-text"), + ollama_verify_ssl=os.getenv("OLLAMA_VERIFY_SSL", "true").lower() == "true", ) diff --git a/nextcloud_mcp_server/embedding/__init__.py b/nextcloud_mcp_server/embedding/__init__.py new file mode 100644 index 0000000..3b06aba --- /dev/null +++ b/nextcloud_mcp_server/embedding/__init__.py @@ -0,0 +1,5 @@ +"""Embedding service package for generating vector embeddings.""" + +from .service import EmbeddingService, get_embedding_service + +__all__ = ["EmbeddingService", "get_embedding_service"] diff --git a/nextcloud_mcp_server/embedding/base.py b/nextcloud_mcp_server/embedding/base.py new file mode 100644 index 0000000..b17e264 --- /dev/null +++ b/nextcloud_mcp_server/embedding/base.py @@ -0,0 +1,43 @@ +"""Abstract base class for embedding providers.""" + +from abc import ABC, abstractmethod + + +class EmbeddingProvider(ABC): + """Base class for embedding providers.""" + + @abstractmethod + async def embed(self, text: str) -> list[float]: + """ + Generate embedding vector for text. + + Args: + text: Input text to embed + + Returns: + Vector embedding as list of floats + """ + pass + + @abstractmethod + async def embed_batch(self, texts: list[str]) -> list[list[float]]: + """ + Generate embeddings for multiple texts (optimized). + + Args: + texts: List of texts to embed + + Returns: + List of vector embeddings + """ + pass + + @abstractmethod + def get_dimension(self) -> int: + """ + Get embedding dimension for this provider. + + Returns: + Vector dimension (e.g., 768 for nomic-embed-text) + """ + pass diff --git a/nextcloud_mcp_server/embedding/ollama_provider.py b/nextcloud_mcp_server/embedding/ollama_provider.py new file mode 100644 index 0000000..6050e8b --- /dev/null +++ b/nextcloud_mcp_server/embedding/ollama_provider.py @@ -0,0 +1,85 @@ +"""Ollama embedding provider.""" + +import logging + +import httpx + +from .base import EmbeddingProvider + +logger = logging.getLogger(__name__) + + +class OllamaEmbeddingProvider(EmbeddingProvider): + """Ollama embedding provider with TLS support.""" + + def __init__( + self, + base_url: str, + model: str = "nomic-embed-text", + verify_ssl: bool = True, + ): + """ + Initialize Ollama embedding provider. + + Args: + base_url: Ollama API base URL (e.g., https://ollama.internal.coutinho.io:443) + model: Embedding model name (default: nomic-embed-text) + verify_ssl: Verify SSL certificates (default: True) + """ + self.base_url = base_url.rstrip("/") + self.model = model + self.verify_ssl = verify_ssl + self.client = httpx.AsyncClient(verify=verify_ssl, timeout=30.0) + self._dimension = 768 # nomic-embed-text default + logger.info( + f"Initialized Ollama provider: {base_url} (model={model}, verify_ssl={verify_ssl})" + ) + + async def embed(self, text: str) -> list[float]: + """ + Generate embedding vector for text. + + Args: + text: Input text to embed + + Returns: + Vector embedding as list of floats + """ + response = await self.client.post( + f"{self.base_url}/api/embeddings", + json={"model": self.model, "prompt": text}, + ) + response.raise_for_status() + return response.json()["embedding"] + + async def embed_batch(self, texts: list[str]) -> list[list[float]]: + """ + Generate embeddings for multiple texts (batched requests). + + Note: Ollama doesn't have native batch API, so we send requests sequentially. + For better performance with large batches, consider using asyncio.gather(). + + Args: + texts: List of texts to embed + + Returns: + List of vector embeddings + """ + embeddings = [] + for text in texts: + embedding = await self.embed(text) + embeddings.append(embedding) + return embeddings + + def get_dimension(self) -> int: + """ + Get embedding dimension. + + Returns: + Vector dimension (768 for nomic-embed-text) + """ + return self._dimension + + async def close(self): + """Close HTTP client.""" + await self.client.aclose() diff --git a/nextcloud_mcp_server/embedding/service.py b/nextcloud_mcp_server/embedding/service.py new file mode 100644 index 0000000..758744a --- /dev/null +++ b/nextcloud_mcp_server/embedding/service.py @@ -0,0 +1,102 @@ +"""Embedding service with provider detection.""" + +import logging +import os + +from .base import EmbeddingProvider +from .ollama_provider import OllamaEmbeddingProvider + +logger = logging.getLogger(__name__) + + +class EmbeddingService: + """Unified embedding service with automatic provider detection.""" + + def __init__(self): + """Initialize embedding service with auto-detected provider.""" + self.provider = self._detect_provider() + + def _detect_provider(self) -> EmbeddingProvider: + """ + Auto-detect available embedding provider. + + Checks environment variables in order: + 1. OLLAMA_BASE_URL - Use Ollama provider + + Returns: + Configured embedding provider + + Raises: + ValueError: If no embedding provider is configured + """ + # Ollama provider (for this deployment) + ollama_url = os.getenv("OLLAMA_BASE_URL") + if ollama_url: + return OllamaEmbeddingProvider( + base_url=ollama_url, + model=os.getenv("OLLAMA_EMBEDDING_MODEL", "nomic-embed-text"), + verify_ssl=os.getenv("OLLAMA_VERIFY_SSL", "true").lower() == "true", + ) + + raise ValueError( + "No embedding provider configured. " + "Set OLLAMA_BASE_URL environment variable." + ) + + async def embed(self, text: str) -> list[float]: + """ + Generate embedding vector for text. + + Args: + text: Input text to embed + + Returns: + Vector embedding as list of floats + """ + return await self.provider.embed(text) + + async def embed_batch(self, texts: list[str]) -> list[list[float]]: + """ + Generate embeddings for multiple texts. + + Args: + texts: List of texts to embed + + Returns: + List of vector embeddings + """ + return await self.provider.embed_batch(texts) + + def get_dimension(self) -> int: + """ + Get embedding dimension. + + Returns: + Vector dimension + """ + return self.provider.get_dimension() + + async def close(self): + """Close provider resources.""" + if hasattr(self.provider, "close") and callable( + getattr(self.provider, "close") + ): + close_method = getattr(self.provider, "close") + await close_method() + + +# Singleton instance +_embedding_service: EmbeddingService | None = None + + +def get_embedding_service() -> EmbeddingService: + """ + Get singleton embedding service instance. + + Returns: + Global EmbeddingService instance + """ + global _embedding_service + if _embedding_service is None: + _embedding_service = EmbeddingService() + return _embedding_service diff --git a/nextcloud_mcp_server/vector/__init__.py b/nextcloud_mcp_server/vector/__init__.py new file mode 100644 index 0000000..00c11cb --- /dev/null +++ b/nextcloud_mcp_server/vector/__init__.py @@ -0,0 +1,16 @@ +"""Vector database and background sync package.""" + +from .document_chunker import DocumentChunker +from .processor import process_document, processor_task +from .qdrant_client import get_qdrant_client +from .scanner import DocumentTask, scan_user_documents, scanner_task + +__all__ = [ + "get_qdrant_client", + "DocumentChunker", + "scanner_task", + "scan_user_documents", + "DocumentTask", + "processor_task", + "process_document", +] diff --git a/nextcloud_mcp_server/vector/document_chunker.py b/nextcloud_mcp_server/vector/document_chunker.py new file mode 100644 index 0000000..5855154 --- /dev/null +++ b/nextcloud_mcp_server/vector/document_chunker.py @@ -0,0 +1,51 @@ +"""Document chunking for large texts.""" + +import logging + +logger = logging.getLogger(__name__) + + +class DocumentChunker: + """Chunk large documents for optimal embedding.""" + + def __init__(self, chunk_size: int = 512, overlap: int = 50): + """ + Initialize document chunker. + + Args: + chunk_size: Number of words per chunk (default: 512) + overlap: Number of overlapping words between chunks (default: 50) + """ + self.chunk_size = chunk_size + self.overlap = overlap + + def chunk_text(self, content: str) -> list[str]: + """ + Split text into overlapping chunks. + + Uses simple word-based chunking with configurable overlap to preserve + context across chunk boundaries. + + Args: + content: Text content to chunk + + Returns: + List of text chunks (may be single item if content is small) + """ + # Simple word-based chunking + words = content.split() + + if len(words) <= self.chunk_size: + return [content] + + chunks = [] + start = 0 + + while start < len(words): + end = start + self.chunk_size + chunk_words = words[start:end] + chunks.append(" ".join(chunk_words)) + start = end - self.overlap + + logger.debug(f"Chunked document into {len(chunks)} chunks ({len(words)} words)") + return chunks diff --git a/nextcloud_mcp_server/vector/processor.py b/nextcloud_mcp_server/vector/processor.py new file mode 100644 index 0000000..defc1d4 --- /dev/null +++ b/nextcloud_mcp_server/vector/processor.py @@ -0,0 +1,219 @@ +"""Processor task for vector database synchronization. + +Processes documents from queue: fetches content, generates embeddings, stores in Qdrant. +""" + +import asyncio +import logging +import time + +import anyio +from httpx import HTTPStatusError +from qdrant_client.models import FieldCondition, Filter, MatchValue, PointStruct + +from nextcloud_mcp_server.client import NextcloudClient +from nextcloud_mcp_server.config import get_settings +from nextcloud_mcp_server.embedding import get_embedding_service +from nextcloud_mcp_server.vector.document_chunker import DocumentChunker +from nextcloud_mcp_server.vector.qdrant_client import get_qdrant_client +from nextcloud_mcp_server.vector.scanner import DocumentTask + +logger = logging.getLogger(__name__) + + +async def processor_task( + worker_id: int, + document_queue: asyncio.Queue, + shutdown_event: anyio.Event, + nc_client: NextcloudClient, + user_id: str, +): + """ + Process documents from queue concurrently. + + Each processor task runs in a loop: + 1. Pull document from queue (with timeout) + 2. Fetch content from Nextcloud + 3. Tokenize and chunk text + 4. Generate embeddings (I/O bound - external API) + 5. Upload vectors to Qdrant + 6. Mark task complete + + Multiple processors run concurrently for I/O parallelism. + + Args: + worker_id: Worker identifier for logging + document_queue: Queue to pull documents from + shutdown_event: Event signaling shutdown + nc_client: Authenticated Nextcloud client + user_id: User being processed + """ + logger.info(f"Processor {worker_id} started") + + while not shutdown_event.is_set(): + try: + # Get document with timeout (allows checking shutdown) + doc_task = await asyncio.wait_for( + document_queue.get(), + timeout=1.0, + ) + + # Process document + await process_document(doc_task, nc_client) + + # Mark complete + document_queue.task_done() + + except asyncio.TimeoutError: + # No documents available, continue + continue + + except Exception as e: + logger.error( + f"Processor {worker_id} error processing " + f"{doc_task.doc_type}_{doc_task.doc_id}: {e}", + exc_info=True, + ) + # Mark task done even on error to prevent queue blocking + try: + document_queue.task_done() + except ValueError: + pass + + logger.info(f"Processor {worker_id} stopped") + + +async def process_document(doc_task: DocumentTask, nc_client: NextcloudClient): + """ + Process a single document: fetch, tokenize, embed, store in Qdrant. + + Implements retry logic with exponential backoff for transient failures. + + Args: + doc_task: Document task to process + nc_client: Authenticated Nextcloud client + """ + logger.debug( + f"Processing {doc_task.doc_type}_{doc_task.doc_id} " + f"for {doc_task.user_id} ({doc_task.operation})" + ) + + qdrant_client = await get_qdrant_client() + settings = get_settings() + + # Handle deletion + if doc_task.operation == "delete": + await qdrant_client.delete( + collection_name=settings.qdrant_collection, + points_selector=Filter( + must=[ + FieldCondition( + key="user_id", + match=MatchValue(value=doc_task.user_id), + ), + FieldCondition( + key="doc_id", + match=MatchValue(value=doc_task.doc_id), + ), + FieldCondition( + key="doc_type", + match=MatchValue(value=doc_task.doc_type), + ), + ] + ), + ) + logger.info( + f"Deleted {doc_task.doc_type}_{doc_task.doc_id} for {doc_task.user_id}" + ) + return + + # Handle indexing with retry + max_retries = 3 + retry_delay = 1.0 + + for attempt in range(max_retries): + try: + await _index_document(doc_task, nc_client, qdrant_client) + return # Success + + except (HTTPStatusError, Exception) as e: + if attempt < max_retries - 1: + logger.warning( + f"Retry {attempt + 1}/{max_retries} for " + f"{doc_task.doc_type}_{doc_task.doc_id}: {e}" + ) + await anyio.sleep(retry_delay) + retry_delay *= 2 # Exponential backoff + else: + logger.error( + f"Failed to index {doc_task.doc_type}_{doc_task.doc_id} " + f"after {max_retries} retries: {e}" + ) + raise + + +async def _index_document( + doc_task: DocumentTask, nc_client: NextcloudClient, qdrant_client +): + """ + Index a single document (called by process_document with retry). + + Args: + doc_task: Document task to index + nc_client: Authenticated Nextcloud client + qdrant_client: Qdrant client instance + """ + settings = get_settings() + + # Fetch document content + if doc_task.doc_type == "note": + document = await nc_client.notes.get_note(int(doc_task.doc_id)) + content = f"{document['title']}\n\n{document['content']}" + title = document["title"] + etag = document.get("etag", "") + else: + raise ValueError(f"Unsupported doc_type: {doc_task.doc_type}") + + # Tokenize and chunk + chunker = DocumentChunker(chunk_size=512, overlap=50) + chunks = chunker.chunk_text(content) + + # Generate embeddings (I/O bound - external API call) + embedding_service = get_embedding_service() + embeddings = await embedding_service.embed_batch(chunks) + + # Prepare Qdrant points + indexed_at = int(time.time()) + points = [] + + for i, (chunk, embedding) in enumerate(zip(chunks, embeddings)): + points.append( + PointStruct( + id=f"{doc_task.doc_type}_{doc_task.doc_id}_{i}", + vector=embedding, + payload={ + "user_id": doc_task.user_id, + "doc_id": doc_task.doc_id, + "doc_type": doc_task.doc_type, + "title": title, + "excerpt": chunk[:200], + "indexed_at": indexed_at, + "modified_at": doc_task.modified_at, + "etag": etag, + "chunk_index": i, + "total_chunks": len(chunks), + }, + ) + ) + + # Upsert to Qdrant + await qdrant_client.upsert( + collection_name=settings.qdrant_collection, + points=points, + wait=True, + ) + + logger.info( + f"Indexed {doc_task.doc_type}_{doc_task.doc_id} for {doc_task.user_id} " + f"({len(chunks)} chunks)" + ) diff --git a/nextcloud_mcp_server/vector/qdrant_client.py b/nextcloud_mcp_server/vector/qdrant_client.py new file mode 100644 index 0000000..733d769 --- /dev/null +++ b/nextcloud_mcp_server/vector/qdrant_client.py @@ -0,0 +1,66 @@ +"""Qdrant client wrapper.""" + +import logging +import os + +from qdrant_client import AsyncQdrantClient +from qdrant_client.models import Distance, VectorParams + +logger = logging.getLogger(__name__) + + +# Singleton instance +_qdrant_client: AsyncQdrantClient | None = None + + +async def get_qdrant_client() -> AsyncQdrantClient: + """ + Get singleton Qdrant client instance. + + Automatically creates collection on first use if it doesn't exist. + + Returns: + Configured AsyncQdrantClient instance + + Raises: + Exception: If Qdrant connection fails or collection creation fails + """ + global _qdrant_client + + if _qdrant_client is None: + url = os.getenv("QDRANT_URL", "http://qdrant:6333") + api_key = os.getenv("QDRANT_API_KEY") + + _qdrant_client = AsyncQdrantClient( + url=url, + api_key=api_key, + timeout=30, + ) + + # Ensure collection exists + collection_name = os.getenv("QDRANT_COLLECTION", "nextcloud_content") + + # Import here to avoid circular dependency + from nextcloud_mcp_server.embedding import get_embedding_service + + embedding_service = get_embedding_service() + dimension = embedding_service.get_dimension() + + try: + await _qdrant_client.get_collection(collection_name) + logger.info(f"Using existing Qdrant collection: {collection_name}") + except Exception: + # Collection doesn't exist, create it + await _qdrant_client.create_collection( + collection_name=collection_name, + vectors_config=VectorParams( + size=dimension, + distance=Distance.COSINE, + ), + ) + logger.info( + f"Created Qdrant collection: {collection_name} " + f"(dimension={dimension}, distance=COSINE)" + ) + + return _qdrant_client diff --git a/nextcloud_mcp_server/vector/scanner.py b/nextcloud_mcp_server/vector/scanner.py new file mode 100644 index 0000000..aa5c682 --- /dev/null +++ b/nextcloud_mcp_server/vector/scanner.py @@ -0,0 +1,172 @@ +"""Scanner task for vector database synchronization. + +Periodically scans enabled users' content and queues changed documents for processing. +""" + +import asyncio +import logging +from dataclasses import dataclass + +import anyio +from qdrant_client.models import FieldCondition, Filter, MatchValue + +from nextcloud_mcp_server.client import NextcloudClient +from nextcloud_mcp_server.config import get_settings +from nextcloud_mcp_server.vector.qdrant_client import get_qdrant_client + +logger = logging.getLogger(__name__) + + +@dataclass +class DocumentTask: + """Document task for processing queue.""" + + user_id: str + doc_id: str + doc_type: str # "note", "file", "calendar" + operation: str # "index" or "delete" + modified_at: int + + +async def scanner_task( + document_queue: asyncio.Queue, + shutdown_event: anyio.Event, + wake_event: anyio.Event, + nc_client: NextcloudClient, + user_id: str, +): + """ + Periodic scanner that detects changed documents for enabled user. + + For BasicAuth mode, scans a single user with credentials available at runtime. + + Args: + document_queue: Queue to enqueue changed documents + shutdown_event: Event signaling shutdown + wake_event: Event to trigger immediate scan + nc_client: Authenticated Nextcloud client + user_id: User to scan + """ + logger.info(f"Scanner task started for user: {user_id}") + settings = get_settings() + + while not shutdown_event.is_set(): + try: + # Scan user documents + await scan_user_documents( + user_id=user_id, + document_queue=document_queue, + nc_client=nc_client, + ) + + except Exception as e: + logger.error(f"Scanner error: {e}", exc_info=True) + + # Sleep until next interval or wake event + try: + with anyio.move_on_after(settings.vector_sync_scan_interval): + # Wait for wake event or shutdown (whichever comes first) + await wake_event.wait() + except anyio.get_cancelled_exc_class(): + # Shutdown, exit loop + break + + logger.info("Scanner task stopped") + + +async def scan_user_documents( + user_id: str, + document_queue: asyncio.Queue, + nc_client: NextcloudClient, + initial_sync: bool = False, +): + """ + Scan a single user's documents and queue changes. + + Args: + user_id: User to scan + document_queue: Queue to enqueue changed documents + nc_client: Authenticated Nextcloud client + initial_sync: If True, queue all documents (first-time sync) + """ + logger.info(f"Scanning documents for user: {user_id}") + + # Fetch all notes from Nextcloud + notes = await nc_client.notes.list_notes() + logger.debug(f"Found {len(notes)} notes for {user_id}") + + if initial_sync: + # Queue everything on first sync + for note in notes: + await document_queue.put( + DocumentTask( + user_id=user_id, + doc_id=str(note["id"]), + doc_type="note", + operation="index", + modified_at=note["modified"], + ) + ) + logger.info(f"Queued {len(notes)} documents for initial sync: {user_id}") + return + + # Get indexed state from Qdrant + qdrant_client = await get_qdrant_client() + scroll_result = await qdrant_client.scroll( + collection_name=get_settings().qdrant_collection, + scroll_filter=Filter( + must=[ + FieldCondition(key="user_id", match=MatchValue(value=user_id)), + FieldCondition(key="doc_type", match=MatchValue(value="note")), + ] + ), + with_payload=["doc_id", "indexed_at"], + with_vectors=False, + limit=10000, + ) + + indexed_docs = { + point.payload["doc_id"]: point.payload["indexed_at"] + for point in scroll_result[0] + } + + logger.debug(f"Found {len(indexed_docs)} indexed documents in Qdrant") + + # Compare and queue changes + queued = 0 + for note in notes: + doc_id = str(note["id"]) + indexed_at = indexed_docs.get(doc_id) + + # Queue if never indexed or modified since last index + if indexed_at is None or note["modified"] > indexed_at: + await document_queue.put( + DocumentTask( + user_id=user_id, + doc_id=doc_id, + doc_type="note", + operation="index", + modified_at=note["modified"], + ) + ) + queued += 1 + + # Check for deleted documents (in Qdrant but not in Nextcloud) + nextcloud_doc_ids = {str(note["id"]) for note in notes} + for doc_id in indexed_docs: + if doc_id not in nextcloud_doc_ids: + await document_queue.put( + DocumentTask( + user_id=user_id, + doc_id=doc_id, + doc_type="note", + operation="delete", + modified_at=0, + ) + ) + queued += 1 + + if queued > 0: + logger.info(f"Queued {queued} documents for incremental sync: {user_id}") + else: + logger.debug(f"No changes detected for {user_id}") diff --git a/pyproject.toml b/pyproject.toml index e48d876..a0da862 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "pyjwt[crypto]>=2.8.0", "aiosqlite>=0.20.0", # Async SQLite for refresh token storage "authlib>=1.6.5", + "qdrant-client>=1.7.0", # Vector database for semantic search ] classifiers = [ "Development Status :: 4 - Beta", diff --git a/uv.lock b/uv.lock index 85bf9f4..0f94096 100644 --- a/uv.lock +++ b/uv.lock @@ -537,6 +537,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, + { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -989,6 +1040,7 @@ dependencies = [ { name = "pydantic" }, { name = "pyjwt", extra = ["crypto"] }, { name = "pythonvcard4" }, + { name = "qdrant-client" }, ] [package.dev-dependencies] @@ -1019,6 +1071,7 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.11.4" }, { name = "pyjwt", extras = ["crypto"], specifier = ">=2.8.0" }, { name = "pythonvcard4", specifier = ">=0.2.0" }, + { name = "qdrant-client", specifier = ">=1.7.0" }, ] [package.metadata.requires-dev] @@ -1036,6 +1089,87 @@ dev = [ { name = "ty", specifier = ">=0.0.1a25" }, ] +[[package]] +name = "numpy" +version = "2.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e7/0e07379944aa8afb49a556a2b54587b828eb41dc9adc56fb7615b678ca53/numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", size = 21259519, upload-time = "2025-10-15T16:15:19.012Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cb/5a69293561e8819b09e34ed9e873b9a82b5f2ade23dce4c51dc507f6cfe1/numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", size = 14452796, upload-time = "2025-10-15T16:15:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/ff11611200acd602a1e5129e36cfd25bf01ad8e5cf927baf2e90236eb02e/numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", size = 5381639, upload-time = "2025-10-15T16:15:25.572Z" }, + { url = "https://files.pythonhosted.org/packages/ea/77/e95c757a6fe7a48d28a009267408e8aa382630cc1ad1db7451b3bc21dbb4/numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", size = 6914296, upload-time = "2025-10-15T16:15:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d2/137c7b6841c942124eae921279e5c41b1c34bab0e6fc60c7348e69afd165/numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", size = 14591904, upload-time = "2025-10-15T16:15:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/bb/32/67e3b0f07b0aba57a078c4ab777a9e8e6bc62f24fb53a2337f75f9691699/numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", size = 16939602, upload-time = "2025-10-15T16:15:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/9639c30e32c93c4cee3ccdb4b09c2d0fbff4dcd06d36b357da06146530fb/numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", size = 16372661, upload-time = "2025-10-15T16:15:33.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/a685079529be2b0156ae0c11b13d6be647743095bb51d46589e95be88086/numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", size = 18884682, upload-time = "2025-10-15T16:15:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/cf/85/f6f00d019b0cc741e64b4e00ce865a57b6bed945d1bbeb1ccadbc647959b/numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", size = 6570076, upload-time = "2025-10-15T16:15:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/7d/10/f8850982021cb90e2ec31990291f9e830ce7d94eef432b15066e7cbe0bec/numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", size = 13089358, upload-time = "2025-10-15T16:15:40.404Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ad/afdd8351385edf0b3445f9e24210a9c3971ef4de8fd85155462fc4321d79/numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", size = 10462292, upload-time = "2025-10-15T16:15:42.896Z" }, + { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, + { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, + { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, + { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, + { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, + { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, + { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, + { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, + { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, + { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, + { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, + { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, + { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, + { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, + { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, + { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, + { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, + { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, + { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, + { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, + { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, + { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b6/64898f51a86ec88ca1257a59c1d7fd077b60082a119affefcdf1dd0df8ca/numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", size = 21131552, upload-time = "2025-10-15T16:17:55.845Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/f135dc6ebe2b6a3c77f4e4838fa63d350f85c99462012306ada1bd4bc460/numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", size = 14377796, upload-time = "2025-10-15T16:17:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a4/f33f9c23fcc13dd8412fc8614559b5b797e0aba9d8e01dfa8bae10c84004/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", size = 5306904, upload-time = "2025-10-15T16:18:00.596Z" }, + { url = "https://files.pythonhosted.org/packages/28/af/c44097f25f834360f9fb960fa082863e0bad14a42f36527b2a121abdec56/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", size = 6819682, upload-time = "2025-10-15T16:18:02.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8c/cd283b54c3c2b77e188f63e23039844f56b23bba1712318288c13fe86baf/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", size = 14422300, upload-time = "2025-10-15T16:18:04.271Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f0/8404db5098d92446b3e3695cf41c6f0ecb703d701cb0b7566ee2177f2eee/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", size = 16760806, upload-time = "2025-10-15T16:18:06.668Z" }, + { url = "https://files.pythonhosted.org/packages/95/8e/2844c3959ce9a63acc7c8e50881133d86666f0420bcde695e115ced0920f/numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", size = 12973130, upload-time = "2025-10-15T16:18:09.397Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -1181,6 +1315,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "portalocker" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.51" @@ -1193,6 +1339,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, ] +[[package]] +name = "protobuf" +version = "6.33.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/ff/64a6c8f420818bb873713988ca5492cba3a7946be57e027ac63495157d97/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", size = 443463, upload-time = "2025-10-15T20:39:52.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/ee/52b3fa8feb6db4a833dfea4943e175ce645144532e8a90f72571ad85df4e/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", size = 425593, upload-time = "2025-10-15T20:39:40.29Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c6/7a465f1825872c55e0341ff4a80198743f73b69ce5d43ab18043699d1d81/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", size = 436882, upload-time = "2025-10-15T20:39:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a9/b6eee662a6951b9c3640e8e452ab3e09f117d99fc10baa32d1581a0d4099/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", size = 427521, upload-time = "2025-10-15T20:39:43.803Z" }, + { url = "https://files.pythonhosted.org/packages/10/35/16d31e0f92c6d2f0e77c2a3ba93185130ea13053dd16200a57434c882f2b/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", size = 324445, upload-time = "2025-10-15T20:39:44.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/2a981a13e35cda8b75b5585aaffae2eb904f8f351bdd3870769692acbd8a/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298", size = 339159, upload-time = "2025-10-15T20:39:46.186Z" }, + { url = "https://files.pythonhosted.org/packages/21/51/0b1cbad62074439b867b4e04cc09b93f6699d78fd191bed2bbb44562e077/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", size = 323172, upload-time = "2025-10-15T20:39:47.465Z" }, + { url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477, upload-time = "2025-10-15T20:39:51.311Z" }, +] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -1598,6 +1759,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "qdrant-client" +version = "1.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "httpx", extra = ["http2"] }, + { name = "numpy" }, + { name = "portalocker" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/8b/76c7d325e11d97cb8eb5e261c3759e9ed6664735afbf32fdded5b580690c/qdrant_client-1.15.1.tar.gz", hash = "sha256:631f1f3caebfad0fd0c1fba98f41be81d9962b7bf3ca653bed3b727c0e0cbe0e", size = 295297, upload-time = "2025-07-31T19:35:19.627Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/33/d8df6a2b214ffbe4138db9a1efe3248f67dc3c671f82308bea1582ecbbb7/qdrant_client-1.15.1-py3-none-any.whl", hash = "sha256:2b975099b378382f6ca1cfb43f0d59e541be6e16a5892f282a4b8de7eff5cb63", size = 337331, upload-time = "2025-07-31T19:35:17.539Z" }, +] + [[package]] name = "questionary" version = "2.1.1"