diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bc23ae0..f2edb5f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,10 +9,14 @@ on: env: REGISTRY: crretoxmas2024.azurecr.io NAMESPACE: reto-xmas-2025-goland-ia-backend + ROLLOUT_TIMEOUT: 60s + READY_CHECK_RETRIES: 20 + READY_CHECK_SLEEP: 15 jobs: build-and-deploy: runs-on: ubuntu-latest + timeout-minutes: 15 strategy: fail-fast: false matrix: @@ -25,7 +29,7 @@ jobs: path: ./RAGManager image: reto-xmas-2025-goland-ia-backend-rag-manager deployment: rag-manager - + steps: - name: Checkout code uses: actions/checkout@v4 @@ -51,6 +55,7 @@ jobs: ${{ env.REGISTRY }}/${{ matrix.service.image }}:${{ github.sha }} cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.service.image }}:buildcache cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.service.image }}:buildcache,mode=max + provenance: false - name: Set up kubectl uses: azure/setup-kubectl@v3 @@ -63,28 +68,78 @@ jobs: echo "${{ secrets.KUBECONFIG }}" | base64 -d > $HOME/.kube/config chmod 600 $HOME/.kube/config - - name: Restart deployment + - name: Update deployment image run: | - kubectl rollout restart deployment/${{ matrix.service.deployment }} -n ${{ env.NAMESPACE }} - kubectl rollout status deployment/${{ matrix.service.deployment }} -n ${{ env.NAMESPACE }} --timeout=5m + kubectl set image deployment/${{ matrix.service.deployment }} \ + api=${{ env.REGISTRY }}/${{ matrix.service.image }}:${{ github.sha }} \ + -n ${{ env.NAMESPACE }} - - name: Verify deployment + - name: Wait for deployment to be ready (robust) run: | - echo "✅ Deployment successful for ${{ matrix.service.name }}" - kubectl get pods -n ${{ env.NAMESPACE }} -l app=${{ matrix.service.deployment }} + set -e + + echo "Checking rollout status (non-blocking)..." + kubectl rollout status deployment/${{ matrix.service.deployment }} \ + -n ${{ env.NAMESPACE }} \ + --timeout=${{ env.ROLLOUT_TIMEOUT }} || true + + echo "Waiting for available replicas..." + + for i in $(seq 1 $READY_CHECK_RETRIES); do + DESIRED=$(kubectl get deployment/${{ matrix.service.deployment }} -n ${{ env.NAMESPACE }} -o jsonpath='{.spec.replicas}') + AVAILABLE=$(kubectl get deployment/${{ matrix.service.deployment }} -n ${{ env.NAMESPACE }} -o jsonpath='{.status.availableReplicas}') + + echo "Attempt $i: $AVAILABLE / $DESIRED replicas available" + + if [ "$AVAILABLE" = "$DESIRED" ]; then + echo "Deployment is ready" + exit 0 + fi + + sleep $READY_CHECK_SLEEP + done + + echo "Deployment did not become ready in time" + kubectl describe deployment/${{ matrix.service.deployment }} -n ${{ env.NAMESPACE }} + exit 1 + + - name: Verify pods + if: success() + run: | + kubectl get pods -n ${{ env.NAMESPACE }} -l app=${{ matrix.service.deployment }} -o wide + + - name: Get logs on failure + if: failure() + run: | + echo "=== Deployment ===" + kubectl describe deployment/${{ matrix.service.deployment }} -n ${{ env.NAMESPACE }} + + echo "=== Pods ===" + kubectl get pods -n ${{ env.NAMESPACE }} -l app=${{ matrix.service.deployment }} -o wide + + echo "=== Pod Logs ===" + kubectl logs -n ${{ env.NAMESPACE }} \ + -l app=${{ matrix.service.deployment }} \ + --tail=100 \ + --all-containers=true \ + --prefix=true || true - name: Deployment Summary if: always() run: | - echo "### 🚀 Deployment Summary" >> $GITHUB_STEP_SUMMARY + STATUS="${{ job.status }}" + + echo "### Deployment - ${{ matrix.service.name }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "**Service:** ${{ matrix.service.name }}" >> $GITHUB_STEP_SUMMARY - echo "**Image:** ${{ env.REGISTRY }}/${{ matrix.service.image }}:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY - echo "**Status:** ${{ job.status }}" >> $GITHUB_STEP_SUMMARY + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Image | \`${{ env.REGISTRY }}/${{ matrix.service.image }}:${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Status | $STATUS |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + echo "#### Pods:" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - kubectl get pods -n ${{ env.NAMESPACE }} -l app=${{ matrix.service.deployment }} >> $GITHUB_STEP_SUMMARY + kubectl get pods -n ${{ env.NAMESPACE }} -l app=${{ matrix.service.deployment }} >> $GITHUB_STEP_SUMMARY || true echo '```' >> $GITHUB_STEP_SUMMARY notify-success: @@ -95,8 +150,34 @@ jobs: steps: - name: Success Summary run: | - echo "### ✅ Deployment Successful!" >> $GITHUB_STEP_SUMMARY + echo "### All Services Deployed" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "All services deployed successfully:" >> $GITHUB_STEP_SUMMARY - echo "- 🌐 DocsManager: https://goland-ia-backend-docs-manager.reto-ucu.net" >> $GITHUB_STEP_SUMMARY - echo "- 🌐 RAGManager: https://goland-ia-backend-rag-manager.reto-ucu.net" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + echo "**Live URLs:**" >> $GITHUB_STEP_SUMMARY + echo "- [DocsManager](https://goland-ia-backend-docs-manager.reto-ucu.net/docs)" >> $GITHUB_STEP_SUMMARY + echo "- [RAGManager](https://goland-ia-backend-rag-manager.reto-ucu.net/docs)" >> $GITHUB_STEP_SUMMARY + + - name: Notificar a Discord (deploy exitoso) + run: | + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{\"content\": \"✅ **Deploy exitoso**\\n\\n**Servicio:** \\\\`${{ matrix.service.name }}\\\\`\\n**Imagen:** \\\\`${{ env.REGISTRY }}/${{ matrix.service.image }}:${{ github.sha }}\\\\`\\n**Namespace:** \\\\`${{ env.NAMESPACE }}\\\\`\\n**Autor:** ${{ github.actor }}\\n**Commit:** [${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})\\n\\n[Ver ejecución en GitHub Actions](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\\n\\n:rocket: ¡Todo salió bien!\"}" \ + https://discord.com/api/webhooks/1449147972330852463/sMs3QYOvQ6oiaPHR4PlBluwNrhWSsVmfm3_Miz6UAFrPxj1SsbqNnHXc5eK9h8ZSaumk + + notify-failure: + name: Deployment Failed + runs-on: ubuntu-latest + needs: [build-and-deploy] + if: failure() + steps: + - name: Failure Summary + run: | + echo "### Deployment Failed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Please check deployment logs above." >> $GITHUB_STEP_SUMMARY + + - name: Notificar a Discord (deploy fallido) + run: | + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{\"content\": \"❌ **Falló el deploy**\\n\\n**Servicio:** \\\\`${{ matrix.service.name }}\\\\`\\n**Imagen:** \\\\`${{ env.REGISTRY }}/${{ matrix.service.image }}:${{ github.sha }}\\\\`\\n**Namespace:** \\\\`${{ env.NAMESPACE }}\\\\`\\n**Autor:** ${{ github.actor }}\\n**Commit:** [${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})\\n\\n[Ver ejecución en GitHub Actions](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\\n\\n:warning: Revisa los logs para más detalles.\"}" \ + https://discord.com/api/webhooks/1449147972330852463/sMs3QYOvQ6oiaPHR4PlBluwNrhWSsVmfm3_Miz6UAFrPxj1SsbqNnHXc5eK9h8ZSaumk \ No newline at end of file diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index f78f51c..8a084f6 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -2,6 +2,7 @@ name: PR Validation on: pull_request: + types: [opened] branches: - main @@ -66,6 +67,13 @@ jobs: severity: 'CRITICAL,HIGH' exit-code: '0' + - name: SonarQube Scan + uses: sonarsource/sonarqube-scan-action@v2 + with: + host: https://sonarqube.reto-ucu.net/ + login: ${{ secrets.SONAR_TOKEN }} + projectKey: reto-xmas-2025-goland-ia-backend + pr-summary: name: PR Summary runs-on: ubuntu-latest @@ -96,4 +104,16 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, body: message - }); \ No newline at end of file + }); + + discord-pr-notify: + name: Notificar PR en Discord + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Notificar a Discord (nuevo PR) + run: | + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{\"content\": \"🔔 Nuevo Pull Request: [${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }}) por ${{ github.event.pull_request.user.login }}\"}" \ + https://discord.com/api/webhooks/1449147972330852463/sMs3QYOvQ6oiaPHR4PlBluwNrhWSsVmfm3_Miz6UAFrPxj1SsbqNnHXc5eK9h8ZSaumk \ No newline at end of file diff --git a/DocsManager/.python-version b/DocsManager/.python-version index 6324d40..24ee5b1 100644 --- a/DocsManager/.python-version +++ b/DocsManager/.python-version @@ -1 +1 @@ -3.14 +3.13 diff --git a/DocsManager/Dockerfile b/DocsManager/Dockerfile index 5fb3d6e..a8ac8ea 100644 --- a/DocsManager/Dockerfile +++ b/DocsManager/Dockerfile @@ -1,13 +1,26 @@ -FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim +FROM python:3.12-slim WORKDIR /app -COPY pyproject.toml uv.lock* ./ +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ + mv /root/.local/bin/uv /usr/local/bin/uv && \ + mv /root/.local/bin/uvx /usr/local/bin/uvx + -RUN uv sync --frozen --no-cache || uv sync --no-cache COPY . . +RUN uv sync --no-dev --no-cache \ + && uv pip list \ + && uv pip show fastapi uvicorn + EXPOSE 8000 -CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +CMD ["uv", "run", "--no-sync", "python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/DocsManager/app/api/routes/chatMessage.py b/DocsManager/app/api/routes/chatMessage.py index 343627b..944fb8d 100644 --- a/DocsManager/app/api/routes/chatMessage.py +++ b/DocsManager/app/api/routes/chatMessage.py @@ -1,10 +1,9 @@ from fastapi import APIRouter, Depends -from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from app.core.db_connection import get_db -from ag_ui.core import RunAgentInput -from app.services.chatMessage import process_agent_message +from app.schemas.chatMessage import UserMessageIn, AssistantMessageOut +from app.services.chatMessage import create_user_message router = APIRouter( @@ -15,15 +14,19 @@ @router.post( "/messages", + response_model=AssistantMessageOut ) -async def post_user_message( - payload: RunAgentInput, +def post_user_message( + payload: UserMessageIn, db: Session = Depends(get_db) ): - """Handle chat messages using the AG-UI protocol. - - This endpoint accepts RunAgentInput and returns a stream of AG-UI events. - """ - return StreamingResponse( - process_agent_message(db, payload) + assistant_msg, session_id = create_user_message( + db=db, + message=payload.message, + session_id=payload.session_id ) + + return { + "session_id": session_id, + "message": assistant_msg.message + } diff --git a/DocsManager/app/core/config.py b/DocsManager/app/core/config.py index 5c15864..6853b4a 100644 --- a/DocsManager/app/core/config.py +++ b/DocsManager/app/core/config.py @@ -1,5 +1,5 @@ from pydantic_settings import BaseSettings, SettingsConfigDict -from pydantic import field_validator, model_validator +from pydantic import field_validator, model_validator, Field from typing import Optional from urllib.parse import quote_plus import logging @@ -29,7 +29,6 @@ class Settings(BaseSettings): minio_access_key: str minio_secret_key: str minio_bucket: str = "documents" - minio_folder: str = "rag-docs" minio_use_ssl: bool = True # Database Configuration (for SQLAlchemy) diff --git a/DocsManager/app/schemas/chatMessage.py b/DocsManager/app/schemas/chatMessage.py new file mode 100644 index 0000000..e900b7a --- /dev/null +++ b/DocsManager/app/schemas/chatMessage.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel +from uuid import UUID +from typing import Optional + +class UserMessageIn(BaseModel): + session_id: Optional[UUID] = None + message: str + + +class AssistantMessageOut(BaseModel): + session_id: UUID + message: str diff --git a/DocsManager/app/services/chatMessage.py b/DocsManager/app/services/chatMessage.py index 5f87f1e..26a943a 100644 --- a/DocsManager/app/services/chatMessage.py +++ b/DocsManager/app/services/chatMessage.py @@ -1,25 +1,8 @@ from sqlalchemy.orm import Session -from typing import AsyncIterator -from uuid import uuid4 from app.models.chat_message import ChatMessage from app.schemas.enums.sender_type import SenderType from app.models.chat_session import ChatSession -from ag_ui.core import ( - RunAgentInput, - RunStartedEvent, - TextMessageStartEvent, - TextMessageContentEvent, - TextMessageEndEvent, - RunFinishedEvent, - MessagesSnapshotEvent, - AssistantMessage, - TextInputContent -) -from ag_ui.encoder import ( - EventEncoder -) - def assistant_reply(text: str) -> str: return f"Respuesta del asistente a: {text}" @@ -31,7 +14,7 @@ def create_user_message( session_id=None ): # 1. Si no hay session_id → crear sesión nueva - if not session_id: + if session_id is None: session = ChatSession() db.add(session) db.flush() # genera el UUID sin commit @@ -62,100 +45,4 @@ def create_user_message( db.commit() db.refresh(assistant_msg) - return assistant_msg, session_id - - -async def process_agent_message( - db: Session, - payload: RunAgentInput -) -> AsyncIterator[str]: - """Process agent message and generate AG-UI protocol events. - - Args: - db: Database session - payload: RunAgentInput containing the user's message and context - - Yields: - AG-UI protocol events as encoded strings - """ - encoder = EventEncoder() - - try: - # Extract the user message from the messages list - user_message_text = "" - if payload.messages: - last_message = payload.messages[-1] - if hasattr(last_message, 'content'): - if isinstance(last_message.content, list) and len(last_message.content) > 0: - user_message_text = last_message.content[0].text - elif isinstance(last_message.content, str): - user_message_text = last_message.content - - # Use thread_id as session_id (AG-UI uses thread_id for conversation tracking) - session_id = payload.thread_id if hasattr(payload, 'thread_id') else None - - if not payload.run_id: - payload.run_id = str(uuid4()) - - # Emit RUN_STARTED event - run_started = RunStartedEvent( - run_id=payload.run_id, - thread_id=payload.thread_id - ) - yield encoder.encode(run_started) - - # Process the message through the service - assistant_msg, session_id = create_user_message( - db=db, - message=user_message_text, - session_id=session_id - ) - - # Generate a unique message ID for the assistant response - message_id = f"msg_{payload.run_id}" - - # Emit TEXT_MESSAGE_START event - text_start = TextMessageStartEvent( - run_id=payload.run_id, - message_id=message_id - ) - yield encoder.encode(text_start) - - # Emit TEXT_MESSAGE_CONTENT event with the assistant's response - text_content = TextMessageContentEvent( - run_id=payload.run_id, - message_id=message_id, - delta=assistant_msg.message - ) - yield encoder.encode(text_content) - - # Emit TEXT_MESSAGE_END event - text_end = TextMessageEndEvent( - run_id=payload.run_id, - message_id=message_id - ) - yield encoder.encode(text_end) - - # Emit MESSAGES_SNAPSHOT event with the full conversation - messages_snapshot = MessagesSnapshotEvent( - run_id=payload.run_id, - messages=[ - *payload.messages, # Include previous messages - AssistantMessage( - id=message_id, - content=[TextInputContent(text=assistant_msg.message)] - ) - ] - ) - yield encoder.encode(messages_snapshot) - - # Emit RUN_FINISHED event - run_finished = RunFinishedEvent( - run_id=payload.run_id - ) - yield encoder.encode(run_finished) - - except Exception as e: - # In case of error, we should emit a RUN_ERROR event - # For now, we'll just re-raise the exception - raise \ No newline at end of file + return assistant_msg, session_id \ No newline at end of file diff --git a/DocsManager/pyproject.toml b/DocsManager/pyproject.toml index d18bf00..1f7001f 100644 --- a/DocsManager/pyproject.toml +++ b/DocsManager/pyproject.toml @@ -3,22 +3,27 @@ name = "goland-ia" version = "0.1.0" description = "Add your description here" readme = "README.md" -requires-python = ">=3.14" +requires-python = ">=3.12,<3.14" + dependencies = [ "fastapi>=0.124.2", - "ipython>=9.8.0", + "sqlalchemy>=2.0.35", + "asyncpg>=0.29.0", + "psycopg2-binary>=2.9.9", + "langgraph>=1.0.4", "typing-extensions>=4.15.0", "uvicorn>=0.38.0", "sqlalchemy>=2.0.0", - "psycopg2-binary>=2.9.0", "pgvector>=0.3.0", "pydantic-settings>=2.0.0", "pika>=1.3.0", "minio>=7.2.0", "python-multipart>=0.0.6", - "ag-ui-protocol>=0.1.10", ] +[project.scripts] +start = "uvicorn main:app --reload --host 0.0.0.0 --port 8000" + [project.optional-dependencies] dev = [ "ruff>=0.14.0", @@ -26,7 +31,7 @@ dev = [ [tool.ruff] # Enable pyproject.toml support -target-version = "py314" +target-version = "py313" line-length = 120 # Enable auto-fix @@ -79,4 +84,4 @@ indent-style = "space" skip-magic-trailing-comma = false # Automatically detect line ending -line-ending = "auto" +line-ending = "auto" \ No newline at end of file diff --git a/DocsManager/uv.lock b/DocsManager/uv.lock index 3bd1267..335add6 100644 --- a/DocsManager/uv.lock +++ b/DocsManager/uv.lock @@ -2,18 +2,6 @@ version = 1 revision = 3 requires-python = ">=3.12, <3.14" -[[package]] -name = "ag-ui-protocol" -version = "0.1.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/bb/5a5ec893eea5805fb9a3db76a9888c3429710dfb6f24bbb37568f2cf7320/ag_ui_protocol-0.1.10.tar.gz", hash = "sha256:3213991c6b2eb24bb1a8c362ee270c16705a07a4c5962267a083d0959ed894f4", size = 6945, upload-time = "2025-11-06T15:17:17.068Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889, upload-time = "2025-11-06T15:17:15.325Z" }, -] - [[package]] name = "annotated-doc" version = "0.0.4" @@ -228,7 +216,6 @@ name = "goland-ia" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "ag-ui-protocol" }, { name = "asyncpg" }, { name = "fastapi" }, { name = "langgraph" }, @@ -250,13 +237,13 @@ dev = [ [package.metadata] requires-dist = [ - { name = "ag-ui-protocol", specifier = ">=0.1.10" }, { name = "asyncpg", specifier = ">=0.29.0" }, { name = "fastapi", specifier = ">=0.124.2" }, { name = "langgraph", specifier = ">=1.0.4" }, { name = "minio", specifier = ">=7.2.0" }, { name = "pgvector", specifier = ">=0.3.0" }, { name = "pika", specifier = ">=1.3.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.0" }, { name = "psycopg2-binary", specifier = ">=2.9.9" }, { name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "python-multipart", specifier = ">=0.0.6" }, diff --git a/RAGManager/.env.example b/RAGManager/.env.example index f61c8f4..895cbeb 100644 --- a/RAGManager/.env.example +++ b/RAGManager/.env.example @@ -1,4 +1,7 @@ -DATABASE_URL=postgresql://postgres:postgres@postgres:5432/vectordb +# Database Configuration +# NOTE: Use psycopg2 (sync driver), NOT asyncpg. The RAGManager uses synchronous SQLAlchemy. +# LangChain PGVector will automatically convert this to psycopg3 format internally. +DATABASE_URL=postgresql+psycopg2://postgres:postgres@postgres:5432/vectordb # RabbitMQ Configuration RABBITMQ_USER=guest diff --git a/RAGManager/Dockerfile b/RAGManager/Dockerfile index 38ce443..33c9eb8 100644 --- a/RAGManager/Dockerfile +++ b/RAGManager/Dockerfile @@ -1,13 +1,22 @@ -FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim +FROM python:3.12-slim WORKDIR /app -COPY pyproject.toml uv.lock* ./ +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* -RUN uv sync --frozen --no-cache || uv sync --no-cache +RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \ + mv /root/.local/bin/uv /usr/local/bin/uv && \ + mv /root/.local/bin/uvx /usr/local/bin/uvx COPY . . +RUN uv sync --no-dev --no-cache + EXPOSE 8000 -CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +CMD ["uv", "run", "--no-sync", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/RAGManager/RAG Manager API/Get Chat History.bru b/RAGManager/RAG Manager API/Get Chat History.bru new file mode 100644 index 0000000..30b5e13 --- /dev/null +++ b/RAGManager/RAG Manager API/Get Chat History.bru @@ -0,0 +1,15 @@ +meta { + name: Get Chat History + type: http + seq: 1 +} + +get { + url: {{base_url}}/api/{{api_version}}/chat/history/{{test_session_id}} + body: none + auth: none +} + +settings { + encodeUrl: true +} diff --git a/RAGManager/RAG Manager API/bruno.json b/RAGManager/RAG Manager API/bruno.json new file mode 100644 index 0000000..6c9acf1 --- /dev/null +++ b/RAGManager/RAG Manager API/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "RAG Manager API", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/RAGManager/RAG Manager API/environments/Local Development.bru b/RAGManager/RAG Manager API/environments/Local Development.bru new file mode 100644 index 0000000..7f6c634 --- /dev/null +++ b/RAGManager/RAG Manager API/environments/Local Development.bru @@ -0,0 +1,5 @@ +vars { + base_url: http://localhost:8000 + api_version: v1 + test_session_id: 550e8400-e29b-41d4-a716-446655440000 +} diff --git a/RAGManager/app/agents/graph.py b/RAGManager/app/agents/graph.py index 8916ea5..665feec 100644 --- a/RAGManager/app/agents/graph.py +++ b/RAGManager/app/agents/graph.py @@ -23,12 +23,12 @@ def create_agent_graph() -> StateGraph: Create and configure the LangGraph agent graph. The graph implements the following flow: - 1. START -> agent_host (Nodo 1) - Prepares state, no DB operations + 1. START -> agent_host (Nodo 1) - Prepares state and retrieves chat history 2. agent_host -> guard_inicial (Nodo 2) - Validates for malicious content 3. guard_inicial -> [conditional]: - - malicious -> fallback_inicial -> END (stops processing, no DB save) + - malicious -> fallback_inicial -> END (stops processing, chat history available) - continue -> parafraseo (Nodo 4) - 4. parafraseo -> Saves message to DB, retrieves chat history, paraphrases + 4. parafraseo -> Saves message to DB and paraphrases using chat history 5. parafraseo -> retriever (Nodo 5) - Retrieves relevant chunks from vector DB 6. retriever -> context_builder (Nodo 6) - Builds enriched query and generates response 7. context_builder -> guard_final (Nodo 8) - Validates response for risky content diff --git a/RAGManager/app/agents/nodes/agent_host.py b/RAGManager/app/agents/nodes/agent_host.py index b619573..e1e1aa1 100644 --- a/RAGManager/app/agents/nodes/agent_host.py +++ b/RAGManager/app/agents/nodes/agent_host.py @@ -1,8 +1,11 @@ """Nodo 1: Agent Host - Entry point that prepares initial state.""" import logging +from uuid import UUID from app.agents.state import AgentState +from app.core.database_connection import SessionLocal +from app.services.chat import get_chat_history logger = logging.getLogger(__name__) @@ -14,16 +17,17 @@ def agent_host(state: AgentState) -> AgentState: This node: 1. Receives the initial prompt and optional chat_session_id 2. Extracts the prompt from messages - 3. Prepares state for validation (no DB operations yet) + 3. Retrieves chat history if session_id is provided + 4. Prepares state with context for the entire flow - Note: Chat history retrieval and message saving is deferred to parafraseo - node to ensure malicious messages are not saved to the database. + Note: Chat history is retrieved here (not in parafraseo) so it's available + throughout the entire flow, regardless of which path is taken (normal or fallback). Args: state: Agent state containing the user prompt and optional chat_session_id Returns: - Updated state with prompt and initial_context set (no DB operations) + Updated state with prompt, chat_messages, and initial_context set """ updated_state = state.copy() @@ -32,18 +36,50 @@ def agent_host(state: AgentState) -> AgentState: last_message = messages[-1] if messages else None prompt = last_message.content if last_message else "" - # Validate user_id is provided - user_id = state.get("user_id") - if not user_id: - logger.error("user_id is required in state but was not provided") - updated_state["error_message"] = "user_id is required" - return updated_state - - # Set prompt and initial context (no DB operations) + # Set prompt and initial context updated_state["prompt"] = prompt updated_state["initial_context"] = prompt - updated_state["chat_messages"] = None # Will be set in parafraseo after validation - logger.debug("Agent host prepared state for validation (no DB operations)") + # Retrieve chat history if session exists + chat_session_id = state.get("chat_session_id") + + if chat_session_id: + try: + # Convert to UUID if string + session_uuid = UUID(chat_session_id) if isinstance(chat_session_id, str) else chat_session_id + + db = SessionLocal() + try: + # Get chat history from database + chat_messages = get_chat_history(db, session_uuid) + + # Convert SQLAlchemy models to dicts for state + updated_state["chat_messages"] = [ + { + "id": msg.id, + "sender": msg.sender, + "message": msg.message, + "created_at": msg.created_at.isoformat() if msg.created_at else None, + } + for msg in chat_messages + ] + + logger.info(f"Retrieved {len(chat_messages)} messages for session {session_uuid}") + finally: + db.close() + + except ValueError as e: + # Session doesn't exist - this is okay for new sessions + logger.info(f"Chat session not found (likely new session): {e}") + updated_state["chat_messages"] = [] + except Exception as e: + logger.error(f"Error retrieving chat history: {e}", exc_info=True) + updated_state["chat_messages"] = [] + else: + # No session ID provided - new conversation + logger.info("No chat_session_id provided - starting new conversation") + updated_state["chat_messages"] = [] + + logger.debug(f"Agent host prepared state with {len(updated_state.get('chat_messages', []))} chat messages") return updated_state diff --git a/RAGManager/app/agents/nodes/context_builder.py b/RAGManager/app/agents/nodes/context_builder.py index 79f2195..b550659 100644 --- a/RAGManager/app/agents/nodes/context_builder.py +++ b/RAGManager/app/agents/nodes/context_builder.py @@ -1,10 +1,19 @@ """Nodo 6: Context Builder - Enriches query with retrieved context.""" +import logging + from app.agents.state import AgentState -from langchain_core.messages import SystemMessage +from app.core.config import settings +from langchain_core.messages import HumanMessage, SystemMessage from langchain_openai import ChatOpenAI -llm = ChatOpenAI(model="gpt-5-nano") +logger = logging.getLogger(__name__) + +# Initialize Primary LLM with configuration from settings +llm = ChatOpenAI( + model="gpt-4.1-mini", # Primary LLM model for context-aware responses + openai_api_key=settings.openai_api_key, +) def context_builder(state: AgentState) -> AgentState: @@ -23,30 +32,82 @@ def context_builder(state: AgentState) -> AgentState: Returns: Updated state with enriched_query and primary_response set """ - # TODO: Implement context building and primary LLM call - # This should: - # 1. Combine paraphrased_text with relevant_chunks into enriched_query - # 2. Format the query appropriately (e.g., with system prompts, context sections) - # 3. Call Primary LLM with the enriched query - # 4. Store the LLM response in primary_response - - # Placeholder: For now, we'll create a simple enriched query updated_state = state.copy() + + # Get paraphrased text and relevant chunks from state paraphrased = state.get("paraphrased_text", "") chunks = state.get("relevant_chunks", []) - + + # Fallback if paraphrased_text is not available + if not paraphrased: + logger.warning("No paraphrased text found in state. Using original prompt.") + messages = state.get("messages", []) + if messages: + paraphrased = messages[-1].content if hasattr(messages[-1], "content") else str(messages[-1]) + else: + paraphrased = state.get("prompt", "") + # Build enriched query with context - context_section = "\n\n".join(chunks) if chunks else "No relevant context found." + if chunks: + context_section = "\n\n---\n\n".join([f"Context {i+1}:\n{chunk}" for i, chunk in enumerate(chunks)]) + else: + context_section = "No relevant context found in the knowledge base." + logger.warning("No relevant chunks found for context building") - system_content = f"""You are a helpful assistant. Use the following context to answer the user's question. -If the answer is not in the context, say you don't know. + # Create enriched query combining paraphrased text and context + enriched_query = f"""User Question: {paraphrased} -Context: -{context_section}""" +Relevant Context from Knowledge Base: +{context_section} - messages = [SystemMessage(content=system_content)] + state["messages"] +Please provide a comprehensive answer based on the context provided above. If the context does not contain enough information to answer the question, please indicate that clearly.""" + + # System prompt for the Primary LLM + system_content = """You are a helpful assistant specialized in providing accurate, context-based answers about nutrition and culinary topics. - # Call Primary LLM - response = llm.invoke(messages) +Your task is to: +1. Use the provided context to answer the user's question accurately +2. If the context contains relevant information, provide a comprehensive answer +3. If the context does not contain enough information, clearly state that you don't have sufficient information in the knowledge base +4. Always base your answer on the provided context - do not make up information +5. If the question is not related to the context, politely redirect the conversation - return {"messages": [response]} +Be concise, accurate, and helpful.""" + + # Prepare messages for LLM + messages_for_llm = [ + SystemMessage(content=system_content), + HumanMessage(content=enriched_query) + ] + + try: + # Call Primary LLM + logger.info("Calling Primary LLM with enriched query") + response = llm.invoke(messages_for_llm) + + # Extract response content + primary_response = response.content if hasattr(response, "content") else str(response) + + # Update state with enriched query and primary response (as defined in state.py) + updated_state["enriched_query"] = enriched_query + updated_state["primary_response"] = primary_response + + # Also set generated_response for guard_final (Nodo 7: Generator not yet implemented) + # For now, generated_response = primary_response + # This allows guard_final to validate the response + updated_state["generated_response"] = primary_response + + # Also update messages for LangGraph compatibility + updated_state["messages"] = state.get("messages", []) + [response] + + logger.info("Successfully generated primary response from LLM") + + except Exception as e: + logger.error(f"Error calling Primary LLM: {e}", exc_info=True) + # Set error state + updated_state["error_message"] = f"Error in context builder: {str(e)}" + updated_state["enriched_query"] = enriched_query + updated_state["primary_response"] = None + updated_state["generated_response"] = None + + return updated_state diff --git a/RAGManager/app/agents/nodes/parafraseo.py b/RAGManager/app/agents/nodes/parafraseo.py index 675e19a..eed68a9 100644 --- a/RAGManager/app/agents/nodes/parafraseo.py +++ b/RAGManager/app/agents/nodes/parafraseo.py @@ -1,4 +1,4 @@ -"""Nodo 4: Parafraseo - Saves message, retrieves chat history, and paraphrases user input.""" +"""Nodo 4: Parafraseo - Saves message and paraphrases user input using chat history.""" import json import logging @@ -14,21 +14,22 @@ def parafraseo(state: AgentState) -> AgentState: """ - Parafraseo node - Saves message to DB, retrieves chat history, and paraphrases user input. + Parafraseo node - Saves message to DB and paraphrases user input using chat history. This node: 1. Receives a validated user message (after guard_inicial validation) - 2. Saves the user's message to the chat session in PostgreSQL (endpoint 1 - placeholder) - 3. Retrieves the last 10 messages of the conversation (endpoint 2 - placeholder) - 4. Uses the last message to understand user's intentions and the remaining 9 (older) messages as context + 2. Saves the user's message to the chat session in PostgreSQL (TODO - placeholder) + 3. Uses chat history (already retrieved by agent_host) as context + 4. Uses the last message to understand user's intentions and chat history as context 5. Sends to LLM with instructions to return 3 differently phrased statements that encapsulate the user's intentions according to the last message and chat history Args: - state: Agent state containing validated user message, chat_session_id, and user_id + state: Agent state containing validated user message, chat_messages (from agent_host), + chat_session_id, and user_id Returns: - Updated state with chat_messages, paraphrased_text, and paraphrased_statements set + Updated state with paraphrased_text and paraphrased_statements set """ updated_state = state.copy() @@ -43,31 +44,16 @@ def parafraseo(state: AgentState) -> AgentState: last_user_message = messages[-1] user_message_content = last_user_message.content if hasattr(last_user_message, 'content') else str(last_user_message) - # TODO: Endpoint 1 - Save message to PostgreSQL database according to chat session + # TODO: Save message to PostgreSQL database according to chat session # This should: - # 1. Call an endpoint (not yet developed) that: - # - Saves the current user message to the chat session in PostgreSQL - # - Uses chat_session_id and user_id from state - # - Returns success/failure status - # 2. Handle errors appropriately (session not found, permission denied, etc.) - logger.info("Endpoint 1 (save message to DB) not yet implemented - skipping") - - # TODO: Endpoint 2 - Retrieve last 10 messages of the conversation - # This should: - # 1. Call an endpoint (not yet developed) that: - # - Retrieves the last 10 messages for the chat session - # - Returns a list of message dictionaries with structure: [{"sender": "user|assistant|system", "message": "...", "created_at": "..."}, ...] - # - Messages should be ordered from oldest to newest (or newest to oldest, depending on API design) - # 2. Update state with chat_messages from the endpoint response + # 1. Call a service or endpoint to save the current user message to the chat session + # 2. Use chat_session_id from state # 3. Handle errors appropriately (session not found, permission denied, etc.) + logger.info("Message save to DB not yet implemented - skipping") - # Placeholder: For now, we'll simulate chat history with just the current message - # Once the endpoint is implemented, replace this with the actual endpoint call - chat_messages = [ - {"sender": "user", "message": user_message_content, "created_at": "2025-01-01T00:00:00"} - ] - updated_state["chat_messages"] = chat_messages - logger.warning("Endpoint 2 (retrieve chat history) not yet implemented - using current message only") + # Get chat history from state (already retrieved by agent_host) + chat_messages = state.get("chat_messages", []) + logger.info(f"Using {len(chat_messages)} chat messages from state (retrieved by agent_host)") # Process chat history: last message (intentions) + 9 older messages (context) # The last message is the most recent one (for understanding intentions) diff --git a/RAGManager/app/agents/nodes/retriever.py b/RAGManager/app/agents/nodes/retriever.py index 0f2ec42..90bbdd0 100644 --- a/RAGManager/app/agents/nodes/retriever.py +++ b/RAGManager/app/agents/nodes/retriever.py @@ -163,4 +163,4 @@ def retriever(state: AgentState) -> AgentState: # Store the unique chunk contents in state updated_state["relevant_chunks"] = unique_chunks - return updated_state + return updated_state \ No newline at end of file diff --git a/RAGManager/app/api/routes/__init__.py b/RAGManager/app/api/routes/__init__.py index ef443ea..82dfe27 100644 --- a/RAGManager/app/api/routes/__init__.py +++ b/RAGManager/app/api/routes/__init__.py @@ -2,8 +2,9 @@ from fastapi import APIRouter -from app.api.routes import documents +from app.api.routes import chat, documents router = APIRouter(prefix="/api/v1") -router.include_router(documents.router) \ No newline at end of file +router.include_router(documents.router) +router.include_router(chat.router) \ No newline at end of file diff --git a/RAGManager/app/api/routes/chat.py b/RAGManager/app/api/routes/chat.py new file mode 100644 index 0000000..2cdcff6 --- /dev/null +++ b/RAGManager/app/api/routes/chat.py @@ -0,0 +1,70 @@ +"""API routes for chat-related endpoints.""" + +import logging +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from app.core.database_connection import get_db +from app.schemas.chat import ChatHistoryResponse, ChatMessageResponse +from app.services.chat import get_chat_history + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/chat", tags=["chat"]) + + +@router.get("/history/{chat_session_id}", response_model=ChatHistoryResponse) +async def get_chat_history_endpoint( + chat_session_id: UUID, + db: Session = Depends(get_db), +) -> ChatHistoryResponse: + """ + Retrieve the last 10 messages from a chat session. + + This endpoint returns the most recent 10 messages from the specified chat session, + ordered by creation time (most recent first). + + Args: + chat_session_id: UUID of the chat session + db: Database session dependency + + Returns: + ChatHistoryResponse containing the session_id, list of messages, and count + + Raises: + HTTPException: 404 if chat session doesn't exist + HTTPException: 400 if invalid UUID format + HTTPException: 500 for database errors + """ + logger.info(f"Received chat history request for session: {chat_session_id}") + + try: + # Get chat history from service + messages = get_chat_history(db, chat_session_id) + + # Convert SQLAlchemy models to Pydantic models + message_responses = [ + ChatMessageResponse( + id=msg.id, + session_id=msg.session_id, + sender=msg.sender, + message=msg.message, + created_at=msg.created_at, + ) + for msg in messages + ] + + return ChatHistoryResponse( + session_id=chat_session_id, + messages=message_responses, + count=len(message_responses), + ) + + except ValueError as e: + logger.warning(f"Chat session not found: {e}") + raise HTTPException(status_code=404, detail=str(e)) + except Exception as e: + logger.error(f"Error retrieving chat history: {e}") + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") diff --git a/RAGManager/app/core/config.py b/RAGManager/app/core/config.py index b96d119..264d591 100644 --- a/RAGManager/app/core/config.py +++ b/RAGManager/app/core/config.py @@ -13,7 +13,7 @@ class Settings(BaseSettings): minio_access_key: str minio_secret_key: str minio_bucket: str - minio_secure: bool = True + minio_use_ssl: bool = True # OpenAI Configuration openai_api_key: str @@ -21,6 +21,13 @@ class Settings(BaseSettings): # Database Configuration database_url: str + # RabbitMQ Configuration + rabbitmq_user: str + rabbitmq_password: str + rabbitmq_host: str = "localhost" + rabbitmq_port: int = 5672 + rabbitmq_queue_name: str = "document.process" + # Chunking Configuration chunk_size: int = 1000 chunk_overlap: int = 200 @@ -67,6 +74,15 @@ class Settings(BaseSettings): extra="ignore", ) + @property + def rabbitmq_url(self) -> str: + """Returns the RabbitMQ connection URL with URL-encoded credentials.""" + from urllib.parse import quote_plus + + encoded_user = quote_plus(self.rabbitmq_user) + encoded_password = quote_plus(self.rabbitmq_password) + return f"amqp://{encoded_user}:{encoded_password}@{self.rabbitmq_host}:{self.rabbitmq_port}/" + settings = Settings() diff --git a/RAGManager/app/core/rabbitmq.py b/RAGManager/app/core/rabbitmq.py new file mode 100644 index 0000000..53b5b26 --- /dev/null +++ b/RAGManager/app/core/rabbitmq.py @@ -0,0 +1,99 @@ +import json +import logging +from typing import Callable, Optional + +import pika +from pika.exceptions import AMQPConnectionError + +from app.core.config import settings + +logger = logging.getLogger(__name__) + + +class RabbitMQConnection: + """Handles connection and operations with RabbitMQ""" + + def __init__(self): + self.connection: Optional[pika.BlockingConnection] = None + self.channel: Optional[pika.channel.Channel] = None + + def connect(self): + """Establishes connection with RabbitMQ""" + try: + url = settings.rabbitmq_url + logger.info(f"Connecting to RabbitMQ at {settings.rabbitmq_host}:{settings.rabbitmq_port}") + logger.debug( + f"RabbitMQ URL: amqp://{settings.rabbitmq_user}:***@{settings.rabbitmq_host}:{settings.rabbitmq_port}/" + ) + + self.connection = pika.BlockingConnection(pika.URLParameters(url)) + self.channel = self.connection.channel() + logger.info("Connected to RabbitMQ") + except AMQPConnectionError as e: + logger.error(f"Failed to connect to RabbitMQ: {e}") + logger.error(f"Configured host: {settings.rabbitmq_host}") + logger.error(f"Configured port: {settings.rabbitmq_port}") + raise + except Exception as e: + logger.error(f"Unexpected error connecting to RabbitMQ: {e}") + logger.error(f"Error type: {type(e).__name__}") + raise + + def close(self): + """Closes the connection""" + if self.channel and not self.channel.is_closed: + self.channel.close() + logger.info("RabbitMQ channel closed") + if self.connection and not self.connection.is_closed: + self.connection.close() + logger.info("RabbitMQ connection closed") + + def declare_queue(self, queue_name: str, durable: bool = True): + """Declares a queue""" + if not self.channel: + self.connect() + self.channel.queue_declare(queue=queue_name, durable=durable) + logger.info(f"Queue '{queue_name}' declared") + + def consume_messages(self, queue_name: str, callback: Callable): + """ + Start consuming messages from the queue. + + Args: + queue_name: Name of the queue to consume from + callback: Callback function to process messages + """ + if not self.channel: + self.connect() + + # Declare queue (idempotent operation) + self.declare_queue(queue_name) + + # Set QoS to process one message at a time + self.channel.basic_qos(prefetch_count=1) + + # Start consuming + self.channel.basic_consume( + queue=queue_name, + on_message_callback=callback, + auto_ack=False, # Manual acknowledgment + ) + + logger.info(f"Started consuming messages from queue '{queue_name}'") + logger.info("Waiting for messages. To exit press CTRL+C") + + try: + self.channel.start_consuming() + except KeyboardInterrupt: + logger.info("Interrupted by user") + self.channel.stop_consuming() + except Exception as e: + logger.error(f"Error while consuming messages: {e}") + raise + finally: + self.close() + + +# Global instance +rabbitmq = RabbitMQConnection() + diff --git a/RAGManager/app/models/chat.py b/RAGManager/app/models/chat.py index f5eec38..e9c40fd 100644 --- a/RAGManager/app/models/chat.py +++ b/RAGManager/app/models/chat.py @@ -15,9 +15,8 @@ class ChatSession(Base): __tablename__ = "chat_sessions" id = Column(PGUUID(as_uuid=True), primary_key=True, server_default="uuid_generate_v4()") - user_id = Column(Text, nullable=True, index=True) # User ID for ownership validation created_at = Column(TIMESTAMP, default=datetime.utcnow, nullable=False) - metadata = Column(JSONB, nullable=True) + session_metadata = Column("metadata", JSONB, nullable=True) # Map to 'metadata' column in DB # Relationship to messages messages = relationship("ChatMessage", back_populates="session", cascade="all, delete-orphan") diff --git a/RAGManager/app/schemas/chat.py b/RAGManager/app/schemas/chat.py new file mode 100644 index 0000000..babc689 --- /dev/null +++ b/RAGManager/app/schemas/chat.py @@ -0,0 +1,29 @@ +"""Pydantic schemas for chat-related endpoints.""" + +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel + + +class ChatMessageResponse(BaseModel): + """Response schema for a single chat message.""" + + id: int + session_id: UUID + sender: str # 'user', 'assistant', or 'system' + message: str + created_at: datetime + + class Config: + """Pydantic config.""" + + from_attributes = True + + +class ChatHistoryResponse(BaseModel): + """Response schema for chat history endpoint.""" + + session_id: UUID + messages: list[ChatMessageResponse] + count: int diff --git a/RAGManager/app/services/chat.py b/RAGManager/app/services/chat.py new file mode 100644 index 0000000..ed98c3f --- /dev/null +++ b/RAGManager/app/services/chat.py @@ -0,0 +1,44 @@ +"""Service functions for chat-related operations.""" + +import logging +from uuid import UUID + +from sqlalchemy.orm import Session + +from app.models.chat import ChatMessage, ChatSession + +logger = logging.getLogger(__name__) + + +def get_chat_history(db: Session, session_id: UUID) -> list[ChatMessage]: + """ + Retrieve the last 10 messages from a chat session. + + Args: + db: SQLAlchemy database session + session_id: UUID of the chat session + + Returns: + List of ChatMessage objects ordered by created_at DESC (most recent first) + + Raises: + ValueError: If the chat session doesn't exist + """ + # First, validate that the session exists + session = db.query(ChatSession).filter(ChatSession.id == session_id).first() + if not session: + raise ValueError(f"Chat session {session_id} not found") + + # Query the last 10 messages for this session, ordered by created_at DESC + messages = ( + db.query(ChatMessage) + .filter(ChatMessage.session_id == session_id) + .order_by(ChatMessage.created_at.desc()) + .limit(10) + .all() + ) + + # Reverse to get chronological order (oldest first) + # But the plan says "most recent first", so we'll keep DESC order + logger.info(f"Retrieved {len(messages)} messages for session {session_id}") + return messages diff --git a/RAGManager/app/services/embedding_service.py b/RAGManager/app/services/embedding_service.py deleted file mode 100644 index b70500d..0000000 --- a/RAGManager/app/services/embedding_service.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import List, Tuple - -from langchain_core.documents import Document - - -def chunks_to_embeddings(chunks: List[Document]) -> List[Tuple[str, List[float]]]: - """ - Placeholder function - to be implemented later. - - This function will: - 1. Generate embeddings for each chunk using OpenAI's embedding API - 2. Return a list of tuples containing chunk content and its embedding vector - - Args: - chunks: List of LangChain Document chunks to embed - - Returns: - List[Tuple[str, List[float]]]: List of tuples containing (content, embedding_vector) - where embedding_vector is a list of floats with dimension 1536 - - Raises: - NotImplementedError: This function is not yet implemented - """ - raise NotImplementedError("This function will be implemented later") - diff --git a/RAGManager/app/services/minio_client.py b/RAGManager/app/services/minio_client.py index 2cfa106..17843ed 100644 --- a/RAGManager/app/services/minio_client.py +++ b/RAGManager/app/services/minio_client.py @@ -1,6 +1,7 @@ # MinIO client configuration and utilities. import logging +from urllib.parse import urlparse import certifi import urllib3 @@ -14,6 +15,19 @@ def get_minio_client() -> Minio: """Create a MinIO client with proper timeout and retry configuration.""" + # Parse endpoint to extract host and port (urlparse strips the scheme automatically) + parsed = urlparse(settings.minio_endpoint) + endpoint = parsed.netloc or parsed.path + + # Validate that the endpoint is not empty + if not endpoint or endpoint.strip() == "": + error_msg = ( + f"Invalid MinIO endpoint configuration: '{settings.minio_endpoint}'. " + "Endpoint must be a valid host or host:port (e.g., 'localhost:9000')" + ) + logger.error(error_msg) + raise ValueError(error_msg) + # Configure timeout: 10s connect, 30s read timeout = UrllibTimeout(connect=10, read=30) @@ -34,10 +48,10 @@ def get_minio_client() -> Minio: ) return Minio( - endpoint=settings.minio_endpoint, + endpoint=endpoint, access_key=settings.minio_access_key, secret_key=settings.minio_secret_key, - secure=settings.minio_secure, + secure=settings.minio_use_ssl, http_client=http_client, ) diff --git a/RAGManager/app/services/pipeline.py b/RAGManager/app/services/pipeline.py index d9cce0c..47fd346 100644 --- a/RAGManager/app/services/pipeline.py +++ b/RAGManager/app/services/pipeline.py @@ -1,15 +1,38 @@ import logging -from langchain_core.documents import Document - from app.core.config import settings +from app.core.database_connection import SessionLocal +from app.models.document import Document from app.services.chunking_service import document_to_chunks -from app.services.embedding_service import chunks_to_embeddings from app.services.pdf_processor import pdf_to_document +from app.services.vector_store import store_chunks_with_embeddings logger = logging.getLogger(__name__) +def _create_document_record(filename: str, minio_path: str) -> int: + """ + Create a Document record in the database. + + Args: + filename: Original filename of the PDF + minio_path: Path to the PDF in MinIO bucket + + Returns: + int: The created document's ID + """ + db = SessionLocal() + try: + document = Document(filename=filename, minio_path=minio_path) + db.add(document) + db.commit() + db.refresh(document) + logger.info(f"Created document record with id={document.id}") + return document.id + finally: + db.close() + + def process_pdf_pipeline(object_name: str) -> int: """ Orchestrates the PDF processing pipeline. @@ -17,17 +40,16 @@ def process_pdf_pipeline(object_name: str) -> int: This function coordinates the three-stage pipeline: 1. PDF to LangChain Document 2. Document to Chunks - 3. Chunks to Embeddings - 4. Store in database (to be implemented) + 3. Embed and Store in database using PGVector Args: object_name: Path/name of the PDF object in the MinIO bucket Returns: - int: document_id of the created document (mock value for now) + int: document_id of the created document Raises: - NotImplementedError: If any of the pipeline stages are not yet implemented + Exception: If any of the pipeline stages fail """ logger.info(f"Starting PDF processing pipeline for object: {object_name}") @@ -44,27 +66,28 @@ def process_pdf_pipeline(object_name: str) -> int: chunks = document_to_chunks(document, settings.chunk_size, settings.chunk_overlap) logger.info(f"Stage 2 completed successfully. Created {len(chunks)} chunks") - # Stage 3: Chunks to Embeddings - logger.info("Stage 3: Generating embeddings for chunks") - embeddings = chunks_to_embeddings(chunks) - logger.info(f"Stage 3 completed successfully. Generated {len(embeddings)} embeddings") - - # Stage 4: Store in database (placeholder - not implemented yet) - logger.info("Stage 4: Storing chunks and embeddings in database") - # TODO: Implement database storage - # This will: - # 1. Create a Document record in the documents table - # 2. Create DocumentChunk records with embeddings in the document_chunks table - # 3. Return the document_id - raise NotImplementedError("Database storage will be implemented later") - - except NotImplementedError as e: - logger.warning(f"Pipeline stage not implemented: {e}") - # Return a mock document_id for now - # In production, this should be replaced with actual database storage - mock_document_id = 1 - logger.info(f"Pipeline completed with mock document_id: {mock_document_id}") - return mock_document_id + # Stage 3: Embed and Store in database + # First, create the document record to get the document_id + logger.info("Stage 3: Embedding and storing chunks in database") + + # Extract filename from object_name (e.g., "folder/file.pdf" -> "file.pdf") + filename = object_name.split("/")[-1] if "/" in object_name else object_name + + # Create document record in the documents table + document_id = _create_document_record(filename=filename, minio_path=object_name) + + # Store chunks with embeddings using PGVector + # This generates embeddings via OpenAI and stores in the vector database + chunks_stored = store_chunks_with_embeddings( + document_id=document_id, + filename=filename, + chunks=chunks, + ) + logger.info(f"Stage 3 completed successfully. Stored {chunks_stored} chunks with embeddings") + + logger.info(f"Pipeline completed successfully. Document ID: {document_id}") + return document_id + except Exception as e: logger.error(f"Error in PDF processing pipeline: {e}") raise diff --git a/RAGManager/app/services/vector_store.py b/RAGManager/app/services/vector_store.py new file mode 100644 index 0000000..8f57c9e --- /dev/null +++ b/RAGManager/app/services/vector_store.py @@ -0,0 +1,162 @@ +""" +Vector Store Service - Handles embedding generation and storage using LangChain PGVector. + +This service provides functionality to: +1. Initialize PGVector connection with OpenAI embeddings +2. Store document chunks with their embeddings in batches +3. Convert database URLs to psycopg3 format required by langchain-postgres +""" + +import logging +from urllib.parse import urlparse, urlunparse + +from langchain_core.documents import Document +from langchain_openai import OpenAIEmbeddings +from langchain_postgres import PGVector + +from app.core.config import settings + +logger = logging.getLogger(__name__) + +# Collection name for the vector store +COLLECTION_NAME = "document_chunks" + +# Batch size for inserting documents (to handle large PDFs efficiently) +DEFAULT_BATCH_SIZE = 100 + + +def _convert_database_url_to_psycopg(database_url: str) -> str: + """ + Convert database URL to postgresql+psycopg format required by langchain-postgres. + + LangChain PGVector requires postgresql+psycopg:// (psycopg3) format. + This function converts common formats (postgresql://, postgresql+psycopg2://) to the required format. + + Args: + database_url: Original database URL + + Returns: + Database URL in postgresql+psycopg:// format + """ + parsed = urlparse(database_url) + + # Replace driver with psycopg (psycopg3) + if parsed.scheme.startswith("postgresql"): + # Remove any existing driver (e.g., +psycopg2) + base_scheme = "postgresql" + if "+" in parsed.scheme: + base_scheme = parsed.scheme.split("+")[0] + + new_scheme = f"{base_scheme}+psycopg" + new_parsed = parsed._replace(scheme=new_scheme) + return urlunparse(new_parsed) + + return database_url + + +def _get_embeddings() -> OpenAIEmbeddings: + """ + Get OpenAI embeddings instance configured from settings. + + Returns: + OpenAIEmbeddings instance + """ + return OpenAIEmbeddings( + model=settings.embedding_model, + openai_api_key=settings.openai_api_key, + ) + + +def _get_vector_store() -> PGVector: + """ + Get or create PGVector instance for document storage. + + Returns: + PGVector instance configured with embeddings and connection + """ + connection_string = _convert_database_url_to_psycopg(settings.database_url) + embeddings = _get_embeddings() + + vector_store = PGVector( + embeddings=embeddings, + collection_name=COLLECTION_NAME, + connection=connection_string, + use_jsonb=True, + ) + + return vector_store + + +def store_chunks_with_embeddings( + document_id: int, + filename: str, + chunks: list[Document], + batch_size: int = DEFAULT_BATCH_SIZE, +) -> int: + """ + Store document chunks with their embeddings in PGVector. + + This function: + 1. Prepares chunks with metadata (document_id, chunk_index, filename) + 2. Uses LangChain PGVector to generate embeddings and store in batches + 3. Returns the number of chunks stored + + Args: + document_id: ID of the parent document in the documents table + filename: Original filename for metadata + chunks: List of LangChain Document chunks to embed and store + batch_size: Number of chunks to process per batch (default: 100) + + Returns: + int: Number of chunks successfully stored + + Raises: + Exception: If storage fails + """ + if not chunks: + logger.warning("No chunks provided for storage") + return 0 + + logger.info(f"Storing {len(chunks)} chunks for document_id={document_id}") + + # Prepare documents with metadata for PGVector + prepared_docs = [] + for idx, chunk in enumerate(chunks): + # Create a new document with enriched metadata + metadata = { + "document_id": document_id, + "chunk_index": idx, + "filename": filename, + # Preserve any existing metadata from chunking + **chunk.metadata, + } + prepared_docs.append( + Document( + page_content=chunk.page_content, + metadata=metadata, + ) + ) + + # Get vector store instance + vector_store = _get_vector_store() + + # Store documents in batches + total_stored = 0 + for i in range(0, len(prepared_docs), batch_size): + batch = prepared_docs[i : i + batch_size] + batch_num = (i // batch_size) + 1 + total_batches = (len(prepared_docs) + batch_size - 1) // batch_size + + logger.info(f"Processing batch {batch_num}/{total_batches} ({len(batch)} chunks)") + + try: + # PGVector.add_documents handles embedding generation internally + vector_store.add_documents(batch) + total_stored += len(batch) + logger.debug(f"Batch {batch_num} stored successfully") + except Exception as e: + logger.error(f"Error storing batch {batch_num}: {e}") + raise + + logger.info(f"Successfully stored {total_stored} chunks for document_id={document_id}") + return total_stored diff --git a/RAGManager/app/workers/__init__.py b/RAGManager/app/workers/__init__.py new file mode 100644 index 0000000..cd00fa7 --- /dev/null +++ b/RAGManager/app/workers/__init__.py @@ -0,0 +1,2 @@ +"""Workers package for background processing tasks.""" + diff --git a/RAGManager/app/workers/pdf_processor_consumer.py b/RAGManager/app/workers/pdf_processor_consumer.py new file mode 100644 index 0000000..01dcc4d --- /dev/null +++ b/RAGManager/app/workers/pdf_processor_consumer.py @@ -0,0 +1,121 @@ +"""RabbitMQ consumer for processing PDFs from MinIO events.""" + +import json +import logging +from urllib.parse import unquote + +from app.core.config import settings +from app.core.rabbitmq import RabbitMQConnection +from app.services.pipeline import process_pdf_pipeline + +logger = logging.getLogger(__name__) + + +def extract_pdf_path(message_body: dict) -> str: + """ + Extract the PDF path from MinIO event message. + + Args: + message_body: Parsed JSON message from MinIO + + Returns: + str: Decoded object path (e.g., "rag-docs/file.pdf") + + Raises: + ValueError: If message structure is invalid + """ + records = message_body.get("Records", []) + if not records: + raise ValueError("No Records found in message") + + # Extract key from Records[0].s3.object.key + try: + object_key = records[0]["s3"]["object"]["key"] + except (KeyError, IndexError) as e: + raise ValueError(f"Invalid message structure: {e}") from e + + # URL decode: rag-docs%2Farchivo.pdf -> rag-docs/archivo.pdf + decoded_path = unquote(object_key) + + return decoded_path + + +def message_callback(ch, method, properties, body): + """ + Callback function to process RabbitMQ messages. + + Args: + ch: Channel + method: Method + properties: Properties + body: Message body (bytes) + """ + try: + # Parse JSON message + message = json.loads(body) + logger.info(f"Received message from RabbitMQ") + logger.debug(f"Message content: {message}") + + # Extract PDF path + pdf_path = extract_pdf_path(message) + logger.info(f"Extracted PDF path: {pdf_path}") + + # Only process PDFs + if not pdf_path.lower().endswith('.pdf'): + logger.info(f"Skipping non-PDF file: {pdf_path}") + ch.basic_ack(delivery_tag=method.delivery_tag) + return + + # Call the existing pipeline + logger.info(f"Starting PDF processing pipeline for: {pdf_path}") + document_id = process_pdf_pipeline(pdf_path) + logger.info(f"PDF processed successfully: {pdf_path} -> Document ID: {document_id}") + + # Acknowledge the message + ch.basic_ack(delivery_tag=method.delivery_tag) + logger.info(f"Message acknowledged for: {pdf_path}") + + except json.JSONDecodeError as e: + logger.error(f"Failed to parse JSON message: {e}") + # NACK without requeue - malformed messages won't be fixed by retrying + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + except ValueError as e: + logger.error(f"Invalid message structure: {e}") + # NACK without requeue - invalid structure won't be fixed by retrying + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + except Exception as e: + logger.error(f"Error processing message: {e}", exc_info=True) + # NACK without requeue to avoid infinite loops + # In production, consider implementing a dead-letter queue + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + + +def start_consumer(): + """ + Start the RabbitMQ consumer to process PDF files. + + This function runs in a blocking loop and should be executed + in a separate thread or process. + """ + logger.info("Starting PDF processor consumer") + + try: + # Create RabbitMQ connection + rabbitmq = RabbitMQConnection() + rabbitmq.connect() + + # Start consuming messages + queue_name = settings.rabbitmq_queue_name + logger.info(f"Consuming messages from queue: {queue_name}") + + rabbitmq.consume_messages( + queue_name=queue_name, + callback=message_callback + ) + + except KeyboardInterrupt: + logger.info("Consumer interrupted by user") + except Exception as e: + logger.error(f"Fatal error in consumer: {e}", exc_info=True) + raise + diff --git a/RAGManager/main.py b/RAGManager/main.py index dd6914a..100a13d 100644 --- a/RAGManager/main.py +++ b/RAGManager/main.py @@ -1,9 +1,12 @@ import logging +import threading + from fastapi import FastAPI from app.api.routes import router as api_router from app.api.routes.base import router as base_router from app.core.database_connection import init_db +from app.workers.pdf_processor_consumer import start_consumer # Configure logging logging.basicConfig( @@ -21,10 +24,28 @@ @app.on_event("startup") async def startup_event(): - """Initialize database on startup.""" + """Initialize database and start RabbitMQ consumer on startup.""" try: init_db() logging.info("Database initialized successfully") + + # Start RabbitMQ consumer in a separate daemon thread + consumer_thread = threading.Thread(target=start_consumer, daemon=True) + consumer_thread.start() + logging.info("RabbitMQ consumer started successfully") + except Exception as e: - logging.error(f"Failed to initialize database: {e}") - raise \ No newline at end of file + logging.error(f"Failed to initialize: {e}") + raise + + +@app.get("/") +async def root(): + return {"message": "Hello World"} + + +@app.get("/health") +def health_check(): + return {"message": "200 corriendo..."} + + diff --git a/RAGManager/pyproject.toml b/RAGManager/pyproject.toml index 4525963..fb90f0e 100644 --- a/RAGManager/pyproject.toml +++ b/RAGManager/pyproject.toml @@ -1,22 +1,23 @@ [project] -name = "goland-ia" +name = "rag-manager" version = "0.1.0" -description = "Add your description here" +description = "RAG Manager for document processing" readme = "README.md" requires-python = ">=3.12,<3.13" dependencies = [ - "fastapi>=0.124.2", + "fastapi>=0.124.2", "ipython>=9.8.0", "langgraph>=1.0.4", "langchain-core>=0.3.0", "langchain-text-splitters>=0.3.0", - "langchain-postgres>=0.0.5", + "langchain-postgres>=0.0.14", + "langchain-openai>=0.3.0", "typing-extensions>=4.15.0", - "uvicorn>=0.38.0", "sqlalchemy>=2.0.0", - "psycopg2-binary>=2.9.0", + "psycopg2-binary>=2.9.9", "pgvector>=0.3.0", "pydantic-settings>=2.0.0", + "uvicorn>=0.38.0", "guardrails-ai>=0.5.10", "langchain>=1.2.0", "langchain-community>=0.4.1", @@ -25,6 +26,7 @@ dependencies = [ "minio>=7.2.20", "presidio-analyzer>=2.2.360", "presidio-anonymizer>=2.2.360", + "pika>=1.3.0", ] [project.optional-dependencies] @@ -33,14 +35,9 @@ dev = [ ] [tool.ruff] -# Enable pyproject.toml support -target-version = "py314" +target-version = "py312" line-length = 120 - -# Enable auto-fix fix = true - -# Exclude directories exclude = [ ".git", ".venv", @@ -52,7 +49,6 @@ exclude = [ ] [tool.ruff.lint] -# Enable a comprehensive set of rules select = [ "E", # pycodestyle errors "W", # pycodestyle warnings @@ -64,26 +60,16 @@ select = [ "C4", # flake8-comprehensions "SIM", # flake8-simplify ] - ignore = [ - "E501", # Line too long (handled by formatter) + "E501", # Line too long ] - -# Allow autofix for all enabled rules fixable = ["ALL"] unfixable = [] - -# Allow unused variables when underscore-prefixed dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" [tool.ruff.format] -# Use double quotes for strings (like Black) quote-style = "double" - -# Indent with spaces indent-style = "space" - -# Respect magic trailing commas skip-magic-trailing-comma = false # Automatically detect line ending diff --git a/RAGManager/uv.lock b/RAGManager/uv.lock index caa1305..519ef2d 100644 --- a/RAGManager/uv.lock +++ b/RAGManager/uv.lock @@ -144,6 +144,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, + { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -153,6 +169,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "blis" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/d0/d8cc8c9a4488a787e7fa430f6055e5bd1ddb22c340a751d9e901b82e2efe/blis-1.3.3.tar.gz", hash = "sha256:034d4560ff3cc43e8aa37e188451b0440e3261d989bb8a42ceee865607715ecd", size = 2644873, upload-time = "2025-11-17T12:28:30.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/d1/429cf0cf693d4c7dc2efed969bd474e315aab636e4a95f66c4ed7264912d/blis-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a1c74e100665f8e918ebdbae2794576adf1f691680b5cdb8b29578432f623ef", size = 6929663, upload-time = "2025-11-17T12:27:44.482Z" }, + { url = "https://files.pythonhosted.org/packages/11/69/363c8df8d98b3cc97be19aad6aabb2c9c53f372490d79316bdee92d476e7/blis-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f6c595185176ce021316263e1a1d636a3425b6c48366c1fd712d08d0b71849a", size = 1230939, upload-time = "2025-11-17T12:27:46.19Z" }, + { url = "https://files.pythonhosted.org/packages/96/2a/fbf65d906d823d839076c5150a6f8eb5ecbc5f9135e0b6510609bda1e6b7/blis-1.3.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d734b19fba0be7944f272dfa7b443b37c61f9476d9ab054a9ac53555ceadd2e0", size = 2818835, upload-time = "2025-11-17T12:27:48.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ad/58deaa3ad856dd3cc96493e40ffd2ed043d18d4d304f85a65cde1ccbf644/blis-1.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ef6d6e2b599a3a2788eb6d9b443533961265aa4ec49d574ed4bb846e548dcdb", size = 11366550, upload-time = "2025-11-17T12:27:49.958Z" }, + { url = "https://files.pythonhosted.org/packages/78/82/816a7adfe1f7acc8151f01ec86ef64467a3c833932d8f19f8e06613b8a4e/blis-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8c888438ae99c500422d50698e3028b65caa8ebb44e24204d87fda2df64058f7", size = 3023686, upload-time = "2025-11-17T12:27:52.062Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e2/0e93b865f648b5519360846669a35f28ee8f4e1d93d054f6850d8afbabde/blis-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8177879fd3590b5eecdd377f9deafb5dc8af6d684f065bd01553302fb3fcf9a7", size = 14250939, upload-time = "2025-11-17T12:27:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/20/07/fb43edc2ff0a6a367e4a94fc39eb3b85aa1e55e24cc857af2db145ce9f0d/blis-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:f20f7ad69aaffd1ce14fe77de557b6df9b61e0c9e582f75a843715d836b5c8af", size = 6192759, upload-time = "2025-11-17T12:27:56.176Z" }, +] + +[[package]] +name = "catalogue" +version = "2.0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b4/244d58127e1cdf04cf2dc7d9566f0d24ef01d5ce21811bab088ecc62b5ea/catalogue-2.0.10.tar.gz", hash = "sha256:4f56daa940913d3f09d589c191c74e5a6d51762b3a9e37dd53b7437afd6cda15", size = 19561, upload-time = "2023-09-25T06:29:24.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/96/d32b941a501ab566a16358d68b6eb4e4acc373fab3c3c4d7d9e649f7b4bb/catalogue-2.0.10-py3-none-any.whl", hash = "sha256:58c2de0020aa90f4a2da7dfad161bf7b3b054c86a5f09fcedc0b2b740c109a9f", size = 17325, upload-time = "2023-09-25T06:29:23.337Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -222,6 +265,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" }, ] +[[package]] +name = "cloudpathlib" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/18/2ac35d6b3015a0c74e923d94fc69baf8307f7c3233de015d69f99e17afa8/cloudpathlib-0.23.0.tar.gz", hash = "sha256:eb38a34c6b8a048ecfd2b2f60917f7cbad4a105b7c979196450c2f541f4d6b4b", size = 53126, upload-time = "2025-10-07T22:47:56.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8a/c4bb04426d608be4a3171efa2e233d2c59a5c8937850c10d098e126df18e/cloudpathlib-0.23.0-py3-none-any.whl", hash = "sha256:8520b3b01468fee77de37ab5d50b1b524ea6b4a8731c35d1b7407ac0cd716002", size = 62755, upload-time = "2025-10-07T22:47:54.905Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -231,45 +283,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "confection" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "srsly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/d3/57c6631159a1b48d273b40865c315cf51f89df7a9d1101094ef12e3a37c2/confection-0.1.5.tar.gz", hash = "sha256:8e72dd3ca6bd4f48913cd220f10b8275978e740411654b6e8ca6d7008c590f0e", size = 38924, upload-time = "2024-05-31T16:17:01.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/00/3106b1854b45bd0474ced037dfe6b73b90fe68a68968cef47c23de3d43d2/confection-0.1.5-py3-none-any.whl", hash = "sha256:e29d3c3f8eac06b3f77eb9dfb4bf2fc6bcc9622a98ca00a698e3d019c6430b14", size = 35451, upload-time = "2024-05-31T16:16:59.075Z" }, +] + [[package]] name = "cryptography" -version = "46.0.3" +version = "44.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/53/d6/1411ab4d6108ab167d06254c5be517681f1e331f90edf1379895bcb87020/cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053", size = 711096, upload-time = "2025-05-02T19:36:04.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/53/c776d80e9d26441bb3868457909b4e74dd9ccabd182e10b2b0ae7a07e265/cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88", size = 6670281, upload-time = "2025-05-02T19:34:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/6a/06/af2cf8d56ef87c77319e9086601bef621bedf40f6f59069e1b6d1ec498c5/cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137", size = 3959305, upload-time = "2025-05-02T19:34:53.042Z" }, + { url = "https://files.pythonhosted.org/packages/ae/01/80de3bec64627207d030f47bf3536889efee8913cd363e78ca9a09b13c8e/cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c", size = 4171040, upload-time = "2025-05-02T19:34:54.675Z" }, + { url = "https://files.pythonhosted.org/packages/bd/48/bb16b7541d207a19d9ae8b541c70037a05e473ddc72ccb1386524d4f023c/cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76", size = 3963411, upload-time = "2025-05-02T19:34:56.61Z" }, + { url = "https://files.pythonhosted.org/packages/42/b2/7d31f2af5591d217d71d37d044ef5412945a8a8e98d5a2a8ae4fd9cd4489/cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359", size = 3689263, upload-time = "2025-05-02T19:34:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/25/50/c0dfb9d87ae88ccc01aad8eb93e23cfbcea6a6a106a9b63a7b14c1f93c75/cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43", size = 4196198, upload-time = "2025-05-02T19:35:00.988Z" }, + { url = "https://files.pythonhosted.org/packages/66/c9/55c6b8794a74da652690c898cb43906310a3e4e4f6ee0b5f8b3b3e70c441/cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01", size = 3966502, upload-time = "2025-05-02T19:35:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f7/7cb5488c682ca59a02a32ec5f975074084db4c983f849d47b7b67cc8697a/cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d", size = 4196173, upload-time = "2025-05-02T19:35:05.018Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0b/2f789a8403ae089b0b121f8f54f4a3e5228df756e2146efdf4a09a3d5083/cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904", size = 4087713, upload-time = "2025-05-02T19:35:07.187Z" }, + { url = "https://files.pythonhosted.org/packages/1d/aa/330c13655f1af398fc154089295cf259252f0ba5df93b4bc9d9c7d7f843e/cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44", size = 4299064, upload-time = "2025-05-02T19:35:08.879Z" }, + { url = "https://files.pythonhosted.org/packages/10/a8/8c540a421b44fd267a7d58a1fd5f072a552d72204a3f08194f98889de76d/cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d", size = 2773887, upload-time = "2025-05-02T19:35:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0d/c4b1657c39ead18d76bbd122da86bd95bdc4095413460d09544000a17d56/cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d", size = 3209737, upload-time = "2025-05-02T19:35:12.12Z" }, + { url = "https://files.pythonhosted.org/packages/34/a3/ad08e0bcc34ad436013458d7528e83ac29910943cea42ad7dd4141a27bbb/cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f", size = 6673501, upload-time = "2025-05-02T19:35:13.775Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f0/7491d44bba8d28b464a5bc8cc709f25a51e3eac54c0a4444cf2473a57c37/cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759", size = 3960307, upload-time = "2025-05-02T19:35:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/e5c5d0e1364d3346a5747cdcd7ecbb23ca87e6dea4f942a44e88be349f06/cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645", size = 4170876, upload-time = "2025-05-02T19:35:18.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/96/025cb26fc351d8c7d3a1c44e20cf9a01e9f7cf740353c9c7a17072e4b264/cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2", size = 3964127, upload-time = "2025-05-02T19:35:19.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/44/eb6522db7d9f84e8833ba3bf63313f8e257729cf3a8917379473fcfd6601/cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54", size = 3689164, upload-time = "2025-05-02T19:35:21.449Z" }, + { url = "https://files.pythonhosted.org/packages/68/fb/d61a4defd0d6cee20b1b8a1ea8f5e25007e26aeb413ca53835f0cae2bcd1/cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93", size = 4198081, upload-time = "2025-05-02T19:35:23.187Z" }, + { url = "https://files.pythonhosted.org/packages/1b/50/457f6911d36432a8811c3ab8bd5a6090e8d18ce655c22820994913dd06ea/cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c", size = 3967716, upload-time = "2025-05-02T19:35:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/35/6e/dca39d553075980ccb631955c47b93d87d27f3596da8d48b1ae81463d915/cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f", size = 4197398, upload-time = "2025-05-02T19:35:27.678Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/d1f2fe681eabc682067c66a74addd46c887ebacf39038ba01f8860338d3d/cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5", size = 4087900, upload-time = "2025-05-02T19:35:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f5/3599e48c5464580b73b236aafb20973b953cd2e7b44c7c2533de1d888446/cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b", size = 4301067, upload-time = "2025-05-02T19:35:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/d2c48c8137eb39d0c193274db5c04a75dab20d2f7c3f81a7dcc3a8897701/cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028", size = 2775467, upload-time = "2025-05-02T19:35:33.805Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/51f212198681ea7b0deaaf8846ee10af99fba4e894f67b353524eab2bbe5/cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334", size = 3210375, upload-time = "2025-05-02T19:35:35.369Z" }, +] + +[[package]] +name = "cymem" +version = "2.0.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/2f0fbb32535c3731b7c2974c569fb9325e0a38ed5565a08e1139a3b71e82/cymem-2.0.13.tar.gz", hash = "sha256:1c91a92ae8c7104275ac26bd4d29b08ccd3e7faff5893d3858cb6fadf1bc1588", size = 12320, upload-time = "2025-11-14T14:58:36.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/52/478a2911ab5028cb710b4900d64aceba6f4f882fcb13fd8d40a456a1b6dc/cymem-2.0.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8afbc5162a0fe14b6463e1c4e45248a1b2fe2cbcecc8a5b9e511117080da0eb", size = 43745, upload-time = "2025-11-14T14:57:32.52Z" }, + { url = "https://files.pythonhosted.org/packages/f9/71/f0f8adee945524774b16af326bd314a14a478ed369a728a22834e6785a18/cymem-2.0.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9251d889348fe79a75e9b3e4d1b5fa651fca8a64500820685d73a3acc21b6a8", size = 42927, upload-time = "2025-11-14T14:57:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/62/6d/159780fe162ff715d62b809246e5fc20901cef87ca28b67d255a8d741861/cymem-2.0.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:742fc19764467a49ed22e56a4d2134c262d73a6c635409584ae3bf9afa092c33", size = 258346, upload-time = "2025-11-14T14:57:34.917Z" }, + { url = "https://files.pythonhosted.org/packages/eb/12/678d16f7aa1996f947bf17b8cfb917ea9c9674ef5e2bd3690c04123d5680/cymem-2.0.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f190a92fe46197ee64d32560eb121c2809bb843341733227f51538ce77b3410d", size = 260843, upload-time = "2025-11-14T14:57:36.503Z" }, + { url = "https://files.pythonhosted.org/packages/31/5d/0dd8c167c08cd85e70d274b7235cfe1e31b3cebc99221178eaf4bbb95c6f/cymem-2.0.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d670329ee8dbbbf241b7c08069fe3f1d3a1a3e2d69c7d05ea008a7010d826298", size = 254607, upload-time = "2025-11-14T14:57:38.036Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c9/d6514a412a1160aa65db539836b3d47f9b59f6675f294ec34ae32f867c82/cymem-2.0.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a84ba3178d9128b9ffb52ce81ebab456e9fe959125b51109f5b73ebdfc6b60d6", size = 262421, upload-time = "2025-11-14T14:57:39.265Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fe/3ee37d02ca4040f2fb22d34eb415198f955862b5dd47eee01df4c8f5454c/cymem-2.0.13-cp312-cp312-win_amd64.whl", hash = "sha256:2ff1c41fd59b789579fdace78aa587c5fc091991fa59458c382b116fc36e30dc", size = 40176, upload-time = "2025-11-14T14:57:40.706Z" }, + { url = "https://files.pythonhosted.org/packages/94/fb/1b681635bfd5f2274d0caa8f934b58435db6c091b97f5593738065ddb786/cymem-2.0.13-cp312-cp312-win_arm64.whl", hash = "sha256:6bbd701338df7bf408648191dff52472a9b334f71bcd31a21a41d83821050f67", size = 35959, upload-time = "2025-11-14T14:57:41.682Z" }, ] [[package]] @@ -419,56 +494,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" }, ] -[[package]] -name = "goland-ia" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "fastapi" }, - { name = "guardrails-ai" }, - { name = "ipython" }, - { name = "langchain" }, - { name = "langchain-community" }, - { name = "langchain-core" }, - { name = "langchain-text-splitters" }, - { name = "langgraph" }, - { name = "minio" }, - { name = "pdfplumber" }, - { name = "pgvector" }, - { name = "psycopg2-binary" }, - { name = "pydantic-settings" }, - { name = "sqlalchemy" }, - { name = "typing-extensions" }, - { name = "uvicorn" }, -] - -[package.optional-dependencies] -dev = [ - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "fastapi", specifier = ">=0.124.2" }, - { name = "guardrails-ai", specifier = ">=0.5.10" }, - { name = "ipython", specifier = ">=9.8.0" }, - { name = "langchain", specifier = ">=1.2.0" }, - { name = "langchain-community", specifier = ">=0.4.1" }, - { name = "langchain-core", specifier = ">=0.3.0" }, - { name = "langchain-text-splitters", specifier = ">=1.1.0" }, - { name = "langgraph", specifier = ">=1.0.4" }, - { name = "minio", specifier = ">=7.2.20" }, - { name = "pdfplumber", specifier = ">=0.11.8" }, - { name = "pgvector", specifier = ">=0.3.0" }, - { name = "psycopg2-binary", specifier = ">=2.9.0" }, - { name = "pydantic-settings", specifier = ">=2.0.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.14.0" }, - { name = "sqlalchemy", specifier = ">=2.0.0" }, - { name = "typing-extensions", specifier = ">=4.15.0" }, - { name = "uvicorn", specifier = ">=0.38.0" }, -] -provides-extras = ["dev"] - [[package]] name = "googleapis-common-protos" version = "1.72.0" @@ -922,6 +947,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/97/57497c8b26829e38c8dd4abe972d75e38fc3904324a3042bb01d9e0753b8/langchain_core-1.2.2-py3-none-any.whl", hash = "sha256:3a83dc14217de5cba11b1a0bd43c48702401bbd18dc25cac2ffab5ac83a61cd0", size = 476125, upload-time = "2025-12-16T20:25:52.581Z" }, ] +[[package]] +name = "langchain-openai" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "openai" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/1c/008a6dd7b3523121be1a4f24701b099ae79193dab9b329dfb787bece08bf/langchain_openai-1.1.5.tar.gz", hash = "sha256:a8ca5f3919bd948867c7d427a575b34f7c141110ef7cbc14ea7bbc46363871de", size = 1038129, upload-time = "2025-12-17T19:14:36.392Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/c5/22b690a27ba6b1ca6876270473aab1610cb8767314e5038cb6b826d9b69b/langchain_openai-1.1.5-py3-none-any.whl", hash = "sha256:d3a3b0c39e1513bbb9e5d4526c194909a00c5733195dbe90bfea6619b00420ca", size = 84569, upload-time = "2025-12-17T19:14:35.529Z" }, +] + +[[package]] +name = "langchain-postgres" +version = "0.0.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asyncpg" }, + { name = "langchain-core" }, + { name = "numpy" }, + { name = "pgvector" }, + { name = "psycopg", extra = ["binary"] }, + { name = "psycopg-pool" }, + { name = "sqlalchemy", extra = ["asyncio"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/5e/00065782aa0ad7b5faa9ff6881bcf361f2a7741e39db8e2b3e86164f80c8/langchain_postgres-0.0.16.tar.gz", hash = "sha256:d09aa4ea77ee8600a9ff64de9c185fb558aa388c816c7be04dd4559c878530b7", size = 232526, upload-time = "2025-10-17T15:00:29.203Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a2/516934f8be231e50bb2afda8112641850154b057f5c45d82f42d216bed3d/langchain_postgres-0.0.16-py3-none-any.whl", hash = "sha256:a7375cf9fc9b6965efc207dbcc959424e96b8ffe75d5ced6055676d2613f8d37", size = 46028, upload-time = "2025-10-17T15:00:27.772Z" }, +] + [[package]] name = "langchain-text-splitters" version = "1.1.0" @@ -1183,6 +1240,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, ] +[[package]] +name = "murmurhash" +version = "1.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/2e/88c147931ea9725d634840d538622e94122bceaf346233349b7b5c62964b/murmurhash-1.0.15.tar.gz", hash = "sha256:58e2b27b7847f9e2a6edf10b47a8c8dd70a4705f45dccb7bf76aeadacf56ba01", size = 13291, upload-time = "2025-11-14T09:51:15.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/46/be8522d3456fdccf1b8b049c6d82e7a3c1114c4fc2cfe14b04cba4b3e701/murmurhash-1.0.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d37e3ae44746bca80b1a917c2ea625cf216913564ed43f69d2888e5df97db0cb", size = 27884, upload-time = "2025-11-14T09:50:13.133Z" }, + { url = "https://files.pythonhosted.org/packages/ed/cc/630449bf4f6178d7daf948ce46ad00b25d279065fc30abd8d706be3d87e0/murmurhash-1.0.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0861cb11039409eaf46878456b7d985ef17b6b484103a6fc367b2ecec846891d", size = 27855, upload-time = "2025-11-14T09:50:14.859Z" }, + { url = "https://files.pythonhosted.org/packages/ff/30/ea8f601a9bf44db99468696efd59eb9cff1157cd55cb586d67116697583f/murmurhash-1.0.15-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5a301decfaccfec70fe55cb01dde2a012c3014a874542eaa7cc73477bb749616", size = 134088, upload-time = "2025-11-14T09:50:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/c9/de/c40ce8c0877d406691e735b8d6e9c815f36a82b499d358313db5dbe219d7/murmurhash-1.0.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32c6fde7bd7e9407003370a07b5f4addacabe1556ad3dc2cac246b7a2bba3400", size = 133978, upload-time = "2025-11-14T09:50:17.572Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/bd49963ecd84ebab2fe66595e2d1ed41d5e8b5153af5dc930f0bd827007c/murmurhash-1.0.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d8b43a7011540dc3c7ce66f2134df9732e2bc3bbb4a35f6458bc755e48bde26", size = 132956, upload-time = "2025-11-14T09:50:18.742Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7c/2530769c545074417c862583f05f4245644599f1e9ff619b3dfe2969aafc/murmurhash-1.0.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43bf4541892ecd95963fcd307bf1c575fc0fee1682f41c93007adee71ca2bb40", size = 134184, upload-time = "2025-11-14T09:50:19.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/a4/b249b042f5afe34d14ada2dc4afc777e883c15863296756179652e081c44/murmurhash-1.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:f4ac15a2089dc42e6eb0966622d42d2521590a12c92480aafecf34c085302cca", size = 25647, upload-time = "2025-11-14T09:50:21.049Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/028179259aebc18fd4ba5cae2601d1d47517427a537ab44336446431a215/murmurhash-1.0.15-cp312-cp312-win_arm64.whl", hash = "sha256:4a70ca4ae19e600d9be3da64d00710e79dde388a4d162f22078d64844d0ebdda", size = 23338, upload-time = "2025-11-14T09:50:22.359Z" }, +] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -1429,14 +1502,23 @@ wheels = [ [[package]] name = "pgvector" -version = "0.4.2" +version = "0.3.6" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/d8/fd6009cee3e03214667df488cdcf9609461d729968da94e4f95d6359d304/pgvector-0.3.6.tar.gz", hash = "sha256:31d01690e6ea26cea8a633cde5f0f55f5b246d9c8292d68efdef8c22ec994ade", size = 25421, upload-time = "2024-10-27T00:15:09.632Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/fb/81/f457d6d361e04d061bef413749a6e1ab04d98cfeec6d8abcfe40184750f3/pgvector-0.3.6-py3-none-any.whl", hash = "sha256:f6c269b3c110ccb7496bac87202148ed18f34b390a0189c783e351062400a75a", size = 24880, upload-time = "2024-10-27T00:15:08.045Z" }, +] + +[[package]] +name = "phonenumbers" +version = "9.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/2a/d6e7fecd1bcd37e65fbb9386e492082bd84e196c9cab14e39166b35cea4b/phonenumbers-9.0.20.tar.gz", hash = "sha256:849788eec8e5a9737a99c8b906d18a62d9fced6497ba7033784b6a7e4c89bb2d", size = 2298276, upload-time = "2025-12-05T12:03:51.072Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/b0/e3fd0ae68bc51593ef6f77a44901d1fa14c1b230cdce848b50ed10881ca3/phonenumbers-9.0.20-py2.py3-none-any.whl", hash = "sha256:03bf5dd14891891284ba96f803d0e5e7e11b9306a0ec4fdf25756ada39eacb86", size = 2584219, upload-time = "2025-12-05T12:03:48.388Z" }, ] [[package]] @@ -1467,6 +1549,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622, upload-time = "2025-10-25T00:55:39.247Z" }, ] +[[package]] +name = "preshed" +version = "3.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cymem" }, + { name = "murmurhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/34/eb4f5f0f678e152a96e826da867d2f41c4b18a2d589e40e1dd3347219e91/preshed-3.0.12.tar.gz", hash = "sha256:b73f9a8b54ee1d44529cc6018356896cff93d48f755f29c134734d9371c0d685", size = 15027, upload-time = "2025-11-17T13:00:33.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/f7/ff3aca937eeaee19c52c45ddf92979546e52ed0686e58be4bc09c47e7d88/preshed-3.0.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2779861f5d69480493519ed123a622a13012d1182126779036b99d9d989bf7e9", size = 129958, upload-time = "2025-11-17T12:59:33.391Z" }, + { url = "https://files.pythonhosted.org/packages/80/24/fd654a9c0f5f3ed1a9b1d8a392f063ae9ca29ad0b462f0732ae0147f7cee/preshed-3.0.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffe1fd7d92f51ed34383e20d8b734780c814ca869cfdb7e07f2d31651f90cdf4", size = 124550, upload-time = "2025-11-17T12:59:34.688Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/8271c7f680696f4b0880f44357d2a903d649cb9f6e60a1efc97a203104df/preshed-3.0.12-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:91893404858502cc4e856d338fef3d2a4a552135f79a1041c24eb919817c19db", size = 874987, upload-time = "2025-11-17T12:59:36.062Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a5/ca200187ca1632f1e2c458b72f1bd100fa8b55deecd5d72e1e4ebf09e98c/preshed-3.0.12-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9e06e8f2ba52f183eb9817a616cdebe84a211bb859a2ffbc23f3295d0b189638", size = 866499, upload-time = "2025-11-17T12:59:37.586Z" }, + { url = "https://files.pythonhosted.org/packages/87/a1/943b61f850c44899910c21996cb542d0ef5931744c6d492fdfdd8457e693/preshed-3.0.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbe8b8a2d4f9af14e8a39ecca524b9de6defc91d8abcc95eb28f42da1c23272c", size = 878064, upload-time = "2025-11-17T12:59:39.651Z" }, + { url = "https://files.pythonhosted.org/packages/3e/75/d7fff7f1fa3763619aa85d6ba70493a5d9c6e6ea7958a6e8c9d3e6e88bbe/preshed-3.0.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5d0aaac9c5862f5471fddd0c931dc64d3af2efc5fe3eb48b50765adb571243b9", size = 900540, upload-time = "2025-11-17T12:59:41.384Z" }, + { url = "https://files.pythonhosted.org/packages/e4/12/a2285b78bd097a1e53fb90a1743bc8ce0d35e5b65b6853f3b3c47da398ca/preshed-3.0.12-cp312-cp312-win_amd64.whl", hash = "sha256:0eb8d411afcb1e3b12a0602fb6a0e33140342a732a795251a0ce452aba401dc0", size = 118298, upload-time = "2025-11-17T12:59:42.65Z" }, + { url = "https://files.pythonhosted.org/packages/0b/34/4e8443fe99206a2fcfc63659969a8f8c8ab184836533594a519f3899b1ad/preshed-3.0.12-cp312-cp312-win_arm64.whl", hash = "sha256:dcd3d12903c9f720a39a5c5f1339f7f46e3ab71279fb7a39776768fb840b6077", size = 104746, upload-time = "2025-11-17T12:59:43.934Z" }, +] + +[[package]] +name = "presidio-analyzer" +version = "2.2.360" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "phonenumbers" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "spacy" }, + { name = "tldextract" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/dd/df0d00ff44977868878f391b08feb65b8269904262d0f1387de827c13c7d/presidio_analyzer-2.2.360-py3-none-any.whl", hash = "sha256:aa6e83779b8f23587000ccfa3ef47aa34356a60b51284742e0d2a96b33205c4e", size = 128661, upload-time = "2025-09-09T09:24:20.75Z" }, +] + +[[package]] +name = "presidio-anonymizer" +version = "2.2.360" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/79/d8438168402057092a36ba8f7aee099f107a7960a38eee505f1948f9e0c9/presidio_anonymizer-2.2.360-py3-none-any.whl", hash = "sha256:5f46f36bf3a0b8817b91f09ed3e51093cc331192e148311f1cd69ae0bf3f4a6c", size = 35864, upload-time = "2025-09-09T09:24:12.176Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -1519,22 +1647,51 @@ wheels = [ ] [[package]] -name = "psycopg2-binary" -version = "2.9.11" +name = "psycopg" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, - { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, - { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, - { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, - { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, - { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/4e/1e/8614b01c549dd7e385dacdcd83fe194f6b3acb255a53cc67154ee6bf00e7/psycopg_binary-3.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9387ab615f929e71ef0f4a8a51e986fa06236ccfa9f3ec98a88f60fbf230634", size = 4579832, upload-time = "2025-12-06T17:33:01.388Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/0bb093570fae2f4454d42c1ae6000f15934391867402f680254e4a7def54/psycopg_binary-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ff7489df5e06c12d1829544eaec64970fe27fe300f7cf04c8495fe682064688", size = 4658786, upload-time = "2025-12-06T17:33:05.022Z" }, + { url = "https://files.pythonhosted.org/packages/61/20/1d9383e3f2038826900a14137b0647d755f67551aab316e1021443105ed5/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9742580ecc8e1ac45164e98d32ca6df90da509c2d3ff26be245d94c430f92db4", size = 5454896, upload-time = "2025-12-06T17:33:09.023Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/513c80ad8bbb545e364f7737bf2492d34a4c05eef4f7b5c16428dc42260d/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d45acedcaa58619355f18e0f42af542fcad3fd84ace4b8355d3a5dea23318578", size = 5132731, upload-time = "2025-12-06T17:33:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/f3/28/ddf5f5905f088024bccb19857949467407c693389a14feb527d6171d8215/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d88f32ff8c47cb7f4e7e7a9d1747dcee6f3baa19ed9afa9e5694fd2fb32b61ed", size = 6724495, upload-time = "2025-12-06T17:33:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/6e/93/a1157ebcc650960b264542b547f7914d87a42ff0cc15a7584b29d5807e6b/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59d0163c4617a2c577cb34afbed93d7a45b8c8364e54b2bd2020ff25d5f5f860", size = 4964979, upload-time = "2025-12-06T17:33:20.179Z" }, + { url = "https://files.pythonhosted.org/packages/0e/27/65939ba6798f9c5be4a5d9cd2061ebaf0851798525c6811d347821c8132d/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e750afe74e6c17b2c7046d2c3e3173b5a3f6080084671c8aa327215323df155b", size = 4493648, upload-time = "2025-12-06T17:33:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c4/5e9e4b9b1c1e27026e43387b0ba4aaf3537c7806465dd3f1d5bde631752a/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f26f113013c4dcfbfe9ced57b5bad2035dda1a7349f64bf726021968f9bccad3", size = 4173392, upload-time = "2025-12-06T17:33:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/c6/81/cf43fb76993190cee9af1cbcfe28afb47b1928bdf45a252001017e5af26e/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8309ee4569dced5e81df5aa2dcd48c7340c8dee603a66430f042dfbd2878edca", size = 3909241, upload-time = "2025-12-06T17:33:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/c6377a0d17434674351627489deca493ea0b137c522b99c81d3a106372c8/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6464150e25b68ae3cb04c4e57496ea11ebfaae4d98126aea2f4702dd43e3c12", size = 4219746, upload-time = "2025-12-06T17:33:33.097Z" }, + { url = "https://files.pythonhosted.org/packages/25/32/716c57b28eefe02a57a4c9d5bf956849597f5ea476c7010397199e56cfde/psycopg_binary-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:716a586f99bbe4f710dc58b40069fcb33c7627e95cc6fc936f73c9235e07f9cf", size = 3537494, upload-time = "2025-12-06T17:33:35.82Z" }, +] + +[[package]] +name = "psycopg-pool" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/9a/9470d013d0d50af0da9c4251614aeb3c1823635cab3edc211e3839db0bcf/psycopg_pool-3.3.0.tar.gz", hash = "sha256:fa115eb2860bd88fce1717d75611f41490dec6135efb619611142b24da3f6db5", size = 31606, upload-time = "2025-12-01T11:34:33.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c3/26b8a0908a9db249de3b4169692e1c7c19048a9bc41a4d3209cee7dbb758/psycopg_pool-3.3.0-py3-none-any.whl", hash = "sha256:2e44329155c410b5e8666372db44276a8b1ebd8c90f1c3026ebba40d4bc81063", size = 39995, upload-time = "2025-12-01T11:34:29.761Z" }, ] [[package]] @@ -1739,6 +1896,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, ] +[[package]] +name = "rag-manager" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "guardrails-ai" }, + { name = "ipython" }, + { name = "langchain" }, + { name = "langchain-community" }, + { name = "langchain-core" }, + { name = "langchain-openai" }, + { name = "langchain-postgres" }, + { name = "langchain-text-splitters" }, + { name = "langgraph" }, + { name = "minio" }, + { name = "pdfplumber" }, + { name = "pgvector" }, + { name = "presidio-analyzer" }, + { name = "presidio-anonymizer" }, + { name = "psycopg", extra = ["binary"] }, + { name = "pydantic-settings" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, + { name = "uvicorn" }, +] + +[package.optional-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.124.2" }, + { name = "guardrails-ai", specifier = ">=0.5.10" }, + { name = "ipython", specifier = ">=9.8.0" }, + { name = "langchain", specifier = ">=0.3.0" }, + { name = "langchain", specifier = ">=1.2.0" }, + { name = "langchain-community", specifier = ">=0.4.1" }, + { name = "langchain-core", specifier = ">=0.3.0" }, + { name = "langchain-openai", specifier = ">=0.3.0" }, + { name = "langchain-postgres", specifier = ">=0.0.14" }, + { name = "langchain-text-splitters", specifier = ">=0.3.0" }, + { name = "langchain-text-splitters", specifier = ">=1.1.0" }, + { name = "langgraph", specifier = ">=1.0.4" }, + { name = "minio", specifier = ">=7.2.20" }, + { name = "pdfplumber", specifier = ">=0.11.8" }, + { name = "pgvector", specifier = ">=0.3.0" }, + { name = "presidio-analyzer", specifier = ">=2.2.360" }, + { name = "presidio-anonymizer", specifier = ">=2.2.360" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.0.0" }, + { name = "pydantic-settings", specifier = ">=2.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.14.0" }, + { name = "sqlalchemy", specifier = ">=2.0.0" }, + { name = "typing-extensions", specifier = ">=4.15.0" }, + { name = "uvicorn", specifier = ">=0.38.0" }, +] +provides-extras = ["dev"] + [[package]] name = "referencing" version = "0.37.0" @@ -1790,6 +2007,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-file" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/f8/5dc70102e4d337063452c82e1f0d95e39abfe67aa222ed8a5ddeb9df8de8/requests_file-3.0.1.tar.gz", hash = "sha256:f14243d7796c588f3521bd423c5dea2ee4cc730e54a3cac9574d78aca1272576", size = 6967, upload-time = "2025-10-20T18:56:42.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d5/de8f089119205a09da657ed4784c584ede8381a0ce6821212a6d4ca47054/requests_file-3.0.1-py2.py3-none-any.whl", hash = "sha256:d0f5eb94353986d998f80ac63c7f146a307728be051d4d1cd390dbdb59c10fa2", size = 4514, upload-time = "2025-10-20T18:56:41.184Z" }, +] + [[package]] name = "requests-toolbelt" version = "1.0.0" @@ -1915,6 +2144,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, ] +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -1933,6 +2171,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "smart-open" +version = "7.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/9a/0a7acb748b86e2922982366d780ca4b16c33f7246fa5860d26005c97e4f3/smart_open-7.5.0.tar.gz", hash = "sha256:f394b143851d8091011832ac8113ea4aba6b92e6c35f6e677ddaaccb169d7cb9", size = 53920, upload-time = "2025-11-08T21:38:40.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/95/bc978be7ea0babf2fb48a414b6afaad414c6a9e8b1eafc5b8a53c030381a/smart_open-7.5.0-py3-none-any.whl", hash = "sha256:87e695c5148bbb988f15cec00971602765874163be85acb1c9fb8abc012e6599", size = 63940, upload-time = "2025-11-08T21:38:39.024Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1942,6 +2192,60 @@ 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 = "spacy" +version = "3.8.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "catalogue" }, + { name = "cymem" }, + { name = "jinja2" }, + { name = "murmurhash" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "preshed" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "spacy-legacy" }, + { name = "spacy-loggers" }, + { name = "srsly" }, + { name = "thinc" }, + { name = "tqdm" }, + { name = "typer-slim" }, + { name = "wasabi" }, + { name = "weasel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/9f/424244b0e2656afc9ff82fb7a96931a47397bfce5ba382213827b198312a/spacy-3.8.11.tar.gz", hash = "sha256:54e1e87b74a2f9ea807ffd606166bf29ac45e2bd81ff7f608eadc7b05787d90d", size = 1326804, upload-time = "2025-11-17T20:40:03.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/fb/01eadf4ba70606b3054702dc41fc2ccf7d70fb14514b3cd57f0ff78ebea8/spacy-3.8.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aa1ee8362074c30098feaaf2dd888c829a1a79c4311eec1b117a0a61f16fa6dd", size = 6073726, upload-time = "2025-11-17T20:39:01.679Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f8/07b03a2997fc2621aaeafae00af50f55522304a7da6926b07027bb6d0709/spacy-3.8.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75a036d04c2cf11d6cb566c0a689860cc5a7a75b439e8fea1b3a6b673dabf25d", size = 5724702, upload-time = "2025-11-17T20:39:03.486Z" }, + { url = "https://files.pythonhosted.org/packages/13/0c/c4fa0f379dbe3258c305d2e2df3760604a9fcd71b34f8f65c23e43f4cf55/spacy-3.8.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cb599d2747d4a59a5f90e8a453c149b13db382a8297925cf126333141dbc4f7", size = 32727774, upload-time = "2025-11-17T20:39:05.894Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8e/6a4ba82bed480211ebdf5341b0f89e7271b454307525ac91b5e447825914/spacy-3.8.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:94632e302ad2fb79dc285bf1e9e4d4a178904d5c67049e0e02b7fb4a77af85c4", size = 33215053, upload-time = "2025-11-17T20:39:08.588Z" }, + { url = "https://files.pythonhosted.org/packages/a6/bc/44d863d248e9d7358c76a0aa8b3f196b8698df520650ed8de162e18fbffb/spacy-3.8.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aeca6cf34009d48cda9fb1bbfb532469e3d643817241a73e367b34ab99a5806f", size = 32074195, upload-time = "2025-11-17T20:39:11.601Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7d/0b115f3f16e1dd2d3f99b0f89497867fc11c41aed94f4b7a4367b4b54136/spacy-3.8.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:368a79b8df925b15d89dccb5e502039446fb2ce93cf3020e092d5b962c3349b9", size = 32996143, upload-time = "2025-11-17T20:39:14.705Z" }, + { url = "https://files.pythonhosted.org/packages/7d/48/7e9581b476df76aaf9ee182888d15322e77c38b0bbbd5e80160ba0bddd4c/spacy-3.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:88d65941a87f58d75afca1785bd64d01183a92f7269dcbcf28bd9d6f6a77d1a7", size = 14217511, upload-time = "2025-11-17T20:39:17.316Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1f/307a16f32f90aa5ee7ad8d29ff8620a57132b80a4c8c536963d46d192e1a/spacy-3.8.11-cp312-cp312-win_arm64.whl", hash = "sha256:97b865d6d3658e2ab103a67d6c8a2d678e193e84a07f40d9938565b669ceee39", size = 13614446, upload-time = "2025-11-17T20:39:19.748Z" }, +] + +[[package]] +name = "spacy-legacy" +version = "3.0.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/79/91f9d7cc8db5642acad830dcc4b49ba65a7790152832c4eceb305e46d681/spacy-legacy-3.0.12.tar.gz", hash = "sha256:b37d6e0c9b6e1d7ca1cf5bc7152ab64a4c4671f59c85adaf7a3fcb870357a774", size = 23806, upload-time = "2023-01-23T09:04:15.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/55/12e842c70ff8828e34e543a2c7176dac4da006ca6901c9e8b43efab8bc6b/spacy_legacy-3.0.12-py2.py3-none-any.whl", hash = "sha256:476e3bd0d05f8c339ed60f40986c07387c0a71479245d6d0f4298dbd52cda55f", size = 29971, upload-time = "2023-01-23T09:04:13.45Z" }, +] + +[[package]] +name = "spacy-loggers" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/3d/926db774c9c98acf66cb4ed7faf6c377746f3e00b84b700d0868b95d0712/spacy-loggers-1.0.5.tar.gz", hash = "sha256:d60b0bdbf915a60e516cc2e653baeff946f0cfc461b452d11a4d5458c6fe5f24", size = 20811, upload-time = "2023-09-11T12:26:52.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/78/d1a1a026ef3af911159398c939b1509d5c36fe524c7b644f34a5146c4e16/spacy_loggers-1.0.5-py3-none-any.whl", hash = "sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645", size = 22343, upload-time = "2023-09-11T12:26:50.586Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.45" @@ -1961,6 +2265,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, ] +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, +] + +[[package]] +name = "srsly" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "catalogue" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/77/5633c4ba65e3421b72b5b4bd93aa328360b351b3a1e5bf3c90eb224668e5/srsly-2.5.2.tar.gz", hash = "sha256:4092bc843c71b7595c6c90a0302a197858c5b9fe43067f62ae6a45bc3baa1c19", size = 492055, upload-time = "2025-11-17T14:11:02.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/1c/21f658d98d602a559491b7886c7ca30245c2cd8987ff1b7709437c0f74b1/srsly-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f92b4f883e6be4ca77f15980b45d394d310f24903e25e1b2c46df783c7edcce", size = 656161, upload-time = "2025-11-17T14:10:03.181Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a2/bc6fd484ed703857043ae9abd6c9aea9152f9480a6961186ee6c1e0c49e8/srsly-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ac4790a54b00203f1af5495b6b8ac214131139427f30fcf05cf971dde81930eb", size = 653237, upload-time = "2025-11-17T14:10:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ea/e3895da29a15c8d325e050ad68a0d1238eece1d2648305796adf98dcba66/srsly-2.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce5c6b016050857a7dd365c9dcdd00d96e7ac26317cfcb175db387e403de05bf", size = 1174418, upload-time = "2025-11-17T14:10:05.945Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a5/21996231f53ee97191d0746c3a672ba33a4d86a19ffad85a1c0096c91c5f/srsly-2.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:539c6d0016e91277b5e9be31ebed03f03c32580d49c960e4a92c9003baecf69e", size = 1183089, upload-time = "2025-11-17T14:10:07.335Z" }, + { url = "https://files.pythonhosted.org/packages/7b/df/eb17aa8e4a828e8df7aa7dc471295529d9126e6b710f1833ebe0d8568a8e/srsly-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f24b2c4f4c29da04083f09158543eb3f8893ba0ac39818693b3b259ee8044f0", size = 1122594, upload-time = "2025-11-17T14:10:08.899Z" }, + { url = "https://files.pythonhosted.org/packages/80/74/1654a80e6c8ec3ee32370ea08a78d3651e0ba1c4d6e6be31c9efdb9a2d10/srsly-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d34675047460a3f6999e43478f40d9b43917ea1e93a75c41d05bf7648f3e872d", size = 1139594, upload-time = "2025-11-17T14:10:10.286Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/8393344ca7f0e81965febba07afc5cad68335ed0426408d480b861ab915b/srsly-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:81fd133ba3c66c07f0e3a889d2b4c852984d71ea833a665238a9d47d8e051ba5", size = 654750, upload-time = "2025-11-17T14:10:11.637Z" }, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -1997,6 +2324,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] +[[package]] +name = "thinc" +version = "8.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blis" }, + { name = "catalogue" }, + { name = "confection" }, + { name = "cymem" }, + { name = "murmurhash" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "preshed" }, + { name = "pydantic" }, + { name = "setuptools" }, + { name = "srsly" }, + { name = "wasabi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/3a/2d0f0be132b9faaa6d56f04565ae122684273e4bf4eab8dee5f48dc00f68/thinc-8.3.10.tar.gz", hash = "sha256:5a75109f4ee1c968fc055ce651a17cb44b23b000d9e95f04a4d047ab3cb3e34e", size = 194196, upload-time = "2025-11-17T17:21:46.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/34/ba3b386d92edf50784b60ee34318d47c7f49c198268746ef7851c5bbe8cf/thinc-8.3.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51bc6ef735bdbcab75ab2916731b8f61f94c66add6f9db213d900d3c6a244f95", size = 794509, upload-time = "2025-11-17T17:21:03.21Z" }, + { url = "https://files.pythonhosted.org/packages/07/f3/9f52d18115cd9d8d7b2590d226cb2752d2a5ffec61576b19462b48410184/thinc-8.3.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4f48b4d346915f98e9722c0c50ef911cc16c6790a2b7afebc6e1a2c96a6ce6c6", size = 741084, upload-time = "2025-11-17T17:21:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9c/129c2b740c4e3d3624b6fb3dec1577ef27cb804bc1647f9bc3e1801ea20c/thinc-8.3.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5003f4db2db22cc8d686db8db83509acc3c50f4c55ebdcb2bbfcc1095096f7d2", size = 3846337, upload-time = "2025-11-17T17:21:06.079Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/738cf188dea8240c2be081c83ea47270fea585eba446171757d2cdb9b675/thinc-8.3.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b12484c3ed0632331fada2c334680dd6bc35972d0717343432dfc701f04a9b4c", size = 3901216, upload-time = "2025-11-17T17:21:07.842Z" }, + { url = "https://files.pythonhosted.org/packages/22/92/32f66eb9b1a29b797bf378a0874615d810d79eefca1d6c736c5ca3f8b918/thinc-8.3.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8677c446d3f9b97a465472c58683b785b25dfcf26c683e3f4e8f8c7c188e4362", size = 4827286, upload-time = "2025-11-17T17:21:09.62Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/7ceae1e1f2029efd67ed88e23cd6dc13a5ee647cdc2b35113101b2a62c10/thinc-8.3.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:759c385ac08dcf950238b60b96a28f9c04618861141766928dff4a51b1679b25", size = 5024421, upload-time = "2025-11-17T17:21:11.199Z" }, + { url = "https://files.pythonhosted.org/packages/0b/66/30f9d8d41049b78bc614213d492792fbcfeb1b28642adf661c42110a7ebd/thinc-8.3.10-cp312-cp312-win_amd64.whl", hash = "sha256:bf3f188c3fa1fdcefd547d1f90a1245c29025d6d0e3f71d7fdf21dad210b990c", size = 1718631, upload-time = "2025-11-17T17:21:12.965Z" }, + { url = "https://files.pythonhosted.org/packages/f8/44/32e2a5018a1165a304d25eb9b1c74e5310da19a533a35331e8d824dc6a88/thinc-8.3.10-cp312-cp312-win_arm64.whl", hash = "sha256:234b7e57a6ef4e0260d99f4e8fdc328ed12d0ba9bbd98fdaa567294a17700d1c", size = 1642224, upload-time = "2025-11-17T17:21:14.371Z" }, +] + [[package]] name = "tiktoken" version = "0.12.0" @@ -2016,6 +2373,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, ] +[[package]] +name = "tldextract" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "idna" }, + { name = "requests" }, + { name = "requests-file" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/78/182641ea38e3cfd56e9c7b3c0d48a53d432eea755003aa544af96403d4ac/tldextract-5.3.0.tar.gz", hash = "sha256:b3d2b70a1594a0ecfa6967d57251527d58e00bb5a91a74387baa0d87a0678609", size = 128502, upload-time = "2025-04-22T06:19:37.491Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/7c/ea488ef48f2f544566947ced88541bc45fae9e0e422b2edbf165ee07da99/tldextract-5.3.0-py3-none-any.whl", hash = "sha256:f70f31d10b55c83993f55e91ecb7c5d84532a8972f22ec578ecfbe5ea2292db2", size = 107384, upload-time = "2025-04-22T06:19:36.304Z" }, +] + [[package]] name = "tokenizers" version = "0.22.1" @@ -2186,6 +2558,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, ] +[[package]] +name = "wasabi" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/f9/054e6e2f1071e963b5e746b48d1e3727470b2a490834d18ad92364929db3/wasabi-1.1.3.tar.gz", hash = "sha256:4bb3008f003809db0c3e28b4daf20906ea871a2bb43f9914197d540f4f2e0878", size = 30391, upload-time = "2024-05-31T16:56:18.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/7c/34330a89da55610daa5f245ddce5aab81244321101614751e7537f125133/wasabi-1.1.3-py3-none-any.whl", hash = "sha256:f76e16e8f7e79f8c4c8be49b4024ac725713ab10cd7f19350ad18a8e3f71728c", size = 27880, upload-time = "2024-05-31T16:56:16.699Z" }, +] + [[package]] name = "wcwidth" version = "0.2.14" @@ -2195,6 +2579,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, ] +[[package]] +name = "weasel" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpathlib" }, + { name = "confection" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "smart-open" }, + { name = "srsly" }, + { name = "typer-slim" }, + { name = "wasabi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/d7/edd9c24e60cf8e5de130aa2e8af3b01521f4d0216c371d01212f580d0d8e/weasel-0.4.3.tar.gz", hash = "sha256:f293d6174398e8f478c78481e00c503ee4b82ea7a3e6d0d6a01e46a6b1396845", size = 38733, upload-time = "2025-11-13T23:52:28.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/74/a148b41572656904a39dfcfed3f84dd1066014eed94e209223ae8e9d088d/weasel-0.4.3-py3-none-any.whl", hash = "sha256:08f65b5d0dbded4879e08a64882de9b9514753d9eaa4c4e2a576e33666ac12cf", size = 50757, upload-time = "2025-11-13T23:52:26.982Z" }, +] + [[package]] name = "webcolors" version = "25.10.0" @@ -2204,6 +2608,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, ] +[[package]] +name = "wrapt" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, + { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, + { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +] + [[package]] name = "xxhash" version = "3.6.0"