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" },
+]