Many fixes to Mentor, Query RAG, etc

This commit is contained in:
Renn F
2026-01-03 23:10:31 +01:00
parent 1d173a5203
commit c68644a1e2
36 changed files with 1329 additions and 455 deletions
+5
View File
@@ -2,6 +2,11 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## IMPORTANT NOTES
**IGNORING != FIXING**
**`# noqa` & `# type: ignore` != FIXING**
## Project Overview
**RoboCo** is an AI Agentic Company - a virtual organization of 18 AI agents + 1 human CEO, designed to operate as a complete software development workforce. The system implements a structured organizational hierarchy with formal communication protocols, task management, and quality controls.
+25 -1
View File
@@ -30,7 +30,31 @@ Claim → read full description → plan breakdown → start → journal decisio
Create session for YOUR task with `roboco_session_create_for_tasks()`. Subtasks inherit it automatically.
### 4. SUBTASKS
Create with `roboco_task_create()`. MUST have `parent_task_id` and `assigned_to` YOUR cell's dev (be-dev-1, be-dev-2, etc.).
**CRITICAL: Always set `parent_task_id` to YOUR task ID.** Without this, you create orphan tasks, not subtasks.
```python
# Get YOUR task ID first
my_task = roboco_task_get(task_id)
# Create SUBTASK with parent_task_id
roboco_task_create(
title="Implement user auth endpoint",
parent_task_id=my_task["id"], # REQUIRED - links to your task
assigned_to="be-dev-1", # USE SLUG
...
)
```
**Your cell's agent slugs:**
- Backend: `be-dev-1`, `be-dev-2`, `be-qa`, `be-doc`
- Frontend: `fe-dev-1`, `fe-dev-2`, `fe-qa`, `fe-doc`
- UX/UI: `ux-dev-1`, `ux-dev-2`, `ux-qa`, `ux-doc`
**Without `parent_task_id`:**
- Task becomes a sibling (wrong!)
- Completion tracking breaks
- Your task can't complete
### 5. CREATE BRANCH (Git Tasks)
**For tasks with `requires_git=True`:**
+25 -1
View File
@@ -41,7 +41,31 @@ roboco_git_create_branch(project_slug, task_id, branch_type, "main")
Use `roboco_group_create()` in each relevant cell channel. Cell PMs need groups to create sessions.
### 5. CREATE CELL TASKS
Use `roboco_task_create()` with `parent_task_id`, `team`, and `assigned_to` Cell PM (be-pm, fe-pm, ux-pm).
**CRITICAL: Always set `parent_task_id` to YOUR task ID.** Without this, you create orphan tasks, not subtasks.
```python
# Get YOUR task ID first
my_task = roboco_task_get(task_id)
# Create SUBTASK with parent_task_id
roboco_task_create(
title="Backend: Implement feature X",
parent_task_id=my_task["id"], # REQUIRED - links to your task
team="backend",
assigned_to="be-pm", # USE SLUG
...
)
```
**Agent slugs:**
- `be-pm`, `fe-pm`, `ux-pm` - Cell PMs
**Without `parent_task_id`:**
- Task becomes a sibling (wrong!)
- Completion tracking breaks
- Your task can't complete
- Set `project_id` and `branch_name` for git tasks
- Cell PMs will create subtask branches from your parent branch
+81
View File
@@ -0,0 +1,81 @@
# Agent UUIDs Reference
**ALWAYS use SLUGS when assigning tasks.** The system resolves slugs to UUIDs automatically.
```python
# CORRECT - Use slug
roboco_task_create(assigned_to="be-dev-1", ...)
# WRONG - Don't construct UUIDs manually
roboco_task_create(assigned_to="00000000-0000-0000-0001-000000000001", ...)
```
## UUID Scheme (Reference Only)
```
00000000-0000-0000-{CELL}-00000000000{N}
```
| Cell Code | Team |
|-----------|------|
| `0000` | CEO |
| `0001` | Backend |
| `0002` | Frontend |
| `0003` | UX/UI |
| `0004` | Board/Management |
**NO OTHER CELL CODES EXIST.** Do not construct UUIDs with codes like `0005`, `0006`, etc.
## Backend Cell (0001)
| Slug | UUID |
|------|------|
| `be-dev-1` | `00000000-0000-0000-0001-000000000001` |
| `be-dev-2` | `00000000-0000-0000-0001-000000000002` |
| `be-qa` | `00000000-0000-0000-0001-000000000003` |
| `be-pm` | `00000000-0000-0000-0001-000000000004` |
| `be-doc` | `00000000-0000-0000-0001-000000000005` |
## Frontend Cell (0002)
| Slug | UUID |
|------|------|
| `fe-dev-1` | `00000000-0000-0000-0002-000000000001` |
| `fe-dev-2` | `00000000-0000-0000-0002-000000000002` |
| `fe-qa` | `00000000-0000-0000-0002-000000000003` |
| `fe-pm` | `00000000-0000-0000-0002-000000000004` |
| `fe-doc` | `00000000-0000-0000-0002-000000000005` |
## UX/UI Cell (0003)
| Slug | UUID |
|------|------|
| `ux-dev-1` | `00000000-0000-0000-0003-000000000001` |
| `ux-dev-2` | `00000000-0000-0000-0003-000000000002` |
| `ux-qa` | `00000000-0000-0000-0003-000000000003` |
| `ux-pm` | `00000000-0000-0000-0003-000000000004` |
| `ux-doc` | `00000000-0000-0000-0003-000000000005` |
## Board/Management (0004)
| Slug | UUID |
|------|------|
| `main-pm` | `00000000-0000-0000-0004-000000000001` |
| `product-owner` | `00000000-0000-0000-0004-000000000002` |
| `head-marketing` | `00000000-0000-0000-0004-000000000003` |
| `auditor` | `00000000-0000-0000-0004-000000000004` |
## CEO (0000)
| Slug | UUID |
|------|------|
| `ceo` | `00000000-0000-0000-0000-000000000001` |
## Usage
Most tools accept either slug or UUID:
```python
roboco_task_claim(task_id) # task_id is UUID
roboco_journal_read_team("be-dev-1") # slug works
roboco_journal_read_team("00000000-0000-0000-0001-000000000001") # UUID works
```
@@ -0,0 +1,71 @@
# Channel Reference
All available channels with their slugs and access rules.
## Cell Channels
| Slug | Name | Members |
|------|------|---------|
| `backend-cell` | Backend Cell | be-dev-1, be-dev-2, be-qa, be-pm, be-doc |
| `frontend-cell` | Frontend Cell | fe-dev-1, fe-dev-2, fe-qa, fe-pm, fe-doc |
| `uxui-cell` | UX/UI Cell | ux-dev-1, ux-dev-2, ux-qa, ux-pm, ux-doc |
## Cross-Cell Channels
| Slug | Name | Members |
|------|------|---------|
| `dev-all` | All Developers | All 6 developers |
| `qa-all` | All QA | be-qa, fe-qa, ux-qa |
| `pm-all` | All PMs | be-pm, fe-pm, ux-pm, main-pm |
| `doc-all` | All Documenters | be-doc, fe-doc, ux-doc |
## Management Channels
| Slug | Name | Members |
|------|------|---------|
| `main-pm-board` | Main PM & Board | main-pm, product-owner, head-marketing, auditor |
| `board-private` | Board Private | product-owner, head-marketing, auditor, ceo |
## Special Channels
| Slug | Name | Read | Write |
|------|------|------|-------|
| `announcements` | Announcements | Everyone | PM/Board only |
| `all-hands` | All Hands | Everyone | Everyone |
## Auditor Silent Access
Auditor has silent read access to:
- `backend-cell`
- `frontend-cell`
- `uxui-cell`
- `dev-all`
- `qa-all`
- `pm-all`
- `doc-all`
Auditor does NOT appear in member lists but CAN read.
## Privileged Access
These roles bypass normal membership checks:
- **CEO**: Full access everywhere
- **Auditor**: Silent read everywhere
- **Main PM**: Read access to all cell channels
## Using Channels
```python
# Send message to your cell
roboco_message_send({
channel: "backend-cell",
content: "Starting work on task",
task_id: task_id
})
# Read channel history
roboco_channel_history("backend-cell", limit=50)
# List available channels
roboco_channel_list()
```
+85
View File
@@ -0,0 +1,85 @@
# Configuration Reference
Environment variables for RoboCo (prefix: `ROBOCO_`).
## API Server
| Variable | Default | Description |
|----------|---------|-------------|
| `ROBOCO_HOST` | `127.0.0.1` | API host (0.0.0.0 for containers) |
| `ROBOCO_PORT` | `8000` | API port |
| `ROBOCO_DEBUG` | `false` | Debug mode |
| `ROBOCO_ENVIRONMENT` | `development` | development/staging/production |
## Database
| Variable | Default | Description |
|----------|---------|-------------|
| `ROBOCO_DATABASE_HOST` | `localhost` | PostgreSQL host |
| `ROBOCO_DATABASE_PORT` | `5432` | PostgreSQL port |
| `ROBOCO_DATABASE_USER` | `roboco` | Database user |
| `ROBOCO_DATABASE_PASSWORD` | `roboco` | Database password |
| `ROBOCO_DATABASE_NAME` | `roboco` | Database name |
## Redis
| Variable | Default | Description |
|----------|---------|-------------|
| `ROBOCO_REDIS_HOST` | `localhost` | Redis host |
| `ROBOCO_REDIS_PORT` | `6379` | Redis port |
| `ROBOCO_REDIS_DB` | `0` | Redis database |
## Workspaces
| Variable | Default | Description |
|----------|---------|-------------|
| `ROBOCO_WORKSPACES_ROOT` | `/data/workspaces` | Root for agent workspaces |
| `ROBOCO_WORKSPACE_AUTO_CLONE` | `true` | Auto-clone on first access |
| `ROBOCO_WORKSPACE_CLONE_TIMEOUT` | `300` | Clone timeout (seconds) |
## RAG/Embeddings
| Variable | Default | Description |
|----------|---------|-------------|
| `ROBOCO_DEFAULT_EMBEDDING_MODEL` | `embeddinggemma:300m` | Embedding model |
| `ROBOCO_EMBEDDING_DIMENSIONS` | `768` | Embedding dimensions |
| `ROBOCO_RAG_CHUNK_STRATEGY` | `fixed` | fixed/semantic/hierarchical/contextual |
| `ROBOCO_RAG_CHUNK_SIZE` | `512` | Base chunk size |
| `ROBOCO_RAG_CHUNK_SIZE_DOCS` | `1536` | Chunk size for docs |
| `ROBOCO_RAG_CHUNK_SIZE_JOURNALS` | `1024` | Chunk size for journals |
| `ROBOCO_RAG_CHUNK_OVERLAP` | `128` | Chunk overlap |
| `ROBOCO_RAG_USE_HYDE` | `true` | Use HyDE for queries |
| `ROBOCO_RAG_USE_HYBRID_SEARCH` | `true` | BM25 + vector search |
| `ROBOCO_RAG_USE_CROSS_ENCODER` | `true` | Neural reranking |
| `ROBOCO_RAG_AUTO_UPDATE_ENABLED` | `true` | Auto-update indexes |
| `ROBOCO_RAG_AUTO_UPDATE_INTERVAL` | `300` | Update interval (seconds) |
## LLM
| Variable | Default | Description |
|----------|---------|-------------|
| `ROBOCO_LOCAL_LLM_MODEL` | `glm-4.6:cloud` | Local LLM for RAG |
| `ROBOCO_LOCAL_LLM_BASE_URL` | `http://roboco-ollama:11434/v1` | OpenAI-compat API |
| `ROBOCO_OLLAMA_BASE_URL` | `http://roboco-ollama:11434` | Native Ollama API |
## Security
| Variable | Default | Description |
|----------|---------|-------------|
| `ROBOCO_SECRET_KEY` | (required) | JWT signing key (32+ chars) |
| `ROBOCO_ACCESS_TOKEN_EXPIRE_MINUTES` | `1440` | Token expiry (24 hours) |
## Logging
| Variable | Default | Description |
|----------|---------|-------------|
| `ROBOCO_LOG_LEVEL` | `INFO` | DEBUG/INFO/WARNING/ERROR/CRITICAL |
| `ROBOCO_LOG_FORMAT` | `json` | json or console |
## Sessions
| Variable | Default | Description |
|----------|---------|-------------|
| `ROBOCO_SESSION_DEFAULT_TIMEOUT_SECONDS` | `300` | Session timeout |
| `ROBOCO_SESSION_MAX_MESSAGE_COUNT` | `100` | Max messages per session |
| `ROBOCO_MESSAGE_MAX_LENGTH` | `10000` | Max message length |
+73
View File
@@ -0,0 +1,73 @@
# Escalation Chain Reference
Complete escalation mapping for all agents.
## Chain Overview
```
Cell Members → Cell PM → Main PM → Product Owner → CEO
```
## Full Mapping
| Agent | Escalates To |
|-------|--------------|
| `be-dev-1` | `be-pm` |
| `be-dev-2` | `be-pm` |
| `be-qa` | `be-pm` |
| `be-doc` | `be-pm` |
| `fe-dev-1` | `fe-pm` |
| `fe-dev-2` | `fe-pm` |
| `fe-qa` | `fe-pm` |
| `fe-doc` | `fe-pm` |
| `ux-dev-1` | `ux-pm` |
| `ux-dev-2` | `ux-pm` |
| `ux-qa` | `ux-pm` |
| `ux-doc` | `ux-pm` |
| `be-pm` | `main-pm` |
| `fe-pm` | `main-pm` |
| `ux-pm` | `main-pm` |
| `main-pm` | `product-owner` |
| `product-owner` | `ceo` |
| `head-marketing` | `ceo` |
| `auditor` | `ceo` |
## Cell PM for Team
| Team | Cell PM |
|------|---------|
| `backend` | `be-pm` |
| `frontend` | `fe-pm` |
| `ux_ui` | `ux-pm` |
## Escalation Tool
```python
roboco_task_escalate(
task_id="uuid-here",
reason="Need clarification on requirements"
)
```
Auto-routes to your escalation target. You CANNOT choose a different target.
## CEO Escalation (PM Only)
```python
roboco_task_escalate_to_ceo(
task_id="uuid-here",
notes="Major feature ready for approval"
)
```
Requirements:
- Task in `awaiting_pm_review`
- PR exists (for git tasks)
- Only PMs can call this
## Cannot Skip Levels
System enforces chain:
- Developer CANNOT escalate directly to Main PM
- Cell PM CANNOT escalate directly to CEO
- Each level must acknowledge and decide
+88
View File
@@ -0,0 +1,88 @@
# Permissions Reference
What each role can do in the system.
## Permission Levels
| Level | Roles |
|-------|-------|
| CEO | ceo, system |
| BOARD | product_owner, head_marketing |
| AUDITOR | auditor |
| MAIN_PM | main_pm |
| CELL_PM | cell_pm |
| CELL_MEMBER | developer, qa, documenter |
## Task Permissions
| Action | CEO | Board | Auditor | Main PM | Cell PM | Dev | QA | Doc |
|--------|-----|-------|---------|---------|---------|-----|----|----|
| View All | Yes | Yes | Yes | Yes | - | - | - | - |
| View Own | - | - | - | - | Yes | Yes | Yes | Yes |
| Create | Yes | Yes | Yes | Yes | Yes | - | - | - |
| Assign | Yes | Yes | Yes | Yes | Yes | - | - | - |
| Cancel | - | Yes | - | Yes | Yes | - | - | - |
| Close | Yes | Yes | Yes | Yes | Yes | Yes | - | Yes |
| Claim | - | - | - | Yes | Yes | Yes | Yes | Yes |
| Pass QA | - | - | - | - | - | - | Yes | - |
| Fail QA | - | - | - | - | - | - | Yes | - |
| Docs Complete | - | - | - | - | - | - | - | Yes |
Note: CEO and Auditor CANNOT cancel (by design - observe/approve only).
## Index Permissions
| Action | CEO | Board | Auditor | Main PM | Cell PM | Dev | QA | Doc |
|--------|-----|-------|---------|---------|---------|-----|----|----|
| Index Code | Yes | - | - | Yes | Yes | Yes | - | - |
| Index Docs | Yes | Yes | - | Yes | Yes | Yes | - | Yes |
| Search/Query | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| View Stats | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Clear Index | Yes | - | - | Yes | - | - | - | - |
| Refresh Index | Yes | - | - | Yes | - | - | - | - |
Note: Board (Product Owner, Head Marketing) can only index docs, not code.
## Notification Permissions
| Role | Can Send | Scope |
|------|----------|-------|
| ceo | Yes | All |
| product_owner | Yes | Management chain |
| head_marketing | Yes | Management chain |
| auditor | Yes | All |
| main_pm | Yes | All |
| cell_pm | Yes | Own cell |
| developer | No | - |
| qa | No | - |
| documenter | No | - |
## PM-Capable Roles
These roles can create/assign tasks:
- `ceo`
- `product_owner`
- `head_marketing`
- `main_pm`
- `cell_pm`
## Cancellation Roles
These roles can cancel tasks:
- `product_owner`
- `head_marketing`
- `main_pm`
- `cell_pm`
Note: CEO and Auditor CANNOT cancel (observe/approve only).
## View Scope
| Role | Can View |
|------|----------|
| CEO | All tasks |
| Board | All tasks |
| Auditor | All tasks (silent) |
| Main PM | All tasks |
| Cell PM | Own cell + cross-cell |
| Cell Member | Own cell |
+104
View File
@@ -0,0 +1,104 @@
# API Endpoints Reference
Base URL: `http://{host}:{port}/api/v1`
## Tasks
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/tasks` | List tasks (filtered) |
| GET | `/tasks/my` | My assigned tasks |
| GET | `/tasks/pending` | Pending tasks |
| GET | `/tasks/blocked` | Blocked tasks |
| GET | `/tasks/awaiting-qa` | Tasks awaiting QA |
| GET | `/tasks/awaiting-docs` | Tasks awaiting docs |
| GET | `/tasks/awaiting-pm-review` | Tasks awaiting PM |
| GET | `/tasks/awaiting-ceo-approval` | Tasks for CEO |
| GET | `/tasks/team/{team}` | Tasks by team |
| GET | `/tasks/{id}` | Get task details |
| GET | `/tasks/{id}/subtasks` | Get subtasks |
| POST | `/tasks` | Create task |
| PATCH | `/tasks/{id}` | Update task |
| DELETE | `/tasks/{id}` | Delete task |
| POST | `/tasks/{id}/claim` | Claim task |
| POST | `/tasks/{id}/start` | Start work |
| POST | `/tasks/{id}/pause` | Pause work |
| POST | `/tasks/{id}/resume` | Resume work |
| POST | `/tasks/{id}/block` | Block on task |
| POST | `/tasks/{id}/soft-block` | Block on external |
| POST | `/tasks/{id}/unblock` | Unblock task |
| POST | `/tasks/{id}/verify` | Submit verification |
| POST | `/tasks/{id}/submit-qa` | Submit for QA |
| POST | `/tasks/{id}/pass-qa` | Pass QA |
| POST | `/tasks/{id}/fail-qa` | Fail QA |
| POST | `/tasks/{id}/docs-complete` | Complete docs |
| POST | `/tasks/{id}/submit-pm-review` | Submit to PM |
| POST | `/tasks/{id}/complete` | Complete task |
| POST | `/tasks/{id}/cancel` | Cancel task |
| POST | `/tasks/{id}/activate` | Activate task |
| POST | `/tasks/{id}/escalate` | Escalate task |
| POST | `/tasks/{id}/escalate-to-ceo` | Escalate to CEO |
| POST | `/tasks/{id}/ceo-approve` | CEO approve |
| POST | `/tasks/{id}/ceo-reject` | CEO reject |
| POST | `/tasks/{id}/substitute` | Substitute agent |
| POST | `/tasks/{id}/progress` | Update progress |
## Git
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/git/status` | Git status |
| GET | `/git/log` | Commit history |
| GET | `/git/branches` | List branches |
| GET | `/git/diff` | View diff |
| POST | `/git/commit` | Create commit |
| POST | `/git/push` | Push to remote |
| POST | `/git/branch/create` | Create branch |
| POST | `/git/checkout` | Checkout branch |
| POST | `/git/pr/create` | Create PR |
| POST | `/git/pr/merge` | Merge PR |
## Channels & Messages
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/channels` | List channels |
| GET | `/channels/{slug}/history` | Channel history |
| POST | `/messages` | Send message |
| GET | `/messages/{id}` | Get message |
## Notifications
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/notifications` | List notifications |
| GET | `/notifications/{id}` | Get notification |
| POST | `/notifications` | Send notification |
| POST | `/notifications/{id}/ack` | Acknowledge |
## Journals
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/journals/me/entries` | My entries |
| POST | `/journals/me/entries` | Create entry |
| GET | `/journals/me/stats` | My stats |
| GET | `/journals/{agent}/entries` | Read team journal |
## Knowledge Base
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/optimal/search` | Semantic search |
| POST | `/optimal/query` | RAG query |
| POST | `/optimal/mentor/ask` | Ask mentor |
| GET | `/optimal/stats` | KB stats |
| POST | `/optimal/index/code` | Index code |
| POST | `/optimal/index/docs` | Index docs |
## Health
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/health` | Health check |
| GET | `/health/ready` | Readiness check |
+16 -2
View File
@@ -67,10 +67,17 @@ roboco_task_complete(task_id)
## PM Operations
```python
# Create task
# Create SUBTASK (most common)
roboco_task_create({
title: "Implement auth endpoint",
parent_task_id: my_task_id, # REQUIRED for subtasks
team: "backend",
assigned_to: "be-dev-1" # Use SLUG
})
# Create standalone task (rare)
roboco_task_create({
title: "...",
description: "...",
team: "backend",
status: "backlog"
})
@@ -83,8 +90,15 @@ roboco_task_cancel(task_id, reason)
# Plan
roboco_task_plan(task_id, approach, steps)
# Escalate to CEO (parent tasks only)
roboco_task_escalate_to_ceo(task_id, notes)
```
**CRITICAL**: When creating subtasks, ALWAYS include `parent_task_id`. Without it, you create orphan sibling tasks instead of linked subtasks.
**Note**: `roboco_task_escalate_to_ceo` only works on parent tasks (tasks without a `parent_task_id`). Subtasks must have their parent task escalated instead.
## Progress Updates
```python
+43 -3
View File
@@ -49,11 +49,30 @@
## Self-Review Prevented
**Error**: "Cannot review own work"
**Error**: "Cannot review own work" or "SELF_REVIEW_NOT_ALLOWED"
**Cause**: QA/Documenter trying to claim task they developed
**Cause**: QA trying to claim, pass, or fail a task they originally developed
**Solution**: Another QA/Documenter must handle this task
**Solution**: Another QA must handle this task. Self-review prevention applies to:
- Claiming the task
- Passing QA (`roboco_task_qa_pass`)
- Failing QA (`roboco_task_qa_fail`)
## Cannot Escalate Subtask to CEO
**Error**: "Cannot escalate subtask to CEO - only parent tasks can be escalated"
**Cause**: Attempting to escalate a task that has a `parent_task_id`
**Solution**: Escalate the parent task instead:
```python
# Get the parent task ID
task = roboco_task_get(subtask_id)
parent_id = task.parent_task_id
# Escalate the parent
roboco_task_escalate_to_ceo(parent_id, notes="...")
```
## Git Task: No Branch
@@ -65,3 +84,24 @@
```python
roboco_git_create_branch(project_slug, task_id, "feature")
```
## Task Has Incomplete Subtasks
**Error**: "Cannot complete task - subtasks not finished" with list of task IDs
**Cause**: Trying to complete a parent task while subtasks are still in progress
**Solution**: The error message includes which subtask IDs are blocking. Either:
1. Complete the blocking subtasks first
2. Cancel them if no longer needed: `roboco_task_cancel(subtask_id, reason)`
## Invalid Task Status for Operation
**Error**: "Task is in [status], expected [expected_status]"
**Cause**: Attempting an operation that's not valid for the current task state
**Solution**: Check the task's current status and follow the correct workflow:
- QA operations require `awaiting_qa` status
- Documentation operations require `awaiting_documentation` status
- PM completion requires `awaiting_pm_review` status
+3
View File
@@ -73,6 +73,9 @@ Requirements:
- Task must be in `awaiting_pm_review`
- PR must exist (for git tasks)
- Only PMs can do this
- **PARENT TASKS ONLY** - Subtasks cannot be escalated to CEO
If you need to escalate a subtask, escalate the parent task instead. The CEO reviews the complete feature, not individual components.
## Good Escalation Format
+17 -5
View File
@@ -1,12 +1,24 @@
# Knowledge Base Search
**ALL agents have access to KB/RAG tools.** These are automatically available.
## Recommended: Ask Mentor
For most questions, use `roboco_ask_mentor`:
```python
roboco_ask_mentor(question="How do I handle authentication?")
```
It searches ALL knowledge sources and supports follow-up questions.
## Search Types
| Tool | Purpose |
|------|---------|
| `roboco_kb_search` | Semantic search across indexes |
| `roboco_rag_query` | AI-synthesized answer |
| `roboco_ask_mentor` | Conversational help |
| Tool | Purpose | Best For |
|------|---------|----------|
| `roboco_ask_mentor` | Conversational help | **Most questions** |
| `roboco_kb_search` | Semantic search | Browsing, exploration |
| `roboco_rag_query` | AI-synthesized answer | Quick answers |
## Semantic Search
+6 -1
View File
@@ -85,4 +85,9 @@ roboco_journal_reflect({
QA CANNOT review tasks they originally developed.
System tracks `original_developer` in `quick_context`. If QA == original_developer, claim is FORBIDDEN.
System tracks `original_developer` in `quick_context`. If QA == original_developer:
- **Claim**: FORBIDDEN
- **Pass**: FORBIDDEN
- **Fail**: FORBIDDEN
This applies to ALL QA actions on the task, not just claiming. The system enforces this at both the API and MCP tool level.
+9
View File
@@ -62,8 +62,17 @@ backlog → pending (via roboco_task_activate)
| `awaiting_qa → needs_revision` | qa only |
| `awaiting_documentation → awaiting_pm_review` | documenter only |
| `awaiting_pm_review → completed` | cell_pm, main_pm |
| `awaiting_pm_review → awaiting_ceo_approval` | cell_pm, main_pm (parent tasks only) |
| `awaiting_ceo_approval → completed` | ceo only |
| `awaiting_ceo_approval → needs_revision` | ceo only |
| `any → cancelled` | cell_pm, main_pm |
## CEO Approval Notes
- Only **parent tasks** (no `parent_task_id`) can be escalated to CEO
- Subtasks are completed by their Cell PM, not the CEO
- The CEO reviews the complete feature via the parent task
## Checking State
```python
+50 -46
View File
@@ -482,40 +482,16 @@ async def check_staleness(
return await service.check_index_staleness()
@router.get("/health", response_model=RAGHealthResponse)
async def rag_health_check() -> RAGHealthResponse:
"""
Check RAG system health.
Tests connectivity to:
- Embedding model (sentence-transformers)
- LLM (Ollama for HyDE)
- Vector store (PostgreSQL/pgvector)
Each test has a 10-second timeout to prevent hanging.
"""
async def _check_embedding_health(details: dict[str, Any], timeout: float) -> bool:
"""Test embedding model connectivity."""
import asyncio
import httpx
from roboco.config import settings
details: dict[str, Any] = {}
embedding_ok = False
llm_ok = False
vector_ok = False
health_timeout = 10.0 # seconds
# Test embedding model with timeout
from roboco.services.optimal_brain.shared_embedder import (
get_shared_embedder,
)
from roboco.services.optimal_brain.shared_embedder import get_shared_embedder
try:
async with asyncio.timeout(health_timeout):
async with asyncio.timeout(timeout):
embedder = await get_shared_embedder(model=settings.default_embedding_model)
# Use async method if available (OllamaEmbedder), else run sync in thread
if hasattr(embedder, "aembed_query"):
test_embedding = await embedder.aembed_query("health check")
else:
@@ -523,17 +499,24 @@ async def rag_health_check() -> RAGHealthResponse:
embedder.embed_query, "health check"
)
if test_embedding and len(test_embedding) == settings.embedding_dimensions:
embedding_ok = True
details["embedding_model"] = settings.default_embedding_model
details["embedding_dimensions"] = len(test_embedding)
return True
except TimeoutError:
details["embedding_error"] = f"Timeout after {health_timeout}s"
details["embedding_error"] = f"Timeout after {timeout}s"
except Exception as e:
details["embedding_error"] = str(e)
return False
async def _check_llm_health(details: dict[str, Any], timeout: float) -> bool:
"""Test LLM (Ollama) connectivity."""
import httpx
from roboco.config import settings
# Test LLM (Ollama) - already has timeout via httpx
try:
async with httpx.AsyncClient(timeout=health_timeout) as client:
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.post(
f"{settings.local_llm_base_url}/chat/completions",
json={
@@ -543,39 +526,60 @@ async def rag_health_check() -> RAGHealthResponse:
},
)
if resp.is_success:
llm_ok = True
details["llm_model"] = settings.local_llm_model
details["llm_base_url"] = settings.local_llm_base_url
return True
except Exception as e:
details["llm_error"] = str(e)
return False
async def _check_vector_store_health(details: dict[str, Any], timeout: float) -> bool:
"""Test vector store connectivity and index health."""
import asyncio
# Test vector store with timeout
try:
async with asyncio.timeout(health_timeout):
async with asyncio.timeout(timeout):
service = await get_optimal_service()
stats = await service.get_stats()
if stats.get("initialized"):
vector_ok = True
details["vector_store"] = "connected"
if not stats.get("initialized"):
return False
details["vector_store"] = "connected"
# Test actual search capability per index
index_health: dict[str, str] = {}
for index_type, plugin in service._plugins.items():
try:
outcome = await plugin.search("test", top_k=1)
if outcome.success:
index_health[index_type.value] = "ok"
else:
err_msg = outcome.error_message or "unknown"
index_health[index_type.value] = f"error: {err_msg}"
index_health[index_type.value] = (
"ok"
if outcome.success
else f"error: {outcome.error_message or 'unknown'}"
)
except Exception as idx_e:
index_health[index_type.value] = f"error: {idx_e}"
details["index_health"] = index_health
return True
except TimeoutError:
details["vector_store_error"] = f"Timeout after {health_timeout}s"
details["vector_store_error"] = f"Timeout after {timeout}s"
except Exception as e:
details["vector_store_error"] = str(e)
return False
@router.get("/health", response_model=RAGHealthResponse)
async def rag_health_check() -> RAGHealthResponse:
"""
Check RAG system health.
Tests connectivity to embedding model, LLM, and vector store.
Each test has a 10-second timeout.
"""
details: dict[str, Any] = {}
timeout = 10.0
embedding_ok = await _check_embedding_health(details, timeout)
llm_ok = await _check_llm_health(details, timeout)
vector_ok = await _check_vector_store_health(details, timeout)
return RAGHealthResponse(
healthy=embedding_ok and llm_ok and vector_ok,
+36 -2
View File
@@ -1212,6 +1212,9 @@ async def complete_task(
detail="force_with_cancelled requires justification",
)
# Store original task info for error reporting
original_status = task.status.value if task.status else "unknown"
task = await service.complete(
task_id,
agent_id=agent.agent_id,
@@ -1219,10 +1222,41 @@ async def complete_task(
justification=justification,
)
if not task:
# Provide specific error based on what blocked completion
refetch = await service.get(task_id)
if refetch:
# Check for incomplete descendants
descendants = await service.get_all_descendants(task_id)
incomplete = [
str(d.id)[:8]
for d in descendants
if d.status.value not in ("completed", "cancelled")
]
max_shown = 5
if incomplete:
shown = ", ".join(incomplete[:max_shown])
extra = (
f" (+{len(incomplete) - max_shown} more)"
if len(incomplete) > max_shown
else ""
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Cannot complete task - {len(incomplete)} subtask(s) "
f"still in progress: {shown}{extra}. "
"Monitor and help unblock stuck tasks.",
)
# Check for status issue
if original_status not in ("awaiting_pm_review", "in_progress"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Cannot complete - status is '{original_status}'. "
"Must be 'awaiting_pm_review' or 'in_progress'.",
)
# Fallback generic error
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot complete task - all subtasks must be in terminal states "
"(completed or cancelled). Monitor and help unblock stuck tasks.",
detail="Cannot complete task - check task status and subtasks.",
)
await db.commit()
return task_to_response(task)
+3 -8
View File
@@ -5,18 +5,17 @@ Request/response models for task endpoints.
"""
from datetime import datetime
from typing import TYPE_CHECKING, Any
from typing import Any
from uuid import UUID
from pydantic import BaseModel, Field
from sqlalchemy import select
from roboco.db.tables import ProjectTable, TaskTable, WorkSessionTable
from roboco.models.base import Complexity, TaskNature, TaskStatus, TaskType, Team
from roboco.models.session import SessionScope
from roboco.utils.converters import require_uuid, to_python_uuid, to_python_uuid_list
if TYPE_CHECKING:
from roboco.db.tables import TaskTable
# =============================================================================
# NESTED RESPONSE MODELS
# =============================================================================
@@ -601,10 +600,6 @@ async def enrich_task_with_context(
Call this when full traceability context is needed.
"""
from sqlalchemy import select # noqa: PLC0415
from roboco.db.tables import ProjectTable, WorkSessionTable # noqa: PLC0415
task_dict = task_response.model_dump()
# Get project info if task has project_id
+16 -12
View File
@@ -149,9 +149,10 @@ def _register_developer_tools(mcp: FastMCP, client: ApiClient, agent_id: str) ->
Returns:
Commit details with hash and files changed
"""
return await handle_git_commit(
client, project_slug, message, task_id, files, agent_id
)
from roboco.mcp.git.handlers import GitContext
ctx = GitContext(client=client, project_slug=project_slug, agent_id=agent_id)
return await handle_git_commit(ctx, message, task_id, files)
@mcp.tool()
async def roboco_git_push(
@@ -200,9 +201,10 @@ def _register_developer_tools(mcp: FastMCP, client: ApiClient, agent_id: str) ->
Returns:
PR details with URL and number
"""
return await handle_git_create_pr(
client, project_slug, task_id, title, body, agent_id
)
from roboco.mcp.git.handlers import GitContext
ctx = GitContext(client=client, project_slug=project_slug, agent_id=agent_id)
return await handle_git_create_pr(ctx, task_id, title, body)
def _register_pm_branch_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
@@ -234,9 +236,10 @@ def _register_pm_branch_tools(mcp: FastMCP, client: ApiClient, agent_id: str) ->
Returns:
Created branch info with checkout instructions
"""
return await handle_git_create_branch(
client, project_slug, task_id, branch_type, parent_branch, agent_id
)
from roboco.mcp.git.handlers import GitContext
ctx = GitContext(client=client, project_slug=project_slug, agent_id=agent_id)
return await handle_git_create_branch(ctx, task_id, branch_type, parent_branch)
@mcp.tool()
async def roboco_git_checkout(
@@ -279,9 +282,10 @@ def _register_pm_branch_tools(mcp: FastMCP, client: ApiClient, agent_id: str) ->
Returns:
Merge result with final commit hash
"""
return await handle_git_merge_pr(
client, project_slug, pr_number, task_id, merge_method, agent_id
)
from roboco.mcp.git.handlers import GitContext
ctx = GitContext(client=client, project_slug=project_slug, agent_id=agent_id)
return await handle_git_merge_pr(ctx, pr_number, task_id, merge_method)
def create_git_mcp_server(agent_id: str) -> FastMCP:
+31 -28
View File
@@ -7,6 +7,7 @@ Handler functions for git operations. Each handler:
3. Returns formatted response with guidance
"""
from dataclasses import dataclass
from typing import Any
from roboco.mcp.utils import (
@@ -15,6 +16,16 @@ from roboco.mcp.utils import (
format_success_response,
)
@dataclass
class GitContext:
"""Common context for git operations."""
client: ApiClient
project_slug: str
agent_id: str
# =============================================================================
# READ-ONLY HANDLERS
# =============================================================================
@@ -172,25 +183,23 @@ async def handle_git_diff(
# =============================================================================
async def handle_git_commit( # noqa: PLR0913
client: ApiClient,
project_slug: str,
async def handle_git_commit(
ctx: GitContext,
message: str,
task_id: str,
files: list[str] | None,
agent_id: str,
) -> dict[str, Any]:
"""Handle git commit request."""
payload: dict[str, Any] = {
"project_slug": project_slug,
"project_slug": ctx.project_slug,
"message": message,
"task_id": task_id,
"agent_id": agent_id,
"agent_id": ctx.agent_id,
}
if files:
payload["files"] = files
resp = await client.post("/git/commit", json=payload)
resp = await ctx.client.post("/git/commit", json=payload)
if not resp.ok:
return format_error_response(
"GIT_COMMIT_FAILED",
@@ -247,24 +256,22 @@ async def handle_git_push(
)
async def handle_git_create_pr( # noqa: PLR0913
client: ApiClient,
project_slug: str,
async def handle_git_create_pr(
ctx: GitContext,
task_id: str,
title: str,
body: str,
agent_id: str,
) -> dict[str, Any]:
"""Handle PR creation request."""
payload: dict[str, Any] = {
"project_slug": project_slug,
"project_slug": ctx.project_slug,
"task_id": task_id,
"title": title,
"body": body,
"agent_id": agent_id,
"agent_id": ctx.agent_id,
}
resp = await client.post("/git/pr/create", json=payload)
resp = await ctx.client.post("/git/pr/create", json=payload)
if not resp.ok:
return format_error_response(
"PR_CREATE_FAILED",
@@ -290,13 +297,11 @@ async def handle_git_create_pr( # noqa: PLR0913
# =============================================================================
async def handle_git_create_branch( # noqa: PLR0913
client: ApiClient,
project_slug: str,
async def handle_git_create_branch(
ctx: GitContext,
task_id: str,
branch_type: str,
parent_branch: str | None,
agent_id: str,
) -> dict[str, Any]:
"""Handle branch creation request (PM only)."""
valid_types = {"feature", "bug", "chore", "docs", "hotfix"}
@@ -308,15 +313,15 @@ async def handle_git_create_branch( # noqa: PLR0913
)
payload: dict[str, Any] = {
"project_slug": project_slug,
"project_slug": ctx.project_slug,
"task_id": task_id,
"branch_type": branch_type,
"agent_id": agent_id,
"agent_id": ctx.agent_id,
}
if parent_branch:
payload["parent_branch"] = parent_branch
resp = await client.post("/git/branch/create", json=payload)
resp = await ctx.client.post("/git/branch/create", json=payload)
if not resp.ok:
return format_error_response(
"BRANCH_CREATE_FAILED",
@@ -363,13 +368,11 @@ async def handle_git_checkout(
)
async def handle_git_merge_pr( # noqa: PLR0913
client: ApiClient,
project_slug: str,
async def handle_git_merge_pr(
ctx: GitContext,
pr_number: int,
task_id: str,
merge_method: str,
agent_id: str,
) -> dict[str, Any]:
"""Handle PR merge request (PM only)."""
valid_methods = {"merge", "squash", "rebase"}
@@ -381,14 +384,14 @@ async def handle_git_merge_pr( # noqa: PLR0913
)
payload: dict[str, Any] = {
"project_slug": project_slug,
"project_slug": ctx.project_slug,
"pr_number": pr_number,
"task_id": task_id,
"merge_method": merge_method,
"agent_id": agent_id,
"agent_id": ctx.agent_id,
}
resp = await client.post("/git/pr/merge", json=payload)
resp = await ctx.client.post("/git/pr/merge", json=payload)
if not resp.ok:
return format_error_response(
"PR_MERGE_FAILED",
+13
View File
@@ -279,6 +279,19 @@ async def handle_escalate_to_ceo(
{"current_status": current_status},
)
# Only parent tasks can be escalated to CEO (not subtasks)
if task.get("parent_task_id"):
return format_error_response(
"IS_SUBTASK",
"Cannot escalate subtask to CEO - only parent tasks allowed.",
{
"task_id": task_id,
"parent_task_id": task.get("parent_task_id"),
"guidance": "Escalate the parent task instead.",
},
hint="roboco_kb_search('parent task escalation')",
)
payload = {}
if notes:
payload["notes"] = notes
+26 -10
View File
@@ -258,6 +258,31 @@ def _validate_qa_issues(issues: list[str]) -> dict[str, Any] | None:
return None
async def _validate_qa_fail_request(
client: ApiClient, task_id: str, issues: list[str], agent_id: str
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
"""Validate QA fail request. Returns (task, None) or (None, error)."""
if error := _validate_qa_role(agent_id, "fail"):
return None, error
if error := _validate_qa_issues(issues):
return None, error
task, error = await fetch_task_or_error(client, task_id)
if error:
return None, error
assert task is not None
if error := validate_task_status_in(task, QA_WORKFLOW_STATUSES, "fail QA on"):
return None, error
# Security: prevent QA from reviewing their own work
if error := await _check_self_review(task, agent_id, client):
return None, error
return task, None
async def handle_task_qa_fail(
client: ApiClient,
task_id: str,
@@ -266,20 +291,11 @@ async def handle_task_qa_fail(
agent_id: str,
) -> dict[str, Any]:
"""Handle task QA failure."""
if error := _validate_qa_role(agent_id, "fail"):
return error
if error := _validate_qa_issues(issues):
return error
task, error = await fetch_task_or_error(client, task_id)
task, error = await _validate_qa_fail_request(client, task_id, issues, agent_id)
if error:
return error
assert task is not None
if error := validate_task_status_in(task, QA_WORKFLOW_STATUSES, "fail QA on"):
return error
full_notes = f"{qa_notes}\n\nIssues:\n" + "\n".join(f"- {i}" for i in issues)
fail_resp = await client.post(
f"/tasks/{task_id}/fail-qa", json={"notes": full_notes}
+30 -27
View File
@@ -7,6 +7,7 @@ Handler functions for test/CI operations. Each handler:
3. Returns formatted response with results and guidance
"""
from dataclasses import dataclass
from typing import Any
from roboco.mcp.utils import (
@@ -15,6 +16,17 @@ from roboco.mcp.utils import (
format_success_response,
)
@dataclass
class TestContext:
"""Common context for test operations."""
client: ApiClient
project_slug: str
task_id: str
agent_id: str
# =============================================================================
# READ-ONLY HANDLERS
# =============================================================================
@@ -54,25 +66,22 @@ async def handle_test_status(
# =============================================================================
async def handle_test_run( # noqa: PLR0913
client: ApiClient,
project_slug: str,
task_id: str,
async def handle_test_run(
ctx: TestContext,
test_path: str | None,
verbose: bool,
agent_id: str,
) -> dict[str, Any]:
"""Handle test run request."""
payload: dict[str, Any] = {
"project_slug": project_slug,
"task_id": task_id,
"agent_id": agent_id,
"project_slug": ctx.project_slug,
"task_id": ctx.task_id,
"agent_id": ctx.agent_id,
"verbose": verbose,
}
if test_path:
payload["test_path"] = test_path
resp = await client.post("/test/run", json=payload)
resp = await ctx.client.post("/test/run", json=payload)
if not resp.ok:
return format_error_response(
"TEST_RUN_FAILED",
@@ -97,25 +106,22 @@ async def handle_test_run( # noqa: PLR0913
return format_success_response(data, guidance=guidance, next_step=next_step)
async def handle_test_lint( # noqa: PLR0913
client: ApiClient,
project_slug: str,
task_id: str,
async def handle_test_lint(
ctx: TestContext,
fix: bool,
path: str | None,
agent_id: str,
) -> dict[str, Any]:
"""Handle lint request."""
payload: dict[str, Any] = {
"project_slug": project_slug,
"task_id": task_id,
"agent_id": agent_id,
"project_slug": ctx.project_slug,
"task_id": ctx.task_id,
"agent_id": ctx.agent_id,
"fix": fix,
}
if path:
payload["path"] = path
resp = await client.post("/test/lint", json=payload)
resp = await ctx.client.post("/test/lint", json=payload)
if not resp.ok:
return format_error_response(
"LINT_FAILED",
@@ -145,25 +151,22 @@ async def handle_test_lint( # noqa: PLR0913
)
async def handle_test_format( # noqa: PLR0913
client: ApiClient,
project_slug: str,
task_id: str,
async def handle_test_format(
ctx: TestContext,
check_only: bool,
path: str | None,
agent_id: str,
) -> dict[str, Any]:
"""Handle format request."""
payload: dict[str, Any] = {
"project_slug": project_slug,
"task_id": task_id,
"agent_id": agent_id,
"project_slug": ctx.project_slug,
"task_id": ctx.task_id,
"agent_id": ctx.agent_id,
"check_only": check_only,
}
if path:
payload["path"] = path
resp = await client.post("/test/format", json=payload)
resp = await ctx.client.post("/test/format", json=payload)
if not resp.ok:
return format_error_response(
"FORMAT_FAILED",
+15 -6
View File
@@ -76,9 +76,12 @@ def _register_test_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None
Returns:
Test results with pass/fail counts and output
"""
return await handle_test_run(
client, project_slug, task_id, test_path, verbose, agent_id
from roboco.mcp.test.handlers import TestContext
ctx = TestContext(
client=client, project_slug=project_slug, task_id=task_id, agent_id=agent_id
)
return await handle_test_run(ctx, test_path, verbose)
@mcp.tool()
async def roboco_test_lint(
@@ -101,9 +104,12 @@ def _register_test_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None
Returns:
Lint results with issues found
"""
return await handle_test_lint(
client, project_slug, task_id, fix, path, agent_id
from roboco.mcp.test.handlers import TestContext
ctx = TestContext(
client=client, project_slug=project_slug, task_id=task_id, agent_id=agent_id
)
return await handle_test_lint(ctx, fix, path)
@mcp.tool()
async def roboco_test_format(
@@ -126,9 +132,12 @@ def _register_test_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None
Returns:
Format results with files modified
"""
return await handle_test_format(
client, project_slug, task_id, check_only, path, agent_id
from roboco.mcp.test.handlers import TestContext
ctx = TestContext(
client=client, project_slug=project_slug, task_id=task_id, agent_id=agent_id
)
return await handle_test_format(ctx, check_only, path)
@mcp.tool()
async def roboco_test_typecheck(
+42 -4
View File
@@ -281,6 +281,10 @@ class AgentOrchestrator:
# Git - branch management, commits, PRs
# Role-based permissions enforced at handler level
"mcp__roboco-git__*",
# Agent-to-Agent protocol - cross-cell coordination
"mcp__roboco-a2a__*",
# Test tools - run tests, lint, format
"mcp__roboco-test__*",
# File operations for documenters and developers
# Note: // prefix = absolute path (container paths like /app/docs)
"Write(//app/docs/**)",
@@ -577,11 +581,17 @@ class AgentOrchestrator:
agent_id: str,
git_context: SpawnGitContext | None = None,
) -> Path:
"""Generate role-aware MCP config for an agent.
"""Generate MCP config for an agent.
Different roles get different MCP server access:
- All agents: task, message, journal
- PMs only: notify (for sending notifications)
All agents get access to these MCP servers:
- roboco-task: Task management
- roboco-message: Channel messaging
- roboco-journal: Personal journaling
- roboco-notify: Notifications (read for all, send for PMs)
- roboco-optimal: Knowledge base, RAG, semantic search
- roboco-git: Git operations (role-based at handler level)
- roboco-a2a: Agent-to-Agent protocol
- roboco-test: Test/lint/format tools
Git context is passed to MCP servers so git tools can use defaults.
"""
@@ -679,6 +689,34 @@ class AgentOrchestrator:
"env": mcp_env,
}
# A2A server - Agent-to-Agent protocol for cross-cell coordination
# All agents can discover and request help from other agents
mcp_servers["roboco-a2a"] = {
"command": "uv",
"args": [
"run",
"python",
"-m",
"roboco.mcp.a2a_server",
agent_id,
],
"env": mcp_env,
}
# Test server - run tests, lint, format, typecheck, build
# Role-based permissions enforced at handler level
mcp_servers["roboco-test"] = {
"command": "uv",
"args": [
"run",
"python",
"-m",
"roboco.mcp.test.test_server",
agent_id,
],
"env": mcp_env,
}
config: dict[str, Any] = {"mcpServers": mcp_servers}
# Write to shared config directory (mounted in both orchestrator and agents)
+135
View File
@@ -159,6 +159,9 @@ class OptimalService:
self._plugins: dict[IndexType, BaseIndexPlugin] = {}
self._prompt_templates: dict[str, dict[str, Any]] = {}
self._indexing_task: Any = None # Background indexing task
self._periodic_update_task: Any = None # Periodic update task
self._file_mtimes: dict[str, float] = {} # Track file modification times
self._docs_root: Path | None = None # Cached docs root path
async def initialize(self) -> None:
"""
@@ -258,6 +261,7 @@ class OptimalService:
Runs auto-indexing in background without blocking API startup.
Logs errors but doesn't crash the service if Ollama is unavailable.
After startup indexing, starts periodic update task if enabled.
"""
try:
report = await self._auto_index_on_startup()
@@ -280,6 +284,9 @@ class OptimalService:
error=str(e),
)
# Start periodic update task if enabled
await self._start_periodic_update()
async def _auto_index_on_startup(self, force: bool = False) -> AutoIndexReport:
"""
Auto-index documentation on startup.
@@ -331,6 +338,7 @@ class OptimalService:
for path in possible_docs_roots:
if path.exists() and path.is_dir():
docs_root = path
self._docs_root = path # Cache for periodic updates
break
if docs_root is None:
@@ -391,6 +399,12 @@ class OptimalService:
await self.index_documentation([str(md_file)])
report.successful += 1
# Track mtime for periodic update detection
import contextlib
with contextlib.suppress(OSError):
self._file_mtimes[str(md_file)] = md_file.stat().st_mtime
logger.debug(f"Indexed {name} file", file=str(md_file))
except Exception as e:
error_msg = str(e)
@@ -410,8 +424,129 @@ class OptimalService:
)
return report
# =========================================================================
# PERIODIC UPDATE (File Change Detection)
# =========================================================================
async def _start_periodic_update(self) -> None:
"""Start periodic update task if enabled in config."""
import asyncio
from roboco.config import get_settings
settings = get_settings()
if not settings.rag_auto_update_enabled:
logger.info("RAG auto-update disabled in config")
return
interval = settings.rag_auto_update_interval
logger.info(
"Starting RAG periodic update task",
interval_seconds=interval,
)
self._periodic_update_task = asyncio.create_task(
self._periodic_update_loop(interval)
)
async def _periodic_update_loop(self, interval: int) -> None:
"""Background loop that checks for file changes periodically."""
import asyncio
while True:
try:
await asyncio.sleep(interval)
await self._check_for_updates()
except asyncio.CancelledError:
logger.info("Periodic update task cancelled")
break
except Exception as e:
logger.error("Periodic update check failed", error=str(e))
# Continue running despite errors
def _resolve_docs_root(self) -> Path | None:
"""Resolve and cache the docs root directory."""
if self._docs_root is not None:
return self._docs_root
possible_docs_roots = [
Path("/app/docs"),
Path(__file__).parent.parent.parent / "docs",
Path.cwd() / "docs",
]
for path in possible_docs_roots:
if path.exists() and path.is_dir():
self._docs_root = path
return path
return None
async def _check_for_updates(self) -> None:
"""Scan for new or modified files and index them."""
docs_root = self._resolve_docs_root()
if docs_root is None:
return
rag_dir = docs_root / "rag"
if not rag_dir.exists():
return
new_files: list[Path] = []
modified_files: list[Path] = []
for md_file in rag_dir.rglob("*.md"):
file_path = str(md_file)
try:
current_mtime = md_file.stat().st_mtime
except OSError:
continue
if file_path not in self._file_mtimes:
new_files.append(md_file)
self._file_mtimes[file_path] = current_mtime
elif current_mtime > self._file_mtimes[file_path]:
modified_files.append(md_file)
self._file_mtimes[file_path] = current_mtime
files_to_index = new_files + modified_files
if not files_to_index:
return
logger.info(
"Detected file changes, re-indexing",
new_count=len(new_files),
modified_count=len(modified_files),
)
indexed = 0
for md_file in files_to_index:
try:
await self.index_documentation([str(md_file)])
indexed += 1
logger.debug("Re-indexed file", file=str(md_file))
except Exception as e:
logger.warning(
"Failed to re-index file",
file=str(md_file),
error=str(e),
)
if indexed > 0:
logger.info(
"Periodic update complete",
indexed=indexed,
total_files=len(files_to_index),
)
async def close(self) -> None:
"""Cleanup resources."""
import asyncio
import contextlib
# Cancel periodic update task
if self._periodic_update_task and not self._periodic_update_task.done():
self._periodic_update_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await self._periodic_update_task
for plugin in self._plugins.values():
await plugin.close()
self._plugins.clear()
@@ -76,11 +76,12 @@ class ConversationsIndexPlugin(BaseIndexPlugin):
top_k: int = 5,
) -> list:
"""Search conversations in a specific channel."""
return await self.search(
outcome = await self.search(
query=query,
top_k=top_k,
filters={"channel_id": str(channel_id)},
)
return outcome.results
async def search_by_session(
self,
@@ -89,8 +90,9 @@ class ConversationsIndexPlugin(BaseIndexPlugin):
top_k: int = 5,
) -> list:
"""Search conversations in a specific session."""
return await self.search(
outcome = await self.search(
query=query,
top_k=top_k,
filters={"session_id": str(session_id)},
)
return outcome.results
@@ -125,10 +125,10 @@ class DecisionsIndexPlugin(BaseIndexPlugin):
Returns:
List of similar past decisions
"""
results = await self.search(query=topic, top_k=top_k)
outcome = await self.search(query=topic, top_k=top_k)
decisions = []
for result in results:
for result in outcome.results:
if result.score >= threshold:
decisions.append(
Decision(
@@ -176,11 +176,12 @@ class DecisionsIndexPlugin(BaseIndexPlugin):
top_k: int = 5,
) -> list[SearchResult]:
"""Search decisions by scope (team or org)."""
return await self.search(
outcome = await self.search(
query=query,
top_k=top_k,
filters={"scope": scope},
)
return outcome.results
async def search_by_agent(
self,
@@ -189,8 +190,9 @@ class DecisionsIndexPlugin(BaseIndexPlugin):
top_k: int = 5,
) -> list[SearchResult]:
"""Search decisions made by a specific agent."""
return await self.search(
outcome = await self.search(
query=query,
top_k=top_k,
filters={"agent_id": str(agent_id)},
)
return outcome.results
@@ -113,7 +113,8 @@ class ErrorsIndexPlugin(BaseIndexPlugin):
if context:
query += f" Context: {context}"
results = await self.search(query=query, top_k=top_k)
outcome = await self.search(query=query, top_k=top_k)
results = outcome.results
# Boost results where worked=True
for result in results:
@@ -129,8 +130,9 @@ class ErrorsIndexPlugin(BaseIndexPlugin):
top_k: int = 5,
) -> list[SearchResult]:
"""Search errors from a specific team."""
return await self.search(
outcome = await self.search(
query=query,
top_k=top_k,
filters={"team": team},
)
return outcome.results
@@ -72,11 +72,12 @@ class JournalsIndexPlugin(BaseIndexPlugin):
top_k: int = 5,
) -> list:
"""Search journal entries for a specific agent."""
return await self.search(
outcome = await self.search(
query=query,
top_k=top_k,
filters={"agent_id": str(agent_id)},
)
return outcome.results
async def search_by_type(
self,
@@ -85,8 +86,9 @@ class JournalsIndexPlugin(BaseIndexPlugin):
top_k: int = 5,
) -> list:
"""Search journal entries of a specific type."""
return await self.search(
outcome = await self.search(
query=query,
top_k=top_k,
filters={"entry_type": entry_type},
)
return outcome.results
@@ -146,7 +146,8 @@ class LearningsIndexPlugin(BaseIndexPlugin):
if shareable_only:
filters["shareable"] = True
return await self.search(query=query, top_k=top_k, filters=filters)
outcome = await self.search(query=query, top_k=top_k, filters=filters)
return outcome.results
async def get_learnings_by_category(
self,
@@ -154,11 +155,12 @@ class LearningsIndexPlugin(BaseIndexPlugin):
top_k: int = 20,
) -> list[SearchResult]:
"""Get all learnings in a category."""
return await self.search(
outcome = await self.search(
query=f"learnings about {category}",
top_k=top_k,
filters={"category": category},
)
return outcome.results
async def get_learnings_by_role(
self,
@@ -166,11 +168,12 @@ class LearningsIndexPlugin(BaseIndexPlugin):
top_k: int = 20,
) -> list[SearchResult]:
"""Get learnings from agents with a specific role."""
return await self.search(
outcome = await self.search(
query=f"learnings from {agent_role}",
top_k=top_k,
filters={"agent_role": agent_role},
)
return outcome.results
async def get_team_learnings(
self,
@@ -178,8 +181,9 @@ class LearningsIndexPlugin(BaseIndexPlugin):
top_k: int = 20,
) -> list[SearchResult]:
"""Get learnings from a specific team."""
return await self.search(
outcome = await self.search(
query="team learnings",
top_k=top_k,
filters={"team": team},
)
return outcome.results
@@ -159,7 +159,8 @@ class ReviewsIndexPlugin(BaseIndexPlugin):
query = f"Review for file: {file_path}"
# Search without filters first
results = await self.search(query=query, top_k=top_k * 2)
outcome = await self.search(query=query, top_k=top_k * 2)
results = outcome.results
# Filter by pattern similarity
filtered = []
@@ -181,11 +182,12 @@ class ReviewsIndexPlugin(BaseIndexPlugin):
top_k: int = 5,
) -> list[SearchResult]:
"""Search reviews by type (code, security, performance)."""
return await self.search(
outcome = await self.search(
query=query,
top_k=top_k,
filters={"review_type": review_type},
)
return outcome.results
async def search_by_severity(
self,
@@ -194,8 +196,9 @@ class ReviewsIndexPlugin(BaseIndexPlugin):
top_k: int = 5,
) -> list[SearchResult]:
"""Search reviews by severity."""
return await self.search(
outcome = await self.search(
query=query,
top_k=top_k,
filters={"severity": severity},
)
return outcome.results
@@ -265,7 +265,8 @@ class StandardsIndexPlugin(BaseIndexPlugin):
if severity:
filters["severity"] = severity
return await self.search(query=query, top_k=top_k, filters=filters)
outcome = await self.search(query=query, top_k=top_k, filters=filters)
return outcome.results
async def validate_against_standards(
self,
@@ -454,18 +454,14 @@ class OllamaEmbedder:
f"{self.base_url}/api/embed",
json={"model": self.model, "input": batch},
)
return self._handle_embed_response(
response, input_count=len(batch)
)
return self._handle_embed_response(response, input_count=len(batch))
except httpx.ConnectError as e:
last_error = OllamaConnectionError(
f"Cannot connect to Ollama at {self.base_url}: {e}"
)
except httpx.TimeoutException as e:
last_error = OllamaConnectionError(
f"Ollama request timed out: {e}"
)
last_error = OllamaConnectionError(f"Ollama request timed out: {e}")
except (OllamaModelError, OllamaEmbedderError):
raise
except Exception as e:
@@ -653,9 +649,7 @@ class OllamaEmbedder:
f"Cannot connect to Ollama at {self.base_url}: {e}"
)
except httpx.TimeoutException as e:
last_error = OllamaConnectionError(
f"Ollama request timed out: {e}"
)
last_error = OllamaConnectionError(f"Ollama request timed out: {e}")
except (OllamaModelError, OllamaEmbedderError):
raise
except Exception as e:
+129 -154
View File
@@ -1722,8 +1722,10 @@ class TaskService(BaseService):
)
if ready_for_pm:
# Both conditions met - transition to PM review
task.status = TaskStatus.AWAITING_PM_REVIEW
# Both conditions met - transition to PM review (validated)
self._validate_and_set_status(
task, TaskStatus.AWAITING_PM_REVIEW, "documenter"
)
# Clear assignment so PM can claim the task for review
task.assigned_to = None
self.log.info(
@@ -1908,178 +1910,89 @@ class TaskService(BaseService):
)
return task
async def complete( # noqa: PLR0911, PLR0912, PLR0915
self,
task_id: UUID,
agent_id: UUID | None = None,
force_with_cancelled: bool = False,
justification: str | None = None,
) -> TaskTable | None:
"""
Mark task as completed (PM only).
Approval hierarchy:
1. Cell PM reviews reassigns to Main PM (same awaiting_pm_review state)
2. Main PM reviews leaf task completes
3. Main PM reviews parent task (all descendants terminal) escalates to CEO
PM Override for cancelled subtasks:
Use force_with_cancelled=True with justification to complete despite
cancelled subtasks. Only works if ALL non-completed children are cancelled.
Args:
task_id: The task to complete
agent_id: Optional agent UUID - if provided, allows PM to complete
their own in_progress tasks
force_with_cancelled: Override cancelled subtask check
justification: Required when force_with_cancelled=True
Returns:
The completed task or None if completion not allowed
"""
task = await self.get(task_id)
if not task:
async def _get_completing_agent_role(self, agent_id: UUID | None) -> str | None:
"""Get the role of the completing agent."""
if not agent_id:
return None
agent_result = await self.session.execute(
select(AgentTable).where(AgentTable.id == agent_id)
)
agent = agent_result.scalar_one_or_none()
if agent and agent.role:
return agent.role.value if hasattr(agent.role, "value") else str(agent.role)
return None
# Get the completing agent's role
completing_agent_role = None
if agent_id:
agent_result = await self.session.execute(
select(AgentTable).where(AgentTable.id == agent_id)
)
completing_agent = agent_result.scalar_one_or_none()
if completing_agent and completing_agent.role:
completing_agent_role = (
completing_agent.role.value
if hasattr(completing_agent.role, "value")
else str(completing_agent.role)
)
# Check if PM is completing their own task (assigned to them)
is_own_task = agent_id and task.assigned_to == agent_id
# Two valid completion paths:
# 1. Normal workflow: task in awaiting_pm_review (dev → QA → docs → PM)
# 2. PM's own work: task in in_progress AND assigned to this PM
def _is_valid_completion_status(
self, task: TaskTable, agent_id: UUID | None
) -> bool:
"""Check if task is in a valid status for completion."""
if task.status == TaskStatus.AWAITING_PM_REVIEW:
pass # Normal completion of developer work
elif task.status == TaskStatus.IN_PROGRESS and is_own_task:
pass # PM completing their own task
else:
return True
is_own_task = agent_id and task.assigned_to == agent_id
return task.status == TaskStatus.IN_PROGRESS and bool(is_own_task)
async def _handle_cell_pm_escalation(
self, task: TaskTable, task_id: UUID, agent_id: UUID | None
) -> TaskTable | None:
"""Handle Cell PM escalation to Main PM. Returns task if escalated."""
main_pm_result = await self.session.execute(
select(AgentTable).where(AgentTable.role == AgentRole.MAIN_PM)
)
main_pm = main_pm_result.scalar_one_or_none()
if not main_pm:
self.log.warning(
"Cannot complete task - invalid status for completion",
task_id=str(task_id),
current_status=task.status.value,
is_own_task=is_own_task,
"No Main PM found - proceeding with completion", task_id=str(task_id)
)
return None
# Check ALL descendants (recursive - children, grandchildren, etc.)
task.assigned_to = cast("Any", main_pm.id)
await self.session.flush()
await self._emit_task_event(
EventType.TASK_ESCALATED_TO_MAIN_PM,
task_id,
{
"main_pm_id": str(main_pm.id),
"cell_pm_id": str(agent_id) if agent_id else None,
},
)
self.log.info(
"Cell PM approved - escalating to Main PM",
task_id=str(task_id),
main_pm_id=str(main_pm.id),
)
return task
async def _validate_completion_prerequisites(
self, task: TaskTable, task_id: UUID, agent_id: UUID | None
) -> list[TaskTable] | None:
"""Validate task can be completed. Returns descendants or None."""
if not self._is_valid_completion_status(task, agent_id):
self.log.warning("Cannot complete - invalid status", task_id=str(task_id))
return None
all_descendants = await self.get_all_descendants(task_id)
incomplete_descendants = [
incomplete = [
st
for st in all_descendants
if st.status not in (TaskStatus.COMPLETED, TaskStatus.CANCELLED)
]
if incomplete_descendants:
# Block completion - some descendants are still in progress
if incomplete:
self.log.warning(
"Cannot complete task - incomplete descendants exist",
task_id=str(task_id),
incomplete_count=len(incomplete_descendants),
incomplete_ids=[str(st.id) for st in incomplete_descendants[:5]],
"Cannot complete - incomplete descendants", task_id=str(task_id)
)
return None
return all_descendants
# APPROVAL HIERARCHY: Cell PM → Main PM → CEO
if task.status == TaskStatus.AWAITING_PM_REVIEW:
# Cell PM reviewed - escalate to Main PM
if completing_agent_role == "cell_pm":
main_pm_result = await self.session.execute(
select(AgentTable).where(AgentTable.role == AgentRole.MAIN_PM)
)
main_pm = main_pm_result.scalar_one_or_none()
if main_pm:
task.assigned_to = cast("Any", main_pm.id)
await self.session.flush()
# Emit event for Main PM notification
await self._emit_task_event(
EventType.TASK_ESCALATED_TO_MAIN_PM,
task_id,
{
"main_pm_id": str(main_pm.id),
"cell_pm_id": str(agent_id) if agent_id else None,
},
)
self.log.info(
"Cell PM approved - escalating to Main PM",
task_id=str(task_id),
main_pm_id=str(main_pm.id),
)
return task # Return without completing - Main PM needs to review
else:
self.log.warning(
"No Main PM found - proceeding with completion",
task_id=str(task_id),
)
# Main PM reviewed - check if parent task needs CEO approval
if completing_agent_role == "main_pm" and all_descendants:
# Parent task with descendants - needs CEO approval
self.log.info(
"Main PM approved parent task - escalating to CEO",
task_id=str(task_id),
descendant_count=len(all_descendants),
)
return await self.escalate_to_ceo(task_id, "main_pm")
# Check for cancelled descendants (only matters if force override requested)
cancelled_descendants = [
st for st in all_descendants if st.status == TaskStatus.CANCELLED
]
if cancelled_descendants and not force_with_cancelled:
self.log.warning(
"Cannot complete - cancelled descendants exist",
task_id=str(task_id),
cancelled_count=len(cancelled_descendants),
hint="use force_with_cancelled (CEO only)",
)
return None
if force_with_cancelled and cancelled_descendants:
if not justification:
self.log.warning(
"Cannot force complete - justification required",
task_id=str(task_id),
)
return None
# Log the CEO override
self.log.info(
"CEO override: completing task with cancelled descendants",
task_id=str(task_id),
agent_id=str(agent_id) if agent_id else None,
justification=justification,
cancelled_descendant_ids=[str(st.id) for st in cancelled_descendants],
)
task.completed_at = datetime.now(UTC)
# Validate transition with PM role requirement
self._validate_and_set_status(task, TaskStatus.COMPLETED, "cell_pm")
await self.session.flush()
# RAG auto-indexing hooks (fire-and-forget)
# 1. Extract completion learnings
async def _trigger_completion_hooks(
self, task: TaskTable, agent_id: UUID | None
) -> None:
"""Trigger background RAG indexing hooks after completion."""
bg_task = asyncio.create_task(
self._extract_completion_learnings(task, agent_id)
)
self._background_tasks.add(bg_task)
bg_task.add_done_callback(self._background_tasks.discard)
# 2. Index code changes from commits
if task.commits:
code_task = asyncio.create_task(
self._index_code_changes_background(
@@ -2091,7 +2004,6 @@ class TaskService(BaseService):
self._background_tasks.add(code_task)
code_task.add_done_callback(self._background_tasks.discard)
# 3. Detect and index decisions from notes
if task.dev_notes:
decision_task = asyncio.create_task(
self._index_decisions_background(
@@ -2105,7 +2017,61 @@ class TaskService(BaseService):
self._background_tasks.add(decision_task)
decision_task.add_done_callback(self._background_tasks.discard)
# Unblock any tasks waiting on this one
async def complete(
self,
task_id: UUID,
agent_id: UUID | None = None,
force_with_cancelled: bool = False,
justification: str | None = None,
) -> TaskTable | None:
"""
Mark task as completed (PM only).
Approval hierarchy:
1. Cell PM reviews reassigns to Main PM (same awaiting_pm_review state)
2. Main PM reviews leaf task completes
3. Main PM reviews parent task (all descendants terminal) escalates to CEO
"""
task = await self.get(task_id)
if not task:
return None
completing_agent_role = await self._get_completing_agent_role(agent_id)
all_descendants = await self._validate_completion_prerequisites(
task, task_id, agent_id
)
if all_descendants is None:
return None
# APPROVAL HIERARCHY: Cell PM → Main PM → CEO
if task.status == TaskStatus.AWAITING_PM_REVIEW:
if completing_agent_role == "cell_pm":
escalated = await self._handle_cell_pm_escalation(
task, task_id, agent_id
)
if escalated:
return escalated
if completing_agent_role == "main_pm" and all_descendants:
self.log.info(
"Main PM approved parent - escalating to CEO", task_id=str(task_id)
)
return await self.escalate_to_ceo(task_id, "main_pm")
# Handle cancelled descendants - require force flag and justification
cancelled = [st for st in all_descendants if st.status == TaskStatus.CANCELLED]
if cancelled and (not force_with_cancelled or not justification):
self.log.warning(
"Cannot complete - cancelled descendants", task_id=str(task_id)
)
return None
task.completed_at = datetime.now(UTC)
self._validate_and_set_status(
task, TaskStatus.COMPLETED, completing_agent_role or "cell_pm"
)
await self.session.flush()
await self._trigger_completion_hooks(task, agent_id)
await self._unblock_dependents(task_id)
return task
@@ -2148,6 +2114,15 @@ class TaskService(BaseService):
)
return None
# Only parent tasks can be escalated to CEO (not subtasks)
if task.parent_task_id:
self.log.warning(
"Cannot escalate subtask to CEO - only parent tasks allowed",
task_id=str(task_id),
parent_task_id=str(task.parent_task_id),
)
return None
# Store escalation notes
if notes:
existing_context = task.quick_context or ""
Generated
+115 -118
View File
@@ -874,11 +874,11 @@ wheels = [
[[package]]
name = "filelock"
version = "3.20.1"
version = "3.20.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c1/e0/a75dbe4bca1e7d41307323dad5ea2efdd95408f74ab2de8bd7dba9b51a1a/filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64", size = 19510, upload-time = "2026-01-02T15:33:32.582Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" },
{ url = "https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8", size = 16697, upload-time = "2026-01-02T15:33:31.133Z" },
]
[[package]]
@@ -1339,19 +1339,19 @@ wheels = [
[[package]]
name = "lance-namespace"
version = "0.4.0"
version = "0.4.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "lance-namespace-urllib3-client" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/8d/b117539252afc81b0fb94301e5543516af8594a70242ef247bc88c03cbdc/lance_namespace-0.4.0.tar.gz", hash = "sha256:aedfb5f4413ead9c5f0d2a351fe47b0b68a1dec0dd4331a88f54bce3491f630f", size = 9827, upload-time = "2025-12-21T16:07:51.349Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/8d/1e6f2e32e7c782938583c3ceaea301f85b6a2aff005b43a5b3e95f876e3e/lance_namespace-0.4.3.tar.gz", hash = "sha256:c24fc810d967b59b42894b1b4282a964331807f38172d574d5d61ffef77f5520", size = 9827, upload-time = "2026-01-01T07:54:35.502Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/fe/edbeb9ae7408685e90b2f0609c2f84bc3ef2f65d82bb4dce394de6d9c317/lance_namespace-0.4.0-py3-none-any.whl", hash = "sha256:7d91ee199a9864535ea17bd41787726c06b7ec8efbf06f7275bc54ea9998264f", size = 11701, upload-time = "2025-12-21T16:07:50.368Z" },
{ url = "https://files.pythonhosted.org/packages/0c/cf/31d478e291ca879e846e67f0cc0700df5fe63373d3897374ee4f5a035221/lance_namespace-0.4.3-py3-none-any.whl", hash = "sha256:27dfb93181673b9fdf3b48a60e8075de43429946c91d913a889964c6d2d01f00", size = 11701, upload-time = "2026-01-01T07:54:36.15Z" },
]
[[package]]
name = "lance-namespace-urllib3-client"
version = "0.4.0"
version = "0.4.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
@@ -1359,14 +1359,14 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4c/a2/53643e7ea756cd8c4275219f555a554db340d1e4e7366df39a79d9bd092d/lance_namespace_urllib3_client-0.4.0.tar.gz", hash = "sha256:896bf9336f5b14f5acc0d45ca956e291e0fcc2a0e56c1efe52723c23ae3a3296", size = 154577, upload-time = "2025-12-21T16:07:53.443Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c1/68/17502c6fde1d758d98903551fe88d73a55e5fc9a68c605829206f9611bbb/lance_namespace_urllib3_client-0.4.3.tar.gz", hash = "sha256:4cea0c78692debf5722f953671503178aa7fc0e72a80bea28243a9c093c68944", size = 157358, upload-time = "2026-01-01T07:54:33.115Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/1f/050c1ed613b0ec017fa3b85d35d52658ead1158d95a092c1b83578d39ab5/lance_namespace_urllib3_client-0.4.0-py3-none-any.whl", hash = "sha256:858b44b4b34b4ae8f4d905e10a89e4b14f08213dca9dd6751be09cfa03a7dbdc", size = 261516, upload-time = "2025-12-21T16:07:51.946Z" },
{ url = "https://files.pythonhosted.org/packages/ae/bc/f30dd5812642a0720092723029170b660d9fd0a6018476927714694c97a9/lance_namespace_urllib3_client-0.4.3-py3-none-any.whl", hash = "sha256:bc32e80e6cc92b12fa9287632d776dadd488363938d117f815c9c4450e482ad6", size = 268625, upload-time = "2026-01-01T07:54:34.341Z" },
]
[[package]]
name = "lancedb"
version = "0.26.0"
version = "0.26.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecation" },
@@ -1378,53 +1378,53 @@ dependencies = [
{ name = "tqdm" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/91/fe585b2181bd61efc65e1da410ae8ab7b29a26f156e4ca7d7d616b1234de/lancedb-0.26.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:3a0d435fff1392f056c173f695f71d495c691c555daa9802c056ea23f6a3900e", size = 41174270, upload-time = "2025-12-16T17:16:30.699Z" },
{ url = "https://files.pythonhosted.org/packages/ce/fc/e47e092f4fc97a8810b37dbee07996689bca42f0817f3f3c38d7fb51dd9d/lancedb-0.26.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2206320fd0f33c01e264960afd768987646133cf152c4d3a8b7faf81b3017bf", size = 42936720, upload-time = "2025-12-16T17:24:43.527Z" },
{ url = "https://files.pythonhosted.org/packages/b5/d7/323897d22a7c00ef1dc4f5b76df1a11df549fe887d8e05d689c2224e47b8/lancedb-0.26.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca0322cb4b62d526748f6f29e5b43cce4251c7f693e111897eb1f77e7f1ec2b", size = 45846184, upload-time = "2025-12-16T17:27:33.802Z" },
{ url = "https://files.pythonhosted.org/packages/3a/0b/7671c94b27a5aa267b9f1d6db759c9e08070cb8f783828ade04da9dc7d79/lancedb-0.26.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7f2b8d69a647265b8753576b501354333c3edfd47d12ec9f47e665e8574c92fe", size = 42954293, upload-time = "2025-12-16T17:24:30.335Z" },
{ url = "https://files.pythonhosted.org/packages/52/2e/9f720d6ae7bd3a94d096f320a0ec2f277735423af9d16cf5c61c4a70e6ca/lancedb-0.26.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8e5cc334686a389cf2f28d1c239d13a205098ed98f3914226d3966858e58b957", size = 45896935, upload-time = "2025-12-16T17:27:30.156Z" },
{ url = "https://files.pythonhosted.org/packages/00/0e/4b292c24a9e25ee2cd081d2da930fcdc672ee0eea531fc453c19c73addb5/lancedb-0.26.0-cp39-abi3-win_amd64.whl", hash = "sha256:2fc9b48a11f526de87388002eb3838329db7279241eefb3166c1c6c3b194a3cf", size = 50615000, upload-time = "2025-12-16T17:53:34.409Z" },
{ url = "https://files.pythonhosted.org/packages/45/b5/110651418ceb1fa4ff2eb74ce4bad911ecf49dc765b134f0201d5564aab8/lancedb-0.26.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:b1c4389134ede49e4be0497b9719f573f447e627426bb9e6fc1b642db11fb22d", size = 43416143, upload-time = "2026-01-02T17:57:07.232Z" },
{ url = "https://files.pythonhosted.org/packages/81/8a/b48a14281d7875e5bfccf22d911d9e1fa019c1fe7b805d290a4449e3cf60/lancedb-0.26.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07abd18e0aa4730442d0361bab4491ad469de14f9087c3542e56ca6d7fcda473", size = 45302392, upload-time = "2026-01-02T18:04:55.963Z" },
{ url = "https://files.pythonhosted.org/packages/4b/d0/8f6bc531f290206c7a0061236928710506598a2591ff1fcaea477fc52e7f/lancedb-0.26.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df8eb631519c6ede9975099bea187ea25a09e4617a421fe19e5e1613651cd62f", size = 48372676, upload-time = "2026-01-02T18:08:12.373Z" },
{ url = "https://files.pythonhosted.org/packages/f5/13/d8db83335ddf28afe1fb814ca995da7f67826f337d547e54471d7d425dd1/lancedb-0.26.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2941c9f8aa22244002307c4da5d19f12ab77dcb0569eb4f8a48b60e9c4fdee79", size = 45318771, upload-time = "2026-01-02T18:04:26.429Z" },
{ url = "https://files.pythonhosted.org/packages/08/94/10e9d4b5ba49eeba72024d310dc42e0c24feb8d5676f48e989198121a8a0/lancedb-0.26.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0d5f125b98836a49095c492085f5ecf3a78906fafcab59c367d9347eb372a4cc", size = 48425627, upload-time = "2026-01-02T18:11:57.443Z" },
{ url = "https://files.pythonhosted.org/packages/17/5d/d7a834ce8dd9c5e6ef7a0e308c7de5f87bb8f04c0944a1bea617d9d42dc7/lancedb-0.26.1-cp39-abi3-win_amd64.whl", hash = "sha256:9338d34c6e7472c97e49fd6b2638b29d3d087e8b002d92cafdbb46a8b0b1480e", size = 53214501, upload-time = "2026-01-02T22:34:06.836Z" },
]
[[package]]
name = "librt"
version = "0.7.5"
version = "0.7.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/8a/071f6628363d83e803d4783e0cd24fb9c5b798164300fcfaaa47c30659c0/librt-0.7.5.tar.gz", hash = "sha256:de4221a1181fa9c8c4b5f35506ed6f298948f44003d84d2a8b9885d7e01e6cfa", size = 145868, upload-time = "2025-12-25T03:53:16.039Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/9a/8f61e16de0ff76590af893cfb5b1aa5fa8b13e5e54433d0809c7033f59ed/librt-0.7.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b1795c4b2789b458fa290059062c2f5a297ddb28c31e704d27e161386469691a", size = 55750, upload-time = "2025-12-25T03:52:26.975Z" },
{ url = "https://files.pythonhosted.org/packages/05/7c/a8a883804851a066f301e0bad22b462260b965d5c9e7fe3c5de04e6f91f8/librt-0.7.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2fcbf2e135c11f721193aa5f42ba112bb1046afafbffd407cbc81d8d735c74d0", size = 57170, upload-time = "2025-12-25T03:52:27.948Z" },
{ url = "https://files.pythonhosted.org/packages/d6/5d/b3b47facf5945be294cf8a835b03589f70ee0e791522f99ec6782ed738b3/librt-0.7.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c039bbf79a9a2498404d1ae7e29a6c175e63678d7a54013a97397c40aee026c5", size = 165834, upload-time = "2025-12-25T03:52:29.09Z" },
{ url = "https://files.pythonhosted.org/packages/b4/b6/b26910cd0a4e43e5d02aacaaea0db0d2a52e87660dca08293067ee05601a/librt-0.7.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3919c9407faeeee35430ae135e3a78acd4ecaaaa73767529e2c15ca1d73ba325", size = 174820, upload-time = "2025-12-25T03:52:30.463Z" },
{ url = "https://files.pythonhosted.org/packages/a5/a3/81feddd345d4c869b7a693135a462ae275f964fcbbe793d01ea56a84c2ee/librt-0.7.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26b46620e1e0e45af510d9848ea0915e7040605dd2ae94ebefb6c962cbb6f7ec", size = 189609, upload-time = "2025-12-25T03:52:31.492Z" },
{ url = "https://files.pythonhosted.org/packages/ce/a9/31310796ef4157d1d37648bf4a3b84555319f14cee3e9bad7bdd7bfd9a35/librt-0.7.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9bbb8facc5375476d392990dd6a71f97e4cb42e2ac66f32e860f6e47299d5e89", size = 184589, upload-time = "2025-12-25T03:52:32.59Z" },
{ url = "https://files.pythonhosted.org/packages/32/22/da3900544cb0ac6ab7a2857850158a0a093b86f92b264aa6c4a4f2355ff3/librt-0.7.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e9e9c988b5ffde7be02180f864cbd17c0b0c1231c235748912ab2afa05789c25", size = 178251, upload-time = "2025-12-25T03:52:33.745Z" },
{ url = "https://files.pythonhosted.org/packages/db/77/78e02609846e78b9b8c8e361753b3dbac9a07e6d5b567fe518de9e074ab0/librt-0.7.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:edf6b465306215b19dbe6c3fb63cf374a8f3e1ad77f3b4c16544b83033bbb67b", size = 199852, upload-time = "2025-12-25T03:52:34.826Z" },
{ url = "https://files.pythonhosted.org/packages/2a/25/05706f6b346429c951582f1b3561f4d5e1418d0d7ba1a0c181237cd77b3b/librt-0.7.5-cp313-cp313-win32.whl", hash = "sha256:060bde69c3604f694bd8ae21a780fe8be46bb3dbb863642e8dfc75c931ca8eee", size = 43250, upload-time = "2025-12-25T03:52:35.905Z" },
{ url = "https://files.pythonhosted.org/packages/d9/59/c38677278ac0b9ae1afc611382ef6c9ea87f52ad257bd3d8d65f0eacdc6a/librt-0.7.5-cp313-cp313-win_amd64.whl", hash = "sha256:a82d5a0ee43aeae2116d7292c77cc8038f4841830ade8aa922e098933b468b9e", size = 49421, upload-time = "2025-12-25T03:52:36.895Z" },
{ url = "https://files.pythonhosted.org/packages/c0/47/1d71113df4a81de5fdfbd3d7244e05d3d67e89f25455c3380ca50b92741e/librt-0.7.5-cp313-cp313-win_arm64.whl", hash = "sha256:3c98a8d0ac9e2a7cb8ff8c53e5d6e8d82bfb2839abf144fdeaaa832f2a12aa45", size = 42827, upload-time = "2025-12-25T03:52:37.856Z" },
{ url = "https://files.pythonhosted.org/packages/97/ae/8635b4efdc784220f1378be640d8b1a794332f7f6ea81bb4859bf9d18aa7/librt-0.7.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9937574e6d842f359b8585903d04f5b4ab62277a091a93e02058158074dc52f2", size = 55191, upload-time = "2025-12-25T03:52:38.839Z" },
{ url = "https://files.pythonhosted.org/packages/52/11/ed7ef6955dc2032af37db9b0b31cd5486a138aa792e1bb9e64f0f4950e27/librt-0.7.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5cd3afd71e9bc146203b6c8141921e738364158d4aa7cdb9a874e2505163770f", size = 56894, upload-time = "2025-12-25T03:52:39.805Z" },
{ url = "https://files.pythonhosted.org/packages/24/f1/02921d4a66a1b5dcd0493b89ce76e2762b98c459fe2ad04b67b2ea6fdd39/librt-0.7.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cffa3ef0af29687455161cb446eff059bf27607f95163d6a37e27bcb37180f6", size = 163726, upload-time = "2025-12-25T03:52:40.79Z" },
{ url = "https://files.pythonhosted.org/packages/65/87/27df46d2756fcb7a82fa7f6ca038a0c6064c3e93ba65b0b86fbf6a4f76a2/librt-0.7.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f3f088482e2229387eadf8215c03f7726d56f69cce8c0c40f0795aebc9b361", size = 172470, upload-time = "2025-12-25T03:52:42.226Z" },
{ url = "https://files.pythonhosted.org/packages/9f/a9/e65a35e5d423639f4f3d8e17301ff13cc41c2ff97677fe9c361c26dbfbb7/librt-0.7.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7aa33153a5bb0bac783d2c57885889b1162823384e8313d47800a0e10d0070e", size = 186807, upload-time = "2025-12-25T03:52:43.688Z" },
{ url = "https://files.pythonhosted.org/packages/d7/b0/ac68aa582a996b1241773bd419823290c42a13dc9f494704a12a17ddd7b6/librt-0.7.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:265729b551a2dd329cc47b323a182fb7961af42abf21e913c9dd7d3331b2f3c2", size = 181810, upload-time = "2025-12-25T03:52:45.095Z" },
{ url = "https://files.pythonhosted.org/packages/e1/c1/03f6717677f20acd2d690813ec2bbe12a2de305f32c61479c53f7b9413bc/librt-0.7.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:168e04663e126416ba712114050f413ac306759a1791d87b7c11d4428ba75760", size = 175599, upload-time = "2025-12-25T03:52:46.177Z" },
{ url = "https://files.pythonhosted.org/packages/01/d7/f976ff4c07c59b69bb5eec7e5886d43243075bbef834428124b073471c86/librt-0.7.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:553dc58987d1d853adda8aeadf4db8e29749f0b11877afcc429a9ad892818ae2", size = 196506, upload-time = "2025-12-25T03:52:47.327Z" },
{ url = "https://files.pythonhosted.org/packages/b7/74/004f068b8888e61b454568b5479f88018fceb14e511ac0609cccee7dd227/librt-0.7.5-cp314-cp314-win32.whl", hash = "sha256:263f4fae9eba277513357c871275b18d14de93fd49bf5e43dc60a97b81ad5eb8", size = 39747, upload-time = "2025-12-25T03:52:48.437Z" },
{ url = "https://files.pythonhosted.org/packages/37/b1/ea3ec8fcf5f0a00df21f08972af77ad799604a306db58587308067d27af8/librt-0.7.5-cp314-cp314-win_amd64.whl", hash = "sha256:85f485b7471571e99fab4f44eeb327dc0e1f814ada575f3fa85e698417d8a54e", size = 45970, upload-time = "2025-12-25T03:52:49.389Z" },
{ url = "https://files.pythonhosted.org/packages/5d/30/5e3fb7ac4614a50fc67e6954926137d50ebc27f36419c9963a94f931f649/librt-0.7.5-cp314-cp314-win_arm64.whl", hash = "sha256:49c596cd18e90e58b7caa4d7ca7606049c1802125fcff96b8af73fa5c3870e4d", size = 39075, upload-time = "2025-12-25T03:52:50.395Z" },
{ url = "https://files.pythonhosted.org/packages/a4/7f/0af0a9306a06c2aabee3a790f5aa560c50ec0a486ab818a572dd3db6c851/librt-0.7.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:54d2aef0b0f5056f130981ad45081b278602ff3657fe16c88529f5058038e802", size = 57375, upload-time = "2025-12-25T03:52:51.439Z" },
{ url = "https://files.pythonhosted.org/packages/57/1f/c85e510baf6572a3d6ef40c742eacedc02973ed2acdb5dba2658751d9af8/librt-0.7.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b4791202296ad51ac09a3ff58eb49d9da8e3a4009167a6d76ac418a974e5fd4", size = 59234, upload-time = "2025-12-25T03:52:52.687Z" },
{ url = "https://files.pythonhosted.org/packages/49/b1/bb6535e4250cd18b88d6b18257575a0239fa1609ebba925f55f51ae08e8e/librt-0.7.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e860909fea75baef941ee6436e0453612505883b9d0d87924d4fda27865b9a2", size = 183873, upload-time = "2025-12-25T03:52:53.705Z" },
{ url = "https://files.pythonhosted.org/packages/8e/49/ad4a138cca46cdaa7f0e15fa912ce3ccb4cc0d4090bfeb8ccc35766fa6d5/librt-0.7.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f02c4337bf271c4f06637f5ff254fad2238c0b8e32a3a480ebb2fc5e26f754a5", size = 194609, upload-time = "2025-12-25T03:52:54.884Z" },
{ url = "https://files.pythonhosted.org/packages/9c/2d/3b3cb933092d94bb2c1d3c9b503d8775f08d806588c19a91ee4d1495c2a8/librt-0.7.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f51ffe59f4556243d3cc82d827bde74765f594fa3ceb80ec4de0c13ccd3416", size = 206777, upload-time = "2025-12-25T03:52:55.969Z" },
{ url = "https://files.pythonhosted.org/packages/3a/52/6e7611d3d1347812233dabc44abca4c8065ee97b83c9790d7ecc3f782bc8/librt-0.7.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0b7f080ba30601dfa3e3deed3160352273e1b9bc92e652f51103c3e9298f7899", size = 203208, upload-time = "2025-12-25T03:52:57.036Z" },
{ url = "https://files.pythonhosted.org/packages/27/aa/466ae4654bd2d45903fbf180815d41e3ae8903e5a1861f319f73c960a843/librt-0.7.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb565b4219abc8ea2402e61c7ba648a62903831059ed3564fa1245cc245d58d7", size = 196698, upload-time = "2025-12-25T03:52:58.481Z" },
{ url = "https://files.pythonhosted.org/packages/97/8f/424f7e4525bb26fe0d3e984d1c0810ced95e53be4fd867ad5916776e18a3/librt-0.7.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a3cfb15961e7333ea6ef033dc574af75153b5c230d5ad25fbcd55198f21e0cf", size = 217194, upload-time = "2025-12-25T03:52:59.575Z" },
{ url = "https://files.pythonhosted.org/packages/9e/33/13a4cb798a171b173f3c94db23adaf13a417130e1493933dc0df0d7fb439/librt-0.7.5-cp314-cp314t-win32.whl", hash = "sha256:118716de5ad6726332db1801bc90fa6d94194cd2e07c1a7822cebf12c496714d", size = 40282, upload-time = "2025-12-25T03:53:01.091Z" },
{ url = "https://files.pythonhosted.org/packages/5f/f1/62b136301796399d65dad73b580f4509bcbd347dff885a450bff08e80cb6/librt-0.7.5-cp314-cp314t-win_amd64.whl", hash = "sha256:3dd58f7ce20360c6ce0c04f7bd9081c7f9c19fc6129a3c705d0c5a35439f201d", size = 46764, upload-time = "2025-12-25T03:53:02.381Z" },
{ url = "https://files.pythonhosted.org/packages/49/cb/940431d9410fda74f941f5cd7f0e5a22c63be7b0c10fa98b2b7022b48cb1/librt-0.7.5-cp314-cp314t-win_arm64.whl", hash = "sha256:08153ea537609d11f774d2bfe84af39d50d5c9ca3a4d061d946e0c9d8bce04a1", size = 39728, upload-time = "2025-12-25T03:53:03.306Z" },
{ url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" },
{ url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" },
{ url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" },
{ url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" },
{ url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" },
{ url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" },
{ url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" },
{ url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" },
{ url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" },
{ url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" },
{ url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" },
{ url = "https://files.pythonhosted.org/packages/69/9c/228a5c1224bd23809a635490a162e9cbdc68d99f0eeb4a696f07886b8206/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188, upload-time = "2026-01-01T23:51:43.14Z" },
{ url = "https://files.pythonhosted.org/packages/ba/c2/0e7c6067e2b32a156308205e5728f4ed6478c501947e9142f525afbc6bd2/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895, upload-time = "2026-01-01T23:51:44.534Z" },
{ url = "https://files.pythonhosted.org/packages/0e/77/de50ff70c80855eb79d1d74035ef06f664dd073fb7fb9d9fb4429651b8eb/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724, upload-time = "2026-01-01T23:51:45.571Z" },
{ url = "https://files.pythonhosted.org/packages/6e/19/f8e4bf537899bdef9e0bb9f0e4b18912c2d0f858ad02091b6019864c9a6d/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470, upload-time = "2026-01-01T23:51:46.823Z" },
{ url = "https://files.pythonhosted.org/packages/42/4c/dcc575b69d99076768e8dd6141d9aecd4234cba7f0e09217937f52edb6ed/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806, upload-time = "2026-01-01T23:51:48.009Z" },
{ url = "https://files.pythonhosted.org/packages/fe/f8/4094a2b7816c88de81239a83ede6e87f1138477d7ee956c30f136009eb29/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809, upload-time = "2026-01-01T23:51:49.35Z" },
{ url = "https://files.pythonhosted.org/packages/1b/ac/821b7c0ab1b5a6cd9aee7ace8309c91545a2607185101827f79122219a7e/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597, upload-time = "2026-01-01T23:51:50.636Z" },
{ url = "https://files.pythonhosted.org/packages/71/f9/27f6bfbcc764805864c04211c6ed636fe1d58f57a7b68d1f4ae5ed74e0e0/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506, upload-time = "2026-01-01T23:51:52.535Z" },
{ url = "https://files.pythonhosted.org/packages/46/ba/c9b9c6fc931dd7ea856c573174ccaf48714905b1a7499904db2552e3bbaf/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747, upload-time = "2026-01-01T23:51:53.683Z" },
{ url = "https://files.pythonhosted.org/packages/c5/69/cd1269337c4cde3ee70176ee611ab0058aa42fc8ce5c9dce55f48facfcd8/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971, upload-time = "2026-01-01T23:51:54.697Z" },
{ url = "https://files.pythonhosted.org/packages/79/fd/e0844794423f5583108c5991313c15e2b400995f44f6ec6871f8aaf8243c/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075, upload-time = "2026-01-01T23:51:55.726Z" },
{ url = "https://files.pythonhosted.org/packages/42/02/211fd8f7c381e7b2a11d0fdfcd410f409e89967be2e705983f7c6342209a/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368, upload-time = "2026-01-01T23:51:56.706Z" },
{ url = "https://files.pythonhosted.org/packages/4c/b6/aca257affae73ece26041ae76032153266d110453173f67d7603058e708c/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238, upload-time = "2026-01-01T23:51:58.066Z" },
{ url = "https://files.pythonhosted.org/packages/96/47/7383a507d8e0c11c78ca34c9d36eab9000db5989d446a2f05dc40e76c64f/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870, upload-time = "2026-01-01T23:51:59.204Z" },
{ url = "https://files.pythonhosted.org/packages/a4/b8/50f3d8eec8efdaf79443963624175c92cec0ba84827a66b7fcfa78598e51/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608, upload-time = "2026-01-01T23:52:00.419Z" },
{ url = "https://files.pythonhosted.org/packages/23/d9/1b6520793aadb59d891e3b98ee057a75de7f737e4a8b4b37fdbecb10d60f/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776, upload-time = "2026-01-01T23:52:01.705Z" },
{ url = "https://files.pythonhosted.org/packages/ff/db/331edc3bba929d2756fa335bfcf736f36eff4efcb4f2600b545a35c2ae58/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206, upload-time = "2026-01-01T23:52:03.315Z" },
{ url = "https://files.pythonhosted.org/packages/b2/e1/6af79ec77204e85f6f2294fc171a30a91bb0e35d78493532ed680f5d98be/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697, upload-time = "2026-01-01T23:52:04.857Z" },
{ url = "https://files.pythonhosted.org/packages/f3/46/de55ecce4b2796d6d243295c221082ca3a944dc2fb3a52dcc8660ce7727d/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193, upload-time = "2026-01-01T23:52:06.159Z" },
{ url = "https://files.pythonhosted.org/packages/41/61/33063e271949787a2f8dd33c5260357e3d512a114fc82ca7890b65a76e2d/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277, upload-time = "2026-01-01T23:52:07.625Z" },
{ url = "https://files.pythonhosted.org/packages/06/21/1abd972349f83a696ea73159ac964e63e2d14086fdd9bc7ca878c25fced4/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765, upload-time = "2026-01-01T23:52:08.647Z" },
{ url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" },
]
[[package]]
@@ -2499,60 +2499,60 @@ wheels = [
[[package]]
name = "pillow"
version = "12.0.0"
version = "12.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
{ url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" },
{ url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" },
{ url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" },
{ url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" },
{ url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" },
{ url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" },
{ url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" },
{ url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" },
{ url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" },
{ url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" },
{ url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" },
{ url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
{ url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
{ url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
{ url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
{ url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
{ url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
{ url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
{ url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" },
{ url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" },
{ url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" },
{ url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" },
{ url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" },
{ url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" },
{ url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" },
{ url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" },
{ url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" },
{ url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" },
{ url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" },
{ url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" },
{ url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" },
{ url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" },
{ url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" },
{ url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" },
{ url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" },
{ url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" },
{ url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" },
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
{ url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
{ url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" },
{ url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" },
{ url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" },
{ url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" },
{ url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" },
{ url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" },
{ url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" },
{ url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" },
{ url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" },
{ url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" },
{ url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" },
{ url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" },
{ url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" },
{ url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" },
{ url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" },
{ url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" },
{ url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" },
{ url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" },
{ url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" },
{ url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" },
{ url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" },
{ url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" },
{ url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" },
{ url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" },
{ url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" },
{ url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" },
{ url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" },
{ url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" },
{ url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" },
{ url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" },
{ url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" },
{ url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" },
{ url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" },
{ url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" },
{ url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" },
{ url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" },
{ url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" },
{ url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" },
{ url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" },
{ url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" },
{ url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" },
{ url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" },
{ url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" },
{ url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" },
{ url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" },
]
[[package]]
@@ -2912,15 +2912,15 @@ crypto = [
[[package]]
name = "pymdown-extensions"
version = "10.19.1"
version = "10.20"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/2d/9f30cee56d4d6d222430d401e85b0a6a1ae229819362f5786943d1a8c03b/pymdown_extensions-10.19.1.tar.gz", hash = "sha256:4969c691009a389fb1f9712dd8e7bd70dcc418d15a0faf70acb5117d022f7de8", size = 847839, upload-time = "2025-12-14T17:25:24.42Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3e/35/e3814a5b7df295df69d035cfb8aab78b2967cdf11fcfae7faed726b66664/pymdown_extensions-10.20.tar.gz", hash = "sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52", size = 852774, upload-time = "2025-12-31T19:59:42.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/35/b763e8fbcd51968329b9adc52d188fc97859f85f2ee15fe9f379987d99c5/pymdown_extensions-10.19.1-py3-none-any.whl", hash = "sha256:e8698a66055b1dc0dca2a7f2c9d0ea6f5faa7834a9c432e3535ab96c0c4e509b", size = 266693, upload-time = "2025-12-14T17:25:22.999Z" },
{ url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" },
]
[[package]]
@@ -3492,14 +3492,11 @@ wheels = [
[[package]]
name = "ruamel-yaml"
version = "0.18.16"
version = "0.19.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ruamel-yaml-clib", marker = "python_full_version < '3.14' and platform_python_implementation == 'CPython'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/c7/ee630b29e04a672ecfc9b63227c87fd7a37eb67c1bf30fe95376437f897c/ruamel.yaml-0.18.16.tar.gz", hash = "sha256:a6e587512f3c998b2225d68aa1f35111c29fad14aed561a26e73fab729ec5e5a", size = 147269, upload-time = "2025-10-22T17:54:02.346Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/73/bb1bc2529f852e7bf64a2dec885e89ff9f5cc7bbf6c9340eed30ff2c69c5/ruamel.yaml-0.18.16-py3-none-any.whl", hash = "sha256:048f26d64245bae57a4f9ef6feb5b552a386830ef7a826f235ffb804c59efbba", size = 119858, upload-time = "2025-10-22T17:53:59.012Z" },
{ url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" },
]
[[package]]
@@ -3820,16 +3817,16 @@ wheels = [
[[package]]
name = "speechrecognition"
version = "3.14.4"
version = "3.14.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "audioop-lts" },
{ name = "standard-aifc" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3b/05/ca38458e353991676fff9c8ad027a516bc8483b5c11ff7521c19e4ddddc2/speechrecognition-3.14.4.tar.gz", hash = "sha256:e698248b611589b5ba4f760fcf6ec9bc3a8a25a87a2e0e88ec926c63539b6a6f", size = 32859616, upload-time = "2025-11-19T12:14:02.273Z" }
sdist = { url = "https://files.pythonhosted.org/packages/26/ab/bb1c60e7bfd6b7a736f76439b78ebbfb5e92a81b626b6e94a87e166f2ea4/speechrecognition-3.14.5.tar.gz", hash = "sha256:2d185192986b9b67a1502825a330e971f59a2cae0262f727a19ad1f6b586d00a", size = 32859817, upload-time = "2025-12-31T11:25:46.518Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/44/9645d4eccf04508c9677e4f75816c977a1ce306cf39e6f258f9d9deee8f9/speechrecognition-3.14.4-py3-none-any.whl", hash = "sha256:8b09d99a6ed31f994ed6b1749d6f921717e41179aa6387a79e8d514d30d98577", size = 32856066, upload-time = "2025-11-19T12:13:57.054Z" },
{ url = "https://files.pythonhosted.org/packages/b8/a7/903429719d39ac2c42aa37086c90e816d883560f13c87d51f09a2962e021/speechrecognition-3.14.5-py3-none-any.whl", hash = "sha256:0c496d74e9f29b1daadb0d96f5660f47563e42bf09316dacdd57094c5095977e", size = 32856308, upload-time = "2025-12-31T11:25:41.161Z" },
]
[[package]]
@@ -3868,15 +3865,15 @@ asyncio = [
[[package]]
name = "sse-starlette"
version = "3.1.1"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "starlette" },
]
sdist = { url = "https://files.pythonhosted.org/packages/62/08/8f554b0e5bad3e4e880521a1686d96c05198471eed860b0eb89b57ea3636/sse_starlette-3.1.1.tar.gz", hash = "sha256:bffa531420c1793ab224f63648c059bcadc412bf9fdb1301ac8de1cf9a67b7fb", size = 24306, upload-time = "2025-12-26T15:22:53.836Z" }
sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303, upload-time = "2025-12-31T08:02:20.023Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/31/4c281581a0f8de137b710a07f65518b34bcf333b201cfa06cfda9af05f8a/sse_starlette-3.1.1-py3-none-any.whl", hash = "sha256:bb38f71ae74cfd86b529907a9fda5632195dfa6ae120f214ea4c890c7ee9d436", size = 12442, upload-time = "2025-12-26T15:22:52.911Z" },
{ url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484, upload-time = "2025-12-31T08:02:18.894Z" },
]
[[package]]