From 1cd8b0849d5fba9efaaa2a88d773e0f982d8c0b7 Mon Sep 17 00:00:00 2001 From: Mohammed-Balkhair-hub Date: Sun, 1 Feb 2026 12:50:56 +0300 Subject: [PATCH] built- evaluation agent --- services/ai-service/.gitignore | 2 +- services/ai-service/README.md | 7 + services/ai-service/pyproject.toml | 9 + .../src/agent/Agent_Quick_Reference.md | 523 +++++++ .../agent/EVALUATION_AGENT_IMPLEMENTATION.md | 60 + .../agent/Evaluation_Agent_System_Design.md | 1238 +++++++++++++++++ .../src/agent/LANGGRAPH_EVALUATION_FLOW.md | 478 +++++++ services/ai-service/src/agent/__init__.py | 7 + services/ai-service/src/agent/graph.py | 205 +++ services/ai-service/src/agent/groq_client.py | 31 + services/ai-service/src/agent/mimic_json.py | 21 + services/ai-service/src/agent/report.py | 105 ++ services/ai-service/src/agent/run.py | 80 ++ services/ai-service/src/agent/state.py | 22 + services/ai-service/src/agent/tools.py | 83 ++ services/ai-service/src/api/app.py | 17 +- services/ai-service/src/api/models/README.md | 6 + .../ai-service/src/api/models/__init__.py | 17 + .../ai-service/src/api/models/requests.py | 5 + .../ai-service/src/api/models/responses.py | 49 + .../ai-service/src/api/routers/evaluations.py | 84 ++ .../ai-service/src/processing/__init__.py | 16 +- .../ai-service/src/processing/docx_parser.py | 30 + .../src/processing/file_dispatcher.py | 27 + .../ai-service/src/processing/pptx_parser.py | 26 + .../src/processing/tabular_parser.py | 49 + services/ai-service/src/rag/retrieval.py | 12 +- services/ai-service/src/services/__init__.py | 3 +- .../src/services/evaluation_service.py | 30 + services/ai-service/uv.lock | 589 +++++++- 30 files changed, 3807 insertions(+), 24 deletions(-) create mode 100644 services/ai-service/src/agent/Agent_Quick_Reference.md create mode 100644 services/ai-service/src/agent/EVALUATION_AGENT_IMPLEMENTATION.md create mode 100644 services/ai-service/src/agent/Evaluation_Agent_System_Design.md create mode 100644 services/ai-service/src/agent/LANGGRAPH_EVALUATION_FLOW.md create mode 100644 services/ai-service/src/agent/__init__.py create mode 100644 services/ai-service/src/agent/graph.py create mode 100644 services/ai-service/src/agent/groq_client.py create mode 100644 services/ai-service/src/agent/mimic_json.py create mode 100644 services/ai-service/src/agent/report.py create mode 100644 services/ai-service/src/agent/run.py create mode 100644 services/ai-service/src/agent/state.py create mode 100644 services/ai-service/src/agent/tools.py create mode 100644 services/ai-service/src/api/models/README.md create mode 100644 services/ai-service/src/api/models/__init__.py create mode 100644 services/ai-service/src/api/models/requests.py create mode 100644 services/ai-service/src/api/models/responses.py create mode 100644 services/ai-service/src/api/routers/evaluations.py create mode 100644 services/ai-service/src/processing/docx_parser.py create mode 100644 services/ai-service/src/processing/file_dispatcher.py create mode 100644 services/ai-service/src/processing/pptx_parser.py create mode 100644 services/ai-service/src/processing/tabular_parser.py create mode 100644 services/ai-service/src/services/evaluation_service.py diff --git a/services/ai-service/.gitignore b/services/ai-service/.gitignore index d3ead36..fe2dfa5 100644 --- a/services/ai-service/.gitignore +++ b/services/ai-service/.gitignore @@ -33,4 +33,4 @@ Thumbs.db # Model cache (if downloading models) .cache/ -models/ + diff --git a/services/ai-service/README.md b/services/ai-service/README.md index 6aee078..a9f0343 100644 --- a/services/ai-service/README.md +++ b/services/ai-service/README.md @@ -66,6 +66,8 @@ python run.py - **Health**: `GET /health` - **Setup framework**: `POST /api/v1/frameworks/setup` - Form fields: `framework_name` (string), `section_names` (list of strings), `files` (list of PDFs). Same order for section_names and files. Each PDF is saved as `config/frameworks/{framework_name}/{section_name}.json`. +- **Submit evaluation**: `POST /api/v1/evaluations/submit` + - Form fields: `framework_name` (string), `files` (1+ uploads: PDF, DOCX, PPTX, CSV, XLSX), `control_ids_1`, `control_ids_2`, ... (one per file; each = comma-separated IDs). Returns mimic JSON, `evaluation_id`, `report_path`, `file_evaluations`. Requires `GROQ_API_KEY` in env. ### Other (from `src`) @@ -92,6 +94,11 @@ uv add --group dev pytest Run these in your terminal; dev dependencies stay in a separate group (e.g. `[project.optional-dependencies.dev]` or `[tool.uv]` dev-dependencies) so production installs stay lean. +## Environment + +- **GROQ_API_KEY**: Required for the evaluation agent (`POST /api/v1/evaluations/submit`). Set in `.env` or environment. +- **GROQ_MODEL**: Optional; default `llama-3.3-70b-versatile`. + ## Notes - All data directories are in `.gitignore` (user uploads and generated content) diff --git a/services/ai-service/pyproject.toml b/services/ai-service/pyproject.toml index 23fbfde..d71632c 100644 --- a/services/ai-service/pyproject.toml +++ b/services/ai-service/pyproject.toml @@ -20,6 +20,15 @@ dependencies = [ "python-multipart>=0.0.9", "aiofiles>=24.0.0", "fastapi>=0.128.0", + "groq>=0.4.0", + "langchain-groq>=0.2.0", + "langgraph>=0.2.0", + "langchain-core>=0.3.0", + "reportlab>=4.0.0", + "python-docx>=1.0.0", + "python-pptx>=0.6.0", + "openpyxl>=3.1.0", + "pandas>=2.0.0", ] [dependency-groups] diff --git a/services/ai-service/src/agent/Agent_Quick_Reference.md b/services/ai-service/src/agent/Agent_Quick_Reference.md new file mode 100644 index 0000000..6c91efa --- /dev/null +++ b/services/ai-service/src/agent/Agent_Quick_Reference.md @@ -0,0 +1,523 @@ +# QUICK REFERENCE: Evaluation Agent System + +--- + +## 🎯 System Summary (One Page) + +### What You're Building +``` +Multi-File Compliance Evaluation Agent +β”œβ”€β”€ Frontend: 15 file upload fields +β”œβ”€β”€ Backend: FastAPI + Celery task queue +β”œβ”€β”€ Agent: LangGraph-based evaluation workflow +β”œβ”€β”€ RAG: Qdrant vector DB for framework controls +β”œβ”€β”€ LLM: Qwen2.5-32B (local) + Claude-3.5-Sonnet (API testing) +└── Output: Comprehensive compliance reports (JSON + PDF) +``` + +--- + +## πŸ“Š Technology Stack Comparison + +### Recommended Stack (Final Choice) + +| Component | Technology | Alternatives Considered | Why Chosen | +|-----------|-----------|------------------------|------------| +| **Frontend** | Next.js 14 + shadcn/ui | React, Vue, Svelte | Modern, fast, great DX, TypeScript | +| **Backend** | FastAPI | Django, Flask | Async, fast, great with ML, type hints | +| **Task Queue** | Celery + Redis | RQ, Dramatiq, Bull | Industry standard, reliable, scalable | +| **Agent Framework** | LangGraph | LangChain, CrewAI | Explicit state, checkpointing, debuggable | +| **LLM (Local)** | Qwen2.5-32B-Instruct | Mistral-Nemo-12B, Llama-3.1-70B | **Best Arabic**, 32K context, 32B size | +| **LLM (API Test)** | Claude-3.5-Sonnet | GPT-4o, Mistral Large | Best reasoning, 200K context, documents | +| **Vector DB** | Qdrant | Weaviate, Milvus, Chroma | Fast, local, filtering, open source | +| **Embeddings** | multilingual-e5-large | jina-v3, bge-m3 | 1024-dim, Arabic, instruction-tuned | +| **File Storage** | MinIO | AWS S3, local FS | S3-compatible, self-hosted, reliable | +| **Database** | PostgreSQL 15+ | MySQL, MongoDB | Reliable, JSONB support, battle-tested | +| **Cache** | Redis 7+ | Memcached | Fast, simple, pub/sub, widely used | +| **LLM Serving** | vLLM | Ollama, TGI | **Fastest inference**, optimized, batching | +| **Containers** | Docker Compose | Kubernetes, bare metal | Simple, reproducible, easy local dev | + +--- + +## πŸ€– LLM Selection (CRITICAL DECISION) + +### Production (Local Deployment) + +**RECOMMENDED: Qwen2.5-32B-Instruct** ⭐⭐⭐⭐⭐ + +``` +Provider: Alibaba Cloud +Parameters: 32B (manageable size) +Context: 32,768 tokens +Arabic: ⭐⭐⭐⭐⭐ Native support, trained on 18% Arabic data +Languages: 29 languages (multilingual) +Performance: Competitive with GPT-4 on benchmarks +Deployment: vLLM (recommended) or Ollama +Hardware: 2x RTX 4090 (48GB) OR 1x A100 40GB +Inference: ~50 tokens/sec (vLLM optimized) +License: Apache 2.0 (commercial use OK) +Cost: $0 (self-hosted) + +Why chosen: +βœ… Best Arabic support (native, not just multilingual) +βœ… Perfect size (32B = manageable hardware) +βœ… Large context (32K = handles long documents) +βœ… Production-grade reliability +βœ… Open source, permissive license +βœ… Active development & community +``` + +**Alternative: Mistral-Nemo-12B-Instruct** (If hardware limited) + +``` +Parameters: 12B (lighter) +Context: 128K tokens (HUGE!) +Arabic: ⭐⭐⭐ Decent (multilingual) +Hardware: 1x RTX 4090 (24GB) +Inference: ~80 tokens/sec +License: Apache 2.0 + +Why alternative: +βœ“ Lighter hardware requirements +βœ“ Massive context window +βœ“ Faster inference +βœ— Weaker Arabic than Qwen +``` + +### Testing/Development (API) + +**RECOMMENDED: Claude-3.5-Sonnet** ⭐⭐⭐⭐⭐ + +``` +Provider: Anthropic +Context: 200K tokens +Arabic: ⭐⭐⭐⭐ Very good +Reasoning: ⭐⭐⭐⭐⭐ Best-in-class +Documents: ⭐⭐⭐⭐⭐ Excellent +Cost: $3/$15 per 1M tokens (input/output) +Latency: ~3-5 seconds per evaluation + +Why for testing: +βœ… Best reasoning & document understanding +βœ… Huge context (fits all framework info) +βœ… Fast development iteration +βœ… Reliable output format +βœ… No hardware setup needed +``` + +**Alternative: GPT-4o** + +``` +Context: 128K tokens +Arabic: ⭐⭐⭐⭐ Very good +Cost: $2.50/$10 per 1M tokens + +Why alternative: +βœ“ Slightly cheaper +βœ“ Widely used +βœ— Less consistent than Claude for structured outputs +``` + +--- + +## πŸ—οΈ Architecture Overview + +### System Layers + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ FRONTEND (Next.js) β”‚ +β”‚ - 15 file upload fields β”‚ +β”‚ - Progress tracking β”‚ +β”‚ - Report dashboard β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ HTTP/REST +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ BACKEND API (FastAPI) β”‚ +β”‚ - File validation β”‚ +β”‚ - Task management β”‚ +β”‚ - Authentication β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ Celery Tasks +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ EVALUATION AGENT (LangGraph) β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Graph Workflow (Cyclic) β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ File Processing Node β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ ↓ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ RAG Retrieval Node β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ ↓ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ Tool Execution Node β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ ↓ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ LLM Evaluation Node β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ ↓ β”‚ β”‚ +β”‚ β”‚ [Loop for next file] β”‚ β”‚ +β”‚ β”‚ ↓ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ Aggregation Node β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ ↓ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ Report Generation Node β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ STORAGE LAYER β”‚ +β”‚ - Qdrant (vector DB) β”‚ +β”‚ - MinIO (file storage) β”‚ +β”‚ - PostgreSQL (results) β”‚ +β”‚ - Redis (cache/queue) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ LLM LAYER β”‚ +β”‚ - Qwen2.5-32B (vLLM server) β”‚ +β”‚ - Claude API (testing) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## πŸ”„ Evaluation Workflow (Step-by-Step) + +### Complete Flow + +``` +1. USER SUBMITS + └─ 15 files uploaded with control mappings + +2. VALIDATION + β”œβ”€ Check file types (CSV, PPTX, DOCX, PDF, XLSX) + β”œβ”€ Check file sizes (< 50MB each) + └─ Validate control IDs + +3. STORAGE + β”œβ”€ Save files to MinIO + β”œβ”€ Create DB record (PostgreSQL) + └─ Enqueue Celery task + +4. AGENT START + β”œβ”€ Initialize LangGraph state + └─ Load application data + +5. FILE LOOP (x15 files) + For each file: + + A. LOAD FILE + └─ Download from MinIO, extract text/data + + B. RAG RETRIEVAL + β”œβ”€ Get control IDs for this file + β”œβ”€ Query Qdrant for control details + └─ Assemble framework context + + C. TOOL SELECTION + └─ LLM decides: data validator, calculator, extractor, etc. + + D. TOOL EXECUTION + └─ Run tools, gather outputs + + E. LLM EVALUATION + β”œβ”€ Combine: file + controls + tool outputs + β”œβ”€ Call LLM (Qwen or Claude) + └─ Get structured assessment + + F. SAVE RESULT + └─ Update state, mark file complete + + Time per file: 10-25 seconds + +6. AGGREGATION + β”œβ”€ Combine all 15 evaluations + β”œβ”€ Calculate overall score + β”œβ”€ Find cross-file gaps + └─ Generate recommendations + + Time: 10-20 seconds + +7. REPORT + β”œβ”€ Create structured JSON + β”œβ”€ Generate PDF + └─ Save to database + + Time: 10-20 seconds + +8. NOTIFY + └─ Update dashboard, send email + +TOTAL TIME: 1.5-3 minutes (parallel processing) +``` + +--- + +## πŸ› οΈ Tools Available to Agent + +### Tool Set + +``` +1. DATA VALIDATOR (for CSV/XLSX) + - Validates schema, data types, ranges + - Returns: compliance status + issues + +2. CALCULATOR + - Computes metrics from data + - Returns: calculated values + +3. CONTENT EXTRACTOR + - Extracts specific sections from documents + - Returns: relevant text excerpts + +4. FORMAT CHECKER + - Verifies document structure + - Returns: format compliance status +``` + +--- + +## πŸ“Š Performance Metrics + +### Per-File Evaluation + +``` +File load: 2-5 seconds +Text extraction: 1-3 seconds +RAG retrieval: 0.5-1 second +Tool execution: 1-5 seconds +LLM evaluation: 5-10 seconds (local) / 3-5 seconds (API) +Save result: 0.5 second + +TOTAL PER FILE: 10-25 seconds +``` + +### Full Application (15 files) + +``` +Sequential: 150-375 seconds (2.5-6 minutes) +Parallel (3 files): 50-125 seconds (1-2 minutes) + +Aggregation: 10-20 seconds +Report generation: 10-20 seconds + +TOTAL END-TO-END: 1.5-3 minutes (parallel) + 3-7 minutes (sequential) +``` + +### Scaling + +``` +Single GPU: 2-3 concurrent applications +Multiple GPUs: Linear scaling +API mode: 10-20 concurrent (rate limits) +``` + +--- + +## πŸ’° Cost Analysis + +### Hardware (One-time) + +``` +Option 1: 2x RTX 4090 (48GB total) +Cost: ~$3,500 +Suitable: Qwen2.5-32B + +Option 2: 1x A100 40GB +Cost: ~$10,000 +Suitable: Qwen2.5-32B + +Option 3: 1x RTX 4090 (24GB) +Cost: ~$1,800 +Suitable: Mistral-Nemo-12B (lighter model) +``` + +### Operational (Per Application) + +``` +Local LLM: +- Compute: $0 (owned hardware) +- Electricity: ~$0.10 (15 minutes @ 600W) +- Total: ~$0.10 + +API (Testing): +- Claude: ~$0.50 per application +- GPT-4o: ~$0.30 per application +``` + +### Annual (1000 applications) + +``` +Local: $100 (electricity only) +API: $500 (Claude) or $300 (GPT-4o) + +ROI: Hardware pays for itself in 7-10 months if doing 1000+ evaluations/year +``` + +--- + +## πŸ” Security & Compliance + +### Data Security + +``` +βœ“ Files encrypted at rest (MinIO encryption) +βœ“ Files encrypted in transit (TLS) +βœ“ Local LLM processing (no data sent externally) +βœ“ Access control (JWT + RBAC) +βœ“ Audit logs (all evaluations tracked) +βœ“ File sandboxing (isolated processing) +βœ“ Virus scanning (ClamAV integration) +``` + +### Compliance Features + +``` +βœ“ GDPR-compliant (data retention policies) +βœ“ Audit trail (who, what, when) +βœ“ Deterministic evaluation (reproducible results) +βœ“ Explainable AI (evidence-based assessments) +βœ“ Client control (on-premises deployment) +``` + +--- + +## πŸš€ Implementation Roadmap + +### 12-Week Plan + +``` +Weeks 1-2: Backend API + Storage +Weeks 3-4: RAG System (Qdrant + embeddings) +Weeks 5-7: Agent Implementation (LangGraph) +Weeks 8-9: LLM Integration (Qwen + vLLM) +Week 10: Report Generation +Weeks 11-12: Testing & Refinement +``` + +### MVP Scope + +``` +βœ“ 15 file upload fields +βœ“ File type validation +βœ“ Basic agent workflow (all nodes) +βœ“ RAG retrieval +βœ“ 4 core tools +βœ“ LLM evaluation (API first, then local) +βœ“ JSON report +βœ“ Basic dashboard +``` + +### Phase 2 Features + +``` +- PDF report generation +- Email notifications +- Advanced analytics dashboard +- Batch applications +- Admin panel +- Multi-framework support +``` + +--- + +## βœ… Critical Success Factors + +### Must-Have + +1. **Reliable LLM inference** + - vLLM for performance + - Fallback to API if local fails + - Timeout handling + +2. **State management** + - LangGraph checkpointing + - Resume from failure + - Progress tracking + +3. **Validation layers** + - Input validation (files) + - Output validation (LLM responses) + - Data validation (schemas) + +4. **Error handling** + - Retry logic (3 attempts) + - Graceful degradation + - Clear error messages + +5. **Monitoring** + - Evaluation progress + - LLM performance + - System health + +--- + +## ❓ Decision Points + +### Before Starting Implementation + +1. **Hardware budget?** + - $1,800 (1x 4090) β†’ Use Mistral-Nemo-12B + - $3,500 (2x 4090) β†’ Use Qwen2.5-32B ⭐ Recommended + - $10,000 (A100) β†’ Use Qwen2.5-32B or Command-R+ + +2. **Arabic priority?** + - Critical β†’ Use Qwen2.5-32B ⭐ + - Nice-to-have β†’ Mistral-Nemo-12B OK + +3. **Deployment timeline?** + - <8 weeks β†’ Start with API (Claude), migrate to local + - >8 weeks β†’ Build local from start + +4. **Expected load?** + - <10/day β†’ Single GPU fine + - >50/day β†’ Plan for multiple GPUs + +5. **Client requirements?** + - On-premises only β†’ Local LLM mandatory + - Cloud OK β†’ Consider hybrid (local + API fallback) + +--- + +## 🎯 SUMMARY + +### What You Have + +βœ… **Complete system design** (Frontend β†’ Backend β†’ Agent β†’ Storage β†’ LLM) +βœ… **Technology stack recommendation** (FastAPI, LangGraph, Qwen2.5-32B, Qdrant) +βœ… **Detailed agent architecture** (Graph-based, 6 nodes, state management) +βœ… **LLM selection** (Qwen2.5-32B for local, Claude for API) +βœ… **Performance expectations** (1.5-3 minutes per application) +βœ… **Implementation roadmap** (12 weeks to production) +βœ… **Cost analysis** (Hardware + operational) +βœ… **Security considerations** (Encryption, local processing, audit) + +### Key Decisions Made + +1. **Agent Framework**: LangGraph (over LangChain) +2. **Local LLM**: Qwen2.5-32B-Instruct (best Arabic + size) +3. **API LLM**: Claude-3.5-Sonnet (best reasoning + documents) +4. **Vector DB**: Qdrant (fast, local, filtering) +5. **Backend**: FastAPI + Celery (async, scalable) + +### Next Steps + +1. Review this design document +2. Confirm hardware budget +3. Confirm Arabic priority level +4. Approve technology choices +5. Begin Phase 1 implementation (Backend API) + +--- + +**You're ready to build!** πŸš€ + +Detailed design document: `Evaluation_Agent_System_Design.md` (15,000+ words) \ No newline at end of file diff --git a/services/ai-service/src/agent/EVALUATION_AGENT_IMPLEMENTATION.md b/services/ai-service/src/agent/EVALUATION_AGENT_IMPLEMENTATION.md new file mode 100644 index 0000000..f01bb5c --- /dev/null +++ b/services/ai-service/src/agent/EVALUATION_AGENT_IMPLEMENTATION.md @@ -0,0 +1,60 @@ +# Evaluation Agent Implementation + +One-page reference for future code agents: what was implemented and where it lives. + +## Purpose + +The evaluation endpoint accepts **files** (1 or more) plus a **framework name**, runs a LangGraph-based evaluation agent (Groq LLM + two tools), and returns: + +- **DB-mimic JSON**: `{ framework_name: { field_1: "id1,id2", ..., field_N: "..." } }` β€” control IDs per file. Number of fields must match number of files. +- **evaluation_id**, **report_path** (path to generated PDF), **file_evaluations** (per-file assessment results). + +No database: files are saved under `data/evaluations/{evaluation_id}/`, reports under `data/reports/{evaluation_id}.pdf`. + +## Contract + +- **Input**: `POST /api/v1/evaluations/submit` (multipart/form-data): `framework_name`, `files` (1+), `control_ids_1`, `control_ids_2`, ... (one field per file; each = comma-separated IDs for that file). File 1 uses control_ids_1, file 2 uses control_ids_2, etc. +- **Output**: JSON with `evaluation_id`, `mimic_json`, `report_path`, `file_evaluations`. + +## Where things live + +| Layer | Path | Role | +|-------|------|------| +| Agent | `src/agent/*.py` | LangGraph graph, state, tools, groq_client, mimic_json, report, run | +| Tools | `src/agent/tools.py` | (1) get_control_ids_for_file (from mimic JSON), (2) retrieve_control_details (vector DB via `src.rag`) | +| Parsers | `src/processing/` | pdf_parser (existing), tabular_parser, pptx_parser, docx_parser, file_dispatcher | +| Service | `src/services/evaluation_service.py` | submit_evaluation β†’ run_evaluation_agent | +| API | `src/api/routers/evaluations.py` | POST /api/v1/evaluations/submit | +| App | `src/api/app.py` | Registers evaluations router; startup creates `data/evaluations` and `data/reports` | + +## LLM tools + +1. **get_control_ids_for_file(field_id)** β€” Reads from state’s `mimic_json[framework_name][field_id]`; returns comma-separated control IDs for that file. +2. **retrieve_control_details(control_id, framework_name)** β€” Calls `src.rag.retrieve_control_details` (vector DB). + +**Tool usage**: `retrieve_control_details` must be called **once per control ID**. Do not pass multiple IDs; the tool accepts exactly one `control_id` per call. + +## Output contract + +- **file_evaluations**: list of per-file results. Each entry may include: + - `file_index`, `field_id` + - `control_decisions`: `[{control_id, decision, rationale}, ...]` β€” structured per-control assessment (when LLM returns valid JSON) + - `summary`: overall assessment text + +## Control types + +- **Scored controls** (have non-empty `scale`): use Leader, Excellent, Good, Fair, Low, Unacceptable for `decision`. +- **Binary controls** (empty `scale`): use Compliant or Not Compliant. + +The graph iterates over all files, updates state per file (`file_evaluations`), and produces one comprehensive report at the end. When moving to the next file, `file_eval_done` clears messages via `RemoveMessage(id=REMOVE_ALL_MESSAGES)` so each file gets a fresh LLM context. Prefer LangGraph built-ins and minimal code. + +## Env + +- **GROQ_API_KEY** β€” Required for Groq LLM (evaluation agent). +- **GROQ_MODEL** β€” Optional; default `llama-3.3-70b-versatile`. + +## Extending + +- Add more tools in `src/agent/tools.py` and wire them in the graph. +- Change report format in `src/agent/report.py`. +- Plug in a real DB later without changing the mimic JSON contract (keep the same response shape). diff --git a/services/ai-service/src/agent/Evaluation_Agent_System_Design.md b/services/ai-service/src/agent/Evaluation_Agent_System_Design.md new file mode 100644 index 0000000..1635277 --- /dev/null +++ b/services/ai-service/src/agent/Evaluation_Agent_System_Design.md @@ -0,0 +1,1238 @@ +# Evaluation Agent System Design +## Multi-File Compliance Evaluation with RAG & Tool Integration + +--- + +## 🎯 System Overview + +### High-Level Flow +``` +Frontend Submission Page + ↓ +15 File Upload Fields (CSV, PPTX, DOCX, PDF, XLSX) + ↓ +Backend receives: {file1: [control_ids], file2: [control_ids], ...} + ↓ +Evaluation Agent (RAG + Tools) + ↓ +Evaluates each file against assigned controls + ↓ +Generates comprehensive application report +``` + +--- + +## πŸ“‹ Requirements Analysis + +### What You Need +βœ… **Multi-file evaluation** - Handle 15 files per application +βœ… **Control-specific assessment** - Each file evaluated against specific controls +βœ… **Document understanding** - Process CSV, PPTX, DOCX, PDF, XLSX +βœ… **RAG integration** - Query framework knowledge +βœ… **Tool usage** - Extract, analyze, compute from documents +βœ… **Comprehensive reporting** - Full application assessment +βœ… **Local deployment** - Run on-premises for client security +βœ… **API access** - For testing and development +βœ… **Arabic support** - Handle Arabic documents +βœ… **Reliable performance** - Consistent, production-grade + +### Critical Constraints +⚠️ **NOT 200B parameters** - Must be deployable locally +⚠️ **NO Google models** - Avoid Gemini/PaLM +⚠️ **Document context** - Large context window required +⚠️ **Arabic capable** - Strong Arabic language support + +--- + +## πŸ—οΈ RECOMMENDED ARCHITECTURE + +```mermaid +flowchart TD + subgraph Frontend["FRONTEND (React/Next.js)"] + UI[Submission Page
15 File Upload Fields] + UI --> Upload[Upload Files
+ Control Mapping] + end + + subgraph Backend["BACKEND API (FastAPI)"] + API[FastAPI Endpoints
/submit-application] + API --> Validate[Validation Layer
βœ“ File types
βœ“ Control mapping
βœ“ File size] + Validate --> Queue[Task Queue
Celery + Redis] + end + + subgraph Agent["EVALUATION AGENT (LangGraph)"] + Queue --> Router[Agent Router
Orchestrates evaluation] + + Router --> FileProc[File Processing Node
Extract text/data
from each file] + + FileProc --> RAG[RAG Retrieval Node
Query framework
for controls] + + RAG --> Tools[Tool Execution Node
- Data validation
- Calculation
- Format check] + + Tools --> Eval[Evaluation Node
Assess file vs controls
using LLM] + + Eval --> Memory[State Management
Track progress
per file] + + Memory --> Loop{All files
evaluated?} + Loop -->|No| FileProc + Loop -->|Yes| Aggregate[Aggregation Node
Combine all results] + + Aggregate --> Report[Report Generation
Full application report] + end + + subgraph Storage["DATA LAYER"] + VectorDB[(Vector DB
Qdrant
Framework embeddings)] + FileStore[(File Storage
MinIO/S3
Uploaded files)] + ResultDB[(PostgreSQL
Evaluation results)] + end + + subgraph LLM["LLM LAYER"] + LocalLLM[Local LLM
Qwen2.5-32B-Instruct
or
Mistral-Nemo-12B] + APILLM[API LLM
Claude-3.5-Sonnet
for testing] + end + + Report --> ResultDB + RAG --> VectorDB + FileProc --> FileStore + Eval --> LocalLLM + Eval --> APILLM + + ResultDB --> FinalReport[Final Report
JSON + PDF] + + style Agent fill:#e3f2fd + style LLM fill:#fff4e6 + style Storage fill:#f3e5f5 + style Frontend fill:#e8f5e9 + style Backend fill:#fff9c4 +``` + +--- + +## πŸ”§ TECHNOLOGY STACK (RECOMMENDED) + +### Frontend +``` +Framework: Next.js 14 (React) +UI Library: shadcn/ui + Tailwind CSS +File Upload: react-dropzone +State: Zustand or React Query +Why: Modern, fast, great file handling +``` + +### Backend API +``` +Framework: FastAPI (Python 3.11+) +Task Queue: Celery + Redis +File Storage: MinIO (S3-compatible) or local filesystem +Auth: JWT + OAuth2 +Why: Async support, fast, easy integration with Python ML stack +``` + +### Agent Framework +``` +Framework: LangGraph (NOT LangChain) +State: LangGraph's built-in state management +Orchestration: Graph-based agent workflow +Why: Better control flow than LangChain, explicit state, debuggable +``` + +### Vector Database +``` +Database: Qdrant (Local or Cloud) +Embeddings: multilingual-e5-large-instruct (1024 dim) +Alternative: jina-embeddings-v3 (Arabic-strong) +Why: Fast, local deployment, good filtering, open source +``` + +### LLM Selection (CRITICAL CHOICE) + +#### For Local Deployment (Production) +**Option 1: Qwen2.5-32B-Instruct** ⭐ RECOMMENDED +``` +Model: Alibaba Qwen2.5-32B-Instruct +Parameters: 32B (manageable on local GPU) +Context: 32K tokens (excellent for documents) +Arabic: ⭐⭐⭐⭐⭐ Native Arabic support +Languages: 29 languages including Arabic +Deployment: vLLM or Ollama +Hardware: 2x RTX 4090 (48GB) or A100 (40GB) +Cost: Free (self-hosted) +Reliability: ⭐⭐⭐⭐⭐ Production-grade +License: Apache 2.0 (commercial use OK) +Why: Best balance: size, Arabic, documents, reliability +``` + +**Option 2: Mistral-Nemo-12B-Instruct** (Lighter alternative) +``` +Model: Mistral AI Nemo 12B Instruct +Parameters: 12B (runs on single RTX 4090) +Context: 128K tokens (massive context!) +Arabic: ⭐⭐⭐ Decent (multilingual training) +Deployment: vLLM or Ollama +Hardware: Single RTX 4090 (24GB) or RTX A6000 +Cost: Free (self-hosted) +Reliability: ⭐⭐⭐⭐ Very good +License: Apache 2.0 +Why: Lighter, huge context, easier deployment +``` + +**Option 3: Command-R+ 104B** (If you have resources) +``` +Model: Cohere Command-R+ 104B +Parameters: 104B (requires beefy hardware) +Context: 128K tokens +Arabic: ⭐⭐⭐⭐⭐ Excellent +Deployment: TensorRT-LLM (optimized) +Hardware: 4x A100 (80GB) or equivalent +Cost: Free (self-hosted) +Why: Best quality, but heavy +``` + +#### For API Testing (Development) +**Claude-3.5-Sonnet** ⭐ RECOMMENDED FOR TESTING +``` +Provider: Anthropic +Context: 200K tokens (huge for documents) +Arabic: ⭐⭐⭐⭐ Very good +Cost: $3/$15 per 1M tokens (in/out) +Reliability: ⭐⭐⭐⭐⭐ Best-in-class +Why: Best for testing, excellent reasoning, great with documents +``` + +**Alternative: GPT-4o** +``` +Provider: OpenAI +Context: 128K tokens +Arabic: ⭐⭐⭐⭐ Very good +Cost: $2.50/$10 per 1M tokens +Why: Widely used, reliable, good Arabic +``` + +### Document Processing +``` +PDF: PyMuPDF (fast) or pdfplumber (tables) +DOCX: python-docx +XLSX: openpyxl or pandas +PPTX: python-pptx +CSV: pandas +OCR (if needed): Tesseract + surya (Arabic OCR) +``` + +### Database +``` +Main DB: PostgreSQL 15+ +Cache: Redis 7+ +Vector DB: Qdrant +Why: Reliable, battle-tested, great performance +``` + +### Deployment +``` +Containerization: Docker + Docker Compose +Orchestration: Kubernetes (optional for scaling) +LLM Serving: vLLM (fast inference) or Ollama (easier) +Monitoring: Prometheus + Grafana +Why: Industry standard, reliable, scalable +``` + +--- + +## 🎨 DETAILED SYSTEM DESIGN + +### 1. Frontend: Submission Page + +``` +Component Structure: +β”œβ”€β”€ ApplicationForm.tsx +β”‚ β”œβ”€β”€ FileUploadField.tsx (x15) +β”‚ β”‚ β”œβ”€β”€ Drag & drop zone +β”‚ β”‚ β”œβ”€β”€ File type validation +β”‚ β”‚ β”œβ”€β”€ Size validation +β”‚ β”‚ └── Preview +β”‚ β”œβ”€β”€ ControlMapping (hidden, auto-assigned) +β”‚ └── SubmitButton +β”‚ +└── State Management: + - Files: {field_id: File} + - Control mapping: {field_id: [control_ids]} + - Upload progress + - Validation errors +``` + +**User Flow:** +``` +1. User lands on submission page +2. Sees 15 labeled file upload fields + - Field 1: "Organization Chart" (accepts: PDF, DOCX, PPTX) + - Field 2: "Security Policy" (accepts: PDF, DOCX) + - Field 3: "Access Control Matrix" (accepts: XLSX, CSV) + - ... (15 fields total) +3. User drags/drops or selects files +4. Frontend validates file types +5. Frontend sends to backend: + { + "application_id": "uuid", + "files": [ + { + "field_id": "org_chart", + "file": , + "filename": "org.pdf", + "control_ids": ["ECC-1.1", "ECC-1.2", "ECC-1.3"] + }, + ... (15 files) + ] + } +``` + +--- + +### 2. Backend API: FastAPI Endpoints + +``` +Endpoints: + +POST /api/v1/applications/submit +β”œβ”€ Receives multipart/form-data (files + metadata) +β”œβ”€ Validates: +β”‚ βœ“ File types allowed +β”‚ βœ“ File sizes < 50MB each +β”‚ βœ“ Control IDs valid +β”‚ βœ“ User authenticated +β”œβ”€ Saves files to MinIO/S3 +β”œβ”€ Creates task in Celery queue +└─ Returns: {"application_id": "uuid", "status": "queued"} + +GET /api/v1/applications/{id}/status +└─ Returns: {"status": "processing", "progress": "3/15 files"} + +GET /api/v1/applications/{id}/report +└─ Returns: Full evaluation report (JSON) + +GET /api/v1/applications/{id}/report/pdf +└─ Returns: PDF report for download +``` + +**Backend Processing Flow:** +``` +1. Receive request +2. Validate files & metadata +3. Save files to object storage +4. Create database record: + application_submissions: + - id: uuid + - user_id: uuid + - framework_id: uuid + - status: "queued" + - created_at: timestamp + - files: jsonb (metadata) +5. Enqueue Celery task: + evaluate_application.delay(application_id) +6. Return response to frontend +7. Frontend polls /status endpoint +``` + +--- + +### 3. Evaluation Agent: LangGraph Architecture + +#### Why LangGraph (Not LangChain)? +``` +LangGraph advantages: +βœ“ Explicit control flow (graph-based) +βœ“ Built-in state management +βœ“ Checkpointing (resume failed evaluations) +βœ“ Better debugging +βœ“ Conditional routing +βœ“ Cyclic workflows (iterate over files) + +vs LangChain: +βœ— Sequential chains (hard to loop) +βœ— Hidden state +βœ— Less control +βœ— Harder to debug +``` + +#### Agent Graph Structure + +```python +# Conceptual structure (no code, just design) + +Agent Graph: +β”œβ”€β”€ START +β”œβ”€β”€ [File Router Node] +β”‚ β”œβ”€ Loads next unevaluated file +β”‚ β”œβ”€ Extracts text/data +β”‚ └─ Routes to RAG retrieval +β”‚ +β”œβ”€β”€ [RAG Retrieval Node] +β”‚ β”œβ”€ Gets control IDs for this file +β”‚ β”œβ”€ Queries vector DB for control details +β”‚ β”œβ”€ Retrieves framework context +β”‚ └─ Routes to Tool Selection +β”‚ +β”œβ”€β”€ [Tool Selection Node] +β”‚ β”œβ”€ Decides which tools needed: +β”‚ β”‚ - Data validation tool (for CSV/XLSX) +β”‚ β”‚ - Calculation tool (for metrics) +β”‚ β”‚ - Format checker (for documents) +β”‚ β”‚ - Content extractor (for specific fields) +β”‚ └─ Routes to Tool Execution +β”‚ +β”œβ”€β”€ [Tool Execution Node] +β”‚ β”œβ”€ Runs selected tools +β”‚ β”œβ”€ Gathers tool outputs +β”‚ └─ Routes to Evaluation +β”‚ +β”œβ”€β”€ [Evaluation Node] +β”‚ β”œβ”€ Combines: +β”‚ β”‚ - File content +β”‚ β”‚ - Control requirements (from RAG) +β”‚ β”‚ - Tool outputs +β”‚ β”œβ”€ Calls LLM: +β”‚ β”‚ Prompt: "Evaluate this file against controls X, Y, Z" +β”‚ β”‚ Context: Framework info + file content + tool outputs +β”‚ β”œβ”€ Gets structured assessment +β”‚ └─ Saves to state +β”‚ +β”œβ”€β”€ [Progress Check Node] +β”‚ β”œβ”€ Check: All 15 files evaluated? +β”‚ β”œβ”€ If NO: Route back to File Router (next file) +β”‚ └─ If YES: Route to Aggregation +β”‚ +β”œβ”€β”€ [Aggregation Node] +β”‚ β”œβ”€ Combines all file evaluations +β”‚ β”œβ”€ Calculates overall score +β”‚ β”œβ”€ Identifies cross-file gaps +β”‚ └─ Routes to Report Generation +β”‚ +β”œβ”€β”€ [Report Generation Node] +β”‚ β”œβ”€ Creates structured report: +β”‚ β”‚ - Executive summary +β”‚ β”‚ - Per-file assessments +β”‚ β”‚ - Overall compliance score +β”‚ β”‚ - Gaps & recommendations +β”‚ β”‚ - Control coverage matrix +β”‚ β”œβ”€ Saves to PostgreSQL +β”‚ └─ Generates PDF +β”‚ +└── END +``` + +#### State Schema + +```python +# AgentState (managed by LangGraph) +{ + "application_id": "uuid", + "framework_id": "nca_ecc", + "files": [ + { + "field_id": "org_chart", + "file_path": "s3://bucket/uuid/org.pdf", + "control_ids": ["ECC-1.1", "ECC-1.2"], + "status": "pending|processing|evaluated", + "evaluation": { + "control_assessments": [...], + "score": 85, + "gaps": [...] + } + }, + ... (15 files) + ], + "current_file_index": 0, + "overall_assessment": { + "score": 0, + "status": "in_progress" + }, + "errors": [] +} +``` + +--- + +### 4. RAG System Design + +#### Vector Database Setup + +``` +Collection: framework_controls +Schema: +{ + "id": "ECC-1.1", + "text": "Full control text...", + "metadata": { + "framework": "nca_ecc", + "domain": "Access Control", + "severity": "HIGH", + "keywords": ["authentication", "MFA", ...] + }, + "vector": [0.123, -0.456, ...] (1024-dim) +} + +Indexing: +- Hybrid search (semantic + keyword) +- Filtered by framework_id +- Cached frequently accessed controls +``` + +#### RAG Retrieval Strategy + +``` +For each file evaluation: + +Step 1: Direct Control Lookup +- Input: control_ids = ["ECC-1.1", "ECC-1.2", "ECC-1.3"] +- Query: Get full control details from vector DB +- Output: Control requirements, criteria, rubrics + +Step 2: Contextual Expansion (Optional) +- Input: Control text +- Query: Find related controls (similarity search) +- Output: Related requirements for holistic evaluation + +Step 3: Context Assembly +- Combine: + * Target controls + * Related controls + * Framework guidelines + * Evaluation rubric +- Format: Structured prompt for LLM +``` + +--- + +### 5. Tool System Design + +#### Available Tools + +**Tool 1: Data Validator (for CSV/XLSX)** +``` +Purpose: Validate data files against schema +Input: File path, expected schema +Process: + - Load data (pandas) + - Check required columns exist + - Validate data types + - Check for missing values + - Compute statistics +Output: + { + "valid": true/false, + "issues": ["Missing column: X", ...], + "stats": {"rows": 100, "columns": 15} + } + +When used: +- Control requires data format compliance +- CSV/XLSX files submitted +``` + +**Tool 2: Calculation Tool** +``` +Purpose: Compute metrics from data +Input: Data file, calculation formula +Process: + - Load data + - Apply formula + - Return result +Output: + { + "metric": "compliance_coverage", + "value": 85, + "details": {...} + } + +When used: +- Control requires specific metrics +- Quantitative assessment needed +``` + +**Tool 3: Content Extractor** +``` +Purpose: Extract specific content from documents +Input: Document path, query +Process: + - Parse document structure + - Search for query terms + - Extract relevant sections +Output: + { + "found": true, + "sections": ["Section 3.2: MFA Implementation", ...], + "excerpts": ["We implement MFA using...", ...] + } + +When used: +- Looking for specific policy statements +- Evidence extraction +``` + +**Tool 4: Format Checker** +``` +Purpose: Verify document structure/formatting +Input: Document path, expected format +Process: + - Parse document + - Check structure (headings, sections) + - Validate formatting +Output: + { + "compliant": true, + "structure": {...}, + "issues": [] + } + +When used: +- Control requires specific document format +- Structure validation needed +``` + +#### Tool Selection Logic + +``` +LLM decides which tools to use based on: +1. Control requirements + - "Must provide data in CSV format" β†’ Data Validator + - "Must calculate risk score" β†’ Calculation Tool + - "Must include MFA policy" β†’ Content Extractor + +2. File type + - CSV/XLSX β†’ Data Validator + - PDF/DOCX β†’ Content Extractor + - Any β†’ Format Checker + +3. Evaluation needs + - Quantitative assessment β†’ Calculation Tool + - Evidence gathering β†’ Content Extractor +``` + +--- + +### 6. Evaluation Logic + +#### Per-File Evaluation Prompt Structure + +``` +System Prompt: +"You are a compliance evaluation expert. Evaluate the provided file +against specific controls. Use tool outputs and framework context +to provide accurate assessment. Output structured JSON." + +User Prompt Template: +--- +FRAMEWORK CONTEXT: +{framework_guidelines} + +CONTROLS TO EVALUATE: +{control_1_full_details} +{control_2_full_details} +... + +FILE INFORMATION: +- Name: {filename} +- Type: {file_type} +- Content: {extracted_text} + +TOOL OUTPUTS: +{tool_results} + +TASK: +Evaluate this file against each control listed above. +For each control, provide: +1. Assessment: MET / PARTIAL / UNMET / NOT_APPLICABLE +2. Evidence: Specific excerpts from file +3. Score: 0-100 +4. Reasoning: Why this assessment +5. Gaps: What's missing (if any) + +Output format: +{ + "file_assessment": { + "filename": "...", + "overall_score": 0-100, + "control_assessments": [ + { + "control_id": "ECC-1.1", + "assessment": "MET|PARTIAL|UNMET|N/A", + "evidence": "Excerpt from file...", + "score": 85, + "reasoning": "...", + "gaps": [...] + } + ] + } +} +--- +``` + +#### Cross-File Analysis (Aggregation Phase) + +``` +After all files evaluated: + +System Prompt: +"You are a compliance analyst. Review all file evaluations +and provide holistic application assessment." + +User Prompt: +--- +FILE EVALUATIONS: +{all_15_file_evaluations} + +TASK: +1. Calculate overall compliance score +2. Identify cross-file patterns +3. Find systemic gaps +4. Provide actionable recommendations +5. Highlight strengths + +Output: +{ + "overall_assessment": { + "compliance_score": 0-100, + "grade": "EXCELLENT|GOOD|PARTIAL|INSUFFICIENT", + "summary": "..." + }, + "control_coverage": { + "total_controls": 114, + "met": 85, + "partial": 20, + "unmet": 9 + }, + "strengths": [...], + "gaps": [...], + "recommendations": [...] +} +--- +``` + +--- + +### 7. Report Generation + +#### Report Structure + +``` +COMPLIANCE EVALUATION REPORT +Application ID: {uuid} +Framework: NCA-ECC +Date: {timestamp} + +═══════════════════════════════════════ + +EXECUTIVE SUMMARY +β”œβ”€ Overall Compliance Score: 75/100 +β”œβ”€ Assessment Level: PARTIAL COMPLIANCE +β”œβ”€ Total Controls Evaluated: 114 +β”‚ β”œβ”€ Met: 65 (57%) +β”‚ β”œβ”€ Partially Met: 35 (31%) +β”‚ β”œβ”€ Unmet: 14 (12%) +β”‚ └─ Not Applicable: 0 (0%) +└─ Recommendation: ADDRESS CRITICAL GAPS + +═══════════════════════════════════════ + +PER-FILE ASSESSMENTS + +File 1: Organization Chart (ECC-1.1, ECC-1.2, ECC-1.3) +β”œβ”€ Status: βœ“ EVALUATED +β”œβ”€ Score: 85/100 +β”œβ”€ Assessment: PARTIAL +β”œβ”€ Controls: +β”‚ β”œβ”€ ECC-1.1: βœ“ MET (100/100) +β”‚ β”‚ Evidence: "Clear org structure with defined roles..." +β”‚ β”œβ”€ ECC-1.2: ◐ PARTIAL (75/100) +β”‚ β”‚ Evidence: "Security roles present but not detailed..." +β”‚ β”‚ Gap: "Need more detail on responsibilities" +β”‚ └─ ECC-1.3: βœ“ MET (90/100) +└─ Recommendations: [...] + +File 2: Security Policy (ECC-2.1, ECC-2.2, ...) +... +(Repeat for all 15 files) + +═══════════════════════════════════════ + +CONTROL COVERAGE MATRIX + +| Control ID | Requirement | Status | Score | Files | +|------------|-------------|--------|-------|-------| +| ECC-1.1 | MFA | βœ“ MET | 100 | 1,2 | +| ECC-1.2 | RBAC | ◐ PART | 75 | 1,3 | +| ECC-1.3 | Audit | βœ— UNMET| 0 | - | +... + +═══════════════════════════════════════ + +IDENTIFIED GAPS (Critical) +1. Incident Response Plan missing (ECC-4.1) + - Impact: HIGH + - Required files: Not submitted + - Recommendation: Develop comprehensive IRP + +2. Access Control Matrix incomplete (ECC-1.5) + - Impact: MEDIUM + - Found in: File 3 (partial) + - Gap: Missing role definitions + - Recommendation: Complete matrix with all roles + +═══════════════════════════════════════ + +RECOMMENDATIONS (Prioritized) +1. [HIGH] Develop Incident Response Plan +2. [HIGH] Complete Access Control documentation +3. [MEDIUM] Add details to Security Policy +4. [LOW] Update organization chart + +═══════════════════════════════════════ + +APPENDIX +β”œβ”€ Full control details +β”œβ”€ Evidence excerpts +└─ Evaluation methodology +``` + +#### Report Formats + +``` +JSON: Structured data for frontend display +PDF: Professional report for download/archive +HTML: Interactive web view +Excel: Data analysis (control matrix, scores) +``` + +--- + +## πŸ”„ COMPLETE EVALUATION FLOW + +### Step-by-Step Process + +``` +1. USER SUBMITS APPLICATION + Frontend β†’ Backend API + { + 15 files uploaded, + each mapped to control IDs + } + +2. BACKEND PROCESSING + β”œβ”€ Validate files + β”œβ”€ Save to object storage + β”œβ”€ Create DB record + └─ Enqueue Celery task + +3. AGENT INITIALIZATION + β”œβ”€ Load application data + β”œβ”€ Initialize LangGraph state + └─ Start evaluation workflow + +4. FILE-BY-FILE EVALUATION (Loop x15) + For each file: + + Step A: File Processing + β”œβ”€ Load file from storage + β”œβ”€ Extract text/data + └─ Parse structure + + Step B: RAG Retrieval + β”œβ”€ Query vector DB for controls + β”œβ”€ Get control requirements + └─ Assemble context + + Step C: Tool Selection + β”œβ”€ LLM decides which tools needed + └─ Prepares tool inputs + + Step D: Tool Execution + β”œβ”€ Run tools (data validation, extraction, etc.) + └─ Gather outputs + + Step E: LLM Evaluation + β”œβ”€ Combine: file + controls + tools + β”œβ”€ Call LLM with structured prompt + └─ Get assessment + + Step F: State Update + β”œβ”€ Save file evaluation to state + └─ Mark file as completed + +5. AGGREGATION PHASE + β”œβ”€ Load all 15 file evaluations + β”œβ”€ Calculate overall score + β”œβ”€ Identify cross-file gaps + └─ Generate recommendations + +6. REPORT GENERATION + β”œβ”€ Create structured report + β”œβ”€ Generate PDF + β”œβ”€ Save to database + └─ Notify user + +7. USER VIEWS REPORT + β”œβ”€ Dashboard shows overall score + β”œβ”€ Drill down to file assessments + └─ Download PDF report +``` + +--- + +## πŸ“Š SYSTEM COMPONENTS SUMMARY + +### Technology Choices + +| Component | Technology | Why | +|-----------|-----------|-----| +| **Frontend** | Next.js 14 + shadcn/ui | Modern, fast, great DX | +| **Backend** | FastAPI + Celery | Async, scalable, Python ML integration | +| **Agent** | LangGraph | Explicit control, state management, debuggable | +| **LLM (Local)** | Qwen2.5-32B-Instruct | Best balance: size, Arabic, performance | +| **LLM (API)** | Claude-3.5-Sonnet | Testing, best reasoning, documents | +| **Vector DB** | Qdrant | Fast, local, open source | +| **Embeddings** | multilingual-e5-large | Arabic support, good quality | +| **File Storage** | MinIO (S3-compatible) | Self-hosted, S3 API, reliable | +| **Database** | PostgreSQL | Battle-tested, reliable | +| **Cache** | Redis | Fast, simple, widely used | +| **LLM Serving** | vLLM | Fast inference, optimized | +| **Containers** | Docker + Compose | Easy deployment, isolated | + +### Hardware Requirements + +#### For Qwen2.5-32B (Recommended) +``` +GPU: 2x NVIDIA RTX 4090 (48GB total) + OR 1x A100 40GB/80GB +RAM: 64GB system RAM +CPU: 16+ cores +Storage: 1TB NVMe SSD +Cost: ~$3,500 (2x 4090) or ~$10,000 (A100) +``` + +#### For Mistral-Nemo-12B (Lighter) +``` +GPU: 1x NVIDIA RTX 4090 (24GB) + OR RTX A6000 (48GB) +RAM: 32GB system RAM +CPU: 8+ cores +Storage: 500GB NVMe SSD +Cost: ~$1,800 (4090) or ~$4,500 (A6000) +``` + +--- + +## 🎯 EVALUATION AGENT DETAILED DESIGN + +### Agent Graph (Mermaid) + +```mermaid +flowchart TD + Start([START
Application Submitted]) --> Init[Initialize Agent State
Load application data
Load 15 files metadata] + + Init --> FileRouter{File Router
Get next unevaluated file} + + FileRouter -->|File N| LoadFile[Load File
- Download from storage
- Extract text/data
- Parse structure] + + LoadFile --> GetControls[Get Control IDs
control_ids for this file
from submission metadata] + + GetControls --> RAG[RAG Retrieval
Query vector DB
- Get control details
- Get framework context
- Get rubrics] + + RAG --> ToolSelect[Tool Selection
LLM decides tools needed
Based on:
- Control requirements
- File type
- Evaluation needs] + + ToolSelect --> ToolExec[Tool Execution
Run selected tools:
- Data Validator
- Calculator
- Content Extractor
- Format Checker] + + ToolExec --> EvalLLM[LLM Evaluation
Input: File + Controls + Tools
Process: Structured prompt
Output: Assessment JSON] + + EvalLLM --> SaveResult[Save to State
file_evaluations[N] = result
Mark file as 'evaluated'] + + SaveResult --> Progress{Progress Check
All 15 files
evaluated?} + + Progress -->|No| FileRouter + Progress -->|Yes| Aggregate[Aggregation Phase
Combine all evaluations
Calculate overall score
Find cross-file patterns] + + Aggregate --> Report[Report Generation
Create structured report
- Executive summary
- Per-file details
- Control matrix
- Gaps & recommendations] + + Report --> SaveDB[Save to Database
PostgreSQL:
- Evaluation results
- Report JSON
- Metadata] + + SaveDB --> GenPDF[Generate PDF
Professional report
for download] + + GenPDF --> Notify[Notify User
Email + Dashboard update
Status: 'completed'] + + Notify --> End([END
Report Ready]) + + style Start fill:#e8f5e9 + style End fill:#e8f5e9 + style FileRouter fill:#fff4e6 + style RAG fill:#e3f2fd + style EvalLLM fill:#f3e5f5 + style Report fill:#c8e6c9 +``` + +### Agent State Flow + +```mermaid +stateDiagram-v2 + [*] --> Initialized: Application submitted + + Initialized --> ProcessingFile1: Start file 1 + ProcessingFile1 --> ProcessingFile2: File 1 done + ProcessingFile2 --> ProcessingFile3: File 2 done + ProcessingFile3 --> ProcessingFileN: File 3 done + ProcessingFileN --> Aggregating: All files done + + Aggregating --> GeneratingReport: Aggregation complete + GeneratingReport --> Completed: Report ready + + ProcessingFile1 --> Error: File error + ProcessingFile2 --> Error: File error + Error --> Retry: Retry (max 3) + Retry --> ProcessingFile1: Retry file + Retry --> Failed: Max retries exceeded + + Completed --> [*] + Failed --> [*] + + note right of ProcessingFile1 + Each file evaluation: + - Load file + - RAG retrieval + - Tool execution + - LLM assessment + - Save result + end note + + note right of Aggregating + Combine all results: + - Calculate overall score + - Find patterns + - Generate recommendations + end note +``` + +--- + +## πŸ” SECURITY & RELIABILITY + +### Security Considerations + +``` +1. File Upload Security + βœ“ Whitelist file types (no executables) + βœ“ Virus scanning (ClamAV) + βœ“ File size limits (50MB per file) + βœ“ Sandboxed file processing + +2. Data Privacy + βœ“ Encryption at rest (MinIO/S3) + βœ“ Encryption in transit (TLS) + βœ“ Local LLM (no data leaves premises) + βœ“ Access control (RBAC) + +3. API Security + βœ“ JWT authentication + βœ“ Rate limiting + βœ“ Input validation + βœ“ CORS policies + +4. LLM Security + βœ“ Prompt injection prevention + βœ“ Output validation + βœ“ Structured output parsing + βœ“ Timeout limits +``` + +### Reliability Measures + +``` +1. Error Handling + βœ“ Retry logic (3 attempts) + βœ“ Graceful degradation + βœ“ Clear error messages + βœ“ Fallback mechanisms + +2. Monitoring + βœ“ LLM response times + βœ“ Evaluation progress + βœ“ File processing status + βœ“ System health metrics + +3. Checkpointing + βœ“ LangGraph state snapshots + βœ“ Resume from failure + βœ“ No duplicate evaluations + βœ“ Progress tracking + +4. Testing + βœ“ Unit tests (tools, parsers) + βœ“ Integration tests (agent flow) + βœ“ End-to-end tests (full submission) + βœ“ Load testing (100+ concurrent) +``` + +--- + +## πŸ“ˆ PERFORMANCE EXPECTATIONS + +### Evaluation Time Estimates + +``` +Per File: +- File load: 2-5 seconds +- Text extraction: 1-3 seconds +- RAG retrieval: 0.5-1 second +- Tool execution: 1-5 seconds +- LLM evaluation: 5-10 seconds (local) / 3-5 seconds (API) +- Save result: 0.5 second +Total per file: 10-25 seconds + +Full Application (15 files): +- Sequential: 150-375 seconds (2.5-6 minutes) +- Parallel (3 workers): 50-125 seconds (1-2 minutes) + +Aggregation & Report: +- Aggregation: 10-20 seconds +- Report generation: 5-10 seconds +- PDF creation: 5-10 seconds +Total: 20-40 seconds + +TOTAL END-TO-END: +- Sequential: 3-7 minutes +- Parallel: 1.5-3 minutes +``` + +### Scaling Considerations + +``` +Concurrent Applications: +- Single GPU (32B model): 2-3 concurrent +- Multiple GPUs: Linear scaling +- API mode: 10-20 concurrent (rate limits) + +Optimization: +βœ“ Batch file processing (3-5 files parallel) +βœ“ Cache frequent control retrievals +βœ“ Pre-warm LLM +βœ“ Async file downloads +βœ“ Connection pooling +``` + +--- + +## 🎯 RECOMMENDED IMPLEMENTATION PHASES + +### Phase 1: Foundation (Week 1-2) +``` +βœ“ Set up FastAPI backend +βœ“ Implement file upload endpoints +βœ“ Set up MinIO/S3 storage +βœ“ Set up PostgreSQL + Redis +βœ“ Basic frontend (file uploads) +βœ“ Test with dummy data +``` + +### Phase 2: RAG System (Week 3-4) +``` +βœ“ Set up Qdrant +βœ“ Index framework controls +βœ“ Implement retrieval logic +βœ“ Test control queries +βœ“ Optimize embeddings +``` + +### Phase 3: Agent (Week 5-7) +``` +βœ“ Set up LangGraph +βœ“ Implement agent nodes +βœ“ Implement tools +βœ“ Test file-by-file evaluation +βœ“ Add checkpointing +``` + +### Phase 4: LLM Integration (Week 8-9) +``` +βœ“ Deploy Qwen2.5-32B locally (vLLM) +βœ“ Configure prompts +βœ“ Test evaluations +βœ“ Tune parameters +βœ“ Add Claude API for testing +``` + +### Phase 5: Report Generation (Week 10) +``` +βœ“ Implement report structure +βœ“ Generate PDF +βœ“ Add visualizations +βœ“ Test with real data +``` + +### Phase 6: Testing & Refinement (Week 11-12) +``` +βœ“ End-to-end testing +βœ“ Load testing +βœ“ Security testing +βœ“ User acceptance testing +βœ“ Performance tuning +``` + +--- + +## πŸŽ“ FINAL RECOMMENDATIONS + +### Critical Success Factors + +1. **LLM Choice** + - **Go with Qwen2.5-32B-Instruct** for production + - Excellent Arabic, manageable size, great performance + - Use Claude-3.5-Sonnet for development/testing + +2. **Agent Framework** + - **Use LangGraph, not LangChain** + - Explicit state management critical for multi-file workflows + - Checkpointing essential for reliability + +3. **RAG Strategy** + - **Keep it simple** - Direct control lookup is enough + - Cache frequently accessed controls + - Optimize for speed over fancy retrieval + +4. **Tool Design** + - **Start with 4 core tools** + - Add more only if needed + - Keep tools focused and testable + +5. **Report Quality** + - **Structured JSON first, PDF second** + - Make reports actionable (clear gaps, recommendations) + - Provide evidence for every assessment + +### Potential Pitfalls to Avoid + +❌ **Don't** use 200B models (too heavy, unnecessary) +❌ **Don't** use Google models (per your constraint) +❌ **Don't** use LangChain (less control than LangGraph) +❌ **Don't** evaluate all files in one LLM call (exceeds context, low quality) +❌ **Don't** skip validation (files and outputs must be validated) +❌ **Don't** forget checkpointing (long evaluations can fail) +❌ **Don't** ignore Arabic support (critical for your use case) + +### Questions to Resolve Before Implementation + +1. **Hardware budget?** (Affects LLM choice) +2. **Expected load?** (Concurrent applications) +3. **SLA requirements?** (How fast must evaluations complete?) +4. **Arabic priority?** (Documents all in Arabic? Mixed?) +5. **Client deployment?** (On-premises? Cloud? Hybrid?) + +--- + +## πŸ“š SUMMARY + +You now have: +βœ… **Complete system architecture** (Frontend β†’ Backend β†’ Agent β†’ Storage) +βœ… **Technology stack recommendation** (FastAPI, LangGraph, Qwen2.5-32B, Qdrant) +βœ… **Detailed agent design** (Graph-based, state management, tools) +βœ… **Evaluation workflow** (File-by-file β†’ Aggregation β†’ Report) +βœ… **Security & reliability measures** +βœ… **Performance expectations** (1.5-3 minutes per application) +βœ… **Implementation roadmap** (12-week plan) + +**Next step**: Review this design, confirm technology choices, then move to implementation phase. + +πŸš€ **You're ready to build a professional, production-grade compliance evaluation system!** \ No newline at end of file diff --git a/services/ai-service/src/agent/LANGGRAPH_EVALUATION_FLOW.md b/services/ai-service/src/agent/LANGGRAPH_EVALUATION_FLOW.md new file mode 100644 index 0000000..fdadcb0 --- /dev/null +++ b/services/ai-service/src/agent/LANGGRAPH_EVALUATION_FLOW.md @@ -0,0 +1,478 @@ +# LangGraph Evaluation Agent β€” Flow Documentation + +This document describes the LangGraph evaluation workflow with detailed input/output and processing for each node. Aligned with `graph.py`, `state.py`, `run.py`, `tools.py`, `report.py`, and `mimic_json.py`. + +--- + +## 1. High-Level Workflow + +```mermaid +flowchart TD + START([START]) --> FileProc[file_processing] + FileProc --> MimicJSON[mimic_json] + MimicJSON --> Agent[file_eval_agent] + Agent --> CondTools{_should_continue_tools} + CondTools -->|"tool_calls"| Tools[file_eval_tools] + CondTools -->|"no tool_calls"| Done[file_eval_done] + Tools --> Agent + Done --> CondMore{_more_files} + CondMore -->|"more files"| Agent + CondMore -->|"all done"| Report[report] + Report --> END_NODE([END]) +``` + +--- + +## 2. State Schema (EvaluationState) + +| Key | Type | Description | +|-----|------|-------------| +| `evaluation_id` | str | UUID for this evaluation run | +| `framework_name` | str | Framework identifier (e.g. NDI) | +| `files` | list[dict] | `[{path, extracted_text, field_id}, ...]` per file | +| `mimic_json` | dict | `{framework_name: {field_1: "id1,id2", field_2: "..."}}` | +| `current_file_index` | int | Index of file being evaluated (0-based) | +| `file_evaluations` | list[dict] | Accumulated per-file results | +| `report_path` | str | Path to generated PDF | +| `errors` | list[str] | Error messages | +| `messages` | list | LLM conversation (add_messages reducer) | + +--- + +## 3. Initial State (from run.py) + +```mermaid +flowchart LR + subgraph run [run_evaluation_agent] + A[Persist files to data/evaluations/id/] --> B[Build mimic_json from control_ids_per_file] + B --> C[Create initial_state] + end + subgraph initialState [Initial State] + I1[evaluation_id: uuid] + I2[framework_name: NDI] + I3[files: path, extracted_text empty, field_id] + I4[mimic_json: field -> control IDs] + I5[current_file_index: 0] + I6[file_evaluations: empty list] + I7[messages: empty list] + end +``` + +--- + +## 4. Node Details + +### 4.1 file_processing + +**Input (from state):** + +| Key | Value | +|-----|-------| +| `files` | `[{path, extracted_text: "", field_id}, ...]` (extracted_text empty from run.py) | + +**Processing:** + +1. If `files` is empty: return `{"errors": [... "No files in state"]}` and stop. +2. For each file: call `extract_text_from_file(path)` from `src.processing` (dispatches by extension: pdf, csv, xlsx, xls, pptx, docx). +3. On exception: set `extracted_text` to `"[Extraction error: {e}]"`. +4. Overwrite `files` with result list; set `current_file_index` to 0; set `file_evaluations` to `[]`. + +**Output (state update):** + +```json +{ + "files": [ + {"path": "/path/to/data/evaluations/{id}/file1.pdf", "extracted_text": "...", "field_id": "field_1"}, + {"path": "/path/to/data/evaluations/{id}/file2.pdf", "extracted_text": "...", "field_id": "field_2"} + ], + "current_file_index": 0, + "file_evaluations": [] +} +``` + +**Mermaid:** + +```mermaid +flowchart TD + subgraph Input [Input] + IN1[files with path and field_id] + end + subgraph Process [Processing] + P1[extract_text_from_file for each path] + P2[Truncate errors to extracted_text] + P3[Reset current_file_index to 0] + P3a[Reset file_evaluations to empty] + end + subgraph Output [Output] + OUT1[files with extracted_text populated] + OUT2[current_file_index: 0] + OUT3[file_evaluations empty] + end + IN1 --> P1 --> P2 --> P3 --> P3a --> OUT1 + P3 --> OUT2 + P3a --> OUT3 +``` + +--- + +### 4.2 mimic_json + +**Input:** State already has `mimic_json` from `run.py` (built from `control_ids_per_file`). + +**Processing:** No-op. Ensures `mimic_json` is present (already set before graph invoke). + +**Output:** `{}` (no state change). + +--- + +### 4.3 file_eval_agent + +**Input (from state):** + +| Key | Value | +|-----|-------| +| `current_file_index` | 0, 1, ... | +| `files` | List with `extracted_text` populated | +| `messages` | Empty (new file) or prior conversation (tool round) | + +**Processing:** + +1. If `messages` is empty: + - Build prompt via `_build_file_prompt(state)` (uses `current_file_index`, `files[idx]`, `framework_name`; file content truncated to 15k chars). + - If prompt is empty (idx >= len(files)): return `{"errors": [... "No file at index"]}`. + - Set `messages = [HumanMessage(content=prompt)]`. +2. Invoke Groq LLM via `get_groq_llm().bind_tools(get_tool_schemas())` with `messages`. +3. Append `response` (AIMessage). If starting (len(messages)==1 and HumanMessage): return `messages + [response]`; else return `[response]` (add_messages merges). + +**Output (state update):** + +```json +{ + "messages": [HumanMessage(...), AIMessage(content="...", tool_calls=[...])] +} +``` + +Or when done with tools: + +```json +{ + "messages": [..., AIMessage(content="{\"control_decisions\": [...], \"summary\": \"...\"}")] +} +``` + +**Mermaid:** + +```mermaid +flowchart TD + subgraph Input [Input] + IN1[current_file_index] + IN2[files with extracted_text] + IN3[messages empty or prior] + end + subgraph Process [Processing] + P1{messages empty?} + P2[Build _build_file_prompt] + P3[Create HumanMessage] + P4[Invoke Groq LLM with tools] + P5[Return AIMessage] + end + subgraph Output [Output] + OUT1[messages: HumanMessage + AIMessage] + OUT2[or messages: AIMessage only if tool round] + end + IN1 --> P1 + IN2 --> P1 + IN3 --> P1 + P1 -->|yes| P2 --> P3 --> P4 --> P5 --> OUT1 + P1 -->|no| P4 --> P5 --> OUT2 +``` + +--- + +### 4.4 _should_continue_tools (conditional) + +**Input:** `messages` (from state). + +**Logic:** + +- If `messages` is empty β†’ `"file_eval_done"` +- Else if last message is AIMessage and has `tool_calls` β†’ `"tools"` +- Else β†’ `"file_eval_done"` + +**Mermaid:** + +```mermaid +flowchart TD + IN[messages] --> CHECK1{messages empty?} + CHECK1 -->|yes| DONE[Route to file_eval_done] + CHECK1 -->|no| CHECK2{Last AIMessage has tool_calls?} + CHECK2 -->|yes| TOOLS[Route to file_eval_tools] + CHECK2 -->|no| DONE +``` + +--- + +### 4.5 file_eval_tools + +**Input (from state):** + +| Key | Value | +|-----|-------| +| `messages` | Last message is AIMessage with `tool_calls` | +| `mimic_json`, `framework_name` | For `execute_tool` (get_control_ids_for_file) | + +**Processing:** + +1. If last message is not AIMessage or has no `tool_calls`: return `{}` (no state change). +2. For each tool call in last.tool_calls: + - `execute_tool(dict(state), name, args)`: + - `get_control_ids_for_file(field_id)` β†’ string (comma-separated IDs). + - `retrieve_control_details(control_id, framework_name, top_k_pdf?)` β†’ dict; serialized via `json.dumps` for ToolMessage content. + - On exception: content = `str(e)`. + - Create ToolMessage(content, tool_call_id=tid). +3. Return `{"messages": tool_messages}` (add_messages appends). + +**Output (state update):** + +```json +{ + "messages": [ToolMessage(content="DG.1.1,DG.1.2", tool_call_id="..."), ToolMessage(content="{...}", tool_call_id="...")] +} +``` + +**Mermaid:** + +```mermaid +flowchart TD + subgraph Input [Input] + IN1[AIMessage with tool_calls] + IN2[state: mimic_json, framework_name] + end + subgraph Process [Processing] + P1[For each tool_call] + P2[execute_tool: get_control_ids_for_file] + P2b[execute_tool: retrieve_control_details] + P3[Create ToolMessage per result] + end + subgraph Output [Output] + OUT1[messages: ToolMessages appended] + end + IN1 --> P1 --> P2 + P1 --> P2b + IN2 --> P2 + IN2 --> P2b + P2 --> P3 --> OUT1 + P2b --> P3 +``` + +--- + +### 4.6 file_eval_done + +**Input (from state):** + +| Key | Value | +|-----|-------| +| `messages` | Last message is AIMessage with `content` (final JSON) | +| `current_file_index` | Index of just-evaluated file | +| `file_evaluations` | Prior evaluations | +| `files` | For field_id | + +**Processing:** + +1. Extract `content` from last AIMessage (empty string if none). +2. Strip markdown code fences (```...```) if present. +3. Parse JSON: + - If parsed and `control_decisions` is a non-empty list: append `{file_index, field_id, control_decisions, summary}`. + - Else if parsed: append `{file_index, field_id, summary: content}`. + - On JSONDecodeError/TypeError: append `{file_index, field_id, summary: content}`. +4. Increment `current_file_index` by 1. +5. Return `messages: [RemoveMessage(id=REMOVE_ALL_MESSAGES)]` so add_messages clears all prior messages for the next file. + +**Output (state update):** + +```json +{ + "file_evaluations": [ + {"file_index": 0, "field_id": "field_1", "control_decisions": [...], "summary": "..."}, + {"file_index": 1, "field_id": "field_2", "control_decisions": [...], "summary": "..."} + ], + "current_file_index": 1, + "messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)] +} +``` + +**Mermaid:** + +```mermaid +flowchart TD + subgraph Input [Input] + IN1[messages last AIMessage content] + IN2[current_file_index] + IN3[file_evaluations] + end + subgraph Process [Processing] + P1[Extract content from last AIMessage] + P2[Strip markdown fences] + P3{Parse JSON} + P4[Append control_decisions + summary] + P5[Append raw summary if no control_decisions] + P6[Increment current_file_index] + P7[Clear messages with RemoveMessage] + end + subgraph Output [Output] + OUT1[file_evaluations appended] + OUT2[current_file_index + 1] + OUT3[messages cleared] + end + IN1 --> P1 --> P2 --> P3 + P3 -->|valid control_decisions| P4 --> OUT1 + P3 -->|invalid or empty| P5 --> OUT1 + IN2 --> P6 --> OUT2 + P7 --> OUT3 +``` + +--- + +### 4.7 _more_files (conditional) + +**Input:** `current_file_index`, `files` (length). + +**Logic:** + +- If `current_file_index < len(files)` β†’ `"file_eval_agent"` (more files to evaluate) +- Else β†’ `"report"` (all done) + +**Mermaid:** + +```mermaid +flowchart TD + IN1[current_file_index] --> CHECK{current_file_index < len files?} + IN2[len files] + IN2 --> CHECK + CHECK -->|yes| AGENT[Route to file_eval_agent] + CHECK -->|no| REPORT[Route to report] +``` + +--- + +### 4.8 report + +**Input (from state):** + +| Key | Value | +|-----|-------| +| `evaluation_id` | For filename | +| `framework_name` | For header | +| `file_evaluations` | All per-file results | +| `mimic_json` | Control IDs per field | + +**Processing:** + +1. Create `data/reports/{evaluation_id}.pdf` via ReportLab `SimpleDocTemplate`. +2. Add title, evaluation ID, framework; Executive Summary; Control IDs per file (from `mimic_json[framework_name]`, sorted). +3. For each `file_evaluations`: + - Heading: "File N (Field: field_N)" + - If `control_decisions` is a non-empty list: render Table (Control ID | Decision | Rationale), colWidths [70, 70, 270], cells as Paragraph for wrapping. + - Summary: `ev.get("summary") or ev.get("evaluation")` (truncate to 2k chars); if none and no control_decisions, use `str(ev)`. +4. `doc.build(story)` and return path. + +**Output (state update):** + +```json +{ + "report_path": "/path/to/data/reports/{evaluation_id}.pdf" +} +``` + +**Mermaid:** + +```mermaid +flowchart TD + subgraph Input [Input] + IN1[evaluation_id] + IN2[framework_name] + IN3[file_evaluations] + IN4[mimic_json] + end + subgraph Process [Processing] + P1[Create PDF at data/reports/eval_id.pdf] + P2[Add title and metadata] + P3[Add Executive Summary] + P4[Add Control IDs per file] + P5[For each file_evaluation] + P6[Add table or summary] + P7[Build PDF] + end + subgraph Output [Output] + OUT1[report_path] + end + IN1 --> P1 + IN2 --> P2 + IN3 --> P5 + IN4 --> P4 + P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7 --> OUT1 +``` + +--- + +## 5. Tools Used by file_eval_agent + +| Tool | Args | Returns | Notes | +|------|------|---------|-------| +| `get_control_ids_for_file` | `field_id` | str (comma-separated IDs) | From `mimic_json[framework_name][field_id]` | +| `retrieve_control_details` | `control_id`, `framework_name`, `top_k_pdf`? (default 5) | `{json_cards, pdf_chunks}` | RAG via `src.rag.retrieve_control_details` | + +--- + +## 6. Per-File Loop (Detailed) + +```mermaid +flowchart TD + subgraph File1 [File 1] + A1[file_eval_agent: build prompt for file 0] + A1 --> T1[LLM returns tool_calls] + T1 --> F1[file_eval_tools: execute tools] + F1 --> A1 + A1 --> T2[LLM returns final JSON] + T2 --> D1[file_eval_done: append evaluation, clear messages] + end + D1 --> A2 + subgraph File2 [File 2] + A2[file_eval_agent: messages empty, build prompt for file 1] + A2 --> F2[file_eval_tools] + F2 --> A2 + A2 --> D2[file_eval_done: append, clear] + end + D2 --> Report[report] +``` + +--- + +## 7. Edge Summary + +| From | To | Condition | +|------|----|-----------| +| START | file_processing | Always | +| file_processing | mimic_json | Always | +| mimic_json | file_eval_agent | Always | +| file_eval_agent | file_eval_tools | `_should_continue_tools` β†’ "tools" (last AIMessage has tool_calls) | +| file_eval_agent | file_eval_done | `_should_continue_tools` β†’ "file_eval_done" (no tool_calls or empty messages) | +| file_eval_tools | file_eval_agent | Always | +| file_eval_done | file_eval_agent | `_more_files` β†’ "file_eval_agent" (current_file_index < len(files)) | +| file_eval_done | report | `_more_files` β†’ "report" (current_file_index >= len(files)) | +| report | END | Always | + +--- + +## 8. Code References + +| Component | File | Function / Class | +|-----------|------|------------------| +| Graph definition | `graph.py` | `build_evaluation_graph` | +| State schema | `state.py` | `EvaluationState` | +| Entry point | `run.py` | `run_evaluation_agent` | +| Tools | `tools.py` | `get_tool_schemas`, `execute_tool` | +| Report | `report.py` | `build_report_pdf` | +| Mimic JSON | `mimic_json.py` | `build_mimic_json` | +| Text extraction | `processing/file_dispatcher.py` | `extract_text_from_file` | diff --git a/services/ai-service/src/agent/__init__.py b/services/ai-service/src/agent/__init__.py new file mode 100644 index 0000000..42b2404 --- /dev/null +++ b/services/ai-service/src/agent/__init__.py @@ -0,0 +1,7 @@ +""" +Evaluation agent: LangGraph-based workflow for multi-file compliance evaluation. +""" + +from .run import run_evaluation_agent + +__all__ = ["run_evaluation_agent"] diff --git a/services/ai-service/src/agent/graph.py b/services/ai-service/src/agent/graph.py new file mode 100644 index 0000000..ddb366b --- /dev/null +++ b/services/ai-service/src/agent/graph.py @@ -0,0 +1,205 @@ +""" +LangGraph evaluation graph: file processing -> mimic JSON -> file evaluation loop -> report. +""" + +import json +from typing import Any, Literal + +from langchain_core.messages import AIMessage, HumanMessage, ToolMessage +from langchain_core.messages import RemoveMessage +from langgraph.graph import StateGraph, END, START +from langgraph.graph.message import REMOVE_ALL_MESSAGES + +from .state import EvaluationState +from .groq_client import get_groq_llm +from .tools import get_tool_schemas, execute_tool +from .report import build_report_pdf + +def _file_processing_node(state: EvaluationState) -> dict[str, Any]: + """Extract text for all files; set state.files with path, extracted_text, field_id.""" + from src.processing import extract_text_from_file + + files = state.get("files") or [] + if not files: + return {"errors": (state.get("errors") or []) + ["No files in state"]} + result = [] + for i, f in enumerate(files): + path = f.get("path") or "" + field_id = f.get("field_id") or f"field_{i + 1}" + try: + text = extract_text_from_file(path) + except Exception as e: + text = f"[Extraction error: {e}]" + result.append({"path": path, "extracted_text": text, "field_id": field_id}) + return { + "files": result, + "current_file_index": 0, + "file_evaluations": [], + } + + +def _mimic_json_node(state: EvaluationState) -> dict[str, Any]: + """Ensure mimic_json is in state (already set by run.py from client or inference).""" + return {} + + +def _build_file_prompt(state: EvaluationState) -> str: + """Build the human prompt for the current file evaluation.""" + idx = state.get("current_file_index", 0) + files = state.get("files") or [] + if idx >= len(files): + return "" + f = files[idx] + field_id = f.get("field_id", f"field_{idx + 1}") + text = (f.get("extracted_text") or "")[:15000] + fw = state.get("framework_name", "") + total = len(files) + return ( + f"You are evaluating file {idx + 1} of {total} for framework '{fw}'.\n" + f"Field ID for this file: {field_id}.\n\n" + "## Tool usage (follow strictly)\n" + f"1. Call get_control_ids_for_file once with field_id='{field_id}' to get the control IDs for this file.\n" + "2. For EACH control ID returned, call retrieve_control_details onceβ€”one control_id per call. Do NOT pass multiple IDs.\n" + "3. Use the returned description, calculation, threshold, and scale from each control to understand what it requires.\n\n" + "## Control types and decision semantics\n" + "- If a control has a non-empty 'scale' field (e.g. Leader, Excellent, Good, Fair, Low, Unacceptable): use those levels for your decision.\n" + "- If a control has an empty 'scale' (policy/requirement): use 'Compliant' or 'Not Compliant'.\n\n" + "## Output format (required)\n" + "After retrieving and evaluating all controls, respond with JSON only (no extra text):\n" + '{"control_decisions": [{"control_id": "DG.1.1", "decision": "Compliant", "rationale": "..."}, ...], ' + '"summary": "1-2 paragraph overall assessment of the file against all controls."}\n' + "decision must match control type: use scale levels (Leader, Excellent, Good, Fair, Low, Unacceptable) when scale exists; otherwise Compliant or Not Compliant.\n\n" + f"## File content to evaluate\n\n{text}" + ) + + +def _file_eval_agent_node(state: EvaluationState) -> dict[str, Any]: + """Run the LLM for the current file; if first time for this file, set HumanMessage.""" + messages = list(state.get("messages") or []) + # Start of a new file (messages cleared by file_eval_done): set prompt + if not messages: + prompt = _build_file_prompt(state) + if not prompt: + return {"errors": (state.get("errors") or []) + ["No file at index"]} + messages = [HumanMessage(content=prompt)] + llm = get_groq_llm().bind_tools(get_tool_schemas()) + response = llm.invoke(messages) + # add_messages reducer appends; when starting we need HumanMessage + AIMessage in one update + if len(messages) == 1 and isinstance(messages[0], HumanMessage): + return {"messages": messages + [response]} + return {"messages": [response]} + + +def _should_continue_tools(state: EvaluationState) -> Literal["tools", "file_eval_done"]: + """If last message has tool_calls, go to tools; else file_eval_done.""" + messages = state.get("messages") or [] + if not messages: + return "file_eval_done" + last = messages[-1] + if isinstance(last, AIMessage) and getattr(last, "tool_calls", None): + return "tools" + return "file_eval_done" + + +def _file_eval_tools_node(state: EvaluationState) -> dict[str, Any]: + """Execute tools with access to state (for get_control_ids_for_file).""" + messages = list(state.get("messages") or []) + last = messages[-1] if messages else None + if not isinstance(last, AIMessage) or not getattr(last, "tool_calls", None): + return {} + tool_messages = [] + for tc in last.tool_calls: + name = tc.get("name", "") + args = tc.get("args") or {} + tid = tc.get("id", "") + try: + result = execute_tool(dict(state), name, args) + content = result if isinstance(result, str) else json.dumps(result, ensure_ascii=False) + except Exception as e: + content = str(e) + tool_messages.append(ToolMessage(content=content, tool_call_id=tid)) + return {"messages": tool_messages} + + +def _file_eval_done_node(state: EvaluationState) -> dict[str, Any]: + """Append evaluation from last message to file_evaluations; increment current_file_index; clear messages.""" + messages = state.get("messages") or [] + evaluations = list(state.get("file_evaluations") or []) + files = state.get("files") or [] + idx = state.get("current_file_index", 0) + field_id = files[idx].get("field_id", f"field_{idx + 1}") if idx < len(files) else f"field_{idx + 1}" + content = "" + if messages: + last = messages[-1] + if isinstance(last, AIMessage) and hasattr(last, "content") and last.content: + content = last.content + # Strip markdown code fences if present + content_stripped = content.strip() + if content_stripped.startswith("```"): + lines = content_stripped.split("\n") + if lines[0].startswith("```"): + lines = lines[1:] + if lines and lines[-1].strip() == "```": + lines = lines[:-1] + content_stripped = "\n".join(lines) + else: + content_stripped = content + # Try to parse as structured JSON + try: + parsed = json.loads(content_stripped) + if isinstance(parsed, dict) and isinstance(parsed.get("control_decisions"), list) and parsed["control_decisions"]: + evaluations.append({ + "file_index": idx, + "field_id": field_id, + "control_decisions": parsed["control_decisions"], + "summary": parsed.get("summary", ""), + }) + else: + evaluations.append({"file_index": idx, "field_id": field_id, "summary": content}) + except (json.JSONDecodeError, TypeError): + evaluations.append({"file_index": idx, "field_id": field_id, "summary": content}) + return { + "file_evaluations": evaluations, + "current_file_index": idx + 1, + "messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)], + } + + +def _more_files(state: EvaluationState) -> Literal["file_eval_agent", "report"]: + """If more files remain, next file; else report.""" + files = state.get("files") or [] + if (state.get("current_file_index") or 0) < len(files): + return "file_eval_agent" + return "report" + + +def _report_node(state: EvaluationState) -> dict[str, Any]: + """Build comprehensive report PDF; set report_path.""" + path = build_report_pdf(dict(state)) + return {"report_path": path} + + +def build_evaluation_graph() -> StateGraph: + """Build and compile the LangGraph evaluation graph.""" + workflow = StateGraph(EvaluationState) + + workflow.add_node("file_processing", _file_processing_node) + workflow.add_node("mimic_json", _mimic_json_node) + workflow.add_node("file_eval_agent", _file_eval_agent_node) + workflow.add_node("file_eval_tools", _file_eval_tools_node) + workflow.add_node("file_eval_done", _file_eval_done_node) + workflow.add_node("report", _report_node) + + workflow.add_edge(START, "file_processing") + workflow.add_edge("file_processing", "mimic_json") + workflow.add_edge("mimic_json", "file_eval_agent") + workflow.add_conditional_edges( + "file_eval_agent", + _should_continue_tools, + {"tools": "file_eval_tools", "file_eval_done": "file_eval_done"}, + ) + workflow.add_edge("file_eval_tools", "file_eval_agent") + workflow.add_conditional_edges("file_eval_done", _more_files, {"file_eval_agent": "file_eval_agent", "report": "report"}) + workflow.add_edge("report", END) + + return workflow.compile() \ No newline at end of file diff --git a/services/ai-service/src/agent/groq_client.py b/services/ai-service/src/agent/groq_client.py new file mode 100644 index 0000000..5a0da4a --- /dev/null +++ b/services/ai-service/src/agent/groq_client.py @@ -0,0 +1,31 @@ +""" +Groq LLM client for the evaluation agent. Thin wrapper around langchain_groq. +""" + +import os +from typing import Any + +from dotenv import load_dotenv + +load_dotenv() + +# Model name; can be overridden via env (llama-3.1-70b-versatile was decommissioned Jan 2025) +GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile") + + +def get_groq_llm(model: str | None = None, **kwargs: Any): + """ + Return a Groq chat model (LangChain) for use with LangGraph. + Requires GROQ_API_KEY in env. + """ + from langchain_groq import ChatGroq + + api_key = os.getenv("GROQ_API_KEY") + if not api_key: + raise ValueError("GROQ_API_KEY not set in environment") + return ChatGroq( + model=model or GROQ_MODEL, + api_key=api_key, + temperature=0.2, + **kwargs, + ) diff --git a/services/ai-service/src/agent/mimic_json.py b/services/ai-service/src/agent/mimic_json.py new file mode 100644 index 0000000..6faad46 --- /dev/null +++ b/services/ai-service/src/agent/mimic_json.py @@ -0,0 +1,21 @@ +""" +Pure logic: build the DB-mimic dict { framework_name: { field_1: "id1,id2", ..., field_N: "..." } }. +""" + +from typing import Any + + +def build_mimic_json( + framework_name: str, + field_control_ids: list[tuple[str, str]], +) -> dict[str, Any]: + """ + Build the mimic JSON from framework name and list of (field_id, comma-separated control IDs). + field_control_ids length must match the number of files (one entry per file). + """ + if not field_control_ids: + raise ValueError("At least one field/control_id pair is required") + inner = {} + for field_id, ids_str in field_control_ids: + inner[field_id] = (ids_str or "").strip() + return {framework_name: inner} diff --git a/services/ai-service/src/agent/report.py b/services/ai-service/src/agent/report.py new file mode 100644 index 0000000..46efd44 --- /dev/null +++ b/services/ai-service/src/agent/report.py @@ -0,0 +1,105 @@ +""" +Report generation: comprehensive report from file_evaluations and mimic_json; save PDF. +""" + +from pathlib import Path +from typing import Any + +from reportlab.lib import colors +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle + + +def _project_root() -> Path: + return Path(__file__).parent.parent.parent + + +def build_report_pdf(state: dict[str, Any]) -> str: + """ + Build a comprehensive report PDF from state (file_evaluations, mimic_json, evaluation_id). + Saves to data/reports/{evaluation_id}.pdf. Returns the path (str). + """ + evaluation_id = state.get("evaluation_id") or "unknown" + framework_name = state.get("framework_name") or "" + file_evaluations = state.get("file_evaluations") or [] + mimic_json = state.get("mimic_json") or {} + + root = _project_root() + reports_dir = root / "data" / "reports" + reports_dir.mkdir(parents=True, exist_ok=True) + path = reports_dir / f"{evaluation_id}.pdf" + + doc = SimpleDocTemplate(str(path), pagesize=A4) + styles = getSampleStyleSheet() + story = [] + + story.append(Paragraph("Compliance Evaluation Report", styles["Title"])) + story.append(Spacer(1, 12)) + story.append(Paragraph(f"Evaluation ID: {evaluation_id}", styles["Normal"])) + story.append(Paragraph(f"Framework: {framework_name}", styles["Normal"])) + story.append(Spacer(1, 24)) + + story.append(Paragraph("Executive Summary", styles["Heading1"])) + story.append(Paragraph( + f"Total files evaluated: {len(file_evaluations)}. " + "See per-file assessments below.", + styles["Normal"], + )) + story.append(Spacer(1, 16)) + + story.append(Paragraph("Control IDs per file (mimic JSON)", styles["Heading2"])) + inner = mimic_json.get(framework_name, {}) + for field_id, ids_str in sorted(inner.items()): + story.append(Paragraph(f"{field_id}: {ids_str}", styles["Normal"])) + story.append(Spacer(1, 16)) + + story.append(Paragraph("Per-file assessments", styles["Heading1"])) + for i, ev in enumerate(file_evaluations, 1): + field_id = ev.get("field_id") or f"field_{i}" + story.append(Paragraph(f"File {i} (Field: {field_id})", styles["Heading2"])) + control_decisions = ev.get("control_decisions") + if isinstance(control_decisions, list) and control_decisions: + # Render table: Control ID | Decision | Rationale + # Use Paragraph for cells so text wraps instead of overflowing + def _cell(text: str, style_name: str = "Normal") -> Paragraph: + escaped = str(text).replace("&", "&").replace("<", "<").replace(">", ">").replace("\n", "
") + return Paragraph(escaped, styles[style_name]) + + rows = [[_cell("Control ID", "Heading2"), _cell("Decision", "Heading2"), _cell("Rationale", "Heading2")]] + for cd in control_decisions: + cid = _cell(str(cd.get("control_id", ""))) + decision = _cell(str(cd.get("decision", ""))) + rationale = _cell(str(cd.get("rationale", ""))) + rows.append([cid, decision, rationale]) + # Rationale column gets most width; total ~420pt fits A4 + t = Table(rows, colWidths=[70, 70, 270]) + t.setStyle(TableStyle([ + ("BACKGROUND", (0, 0), (-1, 0), colors.grey), + ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke), + ("ALIGN", (0, 0), (-1, -1), "LEFT"), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), + ("FONTSIZE", (0, 0), (-1, 0), 10), + ("TOPPADDING", (0, 0), (-1, -1), 6), + ("BOTTOMPADDING", (0, 0), (-1, -1), 6), + ("LEFTPADDING", (0, 0), (-1, -1), 6), + ("RIGHTPADDING", (0, 0), (-1, -1), 6), + ("BACKGROUND", (0, 1), (-1, -1), colors.beige), + ("GRID", (0, 0), (-1, -1), 0.5, colors.black), + ])) + story.append(t) + story.append(Spacer(1, 8)) + summary = ev.get("summary") or ev.get("evaluation") or "" + if summary: + text = summary if len(summary) <= 2000 else summary[:2000] + "..." + story.append(Paragraph(text.replace("\n", "
"), styles["Normal"])) + elif not control_decisions: + text = str(ev) + if len(text) > 2000: + text = text[:2000] + "..." + story.append(Paragraph(text.replace("\n", "
"), styles["Normal"])) + story.append(Spacer(1, 8)) + + doc.build(story) + return str(path) diff --git a/services/ai-service/src/agent/run.py b/services/ai-service/src/agent/run.py new file mode 100644 index 0000000..8f896cd --- /dev/null +++ b/services/ai-service/src/agent/run.py @@ -0,0 +1,80 @@ +""" +Entry point: persist uploaded files, build state, run LangGraph, return state (mimic_json, report_path, file_evaluations). +""" + +import uuid +from pathlib import Path +from typing import Any + +from .state import EvaluationState +from .graph import build_evaluation_graph +from .mimic_json import build_mimic_json + + +def _project_root() -> Path: + return Path(__file__).parent.parent.parent + + +def run_evaluation_agent( + framework_name: str, + files: list[tuple[str, bytes]], + control_ids_per_file: list[str] | None = None, +) -> dict[str, Any]: + """ + Run the evaluation agent: persist files, build mimic JSON, run graph, return final state. + + Args: + framework_name: Framework name (e.g. NDI). + files: List of (filename, bytes); 1 or more files. + control_ids_per_file: List of comma-separated control ID strings (one per file). + Must have the same length as files. Can be empty strings if not provided. + + Returns: + dict with mimic_json, evaluation_id, report_path, file_evaluations. + """ + if not files: + raise ValueError("At least one file is required") + n = len(files) + if control_ids_per_file is not None and len(control_ids_per_file) != n: + raise ValueError(f"control_ids_per_file has {len(control_ids_per_file)} entries but {n} files; they must match") + + evaluation_id = str(uuid.uuid4()) + root = _project_root() + eval_dir = root / "data" / "evaluations" / evaluation_id + eval_dir.mkdir(parents=True, exist_ok=True) + + # Persist files and build file list with path and field_id + file_list = [] + for i, (filename, body) in enumerate(files): + safe_name = (filename or f"file_{i+1}").replace("..", "_").strip() or f"file_{i+1}" + path = eval_dir / safe_name + path.write_bytes(body) + field_id = f"field_{i + 1}" + file_list.append({"path": str(path), "extracted_text": "", "field_id": field_id}) + + # Build mimic JSON: from client control_ids (must match file count) + if control_ids_per_file: + field_control_ids = [(f"field_{i+1}", s.strip()) for i, s in enumerate(control_ids_per_file)] + else: + field_control_ids = [(f"field_{i+1}", "") for i in range(n)] + mimic_json = build_mimic_json(framework_name, field_control_ids) + + initial_state: EvaluationState = { + "evaluation_id": evaluation_id, + "framework_name": framework_name, + "files": file_list, + "mimic_json": mimic_json, + "current_file_index": 0, + "file_evaluations": [], + "messages": [], + } + + graph = build_evaluation_graph() + final_state = graph.invoke(initial_state) + + return { + "evaluation_id": evaluation_id, + "mimic_json": final_state.get("mimic_json", mimic_json), + "report_path": final_state.get("report_path", ""), + "file_evaluations": final_state.get("file_evaluations", []), + } diff --git a/services/ai-service/src/agent/state.py b/services/ai-service/src/agent/state.py new file mode 100644 index 0000000..6b564ee --- /dev/null +++ b/services/ai-service/src/agent/state.py @@ -0,0 +1,22 @@ +""" +LangGraph state for the evaluation agent. +""" + +from typing import Annotated, Any, TypedDict + +from langgraph.graph.message import add_messages + + +class EvaluationState(TypedDict, total=False): + """State for the evaluation graph.""" + + evaluation_id: str + framework_name: str + files: list[dict[str, Any]] # [{path, extracted_text, field_id}, ...] + mimic_json: dict[str, Any] # { framework_name: { field_1: "id1,id2", ... } } + current_file_index: int + file_evaluations: list[dict[str, Any]] # per-file assessment results + report_path: str + errors: list[str] + # For agent node: messages (if using message-based LLM) + messages: Annotated[list, add_messages] diff --git a/services/ai-service/src/agent/tools.py b/services/ai-service/src/agent/tools.py new file mode 100644 index 0000000..b104e20 --- /dev/null +++ b/services/ai-service/src/agent/tools.py @@ -0,0 +1,83 @@ +""" +Agent tools: (1) get control IDs for this PDF from mimic JSON, (2) retrieve control details from vector DB. +Both are used by the LLM during file evaluation. +""" + +from typing import Any + +from src.rag import retrieve_control_details as rag_retrieve_control_details +from src.rag.retrieval import RETRIEVE_CONTROL_DETAILS_TOOL_SCHEMA + +# Schema for get_control_ids_for_file (OpenAI-style for Groq) +GET_CONTROL_IDS_TOOL_SCHEMA = { + "type": "function", + "function": { + "name": "get_control_ids_for_file", + "description": ( + "Get the comma-separated control IDs assigned to this file/field from the mimic JSON. " + "Returns a string like 'DG.1.1,DG.1.2,DSI.OE.01'. You must then call retrieve_control_details " + "once for EACH control ID in that list to fetch rules and requirements from the vector DB. " + "Do not call retrieve_control_details with multiple IDsβ€”it accepts only one control_id per call." + ), + "parameters": { + "type": "object", + "properties": { + "field_id": { + "type": "string", + "description": "The field identifier for this file (e.g. field_1, field_2, ..., field_15).", + }, + }, + "required": ["field_id"], + }, + }, +} + + +def get_control_ids_for_file(state: dict[str, Any], field_id: str) -> str: + """ + Read from state's mimic_json: return comma-separated control IDs for the given field_id. + """ + mimic = state.get("mimic_json") or {} + fw = state.get("framework_name") or "" + return mimic.get(fw, {}).get(field_id, "") + + +def retrieve_control_details_multi( + control_ids: str, + framework_name: str, + *, + top_k_pdf: int = 5, +) -> dict[str, Any]: + """ + Retrieve control details for multiple control IDs (comma-separated). + Calls src.rag.retrieve_control_details for each and returns combined result. + """ + ids = [x.strip() for x in control_ids.split(",") if x.strip()] + controls = [] + for cid in ids: + try: + out = rag_retrieve_control_details(cid, framework_name, top_k_pdf=top_k_pdf) + controls.append({"control_id": cid, **out}) + except Exception as e: + controls.append({"control_id": cid, "error": str(e), "json_cards": [], "pdf_chunks": []}) + return {"controls": controls} + + +def get_tool_schemas() -> list[dict]: + """Return list of tool schemas for binding to the LLM (Groq/OpenAI format).""" + return [GET_CONTROL_IDS_TOOL_SCHEMA, RETRIEVE_CONTROL_DETAILS_TOOL_SCHEMA] + + +def execute_tool(state: dict[str, Any], name: str, args: dict[str, Any]) -> Any: + """ + Execute a tool by name with the given args. State is used for get_control_ids_for_file. + """ + if name == "get_control_ids_for_file": + return get_control_ids_for_file(state, args["field_id"]) + if name == "retrieve_control_details": + return rag_retrieve_control_details( + args["control_id"], + args["framework_name"], + top_k_pdf=args.get("top_k_pdf", 5), + ) + raise ValueError(f"Unknown tool: {name}") diff --git a/services/ai-service/src/api/app.py b/services/ai-service/src/api/app.py index df6d238..644ba0b 100644 --- a/services/ai-service/src/api/app.py +++ b/services/ai-service/src/api/app.py @@ -1,11 +1,20 @@ """FastAPI application: CORS, routers, exception handlers.""" +from pathlib import Path + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from src.api.models import ExtractionError -from src.api.routers import frameworks, health +from src.api.routers import evaluations, frameworks, health + +# Ensure data dirs exist at runtime (evaluations uploads, reports PDFs) +def _ensure_data_dirs(): + root = Path(__file__).resolve().parent.parent.parent + (root / "data" / "evaluations").mkdir(parents=True, exist_ok=True) + (root / "data" / "reports").mkdir(parents=True, exist_ok=True) + app = FastAPI( title="Governance Agent API", @@ -25,6 +34,12 @@ app.include_router(health.router) app.include_router(frameworks.router) +app.include_router(evaluations.router) + + +@app.on_event("startup") +def on_startup(): + _ensure_data_dirs() @app.exception_handler(ExtractionError) diff --git a/services/ai-service/src/api/models/README.md b/services/ai-service/src/api/models/README.md new file mode 100644 index 0000000..5541960 --- /dev/null +++ b/services/ai-service/src/api/models/README.md @@ -0,0 +1,6 @@ +# api/models β€” Request/Response Schemas + +- **requests.py** β€” Placeholder; form/file params are validated in routers (Form, File). +- **responses.py** β€” ControlSummary, SetupFrameworkResponse, ErrorResponse; ExtractionError exception for extraction failures. + +Used for OpenAPI docs and response validation. diff --git a/services/ai-service/src/api/models/__init__.py b/services/ai-service/src/api/models/__init__.py new file mode 100644 index 0000000..4c7980d --- /dev/null +++ b/services/ai-service/src/api/models/__init__.py @@ -0,0 +1,17 @@ +"""Pydantic request/response models for the API.""" + +from .responses import ( + ControlSummary, + ErrorResponse, + ExtractionError, + SetupFrameworkResponse, + SubmitEvaluationResponse, +) + +__all__ = [ + "ControlSummary", + "ErrorResponse", + "ExtractionError", + "SetupFrameworkResponse", + "SubmitEvaluationResponse", +] diff --git a/services/ai-service/src/api/models/requests.py b/services/ai-service/src/api/models/requests.py new file mode 100644 index 0000000..12bf3b8 --- /dev/null +++ b/services/ai-service/src/api/models/requests.py @@ -0,0 +1,5 @@ +"""Request schemas for API endpoints. + +Form/File params (framework_name, section_names, files) are validated +directly in the router via Form(...) and File(...). +""" diff --git a/services/ai-service/src/api/models/responses.py b/services/ai-service/src/api/models/responses.py new file mode 100644 index 0000000..c9b8eda --- /dev/null +++ b/services/ai-service/src/api/models/responses.py @@ -0,0 +1,49 @@ +"""Response schemas for API endpoints.""" + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field + + +class ErrorResponse(BaseModel): + """Structured error response for API exceptions.""" + + error: str = Field(..., description="Error code or type") + message: str = Field(..., description="Human-readable message") + detail: Optional[str] = Field(None, description="Additional detail (e.g. framework_name)") + + +class ControlSummary(BaseModel): + """Summary of extracted controls for one section (PDF).""" + + section_name: str = Field(..., description="Name of the section (PDF)") + controls_count: int = Field(..., ge=0, description="Number of controls extracted") + json_path: str = Field(..., description="Relative path to the saved JSON file") + + +class SetupFrameworkResponse(BaseModel): + """Response after successfully setting up a framework from multiple PDFs.""" + + framework_name: str = Field(..., description="Framework identifier") + total_controls: int = Field(..., ge=0, description="Total controls across all sections") + sections: list[ControlSummary] = Field(default_factory=list, description="Per-section summary") + created_at: datetime = Field(default_factory=datetime.utcnow, description="When the setup completed") + + +class SubmitEvaluationResponse(BaseModel): + """Response after submitting an evaluation (15 files + framework).""" + + evaluation_id: str = Field(..., description="Unique evaluation run ID") + mimic_json: dict = Field(..., description="DB-mimic: framework_name -> field_1..field_15 -> comma-separated control IDs") + report_path: str = Field("", description="Path to the generated report PDF") + file_evaluations: list[dict] = Field(default_factory=list, description="Per-file assessment results") + + +class ExtractionError(Exception): + """Raised when framework extraction fails (e.g. LLM API error).""" + + def __init__(self, message: str, framework_name: Optional[str] = None): + super().__init__(message) + self.message = message + self.framework_name = framework_name diff --git a/services/ai-service/src/api/routers/evaluations.py b/services/ai-service/src/api/routers/evaluations.py new file mode 100644 index 0000000..af27dd3 --- /dev/null +++ b/services/ai-service/src/api/routers/evaluations.py @@ -0,0 +1,84 @@ +""" +Evaluation endpoints: submit files + framework, get mimic JSON + report PDF path. +Number of files is flexible; one control_ids field per file (control_ids_1, control_ids_2, ...). +""" + +from fastapi import APIRouter, File, Form, HTTPException, UploadFile + +from src.api.models import SubmitEvaluationResponse +from src.services import EvaluationService + +router = APIRouter(prefix="/api/v1/evaluations", tags=["evaluations"]) + +_MAX_FILES = 20 + + +def _control_ids_form(i: int, default: str = ""): + return Form(default=default, description=f"Comma-separated control IDs for file {i}") + + +@router.post("/submit", response_model=SubmitEvaluationResponse) +async def submit_evaluation( + framework_name: str = Form(..., min_length=1, description="Framework identifier (e.g. NDI)"), + files: list[UploadFile] = File(..., description="Files (PDF, DOCX, PPTX, CSV, XLSX); 1 or more"), + control_ids_1: str = _control_ids_form(1), + control_ids_2: str = _control_ids_form(2), + control_ids_3: str = _control_ids_form(3), + control_ids_4: str = _control_ids_form(4), + control_ids_5: str = _control_ids_form(5), + control_ids_6: str = _control_ids_form(6), + control_ids_7: str = _control_ids_form(7), + control_ids_8: str = _control_ids_form(8), + control_ids_9: str = _control_ids_form(9), + control_ids_10: str = _control_ids_form(10), + control_ids_11: str = _control_ids_form(11), + control_ids_12: str = _control_ids_form(12), + control_ids_13: str = _control_ids_form(13), + control_ids_14: str = _control_ids_form(14), + control_ids_15: str = _control_ids_form(15), + control_ids_16: str = _control_ids_form(16), + control_ids_17: str = _control_ids_form(17), + control_ids_18: str = _control_ids_form(18), + control_ids_19: str = _control_ids_form(19), + control_ids_20: str = _control_ids_form(20), +) -> SubmitEvaluationResponse: + """ + Submit an evaluation: files + framework name. Each file has its own control_ids field + (control_ids_1 for file 1, control_ids_2 for file 2, etc.). Each value = comma-separated IDs. + Returns DB-mimic JSON + evaluation_id + report_path. + """ + if not files: + raise HTTPException(status_code=400, detail="At least one file is required") + + n_files = len(files) + if n_files > _MAX_FILES: + raise HTTPException(status_code=400, detail=f"Maximum {_MAX_FILES} files allowed") + + control_ids_fields = [ + control_ids_1, control_ids_2, control_ids_3, control_ids_4, control_ids_5, + control_ids_6, control_ids_7, control_ids_8, control_ids_9, control_ids_10, + control_ids_11, control_ids_12, control_ids_13, control_ids_14, control_ids_15, + control_ids_16, control_ids_17, control_ids_18, control_ids_19, control_ids_20, + ] + control_ids_per_file = [s.strip() for s in control_ids_fields[:n_files]] + + file_tuples: list[tuple[str, bytes]] = [] + for u in files: + if not u.filename: + raise HTTPException(status_code=400, detail="Each file must have a filename") + body = await u.read() + file_tuples.append((u.filename, body)) + + service = EvaluationService() + result = service.submit_evaluation( + framework_name=framework_name, + files=file_tuples, + control_ids_per_file=control_ids_per_file, + ) + + return SubmitEvaluationResponse( + evaluation_id=result["evaluation_id"], + mimic_json=result["mimic_json"], + report_path=result.get("report_path", ""), + file_evaluations=result.get("file_evaluations", []), + ) diff --git a/services/ai-service/src/processing/__init__.py b/services/ai-service/src/processing/__init__.py index e21c40d..420d61f 100644 --- a/services/ai-service/src/processing/__init__.py +++ b/services/ai-service/src/processing/__init__.py @@ -1,8 +1,18 @@ from .pdf_parser import extract_text_from_pdf from .text_chunker import chunk_text, chunk_text_by_sentences +from .tabular_parser import extract_text_from_csv, extract_text_from_xlsx, extract_text_from_tabular +from .pptx_parser import extract_text_from_pptx +from .docx_parser import extract_text_from_docx +from .file_dispatcher import extract_text_from_file __all__ = [ - "extract_text_from_pdf", - "chunk_text", - "chunk_text_by_sentences" + "extract_text_from_pdf", + "chunk_text", + "chunk_text_by_sentences", + "extract_text_from_csv", + "extract_text_from_xlsx", + "extract_text_from_tabular", + "extract_text_from_pptx", + "extract_text_from_docx", + "extract_text_from_file", ] diff --git a/services/ai-service/src/processing/docx_parser.py b/services/ai-service/src/processing/docx_parser.py new file mode 100644 index 0000000..c4f830b --- /dev/null +++ b/services/ai-service/src/processing/docx_parser.py @@ -0,0 +1,30 @@ +""" +DOCX parser: extract text from Word documents. +""" + +from pathlib import Path + + +def extract_text_from_docx(path: str) -> str: + """ + Extract text from a DOCX file. Reads paragraphs and table cells. + """ + from docx import Document + + p = Path(path) + if not p.exists(): + raise FileNotFoundError(f"DOCX file not found: {path}") + try: + doc = Document(path) + parts = [] + for para in doc.paragraphs: + if para.text.strip(): + parts.append(para.text.strip()) + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + if cell.text.strip(): + parts.append(cell.text.strip()) + return "\n\n".join(parts) + except Exception as e: + raise ValueError(f"Could not read DOCX {path}: {e}") from e diff --git a/services/ai-service/src/processing/file_dispatcher.py b/services/ai-service/src/processing/file_dispatcher.py new file mode 100644 index 0000000..98dc929 --- /dev/null +++ b/services/ai-service/src/processing/file_dispatcher.py @@ -0,0 +1,27 @@ +""" +Dispatcher: given file path (and optional extension), call the right parser and return extracted text. +""" + +from pathlib import Path + +from .pdf_parser import extract_text_from_pdf +from .tabular_parser import extract_text_from_tabular +from .pptx_parser import extract_text_from_pptx +from .docx_parser import extract_text_from_docx + + +def extract_text_from_file(path: str, extension: str | None = None) -> str: + """ + Dispatch by extension to the appropriate parser. Returns extracted text. + Supported: .pdf, .csv, .xlsx, .xls, .pptx, .docx. + """ + ext = (extension or Path(path).suffix).lower().lstrip(".") + if ext == "pdf": + return extract_text_from_pdf(path) + if ext in ("csv", "xlsx", "xls"): + return extract_text_from_tabular(path) + if ext == "pptx": + return extract_text_from_pptx(path) + if ext == "docx": + return extract_text_from_docx(path) + raise ValueError(f"Unsupported file extension: {ext}") diff --git a/services/ai-service/src/processing/pptx_parser.py b/services/ai-service/src/processing/pptx_parser.py new file mode 100644 index 0000000..9488947 --- /dev/null +++ b/services/ai-service/src/processing/pptx_parser.py @@ -0,0 +1,26 @@ +""" +PPTX parser: extract text from PowerPoint files. +""" + +from pathlib import Path + + +def extract_text_from_pptx(path: str) -> str: + """ + Extract text from a PPTX file. Iterates slides and shape text. + """ + from pptx import Presentation + + p = Path(path) + if not p.exists(): + raise FileNotFoundError(f"PPTX file not found: {path}") + try: + prs = Presentation(path) + parts = [] + for slide in prs.slides: + for shape in slide.shapes: + if hasattr(shape, "text") and shape.text: + parts.append(shape.text.strip()) + return "\n\n".join(p for p in parts if p) + except Exception as e: + raise ValueError(f"Could not read PPTX {path}: {e}") from e diff --git a/services/ai-service/src/processing/tabular_parser.py b/services/ai-service/src/processing/tabular_parser.py new file mode 100644 index 0000000..660da55 --- /dev/null +++ b/services/ai-service/src/processing/tabular_parser.py @@ -0,0 +1,49 @@ +""" +Tabular parser: extract text from CSV and XLSX files. +""" + +from pathlib import Path + + +def extract_text_from_csv(path: str) -> str: + """ + Extract text from a CSV file. Returns table as plain text (rows joined). + """ + import pandas as pd + + p = Path(path) + if not p.exists(): + raise FileNotFoundError(f"CSV file not found: {path}") + try: + df = pd.read_csv(path) + return df.to_string(index=False) + except Exception as e: + raise ValueError(f"Could not read CSV {path}: {e}") from e + + +def extract_text_from_xlsx(path: str) -> str: + """ + Extract text from an XLSX file. Reads first sheet; returns table as plain text. + """ + import pandas as pd + + p = Path(path) + if not p.exists(): + raise FileNotFoundError(f"XLSX file not found: {path}") + try: + df = pd.read_excel(path, sheet_name=0) + return df.to_string(index=False) + except Exception as e: + raise ValueError(f"Could not read XLSX {path}: {e}") from e + + +def extract_text_from_tabular(path: str) -> str: + """ + Dispatch by extension: .csv -> extract_text_from_csv, .xlsx/.xls -> extract_text_from_xlsx. + """ + suffix = Path(path).suffix.lower() + if suffix == ".csv": + return extract_text_from_csv(path) + if suffix in (".xlsx", ".xls"): + return extract_text_from_xlsx(path) + raise ValueError(f"Unsupported tabular extension: {suffix}") diff --git a/services/ai-service/src/rag/retrieval.py b/services/ai-service/src/rag/retrieval.py index 4a51e23..8a86b3d 100644 --- a/services/ai-service/src/rag/retrieval.py +++ b/services/ai-service/src/rag/retrieval.py @@ -21,17 +21,19 @@ "function": { "name": "retrieve_control_details", "description": ( - "Retrieve full details for a compliance control by ID. Returns structured " - "JSON control cards (id, description, calculation, threshold, scale) plus " - "relevant PDF passages from the framework. Use this when you need to look up " - "what a specific control requires or when evaluating evidence against a control." + "Retrieve full details for ONE compliance control at a time from the vector database. " + "Returns JSON control cards (id, description, calculation, threshold, scale) plus " + "relevant PDF passages. IMPORTANT: Pass exactly ONE control_id per call. Call this " + "tool separately for each control ID. Do NOT pass comma-separated IDs or multiple IDs. " + "Use the returned description, calculation, and scale to understand what the control " + "requires before evaluating the file content." ), "parameters": { "type": "object", "properties": { "control_id": { "type": "string", - "description": "The control identifier (e.g. DSI.OE.01, DG.1, DG.1.1).", + "description": "Exactly one control ID per call (e.g. DG.1.1, DSI.OE.01).", }, "framework_name": { "type": "string", diff --git a/services/ai-service/src/services/__init__.py b/services/ai-service/src/services/__init__.py index ccb76bc..45f34d7 100644 --- a/services/ai-service/src/services/__init__.py +++ b/services/ai-service/src/services/__init__.py @@ -1,5 +1,6 @@ """Service layer for orchestration.""" from .framework_service import FrameworkService +from .evaluation_service import EvaluationService -__all__ = ["FrameworkService"] +__all__ = ["FrameworkService", "EvaluationService"] diff --git a/services/ai-service/src/services/evaluation_service.py b/services/ai-service/src/services/evaluation_service.py new file mode 100644 index 0000000..b39ebeb --- /dev/null +++ b/services/ai-service/src/services/evaluation_service.py @@ -0,0 +1,30 @@ +""" +Evaluation service: submit 15 files + framework, run agent, return mimic JSON + report path. +No DB; files and report saved to filesystem. +""" + +from typing import Any + +from src.agent import run_evaluation_agent + + +class EvaluationService: + """Orchestrates evaluation submission and agent run.""" + + def submit_evaluation( + self, + framework_name: str, + files: list[tuple[str, bytes]], + control_ids_per_file: list[str] | None = None, + ) -> dict[str, Any]: + """ + Submit an evaluation: files (1+) + framework name. control_ids_per_file must have the same + length as files (one comma-separated control ID string per file). + Runs the LangGraph agent, saves report PDF to data/reports/{evaluation_id}.pdf. + Returns mimic_json, evaluation_id, report_path, file_evaluations. + """ + return run_evaluation_agent( + framework_name=framework_name, + files=files, + control_ids_per_file=control_ids_per_file, + ) diff --git a/services/ai-service/uv.lock b/services/ai-service/uv.lock index 3cf9aa4..942770f 100644 --- a/services/ai-service/uv.lock +++ b/services/ai-service/uv.lock @@ -2,8 +2,12 @@ version = 1 revision = 3 requires-python = ">=3.13" resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version < '3.14'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] [[package]] @@ -32,15 +36,24 @@ dependencies = [ { name = "accelerate" }, { name = "aiofiles" }, { name = "fastapi" }, + { name = "groq" }, { name = "haystack-ai" }, { name = "ipykernel" }, + { name = "langchain-core" }, + { name = "langchain-groq" }, + { name = "langgraph" }, { name = "openai" }, + { name = "openpyxl" }, + { name = "pandas" }, { name = "pdfplumber" }, { name = "pydantic" }, { name = "pypdf" }, + { name = "python-docx" }, { name = "python-dotenv" }, { name = "python-multipart" }, + { name = "python-pptx" }, { name = "qdrant-client" }, + { name = "reportlab" }, { name = "sentence-transformers" }, { name = "torch" }, { name = "transformers" }, @@ -56,15 +69,24 @@ requires-dist = [ { name = "accelerate", specifier = ">=0.24.0" }, { name = "aiofiles", specifier = ">=24.0.0" }, { name = "fastapi", specifier = ">=0.128.0" }, + { name = "groq", specifier = ">=0.4.0" }, { name = "haystack-ai", specifier = ">=2.0.0" }, { name = "ipykernel", specifier = ">=7.1.0" }, + { name = "langchain-core", specifier = ">=0.3.0" }, + { name = "langchain-groq", specifier = ">=0.2.0" }, + { name = "langgraph", specifier = ">=0.2.0" }, { name = "openai", specifier = ">=1.0.0" }, + { name = "openpyxl", specifier = ">=3.1.0" }, + { name = "pandas", specifier = ">=2.0.0" }, { name = "pdfplumber", specifier = ">=0.10.0" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pypdf", specifier = ">=3.0.0" }, + { name = "python-docx", specifier = ">=1.0.0" }, { name = "python-dotenv", specifier = ">=1.0.0" }, { name = "python-multipart", specifier = ">=0.0.9" }, + { name = "python-pptx", specifier = ">=0.6.0" }, { name = "qdrant-client", specifier = ">=1.8.0" }, + { name = "reportlab", specifier = ">=4.0.0" }, { name = "sentence-transformers", specifier = ">=2.2.0" }, { name = "torch", specifier = ">=2.0.0" }, { name = "transformers", specifier = ">=4.35.0" }, @@ -334,7 +356,7 @@ name = "cuda-bindings" version = "12.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, @@ -417,6 +439,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -572,6 +603,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, ] +[[package]] +name = "groq" +version = "0.37.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/78/18948a9056e1509c87e10ab8316a90ecce87035fbd53342dffdf97f4de00/groq-0.37.1.tar.gz", hash = "sha256:7353d6dfb60834fd7aacbb86af106e2dc2aeaff6d0edd65fb2fd0f16bd39314c", size = 145289, upload-time = "2025-12-04T18:08:07.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/d6/645a081750e43f858b7d09dce5d8e1e76cf11e7e4bdba81252e04f78963d/groq-0.37.1-py3-none-any.whl", hash = "sha256:b49f8c8898c55eaec9f71f1342f3fcacc9560d67a08ce5f35fbfb84e8dacd3da", size = 137494, upload-time = "2025-12-04T18:08:05.801Z" }, +] + [[package]] name = "grpcio" version = "1.76.0" @@ -938,6 +986,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -994,6 +1063,113 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] +[[package]] +name = "langchain-core" +version = "1.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/0e/664d8d81b3493e09cbab72448d2f9d693d1fa5aa2bcc488602203a9b6da0/langchain_core-1.2.7.tar.gz", hash = "sha256:e1460639f96c352b4a41c375f25aeb8d16ffc1769499fb1c20503aad59305ced", size = 837039, upload-time = "2026-01-09T17:44:25.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/6f/34a9fba14d191a67f7e2ee3dbce3e9b86d2fa7310e2c7f2c713583481bd2/langchain_core-1.2.7-py3-none-any.whl", hash = "sha256:452f4fef7a3d883357b22600788d37e3d8854ef29da345b7ac7099f33c31828b", size = 490232, upload-time = "2026-01-09T17:44:24.236Z" }, +] + +[[package]] +name = "langchain-groq" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "groq" }, + { name = "langchain-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/13/f8d4ec7a28dd146e73b9c0b5868f7656453a4830b84998f8c4c1c73edf91/langchain_groq-1.1.1.tar.gz", hash = "sha256:09e04d7301fbd7b6b711187d23f42caf7f5222d5fa2cf04683dba9a3f273c4d2", size = 203548, upload-time = "2025-12-12T22:00:46.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/4a/3d6227a16fe9f79968414b50e50869519378b20653805e2e8fab283908e6/langchain_groq-1.1.1-py3-none-any.whl", hash = "sha256:1c6d5146f60205dcde09d7e47bb5291c295d3f0c7bcd2417e4d3a73a04bd1050", size = 19039, upload-time = "2025-12-12T22:00:45.86Z" }, +] + +[[package]] +name = "langgraph" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/5b/f72655717c04e33d3b62f21b166dc063d192b53980e9e3be0e2a117f1c9f/langgraph-1.0.7.tar.gz", hash = "sha256:0cfdfee51e6e8cfe503ecc7367c73933437c505b03fa10a85c710975c8182d9a", size = 497098, upload-time = "2026-01-22T16:57:47.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/0e/fe80144e3e4048e5d19ccdb91ac547c1a7dc3da8dbd1443e210048194c14/langgraph-1.0.7-py3-none-any.whl", hash = "sha256:9d68e8f8dd8f3de2fec45f9a06de05766d9b075b78fb03171779893b7a52c4d2", size = 157353, upload-time = "2026-01-22T16:57:45.997Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/76/55a18c59dedf39688d72c4b06af73a5e3ea0d1a01bc867b88fbf0659f203/langgraph_checkpoint-4.0.0.tar.gz", hash = "sha256:814d1bd050fac029476558d8e68d87bce9009a0262d04a2c14b918255954a624", size = 137320, upload-time = "2026-01-12T20:30:26.38Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/de/ddd53b7032e623f3c7bcdab2b44e8bf635e468f62e10e5ff1946f62c9356/langgraph_checkpoint-4.0.0-py3-none-any.whl", hash = "sha256:3fa9b2635a7c5ac28b338f631abf6a030c3b508b7b9ce17c22611513b589c784", size = 46329, upload-time = "2026-01-12T20:30:25.2Z" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/59/711aecd1a50999456850dc328f3cad72b4372d8218838d8d5326f80cb76f/langgraph_prebuilt-1.0.7.tar.gz", hash = "sha256:38e097e06de810de4d0e028ffc0e432bb56d1fb417620fb1dfdc76c5e03e4bf9", size = 163692, upload-time = "2026-01-22T16:45:22.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/49/5e37abb3f38a17a3487634abc2a5da87c208cc1d14577eb8d7184b25c886/langgraph_prebuilt-1.0.7-py3-none-any.whl", hash = "sha256:e14923516504405bb5edc3977085bc9622c35476b50c1808544490e13871fe7c", size = 35324, upload-time = "2026-01-22T16:45:21.784Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/0f/ed0634c222eed48a31ba48eab6881f94ad690d65e44fe7ca838240a260c1/langgraph_sdk-0.3.3.tar.gz", hash = "sha256:c34c3dce3b6848755eb61f0c94369d1ba04aceeb1b76015db1ea7362c544fb26", size = 130589, upload-time = "2026-01-13T00:30:43.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/be/4ad511bacfdd854afb12974f407cb30010dceb982dc20c55491867b34526/langgraph_sdk-0.3.3-py3-none-any.whl", hash = "sha256:a52ebaf09d91143e55378bb2d0b033ed98f57f48c9ad35c8f81493b88705fc7b", size = 67021, upload-time = "2026-01-13T00:30:42.264Z" }, +] + +[[package]] +name = "langsmith" +version = "0.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "uuid-utils" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/3d/04a79fb7f0e72af88e26295d3b9ab88e5204eafb723a8ed3a948f8df1f19/langsmith-0.6.6.tar.gz", hash = "sha256:64ba70e7b795cff3c498fe6f2586314da1cc855471a5e5b6a357950324af3874", size = 953566, upload-time = "2026-01-27T17:37:21.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/81/62c5cc980a3f5a7476792769616792e0df8ba9c8c4730195ec700a56a962/langsmith-0.6.6-py3-none-any.whl", hash = "sha256:fe655e73b198cd00d0ecd00a26046eaf1f78cd0b2f0d94d1e5591f3143c5f592", size = 308542, upload-time = "2026-01-27T17:37:19.201Z" }, +] + [[package]] name = "lazy-imports" version = "1.2.0" @@ -1003,6 +1179,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cd/62/60ed24fa8707f10c1c5aef94791252b820be3dd6bdfc6e2fcdb08bc8912f/lazy_imports-1.2.0-py3-none-any.whl", hash = "sha256:97134d6552e2ba16f1a278e316f05313ab73b360e848e40d593d08a5c2406fdf", size = 18681, upload-time = "2025-12-28T13:51:49.802Z" }, ] +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1211,7 +1449,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, @@ -1222,7 +1460,7 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, @@ -1249,9 +1487,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, - { name = "nvidia-cusparse-cu12" }, - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, @@ -1262,7 +1500,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, @@ -1327,13 +1565,137 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" }, ] +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/a3/4e09c61a5f0c521cba0bb433639610ae037437669f1a4cbc93799e731d78/orjson-3.11.6.tar.gz", hash = "sha256:0a54c72259f35299fd033042367df781c2f66d10252955ca1efb7db309b954cb", size = 6175856, upload-time = "2026-01-29T15:13:07.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/45/d9c71c8c321277bc1ceebf599bc55ba826ae538b7c61f287e9a7e71bd589/orjson-3.11.6-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e4ae1670caabb598a88d385798692ce2a1b2f078971b3329cfb85253c6097f5b", size = 249828, upload-time = "2026-01-29T15:12:20.14Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7e/4afcf4cfa9c2f93846d70eee9c53c3c0123286edcbeb530b7e9bd2aea1b2/orjson-3.11.6-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:2c6b81f47b13dac2caa5d20fbc953c75eb802543abf48403a4703ed3bff225f0", size = 134339, upload-time = "2026-01-29T15:12:22.01Z" }, + { url = "https://files.pythonhosted.org/packages/40/10/6d2b8a064c8d2411d3d0ea6ab43125fae70152aef6bea77bb50fa54d4097/orjson-3.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:647d6d034e463764e86670644bdcaf8e68b076e6e74783383b01085ae9ab334f", size = 137662, upload-time = "2026-01-29T15:12:23.307Z" }, + { url = "https://files.pythonhosted.org/packages/5a/50/5804ea7d586baf83ee88969eefda97a24f9a5bdba0727f73e16305175b26/orjson-3.11.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8523b9cc4ef174ae52414f7699e95ee657c16aa18b3c3c285d48d7966cce9081", size = 134626, upload-time = "2026-01-29T15:12:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2e/f0492ed43e376722bb4afd648e06cc1e627fc7ec8ff55f6ee739277813ea/orjson-3.11.6-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:313dfd7184cde50c733fc0d5c8c0e2f09017b573afd11dc36bd7476b30b4cb17", size = 140873, upload-time = "2026-01-29T15:12:26.369Z" }, + { url = "https://files.pythonhosted.org/packages/10/15/6f874857463421794a303a39ac5494786ad46a4ab46d92bda6705d78c5aa/orjson-3.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905ee036064ff1e1fd1fb800055ac477cdcb547a78c22c1bc2bbf8d5d1a6fb42", size = 144044, upload-time = "2026-01-29T15:12:28.082Z" }, + { url = "https://files.pythonhosted.org/packages/d2/c7/b7223a3a70f1d0cc2d86953825de45f33877ee1b124a91ca1f79aa6e643f/orjson-3.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce374cb98411356ba906914441fc993f271a7a666d838d8de0e0900dd4a4bc12", size = 142396, upload-time = "2026-01-29T15:12:30.529Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/aa1b6d3ad3cd80f10394134f73ae92a1d11fdbe974c34aa199cc18bb5fcf/orjson-3.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cded072b9f65fcfd188aead45efa5bd528ba552add619b3ad2a81f67400ec450", size = 145600, upload-time = "2026-01-29T15:12:31.848Z" }, + { url = "https://files.pythonhosted.org/packages/f6/cf/e4aac5a46cbd39d7e769ef8650efa851dfce22df1ba97ae2b33efe893b12/orjson-3.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ab85bdbc138e1f73a234db6bb2e4cc1f0fcec8f4bd2bd2430e957a01aadf746", size = 146967, upload-time = "2026-01-29T15:12:33.203Z" }, + { url = "https://files.pythonhosted.org/packages/0b/04/975b86a4bcf6cfeda47aad15956d52fbeda280811206e9967380fa9355c8/orjson-3.11.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:351b96b614e3c37a27b8ab048239ebc1e0be76cc17481a430d70a77fb95d3844", size = 421003, upload-time = "2026-01-29T15:12:35.097Z" }, + { url = "https://files.pythonhosted.org/packages/28/d1/0369d0baf40eea5ff2300cebfe209883b2473ab4aa4c4974c8bd5ee42bb2/orjson-3.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f9959c85576beae5cdcaaf39510b15105f1ee8b70d5dacd90152617f57be8c83", size = 155695, upload-time = "2026-01-29T15:12:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1f/d10c6d6ae26ff1d7c3eea6fd048280ef2e796d4fb260c5424fd021f68ecf/orjson-3.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75682d62b1b16b61a30716d7a2ec1f4c36195de4a1c61f6665aedd947b93a5d5", size = 147392, upload-time = "2026-01-29T15:12:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/8d/43/7479921c174441a0aa5277c313732e20713c0969ac303be9f03d88d3db5d/orjson-3.11.6-cp313-cp313-win32.whl", hash = "sha256:40dc277999c2ef227dcc13072be879b4cfd325502daeb5c35ed768f706f2bf30", size = 139718, upload-time = "2026-01-29T15:12:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/88/bc/9ffe7dfbf8454bc4e75bb8bf3a405ed9e0598df1d3535bb4adcd46be07d0/orjson-3.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:f0f6e9f8ff7905660bc3c8a54cd4a675aa98f7f175cf00a59815e2ff42c0d916", size = 136635, upload-time = "2026-01-29T15:12:40.593Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/51fa90b451470447ea5023b20d83331ec741ae28d1e6d8ed547c24e7de14/orjson-3.11.6-cp313-cp313-win_arm64.whl", hash = "sha256:1608999478664de848e5900ce41f25c4ecdfc4beacbc632b6fd55e1a586e5d38", size = 135175, upload-time = "2026-01-29T15:12:41.997Z" }, + { url = "https://files.pythonhosted.org/packages/31/9f/46ca908abaeeec7560638ff20276ab327b980d73b3cc2f5b205b4a1c60b3/orjson-3.11.6-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6026db2692041d2a23fe2545606df591687787825ad5821971ef0974f2c47630", size = 249823, upload-time = "2026-01-29T15:12:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/ff/78/ca478089818d18c9cd04f79c43f74ddd031b63c70fa2a946eb5e85414623/orjson-3.11.6-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:132b0ab2e20c73afa85cf142e547511feb3d2f5b7943468984658f3952b467d4", size = 134328, upload-time = "2026-01-29T15:12:45.171Z" }, + { url = "https://files.pythonhosted.org/packages/39/5e/cbb9d830ed4e47f4375ad8eef8e4fff1bf1328437732c3809054fc4e80be/orjson-3.11.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b376fb05f20a96ec117d47987dd3b39265c635725bda40661b4c5b73b77b5fde", size = 137651, upload-time = "2026-01-29T15:12:46.602Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3a/35df6558c5bc3a65ce0961aefee7f8364e59af78749fc796ea255bfa0cf5/orjson-3.11.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:954dae4e080574672a1dfcf2a840eddef0f27bd89b0e94903dd0824e9c1db060", size = 134596, upload-time = "2026-01-29T15:12:47.95Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8e/3d32dd7b7f26a19cc4512d6ed0ae3429567c71feef720fe699ff43c5bc9e/orjson-3.11.6-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe515bb89d59e1e4b48637a964f480b35c0a2676de24e65e55310f6016cca7ce", size = 140923, upload-time = "2026-01-29T15:12:49.333Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/1efbf5c99b3304f25d6f0d493a8d1492ee98693637c10ce65d57be839d7b/orjson-3.11.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:380f9709c275917af28feb086813923251e11ee10687257cd7f1ea188bcd4485", size = 144068, upload-time = "2026-01-29T15:12:50.927Z" }, + { url = "https://files.pythonhosted.org/packages/82/83/0d19eeb5be797de217303bbb55dde58dba26f996ed905d301d98fd2d4637/orjson-3.11.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8173e0d3f6081e7034c51cf984036d02f6bab2a2126de5a759d79f8e5a140e7", size = 142493, upload-time = "2026-01-29T15:12:52.432Z" }, + { url = "https://files.pythonhosted.org/packages/32/a7/573fec3df4dc8fc259b7770dc6c0656f91adce6e19330c78d23f87945d1e/orjson-3.11.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dddf9ba706294906c56ef5150a958317b09aa3a8a48df1c52ccf22ec1907eac", size = 145616, upload-time = "2026-01-29T15:12:53.903Z" }, + { url = "https://files.pythonhosted.org/packages/c2/0e/23551b16f21690f7fd5122e3cf40fdca5d77052a434d0071990f97f5fe2f/orjson-3.11.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cbae5c34588dc79938dffb0b6fbe8c531f4dc8a6ad7f39759a9eb5d2da405ef2", size = 146951, upload-time = "2026-01-29T15:12:55.698Z" }, + { url = "https://files.pythonhosted.org/packages/b8/63/5e6c8f39805c39123a18e412434ea364349ee0012548d08aa586e2bd6aa9/orjson-3.11.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f75c318640acbddc419733b57f8a07515e587a939d8f54363654041fd1f4e465", size = 421024, upload-time = "2026-01-29T15:12:57.434Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4d/724975cf0087f6550bd01fd62203418afc0ea33fd099aed318c5bcc52df8/orjson-3.11.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e0ab8d13aa2a3e98b4a43487c9205b2c92c38c054b4237777484d503357c8437", size = 155774, upload-time = "2026-01-29T15:12:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a3/f4c4e3f46b55db29e0a5f20493b924fc791092d9a03ff2068c9fe6c1002f/orjson-3.11.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f884c7fb1020d44612bd7ac0db0babba0e2f78b68d9a650c7959bf99c783773f", size = 147393, upload-time = "2026-01-29T15:13:00.769Z" }, + { url = "https://files.pythonhosted.org/packages/ee/86/6f5529dd27230966171ee126cecb237ed08e9f05f6102bfaf63e5b32277d/orjson-3.11.6-cp314-cp314-win32.whl", hash = "sha256:8d1035d1b25732ec9f971e833a3e299d2b1a330236f75e6fd945ad982c76aaf3", size = 139760, upload-time = "2026-01-29T15:13:02.173Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b5/91ae7037b2894a6b5002fb33f4fbccec98424a928469835c3837fbb22a9b/orjson-3.11.6-cp314-cp314-win_amd64.whl", hash = "sha256:931607a8865d21682bb72de54231655c86df1870502d2962dbfd12c82890d077", size = 136633, upload-time = "2026-01-29T15:13:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/74/f473a3ec7a0a7ebc825ca8e3c86763f7d039f379860c81ba12dcdd456547/orjson-3.11.6-cp314-cp314-win_arm64.whl", hash = "sha256:fe71f6b283f4f1832204ab8235ce07adad145052614f77c876fcf0dac97bc06f", size = 135168, upload-time = "2026-01-29T15:13:05.932Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, + { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, + { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, + { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, +] + [[package]] name = "packaging" -version = "26.0" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" }, + { url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" }, + { url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" }, + { url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" }, + { url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" }, + { url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" }, + { url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" }, + { url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" }, + { url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" }, ] [[package]] @@ -1377,7 +1739,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -1718,6 +2080,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-docx" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.1" @@ -1736,6 +2111,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] +[[package]] +name = "python-pptx" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "pillow" }, + { name = "typing-extensions" }, + { name = "xlsxwriter" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload-time = "2024-08-07T17:33:37.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -1931,6 +2321,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, ] +[[package]] +name = "reportlab" +version = "4.4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/39/42cf24aee570a80e1903221ae3a92a2e34c324794a392eb036cbb6dc3839/reportlab-4.4.9.tar.gz", hash = "sha256:7cf487764294ee791a4781f5a157bebce262a666ae4bbb87786760a9676c9378", size = 3911246, upload-time = "2026-01-15T10:07:56.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/77/546e50edfaba6a0e58e8ec5fdc4446510227cec9e8f40172b60941d5a633/reportlab-4.4.9-py3-none-any.whl", hash = "sha256:68e2d103ae8041a37714e8896ec9b79a1c1e911d68c3bd2ea17546568cf17bfd", size = 1954401, upload-time = "2026-01-15T09:27:59.133Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -1946,6 +2349,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + [[package]] name = "rich" version = "14.2.0" @@ -2509,6 +2924,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + [[package]] name = "urllib3" version = "2.6.3" @@ -2518,6 +2942,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] +[[package]] +name = "uuid-utils" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/7c/3a926e847516e67bc6838634f2e54e24381105b4e80f9338dc35cca0086b/uuid_utils-0.14.0.tar.gz", hash = "sha256:fc5bac21e9933ea6c590433c11aa54aaca599f690c08069e364eb13a12f670b4", size = 22072, upload-time = "2026-01-20T20:37:15.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/42/42d003f4a99ddc901eef2fd41acb3694163835e037fb6dde79ad68a72342/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f6695c0bed8b18a904321e115afe73b34444bc8451d0ce3244a1ec3b84deb0e5", size = 601786, upload-time = "2026-01-20T20:37:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/96/e6/775dfb91f74b18f7207e3201eb31ee666d286579990dc69dd50db2d92813/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4f0a730bbf2d8bb2c11b93e1005e91769f2f533fa1125ed1f00fd15b6fcc732b", size = 303943, upload-time = "2026-01-20T20:37:18.767Z" }, + { url = "https://files.pythonhosted.org/packages/17/82/ea5f5e85560b08a1f30cdc65f75e76494dc7aba9773f679e7eaa27370229/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ce3fd1a4fdedae618fc3edc8faf91897012469169d600133470f49fd699ed3", size = 340467, upload-time = "2026-01-20T20:37:11.794Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/54b06415767f4569882e99b6470c6c8eeb97422686a6d432464f9967fd91/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ae4a98416a440e78f7d9543d11b11cae4bab538b7ed94ec5da5221481748f2", size = 346333, upload-time = "2026-01-20T20:37:12.818Z" }, + { url = "https://files.pythonhosted.org/packages/cb/10/a6bce636b8f95e65dc84bf4a58ce8205b8e0a2a300a38cdbc83a3f763d27/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:971e8c26b90d8ae727e7f2ac3ee23e265971d448b3672882f2eb44828b2b8c3e", size = 470859, upload-time = "2026-01-20T20:37:01.512Z" }, + { url = "https://files.pythonhosted.org/packages/8a/27/84121c51ea72f013f0e03d0886bcdfa96b31c9b83c98300a7bd5cc4fa191/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5cde1fa82804a8f9d2907b7aec2009d440062c63f04abbdb825fce717a5e860", size = 341988, upload-time = "2026-01-20T20:37:22.881Z" }, + { url = "https://files.pythonhosted.org/packages/90/a4/01c1c7af5e6a44f20b40183e8dac37d6ed83e7dc9e8df85370a15959b804/uuid_utils-0.14.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7343862a2359e0bd48a7f3dfb5105877a1728677818bb694d9f40703264a2db", size = 365784, upload-time = "2026-01-20T20:37:10.808Z" }, + { url = "https://files.pythonhosted.org/packages/04/f0/65ee43ec617b8b6b1bf2a5aecd56a069a08cca3d9340c1de86024331bde3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c51e4818fdb08ccec12dc7083a01f49507b4608770a0ab22368001685d59381b", size = 523750, upload-time = "2026-01-20T20:37:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/95/d3/6bf503e3f135a5dfe705a65e6f89f19bccd55ac3fb16cb5d3ec5ba5388b8/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:181bbcccb6f93d80a8504b5bd47b311a1c31395139596edbc47b154b0685b533", size = 615818, upload-time = "2026-01-20T20:37:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/df/6c/99937dd78d07f73bba831c8dc9469dfe4696539eba2fc269ae1b92752f9e/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c8ae96101c3524ba8dbf762b6f05e9e9d896544786c503a727c5bf5cb9af1a7", size = 580831, upload-time = "2026-01-20T20:37:19.691Z" }, + { url = "https://files.pythonhosted.org/packages/44/fa/bbc9e2c25abd09a293b9b097a0d8fc16acd6a92854f0ec080f1ea7ad8bb3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00ac3c6edfdaff7e1eed041f4800ae09a3361287be780d7610a90fdcde9befdc", size = 546333, upload-time = "2026-01-20T20:37:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9b/e5e99b324b1b5f0c62882230455786df0bc66f67eff3b452447e703f45d2/uuid_utils-0.14.0-cp39-abi3-win32.whl", hash = "sha256:ec2fd80adf8e0e6589d40699e6f6df94c93edcc16dd999be0438dd007c77b151", size = 177319, upload-time = "2026-01-20T20:37:04.208Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/2c7d417ea483b6ff7820c948678fdf2ac98899dc7e43bb15852faa95acaf/uuid_utils-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:efe881eb43a5504fad922644cb93d725fd8a6a6d949bd5a4b4b7d1a1587c7fd1", size = 182566, upload-time = "2026-01-20T20:37:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/49e4bdda28e962fbd7266684171ee29b3d92019116971d58783e51770745/uuid_utils-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:32b372b8fd4ebd44d3a219e093fe981af4afdeda2994ee7db208ab065cfcd080", size = 182809, upload-time = "2026-01-20T20:37:05.139Z" }, +] + [[package]] name = "uvicorn" version = "0.40.0" @@ -2669,3 +3115,120 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] + +[[package]] +name = "xlsxwriter" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940, upload-time = "2025-09-16T00:16:21.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]