diff --git a/.env.example b/.env.example
index 80e2286caec..812986dca30 100644
--- a/.env.example
+++ b/.env.example
@@ -394,24 +394,6 @@ IMAGE_TOOLS_DEBUG=false
# CONTEXT_COMPRESSION_THRESHOLD=0.85 # Compress at 85% of context limit
# Model is set via compression.summary_model in config.yaml (default: google/gemini-3-flash-preview)
-# =============================================================================
-# RL TRAINING (Tinker + Atropos)
-# =============================================================================
-# Run reinforcement learning training on language models using the Tinker API.
-# Requires the rl-server to be running (from tinker-atropos package).
-
-# Tinker API Key - RL training service
-# Get at: https://tinker-console.thinkingmachines.ai/keys
-# TINKER_API_KEY=
-
-# Weights & Biases API Key - Experiment tracking and metrics
-# Get at: https://wandb.ai/authorize
-# WANDB_API_KEY=
-
-# RL API Server URL (default: http://localhost:8080)
-# Change if running the rl-server on a different host/port
-# RL_API_URL=http://localhost:8080
-
# =============================================================================
# SKILLS HUB (GitHub integration for skill search/install/publish)
# =============================================================================
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 76580d6e8e5..00000000000
--- a/.gitmodules
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "tinker-atropos"]
- path = tinker-atropos
- url = https://github.com/nousresearch/tinker-atropos
diff --git a/AGENTS.md b/AGENTS.md
index da9f903eefb..d5d32f99c3d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -56,7 +56,6 @@ hermes-agent/
├── tui_gateway/ # Python JSON-RPC backend for the TUI
├── acp_adapter/ # ACP server (VS Code / Zed / JetBrains integration)
├── cron/ # Scheduler — jobs.py, scheduler.py
-├── environments/ # RL training environments (Atropos)
├── scripts/ # run_tests.sh, release.py, auxiliary scripts
├── website/ # Docusaurus docs site
└── tests/ # Pytest suite (~17k tests across ~900 files as of May 2026)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4bbc3c67c70..9cbc26112f6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -91,9 +91,6 @@ export VIRTUAL_ENV="$(pwd)/venv"
# Install with all extras (messaging, cron, CLI menus, dev tools)
uv pip install -e ".[all,dev]"
-# Optional: RL training submodule
-# git submodule update --init tinker-atropos && uv pip install -e "./tinker-atropos"
-
# Optional: browser tools
npm install
```
@@ -196,7 +193,6 @@ hermes-agent/
│
├── skills/ # Bundled skills (copied to ~/.hermes/skills/ on install)
├── optional-skills/ # Official optional skills (discoverable via hub, not activated by default)
-├── environments/ # RL training environments (Atropos integration)
├── tests/ # Test suite
├── website/ # Documentation site (hermes-agent.nousresearch.com)
│
diff --git a/README.md b/README.md
index 7e71632c310..efe5515f4d8 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ Use any model you want — [Nous Portal](https://portal.nousresearch.com), [Open
| Scheduled automations | Built-in cron scheduler with delivery to any platform. Daily reports, nightly backups, weekly audits — all in natural language, running unattended. |
| Delegates and parallelizes | Spawn isolated subagents for parallel workstreams. Write Python scripts that call tools via RPC, collapsing multi-step pipelines into zero-context-cost turns. |
| Runs anywhere, not just your laptop | Seven terminal backends — local, Docker, SSH, Singularity, Modal, Daytona, and Vercel Sandbox. Daytona and Modal offer serverless persistence — your agent's environment hibernates when idle and wakes on demand, costing nearly nothing between sessions. Run it on a $5 VPS or a GPU cluster. |
-| Research-ready | Batch trajectory generation, Atropos RL environments, trajectory compression for training the next generation of tool-calling models. |
+| Research-ready | Batch trajectory generation, trajectory compression for training the next generation of tool-calling models. |
---
@@ -175,8 +175,6 @@ uv pip install -e ".[all,dev]"
scripts/run_tests.sh
```
-> **RL Training (optional):** The RL/Atropos integration (`environments/`) — see [`CONTRIBUTING.md`](https://github.com/NousResearch/hermes-agent/blob/main/CONTRIBUTING.md#development-setup) for the full setup.
-
---
## Community
diff --git a/README.zh-CN.md b/README.zh-CN.md
index ea7fea8dcce..9a964574413 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -23,7 +23,7 @@
| 定时自动化 | 内置 cron 调度器,支持向任何平台投递。日报、夜间备份、周审计——全部用自然语言描述,无人值守运行。 |
| 委派与并行 | 生成隔离子代理处理并行工作流。编写 Python 脚本通过 RPC 调用工具,将多步管道压缩为零上下文开销的轮次。 |
| 随处运行 | 六种终端后端——本地、Docker、SSH、Daytona、Singularity 和 Modal。Daytona 和 Modal 提供 Serverless 持久化——代理环境空闲时休眠、按需唤醒,空闲期间几乎零成本。$5 VPS 或 GPU 集群都能跑。 |
-| 研究就绪 | 批量轨迹生成、Atropos RL 环境、轨迹压缩——用于训练下一代工具调用模型。 |
+| 研究就绪 | 批量轨迹生成、轨迹压缩——用于训练下一代工具调用模型。 |
---
@@ -161,12 +161,6 @@ uv pip install -e ".[all,dev]"
python -m pytest tests/ -q
```
-> **RL 训练(可选):** 如需参与 RL/Tinker-Atropos 集成开发:
-> ```bash
-> git submodule update --init tinker-atropos
-> uv pip install -e "./tinker-atropos"
-> ```
-
---
## 社区
diff --git a/agent/display.py b/agent/display.py
index 6c5c970aeff..cdfc88f46a3 100644
--- a/agent/display.py
+++ b/agent/display.py
@@ -240,21 +240,6 @@ def build_tool_preview(tool_name: str, args: dict, max_len: int | None = None) -
msg = msg[:17] + "..."
return f"to {target}: \"{msg}\""
- if tool_name.startswith("rl_"):
- rl_previews = {
- "rl_list_environments": "listing envs",
- "rl_select_environment": args.get("name", ""),
- "rl_get_current_config": "reading config",
- "rl_edit_config": f"{args.get('field', '')}={args.get('value', '')}",
- "rl_start_training": "starting",
- "rl_check_status": args.get("run_id", "")[:16],
- "rl_stop_training": f"stopping {args.get('run_id', '')[:16]}",
- "rl_get_results": args.get("run_id", "")[:16],
- "rl_list_runs": "listing runs",
- "rl_test_inference": f"{args.get('num_steps', 3)} steps",
- }
- return rl_previews.get(tool_name)
-
key = primary_args.get(tool_name)
if not key:
for fallback_key in ("query", "text", "command", "path", "name", "prompt", "code", "goal"):
@@ -981,15 +966,6 @@ def get_cute_tool_message(
if action == "list":
return _wrap(f"┊ ⏰ cron listing {dur}")
return _wrap(f"┊ ⏰ cron {action} {args.get('job_id', '')} {dur}")
- if tool_name.startswith("rl_"):
- rl = {
- "rl_list_environments": "list envs", "rl_select_environment": f"select {args.get('name', '')}",
- "rl_get_current_config": "get config", "rl_edit_config": f"set {args.get('field', '?')}",
- "rl_start_training": "start training", "rl_check_status": f"status {args.get('run_id', '?')[:12]}",
- "rl_stop_training": f"stop {args.get('run_id', '?')[:12]}", "rl_get_results": f"results {args.get('run_id', '?')[:12]}",
- "rl_list_runs": "list runs", "rl_test_inference": "test inference",
- }
- return _wrap(f"┊ 🧪 rl {rl.get(tool_name, tool_name.replace('rl_', ''))} {dur}")
if tool_name == "execute_code":
code = args.get("code", "")
first_line = code.strip().split("\n")[0] if code.strip() else ""
diff --git a/cli-config.yaml.example b/cli-config.yaml.example
index 3f98b8868ec..f5fb7156380 100644
--- a/cli-config.yaml.example
+++ b/cli-config.yaml.example
@@ -457,7 +457,7 @@ prompt_caching:
# Two stores: MEMORY.md (agent's notes) and USER.md (user profile).
# Character limits keep the memory small and focused. The agent manages
# pruning -- when at the limit, it must consolidate or replace entries.
-# Disabled by default in batch_runner and RL environments.
+# Disabled by default in batch_runner.
#
memory:
# Agent's personal notes: environment facts, conventions, things learned
@@ -715,10 +715,9 @@ platform_toolsets:
# todo - todo (in-memory task planning, no deps)
# tts - text_to_speech (Edge TTS free, or ELEVENLABS/OPENAI/MINIMAX/MISTRAL key)
# cronjob - cronjob (create/list/update/pause/resume/run/remove scheduled tasks)
-# rl - rl_list_environments, rl_start_training, etc. (requires TINKER_API_KEY)
#
# PRESETS (curated bundles):
-# hermes-cli - All of the above except rl + send_message
+# hermes-cli - All of the above except send_message
# hermes-telegram - terminal, file, web, vision, image_gen, tts, browser,
# skills, todo, cronjob, send_message
# hermes-discord - Same as hermes-telegram
@@ -744,7 +743,6 @@ platform_toolsets:
# session_search - Search and recall past conversations (FTS5 + Gemini Flash summarization)
# tts - Text-to-speech (Edge TTS free, ElevenLabs, OpenAI, MiniMax, Mistral)
# cronjob - Schedule and manage automated tasks (CLI-only)
-# rl - RL training tools (Tinker-Atropos)
#
# Composite toolsets:
# debugging - terminal + web + file (for troubleshooting)
diff --git a/environments/README.md b/environments/README.md
deleted file mode 100644
index 3936e1f35bc..00000000000
--- a/environments/README.md
+++ /dev/null
@@ -1,324 +0,0 @@
-# Hermes-Agent Atropos Environments
-
-This directory contains the integration layer between **hermes-agent's** tool-calling capabilities and the **Atropos** RL training framework. It provides everything needed to run agentic LLMs through multi-turn tool-calling loops, score their output with arbitrary reward functions, and feed results into Atropos for training or evaluation.
-
-## Architecture Overview
-
-```
- Atropos Framework
- ┌───────────────────────┐
- │ BaseEnv │ (atroposlib)
- │ - Server management │
- │ - Worker scheduling │
- │ - Wandb logging │
- │ - CLI (serve/process/ │
- │ evaluate) │
- └───────────┬───────────┘
- │ inherits
- ┌───────────┴───────────┐
- │ HermesAgentBaseEnv │ hermes_base_env.py
- │ - Terminal backend │
- │ - Tool resolution │
- │ - Agent loop │
- │ - ToolContext │
- │ - Async patches │
- └───────────┬───────────┘
- │ inherits
- ┌─────────────────┼─────────────────┐
- │ │ │
- TerminalTestEnv HermesSweEnv TerminalBench2EvalEnv
- (stack testing) (SWE training) (TB2 benchmark eval)
-```
-
-### Inheritance Chain
-
-**BaseEnv** (from `atroposlib`) is the Atropos base class. It provides:
-- Server management (OpenAI-compatible API servers, VLLM, SGLang)
-- Worker scheduling for parallel rollouts
-- Wandb integration for metrics and rollout logging
-- CLI interface with three subcommands: `serve`, `process`, `evaluate`
-- `evaluate_log()` for saving eval results to JSON + samples.jsonl
-
-**HermesAgentBaseEnv** (`hermes_base_env.py`) extends BaseEnv with hermes-agent specifics:
-- Sets `os.environ["TERMINAL_ENV"]` to configure the terminal backend (local, docker, ssh, singularity, modal, daytona, vercel_sandbox)
-- Resolves hermes-agent toolsets via `_resolve_tools_for_group()` (calls `get_tool_definitions()` which queries `tools/registry.py`)
-- Implements `collect_trajectory()` which runs the full agent loop and computes rewards
-- Supports two-phase operation (Phase 1: OpenAI server, Phase 2: VLLM ManagedServer)
-- Applies monkey patches for async-safe tool operation at import time
-
-Concrete environments inherit from `HermesAgentBaseEnv` and implement:
-- `setup()` -- Load dataset, initialize state
-- `get_next_item()` -- Return the next item for rollout
-- `format_prompt()` -- Convert a dataset item into the user message
-- `compute_reward()` -- Score the rollout using ToolContext
-- `evaluate()` -- Periodic evaluation logic
-
-## Core Components
-
-### Agent Loop (`agent_loop.py`)
-
-`HermesAgentLoop` is the reusable multi-turn agent engine. It runs the same pattern as hermes-agent's `run_agent.py`:
-
-1. Send messages + tools to the API via `server.chat_completion()`
-2. If the response contains `tool_calls`, execute each one via `handle_function_call()` (which delegates to `tools/registry.py`'s `dispatch()`)
-3. Append tool results to the conversation and go back to step 1
-4. If the response has no tool_calls, the agent is done
-
-Tool calls are executed in a thread pool (`run_in_executor`) so backends that use `asyncio.run()` internally (Modal, Docker) don't deadlock inside Atropos's event loop.
-
-Returns an `AgentResult` containing the full conversation history, turn count, reasoning content per turn, tool errors, and optional ManagedServer state (for Phase 2).
-
-### Tool Context (`tool_context.py`)
-
-`ToolContext` is a per-rollout handle that gives reward/verification functions direct access to **all** hermes-agent tools, scoped to the rollout's `task_id`. The same `task_id` means the terminal/browser session is the SAME one the model used during its rollout -- all state (files, processes, browser tabs) is preserved.
-
-```python
-async def compute_reward(self, item, result, ctx: ToolContext):
- # Run tests in the model's terminal sandbox
- test = ctx.terminal("pytest -v")
- if test["exit_code"] == 0:
- return 1.0
-
- # Check if a file was created
- content = ctx.read_file("/workspace/solution.py")
- if content.get("content"):
- return 0.5
-
- # Download files locally for verification (binary-safe)
- ctx.download_file("/remote/output.bin", "/local/output.bin")
-
- return 0.0
-```
-
-Available methods:
-- **Terminal**: `terminal(command, timeout)` -- run shell commands
-- **Files**: `read_file(path)`, `write_file(path, content)`, `search(query, path)`
-- **Transfers**: `upload_file()`, `upload_dir()`, `download_file()`, `download_dir()` -- binary-safe file transfers between host and sandbox
-- **Web**: `web_search(query)`, `web_extract(urls)`
-- **Browser**: `browser_navigate(url)`, `browser_snapshot()`
-- **Generic**: `call_tool(name, args)` -- call any hermes-agent tool by name
-- **Cleanup**: `cleanup()` -- release all resources (called automatically after `compute_reward`)
-
-### Patches (`patches.py`)
-
-**Problem**: Some hermes-agent tools use `asyncio.run()` internally (e.g., the Modal backend). This crashes when called from inside Atropos's event loop because `asyncio.run()` cannot be nested.
-
-**Solution**: `ModalEnvironment` uses a dedicated `_AsyncWorker` background thread with its own event loop. The calling code sees a sync interface, but internally all async Modal SDK calls happen on the worker thread so they don't conflict with Atropos's loop. This is built directly into `tools/environments/modal.py` — no monkey-patching required.
-
-`patches.py` is now a no-op (kept for backward compatibility with imports).
-
-### Tool Call Parsers (`tool_call_parsers/`)
-
-Client-side parsers that extract structured `tool_calls` from raw model output text. Used in **Phase 2** (VLLM server type) where ManagedServer's `/generate` endpoint returns raw text without tool call parsing.
-
-Each parser is a standalone reimplementation of the corresponding VLLM parser's `extract_tool_calls()` logic. No VLLM dependency -- only standard library (`re`, `json`, `uuid`) and `openai` types.
-
-Available parsers:
-- `hermes` -- Hermes/ChatML `` XML format
-- `mistral` -- Mistral `[TOOL_CALLS]` format
-- `llama3_json` -- Llama 3 JSON tool calling
-- `qwen` -- Qwen tool calling format
-- `qwen3_coder` -- Qwen3 Coder format
-- `deepseek_v3` -- DeepSeek V3 format
-- `deepseek_v3_1` -- DeepSeek V3.1 format
-- `kimi_k2` -- Kimi K2 format
-- `longcat` -- Longcat format
-- `glm45` / `glm47` -- GLM model formats
-
-Usage:
-```python
-from environments.tool_call_parsers import get_parser
-
-parser = get_parser("hermes")
-content, tool_calls = parser.parse(raw_model_output)
-```
-
-In Phase 1 (OpenAI server type), these parsers are not needed -- the server handles tool call parsing natively.
-
-## Two-Phase Operation
-
-### Phase 1: OpenAI Server (Evaluation / SFT Data Generation)
-
-Uses `server.chat_completion()` with `tools=` parameter. The server (VLLM, SGLang, OpenRouter, OpenAI) handles tool call parsing natively. Returns `ChatCompletion` objects with structured `tool_calls`.
-
-- Good for: evaluation, SFT data generation, testing
-- Run with: `serve` (with `run-api`), `process`, or `evaluate` subcommands
-- Placeholder tokens are created for the Atropos pipeline
-
-### Phase 2: VLLM ManagedServer (Full RL Training)
-
-Uses ManagedServer for exact token IDs + logprobs via `/generate`. Client-side tool call parser (from `tool_call_parsers/`) reconstructs structured `tool_calls` from raw output.
-
-- Good for: full RL training with GRPO/PPO
-- Run with: `serve` subcommand
-- Real tokens, masks, and logprobs flow through the pipeline
-
-## Directory Structure
-
-```
-environments/
-├── README.md # This file
-├── __init__.py # Package exports
-├── hermes_base_env.py # Abstract base (HermesAgentBaseEnv)
-├── agent_loop.py # Multi-turn agent engine (HermesAgentLoop)
-├── tool_context.py # Per-rollout tool access for reward functions
-├── patches.py # Async-safety patches for Modal backend
-│
-├── tool_call_parsers/ # Phase 2 client-side parsers
-│ ├── __init__.py # Registry + base class
-│ ├── hermes_parser.py
-│ ├── mistral_parser.py
-│ ├── llama_parser.py
-│ ├── qwen_parser.py
-│ ├── qwen3_coder_parser.py
-│ ├── deepseek_v3_parser.py
-│ ├── deepseek_v3_1_parser.py
-│ ├── kimi_k2_parser.py
-│ ├── longcat_parser.py
-│ ├── glm45_parser.py
-│ └── glm47_parser.py
-│
-├── terminal_test_env/ # Stack validation environment
-│ └── terminal_test_env.py
-│
-├── hermes_swe_env/ # SWE-bench style training environment
-│ └── hermes_swe_env.py
-│
-└── benchmarks/ # Evaluation benchmarks
- ├── terminalbench_2/ # 89 terminal tasks, Modal sandboxes
- │ └── terminalbench2_env.py
- ├── tblite/ # 100 calibrated tasks (fast TB2 proxy)
- │ └── tblite_env.py
- └── yc_bench/ # Long-horizon strategic benchmark
- └── yc_bench_env.py
-```
-
-## Concrete Environments
-
-### TerminalTestEnv (`terminal_test_env/`)
-
-A self-contained environment with inline tasks (no external dataset needed) for validating the full stack end-to-end. Each task asks the model to create a file at a known path, and the verifier checks the content matches.
-
-```bash
-# Serve mode (needs run-api)
-run-api
-python environments/terminal_test_env/terminal_test_env.py serve
-
-# Process mode (no run-api, saves to JSONL)
-python environments/terminal_test_env/terminal_test_env.py process \
- --env.data_path_to_save_groups terminal_test_output.jsonl
-```
-
-### HermesSweEnv (`hermes_swe_env/`)
-
-SWE-bench style training environment. The model gets a coding task, uses terminal + file + web tools to solve it, and the reward function runs tests in the same Modal sandbox.
-
-```bash
-python environments/hermes_swe_env/hermes_swe_env.py serve \
- --openai.model_name YourModel \
- --env.dataset_name bigcode/humanevalpack \
- --env.terminal_backend modal
-```
-
-### TerminalBench2EvalEnv (`benchmarks/terminalbench_2/`)
-
-**Eval-only** environment for the Terminal-Bench 2.0 benchmark (89 tasks). Each task gets a pre-built Docker Hub image, a natural language instruction, and a test suite. The agent uses terminal + file tools to solve the task, then the test suite verifies correctness.
-
-Follows the standard Atropos eval pattern (like GPQA, MMLU, etc.):
-- Run via `evaluate` subcommand (no `run-api` needed)
-- `setup()` loads the dataset, `evaluate()` runs all tasks
-- `rollout_and_score_eval()` handles per-task agent loop + test verification
-- Downloads verifier output locally for reliable reward checking (Harbor pattern)
-
-```bash
-# Run full benchmark
-python environments/benchmarks/terminalbench_2/terminalbench2_env.py evaluate \
- --openai.model_name anthropic/claude-opus-4.6
-
-# Run subset of tasks
-python environments/benchmarks/terminalbench_2/terminalbench2_env.py evaluate \
- --openai.model_name anthropic/claude-opus-4.6 \
- --env.task_filter fix-git,git-multibranch
-
-# Skip specific tasks
-python environments/benchmarks/terminalbench_2/terminalbench2_env.py evaluate \
- --openai.model_name anthropic/claude-opus-4.6 \
- --env.skip_tasks heavy-task,slow-task
-```
-
-## Creating a New Environment
-
-### Training Environment
-
-1. Create a new directory under `environments/`
-2. Create your env file inheriting from `HermesAgentBaseEnv`
-3. Implement the four abstract methods + `evaluate()`
-
-```python
-from environments.hermes_base_env import HermesAgentBaseEnv, HermesAgentEnvConfig
-
-class MyEnvConfig(HermesAgentEnvConfig):
- pass # Add custom fields as needed
-
-class MyEnv(HermesAgentBaseEnv):
- name = "my-env"
- env_config_cls = MyEnvConfig
-
- @classmethod
- def config_init(cls):
- env_config = MyEnvConfig(
- enabled_toolsets=["terminal", "file"],
- terminal_backend="modal",
- # ... other config
- )
- server_configs = [APIServerConfig(...)]
- return env_config, server_configs
-
- async def setup(self):
- self.dataset = load_dataset(...)
- self.iter = 0
-
- async def get_next_item(self):
- item = self.dataset[self.iter % len(self.dataset)]
- self.iter += 1
- return item
-
- def format_prompt(self, item):
- return item["instruction"]
-
- async def compute_reward(self, item, result, ctx):
- # ctx gives you full tool access to the rollout's sandbox
- test = ctx.terminal("pytest -v")
- return 1.0 if test["exit_code"] == 0 else 0.0
-
- async def evaluate(self, *args, **kwargs):
- # Periodic evaluation logic
- ...
-
-if __name__ == "__main__":
- MyEnv.cli()
-```
-
-### Eval-Only Environment (Benchmark)
-
-For eval benchmarks, follow the pattern in `terminalbench2_env.py`:
-1. Create under `environments/benchmarks/your-benchmark/`
-2. Inherit from `HermesAgentBaseEnv`
-3. Set eval-only config: `eval_handling=STOP_TRAIN`, `steps_per_eval=1`, `total_steps=1`
-4. Stub the training methods (`collect_trajectories`, `score`)
-5. Implement `rollout_and_score_eval()` and `evaluate()`
-6. Run with `evaluate` subcommand
-
-## Key Config Fields
-
-| Field | Description | Default |
-|-------|-------------|---------|
-| `enabled_toolsets` | Which hermes toolsets to enable | `None` (all) |
-| `disabled_toolsets` | Toolsets to disable | `None` |
-| `distribution` | Probabilistic toolset distribution name | `None` |
-| `max_agent_turns` | Max LLM calls per rollout | `30` |
-| `agent_temperature` | Sampling temperature | `1.0` |
-| `terminal_backend` | `local`, `docker`, `modal`, `daytona`, `ssh`, `singularity` | `local` |
-| `system_prompt` | System message for the agent | `None` |
-| `tool_call_parser` | Parser name for Phase 2 | `hermes` |
-| `eval_handling` | `STOP_TRAIN`, `LIMIT_TRAIN`, `NONE` | `STOP_TRAIN` |
diff --git a/environments/__init__.py b/environments/__init__.py
deleted file mode 100644
index 282bc06b0b3..00000000000
--- a/environments/__init__.py
+++ /dev/null
@@ -1,36 +0,0 @@
-"""
-Hermes-Agent Atropos Environments
-
-Provides a layered integration between hermes-agent's tool-calling capabilities
-and the Atropos RL training framework.
-
-Core layers:
- - agent_loop: Reusable multi-turn agent loop with standard OpenAI-spec tool calling
- - tool_context: Per-rollout tool access handle for reward/verification functions
- - hermes_base_env: Abstract base environment (BaseEnv subclass) for Atropos
- - tool_call_parsers: Client-side tool call parser registry for Phase 2 (VLLM /generate)
-
-Concrete environments:
- - terminal_test_env/: Simple file-creation tasks for testing the stack
- - hermes_swe_env/: SWE-bench style tasks with Modal sandboxes
-
-Benchmarks (eval-only):
- - benchmarks/terminalbench_2/: Terminal-Bench 2.0 evaluation
-"""
-
-try:
- from environments.agent_loop import AgentResult, HermesAgentLoop
- from environments.tool_context import ToolContext
- from environments.hermes_base_env import HermesAgentBaseEnv, HermesAgentEnvConfig
-except ImportError:
- # atroposlib not installed — environments are unavailable but
- # submodules like tool_call_parsers can still be imported directly.
- pass
-
-__all__ = [
- "AgentResult",
- "HermesAgentLoop",
- "ToolContext",
- "HermesAgentBaseEnv",
- "HermesAgentEnvConfig",
-]
diff --git a/environments/agent_loop.py b/environments/agent_loop.py
deleted file mode 100644
index 7ca3a0f6ddb..00000000000
--- a/environments/agent_loop.py
+++ /dev/null
@@ -1,534 +0,0 @@
-"""
-HermesAgentLoop -- Reusable Multi-Turn Agent Engine
-
-Runs the hermes-agent tool-calling loop using standard OpenAI-spec tool calling.
-Works with any server that returns ChatCompletion objects with tool_calls:
- - Phase 1: OpenAI server type (VLLM, SGLang, OpenRouter, OpenAI API)
- - Phase 2: ManagedServer with client-side tool call parser
-
-The loop passes tools= and checks response.choices[0].message.tool_calls,
-identical to hermes-agent's run_agent.py. Tool execution is dispatched via
-handle_function_call() from model_tools.py.
-"""
-
-import asyncio
-import concurrent.futures
-import json
-import logging
-import os
-import uuid
-from dataclasses import dataclass, field
-from typing import Any, Dict, List, Optional, Set
-
-from model_tools import handle_function_call
-from tools.terminal_tool import get_active_env
-from tools.tool_result_storage import maybe_persist_tool_result, enforce_turn_budget
-
-# Thread pool for running sync tool calls that internally use asyncio.run()
-# (e.g., the Modal/Docker/Daytona terminal backends). Running them in a separate
-# thread gives them a clean event loop so they don't deadlock inside Atropos's loop.
-# Size must be large enough for concurrent eval tasks (e.g., 89 TB2 tasks all
-# making tool calls). Too small = thread pool starvation, tasks queue for minutes.
-# Resized at runtime by HermesAgentBaseEnv.__init__ via resize_tool_pool().
-_tool_executor = concurrent.futures.ThreadPoolExecutor(max_workers=128)
-
-
-def resize_tool_pool(max_workers: int):
- """
- Replace the global tool executor with a new one of the given size.
-
- Called by HermesAgentBaseEnv.__init__ based on config.tool_pool_size.
- Safe to call before any tasks are submitted.
- """
- global _tool_executor
- old_executor = _tool_executor
- _tool_executor = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)
- old_executor.shutdown(wait=False)
- logger.info("Tool thread pool resized to %d workers", max_workers)
-
-logger = logging.getLogger(__name__)
-
-
-@dataclass
-class ToolError:
- """Record of a tool execution error during the agent loop."""
-
- turn: int # Which turn the error occurred on
- tool_name: str # Which tool was called
- arguments: str # The arguments passed (truncated)
- error: str # The error message
- tool_result: str # The raw result returned to the model
-
-
-@dataclass
-class AgentResult:
- """Result of running the agent loop."""
-
- # Full conversation history in OpenAI message format
- messages: List[Dict[str, Any]]
- # ManagedServer.get_state() if available (Phase 2), None otherwise
- managed_state: Optional[Dict[str, Any]] = None
- # How many LLM calls were made
- turns_used: int = 0
- # True if model stopped calling tools naturally (vs hitting max_turns)
- finished_naturally: bool = False
- # Extracted reasoning content per turn (from PR #297 helpers)
- reasoning_per_turn: List[Optional[str]] = field(default_factory=list)
- # Tool errors encountered during the loop
- tool_errors: List[ToolError] = field(default_factory=list)
-
-
-def _extract_reasoning_from_message(message) -> Optional[str]:
- """
- Extract reasoning content from a ChatCompletion message.
-
- Handles multiple provider formats:
- 1. message.reasoning_content field (some providers)
- 2. message.reasoning field (some providers)
- 3. message.reasoning_details[].text (OpenRouter style)
-
- Note: block extraction from content is NOT done here -- that's
- handled by the response already in Phase 1 (server does it) or by
- ManagedServer's patch in Phase 2.
-
- Args:
- message: The assistant message from ChatCompletion response
-
- Returns:
- Extracted reasoning text, or None if not found
- """
- # Check reasoning_content field (common across providers)
- if hasattr(message, "reasoning_content") and message.reasoning_content:
- return message.reasoning_content
-
- # Check reasoning field
- if hasattr(message, "reasoning") and message.reasoning:
- return message.reasoning
-
- # Check reasoning_details (OpenRouter style)
- if hasattr(message, "reasoning_details") and message.reasoning_details:
- for detail in message.reasoning_details:
- if hasattr(detail, "text") and detail.text:
- return detail.text
- if isinstance(detail, dict) and detail.get("text"):
- return detail["text"]
-
- return None
-
-
-class HermesAgentLoop:
- """
- Runs hermes-agent's tool-calling loop using standard OpenAI-spec tool calling.
-
- Same pattern as run_agent.py:
- - Pass tools= to the API
- - Check response.choices[0].message.tool_calls
- - Dispatch via handle_function_call()
-
- Works identically with any server type -- OpenAI, VLLM, SGLang, OpenRouter,
- or ManagedServer with a parser. The server determines how tool_calls get
- populated on the response.
- """
-
- def __init__(
- self,
- server,
- tool_schemas: List[Dict[str, Any]],
- valid_tool_names: Set[str],
- max_turns: int = 30,
- task_id: Optional[str] = None,
- temperature: float = 1.0,
- max_tokens: Optional[int] = None,
- extra_body: Optional[Dict[str, Any]] = None,
- budget_config: Optional["BudgetConfig"] = None,
- ):
- """
- Initialize the agent loop.
-
- Args:
- server: Server object with chat_completion() method (OpenAIServer,
- ManagedServer, ServerManager, etc.)
- tool_schemas: OpenAI-format tool definitions from get_tool_definitions()
- valid_tool_names: Set of tool names the model is allowed to call
- max_turns: Maximum number of LLM calls before stopping
- task_id: Unique ID for terminal/browser session isolation
- temperature: Sampling temperature for generation
- max_tokens: Max tokens per generation (None for server default)
- extra_body: Extra parameters passed to the OpenAI client's create() call.
- Used for OpenRouter provider preferences, transforms, etc.
- e.g. {"provider": {"ignore": ["DeepInfra"]}}
- budget_config: Tool result persistence budget. Controls per-tool
- thresholds, per-turn aggregate budget, and preview size.
- If None, uses DEFAULT_BUDGET (current hardcoded values).
- """
- from tools.budget_config import DEFAULT_BUDGET
- self.server = server
- self.tool_schemas = tool_schemas
- self.valid_tool_names = valid_tool_names
- self.max_turns = max_turns
- self.task_id = task_id or str(uuid.uuid4())
- self.temperature = temperature
- self.max_tokens = max_tokens
- self.extra_body = extra_body
- self.budget_config = budget_config or DEFAULT_BUDGET
-
- async def run(self, messages: List[Dict[str, Any]]) -> AgentResult:
- """
- Execute the full agent loop using standard OpenAI tool calling.
-
- Args:
- messages: Initial conversation messages (system + user).
- Modified in-place as the conversation progresses.
-
- Returns:
- AgentResult with full conversation history, managed state, and metadata
- """
- reasoning_per_turn = []
- tool_errors: List[ToolError] = []
-
- # Per-loop TodoStore for the todo tool (ephemeral, dies with the loop)
- from tools.todo_tool import TodoStore, todo_tool as _todo_tool
- _todo_store = TodoStore()
-
- # Extract user task from first user message for browser_snapshot context
- _user_task = None
- for msg in messages:
- if msg.get("role") == "user":
- content = msg.get("content", "")
- if isinstance(content, str) and content.strip():
- _user_task = content.strip()[:500] # Cap to avoid huge strings
- break
-
- import time as _time
-
- for turn in range(self.max_turns):
- turn_start = _time.monotonic()
-
- # Build the chat_completion kwargs
- chat_kwargs = {
- "messages": messages,
- "n": 1,
- "temperature": self.temperature,
- }
-
- # Only pass tools if we have them
- if self.tool_schemas:
- chat_kwargs["tools"] = self.tool_schemas
-
- # Only pass max_tokens if explicitly set
- if self.max_tokens is not None:
- chat_kwargs["max_tokens"] = self.max_tokens
-
- # Inject extra_body for provider-specific params (e.g., OpenRouter
- # provider preferences like banned/preferred providers, transforms)
- if self.extra_body:
- chat_kwargs["extra_body"] = self.extra_body
-
- # Make the API call -- standard OpenAI spec
- api_start = _time.monotonic()
- try:
- response = await self.server.chat_completion(**chat_kwargs)
- except Exception as e:
- api_elapsed = _time.monotonic() - api_start
- logger.error("API call failed on turn %d (%.1fs): %s", turn + 1, api_elapsed, e)
- return AgentResult(
- messages=messages,
- managed_state=self._get_managed_state(),
- turns_used=turn + 1,
- finished_naturally=False,
- reasoning_per_turn=reasoning_per_turn,
- tool_errors=tool_errors,
- )
-
- api_elapsed = _time.monotonic() - api_start
-
- if not response or not response.choices:
- logger.warning("Empty response on turn %d (api=%.1fs)", turn + 1, api_elapsed)
- return AgentResult(
- messages=messages,
- managed_state=self._get_managed_state(),
- turns_used=turn + 1,
- finished_naturally=False,
- reasoning_per_turn=reasoning_per_turn,
- tool_errors=tool_errors,
- )
-
- assistant_msg = response.choices[0].message
-
- # Extract reasoning content from the response (all provider formats)
- reasoning = _extract_reasoning_from_message(assistant_msg)
- reasoning_per_turn.append(reasoning)
-
- # Check for tool calls -- standard OpenAI spec.
- # Fallback: if response has no structured tool_calls but content
- # contains raw tool call tags (e.g. ), parse them using
- # hermes-agent's standalone parsers. This handles the case where
- # ManagedServer's ToolCallTranslator couldn't parse because vLLM
- # isn't installed.
- if (
- not assistant_msg.tool_calls
- and assistant_msg.content
- and self.tool_schemas
- and "" in (assistant_msg.content or "")
- ):
- try:
- from environments.tool_call_parsers import get_parser
- fallback_parser = get_parser("hermes")
- parsed_content, parsed_calls = fallback_parser.parse(
- assistant_msg.content
- )
- if parsed_calls:
- assistant_msg.tool_calls = parsed_calls
- if parsed_content is not None:
- assistant_msg.content = parsed_content
- logger.debug(
- "Fallback parser extracted %d tool calls from raw content",
- len(parsed_calls),
- )
- except Exception:
- pass # Fall through to no tool calls
-
- if assistant_msg.tool_calls:
- # Normalize tool calls to dicts — they may come as objects
- # (OpenAI API) or dicts (vLLM ToolCallTranslator).
- def _tc_to_dict(tc):
- if isinstance(tc, dict):
- return {
- "id": tc.get("id", f"call_{uuid.uuid4().hex[:8]}"),
- "type": "function",
- "function": {
- "name": tc.get("function", {}).get("name", tc.get("name", "")),
- "arguments": tc.get("function", {}).get("arguments", tc.get("arguments", "{}")),
- },
- }
- return {
- "id": tc.id,
- "type": "function",
- "function": {
- "name": tc.function.name,
- "arguments": tc.function.arguments,
- },
- }
-
- # Build the assistant message dict for conversation history
- msg_dict: Dict[str, Any] = {
- "role": "assistant",
- "content": assistant_msg.content or "",
- "tool_calls": [_tc_to_dict(tc) for tc in assistant_msg.tool_calls],
- }
-
- # Preserve reasoning_content for multi-turn chat template handling
- # (e.g., Kimi-K2's template renders blocks differently
- # for history vs. the latest turn based on this field)
- if reasoning:
- msg_dict["reasoning_content"] = reasoning
-
- messages.append(msg_dict)
-
- # Execute each tool call via hermes-agent's dispatch
- for tc in assistant_msg.tool_calls:
- # Handle both object (OpenAI) and dict (vLLM) formats
- if isinstance(tc, dict):
- tool_name = tc.get("function", {}).get("name", tc.get("name", ""))
- tool_args_raw = tc.get("function", {}).get("arguments", tc.get("arguments", "{}"))
- else:
- tool_name = tc.function.name
- tool_args_raw = tc.function.arguments
-
- # Validate tool name
- if tool_name not in self.valid_tool_names:
- tool_result = json.dumps(
- {
- "error": f"Unknown tool '{tool_name}'. "
- f"Available tools: {sorted(self.valid_tool_names)}"
- }
- )
- tool_errors.append(ToolError(
- turn=turn + 1, tool_name=tool_name,
- arguments=tool_args_raw[:200],
- error=f"Unknown tool '{tool_name}'",
- tool_result=tool_result,
- ))
- logger.warning(
- "Model called unknown tool '%s' on turn %d",
- tool_name, turn + 1,
- )
- else:
- # Parse arguments
- try:
- args = json.loads(tool_args_raw)
- except json.JSONDecodeError as e:
- args = None
- tool_result = json.dumps(
- {"error": f"Invalid JSON in tool arguments: {e}. Please retry with valid JSON."}
- )
- tool_errors.append(ToolError(
- turn=turn + 1, tool_name=tool_name,
- arguments=tool_args_raw[:200],
- error=f"Invalid JSON: {e}",
- tool_result=tool_result,
- ))
- logger.warning(
- "Invalid JSON in tool call arguments for '%s': %s",
- tool_name, tool_args_raw[:200],
- )
-
- # Dispatch tool only if arguments parsed successfully
- if args is not None:
- try:
- if tool_name == "terminal":
- backend = os.getenv("TERMINAL_ENV", "local")
- cmd_preview = args.get("command", "")[:80]
- logger.info(
- "[%s] $ %s", self.task_id[:8], cmd_preview,
- )
-
- tool_submit_time = _time.monotonic()
-
- # Todo tool -- handle locally (needs per-loop TodoStore)
- if tool_name == "todo":
- tool_result = _todo_tool(
- todos=args.get("todos"),
- merge=args.get("merge", False),
- store=_todo_store,
- )
- tool_elapsed = _time.monotonic() - tool_submit_time
- elif tool_name == "memory":
- tool_result = json.dumps({"error": "Memory is not available in RL environments."})
- tool_elapsed = _time.monotonic() - tool_submit_time
- elif tool_name == "session_search":
- tool_result = json.dumps({"error": "Session search is not available in RL environments."})
- tool_elapsed = _time.monotonic() - tool_submit_time
- else:
- # Run tool calls in a thread pool so backends that
- # use asyncio.run() internally (modal, docker, daytona) get
- # a clean event loop instead of deadlocking.
- loop = asyncio.get_running_loop()
- # Capture current tool_name/args for the lambda
- _tn, _ta, _tid = tool_name, args, self.task_id
- tool_result = await loop.run_in_executor(
- _tool_executor,
- lambda: handle_function_call(
- _tn, _ta, task_id=_tid,
- user_task=_user_task,
- ),
- )
- tool_elapsed = _time.monotonic() - tool_submit_time
-
- # Log slow tools and thread pool stats for debugging
- pool_active = _tool_executor._work_queue.qsize()
- if tool_elapsed > 30:
- logger.warning(
- "[%s] turn %d: %s took %.1fs (pool queue=%d)",
- self.task_id[:8], turn + 1, tool_name,
- tool_elapsed, pool_active,
- )
- except Exception as e:
- tool_result = json.dumps(
- {"error": f"Tool execution failed: {type(e).__name__}: {str(e)}"}
- )
- tool_errors.append(ToolError(
- turn=turn + 1, tool_name=tool_name,
- arguments=tool_args_raw[:200],
- error=f"{type(e).__name__}: {str(e)}",
- tool_result=tool_result,
- ))
- logger.error(
- "Tool '%s' execution failed on turn %d: %s",
- tool_name, turn + 1, e,
- )
-
- # Also check if the tool returned an error in its JSON result
- try:
- result_data = json.loads(tool_result)
- if isinstance(result_data, dict):
- err = result_data.get("error")
- exit_code = result_data.get("exit_code")
- if err and exit_code and exit_code < 0:
- tool_errors.append(ToolError(
- turn=turn + 1, tool_name=tool_name,
- arguments=tool_args_raw[:200],
- error=str(err),
- tool_result=tool_result[:500],
- ))
- except (json.JSONDecodeError, TypeError):
- pass
-
- tc_id = tc.get("id", "") if isinstance(tc, dict) else tc.id
- tool_result = maybe_persist_tool_result(
- content=tool_result,
- tool_name=tool_name,
- tool_use_id=tc_id,
- env=get_active_env(self.task_id),
- config=self.budget_config,
- )
-
- messages.append(
- {
- "role": "tool",
- "tool_call_id": tc_id,
- "content": tool_result,
- }
- )
-
- num_tcs = len(assistant_msg.tool_calls)
- if num_tcs > 0:
- enforce_turn_budget(
- messages[-num_tcs:],
- env=get_active_env(self.task_id),
- config=self.budget_config,
- )
-
- turn_elapsed = _time.monotonic() - turn_start
- logger.info(
- "[%s] turn %d: api=%.1fs, %d tools, turn_total=%.1fs",
- self.task_id[:8], turn + 1, api_elapsed,
- len(assistant_msg.tool_calls), turn_elapsed,
- )
-
- else:
- # No tool calls -- model is done
- msg_dict = {
- "role": "assistant",
- "content": assistant_msg.content or "",
- }
- if reasoning:
- msg_dict["reasoning_content"] = reasoning
- messages.append(msg_dict)
-
- turn_elapsed = _time.monotonic() - turn_start
- logger.info(
- "[%s] turn %d: api=%.1fs, no tools (finished), turn_total=%.1fs",
- self.task_id[:8], turn + 1, api_elapsed, turn_elapsed,
- )
-
- return AgentResult(
- messages=messages,
- managed_state=self._get_managed_state(),
- turns_used=turn + 1,
- finished_naturally=True,
- reasoning_per_turn=reasoning_per_turn,
- tool_errors=tool_errors,
- )
-
- # Hit max turns without the model stopping
- logger.info("Agent hit max_turns (%d) without finishing", self.max_turns)
- return AgentResult(
- messages=messages,
- managed_state=self._get_managed_state(),
- turns_used=self.max_turns,
- finished_naturally=False,
- reasoning_per_turn=reasoning_per_turn,
- tool_errors=tool_errors,
- )
-
- def _get_managed_state(self) -> Optional[Dict[str, Any]]:
- """
- Get ManagedServer state if the server supports it.
-
- Returns state dict with SequenceNodes containing tokens/logprobs/masks,
- or None if the server doesn't support get_state() (e.g., regular OpenAI server).
- """
- if hasattr(self.server, "get_state"):
- return self.server.get_state()
- return None
diff --git a/environments/agentic_opd_env.py b/environments/agentic_opd_env.py
deleted file mode 100644
index c6ed88756bf..00000000000
--- a/environments/agentic_opd_env.py
+++ /dev/null
@@ -1,1214 +0,0 @@
-"""
-AgenticOPDEnv — On-Policy Distillation for Agentic Tool-Calling Tasks
-=====================================================================
-
-First Atropos environment to populate the distill_token_ids / distill_logprobs
-fields on ScoredDataGroup, enabling on-policy distillation (OPD) training.
-
-Key idea (from OpenClaw-RL, Princeton 2026):
- Every time an agent receives a next-state signal (tool result, error trace,
- test verdict), that signal contains hindsight information about how the
- agent's PREVIOUS response could have been better. This environment:
-
- 1. Runs standard agentic rollouts (tool-calling agent loop)
- 2. Walks the conversation to find (assistant_turn, next_state) pairs
- 3. Uses an LLM judge to extract "hints" from next-state signals
- 4. Builds an enhanced prompt (original context + hint)
- 5. Scores the student's response tokens under the enhanced distribution
- using VLLM's prompt_logprobs (via Atropos's get_logprobs API)
- 6. Packages the teacher's top-K predictions as distill_token_ids /
- distill_logprobs on the ScoredDataGroup
-
-The trainer then computes per-token advantages:
- A_t = teacher_logprob(token_t) - student_logprob(token_t)
- Positive → teacher approves this token (upweight)
- Negative → teacher disapproves (downweight)
-
-This gives dense, token-level training signal from every tool interaction,
-instead of just a scalar reward at the end of the trajectory.
-
-Task: Coding tasks with test verification (rich next-state signals from
-test results, error messages, terminal output). Falls back to built-in
-coding problems if no HuggingFace dataset is configured.
-
-Requirements:
- - VLLM backend (server_type: vllm) — needed for prompt logprob scoring
- - Phase 2 mode (ManagedServer) — needed for token-level tracking
-
-Usage:
- # Process mode (offline data generation with OPD)
- python environments/agentic_opd_env.py process \\
- --env.total_steps 10 --env.group_size 2 \\
- --env.data_path_to_save_groups output.jsonl \\
- --openai.base_url http://localhost:8000/v1 \\
- --openai.model_name Qwen/Qwen3-4B
-
- # Serve mode (connected to Atropos trainer)
- python environments/agentic_opd_env.py serve \\
- --openai.base_url http://localhost:8000/v1 \\
- --openai.model_name Qwen/Qwen3-4B
-
- # Evaluate mode
- python environments/agentic_opd_env.py evaluate \\
- --env.eval_size 10 \\
- --openai.base_url http://localhost:8000/v1 \\
- --openai.model_name Qwen/Qwen3-4B
-
-Reference: Wang et al., "OpenClaw-RL: Train Any Agent Simply by Talking"
- arXiv:2603.10165, March 2026
-"""
-
-from __future__ import annotations
-
-import asyncio
-import copy
-import json
-import logging
-import os
-import random
-import re
-import sys
-import time
-import uuid
-from pathlib import Path
-from typing import Any, Dict, List, Optional, Set, Tuple, Union
-
-from pydantic import Field
-
-# Ensure hermes-agent root is on path
-_repo_root = Path(__file__).resolve().parent.parent
-if str(_repo_root) not in sys.path:
- sys.path.insert(0, str(_repo_root))
-
-from atroposlib.envs.base import ScoredDataGroup, ScoredDataItem
-from atroposlib.envs.server_handling.server_manager import APIServerConfig
-from atroposlib.type_definitions import Item
-
-from environments.hermes_base_env import HermesAgentBaseEnv, HermesAgentEnvConfig
-from environments.agent_loop import AgentResult, HermesAgentLoop
-from environments.tool_context import ToolContext
-
-logger = logging.getLogger(__name__)
-
-
-# ═══════════════════════════════════════════════════════════════════════
-# Built-in coding tasks (fallback when no HF dataset is configured)
-# ═══════════════════════════════════════════════════════════════════════
-
-BUILTIN_CODING_TASKS = [
- {
- "task": "Write a Python function `fizzbuzz(n)` that returns a list of strings from 1 to n. "
- "For multiples of 3 return 'Fizz', for multiples of 5 return 'Buzz', "
- "for multiples of both return 'FizzBuzz', otherwise the number as a string.",
- "test_code": (
- "from solution import fizzbuzz\n"
- "assert fizzbuzz(15) == ['1','2','Fizz','4','Buzz','Fizz','7','8','Fizz','Buzz','11','Fizz','13','14','FizzBuzz']\n"
- "assert fizzbuzz(1) == ['1']\n"
- "assert fizzbuzz(0) == []\n"
- "print('All tests passed!')\n"
- ),
- "difficulty": "easy",
- },
- {
- "task": "Write a Python function `is_palindrome(s)` that checks if a string is a palindrome, "
- "ignoring case and non-alphanumeric characters. Return True or False.",
- "test_code": (
- "from solution import is_palindrome\n"
- "assert is_palindrome('A man, a plan, a canal: Panama') == True\n"
- "assert is_palindrome('race a car') == False\n"
- "assert is_palindrome('') == True\n"
- "assert is_palindrome('Was it a car or a cat I saw?') == True\n"
- "print('All tests passed!')\n"
- ),
- "difficulty": "easy",
- },
- {
- "task": "Write a Python function `two_sum(nums, target)` that returns the indices of the two "
- "numbers in `nums` that add up to `target`. Assume exactly one solution exists. "
- "Return a list of two indices [i, j] where i < j.",
- "test_code": (
- "from solution import two_sum\n"
- "assert two_sum([2, 7, 11, 15], 9) == [0, 1]\n"
- "assert two_sum([3, 2, 4], 6) == [1, 2]\n"
- "assert two_sum([3, 3], 6) == [0, 1]\n"
- "print('All tests passed!')\n"
- ),
- "difficulty": "easy",
- },
- {
- "task": "Write a Python function `flatten(lst)` that takes an arbitrarily nested list and "
- "returns a flat list of all elements. For example, flatten([1, [2, [3, 4], 5]]) "
- "should return [1, 2, 3, 4, 5].",
- "test_code": (
- "from solution import flatten\n"
- "assert flatten([1, [2, [3, 4], 5]]) == [1, 2, 3, 4, 5]\n"
- "assert flatten([]) == []\n"
- "assert flatten([1, 2, 3]) == [1, 2, 3]\n"
- "assert flatten([[[[1]]]]) == [1]\n"
- "assert flatten([1, [2], [[3]], [[[4]]]]) == [1, 2, 3, 4]\n"
- "print('All tests passed!')\n"
- ),
- "difficulty": "medium",
- },
- {
- "task": "Write a Python function `longest_common_prefix(strs)` that finds the longest "
- "common prefix string amongst a list of strings. If there is no common prefix, "
- "return an empty string.",
- "test_code": (
- "from solution import longest_common_prefix\n"
- "assert longest_common_prefix(['flower', 'flow', 'flight']) == 'fl'\n"
- "assert longest_common_prefix(['dog', 'racecar', 'car']) == ''\n"
- "assert longest_common_prefix(['interspecies', 'interstellar', 'interstate']) == 'inters'\n"
- "assert longest_common_prefix(['a']) == 'a'\n"
- "assert longest_common_prefix([]) == ''\n"
- "print('All tests passed!')\n"
- ),
- "difficulty": "easy",
- },
- {
- "task": "Write a Python function `group_anagrams(strs)` that groups anagrams together. "
- "Return a list of lists, where each inner list contains strings that are anagrams of "
- "each other. The order of groups and strings within groups does not matter.",
- "test_code": (
- "from solution import group_anagrams\n"
- "result = group_anagrams(['eat', 'tea', 'tan', 'ate', 'nat', 'bat'])\n"
- "result_sorted = sorted([sorted(g) for g in result])\n"
- "assert result_sorted == [['ate', 'eat', 'tea'], ['bat'], ['nat', 'tan']]\n"
- "assert group_anagrams([]) == []\n"
- "assert group_anagrams(['a']) == [['a']]\n"
- "print('All tests passed!')\n"
- ),
- "difficulty": "medium",
- },
- {
- "task": "Write a Python function `valid_parentheses(s)` that determines if a string "
- "containing just '(', ')', '{', '}', '[' and ']' is valid. A string is valid if "
- "open brackets are closed by the same type and in the correct order.",
- "test_code": (
- "from solution import valid_parentheses\n"
- "assert valid_parentheses('()') == True\n"
- "assert valid_parentheses('()[]{}') == True\n"
- "assert valid_parentheses('(]') == False\n"
- "assert valid_parentheses('([)]') == False\n"
- "assert valid_parentheses('{[]}') == True\n"
- "assert valid_parentheses('') == True\n"
- "print('All tests passed!')\n"
- ),
- "difficulty": "easy",
- },
- {
- "task": "Write a Python function `merge_intervals(intervals)` that merges overlapping "
- "intervals. Each interval is a list [start, end]. Return the merged intervals sorted "
- "by start time.",
- "test_code": (
- "from solution import merge_intervals\n"
- "assert merge_intervals([[1,3],[2,6],[8,10],[15,18]]) == [[1,6],[8,10],[15,18]]\n"
- "assert merge_intervals([[1,4],[4,5]]) == [[1,5]]\n"
- "assert merge_intervals([[1,4],[0,4]]) == [[0,4]]\n"
- "assert merge_intervals([]) == []\n"
- "assert merge_intervals([[1,2]]) == [[1,2]]\n"
- "print('All tests passed!')\n"
- ),
- "difficulty": "medium",
- },
-]
-
-
-# ═══════════════════════════════════════════════════════════════════════
-# Hint extraction prompts (adapted from OpenClaw-RL)
-# ═══════════════════════════════════════════════════════════════════════
-
-_HINT_JUDGE_SYSTEM = (
- "You are a process reward model used for hindsight hint extraction.\n"
- "You are given:\n"
- "1) The assistant response at turn t.\n"
- "2) The next state at turn t+1, along with its **role**.\n\n"
- "## Understanding the next state's role\n"
- "- role='user': A reply from the user (follow-up, correction, new request, etc.).\n"
- "- role='tool': The return value of a tool the assistant invoked. "
- "This content was NOT available before the assistant's action — "
- "it exists BECAUSE the assistant called the tool. "
- "A successful, non-error tool output generally means the assistant's "
- "action was appropriate; do NOT treat it as information the assistant "
- "should have already known.\n\n"
- "Your goal is to decide whether the next state reveals useful hindsight information\n"
- "that could have helped improve the assistant response at turn t.\n\n"
- "Output format rules (strict):\n"
- "- You MUST include exactly one final decision token: \\boxed{1} or \\boxed{-1}.\n"
- "- If and only if decision is \\boxed{1}, provide a concise, information-dense hint in 1-3 sentences,\n"
- " wrapped between [HINT_START] and [HINT_END].\n"
- "- If decision is \\boxed{-1}, do not provide a hint block.\n"
- "- Hint must be concrete and actionable for improving the previous response."
-)
-
-_BOXED_RE = re.compile(r"\\boxed\{(-?\d+)\}")
-_HINT_RE = re.compile(r"\[HINT_START\](.*?)\[HINT_END\]", re.DOTALL)
-
-
-def _build_hint_judge_messages(
- response_text: str, next_state_text: str, next_state_role: str = "tool"
-) -> list[dict]:
- """Build messages for the hint extraction judge."""
- user = (
- f"## Assistant response (turn t)\n{response_text}\n\n"
- f"## Next state (turn t+1) [role: {next_state_role}]\n{next_state_text}\n\n"
- "Now output your decision and (if positive) the hint in the required format."
- )
- return [
- {"role": "system", "content": _HINT_JUDGE_SYSTEM},
- {"role": "user", "content": user},
- ]
-
-
-def _parse_hint_result(text: str) -> tuple[int | None, str]:
- """Parse the judge's boxed decision and hint text."""
- boxed = _BOXED_RE.findall(text)
- score = int(boxed[-1]) if boxed else None
- if score not in {1, -1}:
- score = None
- hint_matches = _HINT_RE.findall(text)
- hint = hint_matches[-1].strip() if hint_matches else ""
- return score, hint
-
-
-def _select_best_hint(votes: list[dict]) -> dict | None:
- """Select the best hint from majority-voted judge results."""
- good = [
- v
- for v in votes
- if v.get("score") == 1
- and isinstance(v.get("hint"), str)
- and len(v["hint"].strip()) > 10
- ]
- if not good:
- return None
- return max(good, key=lambda v: len(v["hint"].strip()))
-
-
-def _append_hint_to_messages(messages: list[dict], hint: str) -> list[dict]:
- """Clone messages and append hint to the last user message."""
- cloned = copy.deepcopy(messages)
- if not cloned:
- return [{"role": "user", "content": f"[user's hint / instruction]\n{hint}"}]
-
- # Find last user message
- target_idx = None
- for i in range(len(cloned) - 1, -1, -1):
- if cloned[i].get("role") == "user":
- target_idx = i
- break
- if target_idx is None:
- target_idx = len(cloned) - 1
-
- content = cloned[target_idx].get("content", "")
- if isinstance(content, list):
- content = " ".join(
- c.get("text", "") if isinstance(c, dict) else str(c) for c in content
- )
- suffix = f"\n\n[user's hint / instruction]\n{hint.strip()}"
- cloned[target_idx]["content"] = (content + suffix).strip()
- return cloned
-
-
-# ═══════════════════════════════════════════════════════════════════════
-# Configuration
-# ═══════════════════════════════════════════════════════════════════════
-
-
-class AgenticOPDConfig(HermesAgentEnvConfig):
- """Configuration for the agentic OPD environment."""
-
- # --- OPD settings ---
- opd_enabled: bool = Field(
- default=True,
- description="Enable on-policy distillation pipeline. When disabled, "
- "the environment behaves like a standard agentic env (no distill fields).",
- )
- distill_topk: int = Field(
- default=50,
- description="Number of top-K teacher logprobs per position for distillation.",
- )
- prm_votes: int = Field(
- default=3,
- description="Number of independent judge queries for majority-voted hint extraction.",
- )
- hint_max_next_state_chars: int = Field(
- default=4000,
- description="Maximum characters of next-state text to include in the hint judge prompt. "
- "Tool results can be very long — truncating prevents judge context overflow.",
- )
-
- # --- Reward settings ---
- correctness_weight: float = Field(
- default=0.7,
- description="Weight for test pass/fail in reward.",
- )
- efficiency_weight: float = Field(
- default=0.15,
- description="Weight for efficiency (fewer turns = better).",
- )
- tool_usage_weight: float = Field(
- default=0.15,
- description="Weight for appropriate tool usage signal.",
- )
-
- # --- Dataset ---
- dataset_name: Optional[str] = Field(
- default=None,
- description="HuggingFace dataset with coding tasks. "
- "Expected fields: 'task' (problem description) and 'test_code' (pytest/assert tests). "
- "Falls back to built-in tasks if not set or unavailable.",
- )
-
- # --- Eval ---
- eval_size: int = Field(
- default=10,
- description="Number of held-out items for evaluation.",
- )
- eval_split_ratio: float = Field(
- default=0.15,
- description="Fraction of dataset to hold out for evaluation.",
- )
-
-
-# ═══════════════════════════════════════════════════════════════════════
-# Environment
-# ═══════════════════════════════════════════════════════════════════════
-
-
-class AgenticOPDEnv(HermesAgentBaseEnv):
- """
- RL environment with on-policy distillation from next-state signals.
-
- Runs coding tasks where the agent writes code and runs tests.
- Tool results (test pass/fail, error traces) serve as next-state signals
- for hint extraction and teacher logprob scoring.
-
- This is the first Atropos environment to populate distill_token_ids
- and distill_logprobs on ScoredDataGroup for OPD training.
- """
-
- name = "agentic-opd"
- env_config_cls = AgenticOPDConfig
-
- # Default toolsets: terminal for running code, file for writing it
- default_toolsets = ["terminal", "file"]
-
- @classmethod
- def config_init(cls) -> Tuple[AgenticOPDConfig, List[APIServerConfig]]:
- """Default configuration."""
- env_config = AgenticOPDConfig(
- # Toolsets
- enabled_toolsets=["terminal", "file"],
- # Agent loop
- max_agent_turns=15,
- agent_temperature=1.0,
- system_prompt=(
- "You are a skilled Python programmer. When given a coding task:\n"
- "1. Write the solution to a file called 'solution.py'\n"
- "2. Write the test code to a file called 'test_solution.py'\n"
- "3. Run the tests with: python test_solution.py\n"
- "4. If tests fail, read the error output carefully, fix your code, and re-run\n"
- "5. Once all tests pass, report success\n\n"
- "Be efficient — write clean code and fix errors methodically."
- ),
- # OPD
- opd_enabled=True,
- distill_topk=50,
- prm_votes=3,
- # Training
- group_size=4,
- total_steps=500,
- steps_per_eval=50,
- use_wandb=True,
- wandb_name="agentic-opd",
- )
-
- server_configs = [
- APIServerConfig(
- base_url="http://localhost:8000/v1",
- model_name="Qwen/Qwen3-4B",
- server_type="vllm",
- )
- ]
-
- return env_config, server_configs
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self._items: list[dict] = []
- self._eval_items: list[dict] = []
- self._index: int = 0
-
- # Metric buffers
- self._reward_buffer: list[float] = []
- self._correctness_buffer: list[float] = []
- self._efficiency_buffer: list[float] = []
- self._tool_usage_buffer: list[float] = []
- self._hints_extracted_buffer: list[int] = []
- self._opd_turns_scored_buffer: list[int] = []
-
- # ═══════════════════════════════════════════════════════════════════
- # 1. setup — load dataset
- # ═══════════════════════════════════════════════════════════════════
-
- async def setup(self) -> None:
- """Load coding tasks from HuggingFace or use built-in set."""
- if self.config.dataset_name:
- try:
- from datasets import load_dataset
-
- logger.info(
- "Loading dataset '%s'...", self.config.dataset_name
- )
- ds = load_dataset(
- self.config.dataset_name, split=self.config.dataset_split
- )
- task_field = self.config.prompt_field
- self._items = [
- {
- "task": row.get(task_field, row.get("task", "")),
- "test_code": row.get("test_code", row.get("tests", "")),
- "difficulty": row.get("difficulty", "unknown"),
- }
- for row in ds
- if row.get(task_field, row.get("task", ""))
- ]
- if self._items:
- random.shuffle(self._items)
- eval_size = max(
- self.config.eval_size,
- int(len(self._items) * self.config.eval_split_ratio),
- )
- self._eval_items = self._items[:eval_size]
- self._items = self._items[eval_size:]
- logger.info(
- "Loaded %d train / %d eval items from '%s'",
- len(self._items),
- len(self._eval_items),
- self.config.dataset_name,
- )
- return
- except Exception as e:
- logger.warning(
- "Could not load dataset '%s': %s. Using built-in tasks.",
- self.config.dataset_name,
- e,
- )
-
- # Fallback to built-in tasks
- items = copy.deepcopy(BUILTIN_CODING_TASKS)
- random.shuffle(items)
- split = max(1, len(items) * 85 // 100)
- self._items = items[:split]
- self._eval_items = items[split:]
- logger.info(
- "Using built-in coding tasks: %d train / %d eval items",
- len(self._items),
- len(self._eval_items),
- )
-
- # ═══════════════════════════════════════════════════════════════════
- # 2. get_next_item
- # ═══════════════════════════════════════════════════════════════════
-
- async def get_next_item(self) -> dict:
- """Return the next coding task, cycling through the dataset."""
- if not self._items:
- raise RuntimeError("Dataset is empty. Did you call setup()?")
- item = self._items[self._index % len(self._items)]
- self._index += 1
- return item
-
- # ═══════════════════════════════════════════════════════════════════
- # 3. format_prompt
- # ═══════════════════════════════════════════════════════════════════
-
- def format_prompt(self, item: dict) -> str:
- """Format the coding task as a user prompt."""
- prompt = (
- f"Solve the following coding task.\n\n"
- f"## Task\n{item['task']}\n\n"
- )
- if item.get("test_code"):
- prompt += (
- f"## Tests\nThe following test code will be used to verify your solution:\n"
- f"```python\n{item['test_code']}```\n\n"
- )
- prompt += (
- "## Instructions\n"
- "1. Write your solution to `solution.py`\n"
- "2. Write the test code to `test_solution.py`\n"
- "3. Run `python test_solution.py` to verify\n"
- "4. Fix any failures and re-run until all tests pass\n"
- )
- return prompt
-
- # ═══════════════════════════════════════════════════════════════════
- # 4. compute_reward
- # ═══════════════════════════════════════════════════════════════════
-
- async def compute_reward(
- self,
- item: dict,
- result: AgentResult,
- ctx: ToolContext,
- ) -> float:
- """
- Multi-signal reward:
- - correctness (0.7): Did the tests pass?
- - efficiency (0.15): Fewer turns = better
- - tool_usage (0.15): Did the agent actually write + run code?
- """
- cfg = self.config
-
- # ---- Signal 1: Test correctness ----
- # Check if test_solution.py exists and passes in the agent's sandbox
- correctness = 0.0
- try:
- test_result = ctx.terminal("python test_solution.py 2>&1", timeout=30)
- output = test_result.get("output", "")
- exit_code = test_result.get("exit_code", 1)
- if exit_code == 0 and "passed" in output.lower():
- correctness = 1.0
- elif exit_code == 0:
- correctness = 0.8 # Ran without error but no explicit "passed"
- elif "assert" in output.lower() and "error" in output.lower():
- correctness = 0.2 # Partial — code runs but assertions fail
- else:
- correctness = 0.1 # Code errors out entirely
- except Exception as e:
- logger.debug("Test execution failed in reward: %s", e)
- correctness = 0.0
-
- # ---- Signal 2: Efficiency ----
- max_turns = cfg.max_agent_turns
- turns_used = result.turns_used
- if turns_used <= 3:
- efficiency = 1.0
- elif turns_used <= max_turns // 2:
- efficiency = 0.8
- elif turns_used <= max_turns * 3 // 4:
- efficiency = 0.5
- else:
- efficiency = 0.2
-
- # ---- Signal 3: Tool usage ----
- tools_used = set()
- for msg in result.messages:
- if msg.get("role") == "assistant" and msg.get("tool_calls"):
- for tc in msg["tool_calls"]:
- fn = tc.get("function", {}) if isinstance(tc, dict) else {}
- name = fn.get("name", "")
- if name:
- tools_used.add(name)
-
- # Good: used both terminal and file tools
- if "terminal" in tools_used and ("write_file" in tools_used or "patch" in tools_used):
- tool_usage = 1.0
- elif "terminal" in tools_used:
- tool_usage = 0.6
- elif tools_used:
- tool_usage = 0.3
- else:
- tool_usage = 0.0
-
- # ---- Combine ----
- reward = (
- cfg.correctness_weight * correctness
- + cfg.efficiency_weight * efficiency
- + cfg.tool_usage_weight * tool_usage
- )
- reward = min(1.0, max(0.0, reward))
-
- # Track metrics
- self._reward_buffer.append(reward)
- self._correctness_buffer.append(correctness)
- self._efficiency_buffer.append(efficiency)
- self._tool_usage_buffer.append(tool_usage)
-
- logger.debug(
- "Reward: correctness=%.2f, efficiency=%.2f, tool_usage=%.2f → %.3f",
- correctness,
- efficiency,
- tool_usage,
- reward,
- )
- return reward
-
- # ═══════════════════════════════════════════════════════════════════
- # 5. collect_trajectories — OPD pipeline
- # ═══════════════════════════════════════════════════════════════════
-
- async def collect_trajectories(
- self, item: Item
- ) -> Tuple[
- Union[Optional[ScoredDataGroup], List[Optional[ScoredDataGroup]]],
- List[Item],
- ]:
- """
- Override collect_trajectories to add the OPD pipeline.
-
- 1. Run standard rollouts via super() → ScoredDataGroup with tokens/masks/scores
- 2. For each rollout, extract hints from next-state signals
- 3. Score student tokens under enhanced (hint-augmented) distribution
- 4. Add distill_token_ids / distill_logprobs to the ScoredDataGroup
- """
- # Step 1: Run standard rollouts
- scored_group, backlog = await super().collect_trajectories(item)
-
- # Step 2: OPD pipeline (only if enabled and we have VLLM server)
- if (
- self.config.opd_enabled
- and scored_group is not None
- and isinstance(scored_group, dict)
- and self._use_managed_server()
- ):
- await self._apply_opd_pipeline(scored_group)
-
- return scored_group, backlog
-
- async def _apply_opd_pipeline(self, group: ScoredDataGroup) -> None:
- """
- Apply on-policy distillation to each rollout in the group.
-
- For each rollout's messages:
- 1. Find (assistant, next_state) turn pairs
- 2. Extract hints via LLM judge with majority voting
- 3. Build enhanced prompt (original + hint)
- 4. Score student tokens under enhanced distribution via get_logprobs
- 5. Add distill_token_ids / distill_logprobs to the group
- """
- messages_list = group.get("messages", [])
- tokens_list = group.get("tokens", [])
-
- if not messages_list or not tokens_list:
- logger.debug("OPD: No messages or tokens to process")
- return
-
- all_distill_token_ids: List[Optional[List[List[int]]]] = []
- all_distill_logprobs: List[Optional[List[List[float]]]] = []
-
- for seq_idx, (messages, student_tokens) in enumerate(
- zip(messages_list, tokens_list)
- ):
- try:
- distill_ids, distill_lps = await self._opd_for_sequence(
- messages, student_tokens
- )
- all_distill_token_ids.append(distill_ids)
- all_distill_logprobs.append(distill_lps)
- except Exception as e:
- logger.warning(
- "OPD failed for sequence %d: %s", seq_idx, e
- )
- all_distill_token_ids.append(None)
- all_distill_logprobs.append(None)
-
- # Only set distill fields if at least one sequence succeeded
- any_succeeded = any(d is not None for d in all_distill_token_ids)
- if any_succeeded:
- # Replace None entries with zero-padded arrays matching token length
- for i in range(len(all_distill_token_ids)):
- if all_distill_token_ids[i] is None and i < len(tokens_list):
- seq_len = len(tokens_list[i])
- k = self.config.distill_topk
- all_distill_token_ids[i] = [[0] * k] * seq_len
- all_distill_logprobs[i] = [[0.0] * k] * seq_len
-
- group["distill_token_ids"] = all_distill_token_ids
- group["distill_logprobs"] = all_distill_logprobs
- logger.info(
- "OPD: Set distill fields on %d/%d sequences",
- sum(1 for d in all_distill_token_ids if d is not None),
- len(all_distill_token_ids),
- )
-
- async def _opd_for_sequence(
- self, messages: List[Dict], student_tokens: List[int]
- ) -> Tuple[List[List[int]], List[List[float]]]:
- """
- Run OPD for a single rollout sequence.
-
- 1. Walk conversation to find (assistant, next_state) pairs
- 2. Extract hints from next-state signals
- 3. For each hint-augmented turn, score student tokens via get_logprobs
- 4. Merge per-turn teacher logprobs into a full-sequence distill array
-
- Returns:
- (distill_token_ids, distill_logprobs) each of shape [seq_len][top_k]
- """
- k = self.config.distill_topk
- seq_len = len(student_tokens)
-
- # Initialize with zeros (no distill info = neutral)
- distill_token_ids: List[List[int]] = [[0] * k for _ in range(seq_len)]
- distill_logprobs: List[List[float]] = [[0.0] * k for _ in range(seq_len)]
-
- # Find (assistant, next_state) turn pairs
- turn_pairs = self._extract_turn_pairs(messages)
- if not turn_pairs:
- return distill_token_ids, distill_logprobs
-
- hints_extracted = 0
- turns_scored = 0
-
- for pair in turn_pairs:
- try:
- hint = await self._extract_hint(
- pair["assistant_text"],
- pair["next_state_text"],
- pair["next_state_role"],
- )
- if not hint:
- continue
-
- hints_extracted += 1
-
- # Build enhanced prompt with hint
- enhanced_messages = _append_hint_to_messages(
- pair["context_messages"], hint
- )
-
- # Tokenize the enhanced prompt
- if not self.tokenizer:
- logger.warning("OPD: No tokenizer available, skipping scoring")
- continue
-
- enhanced_prompt = self.tokenizer.apply_chat_template(
- enhanced_messages,
- tokenize=False,
- add_generation_prompt=True,
- )
-
- # Tokenize the assistant response to score
- response_text = pair["assistant_text"]
- enhanced_full_text = enhanced_prompt + response_text
- enhanced_ids = self.tokenizer(
- enhanced_full_text, add_special_tokens=False
- )["input_ids"]
-
- response_ids = self.tokenizer(
- response_text, add_special_tokens=False
- )["input_ids"]
- response_len = len(response_ids)
-
- if response_len == 0:
- continue
-
- # Score via get_logprobs — teacher scoring the student's tokens
- # under the enhanced (hint-augmented) distribution
- try:
- logprob_result = await self.server.get_logprobs(
- input_ids=enhanced_ids,
- top_k=k,
- split="eval", # Use eval semaphore to not block training
- )
- except Exception as e:
- logger.debug("get_logprobs failed: %s", e)
- continue
-
- teacher_topk_ids = logprob_result.get("prompt_topk_token_ids", [])
- teacher_topk_lps = logprob_result.get("prompt_topk_logprobs", [])
-
- if not teacher_topk_ids:
- continue
-
- # Extract only the response positions (last response_len entries)
- if len(teacher_topk_ids) >= response_len:
- resp_topk_ids = teacher_topk_ids[-response_len:]
- resp_topk_lps = teacher_topk_lps[-response_len:]
- else:
- # Pad from the left if the response was shorter than expected
- pad_len = response_len - len(teacher_topk_ids)
- resp_topk_ids = [[0] * k] * pad_len + teacher_topk_ids
- resp_topk_lps = [[0.0] * k] * pad_len + teacher_topk_lps
-
- # Map these back to the student's full sequence positions
- # Find where this assistant turn's tokens appear in the full sequence
- turn_start = self._find_token_span(
- student_tokens, response_ids
- )
- if turn_start is not None:
- for j in range(min(response_len, seq_len - turn_start)):
- pos = turn_start + j
- if pos < seq_len and j < len(resp_topk_ids):
- # Pad/truncate to exactly k entries
- ids = resp_topk_ids[j][:k]
- lps = resp_topk_lps[j][:k]
- while len(ids) < k:
- ids.append(0)
- lps.append(0.0)
- distill_token_ids[pos] = ids
- distill_logprobs[pos] = lps
- turns_scored += 1
-
- except Exception as e:
- logger.debug("OPD turn processing failed: %s", e)
- continue
-
- # Track OPD metrics
- self._hints_extracted_buffer.append(hints_extracted)
- self._opd_turns_scored_buffer.append(turns_scored)
-
- logger.debug(
- "OPD sequence: %d turn pairs, %d hints extracted, %d turns scored",
- len(turn_pairs),
- hints_extracted,
- turns_scored,
- )
- return distill_token_ids, distill_logprobs
-
- def _extract_turn_pairs(
- self, messages: List[Dict]
- ) -> List[Dict[str, Any]]:
- """
- Walk conversation messages to find (assistant, next_state) pairs.
-
- A "turn pair" is an assistant message with content (the response)
- followed by one or more tool results or a user reply (the next state).
-
- Returns list of dicts:
- {
- "context_messages": messages up to (not including) the assistant turn,
- "assistant_text": the assistant's response text,
- "next_state_text": the next state content (tool result or user reply),
- "next_state_role": "tool" or "user",
- }
- """
- pairs = []
- i = 0
- while i < len(messages):
- msg = messages[i]
- if msg.get("role") == "assistant" and msg.get("content"):
- # Found an assistant message with content
- assistant_text = msg["content"]
- context = messages[:i] # Everything before this turn
-
- # Look ahead for next state
- j = i + 1
- # Skip tool_calls-only assistant messages and collect tool results
- next_states = []
- while j < len(messages):
- next_msg = messages[j]
- if next_msg.get("role") == "tool":
- next_states.append(next_msg)
- j += 1
- elif next_msg.get("role") == "user":
- next_states.append(next_msg)
- break
- else:
- break
-
- if next_states:
- # Combine all next-state content
- next_text_parts = []
- next_role = next_states[0].get("role", "tool")
- for ns in next_states:
- content = ns.get("content", "")
- if content:
- # Truncate very long tool outputs
- max_chars = self.config.hint_max_next_state_chars
- if len(content) > max_chars:
- content = content[:max_chars] + "\n...[truncated]"
- next_text_parts.append(content)
-
- next_text = "\n---\n".join(next_text_parts)
- if next_text.strip():
- pairs.append(
- {
- "context_messages": context,
- "assistant_text": assistant_text,
- "next_state_text": next_text,
- "next_state_role": next_role,
- }
- )
- i += 1
- return pairs
-
- async def _extract_hint(
- self,
- assistant_text: str,
- next_state_text: str,
- next_state_role: str,
- ) -> Optional[str]:
- """
- Extract a hindsight hint from a next-state signal using majority-voted LLM judge.
-
- Returns the hint string if the judge votes positively, None otherwise.
- """
- judge_messages = _build_hint_judge_messages(
- response_text=assistant_text,
- next_state_text=next_state_text,
- next_state_role=next_state_role,
- )
-
- # Majority voting across multiple judge queries
- votes = []
- tasks = []
- for _ in range(self.config.prm_votes):
- tasks.append(
- self.server.chat_completion(
- messages=judge_messages,
- n=1,
- max_tokens=500,
- temperature=0.7,
- split="eval",
- )
- )
-
- results = await asyncio.gather(*tasks, return_exceptions=True)
-
- for result in results:
- if isinstance(result, Exception):
- logger.debug("Hint judge call failed: %s", result)
- votes.append({"score": None, "hint": ""})
- continue
- try:
- text = result.choices[0].message.content or ""
- score, hint = _parse_hint_result(text)
- votes.append({"score": score, "hint": hint})
- except Exception as e:
- logger.debug("Hint parse failed: %s", e)
- votes.append({"score": None, "hint": ""})
-
- selected = _select_best_hint(votes)
- if selected is None:
- return None
- return selected["hint"]
-
- @staticmethod
- def _find_token_span(
- full_tokens: List[int], sub_tokens: List[int]
- ) -> Optional[int]:
- """
- Find where sub_tokens appears in full_tokens.
- Returns the start index, or None if not found.
-
- Uses a sliding window search. For long sequences, searches
- from the end since assistant responses are typically at the end.
- """
- if not sub_tokens or not full_tokens:
- return None
- sub_len = len(sub_tokens)
- full_len = len(full_tokens)
- if sub_len > full_len:
- return None
-
- # Search backwards (assistant responses are usually near the end)
- for i in range(full_len - sub_len, -1, -1):
- if full_tokens[i : i + sub_len] == sub_tokens:
- return i
- return None
-
- # ═══════════════════════════════════════════════════════════════════
- # 6. evaluate
- # ═══════════════════════════════════════════════════════════════════
-
- async def evaluate(self, *args, **kwargs) -> None:
- """
- Evaluate on held-out coding tasks using the full agent loop.
- No OPD during eval — just standard agentic evaluation.
- """
- if not self._eval_items:
- logger.warning("No eval items available.")
- return
-
- eval_size = min(self.config.eval_size, len(self._eval_items))
- eval_items = self._eval_items[:eval_size]
-
- logger.info("Running eval on %d coding tasks...", len(eval_items))
- start_time = time.time()
- samples = []
-
- tools, valid_names = self._resolve_tools_for_group()
-
- for i, item in enumerate(eval_items):
- task_id = str(uuid.uuid4())
- logger.info(
- "Eval [%d/%d]: %s...", i + 1, len(eval_items), item["task"][:60]
- )
-
- try:
- messages: List[Dict[str, Any]] = []
- if self.config.system_prompt:
- messages.append(
- {"role": "system", "content": self.config.system_prompt}
- )
- messages.append(
- {"role": "user", "content": self.format_prompt(item)}
- )
-
- agent = HermesAgentLoop(
- server=self.server,
- tool_schemas=tools,
- valid_tool_names=valid_names,
- max_turns=self.config.max_agent_turns,
- task_id=task_id,
- temperature=0.0,
- max_tokens=self.config.max_token_length,
- extra_body=self.config.extra_body,
- budget_config=self.config.build_budget_config(),
- )
- result = await agent.run(messages)
-
- # Compute reward (track buffer lengths to rollback eval pollution)
- buf_len = len(self._correctness_buffer)
- ctx = ToolContext(task_id)
- try:
- reward = await self.compute_reward(item, result, ctx)
- finally:
- ctx.cleanup()
-
- # Extract correctness and rollback training buffers
- correctness = (
- self._correctness_buffer[buf_len]
- if len(self._correctness_buffer) > buf_len
- else 0.0
- )
- for buf in (
- self._reward_buffer,
- self._correctness_buffer,
- self._efficiency_buffer,
- self._tool_usage_buffer,
- ):
- if len(buf) > buf_len:
- buf.pop()
-
- # Also rollback OPD buffers if they were touched
- for buf in (
- self._hints_extracted_buffer,
- self._opd_turns_scored_buffer,
- ):
- if len(buf) > buf_len:
- buf.pop()
-
- # Extract final response
- final_response = ""
- for msg in reversed(result.messages):
- if (
- msg.get("role") == "assistant"
- and msg.get("content")
- and not final_response
- ):
- final_response = msg["content"]
- break
-
- samples.append(
- {
- "prompt": item["task"][:200],
- "response": final_response[:500],
- "correctness": correctness,
- "reward": reward,
- "turns": result.turns_used,
- }
- )
-
- logger.info(
- " → correctness=%.2f, reward=%.3f, turns=%d",
- correctness,
- reward,
- result.turns_used,
- )
-
- except Exception as e:
- logger.error("Eval error: %s", e)
- samples.append(
- {
- "prompt": item["task"][:200],
- "response": f"ERROR: {e}",
- "correctness": 0.0,
- "reward": 0.0,
- "turns": 0,
- }
- )
-
- end_time = time.time()
-
- correctness_scores = [s["correctness"] for s in samples]
- rewards = [s["reward"] for s in samples]
- n = len(samples)
-
- eval_metrics = {
- "eval/mean_correctness": sum(correctness_scores) / n if n else 0.0,
- "eval/mean_reward": sum(rewards) / n if n else 0.0,
- "eval/pass_rate": (
- sum(1 for c in correctness_scores if c >= 0.8) / n if n else 0.0
- ),
- "eval/n_items": n,
- }
-
- logger.info(
- "Eval complete — correctness=%.3f, reward=%.3f, pass_rate=%.0f%%",
- eval_metrics["eval/mean_correctness"],
- eval_metrics["eval/mean_reward"],
- eval_metrics["eval/pass_rate"] * 100,
- )
-
- await self.evaluate_log(
- metrics=eval_metrics,
- samples=samples,
- start_time=start_time,
- end_time=end_time,
- )
-
- # ═══════════════════════════════════════════════════════════════════
- # 7. wandb_log — custom OPD metrics
- # ═══════════════════════════════════════════════════════════════════
-
- async def wandb_log(self, wandb_metrics: Optional[Dict] = None) -> None:
- """Log reward breakdown and OPD-specific metrics to wandb."""
- if wandb_metrics is None:
- wandb_metrics = {}
-
- if self._reward_buffer:
- n = len(self._reward_buffer)
- wandb_metrics["train/mean_reward"] = sum(self._reward_buffer) / n
- wandb_metrics["train/mean_correctness"] = (
- sum(self._correctness_buffer) / n
- )
- wandb_metrics["train/mean_efficiency"] = (
- sum(self._efficiency_buffer) / n
- )
- wandb_metrics["train/mean_tool_usage"] = (
- sum(self._tool_usage_buffer) / n
- )
- wandb_metrics["train/pass_rate"] = (
- sum(1 for c in self._correctness_buffer if c >= 0.8) / n
- )
- wandb_metrics["train/total_rollouts"] = n
-
- self._reward_buffer.clear()
- self._correctness_buffer.clear()
- self._efficiency_buffer.clear()
- self._tool_usage_buffer.clear()
-
- # OPD-specific metrics
- if self._hints_extracted_buffer:
- n = len(self._hints_extracted_buffer)
- wandb_metrics["opd/mean_hints_per_rollout"] = (
- sum(self._hints_extracted_buffer) / n
- )
- wandb_metrics["opd/mean_turns_scored"] = (
- sum(self._opd_turns_scored_buffer) / n
- )
- wandb_metrics["opd/hint_rate"] = (
- sum(1 for h in self._hints_extracted_buffer if h > 0) / n
- )
- wandb_metrics["opd/total_hints"] = sum(self._hints_extracted_buffer)
- wandb_metrics["opd/total_scored_turns"] = sum(
- self._opd_turns_scored_buffer
- )
-
- self._hints_extracted_buffer.clear()
- self._opd_turns_scored_buffer.clear()
-
- await super().wandb_log(wandb_metrics)
-
-
-# ═══════════════════════════════════════════════════════════════════════
-# Entry point
-# ═══════════════════════════════════════════════════════════════════════
-
-if __name__ == "__main__":
- AgenticOPDEnv.cli()
diff --git a/environments/benchmarks/__init__.py b/environments/benchmarks/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/environments/benchmarks/tblite/README.md b/environments/benchmarks/tblite/README.md
deleted file mode 100644
index 54b3745c383..00000000000
--- a/environments/benchmarks/tblite/README.md
+++ /dev/null
@@ -1,73 +0,0 @@
-# OpenThoughts-TBLite Evaluation Environment
-
-This environment evaluates terminal agents on the [OpenThoughts-TBLite](https://huggingface.co/datasets/open-thoughts/OpenThoughts-TBLite) benchmark, a difficulty-calibrated subset of [Terminal-Bench 2.0](https://www.tbench.ai/leaderboard/terminal-bench/2.0).
-
-## Source
-
-OpenThoughts-TBLite was created by the [OpenThoughts](https://www.openthoughts.ai/) Agent team in collaboration with [Snorkel AI](https://snorkel.ai/) and [Bespoke Labs](https://bespokelabs.ai/). The original dataset and documentation live at:
-
-- **Dataset (source):** [open-thoughts/OpenThoughts-TBLite](https://huggingface.co/datasets/open-thoughts/OpenThoughts-TBLite)
-- **GitHub:** [open-thoughts/OpenThoughts-TBLite](https://github.com/open-thoughts/OpenThoughts-TBLite)
-- **Blog post:** [openthoughts.ai/blog/openthoughts-tblite](https://www.openthoughts.ai/blog/openthoughts-tblite)
-
-## Our Dataset
-
-We converted the source into the same schema used by our Terminal-Bench 2.0 environment (pre-built Docker Hub images, base64-encoded test tarballs, etc.) and published it as:
-
-- **Dataset (ours):** [NousResearch/openthoughts-tblite](https://huggingface.co/datasets/NousResearch/openthoughts-tblite)
-- **Docker images:** `nousresearch/tblite-:latest` on Docker Hub (100 images)
-
-The conversion script is at `scripts/prepare_tblite_dataset.py`.
-
-## Why TBLite?
-
-Terminal-Bench 2.0 is one of the strongest frontier evaluations for terminal agents, but when a model scores near the floor (e.g., Qwen 3 8B at <1%), many changes look identical in aggregate score. TBLite addresses this by calibrating task difficulty using Claude Haiku 4.5 as a reference:
-
-| Difficulty | Pass Rate Range | Tasks |
-|------------|----------------|-------|
-| Easy | >= 70% | 40 |
-| Medium | 40-69% | 26 |
-| Hard | 10-39% | 26 |
-| Extreme | < 10% | 8 |
-
-This gives enough solvable tasks to detect small improvements quickly, while preserving enough hard tasks to avoid saturation. The correlation between TBLite and TB2 scores is **r = 0.911**.
-
-TBLite also runs 2.6-8x faster than the full TB2, making it practical for iteration loops.
-
-## Usage
-
-```bash
-# Run the full benchmark
-python environments/benchmarks/tblite/tblite_env.py evaluate
-
-# Filter to specific tasks
-python environments/benchmarks/tblite/tblite_env.py evaluate \
- --env.task_filter "broken-python,pandas-etl"
-
-# Use a different model
-python environments/benchmarks/tblite/tblite_env.py evaluate \
- --server.model_name "qwen/qwen3-30b"
-```
-
-## Architecture
-
-`TBLiteEvalEnv` is a thin subclass of `TerminalBench2EvalEnv`. All evaluation logic (agent loop, Docker sandbox management, test verification, metrics) is inherited. Only the defaults differ:
-
-| Setting | TB2 | TBLite |
-|----------------|----------------------------------|-----------------------------------------|
-| Dataset | `NousResearch/terminal-bench-2` | `NousResearch/openthoughts-tblite` |
-| Tasks | 89 | 100 |
-| Task timeout | 1800s (30 min) | 1200s (20 min) |
-| Wandb name | `terminal-bench-2` | `openthoughts-tblite` |
-
-## Citation
-
-```bibtex
-@software{OpenThoughts-TBLite,
- author = {OpenThoughts-Agent team, Snorkel AI, Bespoke Labs},
- month = Feb,
- title = {{OpenThoughts-TBLite: A High-Signal Benchmark for Iterating on Terminal Agents}},
- howpublished = {https://www.openthoughts.ai/blog/openthoughts-tblite},
- year = {2026}
-}
-```
diff --git a/environments/benchmarks/tblite/__init__.py b/environments/benchmarks/tblite/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/environments/benchmarks/tblite/default.yaml b/environments/benchmarks/tblite/default.yaml
deleted file mode 100644
index cb521828061..00000000000
--- a/environments/benchmarks/tblite/default.yaml
+++ /dev/null
@@ -1,39 +0,0 @@
-# OpenThoughts-TBLite Evaluation -- Default Configuration
-#
-# Eval-only environment for the TBLite benchmark (100 difficulty-calibrated
-# terminal tasks, a faster proxy for Terminal-Bench 2.0).
-# Uses Modal terminal backend for per-task cloud-isolated sandboxes
-# and OpenRouter for inference.
-#
-# Usage:
-# python environments/benchmarks/tblite/tblite_env.py evaluate \
-# --config environments/benchmarks/tblite/default.yaml
-#
-# # Override model:
-# python environments/benchmarks/tblite/tblite_env.py evaluate \
-# --config environments/benchmarks/tblite/default.yaml \
-# --openai.model_name anthropic/claude-sonnet-4
-
-env:
- enabled_toolsets: ["terminal", "file"]
- max_agent_turns: 60
- max_token_length: 32000
- agent_temperature: 0.8
- terminal_backend: "modal"
- terminal_timeout: 300 # 5 min per command (builds, pip install)
- tool_pool_size: 128 # thread pool for 100 parallel tasks
- dataset_name: "NousResearch/openthoughts-tblite"
- test_timeout: 600
- task_timeout: 1200 # 20 min wall-clock per task (TBLite tasks are faster)
- tokenizer_name: "NousResearch/Hermes-3-Llama-3.1-8B"
- use_wandb: true
- wandb_name: "openthoughts-tblite"
- ensure_scores_are_not_same: false
- data_dir_to_save_evals: "environments/benchmarks/evals/openthoughts-tblite"
-
-openai:
- base_url: "https://openrouter.ai/api/v1"
- model_name: "anthropic/claude-opus-4.6"
- server_type: "openai"
- health_check: false
- # api_key loaded from OPENROUTER_API_KEY in .env
diff --git a/environments/benchmarks/tblite/local.yaml b/environments/benchmarks/tblite/local.yaml
deleted file mode 100644
index 35d4b896869..00000000000
--- a/environments/benchmarks/tblite/local.yaml
+++ /dev/null
@@ -1,38 +0,0 @@
-# OpenThoughts-TBLite Evaluation -- Docker Backend (Local Compute)
-#
-# Runs tasks in Docker containers on the local machine.
-# Sandboxed like Modal but no cloud costs. Good for dev/testing.
-#
-# Usage:
-# python environments/benchmarks/tblite/tblite_env.py evaluate \
-# --config environments/benchmarks/tblite/local.yaml
-#
-# # Override concurrency:
-# python environments/benchmarks/tblite/tblite_env.py evaluate \
-# --config environments/benchmarks/tblite/local.yaml \
-# --env.eval_concurrency 4
-
-env:
- enabled_toolsets: ["terminal", "file"]
- max_agent_turns: 60
- max_token_length: 32000
- agent_temperature: 0.8
- terminal_backend: "docker"
- terminal_timeout: 300
- tool_pool_size: 16
- dataset_name: "NousResearch/openthoughts-tblite"
- test_timeout: 600
- task_timeout: 1200
- eval_concurrency: 8 # max 8 tasks at once
- tokenizer_name: "NousResearch/Hermes-3-Llama-3.1-8B"
- use_wandb: false
- wandb_name: "openthoughts-tblite-local"
- ensure_scores_are_not_same: false
- data_dir_to_save_evals: "environments/benchmarks/evals/openthoughts-tblite-local"
-
-openai:
- base_url: "https://openrouter.ai/api/v1"
- model_name: "anthropic/claude-sonnet-4"
- server_type: "openai"
- health_check: false
- # api_key loaded from OPENROUTER_API_KEY in .env
diff --git a/environments/benchmarks/tblite/local_vllm.yaml b/environments/benchmarks/tblite/local_vllm.yaml
deleted file mode 100644
index 17689ba1d35..00000000000
--- a/environments/benchmarks/tblite/local_vllm.yaml
+++ /dev/null
@@ -1,40 +0,0 @@
-# OpenThoughts-TBLite Evaluation -- Local vLLM Backend
-#
-# Runs against a local vLLM server with Docker sandboxes.
-#
-# Start the vLLM server from the atropos directory:
-# python -m example_trainer.vllm_api_server \
-# --model Qwen/Qwen3-4B-Instruct-2507 \
-# --port 9001 \
-# --gpu-memory-utilization 0.8 \
-# --max-model-len=32000
-#
-# Then run:
-# python environments/benchmarks/tblite/tblite_env.py evaluate \
-# --config environments/benchmarks/tblite/local_vllm.yaml
-
-env:
- enabled_toolsets: ["terminal", "file"]
- max_agent_turns: 60
- max_token_length: 16000
- agent_temperature: 0.6
- terminal_backend: "docker"
- terminal_timeout: 300
- tool_pool_size: 16
- dataset_name: "NousResearch/openthoughts-tblite"
- test_timeout: 600
- task_timeout: 1200
- eval_concurrency: 8
- tool_call_parser: "hermes"
- system_prompt: "You are an expert terminal agent. You MUST use the provided tools to complete tasks. Use the terminal tool to run shell commands, read_file to read files, write_file to write files, search_files to search, and patch to edit files. Do NOT write out solutions as text - execute them using the tools. Always start by exploring the environment with terminal commands."
- tokenizer_name: "Qwen/Qwen3-4B-Instruct-2507"
- use_wandb: false
- wandb_name: "tblite-qwen3-4b-instruct"
- ensure_scores_are_not_same: false
- data_dir_to_save_evals: "environments/benchmarks/evals/tblite-qwen3-4b-local"
-
-openai:
- base_url: "http://localhost:9001"
- model_name: "Qwen/Qwen3-4B-Instruct-2507"
- server_type: "vllm"
- health_check: false
diff --git a/environments/benchmarks/tblite/run_eval.sh b/environments/benchmarks/tblite/run_eval.sh
deleted file mode 100755
index 9d860bf5ef7..00000000000
--- a/environments/benchmarks/tblite/run_eval.sh
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/bin/bash
-
-# OpenThoughts-TBLite Evaluation
-#
-# Run from repo root:
-# bash environments/benchmarks/tblite/run_eval.sh
-#
-# Override model:
-# bash environments/benchmarks/tblite/run_eval.sh \
-# --openai.model_name anthropic/claude-sonnet-4
-#
-# Run a subset:
-# bash environments/benchmarks/tblite/run_eval.sh \
-# --env.task_filter broken-python,pandas-etl
-#
-# All terminal settings (backend, timeout, lifetime, pool size) are
-# configured via env config fields -- no env vars needed.
-
-set -euo pipefail
-
-mkdir -p logs evals/openthoughts-tblite
-LOG_FILE="logs/tblite_$(date +%Y%m%d_%H%M%S).log"
-
-echo "OpenThoughts-TBLite Evaluation"
-echo "Log file: $LOG_FILE"
-echo ""
-
-# Unbuffered python output so logs are written in real-time
-export PYTHONUNBUFFERED=1
-
-# Show INFO-level agent loop timing (api/tool durations per turn)
-# These go to the log file; tqdm + [START]/[PASS]/[FAIL] go to terminal
-export LOGLEVEL=INFO
-
-python tblite_env.py evaluate \
- --config default.yaml \
- "$@" \
- 2>&1 | tee "$LOG_FILE"
-
-echo ""
-echo "Log saved to: $LOG_FILE"
-echo "Eval results: evals/openthoughts-tblite/"
diff --git a/environments/benchmarks/tblite/tblite_env.py b/environments/benchmarks/tblite/tblite_env.py
deleted file mode 100644
index 4b23f9cc558..00000000000
--- a/environments/benchmarks/tblite/tblite_env.py
+++ /dev/null
@@ -1,119 +0,0 @@
-"""
-OpenThoughts-TBLite Evaluation Environment
-
-A lighter, faster alternative to Terminal-Bench 2.0 for iterating on terminal
-agents. Uses the same evaluation logic as TerminalBench2EvalEnv but defaults
-to the NousResearch/openthoughts-tblite dataset (100 difficulty-calibrated
-tasks vs TB2's 89 harder tasks).
-
-TBLite tasks are a curated subset of TB2 with a difficulty distribution
-designed to give meaningful signal even for smaller models:
- - Easy (40 tasks): >= 70% pass rate with Claude Haiku 4.5
- - Medium (26 tasks): 40-69% pass rate
- - Hard (26 tasks): 10-39% pass rate
- - Extreme (8 tasks): < 10% pass rate
-
-Usage:
- python environments/benchmarks/tblite/tblite_env.py evaluate
-
- # Filter to specific tasks:
- python environments/benchmarks/tblite/tblite_env.py evaluate \\
- --env.task_filter "broken-python,pandas-etl"
-"""
-
-import os
-import sys
-from pathlib import Path
-from typing import List, Tuple
-
-_repo_root = Path(__file__).resolve().parent.parent.parent.parent
-if str(_repo_root) not in sys.path:
- sys.path.insert(0, str(_repo_root))
-
-from pydantic import Field
-
-from atroposlib.envs.base import EvalHandlingEnum
-from atroposlib.envs.server_handling.server_manager import APIServerConfig
-
-from environments.benchmarks.terminalbench_2.terminalbench2_env import (
- TerminalBench2EvalConfig,
- TerminalBench2EvalEnv,
-)
-
-
-class TBLiteEvalConfig(TerminalBench2EvalConfig):
- """Configuration for the OpenThoughts-TBLite evaluation environment.
-
- Inherits all TB2 config fields. Only the dataset default and task timeout
- differ -- TBLite tasks are calibrated to be faster.
- """
-
- dataset_name: str = Field(
- default="NousResearch/openthoughts-tblite",
- description="HuggingFace dataset containing TBLite tasks.",
- )
-
- task_timeout: int = Field(
- default=1200,
- description="Maximum wall-clock seconds per task. TBLite tasks are "
- "generally faster than TB2, so 20 minutes is usually sufficient.",
- )
-
-
-class TBLiteEvalEnv(TerminalBench2EvalEnv):
- """OpenThoughts-TBLite evaluation environment.
-
- Inherits all evaluation logic from TerminalBench2EvalEnv (agent loop,
- test verification, Docker image resolution, metrics, wandb logging).
- Only the default configuration differs.
- """
-
- name = "openthoughts-tblite"
- env_config_cls = TBLiteEvalConfig
-
- @classmethod
- def config_init(cls) -> Tuple[TBLiteEvalConfig, List[APIServerConfig]]:
- env_config = TBLiteEvalConfig(
- enabled_toolsets=["terminal", "file"],
- disabled_toolsets=None,
- distribution=None,
-
- max_agent_turns=60,
- max_token_length=16000,
- agent_temperature=0.6,
- system_prompt=None,
-
- terminal_backend="modal",
- terminal_timeout=300,
-
- test_timeout=180,
-
- # 100 tasks in parallel
- tool_pool_size=128,
-
- eval_handling=EvalHandlingEnum.STOP_TRAIN,
- group_size=1,
- steps_per_eval=1,
- total_steps=1,
-
- tokenizer_name="NousResearch/Hermes-3-Llama-3.1-8B",
- use_wandb=True,
- wandb_name="openthoughts-tblite",
- ensure_scores_are_not_same=False,
- )
-
- server_configs = [
- APIServerConfig(
- base_url="https://openrouter.ai/api/v1",
- model_name="anthropic/claude-sonnet-4",
- server_type="openai",
- api_key=os.getenv("OPENROUTER_API_KEY", ""),
- health_check=False,
- )
- ]
-
- return env_config, server_configs
-
-
-if __name__ == "__main__":
- TBLiteEvalEnv.cli()
diff --git a/environments/benchmarks/terminalbench_2/__init__.py b/environments/benchmarks/terminalbench_2/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/environments/benchmarks/terminalbench_2/default.yaml b/environments/benchmarks/terminalbench_2/default.yaml
deleted file mode 100644
index eb675b12e70..00000000000
--- a/environments/benchmarks/terminalbench_2/default.yaml
+++ /dev/null
@@ -1,42 +0,0 @@
-# Terminal-Bench 2.0 Evaluation -- Default Configuration
-#
-# Eval-only environment for the TB2 benchmark (89 terminal tasks).
-# Uses Modal terminal backend for per-task cloud-isolated sandboxes
-# and OpenRouter for inference.
-#
-# Usage:
-# python environments/benchmarks/terminalbench_2/terminalbench2_env.py evaluate \
-# --config environments/benchmarks/terminalbench_2/default.yaml
-#
-# # Override model:
-# python environments/benchmarks/terminalbench_2/terminalbench2_env.py evaluate \
-# --config environments/benchmarks/terminalbench_2/default.yaml \
-# --openai.model_name anthropic/claude-sonnet-4
-
-env:
- enabled_toolsets: ["terminal", "file"]
- max_agent_turns: 60
- max_token_length: 32000
- agent_temperature: 0.8
- terminal_backend: "modal"
- terminal_timeout: 300 # 5 min per command (builds, pip install)
- tool_pool_size: 128 # thread pool for 89 parallel tasks
- dataset_name: "NousResearch/terminal-bench-2"
- test_timeout: 600
- task_timeout: 1800 # 30 min wall-clock per task, auto-FAIL if exceeded
- tokenizer_name: "NousResearch/Hermes-3-Llama-3.1-8B"
- use_wandb: true
- wandb_name: "terminal-bench-2"
- ensure_scores_are_not_same: false
- data_dir_to_save_evals: "environments/benchmarks/evals/terminal-bench-2"
- # CRITICAL: Limit concurrent Modal sandbox creations to avoid deadlocks.
- # Modal's blocking calls (App.lookup, etc.) deadlock when too many sandboxes
- # are created simultaneously inside thread pool workers via asyncio.run().
- max_concurrent_tasks: 8
-
-openai:
- base_url: "https://openrouter.ai/api/v1"
- model_name: "anthropic/claude-opus-4.6"
- server_type: "openai"
- health_check: false
- # api_key loaded from OPENROUTER_API_KEY in .env
diff --git a/environments/benchmarks/terminalbench_2/run_eval.sh b/environments/benchmarks/terminalbench_2/run_eval.sh
deleted file mode 100755
index ffbe4848065..00000000000
--- a/environments/benchmarks/terminalbench_2/run_eval.sh
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/bin/bash
-
-# Terminal-Bench 2.0 Evaluation
-#
-# Run from repo root:
-# bash environments/benchmarks/terminalbench_2/run_eval.sh
-#
-# Override model:
-# bash environments/benchmarks/terminalbench_2/run_eval.sh \
-# --openai.model_name anthropic/claude-sonnet-4
-#
-# Run a subset:
-# bash environments/benchmarks/terminalbench_2/run_eval.sh \
-# --env.task_filter fix-git,git-multibranch
-#
-# All terminal settings (backend, timeout, lifetime, pool size) are
-# configured via env config fields -- no env vars needed.
-
-set -euo pipefail
-
-mkdir -p logs evals/terminal-bench-2
-LOG_FILE="logs/terminalbench2_$(date +%Y%m%d_%H%M%S).log"
-
-echo "Terminal-Bench 2.0 Evaluation"
-echo "Log file: $LOG_FILE"
-echo ""
-
-# Unbuffered python output so logs are written in real-time
-export PYTHONUNBUFFERED=1
-
-# Show INFO-level agent loop timing (api/tool durations per turn)
-# These go to the log file; tqdm + [START]/[PASS]/[FAIL] go to terminal
-export LOGLEVEL=INFO
-
-python terminalbench2_env.py evaluate \
- --config default.yaml \
- "$@" \
- 2>&1 | tee "$LOG_FILE"
-
-echo ""
-echo "Log saved to: $LOG_FILE"
-echo "Eval results: evals/terminal-bench-2/"
diff --git a/environments/benchmarks/terminalbench_2/terminalbench2_env.py b/environments/benchmarks/terminalbench_2/terminalbench2_env.py
deleted file mode 100644
index 1a76b8da61e..00000000000
--- a/environments/benchmarks/terminalbench_2/terminalbench2_env.py
+++ /dev/null
@@ -1,1016 +0,0 @@
-"""
-TerminalBench2Env -- Terminal-Bench 2.0 Evaluation Environment
-
-Evaluates agentic LLMs on challenging terminal tasks from Terminal-Bench 2.0.
-Each task provides a unique Docker environment (pre-built on Docker Hub), a natural
-language instruction, and a test suite for verification. The agent uses terminal +
-file tools to complete the task, then the test suite runs inside the same sandbox.
-
-This is an eval-only environment (not a training environment). It is designed to
-be run via the `evaluate` subcommand:
-
- python environments/terminalbench2_env.py evaluate \\
- --env.dataset_name NousResearch/terminal-bench-2
-
-The evaluate flow:
- 1. setup() -- Loads the TB2 dataset from HuggingFace
- 2. evaluate() -- Iterates over all tasks, running each through:
- a. rollout_and_score_eval() -- Per-task agent loop + test verification
- - Resolves Docker image (pre-built Hub image or Dockerfile fallback)
- - Registers per-task Modal sandbox via register_task_env_overrides()
- - Runs the HermesAgentLoop (terminal + file tools)
- - Uploads test suite and runs test.sh in the same sandbox
- - Returns binary pass/fail result
- b. Aggregates per-task, per-category, and overall pass rates
- c. Logs results via evaluate_log() and wandb
-
-Key features:
- - Per-task Modal sandboxes using pre-built Docker Hub images
- - Binary reward: 1.0 if all tests pass, 0.0 otherwise
- - Concurrency-controlled parallel evaluation via asyncio.Semaphore
- - Per-task, per-category, and aggregate pass rate tracking
-"""
-
-import asyncio
-import base64
-import io
-import json
-import logging
-import os
-import shutil
-import sys
-import tarfile
-import tempfile
-import time
-import uuid
-from collections import defaultdict
-from pathlib import Path, PurePosixPath, PureWindowsPath
-from typing import Any, Dict, List, Optional, Tuple, Union
-
-# Ensure repo root is on sys.path for imports
-_repo_root = Path(__file__).resolve().parent.parent.parent.parent
-if str(_repo_root) not in sys.path:
- sys.path.insert(0, str(_repo_root))
-
-from pydantic import Field
-
-from atroposlib.envs.base import EvalHandlingEnum
-from atroposlib.envs.server_handling.server_manager import APIServerConfig
-
-from environments.agent_loop import AgentResult, HermesAgentLoop
-from environments.hermes_base_env import HermesAgentBaseEnv, HermesAgentEnvConfig
-from environments.tool_context import ToolContext
-from tools.terminal_tool import (
- register_task_env_overrides,
- clear_task_env_overrides,
- cleanup_vm,
-)
-
-logger = logging.getLogger(__name__)
-
-
-# =============================================================================
-# Configuration
-# =============================================================================
-
-class TerminalBench2EvalConfig(HermesAgentEnvConfig):
- """
- Configuration for the Terminal-Bench 2.0 evaluation environment.
-
- Extends HermesAgentEnvConfig with TB2-specific settings for dataset loading,
- test execution, task filtering, and eval concurrency.
- """
-
- # --- Dataset ---
- dataset_name: str = Field(
- default="NousResearch/terminal-bench-2",
- description="HuggingFace dataset containing TB2 tasks.",
- )
-
- # --- Test execution ---
- test_timeout: int = Field(
- default=180,
- description="Timeout in seconds for running the test suite after agent completes.",
- )
-
- # --- Image strategy ---
- force_build: bool = Field(
- default=False,
- description="If True, always build from Dockerfile (ignore docker_image). "
- "Useful for testing custom Dockerfiles.",
- )
-
- # --- Task filtering (comma-separated from CLI) ---
- task_filter: Optional[str] = Field(
- default=None,
- description="Comma-separated task names to run (e.g., 'fix-git,git-multibranch'). "
- "If not set, all tasks are run.",
- )
- skip_tasks: Optional[str] = Field(
- default=None,
- description="Comma-separated task names to skip on top of the default skip list.",
- )
-
- # --- Per-task wall-clock timeout ---
- task_timeout: int = Field(
- default=1800,
- description="Maximum wall-clock seconds per task (agent loop + verification). "
- "Tasks exceeding this are scored as FAIL. Default 30 minutes.",
- )
-
- # --- Concurrency control ---
- max_concurrent_tasks: int = Field(
- default=8,
- description="Maximum number of tasks to run concurrently. "
- "Limits concurrent Modal sandbox creations to avoid async/threading deadlocks. "
- "Modal has internal limits and creating too many sandboxes simultaneously "
- "causes blocking calls to deadlock inside the thread pool.",
- )
-
- # --- Eval concurrency ---
- eval_concurrency: int = Field(
- default=0,
- description="Maximum number of tasks to evaluate in parallel. "
- "0 means unlimited (all tasks run concurrently). "
- "Set to 8 for local backends to avoid overwhelming the machine.",
- )
-
-
-# Tasks that cannot run properly on Modal and are excluded from scoring.
-MODAL_INCOMPATIBLE_TASKS = {
- "qemu-startup", # Needs KVM/hardware virtualization
- "qemu-alpine-ssh", # Needs KVM/hardware virtualization
- "crack-7z-hash", # Password brute-force -- too slow for cloud sandbox timeouts
-}
-
-
-# =============================================================================
-# Tar extraction helper
-# =============================================================================
-
-def _normalize_tar_member_parts(member_name: str) -> list:
- """Return safe path components for a tar member or raise ValueError."""
- normalized_name = member_name.replace("\\", "/")
- posix_path = PurePosixPath(normalized_name)
- windows_path = PureWindowsPath(member_name)
-
- if (
- not normalized_name
- or posix_path.is_absolute()
- or windows_path.is_absolute()
- or windows_path.drive
- ):
- raise ValueError(f"Unsafe archive member path: {member_name}")
-
- parts = [part for part in posix_path.parts if part not in {"", "."}]
- if not parts or any(part == ".." for part in parts):
- raise ValueError(f"Unsafe archive member path: {member_name}")
- return parts
-
-
-def _safe_extract_tar(tar: tarfile.TarFile, target_dir: Path) -> None:
- """Extract a tar archive without allowing traversal or link entries."""
- target_dir.mkdir(parents=True, exist_ok=True)
- target_root = target_dir.resolve()
-
- for member in tar.getmembers():
- parts = _normalize_tar_member_parts(member.name)
- target = target_dir.joinpath(*parts)
- target_real = target.resolve(strict=False)
-
- try:
- target_real.relative_to(target_root)
- except ValueError as exc:
- raise ValueError(f"Unsafe archive member path: {member.name}") from exc
-
- if member.isdir():
- target_real.mkdir(parents=True, exist_ok=True)
- continue
-
- if not member.isfile():
- raise ValueError(f"Unsupported archive member type: {member.name}")
-
- target_real.parent.mkdir(parents=True, exist_ok=True)
- extracted = tar.extractfile(member)
- if extracted is None:
- raise ValueError(f"Cannot read archive member: {member.name}")
-
- with extracted, open(target_real, "wb") as dst:
- shutil.copyfileobj(extracted, dst)
-
- try:
- os.chmod(target_real, member.mode & 0o777)
- except OSError:
- pass
-
-
-def _extract_base64_tar(b64_data: str, target_dir: Path):
- """Extract a base64-encoded tar.gz archive into target_dir."""
- if not b64_data:
- return
- raw = base64.b64decode(b64_data)
- buf = io.BytesIO(raw)
- with tarfile.open(fileobj=buf, mode="r:gz") as tar:
- _safe_extract_tar(tar, target_dir)
-
-
-# =============================================================================
-# Main Environment
-# =============================================================================
-
-class TerminalBench2EvalEnv(HermesAgentBaseEnv):
- """
- Terminal-Bench 2.0 evaluation environment (eval-only, no training).
-
- Inherits from HermesAgentBaseEnv for:
- - Terminal backend setup (os.environ["TERMINAL_ENV"])
- - Tool resolution via _resolve_tools_for_group()
- - Monkey patches for async-safe tool operation
- - Wandb trajectory formatting
-
- The evaluate flow (triggered by `environment.py evaluate`):
- 1. setup() -- Load dataset from HuggingFace
- 2. evaluate() -- Run all tasks through rollout_and_score_eval()
-
- Each task in rollout_and_score_eval():
- 1. Resolve Docker image (pre-built Hub image or Dockerfile fallback)
- 2. Register per-task Modal sandbox override
- 3. Run HermesAgentLoop with terminal + file tools
- 4. Upload test suite and execute test.sh in the same sandbox
- 5. Check /logs/verifier/reward.txt for pass/fail
- 6. Clean up sandbox, overrides, and temp files
- """
-
- name = "terminal-bench-2"
- env_config_cls = TerminalBench2EvalConfig
-
- @classmethod
- def config_init(cls) -> Tuple[TerminalBench2EvalConfig, List[APIServerConfig]]:
- """
- Default configuration for Terminal-Bench 2.0 evaluation.
-
- Uses eval-only settings:
- - eval_handling=STOP_TRAIN so the eval flow runs cleanly
- - steps_per_eval=1, total_steps=1 so eval triggers immediately
- - group_size=1 (one rollout per group, each task is expensive)
-
- Uses Modal terminal backend (cloud-isolated sandbox per task) and
- OpenRouter with Claude for inference.
- """
- env_config = TerminalBench2EvalConfig(
- # Terminal + file tools only (the agent interacts via shell commands)
- enabled_toolsets=["terminal", "file"],
- disabled_toolsets=None,
- distribution=None,
-
- # Agent settings -- TB2 tasks are complex, need many turns
- max_agent_turns=60,
- max_token_length=16000,
- agent_temperature=0.6,
- system_prompt=None,
-
- # Modal backend for per-task cloud-isolated sandboxes
- terminal_backend="modal",
- terminal_timeout=300, # 5 min per command (builds, pip install, etc.)
-
- # Test execution timeout (TB2 test scripts can install deps like pytest)
- test_timeout=180,
-
- # 89 tasks run in parallel, each needs a thread for tool calls
- tool_pool_size=128,
-
- # --- Eval-only Atropos settings ---
- # These settings make the env work as an eval-only environment:
- # - STOP_TRAIN: pauses training during eval (standard for eval envs)
- # - steps_per_eval=1, total_steps=1: eval triggers immediately
- # - group_size=1: one rollout per group (each task is expensive)
- eval_handling=EvalHandlingEnum.STOP_TRAIN,
- group_size=1,
- steps_per_eval=1,
- total_steps=1,
-
- tokenizer_name="NousResearch/Hermes-3-Llama-3.1-8B",
- use_wandb=True,
- wandb_name="terminal-bench-2",
- ensure_scores_are_not_same=False, # Binary rewards may all be 0 or 1
- )
-
- # OpenRouter with Claude -- API key loaded from .env
- server_configs = [
- APIServerConfig(
- base_url="https://openrouter.ai/api/v1",
- model_name="anthropic/claude-sonnet-4",
- server_type="openai",
- api_key=os.getenv("OPENROUTER_API_KEY", ""),
- health_check=False,
- )
- ]
-
- return env_config, server_configs
-
- # =========================================================================
- # Setup -- load dataset
- # =========================================================================
-
- async def setup(self):
- """Load the Terminal-Bench 2.0 dataset from HuggingFace."""
- from datasets import load_dataset
-
- # Auto-set terminal_lifetime to task_timeout + 120s so sandboxes
- # never get killed during an active task, but still get cleaned up
- # promptly after the task times out.
- lifetime = self.config.task_timeout + 120
- self.config.terminal_lifetime = lifetime
- os.environ["TERMINAL_LIFETIME_SECONDS"] = str(lifetime)
- print(f" Terminal lifetime auto-set to {lifetime}s (task_timeout + 120s)")
-
- print(f"Loading TB2 dataset from: {self.config.dataset_name}")
- ds = load_dataset(self.config.dataset_name, split="train")
-
- # Apply task filters (comma-separated strings from CLI)
- tasks = list(ds)
- if self.config.task_filter:
- allowed = {name.strip() for name in self.config.task_filter.split(",")}
- tasks = [t for t in tasks if t["task_name"] in allowed]
- print(f" Filtered to {len(tasks)} tasks: {sorted(allowed)}")
-
- # Skip tasks incompatible with the current backend (e.g., QEMU on Modal)
- # plus any user-specified skip_tasks
- skip = set(MODAL_INCOMPATIBLE_TASKS) if self.config.terminal_backend == "modal" else set()
- if self.config.skip_tasks:
- skip |= {name.strip() for name in self.config.skip_tasks.split(",")}
- if skip:
- before = len(tasks)
- tasks = [t for t in tasks if t["task_name"] not in skip]
- skipped = before - len(tasks)
- if skipped > 0:
- print(f" Skipped {skipped} incompatible tasks: {sorted(skip & {t['task_name'] for t in ds})}")
-
- self.all_eval_items = tasks
- self.iter = 0
-
- # Build category index for per-category metrics
- self.category_index: Dict[str, List[int]] = defaultdict(list)
- for i, task in enumerate(self.all_eval_items):
- self.category_index[task.get("category", "unknown")].append(i)
-
- # Reward tracking for wandb logging
- self.eval_metrics: List[Tuple[str, float]] = []
-
- # Streaming JSONL writer -- saves each task's full conversation
- # immediately on completion so data is preserved even on Ctrl+C.
- # Timestamped filename so each run produces a unique file.
- import datetime
- log_dir = os.path.join(os.path.dirname(__file__), "logs")
- os.makedirs(log_dir, exist_ok=True)
- run_ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
- self._streaming_path = os.path.join(log_dir, f"samples_{run_ts}.jsonl")
- self._streaming_file = open(self._streaming_path, "w", encoding="utf-8")
- self._streaming_lock = __import__("threading").Lock()
- print(f" Streaming results to: {self._streaming_path}")
-
- print(f"TB2 ready: {len(self.all_eval_items)} tasks across {len(self.category_index)} categories")
- for cat, indices in sorted(self.category_index.items()):
- print(f" {cat}: {len(indices)} tasks")
-
- def _save_result(self, result: Dict[str, Any]):
- """Write a single task result to the streaming JSONL file immediately."""
- if not hasattr(self, "_streaming_file") or self._streaming_file.closed:
- return
- with self._streaming_lock:
- self._streaming_file.write(json.dumps(result, ensure_ascii=False, default=str) + "\n")
- self._streaming_file.flush()
-
- # =========================================================================
- # Training pipeline stubs -- NOT used in eval-only mode
- # =========================================================================
- # These satisfy the abstract method requirements from HermesAgentBaseEnv.
- # The evaluate subcommand calls setup() -> evaluate() directly, bypassing
- # the training pipeline entirely.
-
- async def get_next_item(self):
- """Return next item (stub -- not used in eval-only mode)."""
- item = self.all_eval_items[self.iter % len(self.all_eval_items)]
- self.iter += 1
- return item
-
- def format_prompt(self, item: Dict[str, Any]) -> str:
- """Return the task's instruction as the user prompt."""
- return item["instruction"]
-
- async def compute_reward(self, item, result, ctx) -> float:
- """Compute reward (stub -- actual verification is in rollout_and_score_eval)."""
- return 0.0
-
- async def collect_trajectories(self, item):
- """Collect trajectories (stub -- not used in eval-only mode)."""
- return None, []
-
- async def score(self, rollout_group_data):
- """Score rollouts (stub -- not used in eval-only mode)."""
- return None
-
- # =========================================================================
- # Docker image resolution
- # =========================================================================
-
- def _resolve_task_image(
- self, item: Dict[str, Any], task_name: str
- ) -> Tuple[str, Optional[Path]]:
- """
- Resolve the Docker image for a task, with fallback to Dockerfile.
-
- Strategy (mirrors Harbor's approach):
- 1. If force_build=True, always build from Dockerfile in environment_tar
- 2. If docker_image is available, use the pre-built Docker Hub image (fast)
- 3. Otherwise, extract Dockerfile from environment_tar and build (slow)
-
- Returns:
- (modal_image, temp_dir) -- modal_image is a Docker Hub name or a
- Dockerfile path. temp_dir is set if we extracted files that need
- cleanup later.
- """
- docker_image = item.get("docker_image", "")
- environment_tar = item.get("environment_tar", "")
-
- # Fast path: use pre-built Docker Hub image
- if docker_image and not self.config.force_build:
- logger.info("Task %s: using pre-built image %s", task_name, docker_image)
- return docker_image, None
-
- # Slow path: extract Dockerfile from environment_tar and build
- if environment_tar:
- task_dir = Path(tempfile.mkdtemp(prefix=f"tb2-{task_name}-"))
- _extract_base64_tar(environment_tar, task_dir)
- dockerfile_path = task_dir / "Dockerfile"
- if dockerfile_path.exists():
- logger.info(
- "Task %s: building from Dockerfile (force_build=%s, docker_image=%s)",
- task_name, self.config.force_build, bool(docker_image),
- )
- return str(dockerfile_path), task_dir
-
- # Neither available -- fall back to Hub image if force_build was True
- if docker_image:
- logger.warning(
- "Task %s: force_build=True but no environment_tar, "
- "falling back to docker_image %s", task_name, docker_image,
- )
- return docker_image, None
-
- return "", None
-
- # =========================================================================
- # Per-task evaluation -- agent loop + test verification
- # =========================================================================
-
- async def rollout_and_score_eval(self, eval_item: Dict[str, Any]) -> Dict:
- """
- Evaluate a single TB2 task: run the agent loop, then verify with tests.
-
- This is the core evaluation method. For each task it:
- 1. Resolves the Docker image and registers the Modal sandbox override
- 2. Runs HermesAgentLoop with terminal + file tools
- 3. Uploads the test suite into the sandbox
- 4. Executes test.sh and checks the result
- 5. Cleans up the sandbox and temp files
-
- Args:
- eval_item: A single TB2 task dict from the dataset
-
- Returns:
- Dict with 'passed' (bool), 'reward' (float), 'task_name' (str),
- 'category' (str), and optional debug info
- """
- task_name = eval_item.get("task_name", "unknown")
- category = eval_item.get("category", "unknown")
- task_id = str(uuid.uuid4())
- task_dir = None # Set if we extract a Dockerfile (needs cleanup)
-
- from tqdm import tqdm
- tqdm.write(f" [START] {task_name} (task_id={task_id[:8]})")
- task_start = time.time()
-
- try:
- # --- 1. Resolve Docker image ---
- modal_image, task_dir = self._resolve_task_image(eval_item, task_name)
- if not modal_image:
- logger.error("Task %s: no docker_image or environment_tar, skipping", task_name)
- return {
- "passed": False, "reward": 0.0,
- "task_name": task_name, "category": category,
- "error": "no_image",
- }
-
- # --- 2. Register per-task image override ---
- # Set both modal_image and docker_image so the task image is used
- # regardless of which backend is configured.
- register_task_env_overrides(task_id, {
- "modal_image": modal_image,
- "docker_image": modal_image,
- "cwd": "/app",
- })
- logger.info(
- "Task %s: registered image override for task_id %s",
- task_name, task_id[:8],
- )
-
- # --- 3. Resolve tools and build messages ---
- tools, valid_names = self._resolve_tools_for_group()
-
- messages: List[Dict[str, Any]] = []
- if self.config.system_prompt:
- messages.append({"role": "system", "content": self.config.system_prompt})
- messages.append({"role": "user", "content": self.format_prompt(eval_item)})
-
- # --- 4. Run agent loop ---
- # Use ManagedServer (Phase 2) for vLLM/SGLang backends to get
- # token-level tracking via /generate. Falls back to direct
- # ServerManager (Phase 1) for OpenAI endpoints.
- if self._use_managed_server():
- async with self.server.managed_server(
- tokenizer=self.tokenizer,
- preserve_think_blocks=bool(self.config.thinking_mode),
- ) as managed:
- agent = HermesAgentLoop(
- server=managed,
- tool_schemas=tools,
- valid_tool_names=valid_names,
- max_turns=self.config.max_agent_turns,
- task_id=task_id,
- temperature=self.config.agent_temperature,
- max_tokens=self.config.max_token_length,
- extra_body=self.config.extra_body,
- budget_config=self.config.build_budget_config(),
- )
- result = await agent.run(messages)
- else:
- agent = HermesAgentLoop(
- server=self.server,
- tool_schemas=tools,
- valid_tool_names=valid_names,
- max_turns=self.config.max_agent_turns,
- task_id=task_id,
- temperature=self.config.agent_temperature,
- max_tokens=self.config.max_token_length,
- extra_body=self.config.extra_body,
- budget_config=self.config.build_budget_config(),
- )
- result = await agent.run(messages)
-
- # --- 5. Verify -- run test suite in the agent's sandbox ---
- # Skip verification if the agent produced no meaningful output
- only_system_and_user = all(
- msg.get("role") in {"system", "user"} for msg in result.messages
- )
- if result.turns_used == 0 or only_system_and_user:
- logger.warning(
- "Task %s: agent produced no output (turns=%d). Reward=0.",
- task_name, result.turns_used,
- )
- reward = 0.0
- else:
- # Run tests in a thread so the blocking ctx.terminal() calls
- # don't freeze the entire event loop (which would stall all
- # other tasks, tqdm updates, and timeout timers).
- ctx = ToolContext(task_id)
- try:
- loop = asyncio.get_running_loop()
- reward = await loop.run_in_executor(
- None, # default thread pool
- self._run_tests, eval_item, ctx, task_name,
- )
- except Exception as e:
- logger.error("Task %s: test verification failed: %s", task_name, e)
- reward = 0.0
- finally:
- ctx.cleanup()
-
- passed = reward == 1.0
- status = "PASS" if passed else "FAIL"
- elapsed = time.time() - task_start
- tqdm.write(f" [{status}] {task_name} (turns={result.turns_used}, {elapsed:.0f}s)")
- logger.info(
- "Task %s: reward=%.1f, turns=%d, finished=%s",
- task_name, reward, result.turns_used, result.finished_naturally,
- )
-
- out = {
- "passed": passed,
- "reward": reward,
- "task_name": task_name,
- "category": category,
- "turns_used": result.turns_used,
- "finished_naturally": result.finished_naturally,
- "messages": result.messages,
- }
- self._save_result(out)
- return out
-
- except Exception as e:
- elapsed = time.time() - task_start
- logger.error("Task %s: rollout failed: %s", task_name, e, exc_info=True)
- tqdm.write(f" [ERROR] {task_name}: {e} ({elapsed:.0f}s)")
- out = {
- "passed": False, "reward": 0.0,
- "task_name": task_name, "category": category,
- "error": str(e),
- }
- self._save_result(out)
- return out
-
- finally:
- # --- Cleanup: clear overrides, sandbox, and temp files ---
- clear_task_env_overrides(task_id)
- try:
- cleanup_vm(task_id)
- except Exception as e:
- logger.debug("VM cleanup for %s: %s", task_id[:8], e)
- if task_dir and task_dir.exists():
- shutil.rmtree(task_dir, ignore_errors=True)
-
- def _run_tests(
- self, item: Dict[str, Any], ctx: ToolContext, task_name: str
- ) -> float:
- """
- Upload and execute the test suite in the agent's sandbox, then
- download the verifier output locally to read the reward.
-
- Follows Harbor's verification pattern:
- 1. Upload tests/ directory into the sandbox
- 2. Execute test.sh inside the sandbox
- 3. Download /logs/verifier/ directory to a local temp dir
- 4. Read reward.txt locally with native Python I/O
-
- Downloading locally avoids issues with the file_read tool on
- the Modal VM and matches how Harbor handles verification.
-
- TB2 test scripts (test.sh) typically:
- 1. Install pytest via uv/pip
- 2. Run pytest against the test files in /tests/
- 3. Write results to /logs/verifier/reward.txt
-
- Args:
- item: The TB2 task dict (contains tests_tar, test_sh)
- ctx: ToolContext scoped to this task's sandbox
- task_name: For logging
-
- Returns:
- 1.0 if tests pass, 0.0 otherwise
- """
- tests_tar = item.get("tests_tar", "")
- test_sh = item.get("test_sh", "")
-
- if not test_sh:
- logger.warning("Task %s: no test_sh content, reward=0", task_name)
- return 0.0
-
- # Create required directories in the sandbox
- ctx.terminal("mkdir -p /tests /logs/verifier")
-
- # Upload test files into the sandbox (binary-safe via base64)
- if tests_tar:
- tests_temp = Path(tempfile.mkdtemp(prefix=f"tb2-tests-{task_name}-"))
- try:
- _extract_base64_tar(tests_tar, tests_temp)
- ctx.upload_dir(str(tests_temp), "/tests")
- except Exception as e:
- logger.warning("Task %s: failed to upload test files: %s", task_name, e)
- finally:
- shutil.rmtree(tests_temp, ignore_errors=True)
-
- # Write the test runner script (test.sh)
- ctx.write_file("/tests/test.sh", test_sh)
- ctx.terminal("chmod +x /tests/test.sh")
-
- # Execute the test suite
- logger.info(
- "Task %s: running test suite (timeout=%ds)",
- task_name, self.config.test_timeout,
- )
- test_result = ctx.terminal(
- "bash /tests/test.sh",
- timeout=self.config.test_timeout,
- )
-
- exit_code = test_result.get("exit_code", -1)
- output = test_result.get("output", "")
-
- # Download the verifier output directory locally, then read reward.txt
- # with native Python I/O. This avoids issues with file_read on the
- # Modal VM and matches Harbor's verification pattern.
- reward = 0.0
- local_verifier_dir = Path(tempfile.mkdtemp(prefix=f"tb2-verifier-{task_name}-"))
- try:
- ctx.download_dir("/logs/verifier", str(local_verifier_dir))
-
- reward_file = local_verifier_dir / "reward.txt"
- if reward_file.exists() and reward_file.stat().st_size > 0:
- content = reward_file.read_text().strip()
- if content == "1":
- reward = 1.0
- elif content == "0":
- reward = 0.0
- else:
- # Unexpected content -- try parsing as float
- try:
- reward = float(content)
- except (ValueError, TypeError):
- logger.warning(
- "Task %s: reward.txt content unexpected (%r), "
- "falling back to exit_code=%d",
- task_name, content, exit_code,
- )
- reward = 1.0 if exit_code == 0 else 0.0
- else:
- # reward.txt not written -- fall back to exit code
- logger.warning(
- "Task %s: reward.txt not found after download, "
- "falling back to exit_code=%d",
- task_name, exit_code,
- )
- reward = 1.0 if exit_code == 0 else 0.0
- except Exception as e:
- logger.warning(
- "Task %s: failed to download verifier dir: %s, "
- "falling back to exit_code=%d",
- task_name, e, exit_code,
- )
- reward = 1.0 if exit_code == 0 else 0.0
- finally:
- shutil.rmtree(local_verifier_dir, ignore_errors=True)
-
- # Log test output for debugging failures
- if reward == 0.0:
- output_preview = output[-500:] if output else "(no output)"
- logger.info(
- "Task %s: FAIL (exit_code=%d)\n%s",
- task_name, exit_code, output_preview,
- )
-
- return reward
-
- # =========================================================================
- # Evaluate -- main entry point for the eval subcommand
- # =========================================================================
-
- async def _eval_with_timeout(self, item: Dict[str, Any]) -> Dict:
- """
- Wrap rollout_and_score_eval with a per-task wall-clock timeout.
-
- If the task exceeds task_timeout seconds, it's automatically scored
- as FAIL. This prevents any single task from hanging indefinitely.
- """
- task_name = item.get("task_name", "unknown")
- category = item.get("category", "unknown")
- try:
- return await asyncio.wait_for(
- self.rollout_and_score_eval(item),
- timeout=self.config.task_timeout,
- )
- except asyncio.TimeoutError:
- from tqdm import tqdm
- elapsed = self.config.task_timeout
- tqdm.write(f" [TIMEOUT] {task_name} (exceeded {elapsed}s wall-clock limit)")
- logger.error("Task %s: wall-clock timeout after %ds", task_name, elapsed)
- out = {
- "passed": False, "reward": 0.0,
- "task_name": task_name, "category": category,
- "error": f"timeout ({elapsed}s)",
- }
- self._save_result(out)
- return out
-
- async def evaluate(self, *args, **kwargs) -> None:
- """
- Run Terminal-Bench 2.0 evaluation over all tasks.
-
- This is the main entry point when invoked via:
- python environments/terminalbench2_env.py evaluate
-
- Runs all tasks through rollout_and_score_eval() via asyncio.gather()
- (same pattern as GPQA and other Atropos eval envs). Each task is
- wrapped with a wall-clock timeout so hung tasks auto-fail.
-
- Suppresses noisy Modal/terminal output (HERMES_QUIET) so the tqdm
- bar stays visible.
- """
- start_time = time.time()
-
- # Route all logging through tqdm.write() so the progress bar stays
- # pinned at the bottom while log lines scroll above it.
- from tqdm import tqdm
-
- class _TqdmHandler(logging.Handler):
- def emit(self, record):
- try:
- tqdm.write(self.format(record))
- except Exception:
- self.handleError(record)
-
- handler = _TqdmHandler()
- handler.setFormatter(logging.Formatter(
- "%(asctime)s [%(name)s] %(levelname)s: %(message)s",
- datefmt="%H:%M:%S",
- ))
- root = logging.getLogger()
- root.handlers = [handler] # Replace any existing handlers
- root.setLevel(logging.INFO)
-
- # Silence noisy third-party loggers that flood the output
- logging.getLogger("httpx").setLevel(logging.WARNING) # Every HTTP request
- logging.getLogger("openai").setLevel(logging.WARNING) # OpenAI client retries
- logging.getLogger("rex-deploy").setLevel(logging.WARNING) # Swerex deployment
- logging.getLogger("rex_image_builder").setLevel(logging.WARNING) # Image builds
-
- print(f"\n{'='*60}")
- print("Starting Terminal-Bench 2.0 Evaluation")
- print(f"{'='*60}")
- print(f" Dataset: {self.config.dataset_name}")
- print(f" Total tasks: {len(self.all_eval_items)}")
- print(f" Max agent turns: {self.config.max_agent_turns}")
- print(f" Task timeout: {self.config.task_timeout}s")
- print(f" Terminal backend: {self.config.terminal_backend}")
- print(f" Tool thread pool: {self.config.tool_pool_size}")
- print(f" Terminal timeout: {self.config.terminal_timeout}s/cmd")
- print(f" Terminal lifetime: {self.config.terminal_lifetime}s (auto: task_timeout + 120)")
- print(f" Max concurrent tasks: {self.config.max_concurrent_tasks}")
- print(f"{'='*60}\n")
-
- # Semaphore to limit concurrent Modal sandbox creations.
- # Without this, all 86 tasks fire simultaneously, each creating a Modal
- # sandbox via asyncio.run() inside a thread pool worker. Modal's blocking
- # calls (App.lookup, etc.) deadlock when too many are created at once.
- semaphore = asyncio.Semaphore(self.config.max_concurrent_tasks)
-
- async def _eval_with_semaphore(item):
- async with semaphore:
- return await self._eval_with_timeout(item)
-
- # Fire all tasks with wall-clock timeout, track live accuracy on the bar
- total_tasks = len(self.all_eval_items)
- eval_tasks = [
- asyncio.ensure_future(_eval_with_semaphore(item))
- for item in self.all_eval_items
- ]
-
- results = []
- passed_count = 0
- pbar = tqdm(total=total_tasks, desc="Evaluating TB2", dynamic_ncols=True)
- try:
- for coro in asyncio.as_completed(eval_tasks):
- result = await coro
- results.append(result)
- if result and result.get("passed"):
- passed_count += 1
- done = len(results)
- pct = (passed_count / done * 100) if done else 0
- pbar.set_postfix_str(f"pass={passed_count}/{done} ({pct:.1f}%)")
- pbar.update(1)
- except (KeyboardInterrupt, asyncio.CancelledError):
- pbar.close()
- print(f"\n\nInterrupted! Cleaning up {len(eval_tasks)} tasks...")
- # Cancel all pending tasks
- for task in eval_tasks:
- task.cancel()
- # Let cancellations propagate (finally blocks run cleanup_vm)
- await asyncio.gather(*eval_tasks, return_exceptions=True)
- # Belt-and-suspenders: clean up any remaining sandboxes
- from tools.terminal_tool import cleanup_all_environments
- cleanup_all_environments()
- print("All sandboxes cleaned up.")
- return
- finally:
- pbar.close()
-
- end_time = time.time()
-
- # Filter out None results (shouldn't happen, but be safe)
- valid_results = [r for r in results if r is not None]
-
- if not valid_results:
- print("Warning: No valid evaluation results obtained")
- return
-
- # ---- Compute metrics ----
- total = len(valid_results)
- passed = sum(1 for r in valid_results if r.get("passed"))
- overall_pass_rate = passed / total if total > 0 else 0.0
-
- # Per-category breakdown
- cat_results: Dict[str, List[Dict]] = defaultdict(list)
- for r in valid_results:
- cat_results[r.get("category", "unknown")].append(r)
-
- # Build metrics dict
- eval_metrics = {
- "eval/pass_rate": overall_pass_rate,
- "eval/total_tasks": total,
- "eval/passed_tasks": passed,
- "eval/evaluation_time_seconds": end_time - start_time,
- }
-
- # Per-category metrics
- for category, cat_items in sorted(cat_results.items()):
- cat_passed = sum(1 for r in cat_items if r.get("passed"))
- cat_total = len(cat_items)
- cat_pass_rate = cat_passed / cat_total if cat_total > 0 else 0.0
- cat_key = category.replace(" ", "_").replace("-", "_").lower()
- eval_metrics[f"eval/pass_rate_{cat_key}"] = cat_pass_rate
-
- # Store metrics for wandb_log
- self.eval_metrics = list(eval_metrics.items())
-
- # ---- Print summary ----
- print(f"\n{'='*60}")
- print("Terminal-Bench 2.0 Evaluation Results")
- print(f"{'='*60}")
- print(f"Overall Pass Rate: {overall_pass_rate:.4f} ({passed}/{total})")
- print(f"Evaluation Time: {end_time - start_time:.1f} seconds")
-
- print("\nCategory Breakdown:")
- for category, cat_items in sorted(cat_results.items()):
- cat_passed = sum(1 for r in cat_items if r.get("passed"))
- cat_total = len(cat_items)
- cat_rate = cat_passed / cat_total if cat_total > 0 else 0.0
- print(f" {category}: {cat_rate:.1%} ({cat_passed}/{cat_total})")
-
- # Print individual task results
- print("\nTask Results:")
- for r in sorted(valid_results, key=lambda x: x.get("task_name", "")):
- status = "PASS" if r.get("passed") else "FAIL"
- turns = r.get("turns_used", "?")
- error = r.get("error", "")
- extra = f" (error: {error})" if error else ""
- print(f" [{status}] {r['task_name']} (turns={turns}){extra}")
-
- print(f"{'='*60}\n")
-
- # Build sample records for evaluate_log (includes full conversations)
- samples = [
- {
- "task_name": r.get("task_name"),
- "category": r.get("category"),
- "passed": r.get("passed"),
- "reward": r.get("reward"),
- "turns_used": r.get("turns_used"),
- "error": r.get("error"),
- "messages": r.get("messages"),
- }
- for r in valid_results
- ]
-
- # Log evaluation results
- try:
- await self.evaluate_log(
- metrics=eval_metrics,
- samples=samples,
- start_time=start_time,
- end_time=end_time,
- generation_parameters={
- "temperature": self.config.agent_temperature,
- "max_tokens": self.config.max_token_length,
- "max_agent_turns": self.config.max_agent_turns,
- "terminal_backend": self.config.terminal_backend,
- },
- )
- except Exception as e:
- print(f"Error logging evaluation results: {e}")
-
- # Close streaming file
- if hasattr(self, "_streaming_file") and not self._streaming_file.closed:
- self._streaming_file.close()
- print(f" Live results saved to: {self._streaming_path}")
-
- # Kill all remaining sandboxes. Timed-out tasks leave orphaned thread
- # pool workers still executing commands -- cleanup_all stops them.
- from tools.terminal_tool import cleanup_all_environments
- print("\nCleaning up all sandboxes...")
- cleanup_all_environments()
-
- # Shut down the tool thread pool so orphaned workers from timed-out
- # tasks are killed immediately instead of retrying against dead
- # sandboxes and spamming the console with TimeoutError warnings.
- from environments.agent_loop import _tool_executor
- _tool_executor.shutdown(wait=False, cancel_futures=True)
- print("Done.")
-
- # =========================================================================
- # Wandb logging
- # =========================================================================
-
- async def wandb_log(self, wandb_metrics: Optional[Dict] = None):
- """Log TB2-specific metrics to wandb."""
- if wandb_metrics is None:
- wandb_metrics = {}
-
- # Add stored eval metrics
- for metric_name, metric_value in self.eval_metrics:
- wandb_metrics[metric_name] = metric_value
- self.eval_metrics = []
-
- await super().wandb_log(wandb_metrics)
-
-
-if __name__ == "__main__":
- TerminalBench2EvalEnv.cli()
diff --git a/environments/benchmarks/yc_bench/README.md b/environments/benchmarks/yc_bench/README.md
deleted file mode 100644
index 7a8aba7874d..00000000000
--- a/environments/benchmarks/yc_bench/README.md
+++ /dev/null
@@ -1,115 +0,0 @@
-# YC-Bench: Long-Horizon Agent Benchmark
-
-[YC-Bench](https://github.com/collinear-ai/yc-bench) by [Collinear AI](https://collinear.ai/) is a deterministic, long-horizon benchmark that tests LLM agents' ability to act as a tech startup CEO. The agent manages a simulated company over 1-3 years, making compounding decisions about resource allocation, cash flow, task management, and prestige specialisation across 4 skill domains.
-
-Unlike TerminalBench2 (which evaluates per-task coding ability with binary pass/fail), YC-Bench measures **long-term strategic coherence** — whether an agent can maintain consistent strategy, manage compounding consequences, and adapt plans over hundreds of turns.
-
-## Setup
-
-```bash
-# Install yc-bench (optional dependency)
-pip install "hermes-agent[yc-bench]"
-
-# Or install from source
-git clone https://github.com/collinear-ai/yc-bench
-cd yc-bench && pip install -e .
-
-# Verify
-yc-bench --help
-```
-
-## Running
-
-```bash
-# From the repo root:
-bash environments/benchmarks/yc_bench/run_eval.sh
-
-# Or directly:
-python environments/benchmarks/yc_bench/yc_bench_env.py evaluate \
- --config environments/benchmarks/yc_bench/default.yaml
-
-# Override model:
-bash environments/benchmarks/yc_bench/run_eval.sh \
- --openai.model_name anthropic/claude-opus-4-20250514
-
-# Quick single-preset test:
-bash environments/benchmarks/yc_bench/run_eval.sh \
- --env.presets '["fast_test"]' --env.seeds '[1]'
-```
-
-## How It Works
-
-### Architecture
-
-```
-HermesAgentLoop (our agent)
- -> terminal tool -> subprocess("yc-bench company status") -> JSON output
- -> terminal tool -> subprocess("yc-bench task accept --task-id X") -> JSON
- -> terminal tool -> subprocess("yc-bench sim resume") -> JSON (advance time)
- -> ... (100-500 turns per run)
-```
-
-The environment initialises the simulation via `yc-bench sim init` (NOT `yc-bench run`, which would start yc-bench's own built-in agent loop). Our `HermesAgentLoop` then drives all interaction through CLI commands.
-
-### Simulation Mechanics
-
-- **4 skill domains**: research, inference, data_environment, training
-- **Prestige system** (1.0-10.0): Gates access to higher-paying tasks
-- **Employee management**: Junior/Mid/Senior with domain-specific skill rates
-- **Throughput splitting**: `effective_rate = base_rate / N` active tasks per employee
-- **Financial pressure**: Monthly payroll, bankruptcy = game over
-- **Deterministic**: SHA256-based RNG — same seed + preset = same world
-
-### Difficulty Presets
-
-| Preset | Employees | Tasks | Focus |
-|-----------|-----------|-------|-------|
-| tutorial | 3 | 50 | Basic loop mechanics |
-| easy | 5 | 100 | Throughput awareness |
-| **medium**| 5 | 150 | Prestige climbing + domain specialisation |
-| **hard** | 7 | 200 | Precise ETA reasoning |
-| nightmare | 8 | 300 | Sustained perfection under payroll pressure |
-| fast_test | (varies) | (varies) | Quick validation (~50 turns) |
-
-Default eval runs **fast_test + medium + hard** × 3 seeds = 9 runs.
-
-### Scoring
-
-```
-composite = 0.5 × survival + 0.5 × normalised_funds
-```
-
-- **Survival** (binary): Did the company avoid bankruptcy?
-- **Normalised funds** (0.0-1.0): Log-scale relative to initial $250K capital
-
-## Configuration
-
-Key fields in `default.yaml`:
-
-| Field | Default | Description |
-|-------|---------|-------------|
-| `presets` | `["fast_test", "medium", "hard"]` | Which presets to evaluate |
-| `seeds` | `[1, 2, 3]` | RNG seeds per preset |
-| `max_agent_turns` | 200 | Max LLM calls per run |
-| `run_timeout` | 3600 | Wall-clock timeout per run (seconds) |
-| `survival_weight` | 0.5 | Weight of survival in composite score |
-| `funds_weight` | 0.5 | Weight of normalised funds in composite |
-| `horizon_years` | null | Override horizon (null = auto from preset) |
-
-## Cost & Time Estimates
-
-Each run is 100-500 LLM turns. Approximate costs per run at typical API rates:
-
-| Preset | Turns | Time | Est. Cost |
-|--------|-------|------|-----------|
-| fast_test | ~50 | 5-10 min | $1-5 |
-| medium | ~200 | 20-40 min | $5-15 |
-| hard | ~300 | 30-60 min | $10-25 |
-
-Full default eval (9 runs): ~3-6 hours, $50-200 depending on model.
-
-## References
-
-- [collinear-ai/yc-bench](https://github.com/collinear-ai/yc-bench) — Official repository
-- [Collinear AI](https://collinear.ai/) — Company behind yc-bench
-- [TerminalBench2](../terminalbench_2/) — Per-task coding benchmark (complementary)
diff --git a/environments/benchmarks/yc_bench/__init__.py b/environments/benchmarks/yc_bench/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/environments/benchmarks/yc_bench/default.yaml b/environments/benchmarks/yc_bench/default.yaml
deleted file mode 100644
index 4396c00ab94..00000000000
--- a/environments/benchmarks/yc_bench/default.yaml
+++ /dev/null
@@ -1,43 +0,0 @@
-# YC-Bench Evaluation -- Default Configuration
-#
-# Long-horizon agent benchmark: agent plays CEO of an AI startup over
-# a simulated 1-3 year run, interacting via yc-bench CLI subcommands.
-#
-# Requires: pip install "hermes-agent[yc-bench]"
-#
-# Usage:
-# python environments/benchmarks/yc_bench/yc_bench_env.py evaluate \
-# --config environments/benchmarks/yc_bench/default.yaml
-#
-# # Override model:
-# python environments/benchmarks/yc_bench/yc_bench_env.py evaluate \
-# --config environments/benchmarks/yc_bench/default.yaml \
-# --openai.model_name anthropic/claude-opus-4-20250514
-
-env:
- enabled_toolsets: ["terminal"]
- max_agent_turns: 200
- max_token_length: 32000
- agent_temperature: 0.0
- terminal_backend: "local"
- terminal_timeout: 60
- presets: ["fast_test", "medium", "hard"]
- seeds: [1, 2, 3]
- run_timeout: 3600 # 60 min wall-clock per run, auto-FAIL if exceeded
- survival_weight: 0.5 # weight of binary survival in composite score
- funds_weight: 0.5 # weight of normalised final funds in composite score
- db_dir: "/tmp/yc_bench_dbs"
- company_name: "BenchCo"
- start_date: "01/01/2025" # MM/DD/YYYY (yc-bench convention)
- tokenizer_name: "NousResearch/Hermes-3-Llama-3.1-8B"
- use_wandb: true
- wandb_name: "yc-bench"
- ensure_scores_are_not_same: false
- data_dir_to_save_evals: "environments/benchmarks/evals/yc-bench"
-
-openai:
- base_url: "https://openrouter.ai/api/v1"
- model_name: "anthropic/claude-sonnet-4.6"
- server_type: "openai"
- health_check: false
- # api_key loaded from OPENROUTER_API_KEY in .env
diff --git a/environments/benchmarks/yc_bench/run_eval.sh b/environments/benchmarks/yc_bench/run_eval.sh
deleted file mode 100755
index 0d793f53d54..00000000000
--- a/environments/benchmarks/yc_bench/run_eval.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/bash
-
-# YC-Bench Evaluation
-#
-# Requires: pip install "hermes-agent[yc-bench]"
-#
-# Run from repo root:
-# bash environments/benchmarks/yc_bench/run_eval.sh
-#
-# Override model:
-# bash environments/benchmarks/yc_bench/run_eval.sh \
-# --openai.model_name anthropic/claude-opus-4-20250514
-#
-# Run a single preset:
-# bash environments/benchmarks/yc_bench/run_eval.sh \
-# --env.presets '["fast_test"]' --env.seeds '[1]'
-
-set -euo pipefail
-
-mkdir -p logs evals/yc-bench
-LOG_FILE="logs/yc_bench_$(date +%Y%m%d_%H%M%S).log"
-
-echo "YC-Bench Evaluation"
-echo "Log: $LOG_FILE"
-echo ""
-
-PYTHONUNBUFFERED=1 LOGLEVEL="${LOGLEVEL:-INFO}" \
- python environments/benchmarks/yc_bench/yc_bench_env.py evaluate \
- --config environments/benchmarks/yc_bench/default.yaml \
- "$@" \
- 2>&1 | tee "$LOG_FILE"
-
-echo ""
-echo "Log saved to: $LOG_FILE"
diff --git a/environments/benchmarks/yc_bench/yc_bench_env.py b/environments/benchmarks/yc_bench/yc_bench_env.py
deleted file mode 100644
index 6e7be2c899b..00000000000
--- a/environments/benchmarks/yc_bench/yc_bench_env.py
+++ /dev/null
@@ -1,848 +0,0 @@
-"""
-YCBenchEvalEnv -- YC-Bench Long-Horizon Agent Benchmark Environment
-
-Evaluates agentic LLMs on YC-Bench: a deterministic, long-horizon benchmark
-where the agent acts as CEO of an AI startup over a simulated 1-3 year run.
-The agent manages cash flow, employees, tasks, and prestige across 4 domains,
-interacting exclusively via CLI subprocess calls against a SQLite-backed
-discrete-event simulation.
-
-Unlike TerminalBench2 (per-task binary pass/fail), YC-Bench measures sustained
-multi-turn strategic coherence -- whether an agent can manage compounding
-decisions over hundreds of turns without going bankrupt.
-
-This is an eval-only environment. Run via:
-
- python environments/benchmarks/yc_bench/yc_bench_env.py evaluate \
- --config environments/benchmarks/yc_bench/default.yaml
-
-The evaluate flow:
- 1. setup() -- Verifies yc-bench installed, builds eval matrix (preset x seed)
- 2. evaluate() -- Iterates over all runs sequentially through:
- a. rollout_and_score_eval() -- Per-run agent loop
- - Initialises a fresh yc-bench simulation via `sim init` (NOT `run`)
- - Runs HermesAgentLoop with terminal tool only
- - Reads final SQLite DB to extract score
- - Returns survival (0/1) + normalised funds score
- b. Aggregates per-preset and overall metrics
- c. Logs results via evaluate_log() and wandb
-
-Key features:
- - CLI-only interface: agent calls yc-bench subcommands via terminal tool
- - Deterministic: same seed + preset = same world (SHA256-based RNG)
- - Multi-dimensional scoring: survival + normalised final funds
- - Per-preset difficulty breakdown in results
- - Isolated SQLite DB per run (no cross-run state leakage)
-
-Requires: pip install hermes-agent[yc-bench]
-"""
-
-import asyncio
-import datetime
-import json
-import logging
-import math
-import os
-import sqlite3
-import subprocess
-import sys
-import threading
-import time
-import uuid
-from collections import defaultdict
-from pathlib import Path
-from typing import Any, Dict, List, Optional, Tuple
-
-_repo_root = Path(__file__).resolve().parent.parent.parent.parent
-if str(_repo_root) not in sys.path:
- sys.path.insert(0, str(_repo_root))
-
-from pydantic import Field
-
-from atroposlib.envs.base import EvalHandlingEnum
-from atroposlib.envs.server_handling.server_manager import APIServerConfig
-
-from environments.agent_loop import HermesAgentLoop
-from environments.hermes_base_env import HermesAgentBaseEnv, HermesAgentEnvConfig
-
-logger = logging.getLogger(__name__)
-
-# =============================================================================
-# System prompt
-# =============================================================================
-
-YC_BENCH_SYSTEM_PROMPT = """\
-You are the autonomous CEO of an early-stage AI startup in a deterministic
-business simulation. You manage the company exclusively through the `yc-bench`
-CLI tool. Your primary goal is to **survive** until the simulation horizon ends
-without going bankrupt, while **maximising final funds**.
-
-## Simulation Mechanics
-
-- **Funds**: You start with $250,000 seed capital. Revenue comes from completing
- tasks. Rewards scale with your prestige: `base × (1 + scale × (prestige − 1))`.
-- **Domains**: There are 4 skill domains: **research**, **inference**,
- **data_environment**, and **training**. Each has its own prestige level
- (1.0-10.0). Higher prestige unlocks better-paying tasks.
-- **Employees**: You have employees (Junior/Mid/Senior) with domain-specific
- skill rates. **Throughput splits**: `effective_rate = base_rate / N` where N
- is the number of active tasks assigned to that employee. Focus beats breadth.
-- **Payroll**: Deducted automatically on the first business day of each month.
- Running out of funds = bankruptcy = game over.
-- **Time**: The simulation runs on business days (Mon-Fri), 09:00-18:00.
- Time only advances when you call `yc-bench sim resume`.
-
-## Task Lifecycle
-
-1. Browse market tasks with `market browse`
-2. Accept a task with `task accept` (this sets its deadline)
-3. Assign employees with `task assign`
-4. Dispatch with `task dispatch` to start work
-5. Call `sim resume` to advance time and let employees make progress
-6. Tasks complete when all domain requirements are fulfilled
-
-**Penalties for failure vary by difficulty preset.** Completing a task on time
-earns full reward + prestige gain. Missing a deadline or cancelling a task
-incurs prestige penalties -- cancelling is always more costly than letting a
-task fail, so cancel only as a last resort.
-
-## CLI Commands
-
-### Observe
-- `yc-bench company status` -- funds, prestige, runway
-- `yc-bench employee list` -- skills, salary, active tasks
-- `yc-bench market browse [--domain D] [--required-prestige-lte N]` -- available tasks
-- `yc-bench task list [--status active|planned]` -- your tasks
-- `yc-bench task inspect --task-id UUID` -- progress, deadline, assignments
-- `yc-bench finance ledger [--category monthly_payroll|task_reward]` -- transaction history
-- `yc-bench report monthly` -- monthly P&L
-
-### Act
-- `yc-bench task accept --task-id UUID` -- accept from market
-- `yc-bench task assign --task-id UUID --employee-id UUID` -- assign employee
-- `yc-bench task dispatch --task-id UUID` -- start work (needs >=1 assignment)
-- `yc-bench task cancel --task-id UUID --reason "text"` -- cancel (prestige penalty)
-- `yc-bench sim resume` -- advance simulation clock
-
-### Memory (persists across context truncation)
-- `yc-bench scratchpad read` -- read your persistent notes
-- `yc-bench scratchpad write --content "text"` -- overwrite notes
-- `yc-bench scratchpad append --content "text"` -- append to notes
-- `yc-bench scratchpad clear` -- clear notes
-
-## Strategy Guidelines
-
-1. **Specialise in 2-3 domains** to climb the prestige ladder faster and unlock
- high-reward tasks. Don't spread thin across all 4 domains early on.
-2. **Focus employees** -- assigning one employee to many tasks halves their
- throughput per additional task. Keep assignments concentrated.
-3. **Use the scratchpad** to track your strategy, upcoming deadlines, and
- employee assignments. This persists even if conversation context is truncated.
-4. **Monitor runway** -- always know how many months of payroll you can cover.
- Accept high-reward tasks before payroll dates.
-5. **Don't over-accept** -- taking too many tasks and missing deadlines cascades
- into prestige loss, locking you out of profitable contracts.
-6. Use `finance ledger` and `report monthly` to track revenue trends.
-
-## Your Turn
-
-Each turn:
-1. Call `yc-bench company status` and `yc-bench task list` to orient yourself.
-2. Check for completed tasks and pending deadlines.
-3. Browse market for profitable tasks within your prestige level.
-4. Accept, assign, and dispatch tasks strategically.
-5. Call `yc-bench sim resume` to advance time.
-6. Repeat until the simulation ends.
-
-Think step by step before acting."""
-
-# Starting funds in cents ($250,000)
-INITIAL_FUNDS_CENTS = 25_000_000
-
-# Default horizon per preset (years)
-_PRESET_HORIZONS = {
- "tutorial": 1,
- "easy": 1,
- "medium": 1,
- "hard": 1,
- "nightmare": 1,
- "fast_test": 1,
- "default": 3,
- "high_reward": 1,
-}
-
-
-# =============================================================================
-# Configuration
-# =============================================================================
-
-class YCBenchEvalConfig(HermesAgentEnvConfig):
- """
- Configuration for the YC-Bench evaluation environment.
-
- Extends HermesAgentEnvConfig with YC-Bench-specific settings for
- preset selection, seed control, scoring, and simulation parameters.
- """
-
- presets: List[str] = Field(
- default=["fast_test", "medium", "hard"],
- description="YC-Bench preset names to evaluate.",
- )
- seeds: List[int] = Field(
- default=[1, 2, 3],
- description="Random seeds -- each preset x seed = one run.",
- )
- run_timeout: int = Field(
- default=3600,
- description="Maximum wall-clock seconds per run. Default 60 minutes.",
- )
- survival_weight: float = Field(
- default=0.5,
- description="Weight of survival (0/1) in composite score.",
- )
- funds_weight: float = Field(
- default=0.5,
- description="Weight of normalised final funds in composite score.",
- )
- db_dir: str = Field(
- default="/tmp/yc_bench_dbs",
- description="Directory for per-run SQLite databases.",
- )
- horizon_years: Optional[int] = Field(
- default=None,
- description=(
- "Simulation horizon in years. If None (default), inferred from "
- "preset name (1 year for most, 3 for 'default')."
- ),
- )
- company_name: str = Field(
- default="BenchCo",
- description="Name of the simulated company.",
- )
- start_date: str = Field(
- default="01/01/2025",
- description="Simulation start date in MM/DD/YYYY format (yc-bench convention).",
- )
-
-
-# =============================================================================
-# Scoring helpers
-# =============================================================================
-
-def _read_final_score(db_path: str) -> Dict[str, Any]:
- """
- Read final game state from a YC-Bench SQLite database.
-
- Returns dict with final_funds_cents (int), survived (bool),
- terminal_reason (str).
-
- Note: yc-bench table names are plural -- 'companies' not 'company',
- 'sim_events' not 'simulation_log'.
- """
- if not os.path.exists(db_path):
- logger.warning("DB not found at %s", db_path)
- return {
- "final_funds_cents": 0,
- "survived": False,
- "terminal_reason": "db_missing",
- }
-
- conn = None
- try:
- conn = sqlite3.connect(db_path)
- cur = conn.cursor()
-
- # Read final funds from the 'companies' table
- cur.execute("SELECT funds_cents FROM companies LIMIT 1")
- row = cur.fetchone()
- funds = row[0] if row else 0
-
- # Determine terminal reason from 'sim_events' table
- terminal_reason = "unknown"
- try:
- cur.execute(
- "SELECT event_type FROM sim_events "
- "WHERE event_type IN ('bankruptcy', 'horizon_end') "
- "ORDER BY scheduled_at DESC LIMIT 1"
- )
- event_row = cur.fetchone()
- if event_row:
- terminal_reason = event_row[0]
- except sqlite3.OperationalError:
- # Table may not exist if simulation didn't progress
- pass
-
- survived = funds >= 0 and terminal_reason != "bankruptcy"
- return {
- "final_funds_cents": funds,
- "survived": survived,
- "terminal_reason": terminal_reason,
- }
-
- except Exception as e:
- logger.error("Failed to read DB %s: %s", db_path, e)
- return {
- "final_funds_cents": 0,
- "survived": False,
- "terminal_reason": f"db_error: {e}",
- }
- finally:
- if conn:
- conn.close()
-
-
-def _compute_composite_score(
- final_funds_cents: int,
- survived: bool,
- survival_weight: float = 0.5,
- funds_weight: float = 0.5,
- initial_funds_cents: int = INITIAL_FUNDS_CENTS,
-) -> float:
- """
- Compute composite score from survival and final funds.
-
- Score = survival_weight * survival_score
- + funds_weight * normalised_funds_score
-
- Normalised funds uses log-scale relative to initial capital:
- - funds <= 0: 0.0
- - funds == initial: ~0.15
- - funds == 10x: ~0.52
- - funds == 100x: 1.0
- """
- survival_score = 1.0 if survived else 0.0
-
- if final_funds_cents <= 0:
- funds_score = 0.0
- else:
- max_ratio = 100.0
- ratio = final_funds_cents / max(initial_funds_cents, 1)
- funds_score = min(math.log1p(ratio) / math.log1p(max_ratio), 1.0)
-
- return survival_weight * survival_score + funds_weight * funds_score
-
-
-# =============================================================================
-# Main Environment
-# =============================================================================
-
-class YCBenchEvalEnv(HermesAgentBaseEnv):
- """
- YC-Bench long-horizon agent benchmark environment (eval-only).
-
- Each eval item is a (preset, seed) pair. The environment initialises the
- simulation via ``yc-bench sim init`` (NOT ``yc-bench run`` which would start
- a competing built-in agent loop). The HermesAgentLoop then drives the
- interaction by calling individual yc-bench CLI commands via the terminal tool.
-
- After the agent loop ends, the SQLite DB is read to extract the final score.
-
- Scoring:
- composite = 0.5 * survival + 0.5 * normalised_funds
- """
-
- name = "yc-bench"
- env_config_cls = YCBenchEvalConfig
-
- @classmethod
- def config_init(cls) -> Tuple[YCBenchEvalConfig, List[APIServerConfig]]:
- env_config = YCBenchEvalConfig(
- enabled_toolsets=["terminal"],
- disabled_toolsets=None,
- distribution=None,
- max_agent_turns=200,
- max_token_length=32000,
- agent_temperature=0.0,
- system_prompt=YC_BENCH_SYSTEM_PROMPT,
- terminal_backend="local",
- terminal_timeout=60,
- presets=["fast_test", "medium", "hard"],
- seeds=[1, 2, 3],
- run_timeout=3600,
- survival_weight=0.5,
- funds_weight=0.5,
- db_dir="/tmp/yc_bench_dbs",
- eval_handling=EvalHandlingEnum.STOP_TRAIN,
- group_size=1,
- steps_per_eval=1,
- total_steps=1,
- tokenizer_name="NousResearch/Hermes-3-Llama-3.1-8B",
- use_wandb=True,
- wandb_name="yc-bench",
- ensure_scores_are_not_same=False,
- )
-
- server_configs = [
- APIServerConfig(
- base_url="https://openrouter.ai/api/v1",
- model_name="anthropic/claude-sonnet-4.6",
- server_type="openai",
- api_key=os.getenv("OPENROUTER_API_KEY", ""),
- health_check=False,
- )
- ]
-
- return env_config, server_configs
-
- # =========================================================================
- # Setup
- # =========================================================================
-
- async def setup(self):
- """Verify yc-bench is installed and build the eval matrix."""
- # Verify yc-bench CLI is available
- try:
- result = subprocess.run(
- ["yc-bench", "--help"], capture_output=True, text=True, timeout=10
- )
- if result.returncode != 0:
- raise FileNotFoundError
- except (FileNotFoundError, subprocess.TimeoutExpired):
- raise RuntimeError(
- "yc-bench CLI not found. Install with:\n"
- ' pip install "hermes-agent[yc-bench]"\n'
- "Or: git clone https://github.com/collinear-ai/yc-bench "
- "&& cd yc-bench && pip install -e ."
- )
- print("yc-bench CLI verified.")
-
- # Build eval matrix: preset x seed
- self.all_eval_items = [
- {"preset": preset, "seed": seed}
- for preset in self.config.presets
- for seed in self.config.seeds
- ]
- self.iter = 0
-
- os.makedirs(self.config.db_dir, exist_ok=True)
- self.eval_metrics: List[Tuple[str, float]] = []
-
- # Streaming JSONL log for crash-safe result persistence
- log_dir = os.path.join(os.path.dirname(__file__), "logs")
- os.makedirs(log_dir, exist_ok=True)
- run_ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
- self._streaming_path = os.path.join(log_dir, f"samples_{run_ts}.jsonl")
- self._streaming_file = open(self._streaming_path, "w", encoding="utf-8")
- self._streaming_lock = threading.Lock()
-
- print(f"\nYC-Bench eval matrix: {len(self.all_eval_items)} runs")
- for item in self.all_eval_items:
- print(f" preset={item['preset']!r} seed={item['seed']}")
- print(f"Streaming results to: {self._streaming_path}\n")
-
- def _save_result(self, result: Dict[str, Any]):
- """Write a single run result to the streaming JSONL file immediately."""
- if not hasattr(self, "_streaming_file") or self._streaming_file.closed:
- return
- with self._streaming_lock:
- self._streaming_file.write(
- json.dumps(result, ensure_ascii=False, default=str) + "\n"
- )
- self._streaming_file.flush()
-
- # =========================================================================
- # Training pipeline stubs (eval-only -- not used)
- # =========================================================================
-
- async def get_next_item(self):
- item = self.all_eval_items[self.iter % len(self.all_eval_items)]
- self.iter += 1
- return item
-
- def format_prompt(self, item: Dict[str, Any]) -> str:
- preset = item["preset"]
- seed = item["seed"]
- return (
- f"A new YC-Bench simulation has been initialized "
- f"(preset='{preset}', seed={seed}).\n"
- f"Your company '{self.config.company_name}' is ready.\n\n"
- "Begin by calling:\n"
- "1. `yc-bench company status` -- see your starting funds and prestige\n"
- "2. `yc-bench employee list` -- see your team and their skills\n"
- "3. `yc-bench market browse --required-prestige-lte 1` -- find tasks "
- "you can take\n\n"
- "Then accept 2-3 tasks, assign employees, dispatch them, and call "
- "`yc-bench sim resume` to advance time. Repeat this loop until the "
- "simulation ends (horizon reached or bankruptcy)."
- )
-
- async def compute_reward(self, item, result, ctx) -> float:
- return 0.0
-
- async def collect_trajectories(self, item):
- return None, []
-
- async def score(self, rollout_group_data):
- return None
-
- # =========================================================================
- # Per-run evaluation
- # =========================================================================
-
- async def rollout_and_score_eval(self, eval_item: Dict[str, Any]) -> Dict:
- """
- Evaluate a single (preset, seed) run.
-
- 1. Sets DATABASE_URL and YC_BENCH_EXPERIMENT env vars
- 2. Initialises the simulation via ``yc-bench sim init`` (NOT ``run``)
- 3. Runs HermesAgentLoop with terminal tool
- 4. Reads SQLite DB to compute final score
- 5. Returns result dict with survival, funds, and composite score
- """
- preset = eval_item["preset"]
- seed = eval_item["seed"]
- run_id = str(uuid.uuid4())[:8]
- run_key = f"{preset}_seed{seed}_{run_id}"
-
- from tqdm import tqdm
- tqdm.write(f" [START] preset={preset!r} seed={seed} (run_id={run_id})")
- run_start = time.time()
-
- # Isolated DB per run -- prevents cross-run state leakage
- db_path = os.path.join(self.config.db_dir, f"yc_bench_{run_key}.db")
- os.environ["DATABASE_URL"] = f"sqlite:///{db_path}"
- os.environ["YC_BENCH_EXPERIMENT"] = preset
-
- # Determine horizon: explicit config override > preset lookup > default 1
- horizon = self.config.horizon_years or _PRESET_HORIZONS.get(preset, 1)
-
- try:
- # ----------------------------------------------------------
- # Step 1: Initialise the simulation via CLI
- # IMPORTANT: We use `sim init`, NOT `yc-bench run`.
- # `yc-bench run` starts yc-bench's own LLM agent loop (via
- # LiteLLM), which would compete with our HermesAgentLoop.
- # `sim init` just sets up the world and returns.
- # ----------------------------------------------------------
- init_cmd = [
- "yc-bench", "sim", "init",
- "--seed", str(seed),
- "--start-date", self.config.start_date,
- "--company-name", self.config.company_name,
- "--horizon-years", str(horizon),
- ]
- init_result = subprocess.run(
- init_cmd, capture_output=True, text=True, timeout=30,
- )
- if init_result.returncode != 0:
- error_msg = (init_result.stderr or init_result.stdout).strip()
- raise RuntimeError(f"yc-bench sim init failed: {error_msg}")
-
- tqdm.write(f" Simulation initialized (horizon={horizon}yr)")
-
- # ----------------------------------------------------------
- # Step 2: Run the HermesAgentLoop
- # ----------------------------------------------------------
- tools, valid_names = self._resolve_tools_for_group()
-
- messages: List[Dict[str, Any]] = [
- {"role": "system", "content": YC_BENCH_SYSTEM_PROMPT},
- {"role": "user", "content": self.format_prompt(eval_item)},
- ]
-
- agent = HermesAgentLoop(
- server=self.server,
- tool_schemas=tools,
- valid_tool_names=valid_names,
- max_turns=self.config.max_agent_turns,
- task_id=run_id,
- temperature=self.config.agent_temperature,
- max_tokens=self.config.max_token_length,
- extra_body=self.config.extra_body,
- budget_config=self.config.build_budget_config(),
- )
- result = await agent.run(messages)
-
- # ----------------------------------------------------------
- # Step 3: Read final score from the simulation DB
- # ----------------------------------------------------------
- score_data = _read_final_score(db_path)
- final_funds = score_data["final_funds_cents"]
- survived = score_data["survived"]
- terminal_reason = score_data["terminal_reason"]
-
- composite = _compute_composite_score(
- final_funds_cents=final_funds,
- survived=survived,
- survival_weight=self.config.survival_weight,
- funds_weight=self.config.funds_weight,
- )
-
- elapsed = time.time() - run_start
- status = "SURVIVED" if survived else "BANKRUPT"
- if final_funds >= 0:
- funds_str = f"${final_funds / 100:,.0f}"
- else:
- funds_str = f"-${abs(final_funds) / 100:,.0f}"
-
- tqdm.write(
- f" [{status}] preset={preset!r} seed={seed} "
- f"funds={funds_str} score={composite:.3f} "
- f"turns={result.turns_used} ({elapsed:.0f}s)"
- )
-
- out = {
- "preset": preset,
- "seed": seed,
- "survived": survived,
- "final_funds_cents": final_funds,
- "final_funds_usd": final_funds / 100,
- "terminal_reason": terminal_reason,
- "composite_score": composite,
- "turns_used": result.turns_used,
- "finished_naturally": result.finished_naturally,
- "elapsed_seconds": elapsed,
- "db_path": db_path,
- "messages": result.messages,
- }
- self._save_result(out)
- return out
-
- except Exception as e:
- elapsed = time.time() - run_start
- logger.error("Run %s failed: %s", run_key, e, exc_info=True)
- tqdm.write(
- f" [ERROR] preset={preset!r} seed={seed}: {e} ({elapsed:.0f}s)"
- )
- out = {
- "preset": preset,
- "seed": seed,
- "survived": False,
- "final_funds_cents": 0,
- "final_funds_usd": 0.0,
- "terminal_reason": f"error: {e}",
- "composite_score": 0.0,
- "turns_used": 0,
- "error": str(e),
- "elapsed_seconds": elapsed,
- }
- self._save_result(out)
- return out
-
- # =========================================================================
- # Evaluate
- # =========================================================================
-
- async def _run_with_timeout(self, item: Dict[str, Any]) -> Dict:
- """Wrap a single rollout with a wall-clock timeout."""
- preset = item["preset"]
- seed = item["seed"]
- try:
- return await asyncio.wait_for(
- self.rollout_and_score_eval(item),
- timeout=self.config.run_timeout,
- )
- except asyncio.TimeoutError:
- from tqdm import tqdm
- tqdm.write(
- f" [TIMEOUT] preset={preset!r} seed={seed} "
- f"(exceeded {self.config.run_timeout}s)"
- )
- out = {
- "preset": preset,
- "seed": seed,
- "survived": False,
- "final_funds_cents": 0,
- "final_funds_usd": 0.0,
- "terminal_reason": f"timeout ({self.config.run_timeout}s)",
- "composite_score": 0.0,
- "turns_used": 0,
- "error": "timeout",
- }
- self._save_result(out)
- return out
-
- async def evaluate(self, *args, **kwargs) -> None:
- """
- Run YC-Bench evaluation over all (preset, seed) combinations.
-
- Runs sequentially -- each run is 100-500 turns, parallelising would
- be prohibitively expensive and cause env var conflicts.
- """
- start_time = time.time()
- from tqdm import tqdm
-
- # --- tqdm-compatible logging handler (TB2 pattern) ---
- class _TqdmHandler(logging.Handler):
- def emit(self, record):
- try:
- tqdm.write(self.format(record))
- except Exception:
- self.handleError(record)
-
- root = logging.getLogger()
- handler = _TqdmHandler()
- handler.setFormatter(
- logging.Formatter("%(levelname)s %(name)s: %(message)s")
- )
- root.handlers = [handler]
- for noisy in ("httpx", "openai"):
- logging.getLogger(noisy).setLevel(logging.WARNING)
-
- # --- Print config summary ---
- print(f"\n{'='*60}")
- print("Starting YC-Bench Evaluation")
- print(f"{'='*60}")
- print(f" Presets: {self.config.presets}")
- print(f" Seeds: {self.config.seeds}")
- print(f" Total runs: {len(self.all_eval_items)}")
- print(f" Max turns/run: {self.config.max_agent_turns}")
- print(f" Run timeout: {self.config.run_timeout}s")
- print(f"{'='*60}\n")
-
- results = []
- pbar = tqdm(
- total=len(self.all_eval_items), desc="YC-Bench", dynamic_ncols=True
- )
-
- try:
- for item in self.all_eval_items:
- result = await self._run_with_timeout(item)
- results.append(result)
- survived_count = sum(1 for r in results if r.get("survived"))
- pbar.set_postfix_str(
- f"survived={survived_count}/{len(results)}"
- )
- pbar.update(1)
-
- except (KeyboardInterrupt, asyncio.CancelledError):
- tqdm.write("\n[INTERRUPTED] Stopping evaluation...")
- pbar.close()
- try:
- from tools.terminal_tool import cleanup_all_environments
- cleanup_all_environments()
- except Exception:
- pass
- if hasattr(self, "_streaming_file") and not self._streaming_file.closed:
- self._streaming_file.close()
- return
-
- pbar.close()
- end_time = time.time()
-
- # --- Compute metrics ---
- valid = [r for r in results if r is not None]
- if not valid:
- print("Warning: No valid results.")
- return
-
- total = len(valid)
- survived_total = sum(1 for r in valid if r.get("survived"))
- survival_rate = survived_total / total if total else 0.0
- avg_score = (
- sum(r.get("composite_score", 0) for r in valid) / total
- if total
- else 0.0
- )
-
- preset_results: Dict[str, List[Dict]] = defaultdict(list)
- for r in valid:
- preset_results[r["preset"]].append(r)
-
- eval_metrics = {
- "eval/survival_rate": survival_rate,
- "eval/avg_composite_score": avg_score,
- "eval/total_runs": total,
- "eval/survived_runs": survived_total,
- "eval/evaluation_time_seconds": end_time - start_time,
- }
-
- for preset, items in sorted(preset_results.items()):
- ps = sum(1 for r in items if r.get("survived"))
- pt = len(items)
- pa = (
- sum(r.get("composite_score", 0) for r in items) / pt
- if pt
- else 0
- )
- key = preset.replace("-", "_")
- eval_metrics[f"eval/survival_rate_{key}"] = ps / pt if pt else 0
- eval_metrics[f"eval/avg_score_{key}"] = pa
-
- self.eval_metrics = list(eval_metrics.items())
-
- # --- Print summary ---
- print(f"\n{'='*60}")
- print("YC-Bench Evaluation Results")
- print(f"{'='*60}")
- print(
- f"Overall survival rate: {survival_rate:.1%} "
- f"({survived_total}/{total})"
- )
- print(f"Average composite score: {avg_score:.4f}")
- print(f"Evaluation time: {end_time - start_time:.1f}s")
-
- print("\nPer-preset breakdown:")
- for preset, items in sorted(preset_results.items()):
- ps = sum(1 for r in items if r.get("survived"))
- pt = len(items)
- pa = (
- sum(r.get("composite_score", 0) for r in items) / pt
- if pt
- else 0
- )
- print(f" {preset}: {ps}/{pt} survived avg_score={pa:.4f}")
- for r in items:
- status = "SURVIVED" if r.get("survived") else "BANKRUPT"
- funds = r.get("final_funds_usd", 0)
- print(
- f" seed={r['seed']} [{status}] "
- f"${funds:,.0f} "
- f"score={r.get('composite_score', 0):.3f}"
- )
-
- print(f"{'='*60}\n")
-
- # --- Log results ---
- samples = [
- {k: v for k, v in r.items() if k != "messages"} for r in valid
- ]
-
- try:
- await self.evaluate_log(
- metrics=eval_metrics,
- samples=samples,
- start_time=start_time,
- end_time=end_time,
- generation_parameters={
- "temperature": self.config.agent_temperature,
- "max_tokens": self.config.max_token_length,
- "max_agent_turns": self.config.max_agent_turns,
- },
- )
- except Exception as e:
- print(f"Error logging results: {e}")
-
- # --- Cleanup (TB2 pattern) ---
- if hasattr(self, "_streaming_file") and not self._streaming_file.closed:
- self._streaming_file.close()
- print(f"Results saved to: {self._streaming_path}")
-
- try:
- from tools.terminal_tool import cleanup_all_environments
- cleanup_all_environments()
- except Exception:
- pass
-
- try:
- from environments.agent_loop import _tool_executor
- _tool_executor.shutdown(wait=False, cancel_futures=True)
- except Exception:
- pass
-
- # =========================================================================
- # Wandb logging
- # =========================================================================
-
- async def wandb_log(self, wandb_metrics: Optional[Dict] = None):
- """Log YC-Bench-specific metrics to wandb."""
- if wandb_metrics is None:
- wandb_metrics = {}
- for k, v in self.eval_metrics:
- wandb_metrics[k] = v
- self.eval_metrics = []
- await super().wandb_log(wandb_metrics)
-
-
-if __name__ == "__main__":
- YCBenchEvalEnv.cli()
diff --git a/environments/hermes_base_env.py b/environments/hermes_base_env.py
deleted file mode 100644
index adefa9b7c3c..00000000000
--- a/environments/hermes_base_env.py
+++ /dev/null
@@ -1,714 +0,0 @@
-"""
-HermesAgentBaseEnv -- Abstract Base Environment for Hermes-Agent + Atropos
-
-Provides the Atropos integration plumbing that all hermes-agent environments share:
-- Two-mode operation (OpenAI server for Phase 1, VLLM ManagedServer for Phase 2)
-- Per-group toolset/distribution resolution
-- Agent loop orchestration via HermesAgentLoop
-- ToolContext creation for reward functions
-- ScoredDataGroup construction from ManagedServer state
-
-Subclasses only need to implement:
- setup() -- Load dataset, initialize state
- get_next_item() -- Return the next item from the dataset
- format_prompt() -- Convert a dataset item into the user message
- compute_reward() -- Score the rollout (has full ToolContext access)
- evaluate() -- Periodic evaluation
-"""
-
-import asyncio
-import json
-import logging
-import os
-import sys
-import uuid
-from abc import abstractmethod
-from pathlib import Path
-from typing import Any, Dict, List, Optional, Set, Tuple, Union
-
-# Ensure the hermes-agent repo root is on sys.path so that imports like
-# `from model_tools import ...` and `from environments.X import ...` work
-# regardless of where the script is invoked from.
-_repo_root = Path(__file__).resolve().parent.parent
-if str(_repo_root) not in sys.path:
- sys.path.insert(0, str(_repo_root))
-
-from dotenv import load_dotenv
-from pydantic import Field
-
-# Load API keys from hermes-agent/.env so all environments can access them
-_env_path = _repo_root / ".env"
-if _env_path.exists():
- load_dotenv(dotenv_path=_env_path)
-
-# Apply monkey patches for async-safe tool operation inside Atropos's event loop.
-# This patches SwerexModalEnvironment to use a background thread instead of
-# asyncio.run(), which would deadlock inside Atropos. Safe for normal CLI too.
-from environments.patches import apply_patches
-apply_patches()
-
-from atroposlib.envs.base import (
- BaseEnv,
- BaseEnvConfig,
- ScoredDataGroup,
- ScoredDataItem,
-)
-from atroposlib.envs.server_handling.server_manager import (
- APIServerConfig,
- ServerBaseline,
- ServerManager,
-)
-from atroposlib.type_definitions import Item
-
-from environments.agent_loop import AgentResult, HermesAgentLoop
-from environments.tool_context import ToolContext
-from tools.budget_config import (
- DEFAULT_RESULT_SIZE_CHARS,
- DEFAULT_TURN_BUDGET_CHARS,
- DEFAULT_PREVIEW_SIZE_CHARS,
-)
-
-# Import hermes-agent toolset infrastructure
-from model_tools import get_tool_definitions
-from toolset_distributions import sample_toolsets_from_distribution
-
-logger = logging.getLogger(__name__)
-
-
-class HermesAgentEnvConfig(BaseEnvConfig):
- """
- Configuration for hermes-agent Atropos environments.
-
- Extends BaseEnvConfig with agent-specific settings for toolsets,
- terminal backend, dataset loading, and tool call parsing.
- """
-
- # --- Toolset configuration ---
- # Mutually exclusive: use either enabled_toolsets OR distribution
- enabled_toolsets: Optional[List[str]] = Field(
- default=None,
- description="Explicit list of hermes toolsets to enable (e.g., ['terminal', 'file', 'web']). "
- "If None and distribution is also None, all available toolsets are enabled.",
- )
- disabled_toolsets: Optional[List[str]] = Field(
- default=None,
- description="Toolsets to disable. Applied as a filter on top of enabled_toolsets or distribution.",
- )
- distribution: Optional[str] = Field(
- default=None,
- description="Name of a toolset distribution from toolset_distributions.py "
- "(e.g., 'development', 'terminal_tasks'). Sampled once per group. "
- "Mutually exclusive with enabled_toolsets.",
- )
-
- # --- Agent loop configuration ---
- max_agent_turns: int = Field(
- default=30,
- description="Maximum number of LLM calls (tool-calling iterations) per rollout.",
- )
- system_prompt: Optional[str] = Field(
- default=None,
- description="System prompt for the agent. Tools are handled via the tools= parameter, "
- "not embedded in the prompt text.",
- )
- agent_temperature: float = Field(
- default=1.0,
- description="Sampling temperature for agent generation during rollouts.",
- )
-
- # --- Terminal backend ---
- terminal_backend: str = Field(
- default="local",
- description="Terminal backend: 'local', 'docker', 'modal', 'daytona', 'ssh', 'singularity'. "
- "Modal or Daytona recommended for production RL (cloud isolation per rollout).",
- )
- terminal_timeout: int = Field(
- default=120,
- description="Per-command timeout in seconds for terminal tool calls. "
- "Commands exceeding this are killed. Increase for tasks with long-running "
- "commands (compilation, pip install, etc.).",
- )
- terminal_lifetime: int = Field(
- default=3600,
- description="Sandbox inactivity lifetime in seconds. The cleanup thread kills "
- "sandboxes that have been idle longer than this. Must be longer than "
- "the longest gap between tool calls (e.g., waiting for LLM response).",
- )
-
- # --- Dataset ---
- dataset_name: Optional[str] = Field(
- default=None,
- description="HuggingFace dataset name. Optional if tasks are defined inline.",
- )
- dataset_split: str = Field(
- default="train",
- description="Dataset split to use.",
- )
- prompt_field: str = Field(
- default="prompt",
- description="Which field in the dataset contains the prompt.",
- )
-
- # --- Thread pool ---
- tool_pool_size: int = Field(
- default=128,
- description="Thread pool size for tool execution. Each concurrent task needs a "
- "thread for tool calls. Must be large enough for parallel evaluation. "
- "Too small = thread pool starvation.",
- )
-
- # --- Phase 2: Tool call parsing ---
- tool_call_parser: str = Field(
- default="hermes",
- description="Tool call parser name for Phase 2 (VLLM server type). "
- "Ignored in Phase 1 (OpenAI server type where VLLM parses natively). "
- "Options: hermes, mistral, llama3_json, qwen, deepseek_v3, etc.",
- )
-
- # --- Tool result budget ---
- # Defaults imported from tools.budget_config (single source of truth).
- default_result_size_chars: int = Field(
- default=DEFAULT_RESULT_SIZE_CHARS,
- description="Default per-tool threshold (chars) for persisting large results "
- "to sandbox. Results exceeding this are written to /tmp/hermes-results/ "
- "and replaced with a preview. Per-tool registry values take precedence "
- "unless overridden via tool_result_overrides.",
- )
- turn_budget_chars: int = Field(
- default=DEFAULT_TURN_BUDGET_CHARS,
- description="Aggregate char budget per assistant turn. If all tool results "
- "in a single turn exceed this, the largest are persisted to disk first.",
- )
- preview_size_chars: int = Field(
- default=DEFAULT_PREVIEW_SIZE_CHARS,
- description="Size of the inline preview shown after a tool result is persisted.",
- )
- tool_result_overrides: Optional[Dict[str, int]] = Field(
- default=None,
- description="Per-tool threshold overrides (chars). Keys are tool names, "
- "values are char thresholds. Overrides both the default and registry "
- "per-tool values. Example: {'terminal': 10000, 'search_files': 5000}. "
- "Note: read_file is pinned to infinity and cannot be overridden.",
- )
-
- # --- Provider-specific parameters ---
- # Passed as extra_body to the OpenAI client's chat.completions.create() call.
- # Useful for OpenRouter provider preferences, transforms, route settings, etc.
- # Example YAML:
- # extra_body:
- # provider:
- # ignore: ["DeepInfra", "Fireworks"]
- # order: ["Together"]
- # transforms: ["middle-out"]
- extra_body: Optional[Dict[str, Any]] = Field(
- default=None,
- description="Extra body parameters passed to the OpenAI client's "
- "chat.completions.create(). Used for OpenRouter provider preferences, "
- "transforms, and other provider-specific settings.",
- )
-
- def build_budget_config(self):
- """Build a BudgetConfig from env config fields."""
- from tools.budget_config import BudgetConfig
- return BudgetConfig(
- default_result_size=self.default_result_size_chars,
- turn_budget=self.turn_budget_chars,
- preview_size=self.preview_size_chars,
- tool_overrides=dict(self.tool_result_overrides) if self.tool_result_overrides else {},
- )
-
-
-class HermesAgentBaseEnv(BaseEnv):
- """
- Abstract base environment for hermes-agent Atropos integration.
-
- Handles two modes of operation:
- - Phase 1 (OpenAI server type): Uses server.chat_completion() directly.
- The server (VLLM, SGLang, OpenRouter, OpenAI) handles tool call parsing
- and reasoning extraction natively. DummyManagedServer provides placeholder
- tokens. Good for SFT data gen, verifier testing, evaluation.
-
- - Phase 2 (VLLM server type): Uses ManagedServer for exact token IDs + logprobs
- via /generate. Client-side tool call parser reconstructs structured tool_calls
- from raw output. Full RL training capability.
-
- Subclasses must implement:
- setup() -- Load dataset, initialize state
- get_next_item() -- Return the next item to roll out
- format_prompt() -- Convert a dataset item into the user message string
- compute_reward() -- Score the rollout using ToolContext
- evaluate() -- Periodic evaluation
- """
-
- name: Optional[str] = "hermes-agent"
- env_config_cls = HermesAgentEnvConfig
-
- def __init__(
- self,
- config: HermesAgentEnvConfig,
- server_configs: Union[ServerBaseline, List[APIServerConfig]],
- slurm=False,
- testing=False,
- ):
- super().__init__(config, server_configs, slurm, testing)
-
- # Set terminal environment variables so hermes tools pick them up.
- # These can all be overridden per-environment via config fields instead
- # of requiring users to set shell env vars.
- if config.terminal_backend:
- os.environ["TERMINAL_ENV"] = config.terminal_backend
- os.environ["TERMINAL_TIMEOUT"] = str(config.terminal_timeout)
- os.environ["TERMINAL_LIFETIME_SECONDS"] = str(config.terminal_lifetime)
- print(
- f"🖥️ Terminal: backend={config.terminal_backend}, "
- f"timeout={config.terminal_timeout}s, lifetime={config.terminal_lifetime}s"
- )
-
- # Resize the agent loop's thread pool for tool execution.
- # This must be large enough for the number of concurrent tasks
- # (e.g., 89 parallel TB2 eval tasks each need a thread for tool calls).
- from environments.agent_loop import resize_tool_pool
- resize_tool_pool(config.tool_pool_size)
-
- # Set tool_parser on the ServerManager so ManagedServer uses it
- # for bidirectional tool call translation (raw text ↔ OpenAI tool_calls).
- if hasattr(self.server, 'tool_parser'):
- self.server.tool_parser = config.tool_call_parser
- print(f"🔧 Tool parser: {config.tool_call_parser}")
-
- # Current group's resolved tools (set in collect_trajectories)
- self._current_group_tools: Optional[Tuple[List[Dict], Set[str]]] = None
-
- # Tool error tracking for wandb logging
- self._tool_error_buffer: List[Dict[str, Any]] = []
-
- # =========================================================================
- # Toolset resolution (per-group)
- # =========================================================================
-
- def _resolve_tools_for_group(self) -> Tuple[List[Dict[str, Any]], Set[str]]:
- """
- Resolve toolsets for a group. Called once in collect_trajectories(),
- then shared by all collect_trajectory() calls in the group.
-
- If distribution is set, samples probabilistically.
- If enabled_toolsets is set, uses that explicit list.
- disabled_toolsets is applied as a filter on top.
-
- Returns:
- (tool_schemas, valid_tool_names) tuple
- """
- config = self.config
-
- if config.distribution:
- group_toolsets = sample_toolsets_from_distribution(config.distribution)
- logger.info("Sampled toolsets from '%s': %s", config.distribution, group_toolsets)
- else:
- group_toolsets = config.enabled_toolsets # None means "all available"
- if group_toolsets is None:
- logger.warning(
- "enabled_toolsets is None -- loading ALL tools including messaging. "
- "Set explicit enabled_toolsets for RL training."
- )
-
- tools = get_tool_definitions(
- enabled_toolsets=group_toolsets,
- disabled_toolsets=config.disabled_toolsets,
- quiet_mode=True,
- )
-
- valid_names = {t["function"]["name"] for t in tools} if tools else set()
- logger.info("Resolved %d tools for group: %s", len(valid_names), sorted(valid_names))
- return tools, valid_names
-
- # =========================================================================
- # Server mode detection
- # =========================================================================
-
- def _use_managed_server(self) -> bool:
- """
- Determine if we should use ManagedServer (Phase 2) or direct server (Phase 1).
-
- Phase 2 (ManagedServer) is used when the server type is 'vllm' or 'sglang',
- which go through the /generate endpoint for exact token tracking.
-
- Phase 1 (direct server) is used for 'openai' server type, which uses
- /v1/chat/completions with native tool call parsing.
- """
- if not self.server.servers:
- return False
-
- server = self.server.servers[0]
- # If the server is an OpenAI server (not VLLM/SGLang), use direct mode
- from atroposlib.envs.server_handling.openai_server import OpenAIServer
- return not isinstance(server, OpenAIServer)
-
- # =========================================================================
- # Core Atropos integration
- # =========================================================================
-
- async def collect_trajectories(
- self, item: Item
- ) -> Tuple[
- Union[Optional[ScoredDataGroup], List[Optional[ScoredDataGroup]]],
- List[Item],
- ]:
- """
- Override collect_trajectories to resolve toolsets once per group,
- then delegate to the standard group-level collection.
-
- The default BaseEnv.collect_trajectories() calls collect_trajectory()
- group_size times in parallel. We resolve tools once here and store
- them for all those calls to use.
- """
- # Resolve toolsets for this group (shared by all rollouts in the group)
- self._current_group_tools = self._resolve_tools_for_group()
-
- # Delegate to the default implementation which calls collect_trajectory()
- # group_size times via asyncio.gather
- return await super().collect_trajectories(item)
-
- # =========================================================================
- # Wandb rollout display -- format trajectories nicely
- # =========================================================================
-
- @staticmethod
- def _format_trajectory_for_display(messages: List[Dict[str, Any]]) -> str:
- """
- Format a conversation's messages into a readable trajectory string
- for wandb rollout tables. Shows tool calls, tool results, and reasoning
- in a structured way instead of raw token decoding.
- """
- parts = []
- for msg in messages:
- role = msg.get("role", "unknown")
- content = msg.get("content", "")
-
- if role == "system":
- parts.append(f"[SYSTEM]\n{content}")
-
- elif role == "user":
- parts.append(f"[USER]\n{content}")
-
- elif role == "assistant":
- # Show reasoning if present
- reasoning = msg.get("reasoning_content", "")
- if reasoning:
- # Truncate long reasoning for display
- if len(reasoning) > 300:
- reasoning = reasoning[:300] + "..."
- parts.append(f"[ASSISTANT thinking]\n{reasoning}")
-
- # Show content
- if content:
- parts.append(f"[ASSISTANT]\n{content}")
-
- # Show tool calls
- tool_calls = msg.get("tool_calls", [])
- for tc in tool_calls:
- func = tc.get("function", {})
- name = func.get("name", "?")
- args = func.get("arguments", "{}")
- # Truncate long arguments for display
- if len(args) > 200:
- args = args[:200] + "..."
- parts.append(f"[TOOL CALL] {name}({args})")
-
- elif role == "tool":
- tool_id = msg.get("tool_call_id", "")
- result = content
- # Truncate long tool results for display
- if len(result) > 500:
- result = result[:500] + "..."
- parts.append(f"[TOOL RESULT] {result}")
-
- return "\n\n".join(parts)
-
- async def add_rollouts_for_wandb(
- self,
- scored_data,
- item=None,
- ):
- """
- Override to show formatted trajectories with tool calls visible,
- instead of raw token decoding which loses all structure.
- """
- num_keep = self.config.num_rollouts_per_group_for_logging
- if num_keep == -1:
- num_keep = self.config.group_size
-
- group = []
- for i in range(min(num_keep, len(scored_data.get("scores", [])))):
- score = scored_data["scores"][i]
-
- # Use messages if available for rich display
- messages = None
- if scored_data.get("messages") and i < len(scored_data["messages"]):
- messages = scored_data["messages"][i]
-
- if messages:
- text = self._format_trajectory_for_display(messages)
- elif scored_data.get("tokens") and i < len(scored_data["tokens"]):
- text = self.tokenizer.decode(scored_data["tokens"][i])
- else:
- text = "(no data)"
-
- group.append((text, score))
-
- self.rollouts_for_wandb.append(group)
- if len(self.rollouts_for_wandb) > self.config.num_rollouts_to_keep:
- self.rollouts_for_wandb.pop(0)
-
- async def wandb_log(self, wandb_metrics: Optional[Dict] = None):
- """Log base metrics including tool errors to wandb."""
- if wandb_metrics is None:
- wandb_metrics = {}
-
- # Log tool error stats
- if self._tool_error_buffer:
- wandb_metrics["train/tool_errors_count"] = len(self._tool_error_buffer)
-
- # Log error details as a summary string (tables can crash wandb on tmp cleanup)
- error_summaries = []
- for err in self._tool_error_buffer:
- error_summaries.append(
- f"[turn {err['turn']}] {err['tool']}({err['args'][:80]}) -> {err['error'][:150]}"
- )
- wandb_metrics["train/tool_error_details"] = "\n".join(error_summaries)
-
- # Also print to stdout for immediate visibility
- for summary in error_summaries:
- print(f" Tool Error: {summary}")
-
- self._tool_error_buffer = []
- else:
- wandb_metrics["train/tool_errors_count"] = 0
-
- await super().wandb_log(wandb_metrics)
-
- async def collect_trajectory(
- self, item: Item
- ) -> Tuple[Optional[Union[ScoredDataItem, Any]], List[Item]]:
- """
- Run a single rollout: agent loop + reward computation.
-
- This is called group_size times in parallel by collect_trajectories().
- Each call gets its own task_id for terminal/browser session isolation.
- """
- task_id = str(uuid.uuid4())
-
- # Get group-level tools (resolved once in collect_trajectories)
- if self._current_group_tools is None:
- # Fallback: resolve per-trajectory if called outside collect_trajectories
- tools, valid_names = self._resolve_tools_for_group()
- else:
- tools, valid_names = self._current_group_tools
-
- # Build initial messages
- messages: List[Dict[str, Any]] = []
- if self.config.system_prompt:
- messages.append({"role": "system", "content": self.config.system_prompt})
- messages.append({"role": "user", "content": self.format_prompt(item)})
-
- # Run the agent loop
- result: AgentResult
- if self._use_managed_server():
- # Phase 2: ManagedServer with ToolCallTranslator -- exact tokens + logprobs
- # tool_parser is set on ServerManager in __init__ and passed through
- # to ManagedServer, which uses ToolCallTranslator for bidirectional
- # translation between raw text and OpenAI tool_calls.
- try:
- async with self.server.managed_server(
- tokenizer=self.tokenizer,
- preserve_think_blocks=bool(self.config.thinking_mode),
- ) as managed:
- agent = HermesAgentLoop(
- server=managed,
- tool_schemas=tools,
- valid_tool_names=valid_names,
- max_turns=self.config.max_agent_turns,
- task_id=task_id,
- temperature=self.config.agent_temperature,
- max_tokens=self.config.max_token_length,
- extra_body=self.config.extra_body,
- budget_config=self.config.build_budget_config(),
- )
- result = await agent.run(messages)
- except NotImplementedError:
- # DummyManagedServer not allowed -- fall back to Phase 1
- logger.warning(
- "ManagedServer not available (OpenAI server?). "
- "Falling back to direct server mode."
- )
- agent = HermesAgentLoop(
- server=self.server,
- tool_schemas=tools,
- valid_tool_names=valid_names,
- max_turns=self.config.max_agent_turns,
- task_id=task_id,
- temperature=self.config.agent_temperature,
- max_tokens=self.config.max_token_length,
- extra_body=self.config.extra_body,
- budget_config=self.config.build_budget_config(),
- )
- result = await agent.run(messages)
- else:
- # Phase 1: OpenAI server -- native tool_calls, placeholder tokens
- agent = HermesAgentLoop(
- server=self.server,
- tool_schemas=tools,
- valid_tool_names=valid_names,
- max_turns=self.config.max_agent_turns,
- task_id=task_id,
- temperature=self.config.agent_temperature,
- max_tokens=self.config.max_token_length,
- extra_body=self.config.extra_body,
- budget_config=self.config.build_budget_config(),
- )
- result = await agent.run(messages)
-
- # Skip reward computation if the agent loop produced no meaningful work
- # (e.g., API call failed on turn 1). No point spinning up a Modal sandbox
- # just to verify files that were never created.
- only_system_and_user = all(
- msg.get("role") in {"system", "user"} for msg in result.messages
- )
- if result.turns_used == 0 or only_system_and_user:
- logger.warning(
- "Agent loop produced no output (turns=%d, msgs=%d). Skipping reward.",
- result.turns_used, len(result.messages),
- )
- reward = 0.0
- else:
- # Compute reward using ToolContext (gives verifier full tool access)
- ctx = ToolContext(task_id)
- try:
- reward = await self.compute_reward(item, result, ctx)
- except Exception as e:
- logger.error("compute_reward failed: %s", e)
- reward = 0.0
- finally:
- ctx.cleanup()
-
- # Track tool errors for wandb logging
- if result.tool_errors:
- for err in result.tool_errors:
- self._tool_error_buffer.append({
- "turn": err.turn,
- "tool": err.tool_name,
- "args": err.arguments[:150],
- "error": err.error[:300],
- "result": err.tool_result[:300],
- })
-
- # Build ScoredDataItem from ManagedServer state
- # Phase 2: real tokens/masks/logprobs from SequenceNodes
- # Phase 1: placeholder tokens (still need a valid ScoredDataItem for the pipeline)
- nodes = (result.managed_state or {}).get("nodes", [])
-
- if nodes:
- # Phase 2 (or DummyManagedServer): use actual node data
- node = nodes[-1] # Final sequence node = full trajectory
- scored_item: Dict[str, Any] = {
- "tokens": node.tokens,
- "masks": node.masked_tokens,
- "scores": reward,
- }
-
- # Include logprobs if available (Phase 2)
- if hasattr(node, "logprobs") and node.logprobs:
- scored_item["advantages"] = None # Computed by trainer
- scored_item["ref_logprobs"] = None
- else:
- # Phase 1 with no managed state: create placeholder tokens
- # so the data pipeline doesn't break. These are NOT suitable
- # for training but allow process mode (SFT data gen) to work.
- # Tokenize the full conversation to get approximate tokens.
- full_text = "\n".join(
- msg.get("content", "") for msg in result.messages if msg.get("content")
- )
- if self.tokenizer:
- tokens = self.tokenizer.encode(full_text, add_special_tokens=True)
- else:
- tokens = list(range(min(len(full_text) // 4, 128)))
-
- scored_item = {
- "tokens": tokens,
- "masks": [-100] + tokens[1:], # Mask first token as prompt
- "scores": reward,
- }
-
- # Always include messages for wandb rollout display and data logging
- scored_item["messages"] = result.messages
-
- return scored_item, []
-
- # =========================================================================
- # Abstract methods -- subclasses must implement
- # =========================================================================
-
- @abstractmethod
- async def setup(self):
- """
- Load dataset, initialize state.
-
- Called once when the environment starts. Typical implementation:
- self.dataset = load_dataset(self.config.dataset_name, split=self.config.dataset_split)
- self.iter = 0
- """
- raise NotImplementedError
-
- @abstractmethod
- async def get_next_item(self) -> Item:
- """
- Return the next item from the dataset for rollout.
-
- Called by the base env's main loop to get items for workers.
- Should cycle through the dataset.
- """
- raise NotImplementedError
-
- @abstractmethod
- def format_prompt(self, item: Item) -> str:
- """
- Convert a dataset item into the user message for the agent.
-
- Args:
- item: Dataset item (dict, tuple, etc.)
-
- Returns:
- The prompt string to send to the agent
- """
- raise NotImplementedError
-
- @abstractmethod
- async def compute_reward(
- self, item: Item, result: AgentResult, ctx: ToolContext
- ) -> float:
- """
- Score the rollout. Has full access to:
- - item: the original dataset item (ground truth, test commands, etc.)
- - result: AgentResult with full messages, turn count, reasoning, etc.
- - ctx: ToolContext -- call ANY hermes-agent tool (terminal, file, web,
- browser, vision...) scoped to this rollout's sandbox. Nothing
- is off-limits.
-
- Args:
- item: The dataset item that was rolled out
- result: The agent's rollout result
- ctx: ToolContext with full tool access for verification
-
- Returns:
- Reward float (typically 0.0 to 1.0, but any float is valid)
- """
- raise NotImplementedError
-
- @abstractmethod
- async def evaluate(self, *args, **kwargs):
- """
- Periodic evaluation. Called every steps_per_eval steps.
-
- Typical implementation runs the agent on a held-out eval set
- and logs metrics via wandb/evaluate_log.
- """
- raise NotImplementedError
diff --git a/environments/hermes_swe_env/__init__.py b/environments/hermes_swe_env/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/environments/hermes_swe_env/default.yaml b/environments/hermes_swe_env/default.yaml
deleted file mode 100644
index 2d0113345f8..00000000000
--- a/environments/hermes_swe_env/default.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-# SWE Environment -- Default Configuration
-#
-# SWE-bench style tasks with Modal sandboxes for cloud isolation.
-# Uses terminal + file + web toolsets.
-#
-# Usage:
-# python environments/hermes_swe_env/hermes_swe_env.py serve \
-# --config environments/hermes_swe_env/default.yaml
-
-env:
- enabled_toolsets: ["terminal", "file", "web"]
- max_agent_turns: 30
- max_token_length: 4096
- group_size: 4
- terminal_backend: "modal"
- tool_call_parser: "hermes"
- tokenizer_name: "NousResearch/DeepHermes-3-Llama-3-3B-Preview"
- dataset_name: "bigcode/humanevalpack"
- dataset_split: "test"
- prompt_field: "prompt"
- steps_per_eval: 50
- total_steps: 500
- use_wandb: true
- wandb_name: "hermes-swe"
- system_prompt: >
- You are a skilled software engineer. You have access to a terminal,
- file tools, and web search. Use these tools to complete the coding task.
- Write clean, working code and verify it runs correctly before finishing.
-
-openai:
- base_url: "http://localhost:8000/v1"
- model_name: "NousResearch/DeepHermes-3-Llama-3-3B-Preview"
- server_type: "openai"
- api_key: ""
diff --git a/environments/hermes_swe_env/hermes_swe_env.py b/environments/hermes_swe_env/hermes_swe_env.py
deleted file mode 100644
index 49c521e5f76..00000000000
--- a/environments/hermes_swe_env/hermes_swe_env.py
+++ /dev/null
@@ -1,229 +0,0 @@
-"""
-HermesSweEnv -- SWE-Bench Style Environment with Modal Sandboxes
-
-A concrete environment for software engineering tasks where the model writes code
-and the reward function runs tests to verify correctness. Uses Modal terminal
-backend for cloud-isolated sandboxes per rollout.
-
-The reward function uses ToolContext.terminal() to run test commands in the same
-Modal sandbox the model used during its agentic loop. All filesystem state from
-the model's tool calls is preserved for verification.
-
-Usage:
- # Phase 1: OpenAI server type
- vllm serve YourModel --tool-parser hermes
- run-api
- python environments/hermes_swe_env.py serve \\
- --openai.base_url http://localhost:8000/v1 \\
- --openai.model_name YourModel \\
- --openai.server_type openai \\
- --env.dataset_name bigcode/humanevalpack \\
- --env.terminal_backend modal
-
- # Phase 2: VLLM server type (full RL training)
- python environments/hermes_swe_env.py serve \\
- --openai.base_url http://localhost:8000/v1 \\
- --openai.model_name YourModel \\
- --openai.server_type vllm \\
- --env.tool_call_parser hermes \\
- --env.terminal_backend modal
-"""
-
-import logging
-import sys
-import time
-from pathlib import Path
-from typing import Any, Dict, List, Optional, Tuple, Union
-
-# Ensure repo root is on sys.path for imports
-_repo_root = Path(__file__).resolve().parent.parent.parent
-if str(_repo_root) not in sys.path:
- sys.path.insert(0, str(_repo_root))
-
-from datasets import load_dataset
-
-from atroposlib.envs.base import ScoredDataGroup
-from atroposlib.envs.server_handling.server_manager import APIServerConfig
-from atroposlib.type_definitions import Item
-
-from environments.agent_loop import AgentResult
-from environments.hermes_base_env import HermesAgentBaseEnv, HermesAgentEnvConfig
-from environments.tool_context import ToolContext
-
-logger = logging.getLogger(__name__)
-
-
-class HermesSweEnvConfig(HermesAgentEnvConfig):
- """Config with defaults for SWE-bench style tasks."""
-
- pass # Inherits all fields, overrides defaults in config_init
-
-
-class HermesSweEnv(HermesAgentBaseEnv):
- """
- SWE-bench style environment using Modal terminal backend.
-
- The model gets a coding task, uses terminal + file + web tools to solve it,
- and the reward function runs tests in the same Modal sandbox to verify.
-
- Subclass this for specific SWE datasets (HumanEval, SWE-bench, etc.)
- and customize format_prompt() and compute_reward() as needed.
- """
-
- name = "hermes-swe"
- env_config_cls = HermesSweEnvConfig
-
- @classmethod
- def config_init(cls) -> Tuple[HermesSweEnvConfig, List[APIServerConfig]]:
- """
- Default configuration for the SWE environment.
-
- Uses Modal terminal backend for cloud isolation and terminal + file + web toolsets.
- """
- env_config = HermesSweEnvConfig(
- # Toolsets: terminal for running code, file for reading/writing, web for docs
- enabled_toolsets=["terminal", "file", "web"],
- disabled_toolsets=None,
- distribution=None,
- # Agent settings -- SWE tasks need more turns
- max_agent_turns=30,
- max_token_length=4096,
- agent_temperature=1.0,
- system_prompt=(
- "You are a skilled software engineer. You have access to a terminal, "
- "file tools, and web search. Use these tools to complete the coding task. "
- "Write clean, working code and verify it runs correctly before finishing."
- ),
- # Modal backend for cloud-isolated sandboxes
- terminal_backend="modal",
- # Dataset -- override via CLI for your specific SWE dataset
- dataset_name="bigcode/humanevalpack",
- dataset_split="test",
- prompt_field="prompt",
- # Atropos settings
- group_size=4,
- tokenizer_name="NousResearch/DeepHermes-3-Llama-3-3B-Preview",
- tool_call_parser="hermes",
- steps_per_eval=50,
- total_steps=500,
- use_wandb=True,
- wandb_name="hermes-swe",
- )
-
- server_configs = [
- APIServerConfig(
- base_url="http://localhost:8000/v1",
- model_name="NousResearch/DeepHermes-3-Llama-3-3B-Preview",
- server_type="openai", # Phase 1; switch to "vllm" for Phase 2
- api_key="",
- )
- ]
-
- return env_config, server_configs
-
- async def setup(self):
- """Load the SWE dataset."""
- if self.config.dataset_name:
- self.dataset = load_dataset(
- self.config.dataset_name, split=self.config.dataset_split
- )
- else:
- # Placeholder if no dataset specified
- self.dataset = []
- self.iter = 0
- self.reward_buffer: List[float] = []
-
- async def get_next_item(self) -> Dict[str, Any]:
- """Cycle through the SWE dataset."""
- if not self.dataset:
- raise ValueError("No dataset loaded. Set dataset_name in config.")
- item = self.dataset[self.iter % len(self.dataset)]
- self.iter += 1
- return item
-
- def format_prompt(self, item: Dict[str, Any]) -> str:
- """
- Format the SWE task prompt.
-
- Override this in subclasses for different dataset formats.
- Default assumes the dataset has a 'prompt' field and optionally a 'test' field.
- """
- prompt = item.get(self.config.prompt_field, "")
-
- # If the dataset has test information, include it in the prompt
- test_info = item.get("test", item.get("test_code", item.get("tests", "")))
- if test_info:
- prompt += f"\n\nTests to pass:\n{test_info}"
-
- return prompt
-
- async def compute_reward(
- self, item: Dict[str, Any], result: AgentResult, ctx: ToolContext
- ) -> float:
- """
- Score by running tests in the model's Modal sandbox.
-
- Default implementation:
- - If the dataset item has a 'test' or 'test_code' field, run it
- - Check exit code: 0 = pass, non-zero = fail
- - Partial credit for file creation
-
- Override this in subclasses for more sophisticated reward logic.
- """
- # Find the test command from the dataset item
- test_code = item.get("test", item.get("test_code", item.get("tests", "")))
-
- if test_code:
- # Run the test in the model's sandbox
- test_result = ctx.terminal(
- f'cd /workspace && python3 -c "{test_code}"', timeout=60
- )
-
- if test_result["exit_code"] == 0:
- self.reward_buffer.append(1.0)
- return 1.0
-
- # Partial credit: check if the model created any Python files
- file_check = ctx.terminal("find /workspace -name '*.py' -newer /tmp/.start_marker 2>/dev/null | head -5")
- if file_check["exit_code"] == 0 and file_check.get("output", "").strip():
- self.reward_buffer.append(0.1)
- return 0.1
-
- self.reward_buffer.append(0.0)
- return 0.0
-
- async def evaluate(self, *args, **kwargs):
- """
- Run evaluation on a held-out set.
-
- Override for dataset-specific evaluation logic.
- """
- start_time = time.time()
- end_time = time.time()
-
- eval_metrics = {"eval/placeholder": 0.0}
- await self.evaluate_log(
- metrics=eval_metrics,
- start_time=start_time,
- end_time=end_time,
- )
-
- async def wandb_log(self, wandb_metrics: Optional[Dict] = None):
- """Log SWE-specific metrics."""
- if wandb_metrics is None:
- wandb_metrics = {}
-
- if self.reward_buffer:
- wandb_metrics["train/avg_reward"] = sum(self.reward_buffer) / len(
- self.reward_buffer
- )
- wandb_metrics["train/pass_rate"] = sum(
- 1 for r in self.reward_buffer if r == 1.0
- ) / len(self.reward_buffer)
- self.reward_buffer = []
-
- await super().wandb_log(wandb_metrics)
-
-
-if __name__ == "__main__":
- HermesSweEnv.cli()
diff --git a/environments/patches.py b/environments/patches.py
deleted file mode 100644
index a5afe751ece..00000000000
--- a/environments/patches.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""
-Monkey patches for making hermes-agent tools work inside async frameworks (Atropos).
-
-Problem:
- Some tools use asyncio.run() internally (e.g., Modal backend via SWE-ReX,
- web_extract). This crashes when called from inside Atropos's event loop because
- asyncio.run() can't be nested.
-
-Solution:
- The Modal environment (tools/environments/modal.py) now uses a dedicated
- _AsyncWorker thread internally, making it safe for both CLI and Atropos use.
- No monkey-patching is required.
-
- This module is kept for backward compatibility. apply_patches() is a no-op.
-
-Usage:
- Call apply_patches() once at import time (done automatically by hermes_base_env.py).
- This is idempotent and safe to call multiple times.
-"""
-
-import logging
-
-logger = logging.getLogger(__name__)
-
-_patches_applied = False
-
-
-def apply_patches():
- """Apply all monkey patches needed for Atropos compatibility."""
- global _patches_applied
- if _patches_applied:
- return
-
- logger.debug("apply_patches() called; no patches needed (async safety is built-in)")
- _patches_applied = True
diff --git a/environments/terminal_test_env/__init__.py b/environments/terminal_test_env/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/environments/terminal_test_env/default.yaml b/environments/terminal_test_env/default.yaml
deleted file mode 100644
index dc971071c3a..00000000000
--- a/environments/terminal_test_env/default.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-# Terminal Test Environment -- Default Configuration
-#
-# Simple file-creation tasks for validating the full Atropos + hermes-agent stack.
-# Uses Modal terminal backend and OpenRouter (Claude) for inference.
-# API keys loaded from ~/hermes-agent/.env
-#
-# Usage:
-# run-api
-# python environments/terminal_test_env/terminal_test_env.py serve \
-# --config environments/terminal_test_env/default.yaml
-
-env:
- enabled_toolsets: ["terminal", "file"]
- max_agent_turns: 10
- max_token_length: 2048
- group_size: 3
- total_steps: 3
- steps_per_eval: 3
- terminal_backend: "modal"
- tool_call_parser: "hermes"
- tokenizer_name: "NousResearch/DeepHermes-3-Llama-3-3B-Preview"
- ensure_scores_are_not_same: false
- use_wandb: false
- system_prompt: >
- You are a helpful assistant with access to a terminal and file tools.
- Complete the user's request by using the available tools.
- Be precise and follow instructions exactly.
-
-openai:
- base_url: "https://openrouter.ai/api/v1"
- model_name: "anthropic/claude-opus-4.6"
- server_type: "openai"
- health_check: false
- # api_key loaded from OPENROUTER_API_KEY in .env
diff --git a/environments/terminal_test_env/terminal_test_env.py b/environments/terminal_test_env/terminal_test_env.py
deleted file mode 100644
index 4d151ee7b76..00000000000
--- a/environments/terminal_test_env/terminal_test_env.py
+++ /dev/null
@@ -1,292 +0,0 @@
-"""
-TerminalTestEnv -- Simple Test Environment for Validating the Stack
-
-A self-contained environment with inline tasks (no external dataset needed).
-Each task asks the model to create a file at a known path with specific content.
-The reward verifier cats the file and checks if the content matches.
-
-Enables only terminal + file toolsets. Uses Modal terminal backend with
-OpenRouter (Claude) by default.
-
-Training tasks (3):
- 1. Create ~/greeting.txt with "Hello from Hermes Agent"
- 2. Create ~/count.txt with numbers 1-5, one per line
- 3. Create ~/answer.txt with the result of 123 + 456
-
-Eval task (1):
- 1. Create ~/result.txt with the result of 6 * 7
-
-Usage:
- # Start Atropos API server
- run-api
-
- # Run environment (uses OpenRouter + Modal by default)
- python environments/terminal_test_env.py serve
-
- # Process mode (no run-api needed, saves to JSONL)
- python environments/terminal_test_env.py process \\
- --env.data_path_to_save_groups terminal_test_output.jsonl
-"""
-
-import logging
-import os
-import sys
-import time
-from pathlib import Path
-from typing import Any, Dict, List, Optional, Tuple, Union
-
-# Ensure repo root is on sys.path for imports
-_repo_root = Path(__file__).resolve().parent.parent.parent
-if str(_repo_root) not in sys.path:
- sys.path.insert(0, str(_repo_root))
-
-from atroposlib.envs.base import ScoredDataGroup
-from atroposlib.envs.server_handling.server_manager import APIServerConfig
-from atroposlib.type_definitions import Item
-
-from environments.agent_loop import AgentResult
-from environments.hermes_base_env import HermesAgentBaseEnv, HermesAgentEnvConfig
-from environments.tool_context import ToolContext
-
-logger = logging.getLogger(__name__)
-
-
-# =============================================================================
-# Inline task definitions -- no external dataset needed
-# =============================================================================
-
-TRAIN_TASKS = [
- {
- "prompt": "Create a file at ~/greeting.txt containing exactly the text: Hello from Hermes Agent",
- "verify_path": "~/greeting.txt",
- "expected_content": "Hello from Hermes Agent",
- },
- {
- "prompt": "Create a file at ~/count.txt containing the numbers 1 through 5, one per line",
- "verify_path": "~/count.txt",
- "expected_content": "1\n2\n3\n4\n5",
- },
- {
- "prompt": "Create a file at ~/answer.txt containing the result of 123 + 456",
- "verify_path": "~/answer.txt",
- "expected_content": "579",
- },
-]
-
-EVAL_TASKS = [
- {
- "prompt": "Create a file at ~/result.txt containing the result of 6 * 7",
- "verify_path": "~/result.txt",
- "expected_content": "42",
- },
-]
-
-
-class TerminalTestEnvConfig(HermesAgentEnvConfig):
- """Config with defaults suitable for terminal testing."""
-
- pass # Inherits all fields, overrides defaults in config_init
-
-
-class TerminalTestEnv(HermesAgentBaseEnv):
- """
- Simple test environment with inline file-creation tasks.
-
- All tasks follow the same pattern: "create a file at ~/X.txt with content Y".
- The verifier runs `cat ~/X.txt` in the rollout's terminal and checks the output
- against the expected string. Same verifier logic for all tasks.
-
- This environment is designed to validate the full stack end-to-end:
- - Agent loop executes tool calls (terminal/file)
- - ToolContext provides terminal access to the reward function
- - Reward function verifies file content via cat
- - Scored data flows through the Atropos pipeline
- """
-
- name = "terminal-test"
- env_config_cls = TerminalTestEnvConfig
-
- @classmethod
- def config_init(cls) -> Tuple[TerminalTestEnvConfig, List[APIServerConfig]]:
- """
- Default configuration for the terminal test environment.
-
- Uses Modal terminal backend for cloud isolation and OpenRouter with
- Claude for inference. API keys loaded from ~/hermes-agent/.env.
- """
- env_config = TerminalTestEnvConfig(
- # Terminal + file tools only
- enabled_toolsets=["terminal", "file"],
- disabled_toolsets=None,
- distribution=None,
- # Agent settings
- max_agent_turns=10, # Simple tasks, don't need many turns
- max_token_length=16000,
- agent_temperature=1.0,
- system_prompt=(
- "You are a helpful assistant with access to a terminal and file tools. "
- "Complete the user's request by using the available tools. "
- "Be precise and follow instructions exactly."
- ),
- # Modal terminal backend for cloud-isolated sandboxes per rollout
- terminal_backend="modal",
- # Atropos settings
- group_size=3, # 3 rollouts per group
- tokenizer_name="NousResearch/q-30b-t-h45-e1",
- tool_call_parser="hermes",
- steps_per_eval=3, # Eval after all 3 steps
- total_steps=3, # 3 groups total (1 group per step)
- use_wandb=True,
- wandb_name="terminal-test",
- ensure_scores_are_not_same=False, # Allow all-same scores for simple tasks
- # No external dataset
- dataset_name=None,
- )
-
- # OpenRouter with Claude -- API key loaded from .env (OPENROUTER_API_KEY)
- server_configs = [
- APIServerConfig(
- base_url="https://openrouter.ai/api/v1",
- model_name="anthropic/claude-opus-4.6",
- server_type="openai",
- api_key=os.getenv("OPENROUTER_API_KEY", ""),
- health_check=False, # OpenRouter doesn't have a /health endpoint
- )
- ]
-
- return env_config, server_configs
-
- async def setup(self):
- """Initialize inline task lists."""
- self.train_tasks = list(TRAIN_TASKS)
- self.eval_tasks = list(EVAL_TASKS)
- self.iter = 0
- # Track reward stats for wandb logging
- self.reward_buffer: List[float] = []
-
- async def get_next_item(self) -> Dict[str, str]:
- """Cycle through training tasks."""
- item = self.train_tasks[self.iter % len(self.train_tasks)]
- self.iter += 1
- return item
-
- def format_prompt(self, item: Dict[str, str]) -> str:
- """The prompt is directly in the task item."""
- return item["prompt"]
-
- async def compute_reward(
- self, item: Dict[str, str], result: AgentResult, ctx: ToolContext
- ) -> float:
- """
- Verify by cat-ing the expected file path and checking content matches.
- Same verifier for all tasks -- they all write a file at a known path.
-
- Scoring:
- 1.0 = exact match
- 0.5 = expected content is present but has extra stuff
- 0.0 = file doesn't exist or content doesn't match
- """
- verify_result = ctx.terminal(f"cat {item['verify_path']}")
-
- # File doesn't exist or can't be read
- if verify_result["exit_code"] != 0:
- self.reward_buffer.append(0.0)
- return 0.0
-
- actual = verify_result.get("output", "").strip()
- expected = item["expected_content"].strip()
-
- # Exact match
- if actual == expected:
- self.reward_buffer.append(1.0)
- return 1.0
-
- # Partial credit: expected content is present but has extra stuff
- if expected in actual:
- self.reward_buffer.append(0.5)
- return 0.5
-
- self.reward_buffer.append(0.0)
- return 0.0
-
- async def evaluate(self, *args, **kwargs):
- """
- Run eval tasks using the agent loop and verify results.
- Logs accuracy metrics.
- """
- start_time = time.time()
- correct = 0
- total = len(self.eval_tasks)
- samples = []
-
- for eval_item in self.eval_tasks:
- try:
- # For eval, we do a simple single-turn completion (not full agent loop)
- # to keep eval fast. The agent loop is tested via training.
- completion = await self.server.chat_completion(
- messages=[
- {"role": "system", "content": self.config.system_prompt or ""},
- {"role": "user", "content": eval_item["prompt"]},
- ],
- n=1,
- max_tokens=self.config.max_token_length,
- temperature=0.0,
- split="eval",
- )
-
- response_content = (
- completion.choices[0].message.content if completion.choices else ""
- )
-
- samples.append(
- {
- "prompt": eval_item["prompt"],
- "response": response_content,
- "expected": eval_item["expected_content"],
- }
- )
-
- except Exception as e:
- logger.error("Eval failed for item: %s", e)
- samples.append(
- {
- "prompt": eval_item["prompt"],
- "response": f"ERROR: {e}",
- "expected": eval_item["expected_content"],
- }
- )
-
- end_time = time.time()
-
- eval_metrics = {
- "eval/num_samples": total,
- }
-
- await self.evaluate_log(
- metrics=eval_metrics,
- samples=samples,
- start_time=start_time,
- end_time=end_time,
- )
-
- async def wandb_log(self, wandb_metrics: Optional[Dict] = None):
- """Log training metrics including reward stats and accuracy."""
- if wandb_metrics is None:
- wandb_metrics = {}
-
- if self.reward_buffer:
- total = len(self.reward_buffer)
- correct = sum(1 for r in self.reward_buffer if r == 1.0)
- partial = sum(1 for r in self.reward_buffer if r == 0.5)
-
- wandb_metrics["train/avg_reward"] = sum(self.reward_buffer) / total
- wandb_metrics["train/accuracy"] = correct / total
- wandb_metrics["train/partial_match_rate"] = partial / total
- wandb_metrics["train/total_rollouts"] = total
- self.reward_buffer = []
-
- await super().wandb_log(wandb_metrics)
-
-
-if __name__ == "__main__":
- TerminalTestEnv.cli()
diff --git a/environments/tool_call_parsers/__init__.py b/environments/tool_call_parsers/__init__.py
deleted file mode 100644
index 8bff3f9d1f0..00000000000
--- a/environments/tool_call_parsers/__init__.py
+++ /dev/null
@@ -1,120 +0,0 @@
-"""
-Tool Call Parser Registry
-
-Client-side parsers that extract structured tool_calls from raw model output text.
-Used in Phase 2 (VLLM server type) where ManagedServer's /generate endpoint returns
-raw text without tool call parsing.
-
-Each parser is a standalone reimplementation of the corresponding VLLM parser's
-non-streaming extract_tool_calls() logic. No VLLM dependency -- only standard library
-(re, json, uuid) and openai types.
-
-Usage:
- from environments.tool_call_parsers import get_parser
-
- parser = get_parser("hermes")
- content, tool_calls = parser.parse(raw_model_output)
- # content = text with tool call markup stripped
- # tool_calls = list of ChatCompletionMessageToolCall objects, or None
-"""
-
-import logging
-from abc import ABC, abstractmethod
-from typing import Dict, List, Optional, Tuple, Type
-
-from openai.types.chat.chat_completion_message_tool_call import (
- ChatCompletionMessageToolCall,
-)
-
-logger = logging.getLogger(__name__)
-
-# Type alias for parser return value
-ParseResult = Tuple[Optional[str], Optional[List[ChatCompletionMessageToolCall]]]
-
-
-class ToolCallParser(ABC):
- """
- Base class for tool call parsers.
-
- Each parser knows how to extract structured tool_calls from a specific
- model family's raw output text format.
- """
-
- @abstractmethod
- def parse(self, text: str) -> ParseResult:
- """
- Parse raw model output text for tool calls.
-
- Args:
- text: Raw decoded text from the model's completion
-
- Returns:
- Tuple of (content, tool_calls) where:
- - content: text with tool call markup stripped (the message 'content' field),
- or None if the entire output was tool calls
- - tool_calls: list of ChatCompletionMessageToolCall objects,
- or None if no tool calls were found
- """
- raise NotImplementedError
-
-
-# Global parser registry: name -> parser class
-PARSER_REGISTRY: Dict[str, Type[ToolCallParser]] = {}
-
-
-def register_parser(name: str):
- """
- Decorator to register a parser class under a given name.
-
- Usage:
- @register_parser("hermes")
- class HermesToolCallParser(ToolCallParser):
- ...
- """
-
- def decorator(cls: Type[ToolCallParser]) -> Type[ToolCallParser]:
- PARSER_REGISTRY[name] = cls
- return cls
-
- return decorator
-
-
-def get_parser(name: str) -> ToolCallParser:
- """
- Get a parser instance by name.
-
- Args:
- name: Parser name (e.g., "hermes", "mistral", "llama3_json")
-
- Returns:
- Instantiated parser
-
- Raises:
- KeyError: If parser name is not found in registry
- """
- if name not in PARSER_REGISTRY:
- available = sorted(PARSER_REGISTRY.keys())
- raise KeyError(
- f"Tool call parser '{name}' not found. Available parsers: {available}"
- )
- return PARSER_REGISTRY[name]()
-
-
-def list_parsers() -> List[str]:
- """Return sorted list of registered parser names."""
- return sorted(PARSER_REGISTRY.keys())
-
-
-# Import all parser modules to trigger registration via @register_parser decorators
-# Each module registers itself when imported
-from environments.tool_call_parsers.hermes_parser import HermesToolCallParser # noqa: E402, F401
-from environments.tool_call_parsers.longcat_parser import LongcatToolCallParser # noqa: E402, F401
-from environments.tool_call_parsers.mistral_parser import MistralToolCallParser # noqa: E402, F401
-from environments.tool_call_parsers.llama_parser import LlamaToolCallParser # noqa: E402, F401
-from environments.tool_call_parsers.qwen_parser import QwenToolCallParser # noqa: E402, F401
-from environments.tool_call_parsers.deepseek_v3_parser import DeepSeekV3ToolCallParser # noqa: E402, F401
-from environments.tool_call_parsers.deepseek_v3_1_parser import DeepSeekV31ToolCallParser # noqa: E402, F401
-from environments.tool_call_parsers.kimi_k2_parser import KimiK2ToolCallParser # noqa: E402, F401
-from environments.tool_call_parsers.glm45_parser import Glm45ToolCallParser # noqa: E402, F401
-from environments.tool_call_parsers.glm47_parser import Glm47ToolCallParser # noqa: E402, F401
-from environments.tool_call_parsers.qwen3_coder_parser import Qwen3CoderToolCallParser # noqa: E402, F401
diff --git a/environments/tool_call_parsers/deepseek_v3_1_parser.py b/environments/tool_call_parsers/deepseek_v3_1_parser.py
deleted file mode 100644
index 8456990c6ad..00000000000
--- a/environments/tool_call_parsers/deepseek_v3_1_parser.py
+++ /dev/null
@@ -1,72 +0,0 @@
-"""
-DeepSeek V3.1 tool call parser.
-
-Similar to V3 but with a slightly different format:
- <|tool▁call▁begin|>function_name<|tool▁sep|>arguments<|tool▁call▁end|>
-
-Note: V3 has type+name before the separator, V3.1 has name before and args after.
-
-Based on VLLM's DeepSeekV31ToolParser.extract_tool_calls()
-"""
-
-import re
-import uuid
-from typing import List, Optional
-
-from openai.types.chat.chat_completion_message_tool_call import (
- ChatCompletionMessageToolCall,
- Function,
-)
-
-from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser
-
-
-@register_parser("deepseek_v3_1")
-@register_parser("deepseek_v31")
-class DeepSeekV31ToolCallParser(ToolCallParser):
- """
- Parser for DeepSeek V3.1 tool calls.
-
- Slightly different regex than V3: function_name comes before the separator,
- arguments come after (no type field, no json code block wrapper).
- """
-
- START_TOKEN = "<|tool▁calls▁begin|>"
-
- # Regex captures: function_name, function_arguments
- PATTERN = re.compile(
- r"<|tool▁call▁begin|>(?P.*?)<|tool▁sep|>(?P.*?)<|tool▁call▁end|>",
- re.DOTALL,
- )
-
- def parse(self, text: str) -> ParseResult:
- if self.START_TOKEN not in text:
- return text, None
-
- try:
- matches = self.PATTERN.findall(text)
- if not matches:
- return text, None
-
- tool_calls: List[ChatCompletionMessageToolCall] = []
- for match in matches:
- func_name, func_args = match
- tool_calls.append(
- ChatCompletionMessageToolCall(
- id=f"call_{uuid.uuid4().hex[:8]}",
- type="function",
- function=Function(
- name=func_name.strip(),
- arguments=func_args.strip(),
- ),
- )
- )
-
- if not tool_calls:
- return text, None
-
- content = text[: text.find(self.START_TOKEN)].strip()
- return content if content else None, tool_calls
-
- except Exception:
- return text, None
diff --git a/environments/tool_call_parsers/deepseek_v3_parser.py b/environments/tool_call_parsers/deepseek_v3_parser.py
deleted file mode 100644
index 61d23d5fecc..00000000000
--- a/environments/tool_call_parsers/deepseek_v3_parser.py
+++ /dev/null
@@ -1,89 +0,0 @@
-"""
-DeepSeek V3 tool call parser.
-
-Format uses special unicode tokens:
- <|tool▁calls▁begin|>
- <|tool▁call▁begin|>type<|tool▁sep|>function_name
- ```json
- {"arg": "value"}
- ```
- <|tool▁call▁end|>
- <|tool▁calls▁end|>
-
-Fixes Issue #989: Support for multiple simultaneous tool calls.
-"""
-
-import re
-import uuid
-import logging
-from typing import List, Optional, Tuple
-
-from openai.types.chat.chat_completion_message_tool_call import (
- ChatCompletionMessageToolCall,
- Function,
-)
-
-from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser
-
-logger = logging.getLogger(__name__)
-
-@register_parser("deepseek_v3")
-class DeepSeekV3ToolCallParser(ToolCallParser):
- """
- Parser for DeepSeek V3 tool calls.
-
- Uses special unicode tokens with fullwidth angle brackets and block elements.
- Extracts type, function name, and JSON arguments from the structured format.
- Ensures all tool calls are captured when the model executes multiple actions.
- """
-
- START_TOKEN = "<|tool▁calls▁begin|>"
-
- # Updated PATTERN: Using \s* instead of literal \n for increased robustness
- # against variations in model formatting (Issue #989).
- PATTERN = re.compile(
- r"<|tool▁call▁begin|>(?P.*?)<|tool▁sep|>(?P.*?)\s*```json\s*(?P.*?)\s*```\s*<|tool▁call▁end|>",
- re.DOTALL,
- )
-
- def parse(self, text: str) -> ParseResult:
- """
- Parses the input text and extracts all available tool calls.
- """
- if self.START_TOKEN not in text:
- return text, None
-
- try:
- # Using finditer to capture ALL tool calls in the sequence
- matches = list(self.PATTERN.finditer(text))
- if not matches:
- return text, None
-
- tool_calls: List[ChatCompletionMessageToolCall] = []
-
- for match in matches:
- func_name = match.group("function_name").strip()
- func_args = match.group("function_arguments").strip()
-
- tool_calls.append(
- ChatCompletionMessageToolCall(
- id=f"call_{uuid.uuid4().hex[:8]}",
- type="function",
- function=Function(
- name=func_name,
- arguments=func_args,
- ),
- )
- )
-
- if tool_calls:
- # Content is text before the first tool call block
- content_index = text.find(self.START_TOKEN)
- content = text[:content_index].strip()
- return content if content else None, tool_calls
-
- return text, None
-
- except Exception as e:
- logger.error(f"Error parsing DeepSeek V3 tool calls: {e}")
- return text, None
diff --git a/environments/tool_call_parsers/glm45_parser.py b/environments/tool_call_parsers/glm45_parser.py
deleted file mode 100644
index e92e29881f1..00000000000
--- a/environments/tool_call_parsers/glm45_parser.py
+++ /dev/null
@@ -1,109 +0,0 @@
-"""
-GLM 4.5 (GLM-4-MoE) tool call parser.
-
-Format uses custom arg_key/arg_value tags rather than standard JSON:
- function_name
- param1value1
- param2value2
-
-
-Values are deserialized using json.loads -> ast.literal_eval -> raw string fallback.
-
-Based on VLLM's Glm4MoeModelToolParser.extract_tool_calls()
-"""
-
-import ast
-import json
-import re
-import uuid
-from typing import Any, Dict, List, Optional
-
-from openai.types.chat.chat_completion_message_tool_call import (
- ChatCompletionMessageToolCall,
- Function,
-)
-
-from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser
-
-
-def _deserialize_value(value: str) -> Any:
- """
- Try to deserialize a string value to its native Python type.
- Attempts json.loads, then ast.literal_eval, then returns raw string.
- """
- try:
- return json.loads(value)
- except (json.JSONDecodeError, TypeError):
- pass
-
- try:
- return ast.literal_eval(value)
- except (ValueError, SyntaxError, TypeError):
- pass
-
- return value
-
-
-@register_parser("glm45")
-class Glm45ToolCallParser(ToolCallParser):
- """
- Parser for GLM 4.5 (GLM-4-MoE) tool calls.
-
- Uses ... tags with / pairs
- instead of standard JSON arguments.
- """
-
- FUNC_CALL_REGEX = re.compile(r".*?", re.DOTALL)
- FUNC_DETAIL_REGEX = re.compile(r"([^\n]*)\n(.*)", re.DOTALL)
- FUNC_ARG_REGEX = re.compile(
- r"(.*?)\s*(.*?)", re.DOTALL
- )
-
- START_TOKEN = ""
-
- def parse(self, text: str) -> ParseResult:
- if self.START_TOKEN not in text:
- return text, None
-
- try:
- matched_calls = self.FUNC_CALL_REGEX.findall(text)
- if not matched_calls:
- return text, None
-
- tool_calls: List[ChatCompletionMessageToolCall] = []
-
- for match in matched_calls:
- detail = self.FUNC_DETAIL_REGEX.search(match)
- if not detail:
- continue
-
- func_name = detail.group(1).strip()
- func_args_raw = detail.group(2)
-
- # Parse arg_key/arg_value pairs
- pairs = self.FUNC_ARG_REGEX.findall(func_args_raw) if func_args_raw else []
- arg_dict: Dict[str, Any] = {}
- for key, value in pairs:
- arg_key = key.strip()
- arg_val = _deserialize_value(value.strip())
- arg_dict[arg_key] = arg_val
-
- tool_calls.append(
- ChatCompletionMessageToolCall(
- id=f"call_{uuid.uuid4().hex[:8]}",
- type="function",
- function=Function(
- name=func_name,
- arguments=json.dumps(arg_dict, ensure_ascii=False),
- ),
- )
- )
-
- if not tool_calls:
- return text, None
-
- content = text[: text.find(self.START_TOKEN)].strip()
- return content if content else None, tool_calls
-
- except Exception:
- return text, None
diff --git a/environments/tool_call_parsers/glm47_parser.py b/environments/tool_call_parsers/glm47_parser.py
deleted file mode 100644
index 6631cf842ce..00000000000
--- a/environments/tool_call_parsers/glm47_parser.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""
-GLM 4.7 tool call parser.
-
-Same as GLM 4.5 but with slightly different regex patterns.
-The tool_call tags may wrap differently and arg parsing handles
-newlines between key/value pairs.
-
-Based on VLLM's Glm47MoeModelToolParser (extends Glm4MoeModelToolParser).
-"""
-
-import re
-
-from environments.tool_call_parsers import ParseResult, register_parser
-from environments.tool_call_parsers.glm45_parser import Glm45ToolCallParser
-
-
-@register_parser("glm47")
-class Glm47ToolCallParser(Glm45ToolCallParser):
- """
- Parser for GLM 4.7 tool calls.
- Extends GLM 4.5 with updated regex patterns.
- """
-
- def __init__(self):
- super().__init__()
- # GLM 4.7 uses a slightly different detail regex that includes
- # the wrapper and optional arg_key content
- self.FUNC_DETAIL_REGEX = re.compile(
- r"(.*?)(.*?)?", re.DOTALL
- )
- # GLM 4.7 handles newlines between arg_key and arg_value tags
- self.FUNC_ARG_REGEX = re.compile(
- r"(.*?)(?:\\n|\s)*(.*?)",
- re.DOTALL,
- )
diff --git a/environments/tool_call_parsers/hermes_parser.py b/environments/tool_call_parsers/hermes_parser.py
deleted file mode 100644
index c6f911db04a..00000000000
--- a/environments/tool_call_parsers/hermes_parser.py
+++ /dev/null
@@ -1,75 +0,0 @@
-"""
-Hermes tool call parser.
-
-Format: {"name": "func", "arguments": {...}}
-Based on VLLM's Hermes2ProToolParser.extract_tool_calls()
-"""
-
-import json
-import re
-import uuid
-from typing import List, Optional, Tuple
-
-from openai.types.chat.chat_completion_message_tool_call import (
- ChatCompletionMessageToolCall,
- Function,
-)
-
-from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser
-
-
-@register_parser("hermes")
-class HermesToolCallParser(ToolCallParser):
- """
- Parser for Hermes-format tool calls.
-
- Matches ... tags containing JSON with "name" and "arguments".
- Also handles unclosed at end-of-string (truncated generation).
- """
-
- # Matches both closed and unclosed tool_call tags
- PATTERN = re.compile(
- r"\s*(.*?)\s*|\s*(.*)", re.DOTALL
- )
-
- def parse(self, text: str) -> ParseResult:
- if "" not in text:
- return text, None
-
- try:
- matches = self.PATTERN.findall(text)
- if not matches:
- return text, None
-
- tool_calls: List[ChatCompletionMessageToolCall] = []
- for match in matches:
- # match is a tuple: (closed_content, unclosed_content)
- raw_json = match[0] if match[0] else match[1]
- if not raw_json.strip():
- continue
-
- tc_data = json.loads(raw_json)
- if "name" not in tc_data:
- continue
- tool_calls.append(
- ChatCompletionMessageToolCall(
- id=f"call_{uuid.uuid4().hex[:8]}",
- type="function",
- function=Function(
- name=tc_data["name"],
- arguments=json.dumps(
- tc_data.get("arguments", {}), ensure_ascii=False
- ),
- ),
- )
- )
-
- if not tool_calls:
- return text, None
-
- # Content is everything before the first tag
- content = text[: text.find("")].strip()
- return content if content else None, tool_calls
-
- except Exception:
- return text, None
diff --git a/environments/tool_call_parsers/kimi_k2_parser.py b/environments/tool_call_parsers/kimi_k2_parser.py
deleted file mode 100644
index 29f40fc2435..00000000000
--- a/environments/tool_call_parsers/kimi_k2_parser.py
+++ /dev/null
@@ -1,93 +0,0 @@
-"""
-Kimi K2 tool call parser.
-
-Format:
- <|tool_calls_section_begin|>
- <|tool_call_begin|>function_id:0<|tool_call_argument_begin|>{"arg": "val"}<|tool_call_end|>
- <|tool_calls_section_end|>
-
-The function_id format is typically "functions.func_name:index" or "func_name:index".
-
-Based on VLLM's KimiK2ToolParser.extract_tool_calls()
-"""
-
-import re
-import uuid
-from typing import List, Optional
-
-from openai.types.chat.chat_completion_message_tool_call import (
- ChatCompletionMessageToolCall,
- Function,
-)
-
-from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser
-
-
-@register_parser("kimi_k2")
-class KimiK2ToolCallParser(ToolCallParser):
- """
- Parser for Kimi K2 tool calls.
-
- Uses section begin/end tokens wrapping individual tool call begin/end tokens.
- The tool_call_id contains the function name (after last dot, before colon).
- """
-
- # Support both singular and plural variants
- START_TOKENS = [
- "<|tool_calls_section_begin|>",
- "<|tool_call_section_begin|>",
- ]
-
- # Regex captures: tool_call_id (e.g., "functions.get_weather:0"), function_arguments
- PATTERN = re.compile(
- r"<\|tool_call_begin\|>\s*(?P[^<]+:\d+)\s*"
- r"<\|tool_call_argument_begin\|>\s*"
- r"(?P(?:(?!<\|tool_call_begin\|>).)*?)\s*"
- r"<\|tool_call_end\|>",
- re.DOTALL,
- )
-
- def parse(self, text: str) -> ParseResult:
- # Check for any variant of the start token
- has_start = any(token in text for token in self.START_TOKENS)
- if not has_start:
- return text, None
-
- try:
- matches = self.PATTERN.findall(text)
- if not matches:
- return text, None
-
- tool_calls: List[ChatCompletionMessageToolCall] = []
- for match in matches:
- function_id, function_args = match
-
- # Extract function name from ID format: "functions.get_weather:0" -> "get_weather"
- function_name = function_id.split(":")[0].split(".")[-1]
-
- tool_calls.append(
- ChatCompletionMessageToolCall(
- id=function_id, # Preserve the original ID format
- type="function",
- function=Function(
- name=function_name,
- arguments=function_args.strip(),
- ),
- )
- )
-
- if not tool_calls:
- return text, None
-
- # Content is everything before the tool calls section
- earliest_start = len(text)
- for token in self.START_TOKENS:
- idx = text.find(token)
- if idx >= 0 and idx < earliest_start:
- earliest_start = idx
-
- content = text[:earliest_start].strip()
- return content if content else None, tool_calls
-
- except Exception:
- return text, None
diff --git a/environments/tool_call_parsers/llama_parser.py b/environments/tool_call_parsers/llama_parser.py
deleted file mode 100644
index 8eb2136a11a..00000000000
--- a/environments/tool_call_parsers/llama_parser.py
+++ /dev/null
@@ -1,96 +0,0 @@
-"""
-Llama 3.x / 4 tool call parser.
-
-Format: The model outputs JSON objects with "name" and "arguments" (or "parameters") keys.
-May be preceded by <|python_tag|> token. Supports multiple JSON objects separated
-by content or semicolons.
-
-Based on VLLM's Llama3JsonToolParser.extract_tool_calls()
-"""
-
-import json
-import re
-import uuid
-from typing import List, Optional
-
-from openai.types.chat.chat_completion_message_tool_call import (
- ChatCompletionMessageToolCall,
- Function,
-)
-
-from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser
-
-
-@register_parser("llama3_json")
-@register_parser("llama4_json")
-class LlamaToolCallParser(ToolCallParser):
- """
- Parser for Llama 3.x and 4 JSON-format tool calls.
-
- Finds JSON objects containing "name" + ("arguments" or "parameters") keys.
- Uses Python's json.JSONDecoder.raw_decode for robust extraction of
- JSON objects from mixed text.
- """
-
- BOT_TOKEN = "<|python_tag|>"
-
- # Regex to find the start of potential JSON objects
- JSON_START = re.compile(r"\{")
-
- def parse(self, text: str) -> ParseResult:
- # Quick check: need either the bot token or a JSON brace
- if self.BOT_TOKEN not in text and "{" not in text:
- return text, None
-
- try:
- decoder = json.JSONDecoder()
- tool_calls: List[ChatCompletionMessageToolCall] = []
- end_index = -1 # Track where the last parsed JSON ended
-
- for match in self.JSON_START.finditer(text):
- start = match.start()
- # Skip if this brace is inside a previously parsed JSON object
- if start <= end_index:
- continue
-
- try:
- obj, json_end = decoder.raw_decode(text[start:])
- end_index = start + json_end
-
- # Must have "name" and either "arguments" or "parameters"
- name = obj.get("name")
- args = obj.get("arguments", obj.get("parameters"))
-
- if not name or args is None:
- continue
-
- # Normalize arguments to JSON string
- if isinstance(args, dict):
- args = json.dumps(args, ensure_ascii=False)
- elif not isinstance(args, str):
- args = json.dumps(args, ensure_ascii=False)
-
- tool_calls.append(
- ChatCompletionMessageToolCall(
- id=f"call_{uuid.uuid4().hex[:8]}",
- type="function",
- function=Function(name=name, arguments=args),
- )
- )
- except (json.JSONDecodeError, KeyError, ValueError):
- continue
-
- if not tool_calls:
- return text, None
-
- # Content is everything before the first tool call JSON
- # Find where the first tool call starts in the text
- first_tc_start = text.find("{")
- if self.BOT_TOKEN in text:
- first_tc_start = text.find(self.BOT_TOKEN)
- content = text[:first_tc_start].strip() if first_tc_start > 0 else None
-
- return content, tool_calls
-
- except Exception:
- return text, None
diff --git a/environments/tool_call_parsers/longcat_parser.py b/environments/tool_call_parsers/longcat_parser.py
deleted file mode 100644
index afecdb86292..00000000000
--- a/environments/tool_call_parsers/longcat_parser.py
+++ /dev/null
@@ -1,69 +0,0 @@
-"""
-Longcat Flash Chat tool call parser.
-
-Same as Hermes but uses tags instead of .
-Based on VLLM's LongcatFlashToolParser (extends Hermes2ProToolParser).
-"""
-
-import json
-import re
-import uuid
-from typing import List, Optional
-
-from openai.types.chat.chat_completion_message_tool_call import (
- ChatCompletionMessageToolCall,
- Function,
-)
-
-from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser
-
-
-@register_parser("longcat")
-class LongcatToolCallParser(ToolCallParser):
- """
- Parser for Longcat Flash Chat tool calls.
- Identical logic to Hermes, just different tag names.
- """
-
- PATTERN = re.compile(
- r"\s*(.*?)\s*|\s*(.*)",
- re.DOTALL,
- )
-
- def parse(self, text: str) -> ParseResult:
- if "" not in text:
- return text, None
-
- try:
- matches = self.PATTERN.findall(text)
- if not matches:
- return text, None
-
- tool_calls: List[ChatCompletionMessageToolCall] = []
- for match in matches:
- raw_json = match[0] if match[0] else match[1]
- if not raw_json.strip():
- continue
-
- tc_data = json.loads(raw_json)
- tool_calls.append(
- ChatCompletionMessageToolCall(
- id=f"call_{uuid.uuid4().hex[:8]}",
- type="function",
- function=Function(
- name=tc_data["name"],
- arguments=json.dumps(
- tc_data.get("arguments", {}), ensure_ascii=False
- ),
- ),
- )
- )
-
- if not tool_calls:
- return text, None
-
- content = text[: text.find("")].strip()
- return content if content else None, tool_calls
-
- except Exception:
- return text, None
diff --git a/environments/tool_call_parsers/mistral_parser.py b/environments/tool_call_parsers/mistral_parser.py
deleted file mode 100644
index a23684e8739..00000000000
--- a/environments/tool_call_parsers/mistral_parser.py
+++ /dev/null
@@ -1,137 +0,0 @@
-"""
-Mistral tool call parser.
-
-Supports two formats depending on tokenizer version:
-- Pre-v11: content[TOOL_CALLS] [{"name": ..., "arguments": {...}}, ...]
-- v11+: content[TOOL_CALLS]tool_name1{"arg": "val"}[TOOL_CALLS]tool_name2{"arg": "val"}
-
-Based on VLLM's MistralToolParser.extract_tool_calls()
-The [TOOL_CALLS] token is the bot_token used by Mistral models.
-"""
-
-import json
-import uuid
-from typing import List, Optional
-
-from openai.types.chat.chat_completion_message_tool_call import (
- ChatCompletionMessageToolCall,
- Function,
-)
-
-from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser
-
-
-def _generate_mistral_id() -> str:
- """Mistral tool call IDs are 9-char alphanumeric strings."""
- import random
- import string
-
- return "".join(random.choices(string.ascii_letters + string.digits, k=9))
-
-
-@register_parser("mistral")
-class MistralToolCallParser(ToolCallParser):
- """
- Parser for Mistral-format tool calls.
-
- Detects format by checking if the content after [TOOL_CALLS] starts with '['
- (pre-v11 JSON array) or with a tool name (v11+ format).
- """
-
- # The [TOOL_CALLS] token -- may appear as different strings depending on tokenizer
- BOT_TOKEN = "[TOOL_CALLS]"
-
- def parse(self, text: str) -> ParseResult:
- if self.BOT_TOKEN not in text:
- return text, None
-
- try:
- parts = text.split(self.BOT_TOKEN)
- content = parts[0].strip()
- raw_tool_calls = parts[1:]
-
- # Detect format: if the first raw part starts with '[', it's pre-v11
- first_raw = raw_tool_calls[0].strip() if raw_tool_calls else ""
- is_pre_v11 = first_raw.startswith("[") or first_raw.startswith("{")
-
- tool_calls: List[ChatCompletionMessageToolCall] = []
-
- if not is_pre_v11:
- # v11+ format: [TOOL_CALLS]tool_name{args}[TOOL_CALLS]tool_name2{args2}
- for raw in raw_tool_calls:
- raw = raw.strip()
- if not raw or "{" not in raw:
- continue
-
- brace_idx = raw.find("{")
- tool_name = raw[:brace_idx].strip()
- args_str = raw[brace_idx:]
-
- # Validate and clean the JSON arguments
- try:
- parsed_args = json.loads(args_str)
- args_str = json.dumps(parsed_args, ensure_ascii=False)
- except json.JSONDecodeError:
- pass # Keep raw if parsing fails
-
- tool_calls.append(
- ChatCompletionMessageToolCall(
- id=_generate_mistral_id(),
- type="function",
- function=Function(name=tool_name, arguments=args_str),
- )
- )
- else:
- # Pre-v11 format: [TOOL_CALLS] [{"name": ..., "arguments": {...}}]
- try:
- parsed = json.loads(first_raw)
- if isinstance(parsed, dict):
- parsed = [parsed]
-
- for tc in parsed:
- if "name" not in tc:
- continue
- args = tc.get("arguments", {})
- if isinstance(args, dict):
- args = json.dumps(args, ensure_ascii=False)
-
- tool_calls.append(
- ChatCompletionMessageToolCall(
- id=_generate_mistral_id(),
- type="function",
- function=Function(
- name=tc["name"], arguments=args
- ),
- )
- )
- except json.JSONDecodeError:
- # Fallback: extract JSON objects using raw_decode
- decoder = json.JSONDecoder()
- idx = 0
- while idx < len(first_raw):
- try:
- obj, end_idx = decoder.raw_decode(first_raw, idx)
- if isinstance(obj, dict) and "name" in obj:
- args = obj.get("arguments", {})
- if isinstance(args, dict):
- args = json.dumps(args, ensure_ascii=False)
- tool_calls.append(
- ChatCompletionMessageToolCall(
- id=_generate_mistral_id(),
- type="function",
- function=Function(
- name=obj["name"], arguments=args
- ),
- )
- )
- idx = end_idx
- except json.JSONDecodeError:
- idx += 1
-
- if not tool_calls:
- return text, None
-
- return content if content else None, tool_calls
-
- except Exception:
- return text, None
diff --git a/environments/tool_call_parsers/qwen3_coder_parser.py b/environments/tool_call_parsers/qwen3_coder_parser.py
deleted file mode 100644
index 042e46f7bf9..00000000000
--- a/environments/tool_call_parsers/qwen3_coder_parser.py
+++ /dev/null
@@ -1,163 +0,0 @@
-"""
-Qwen3-Coder tool call parser.
-
-Format uses XML-style nested tags:
-
-
- value
- value2
-
-
-
-Parameters are extracted from value tags and
-type-converted using the schema if available, otherwise treated as strings.
-
-Based on VLLM's Qwen3CoderToolParser.extract_tool_calls()
-"""
-
-import ast
-import json
-import re
-import uuid
-from typing import Any, Dict, List, Optional
-
-from openai.types.chat.chat_completion_message_tool_call import (
- ChatCompletionMessageToolCall,
- Function,
-)
-
-from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser
-
-
-def _try_convert_value(value: str) -> Any:
- """
- Try to convert a parameter value string to a native Python type.
- Handles null, numbers, booleans, JSON objects/arrays, and falls back to string.
- """
- stripped = value.strip()
-
- # Handle null
- if stripped.lower() == "null":
- return None
-
- # Try JSON first (handles objects, arrays, strings, numbers, booleans)
- try:
- return json.loads(stripped)
- except (json.JSONDecodeError, TypeError):
- pass
-
- # Try Python literal eval (handles tuples, etc.)
- try:
- return ast.literal_eval(stripped)
- except (ValueError, SyntaxError, TypeError):
- pass
-
- # Return as string
- return stripped
-
-
-@register_parser("qwen3_coder")
-class Qwen3CoderToolCallParser(ToolCallParser):
- """
- Parser for Qwen3-Coder XML-format tool calls.
-
- Uses nested XML tags: val
- """
-
- START_TOKEN = ""
- FUNCTION_PREFIX = "(.*?)|(.*?)$", re.DOTALL
- )
-
- # Find function blocks within a tool_call
- FUNCTION_REGEX = re.compile(
- r"||(?=)|$)",
- re.DOTALL,
- )
-
- def _parse_function_call(self, function_str: str) -> Optional[ChatCompletionMessageToolCall]:
- """Parse a single ... block into a ToolCall."""
- try:
- # Extract function name: everything before the first '>'
- gt_idx = function_str.index(">")
- func_name = function_str[:gt_idx].strip()
- params_str = function_str[gt_idx + 1:]
-
- # Extract parameters
- param_dict: Dict[str, Any] = {}
- for match_text in self.PARAMETER_REGEX.findall(params_str):
- if ">" not in match_text:
- continue
- eq_idx = match_text.index(">")
- param_name = match_text[:eq_idx].strip()
- param_value = match_text[eq_idx + 1:]
-
- # Clean up whitespace
- if param_value.startswith("\n"):
- param_value = param_value[1:]
- if param_value.endswith("\n"):
- param_value = param_value[:-1]
-
- param_dict[param_name] = _try_convert_value(param_value)
-
- return ChatCompletionMessageToolCall(
- id=f"call_{uuid.uuid4().hex[:24]}",
- type="function",
- function=Function(
- name=func_name,
- arguments=json.dumps(param_dict, ensure_ascii=False),
- ),
- )
- except (ValueError, IndexError):
- return None
-
- def parse(self, text: str) -> ParseResult:
- if self.FUNCTION_PREFIX not in text:
- return text, None
-
- try:
- # Find all tool_call blocks
- tc_matches = self.TOOL_CALL_REGEX.findall(text)
- raw_blocks = [m[0] if m[0] else m[1] for m in tc_matches]
-
- # Fallback: if no tool_call tags, try the whole text
- if not raw_blocks:
- raw_blocks = [text]
-
- # Find function blocks within each tool_call
- function_strs: List[str] = []
- for block in raw_blocks:
- func_matches = self.FUNCTION_REGEX.findall(block)
- function_strs.extend(m[0] if m[0] else m[1] for m in func_matches)
-
- if not function_strs:
- return text, None
-
- # Parse each function call
- tool_calls: List[ChatCompletionMessageToolCall] = []
- for func_str in function_strs:
- tc = self._parse_function_call(func_str)
- if tc is not None:
- tool_calls.append(tc)
-
- if not tool_calls:
- return text, None
-
- # Content before tool calls
- first_tc = text.find(self.START_TOKEN)
- if first_tc < 0:
- first_tc = text.find(self.FUNCTION_PREFIX)
- content = text[:first_tc].strip() if first_tc > 0 else None
-
- return content, tool_calls
-
- except Exception:
- return text, None
diff --git a/environments/tool_call_parsers/qwen_parser.py b/environments/tool_call_parsers/qwen_parser.py
deleted file mode 100644
index 9c8a8141997..00000000000
--- a/environments/tool_call_parsers/qwen_parser.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""
-Qwen 2.5 tool call parser.
-
-Uses the same format as Hermes.
-Registered as a separate parser name for clarity when using --tool-parser=qwen.
-"""
-
-from environments.tool_call_parsers import register_parser
-from environments.tool_call_parsers.hermes_parser import HermesToolCallParser
-
-
-@register_parser("qwen")
-class QwenToolCallParser(HermesToolCallParser):
- """
- Parser for Qwen 2.5 tool calls.
- Same {"name": ..., "arguments": ...} format as Hermes.
- """
-
- pass # Identical format -- inherits everything from Hermes
diff --git a/environments/tool_context.py b/environments/tool_context.py
deleted file mode 100644
index 9756dadaf7c..00000000000
--- a/environments/tool_context.py
+++ /dev/null
@@ -1,473 +0,0 @@
-"""
-ToolContext -- Unrestricted Tool Access for Reward Functions
-
-A per-rollout handle that gives reward/verification functions direct access to
-ALL hermes-agent tools, scoped to the rollout's task_id. The same task_id means
-the terminal/browser session is the SAME one the model used during its rollout --
-all state (files, processes, browser tabs) is preserved.
-
-The verifier author decides which tools to use. Nothing is hardcoded or gated.
-
-Example usage in a compute_reward():
- async def compute_reward(self, item, result, ctx):
- # Run tests in the model's terminal sandbox
- test = ctx.terminal("pytest -v")
- if test["exit_code"] == 0:
- return 1.0
-
- # Check if a file was created
- content = ctx.read_file("/workspace/solution.py")
- if content.get("content"):
- return 0.5
-
- return 0.0
-"""
-
-import json
-import logging
-import os
-from typing import Any, Dict, List, Optional
-
-import asyncio
-import concurrent.futures
-
-from model_tools import handle_function_call
-from tools.terminal_tool import cleanup_vm
-from tools.browser_tool import cleanup_browser
-
-logger = logging.getLogger(__name__)
-
-# Thread pool for running sync tool calls that internally use asyncio.run()
-_tool_executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
-
-
-def _run_tool_in_thread(tool_name: str, arguments: Dict[str, Any], task_id: str) -> str:
- """
- Run a tool call in a thread pool executor so backends that use asyncio.run()
- internally (modal, docker, daytona) get a clean event loop.
-
- If we're already in an async context, executes handle_function_call() in a
- disposable worker thread and blocks for the result.
- If not (e.g., called from sync code), runs directly.
- """
- try:
- loop = asyncio.get_running_loop()
- # We're in an async context -- need to run in thread
- with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
- future = pool.submit(
- handle_function_call, tool_name, arguments, task_id
- )
- return future.result(timeout=300)
- except RuntimeError:
- # No running event loop -- safe to call directly
- return handle_function_call(tool_name, arguments, task_id)
-
-
-class ToolContext:
- """
- Open-ended access to all hermes-agent tools for a specific rollout.
-
- Passed to compute_reward() so verifiers can use any tool they need:
- terminal commands, file reads/writes, web searches, browser automation, etc.
- All calls share the rollout's task_id for session isolation.
- """
-
- def __init__(self, task_id: str):
- self.task_id = task_id
-
- # -------------------------------------------------------------------------
- # Terminal tools
- # -------------------------------------------------------------------------
-
- def terminal(self, command: str, timeout: int = 180) -> Dict[str, Any]:
- """
- Run a command in the rollout's terminal session.
-
- Args:
- command: Shell command to execute
- timeout: Command timeout in seconds
-
- Returns:
- Dict with 'exit_code' (int) and 'output' (str)
- """
- import os
- backend = os.getenv("TERMINAL_ENV", "local")
- logger.debug("ToolContext.terminal [%s backend] task=%s: %s", backend, self.task_id[:8], command[:100])
-
- # Run via thread helper so modal/docker/daytona backends' asyncio.run() doesn't deadlock
- result = _run_tool_in_thread(
- "terminal",
- {"command": command, "timeout": timeout},
- self.task_id,
- )
- try:
- return json.loads(result)
- except json.JSONDecodeError:
- return {"exit_code": -1, "output": result}
-
- # -------------------------------------------------------------------------
- # File tools
- # -------------------------------------------------------------------------
-
- def read_file(self, path: str) -> Dict[str, Any]:
- """
- Read a file from the rollout's filesystem.
-
- Args:
- path: File path to read
-
- Returns:
- Dict with file content or error
- """
- result = handle_function_call(
- "read_file", {"path": path}, task_id=self.task_id
- )
- try:
- return json.loads(result)
- except json.JSONDecodeError:
- return {"error": result}
-
- def write_file(self, path: str, content: str) -> Dict[str, Any]:
- """
- Write a TEXT file in the rollout's filesystem.
-
- Uses a shell heredoc under the hood, so this is only safe for text content.
- For binary files (images, compiled artifacts, etc.), use upload_file() instead.
-
- Args:
- path: File path to write
- content: Text content to write
-
- Returns:
- Dict with success status or error
- """
- result = handle_function_call(
- "write_file", {"path": path, "content": content}, task_id=self.task_id
- )
- try:
- return json.loads(result)
- except json.JSONDecodeError:
- return {"error": result}
-
- def upload_file(self, local_path: str, remote_path: str) -> Dict[str, Any]:
- """
- Upload a local file to the rollout's sandbox (binary-safe).
-
- Unlike write_file() which passes content through a shell heredoc (text-only),
- this method base64-encodes the file and decodes it inside the sandbox.
- Safe for any file type: binaries, images, archives, etc.
-
- For large files (>1MB), the content is split into chunks to avoid
- hitting shell command-length limits.
-
- Args:
- local_path: Path to a local file on the host
- remote_path: Destination path inside the sandbox
-
- Returns:
- Dict with 'exit_code' and 'output'
- """
- import base64
- from pathlib import Path as _Path
-
- local = _Path(local_path)
- if not local.exists():
- return {"exit_code": -1, "output": f"Local file not found: {local_path}"}
-
- raw = local.read_bytes()
- b64 = base64.b64encode(raw).decode("ascii")
-
- # Ensure parent directory exists in the sandbox
- parent = str(_Path(remote_path).parent)
- if parent not in {".", "/"}:
- self.terminal(f"mkdir -p {parent}", timeout=10)
-
- # For small files, single command is fine
- chunk_size = 60_000 # ~60KB per chunk (well within shell limits)
- if len(b64) <= chunk_size:
- result = self.terminal(
- f"printf '%s' '{b64}' | base64 -d > {remote_path}",
- timeout=30,
- )
- else:
- # For larger files, write base64 in chunks then decode
- tmp_b64 = "/tmp/_hermes_upload.b64"
- self.terminal(f": > {tmp_b64}", timeout=5) # truncate
- for i in range(0, len(b64), chunk_size):
- chunk = b64[i : i + chunk_size]
- self.terminal(f"printf '%s' '{chunk}' >> {tmp_b64}", timeout=15)
- result = self.terminal(
- f"base64 -d {tmp_b64} > {remote_path} && rm -f {tmp_b64}",
- timeout=30,
- )
-
- return result
-
- def upload_dir(self, local_dir: str, remote_dir: str) -> List[Dict[str, Any]]:
- """
- Upload an entire local directory to the rollout's sandbox (binary-safe).
-
- Recursively uploads all files, preserving directory structure.
-
- Args:
- local_dir: Path to a local directory on the host
- remote_dir: Destination directory inside the sandbox
-
- Returns:
- List of results, one per file uploaded
- """
- from pathlib import Path as _Path
-
- local = _Path(local_dir)
- if not local.exists() or not local.is_dir():
- return [{"exit_code": -1, "output": f"Local directory not found: {local_dir}"}]
-
- results = []
- for file_path in sorted(local.rglob("*")):
- if file_path.is_file():
- relative = file_path.relative_to(local)
- target = f"{remote_dir}/{relative}"
- results.append(self.upload_file(str(file_path), target))
- return results
-
- def download_file(self, remote_path: str, local_path: str) -> Dict[str, Any]:
- """
- Download a file from the rollout's sandbox to the host (binary-safe).
-
- The inverse of upload_file(). Base64-encodes the file inside the sandbox,
- reads the encoded data through the terminal, and decodes it locally.
- Safe for any file type.
-
- Args:
- remote_path: Path to the file inside the sandbox
- local_path: Destination path on the host
-
- Returns:
- Dict with 'success' (bool) and 'bytes' (int) or 'error' (str)
- """
- import base64
- from pathlib import Path as _Path
-
- # Base64-encode the file inside the sandbox and capture output
- result = self.terminal(
- f"base64 {remote_path} 2>/dev/null",
- timeout=30,
- )
-
- if result.get("exit_code", -1) != 0:
- return {
- "success": False,
- "error": f"Failed to read remote file: {result.get('output', '')}",
- }
-
- b64_data = result.get("output", "").strip()
- if not b64_data:
- return {"success": False, "error": f"Remote file is empty or missing: {remote_path}"}
-
- try:
- raw = base64.b64decode(b64_data)
- except Exception as e:
- return {"success": False, "error": f"Base64 decode failed: {e}"}
-
- # Write to local host filesystem
- local = _Path(local_path)
- local.parent.mkdir(parents=True, exist_ok=True)
- local.write_bytes(raw)
-
- return {"success": True, "bytes": len(raw)}
-
- def download_dir(self, remote_dir: str, local_dir: str) -> List[Dict[str, Any]]:
- """
- Download a directory from the rollout's sandbox to the host (binary-safe).
-
- Lists all files in the remote directory, then downloads each one.
- Preserves directory structure.
-
- Args:
- remote_dir: Path to the directory inside the sandbox
- local_dir: Destination directory on the host
-
- Returns:
- List of results, one per file downloaded
- """
- from pathlib import Path as _Path
-
- # List files in the remote directory
- ls_result = self.terminal(
- f"find {remote_dir} -type f 2>/dev/null",
- timeout=15,
- )
-
- if ls_result.get("exit_code", -1) != 0:
- return [{"success": False, "error": f"Failed to list remote dir: {remote_dir}"}]
-
- file_list = ls_result.get("output", "").strip()
- if not file_list:
- return [{"success": False, "error": f"Remote directory is empty or missing: {remote_dir}"}]
-
- results = []
- for remote_file in file_list.splitlines():
- remote_file = remote_file.strip()
- if not remote_file:
- continue
- # Compute the relative path to preserve directory structure
- if remote_file.startswith(remote_dir):
- relative = remote_file[len(remote_dir):].lstrip("/")
- else:
- relative = _Path(remote_file).name
- local_file = str(_Path(local_dir) / relative)
- results.append(self.download_file(remote_file, local_file))
-
- return results
-
- def search(self, query: str, path: str = ".") -> Dict[str, Any]:
- """
- Search for text in the rollout's filesystem.
-
- Args:
- query: Search query
- path: Directory to search in
-
- Returns:
- Dict with search results
- """
- result = handle_function_call(
- "search_files", {"pattern": query, "path": path}, task_id=self.task_id
- )
- try:
- return json.loads(result)
- except json.JSONDecodeError:
- return {"error": result}
-
- # -------------------------------------------------------------------------
- # Web tools
- # -------------------------------------------------------------------------
-
- def web_search(self, query: str) -> Dict[str, Any]:
- """
- Search the web.
-
- Args:
- query: Search query
-
- Returns:
- Dict with search results
- """
- result = handle_function_call("web_search", {"query": query})
- try:
- return json.loads(result)
- except json.JSONDecodeError:
- return {"error": result}
-
- def web_extract(self, urls: List[str]) -> Dict[str, Any]:
- """
- Extract content from URLs.
-
- Args:
- urls: List of URLs to extract content from
-
- Returns:
- Dict with extracted content
- """
- result = handle_function_call("web_extract", {"urls": urls})
- try:
- return json.loads(result)
- except json.JSONDecodeError:
- return {"error": result}
-
- # -------------------------------------------------------------------------
- # Browser tools
- # -------------------------------------------------------------------------
-
- def browser_navigate(self, url: str) -> Dict[str, Any]:
- """
- Navigate the rollout's browser session to a URL.
-
- Args:
- url: URL to navigate to
-
- Returns:
- Dict with page snapshot or error
- """
- result = handle_function_call(
- "browser_navigate", {"url": url}, task_id=self.task_id
- )
- try:
- return json.loads(result)
- except json.JSONDecodeError:
- return {"error": result}
-
- def browser_snapshot(self) -> Dict[str, Any]:
- """
- Take a snapshot of the current browser page.
-
- Returns:
- Dict with page content/accessibility snapshot
- """
- result = handle_function_call(
- "browser_snapshot", {}, task_id=self.task_id
- )
- try:
- return json.loads(result)
- except json.JSONDecodeError:
- return {"error": result}
-
- # -------------------------------------------------------------------------
- # Generic tool access
- # -------------------------------------------------------------------------
-
- def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> str:
- """
- Call any hermes-agent tool by name.
-
- This is the generic escape hatch -- if a tool doesn't have a convenience
- wrapper above, you can call it directly here.
-
- Args:
- tool_name: Name of the tool (e.g., "vision_analyze", "skills_list")
- arguments: Dict of arguments for the tool
-
- Returns:
- Raw JSON string result from the tool
- """
- return _run_tool_in_thread(tool_name, arguments, self.task_id)
-
- # -------------------------------------------------------------------------
- # Cleanup
- # -------------------------------------------------------------------------
-
- def cleanup(self):
- """
- Release all resources (terminal VMs, browser sessions, background processes)
- for this rollout.
-
- Called automatically by the base environment via try/finally after
- compute_reward() completes. You generally don't need to call this yourself.
- """
- # Kill any background processes from this rollout (safety net)
- try:
- from tools.process_registry import process_registry
- killed = process_registry.kill_all(task_id=self.task_id)
- if killed:
- logger.debug("Process cleanup for task %s: killed %d process(es)", self.task_id, killed)
- except Exception as e:
- logger.debug("Process cleanup for task %s: %s", self.task_id, e)
-
- try:
- cleanup_vm(self.task_id)
- except Exception as e:
- logger.debug("VM cleanup for task %s: %s", self.task_id, e)
-
- # Suppress browser_tool's noisy debug prints during cleanup.
- # The cleanup still runs (safe), it just doesn't spam the console.
- _prev_quiet = os.environ.get("HERMES_QUIET")
- os.environ["HERMES_QUIET"] = "1"
- try:
- cleanup_browser(self.task_id)
- except Exception as e:
- logger.debug("Browser cleanup for task %s: %s", self.task_id, e)
- finally:
- if _prev_quiet is None:
- os.environ.pop("HERMES_QUIET", None)
- else:
- os.environ["HERMES_QUIET"] = _prev_quiet
diff --git a/environments/web_research_env.py b/environments/web_research_env.py
deleted file mode 100644
index c637a7cbeae..00000000000
--- a/environments/web_research_env.py
+++ /dev/null
@@ -1,719 +0,0 @@
-"""
-WebResearchEnv — RL Environment for Multi-Step Web Research
-============================================================
-
-Trains models to do accurate, efficient, multi-source web research.
-
-Reward signals:
- - Answer correctness (LLM judge, 0.0–1.0)
- - Source diversity (used ≥2 distinct domains)
- - Efficiency (penalizes excessive tool calls)
- - Tool usage (bonus for actually using web tools)
-
-Dataset: FRAMES benchmark (Google, 2024) — multi-hop factual questions
- HuggingFace: google/frames-benchmark
- Fallback: built-in sample questions (no HF token needed)
-
-Usage:
- # Phase 1 (OpenAI-compatible server)
- python environments/web_research_env.py serve \\
- --openai.base_url http://localhost:8000/v1 \\
- --openai.model_name YourModel \\
- --openai.server_type openai
-
- # Process mode (offline data generation)
- python environments/web_research_env.py process \\
- --env.data_path_to_save_groups data/web_research.jsonl
-
- # Standalone eval
- python environments/web_research_env.py evaluate \\
- --openai.base_url http://localhost:8000/v1 \\
- --openai.model_name YourModel
-
-Built by: github.com/jackx707
-Inspired by: GroceryMind — production Hermes agent doing live web research
- across German grocery stores (firecrawl + hermes-agent)
-"""
-
-from __future__ import annotations
-
-import asyncio
-import json
-import logging
-import os
-import random
-import re
-import sys
-from pathlib import Path
-from typing import Any, Dict, List, Optional, Tuple
-from urllib.parse import urlparse
-
-from pydantic import Field
-
-# Ensure hermes-agent root is on path
-_repo_root = Path(__file__).resolve().parent.parent
-if str(_repo_root) not in sys.path:
- sys.path.insert(0, str(_repo_root))
-
-# ---------------------------------------------------------------------------
-# Optional HuggingFace datasets import
-# ---------------------------------------------------------------------------
-try:
- from datasets import load_dataset
- HF_AVAILABLE = True
-except ImportError:
- HF_AVAILABLE = False
-
-from atroposlib.envs.base import ScoredDataGroup
-from atroposlib.envs.server_handling.server_manager import APIServerConfig
-from atroposlib.type_definitions import Item
-
-from environments.hermes_base_env import HermesAgentBaseEnv, HermesAgentEnvConfig
-from environments.agent_loop import AgentResult
-from environments.tool_context import ToolContext
-
-logger = logging.getLogger(__name__)
-
-# ---------------------------------------------------------------------------
-# Fallback sample dataset (used when HuggingFace is unavailable)
-# Multi-hop questions requiring real web search to answer.
-# ---------------------------------------------------------------------------
-SAMPLE_QUESTIONS = [
- {
- "question": "What is the current population of the capital city of the country that won the 2022 FIFA World Cup?",
- "answer": "Buenos Aires has approximately 3 million people in the city proper, or around 15 million in the greater metro area.",
- "difficulty": "medium",
- "hops": 2,
- },
- {
- "question": "Who is the CEO of the company that makes the most widely used open-source container orchestration platform?",
- "answer": "The Linux Foundation oversees Kubernetes. CNCF (Cloud Native Computing Foundation) is the specific body — it does not have a traditional CEO but has an executive director.",
- "difficulty": "medium",
- "hops": 2,
- },
- {
- "question": "What programming language was used to write the original version of the web framework used by Instagram?",
- "answer": "Django, which Instagram was built on, is written in Python.",
- "difficulty": "easy",
- "hops": 2,
- },
- {
- "question": "In what year was the university founded where the inventor of the World Wide Web currently holds a professorship?",
- "answer": "Tim Berners-Lee holds a professorship at MIT (founded 1861) and the University of Southampton (founded 1952).",
- "difficulty": "hard",
- "hops": 3,
- },
- {
- "question": "What is the latest stable version of the programming language that ranks #1 on the TIOBE index as of this year?",
- "answer": "Python is currently #1 on TIOBE. The latest stable version should be verified via the official python.org site.",
- "difficulty": "medium",
- "hops": 2,
- },
- {
- "question": "How many employees does the parent company of Instagram have?",
- "answer": "Meta Platforms (parent of Instagram) employs approximately 70,000+ people as of recent reports.",
- "difficulty": "medium",
- "hops": 2,
- },
- {
- "question": "What is the current interest rate set by the central bank of the country where the Eiffel Tower is located?",
- "answer": "The European Central Bank sets rates for France/eurozone. The current rate should be verified — it has changed frequently in 2023-2025.",
- "difficulty": "hard",
- "hops": 2,
- },
- {
- "question": "Which company acquired the startup founded by the creator of Oculus VR?",
- "answer": "Palmer Luckey founded Oculus VR, which was acquired by Facebook (now Meta). He later founded Anduril Industries.",
- "difficulty": "medium",
- "hops": 2,
- },
- {
- "question": "What is the market cap of the company that owns the most popular search engine in Russia?",
- "answer": "Yandex (now split into separate entities after 2024 restructuring). Current market cap should be verified via financial sources.",
- "difficulty": "hard",
- "hops": 2,
- },
- {
- "question": "What was the GDP growth rate of the country that hosted the most recent Summer Olympics?",
- "answer": "Paris, France hosted the 2024 Summer Olympics. France's recent GDP growth should be verified via World Bank or IMF data.",
- "difficulty": "hard",
- "hops": 2,
- },
-]
-
-
-# ---------------------------------------------------------------------------
-# Configuration
-# ---------------------------------------------------------------------------
-
-class WebResearchEnvConfig(HermesAgentEnvConfig):
- """Configuration for the web research RL environment."""
-
- # Reward weights
- correctness_weight: float = Field(
- default=0.6,
- description="Weight for answer correctness in reward (LLM judge score).",
- )
- tool_usage_weight: float = Field(
- default=0.2,
- description="Weight for tool usage signal (did the model actually use web tools?).",
- )
- efficiency_weight: float = Field(
- default=0.2,
- description="Weight for efficiency signal (penalizes excessive tool calls).",
- )
- diversity_bonus: float = Field(
- default=0.1,
- description="Bonus reward for citing ≥2 distinct domains.",
- )
-
- # Efficiency thresholds
- efficient_max_calls: int = Field(
- default=5,
- description="Maximum tool calls before efficiency penalty begins.",
- )
- heavy_penalty_calls: int = Field(
- default=10,
- description="Tool call count where efficiency penalty steepens.",
- )
-
- # Eval
- eval_size: int = Field(
- default=20,
- description="Number of held-out items for evaluation.",
- )
- eval_split_ratio: float = Field(
- default=0.1,
- description="Fraction of dataset to hold out for evaluation (0.0–1.0).",
- )
-
- # Dataset
- dataset_name: str = Field(
- default="google/frames-benchmark",
- description="HuggingFace dataset name for research questions.",
- )
-
-
-# ---------------------------------------------------------------------------
-# Environment
-# ---------------------------------------------------------------------------
-
-class WebResearchEnv(HermesAgentBaseEnv):
- """
- RL environment for training multi-step web research skills.
-
- The model is given a factual question requiring 2-3 hops of web research
- and must use web_search / web_extract tools to find and synthesize the answer.
-
- Reward is multi-signal:
- 60% — answer correctness (LLM judge)
- 20% — tool usage (did the model actually search the web?)
- 20% — efficiency (penalizes >5 tool calls)
-
- Bonus +0.1 for source diversity (≥2 distinct domains cited).
- """
-
- name = "web-research"
- env_config_cls = WebResearchEnvConfig
-
- # Default toolsets for this environment — web + file for saving notes
- default_toolsets = ["web", "file"]
-
- @classmethod
- def config_init(cls) -> Tuple[WebResearchEnvConfig, List[APIServerConfig]]:
- """Default configuration for the web research environment."""
- env_config = WebResearchEnvConfig(
- enabled_toolsets=["web", "file"],
- max_agent_turns=15,
- agent_temperature=1.0,
- system_prompt=(
- "You are a highly capable research agent. When asked a factual question, "
- "always use web_search to find current, accurate information before answering. "
- "Cite at least 2 sources. Be concise and accurate."
- ),
- group_size=4,
- total_steps=1000,
- steps_per_eval=100,
- use_wandb=True,
- wandb_name="web-research",
- )
-
- server_configs = [
- APIServerConfig(
- base_url="https://openrouter.ai/api/v1",
- model_name="anthropic/claude-sonnet-4.5",
- server_type="openai",
- api_key=os.getenv("OPENROUTER_API_KEY", ""),
- health_check=False,
- )
- ]
-
- return env_config, server_configs
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self._items: list[dict] = []
- self._eval_items: list[dict] = []
- self._index: int = 0
-
- # Metrics tracking for wandb
- self._reward_buffer: list[float] = []
- self._correctness_buffer: list[float] = []
- self._tool_usage_buffer: list[float] = []
- self._efficiency_buffer: list[float] = []
- self._diversity_buffer: list[float] = []
-
- # ------------------------------------------------------------------
- # 1. Setup — load dataset
- # ------------------------------------------------------------------
-
- async def setup(self) -> None:
- """Load the FRAMES benchmark or fall back to built-in samples."""
- if HF_AVAILABLE:
- try:
- logger.info("Loading FRAMES benchmark from HuggingFace...")
- ds = load_dataset(self.config.dataset_name, split="test")
- self._items = [
- {
- "question": row["Prompt"],
- "answer": row["Answer"],
- "difficulty": row.get("reasoning_types", "unknown"),
- "hops": 2,
- }
- for row in ds
- ]
- # Hold out for eval
- eval_size = max(
- self.config.eval_size,
- int(len(self._items) * self.config.eval_split_ratio),
- )
- random.shuffle(self._items)
- self._eval_items = self._items[:eval_size]
- self._items = self._items[eval_size:]
- logger.info(
- f"Loaded {len(self._items)} train / {len(self._eval_items)} eval items "
- f"from FRAMES benchmark."
- )
- return
- except Exception as e:
- logger.warning(f"Could not load FRAMES from HuggingFace: {e}. Using built-in samples.")
-
- # Fallback
- random.shuffle(SAMPLE_QUESTIONS)
- split = max(1, len(SAMPLE_QUESTIONS) * 8 // 10)
- self._items = SAMPLE_QUESTIONS[:split]
- self._eval_items = SAMPLE_QUESTIONS[split:]
- logger.info(
- f"Using built-in sample dataset: {len(self._items)} train / "
- f"{len(self._eval_items)} eval items."
- )
-
- # ------------------------------------------------------------------
- # 2. get_next_item — return the next question
- # ------------------------------------------------------------------
-
- async def get_next_item(self) -> dict:
- """Return the next item, cycling through the dataset."""
- if not self._items:
- raise RuntimeError("Dataset is empty. Did you call setup()?")
- item = self._items[self._index % len(self._items)]
- self._index += 1
- return item
-
- # ------------------------------------------------------------------
- # 3. format_prompt — build the user-facing prompt
- # ------------------------------------------------------------------
-
- def format_prompt(self, item: dict) -> str:
- """Format the research question as a task prompt."""
- return (
- f"Research the following question thoroughly using web search. "
- f"You MUST search the web to find current, accurate information — "
- f"do not rely solely on your training data.\n\n"
- f"Question: {item['question']}\n\n"
- f"Requirements:\n"
- f"- Use web_search and/or web_extract tools to find information\n"
- f"- Search at least 2 different sources\n"
- f"- Provide a concise, accurate answer (2-4 sentences)\n"
- f"- Cite the sources you used"
- )
-
- # ------------------------------------------------------------------
- # 4. compute_reward — multi-signal scoring
- # ------------------------------------------------------------------
-
- async def compute_reward(
- self,
- item: dict,
- result: AgentResult,
- ctx: ToolContext,
- ) -> float:
- """
- Multi-signal reward function:
-
- correctness_weight * correctness — LLM judge comparing answer to ground truth
- tool_usage_weight * tool_used — binary: did the model use web tools?
- efficiency_weight * efficiency — penalizes wasteful tool usage
- + diversity_bonus — source diversity (≥2 distinct domains)
- """
- # Extract final response from messages (last assistant message with content)
- final_response = ""
- tools_used: list[str] = []
- for msg in reversed(result.messages):
- if msg.get("role") == "assistant" and msg.get("content") and not final_response:
- final_response = msg["content"]
- # Collect tool names from tool call messages
- if msg.get("role") == "assistant" and msg.get("tool_calls"):
- for tc in msg["tool_calls"]:
- fn = tc.get("function", {}) if isinstance(tc, dict) else {}
- name = fn.get("name", "")
- if name:
- tools_used.append(name)
- tool_call_count: int = result.turns_used or len(tools_used)
-
- cfg = self.config
-
- # ---- Signal 1: Answer correctness (LLM judge) ----------------
- correctness = await self._llm_judge(
- question=item["question"],
- expected=item["answer"],
- model_answer=final_response,
- )
-
- # ---- Signal 2: Web tool usage --------------------------------
- web_tools = {"web_search", "web_extract", "search", "firecrawl"}
- tool_used = 1.0 if any(t in web_tools for t in tools_used) else 0.0
-
- # ---- Signal 3: Efficiency ------------------------------------
- if tool_call_count <= cfg.efficient_max_calls:
- efficiency = 1.0
- elif tool_call_count <= cfg.heavy_penalty_calls:
- efficiency = 1.0 - (tool_call_count - cfg.efficient_max_calls) * 0.08
- else:
- efficiency = max(0.0, 1.0 - (tool_call_count - cfg.efficient_max_calls) * 0.12)
-
- # ---- Bonus: Source diversity ---------------------------------
- domains = self._extract_domains(final_response)
- diversity = cfg.diversity_bonus if len(domains) >= 2 else 0.0
-
- # ---- Combine ------------------------------------------------
- reward = (
- cfg.correctness_weight * correctness
- + cfg.tool_usage_weight * tool_used
- + cfg.efficiency_weight * efficiency
- + diversity
- )
- reward = min(1.0, max(0.0, reward)) # clamp to [0, 1]
-
- # Track for wandb
- self._reward_buffer.append(reward)
- self._correctness_buffer.append(correctness)
- self._tool_usage_buffer.append(tool_used)
- self._efficiency_buffer.append(efficiency)
- self._diversity_buffer.append(diversity)
-
- logger.debug(
- f"Reward breakdown — correctness={correctness:.2f}, "
- f"tool_used={tool_used:.1f}, efficiency={efficiency:.2f}, "
- f"diversity={diversity:.1f} → total={reward:.3f}"
- )
-
- return reward
-
- # ------------------------------------------------------------------
- # 5. evaluate — run on held-out eval split
- # ------------------------------------------------------------------
-
- async def evaluate(self, *args, **kwargs) -> None:
- """Run evaluation on the held-out split using the full agent loop with tools.
-
- Each eval item runs through the same agent loop as training —
- the model can use web_search, web_extract, etc. to research answers.
- This measures actual agentic research capability, not just knowledge.
- """
- import time
- import uuid
- from environments.agent_loop import HermesAgentLoop
- from environments.tool_context import ToolContext
-
- items = self._eval_items
- if not items:
- logger.warning("No eval items available.")
- return
-
- eval_size = min(self.config.eval_size, len(items))
- eval_items = items[:eval_size]
-
- logger.info(f"Running eval on {len(eval_items)} questions (with agent loop + tools)...")
- start_time = time.time()
- samples = []
-
- # Resolve tools once for all eval items
- tools, valid_names = self._resolve_tools_for_group()
-
- for i, item in enumerate(eval_items):
- task_id = str(uuid.uuid4())
- logger.info(f"Eval [{i+1}/{len(eval_items)}]: {item['question'][:80]}...")
-
- try:
- # Build messages
- messages: List[Dict[str, Any]] = []
- if self.config.system_prompt:
- messages.append({"role": "system", "content": self.config.system_prompt})
- messages.append({"role": "user", "content": self.format_prompt(item)})
-
- # Run the full agent loop with tools
- agent = HermesAgentLoop(
- server=self.server,
- tool_schemas=tools,
- valid_tool_names=valid_names,
- max_turns=self.config.max_agent_turns,
- task_id=task_id,
- temperature=0.0, # Deterministic for eval
- max_tokens=self.config.max_token_length,
- extra_body=self.config.extra_body,
- budget_config=self.config.build_budget_config(),
- )
- result = await agent.run(messages)
-
- # Extract final response and tool usage from messages
- final_response = ""
- tool_call_count = 0
- for msg in reversed(result.messages):
- if msg.get("role") == "assistant" and msg.get("content") and not final_response:
- final_response = msg["content"]
- if msg.get("role") == "assistant" and msg.get("tool_calls"):
- tool_call_count += len(msg["tool_calls"])
-
- # Compute reward (includes LLM judge for correctness)
- # Temporarily save buffer lengths so we can extract the
- # correctness score without calling judge twice, and avoid
- # polluting training metric buffers with eval data.
- buf_len = len(self._correctness_buffer)
- ctx = ToolContext(task_id)
- try:
- reward = await self.compute_reward(item, result, ctx)
- finally:
- ctx.cleanup()
-
- # Extract correctness from the buffer (compute_reward appended it)
- # then remove eval entries from training buffers
- correctness = (
- self._correctness_buffer[buf_len]
- if len(self._correctness_buffer) > buf_len
- else 0.0
- )
- # Roll back buffers to avoid polluting training metrics
- for buf in (
- self._reward_buffer, self._correctness_buffer,
- self._tool_usage_buffer, self._efficiency_buffer,
- self._diversity_buffer,
- ):
- if len(buf) > buf_len:
- buf.pop()
-
- samples.append({
- "prompt": item["question"],
- "response": final_response[:500],
- "expected": item["answer"],
- "correctness": correctness,
- "reward": reward,
- "tool_calls": tool_call_count,
- "turns": result.turns_used,
- })
-
- logger.info(
- f" → correctness={correctness:.2f}, reward={reward:.3f}, "
- f"tools={tool_call_count}, turns={result.turns_used}"
- )
-
- except Exception as e:
- logger.error(f"Eval error on item: {e}")
- samples.append({
- "prompt": item["question"],
- "response": f"ERROR: {e}",
- "expected": item["answer"],
- "correctness": 0.0,
- "reward": 0.0,
- "tool_calls": 0,
- "turns": 0,
- })
-
- end_time = time.time()
-
- # Compute aggregate metrics
- correctness_scores = [s["correctness"] for s in samples]
- rewards = [s["reward"] for s in samples]
- tool_counts = [s["tool_calls"] for s in samples]
- n = len(samples)
-
- eval_metrics = {
- "eval/mean_correctness": sum(correctness_scores) / n if n else 0.0,
- "eval/mean_reward": sum(rewards) / n if n else 0.0,
- "eval/mean_tool_calls": sum(tool_counts) / n if n else 0.0,
- "eval/tool_usage_rate": sum(1 for t in tool_counts if t > 0) / n if n else 0.0,
- "eval/n_items": n,
- }
-
- logger.info(
- f"Eval complete — correctness={eval_metrics['eval/mean_correctness']:.3f}, "
- f"reward={eval_metrics['eval/mean_reward']:.3f}, "
- f"tool_usage={eval_metrics['eval/tool_usage_rate']:.0%}"
- )
-
- await self.evaluate_log(
- metrics=eval_metrics,
- samples=samples,
- start_time=start_time,
- end_time=end_time,
- )
-
- # ------------------------------------------------------------------
- # 6. wandb_log — custom metrics
- # ------------------------------------------------------------------
-
- async def wandb_log(self, wandb_metrics: Optional[Dict] = None) -> None:
- """Log reward breakdown metrics to wandb."""
- if wandb_metrics is None:
- wandb_metrics = {}
-
- if self._reward_buffer:
- n = len(self._reward_buffer)
- wandb_metrics["train/mean_reward"] = sum(self._reward_buffer) / n
- wandb_metrics["train/mean_correctness"] = sum(self._correctness_buffer) / n
- wandb_metrics["train/mean_tool_usage"] = sum(self._tool_usage_buffer) / n
- wandb_metrics["train/mean_efficiency"] = sum(self._efficiency_buffer) / n
- wandb_metrics["train/mean_diversity"] = sum(self._diversity_buffer) / n
- wandb_metrics["train/total_rollouts"] = n
-
- # Accuracy buckets
- wandb_metrics["train/correct_rate"] = (
- sum(1 for c in self._correctness_buffer if c >= 0.7) / n
- )
- wandb_metrics["train/tool_usage_rate"] = (
- sum(1 for t in self._tool_usage_buffer if t > 0) / n
- )
-
- # Clear buffers
- self._reward_buffer.clear()
- self._correctness_buffer.clear()
- self._tool_usage_buffer.clear()
- self._efficiency_buffer.clear()
- self._diversity_buffer.clear()
-
- await super().wandb_log(wandb_metrics)
-
- # ------------------------------------------------------------------
- # Private helpers
- # ------------------------------------------------------------------
-
- async def _llm_judge(
- self,
- question: str,
- expected: str,
- model_answer: str,
- ) -> float:
- """
- Use the server's LLM to judge answer correctness.
- Falls back to keyword heuristic if LLM call fails.
- """
- if not model_answer or not model_answer.strip():
- return 0.0
-
- judge_prompt = (
- "You are an impartial judge evaluating the quality of an AI research answer.\n\n"
- f"Question: {question}\n\n"
- f"Reference answer: {expected}\n\n"
- f"Model answer: {model_answer}\n\n"
- "Score the model answer on a scale from 0.0 to 1.0 where:\n"
- " 1.0 = fully correct and complete\n"
- " 0.7 = mostly correct with minor gaps\n"
- " 0.4 = partially correct\n"
- " 0.1 = mentions relevant topic but wrong or very incomplete\n"
- " 0.0 = completely wrong or no answer\n\n"
- "Consider: factual accuracy, completeness, and relevance.\n"
- 'Respond with ONLY a JSON object: {"score": , "reason": ""}'
- )
-
- try:
- response = await self.server.chat_completion(
- messages=[{"role": "user", "content": judge_prompt}],
- n=1,
- max_tokens=150,
- temperature=0.0,
- split="eval",
- )
- text = response.choices[0].message.content if response.choices else ""
- parsed = self._parse_judge_json(text)
- if parsed is not None:
- return float(parsed)
- except Exception as e:
- logger.debug(f"LLM judge failed: {e}. Using heuristic.")
-
- return self._heuristic_score(expected, model_answer)
-
- @staticmethod
- def _parse_judge_json(text: str) -> Optional[float]:
- """Extract the score float from LLM judge JSON response."""
- try:
- clean = re.sub(r"```(?:json)?|```", "", text).strip()
- data = json.loads(clean)
- score = float(data.get("score", -1))
- if 0.0 <= score <= 1.0:
- return score
- except Exception:
- match = re.search(r'"score"\s*:\s*([0-9.]+)', text)
- if match:
- score = float(match.group(1))
- if 0.0 <= score <= 1.0:
- return score
- return None
-
- @staticmethod
- def _heuristic_score(expected: str, model_answer: str) -> float:
- """Lightweight keyword overlap score as fallback."""
- stopwords = {
- "the", "a", "an", "is", "are", "was", "were", "of", "in", "on",
- "at", "to", "for", "with", "and", "or", "but", "it", "its",
- "this", "that", "as", "by", "from", "be", "has", "have", "had",
- }
-
- def tokenize(text: str) -> set:
- tokens = re.findall(r'\b\w+\b', text.lower())
- return {t for t in tokens if t not in stopwords and len(t) > 2}
-
- expected_tokens = tokenize(expected)
- answer_tokens = tokenize(model_answer)
-
- if not expected_tokens:
- return 0.5
-
- overlap = len(expected_tokens & answer_tokens)
- union = len(expected_tokens | answer_tokens)
-
- jaccard = overlap / union if union > 0 else 0.0
- recall = overlap / len(expected_tokens)
- return min(1.0, 0.4 * jaccard + 0.6 * recall)
-
- @staticmethod
- def _extract_domains(text: str) -> set:
- """Extract unique domains from URLs cited in the response."""
- urls = re.findall(r'https?://[^\s\)>\]"\']+', text)
- domains = set()
- for url in urls:
- try:
- parsed = urlparse(url)
- domain = parsed.netloc.lower().lstrip("www.")
- if domain:
- domains.add(domain)
- except Exception:
- pass
- return domains
-
-
-# ---------------------------------------------------------------------------
-# Entry point
-# ---------------------------------------------------------------------------
-
-if __name__ == "__main__":
- WebResearchEnv.cli()
diff --git a/hermes_cli/config.py b/hermes_cli/config.py
index c3a8152f4a7..a560e1e6a1e 100644
--- a/hermes_cli/config.py
+++ b/hermes_cli/config.py
@@ -2138,22 +2138,6 @@ OPTIONAL_ENV_VARS = {
"password": True,
"category": "tool",
},
- "TINKER_API_KEY": {
- "description": "Tinker API key for RL training",
- "prompt": "Tinker API key",
- "url": "https://tinker-console.thinkingmachines.ai/keys",
- "tools": ["rl_start_training", "rl_check_status", "rl_stop_training"],
- "password": True,
- "category": "tool",
- },
- "WANDB_API_KEY": {
- "description": "Weights & Biases API key for experiment tracking",
- "prompt": "WandB API key",
- "url": "https://wandb.ai/authorize",
- "tools": ["rl_get_results", "rl_check_status"],
- "password": True,
- "category": "tool",
- },
"VOICE_TOOLS_OPENAI_KEY": {
"description": "OpenAI API key for voice transcription (Whisper) and OpenAI TTS",
"prompt": "OpenAI API Key (for Whisper STT + TTS)",
@@ -4990,8 +4974,7 @@ def set_config_value(key: str, value: str):
'FAL_KEY', 'TELEGRAM_BOT_TOKEN', 'DISCORD_BOT_TOKEN',
'TERMINAL_SSH_HOST', 'TERMINAL_SSH_USER', 'TERMINAL_SSH_KEY',
'SUDO_PASSWORD', 'SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN',
- 'GITHUB_TOKEN', 'HONCHO_API_KEY', 'WANDB_API_KEY',
- 'TINKER_API_KEY',
+ 'GITHUB_TOKEN', 'HONCHO_API_KEY',
]
if key.upper() in api_keys or key.upper().endswith(('_API_KEY', '_TOKEN')) or key.upper().startswith('TERMINAL_SSH'):
diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py
index a551d4d204e..c2035b03e6e 100644
--- a/hermes_cli/doctor.py
+++ b/hermes_cli/doctor.py
@@ -1595,28 +1595,6 @@ def run_doctor(args):
for _issue in _r.issues:
issues.append(_issue)
- # =========================================================================
- # Check: Submodules
- # =========================================================================
- print()
- print(color("◆ Submodules", Colors.CYAN, Colors.BOLD))
-
- # tinker-atropos (RL training backend)
- tinker_dir = PROJECT_ROOT / "tinker-atropos"
- if tinker_dir.exists() and (tinker_dir / "pyproject.toml").exists():
- if py_version >= (3, 11):
- try:
- __import__("tinker_atropos")
- check_ok("tinker-atropos", "(RL training backend)")
- except ImportError:
- install_cmd = f"{_python_install_cmd()} -e ./tinker-atropos"
- check_warn("tinker-atropos found but not installed", f"(run: {install_cmd})")
- issues.append(f"Install tinker-atropos: {install_cmd}")
- else:
- check_warn("tinker-atropos requires Python 3.11+", f"(current: {py_version.major}.{py_version.minor})")
- else:
- check_warn("tinker-atropos not found", "(run: git submodule update --init --recursive)")
-
# =========================================================================
# Check: Tool Availability
# =========================================================================
diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py
index 6a8bf950589..5d635b2c464 100644
--- a/hermes_cli/setup.py
+++ b/hermes_cli/setup.py
@@ -522,14 +522,6 @@ def _print_setup_summary(config: dict, hermes_home):
elif managed_nous_tools_enabled() and subscription_features.nous_auth_present:
tool_status.append(("Modal Execution (optional via Nous subscription)", True, None))
- # Tinker + WandB (RL training)
- if get_env_value("TINKER_API_KEY") and get_env_value("WANDB_API_KEY"):
- tool_status.append(("RL Training (Tinker)", True, None))
- elif get_env_value("TINKER_API_KEY"):
- tool_status.append(("RL Training (Tinker)", False, "WANDB_API_KEY"))
- else:
- tool_status.append(("RL Training (Tinker)", False, "TINKER_API_KEY"))
-
# Home Assistant
if get_env_value("HASS_TOKEN"):
tool_status.append(("Smart Home (Home Assistant)", True, None))
diff --git a/hermes_cli/status.py b/hermes_cli/status.py
index b4417091ca7..f2164ac8a4d 100644
--- a/hermes_cli/status.py
+++ b/hermes_cli/status.py
@@ -141,8 +141,6 @@ def show_status(args):
"Browser Use": "BROWSER_USE_API_KEY", # Optional — local browser works without this
"Browserbase": "BROWSERBASE_API_KEY", # Optional — direct credentials only
"FAL": "FAL_KEY",
- "Tinker": "TINKER_API_KEY",
- "WandB": "WANDB_API_KEY",
"ElevenLabs": "ELEVENLABS_API_KEY",
"GitHub": "GITHUB_TOKEN",
}
diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py
index 87474040530..fc5b1acf5cf 100644
--- a/hermes_cli/tools_config.py
+++ b/hermes_cli/tools_config.py
@@ -71,7 +71,6 @@ CONFIGURABLE_TOOLSETS = [
("delegation", "👥 Task Delegation", "delegate_task"),
("cronjob", "⏰ Cron Jobs", "create/list/update/pause/resume/run, with optional attached skills"),
("messaging", "📨 Cross-Platform Messaging", "send_message"),
- ("rl", "🧪 RL Training", "Tinker-Atropos training tools"),
("homeassistant", "🏠 Home Assistant", "smart home device control"),
("spotify", "🎵 Spotify", "playback, search, playlists, library"),
("discord", "💬 Discord (read/participate)", "fetch messages, search members, create thread"),
@@ -87,7 +86,7 @@ CONFIGURABLE_TOOLSETS = [
# Video gen is off by default — it's a niche, paid, slow feature. Users
# who want it opt in via `hermes tools` → Video Generation, which walks
# them through provider + model selection.
-_DEFAULT_OFF_TOOLSETS = {"moa", "homeassistant", "rl", "spotify", "discord", "discord_admin", "video", "video_gen"}
+_DEFAULT_OFF_TOOLSETS = {"moa", "homeassistant", "spotify", "discord", "discord_admin", "video", "video_gen"}
# Platform-scoped toolsets: only appear in the `hermes tools` checklist for
# these platforms, and only resolve/save for these platforms. A toolset
@@ -424,22 +423,6 @@ TOOL_CATEGORIES = {
},
],
},
- "rl": {
- "name": "RL Training",
- "icon": "🧪",
- "requires_python": (3, 11),
- "providers": [
- {
- "name": "Tinker / Atropos",
- "tag": "RL training platform",
- "env_vars": [
- {"key": "TINKER_API_KEY", "prompt": "Tinker API key", "url": "https://tinker-console.thinkingmachines.ai/keys"},
- {"key": "WANDB_API_KEY", "prompt": "WandB API key", "url": "https://wandb.ai/authorize"},
- ],
- "post_setup": "rl_training",
- },
- ],
- },
"langfuse": {
"name": "Langfuse Observability",
"icon": "📊",
@@ -912,24 +895,6 @@ def _run_post_setup(post_setup_key: str):
_print_warning(f" Spotify login failed: {exc}")
_print_info(" Run manually: hermes auth spotify")
- elif post_setup_key == "rl_training":
- try:
- __import__("tinker_atropos")
- except ImportError:
- tinker_dir = PROJECT_ROOT / "tinker-atropos"
- if tinker_dir.exists() and (tinker_dir / "pyproject.toml").exists():
- _print_info(" Installing tinker-atropos submodule...")
- result = _pip_install(["-e", str(tinker_dir)])
- if result.returncode == 0:
- _print_success(" tinker-atropos installed")
- else:
- _print_warning(" tinker-atropos install failed - run manually:")
- _print_info(' uv pip install -e "./tinker-atropos"')
- else:
- _print_warning(" tinker-atropos submodule not found - run:")
- _print_info(" git submodule update --init --recursive")
- _print_info(' uv pip install -e "./tinker-atropos"')
-
elif post_setup_key == "langfuse":
# Install the langfuse SDK.
try:
diff --git a/model_tools.py b/model_tools.py
index 0b9178111a5..db19bb67e53 100644
--- a/model_tools.py
+++ b/model_tools.py
@@ -97,9 +97,7 @@ def _run_async(coro):
asyncio.run()'s create-and-destroy lifecycle.
This is the single source of truth for sync->async bridging in tool
- handlers. The RL paths (agent_loop.py, tool_context.py) also provide
- outer thread-pool wrapping as defense-in-depth, but each handler is
- self-protecting via this function.
+ handlers. Each handler is self-protecting via this function.
"""
try:
loop = asyncio.get_running_loop()
@@ -231,13 +229,6 @@ _LEGACY_TOOLSET_MAP = {
"browser_vision", "browser_console"
],
"cronjob_tools": ["cronjob"],
- "rl_tools": [
- "rl_list_environments", "rl_select_environment",
- "rl_get_current_config", "rl_edit_config",
- "rl_start_training", "rl_check_status",
- "rl_stop_training", "rl_get_results",
- "rl_list_runs", "rl_test_inference"
- ],
"file_tools": ["read_file", "write_file", "patch", "search_files"],
"tts_tools": ["text_to_speech"],
}
diff --git a/nix/hermes-agent.nix b/nix/hermes-agent.nix
index ce8be16cfdd..6c391878cc5 100644
--- a/nix/hermes-agent.nix
+++ b/nix/hermes-agent.nix
@@ -192,7 +192,6 @@ stdenv.mkDerivation {
source .venv/bin/activate
uv pip install -e ".[all]"
[ -d mini-swe-agent ] && uv pip install -e ./mini-swe-agent 2>/dev/null || true
- [ -d tinker-atropos ] && uv pip install -e ./tinker-atropos 2>/dev/null || true
mkdir -p .nix-stamps
echo "$STAMP_VALUE" > "$STAMP"
else
diff --git a/optional-skills/mlops/hermes-atropos-environments/SKILL.md b/optional-skills/mlops/hermes-atropos-environments/SKILL.md
deleted file mode 100644
index 6766c381014..00000000000
--- a/optional-skills/mlops/hermes-atropos-environments/SKILL.md
+++ /dev/null
@@ -1,303 +0,0 @@
----
-name: hermes-atropos-environments
-description: Build, test, and debug Hermes Agent RL environments for Atropos training. Covers the HermesAgentBaseEnv interface, reward functions, agent loop integration, evaluation with tools, wandb logging, and the three CLI modes (serve/process/evaluate). Use when creating, reviewing, or fixing RL environments in the hermes-agent repo.
-version: 1.1.0
-author: Hermes Agent
-license: MIT
-platforms: [linux, macos, windows]
-metadata:
- hermes:
- tags: [atropos, rl, environments, training, reinforcement-learning, reward-functions]
- related_skills: [axolotl, fine-tuning-with-trl, lm-evaluation-harness]
----
-
-# Hermes Agent Atropos Environments
-
-Guide for building RL environments in the hermes-agent repo that integrate with the Atropos training framework.
-
-## Architecture Overview
-
-```
-Atropos BaseEnv (atroposlib/envs/base.py)
- └── HermesAgentBaseEnv (environments/hermes_base_env.py)
- ├── Handles agent loop orchestration
- ├── Handles tool resolution per group
- ├── Handles ToolContext for reward verification
- └── YOUR ENVIRONMENT (environments/your_env.py)
- Only implements: setup, get_next_item, format_prompt,
- compute_reward, evaluate, wandb_log
-```
-
-Hermes environments are special because they run a **multi-turn agent loop with tool calling** — not just single-turn completions. The base env handles the loop; you implement the task and scoring.
-
-## File Locations
-
-| File | Purpose |
-|------|---------|
-| `environments/hermes_base_env.py` | Base class with agent loop + tool resolution |
-| `environments/agent_loop.py` | `HermesAgentLoop` + `AgentResult` dataclass |
-| `environments/tool_context.py` | `ToolContext` for reward verification |
-| `environments/tool_call_parsers.py` | Phase 2 tool call parsers (hermes, mistral, etc.) |
-| `environments/your_env.py` | Your environment implementation |
-
-## Inference Setup — Ask the User First
-
-**IMPORTANT:** Before running any test, evaluation, or data generation command, always ask the user how they want to handle inference. Do NOT assume OpenRouter or any specific endpoint. Present these options:
-
-1. **OpenRouter** — Ask which model they want to use (e.g., `anthropic/claude-sonnet-4.5`, `google/gemini-2.5-pro`, `meta-llama/llama-3.3-70b-instruct`, etc.). Requires `OPENROUTER_API_KEY` in environment.
-2. **Self-hosted VLLM endpoint** — Ask for their base URL (e.g., `http://localhost:8000/v1`) and model name. Set `--openai.server_type vllm`.
-3. **Other OpenAI-compatible API** — Ask for the base URL, model name, and any required API key. Set `--openai.server_type openai` and `--openai.health_check false`.
-4. **Local Atropos training server** — For `serve` mode with a live training loop. Default `http://localhost:8000/v1`.
-
-Once the user tells you their setup, use those values in all CLI commands for that session. Example prompts:
-
-> "Before I run this, how would you like to handle inference?
-> 1. OpenRouter (I'll need your preferred model, e.g. claude-sonnet-4.5)
-> 2. A self-hosted VLLM endpoint (give me the URL and model name)
-> 3. Another OpenAI-compatible API (give me the URL, model, and any auth details)
-> 4. Local Atropos training server (serve mode)"
-
-### Key flags by provider:
-
-| Provider | `--openai.server_type` | `--openai.health_check` | `--openai.api_key` |
-|----------|----------------------|------------------------|-------------------|
-| OpenRouter | `openai` | `false` | `$OPENROUTER_API_KEY` |
-| VLLM (self-hosted) | `vllm` | (default) | (not needed) |
-| Other OpenAI-compatible | `openai` | `false` | As needed |
-| Local Atropos | (default) | (default) | (not needed) |
-
-## Required Methods
-
-### 1. `setup()` — Load dataset and initialize state
-
-```python
-async def setup(self) -> None:
- """Called once at startup. Load datasets, initialize state."""
- # Try HuggingFace first, fallback to built-in samples
- try:
- from datasets import load_dataset
- ds = load_dataset("your/dataset", split="test")
- self._items = [...]
- except Exception:
- self._items = BUILTIN_SAMPLES
-
- # Always split into train/eval
- random.shuffle(self._items)
- eval_size = max(20, int(len(self._items) * 0.1))
- self._eval_items = self._items[:eval_size]
- self._items = self._items[eval_size:]
-```
-
-### 2. `get_next_item()` — Return next training item
-
-```python
-async def get_next_item(self) -> dict:
- """Return next item, cycling through dataset."""
- item = self._items[self._index % len(self._items)]
- self._index += 1
- return item
-```
-
-### 3. `format_prompt(item)` — Convert item to user message
-
-```python
-def format_prompt(self, item: dict) -> str:
- """Convert a dataset item into the user-facing prompt."""
- return f"Research this question: {item['question']}"
-```
-
-### 4. `compute_reward(item, result, ctx)` — Score the rollout
-
-**CRITICAL**: `result` is an `AgentResult`, NOT a dict. It has these attributes:
-- `result.messages` — List of message dicts (OpenAI format)
-- `result.turns_used` — Number of LLM calls made
-- `result.finished_naturally` — True if model stopped voluntarily
-- `result.tool_errors` — List of ToolError objects
-
-**AgentResult does NOT have**: `final_response`, `tool_calls`, `tools_used`.
-You must extract these from `result.messages`:
-
-```python
-async def compute_reward(self, item, result: AgentResult, ctx: ToolContext) -> float:
- # Extract final response (last assistant message with content)
- final_response = ""
- tools_used = []
- for msg in reversed(result.messages):
- if msg.get("role") == "assistant" and msg.get("content") and not final_response:
- final_response = msg["content"]
- if msg.get("role") == "assistant" and msg.get("tool_calls"):
- for tc in msg["tool_calls"]:
- fn = tc.get("function", {}) if isinstance(tc, dict) else {}
- name = fn.get("name", "")
- if name:
- tools_used.append(name)
-
- # Score using LLM judge, heuristic, or ToolContext verification
- correctness = await self._llm_judge(item, final_response)
- return correctness
-```
-
-`ctx` (ToolContext) gives you terminal/file access to the agent's sandbox for verification:
-```python
-# Run tests in the agent's sandbox
-result = ctx.terminal("pytest /workspace/test.py")
-return 1.0 if result["exit_code"] == 0 else 0.0
-```
-
-### 5. `evaluate()` — Periodic evaluation with full agent loop
-
-**MUST use the full agent loop with tools**, not single-turn chat_completion.
-The whole point of hermes-agent environments is agentic evaluation:
-
-```python
-async def evaluate(self, *args, **kwargs) -> None:
- import time, uuid
- from environments.agent_loop import HermesAgentLoop
- from environments.tool_context import ToolContext
-
- start_time = time.time()
- tools, valid_names = self._resolve_tools_for_group()
- samples = []
-
- for item in self._eval_items[:self.config.eval_size]:
- task_id = str(uuid.uuid4())
- messages = []
- if self.config.system_prompt:
- messages.append({"role": "system", "content": self.config.system_prompt})
- messages.append({"role": "user", "content": self.format_prompt(item)})
-
- agent = HermesAgentLoop(
- server=self.server,
- tool_schemas=tools,
- valid_tool_names=valid_names,
- max_turns=self.config.max_agent_turns,
- task_id=task_id,
- temperature=0.0, # Deterministic for eval
- max_tokens=self.config.max_token_length,
- extra_body=self.config.extra_body,
- )
- result = await agent.run(messages)
-
- ctx = ToolContext(task_id)
- try:
- reward = await self.compute_reward(item, result, ctx)
- finally:
- ctx.cleanup()
-
- samples.append({"prompt": ..., "response": ..., "reward": reward})
-
- eval_metrics = {"eval/mean_reward": ...}
- await self.evaluate_log(metrics=eval_metrics, samples=samples,
- start_time=start_time, end_time=time.time())
-```
-
-### 6. `wandb_log()` — Custom metrics logging
-
-Always call `super().wandb_log()` at the end:
-
-```python
-async def wandb_log(self, wandb_metrics=None):
- if wandb_metrics is None:
- wandb_metrics = {}
- if self._reward_buffer:
- n = len(self._reward_buffer)
- wandb_metrics["train/mean_reward"] = sum(self._reward_buffer) / n
- self._reward_buffer.clear()
- await super().wandb_log(wandb_metrics) # MUST call super
-```
-
-**Pitfall**: `compute_reward` appends to metric buffers. During eval, this pollutes training metrics. Roll back buffer entries added during eval.
-
-## Config Class
-
-Always create a custom config subclass with Pydantic Field descriptors. Key inherited fields you can tune: `enabled_toolsets`, `max_agent_turns`, `agent_temperature`, `system_prompt`, `terminal_backend`, `group_size`, `steps_per_eval`, `total_steps`.
-
-## config_init() — Default Configuration
-
-Classmethod returning `(YourEnvConfig, [APIServerConfig(...)])`. Set server_type to "openai" for OpenRouter/external APIs. Load API key from environment variable.
-
-## Three CLI Modes
-
-```bash
-# SERVE — Full training loop (connects to Atropos API server)
-python environments/my_env.py serve --openai.base_url http://localhost:8000/v1
-
-# PROCESS — Offline data generation (saves JSONL)
-python environments/my_env.py process --env.total_steps 10 --env.group_size 1 \
- --env.use_wandb false --env.data_path_to_save_groups output.jsonl \
- --openai.base_url "" \
- --openai.model_name "" \
- --openai.server_type --openai.health_check false
-
-# EVALUATE — Standalone eval (runs setup + evaluate only)
-python environments/my_env.py evaluate --env.eval_size 20 \
- --env.data_dir_to_save_evals /tmp/eval_results \
- --openai.base_url "" \
- --openai.model_name "" \
- --openai.server_type --openai.health_check false
-```
-
-Config priority: CLI args > YAML file > config_init() defaults.
-
-## Common Pitfalls
-
-1. **AgentResult has .messages, not .final_response** — Extract the final response by iterating reversed(result.messages) looking for the last assistant message with content.
-
-2. **evaluate() must use HermesAgentLoop, not chat_completion** — Single-turn chat_completion has no tools. The whole point of hermes-agent benchmarks is agentic evaluation with tool use.
-
-3. **Don't call _llm_judge twice** — If compute_reward already calls it, extract the score from the buffer instead of calling judge separately in evaluate().
-
-4. **Eval pollutes training buffers** — compute_reward appends to metric buffers. During eval, roll back buffer entries to keep training metrics clean.
-
-5. **Always set health_check=false for OpenRouter** — OpenRouter has no /health endpoint.
-
-6. **Set data_dir_to_save_evals in evaluate mode** — Without it, results aren't saved.
-
-7. **default_toolsets class variable vs enabled_toolsets config** — The class variable is a hint; the config field is what actually controls tool resolution.
-
-8. **Tool call parsing in messages** — Tool calls are dicts with `{"function": {"name": ..., "arguments": ...}}`. Always check `isinstance(tc, dict)`.
-
-9. **ToolContext.cleanup()** — Always call in a finally block to release sandbox resources.
-
-10. **server_type must be "openai" for external APIs** — Without it, Atropos assumes a local VLLM server.
-
-11. **Always ask the user for their inference setup** — Never hardcode or assume a specific provider/model. See the "Inference Setup" section above.
-
-## Reward Function Patterns
-
-### LLM Judge (for open-ended tasks)
-Use `self.server.chat_completion()` with a scoring prompt. Parse JSON response for score float. Always include a heuristic fallback (keyword overlap) for when the judge call fails.
-
-### Binary Verification (for code/terminal tasks)
-Use `ctx.terminal("pytest test.py -q")` to run tests in the agent's sandbox. Return 1.0 for pass, 0.0 for fail.
-
-### Multi-Signal (combine multiple indicators)
-Weight correctness (0.6) + tool usage (0.2) + efficiency (0.2) + optional bonuses. Clamp to [0, 1].
-
-## Testing Your Environment
-
-1. **Import test**: `python -c "from environments.my_env import MyEnv; print('OK')"`
-2. **Ask the user for inference setup** (see "Inference Setup" section above)
-3. **Process mode** (1 item): Verify JSONL output has valid tokens, masks, scores
-4. **Evaluate mode**: Verify full agent loop runs with tools, metrics logged correctly
-5. **Check reward range**: Scores should be in [0, 1], not all identical
-
-## Minimum Implementation Checklist
-
-```python
-class MyEnv(HermesAgentBaseEnv):
- name = "my-env"
- env_config_cls = MyEnvConfig
-
- @classmethod
- def config_init(cls): ... # Default server + env config
- async def setup(self): ... # Load dataset + train/eval split
- async def get_next_item(self): ... # Cycle through training items
- def format_prompt(self, item): ... # Item → user message string
- async def compute_reward(self, item, result, ctx): ... # Score rollout
- async def evaluate(self, *args, **kwargs): ... # Full agent loop eval
- async def wandb_log(self, metrics=None): ... # Custom metrics + super()
-
-if __name__ == "__main__":
- MyEnv.cli()
-```
diff --git a/optional-skills/mlops/hermes-atropos-environments/references/agentresult-fields.md b/optional-skills/mlops/hermes-atropos-environments/references/agentresult-fields.md
deleted file mode 100644
index bc6d6050581..00000000000
--- a/optional-skills/mlops/hermes-atropos-environments/references/agentresult-fields.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# AgentResult Fields Reference
-
-`AgentResult` is defined in `environments/agent_loop.py` as a dataclass.
-
-## Fields
-
-| Field | Type | Description |
-|-------|------|-------------|
-| `messages` | `List[Dict[str, Any]]` | Full conversation history in OpenAI message format |
-| `managed_state` | `Optional[Dict]` | ManagedServer.get_state() if Phase 2, else None |
-| `turns_used` | `int` | Number of LLM calls made during the loop |
-| `finished_naturally` | `bool` | True if model stopped calling tools on its own |
-| `reasoning_per_turn` | `List[Optional[str]]` | Extracted reasoning content per turn |
-| `tool_errors` | `List[ToolError]` | Tool errors encountered during the loop |
-
-## ToolError Fields
-
-| Field | Type | Description |
-|-------|------|-------------|
-| `turn` | `int` | Which turn the error occurred |
-| `tool_name` | `str` | Name of the tool that failed |
-| `arguments` | `str` | Arguments passed to the tool |
-| `error` | `str` | Error message |
-| `tool_result` | `str` | The result returned to the model |
-
-## Extracting Data from Messages
-
-Messages follow OpenAI format. Common patterns:
-
-```python
-# Get final assistant response
-for msg in reversed(result.messages):
- if msg.get("role") == "assistant" and msg.get("content"):
- final_response = msg["content"]
- break
-
-# Get all tool names used
-tools = []
-for msg in result.messages:
- if msg.get("role") == "assistant" and msg.get("tool_calls"):
- for tc in msg["tool_calls"]:
- fn = tc.get("function", {}) if isinstance(tc, dict) else {}
- tools.append(fn.get("name", ""))
-
-# Get tool results
-for msg in result.messages:
- if msg.get("role") == "tool":
- tool_output = msg.get("content", "")
- call_id = msg.get("tool_call_id", "")
-```
-
-## Fields that DO NOT EXIST
-
-These are common mistakes — AgentResult does NOT have:
-- `final_response` — extract from messages
-- `tool_calls` — extract from messages
-- `tools_used` — extract from messages
-- `output` — extract from messages
-- `response` — extract from messages
diff --git a/optional-skills/mlops/hermes-atropos-environments/references/atropos-base-env.md b/optional-skills/mlops/hermes-atropos-environments/references/atropos-base-env.md
deleted file mode 100644
index e76895905e1..00000000000
--- a/optional-skills/mlops/hermes-atropos-environments/references/atropos-base-env.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# Atropos BaseEnv Reference
-
-Source: `atroposlib/envs/base.py` (~2124 lines)
-
-## Abstract Methods (MUST implement)
-
-| Method | Signature | Description |
-|--------|-----------|-------------|
-| `get_next_item()` | `async def get_next_item(self) -> Item` | Return next item for trajectory. Return None to pause. |
-| `evaluate()` | `async def evaluate(self, *args, **kwargs)` | Called every steps_per_eval steps. |
-| `setup()` | `async def setup(self)` | Called once at start. Load datasets, init models. |
-| `collect_trajectory()` | `async def collect_trajectory(self, item) -> Tuple[Optional[ScoredDataItem], List[Item]]` | Single rollout. Or override collect_trajectories instead. |
-
-## Overridable Methods
-
-| Method | Default Behavior | Override When |
-|--------|-----------------|---------------|
-| `collect_trajectories()` | Runs collect_trajectory group_size times in parallel | Batch generation, MCTS, coupled rollouts |
-| `wandb_log()` | Logs completion lengths, rollout table, perf stats | Add custom metrics (always call super) |
-| `config_init()` | Returns (env_config_cls(), ServerBaseline()) | Custom defaults + server configs |
-| `postprocess_histories()` | Passthrough | Final processing before sending to trainer |
-| `save_checkpoint()` | Saves JSON to checkpoint_dir | Custom serialization |
-| `cleanup()` | No-op | Release resources after each rollout |
-
-## ScoredDataGroup Structure
-
-```python
-ScoredDataGroup = TypedDict with:
- tokens: List[List[int]] # Token IDs per rollout
- masks: List[List[int]] # -100=prompt, token_id=completion
- scores: List[float] # Score per rollout
- advantages: Optional[...] # Per-token advantages
- ref_logprobs: Optional[...] # Reference model logprobs
- messages: Optional[...] # OpenAI-format messages
- inference_logprobs: Optional[...] # Inference logprobs
-```
-
-## BaseEnvConfig Key Fields
-
-| Field | Default | Description |
-|-------|---------|-------------|
-| `group_size` | 4 | Responses grouped for scoring |
-| `steps_per_eval` | 100 | Steps between evaluations |
-| `max_token_length` | 2048 | Max token length for generations |
-| `total_steps` | 1000 | Total training steps |
-| `use_wandb` | True | Enable wandb logging |
-| `tokenizer_name` | DeepHermes-3 | Tokenizer for token encoding |
-| `ensure_scores_are_not_same` | True | Skip groups with identical scores |
-| `worker_timeout` | 600 | Task timeout seconds |
-
-## Data Flow
-
-```
-env_manager() → add_train_workers() → handle_env()
- → collect_trajectories() → postprocess_histories()
- → handle_send_to_api() → training server
-```
-
-## Atropos Environment Statistics (82 environments analyzed)
-
-- 95% implement setup, collect_trajectories, evaluate, get_next_item
-- 76% override wandb_log
-- 54% have custom config class
-- Most use collect_trajectories (plural), not collect_trajectory (singular)
-- Common reward patterns: LLM-judge (~40), regex-extract (~35), code-exec (~12)
diff --git a/optional-skills/mlops/hermes-atropos-environments/references/usage-patterns.md b/optional-skills/mlops/hermes-atropos-environments/references/usage-patterns.md
deleted file mode 100644
index 5d4b3c1e820..00000000000
--- a/optional-skills/mlops/hermes-atropos-environments/references/usage-patterns.md
+++ /dev/null
@@ -1,199 +0,0 @@
-# Usage Patterns — Testing Environments and Evaluating Models
-
-## Pattern 1: Test Your Environment Works (process mode)
-
-Use `process` mode to verify your environment runs end-to-end before
-committing. This generates trajectories without needing an Atropos
-training server.
-
-**Before running:** Ask the user for their inference setup (see SKILL.md "Inference Setup" section). Replace ``, ``, and `` below with their chosen values.
-
-### Step 1: Run 1 trajectory
-
-```bash
-cd ~/.hermes/hermes-agent
-source venv/bin/activate
-
-python environments/your_env.py process \
- --env.total_steps 1 \
- --env.group_size 1 \
- --env.use_wandb false \
- --env.data_path_to_save_groups /tmp/test_output.jsonl \
- --openai.base_url "" \
- --openai.model_name "" \
- --openai.server_type \
- --openai.health_check false
-```
-
-### Step 2: Verify the output
-
-```python
-import json
-for line in open("/tmp/test_output.jsonl"):
- data = json.loads(line)
- print(f"Scores: {data.get('scores', [])}")
- print(f"Token sequences: {len(data.get('tokens', []))}")
- # Check messages include tool calls
- for msg_list in data.get("messages", []):
- roles = [m.get("role") for m in msg_list]
- print(f"Roles: {roles}")
- for m in reversed(msg_list):
- if m.get("role") == "assistant" and m.get("content"):
- print(f"Response: {m['content'][:200]}...")
- break
-```
-
-### What to check:
-- **Scores are not all 0.0** — if so, compute_reward is broken
-- **Scores are in [0, 1]** — not negative, not >1
-- **Messages include "tool" role entries** — agent used tools
-- **Token sequences are non-empty**
-- **An HTML visualization is generated** next to the .jsonl
-
-### Common failures:
-- `'AgentResult' object has no attribute 'X'` — accessing a field that doesn't exist. See agentresult-fields.md.
-- Score always 0.0 — reward function erroring silently
-- Score always 1.0 — verification too lenient or not running
-
-
-## Pattern 2: Evaluate a Model (evaluate mode)
-
-Use `evaluate` mode to benchmark a model on your environment's eval
-split. This runs the full agent loop with tools for each eval item.
-
-### Step 1: Run evaluation
-
-```bash
-python environments/your_env.py evaluate \
- --env.eval_size 20 \
- --env.use_wandb false \
- --env.data_dir_to_save_evals /tmp/eval_results \
- --openai.base_url "" \
- --openai.model_name "" \
- --openai.server_type \
- --openai.health_check false
-```
-
-### Step 2: Read results
-
-Stdout shows a lighteval-compatible table:
-
-```
-Evaluation Results: your-env_eval
-|Metric | Value|
-|mean correctness| 0.850 |
-|mean reward | 0.920 |
-|mean tool calls | 4.300 |
-|n items | 20 |
-Evaluation completed in 367 seconds
-```
-
-JSON results saved to the eval directory:
-
-```python
-import json
-data = json.load(open("/tmp/eval_results/metrics.json"))
-for metric, value in data["results"]["all"].items():
- print(f"{metric}: {value}")
-```
-
-### Step 3: Compare models
-
-Run evaluate with different models and compare the metrics.json files.
-
-### What to check:
-- **"data_dir_to_save_evals is not set"** — you forgot the flag, results won't be saved
-- **Tool usage rate = 0** — evaluate() is using chat_completion instead of HermesAgentLoop
-- **All scores identical** — judge failing, falling back to heuristic
-- **Very slow** — each item runs a full agent loop (~30-90s). Use `--env.eval_size 5` for quick checks.
-
-
-## Pattern 3: Generate Training Data (process mode, larger scale)
-
-Generate trajectory data for offline training or analysis:
-
-```bash
-python environments/your_env.py process \
- --env.total_steps 50 \
- --env.group_size 4 \
- --env.use_wandb false \
- --env.data_path_to_save_groups data/trajectories.jsonl \
- --openai.base_url "" \
- --openai.model_name "" \
- --openai.server_type \
- --openai.health_check false
-```
-
-### Analyze the distribution:
-
-```python
-import json
-scores = []
-for line in open("data/trajectories.jsonl"):
- data = json.loads(line)
- scores.extend(data.get("scores", []))
-
-print(f"Total: {len(scores)}, Mean: {sum(scores)/len(scores):.3f}")
-for bucket in [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]:
- count = sum(1 for s in scores if abs(s - bucket) < 0.1)
- print(f" {bucket:.1f}: {'█' * count} ({count})")
-```
-
-### What to check:
-- **Score distribution has variance** — RL needs score variance. All-same scores are useless.
-
-
-## Pattern 4: Full RL Training (serve mode)
-
-For actual RL training with Atropos:
-
-```bash
-# Terminal 1: Start Atropos API server
-run-api
-
-# Terminal 2: Start your environment
-python environments/your_env.py serve \
- --config environments/your_env/default.yaml
-```
-
-For Phase 2 with VLLM:
-
-```bash
-# Terminal 1: VLLM server
-python -m vllm.entrypoints.openai.api_server --model your-model --port 8000
-
-# Terminal 2: Atropos API
-run-api
-
-# Terminal 3: Environment
-python environments/your_env.py serve \
- --openai.base_url http://localhost:8000/v1 \
- --openai.model_name your-model \
- --openai.server_type vllm
-```
-
-
-## Pattern 5: Quick Smoke Test
-
-Verify imports and config before spending money on API calls:
-
-```python
-from environments.your_env import YourEnv
-print(f"Name: {YourEnv.name}")
-cfg, servers = YourEnv.config_init()
-print(f"Toolsets: {cfg.enabled_toolsets}")
-print(f"Server: {servers[0].model_name}")
-print("All imports OK")
-```
-
-
-## Timing Expectations
-
-| Mode | Items | Time per item | Total |
-|------|-------|--------------|-------|
-| process (1 item) | 1 | 30-90s | ~1 min |
-| evaluate (5 items) | 5 | 30-90s | ~5 min |
-| evaluate (20 items) | 20 | 30-90s | ~15-30 min |
-| process (50 items) | 50 | 30-90s | ~30-75 min |
-
-Times are for cloud APIs with Claude Sonnet-class models. Local models may be faster or slower depending on hardware.
diff --git a/pyproject.toml b/pyproject.toml
index a880bcb05bf..982dc01be17 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -166,14 +166,6 @@ youtube = [
]
# `hermes dashboard` (localhost SPA + API). Not in core to keep the default install lean.
web = ["fastapi==0.133.1", "uvicorn[standard]==0.41.0"]
-rl = [
- "atroposlib @ git+https://github.com/NousResearch/atropos.git@c20c85256e5a45ad31edf8b7276e9c5ee1995a30",
- "tinker @ git+https://github.com/thinking-machines-lab/tinker.git@30517b667f18a3dfb7ef33fb56cf686d5820ba2b",
- "fastapi==0.133.1",
- "uvicorn[standard]==0.41.0",
- "wandb==0.25.1",
-]
-yc-bench = ["yc-bench @ git+https://github.com/collinear-ai/yc-bench.git@bfb0c88062450f46341bd9a5298903fc2e952a5c ; python_version >= '3.12'"]
all = [
# Policy (2026-05-12): `[all]` includes only extras that genuinely
# CAN'T be lazy-installed via `tools/lazy_deps.py` — i.e. things every
@@ -215,7 +207,7 @@ hermes-agent = "run_agent:main"
hermes-acp = "acp_adapter.entry:main"
[tool.setuptools]
-py-modules = ["run_agent", "model_tools", "toolsets", "batch_runner", "trajectory_compressor", "toolset_distributions", "cli", "hermes_bootstrap", "hermes_constants", "hermes_state", "hermes_time", "hermes_logging", "rl_cli", "utils"]
+py-modules = ["run_agent", "model_tools", "toolsets", "batch_runner", "trajectory_compressor", "toolset_distributions", "cli", "hermes_bootstrap", "hermes_constants", "hermes_state", "hermes_time", "hermes_logging", "utils"]
[tool.setuptools.package-data]
hermes_cli = ["web_dist/**/*"]
@@ -238,11 +230,7 @@ python-version = "3.13"
unknown-argument = "warn"
redundant-cast = "ignore"
-[tool.ty.src]
-exclude = ["tinker-atropos"]
-
[tool.ruff]
-exclude = ["tinker-atropos"]
preview = true # required for PLW1514 (unspecified-encoding) — preview rule
[tool.ruff.lint]
diff --git a/rl_cli.py b/rl_cli.py
deleted file mode 100644
index e3996a29df6..00000000000
--- a/rl_cli.py
+++ /dev/null
@@ -1,446 +0,0 @@
-#!/usr/bin/env python3
-"""
-RL Training CLI Runner
-
-Dedicated CLI runner for RL training workflows with:
-- Extended timeouts for long-running training
-- RL-focused system prompts
-- Full toolset including RL training tools
-- Special handling for 30-minute check intervals
-
-Usage:
- python rl_cli.py "Train a model on GSM8k for math reasoning"
- python rl_cli.py --interactive
- python rl_cli.py --list-environments
-
-Environment Variables:
- TINKER_API_KEY: API key for Tinker service (required)
- WANDB_API_KEY: API key for WandB metrics (required)
- OPENROUTER_API_KEY: API key for OpenRouter (required for agent)
-"""
-
-import asyncio
-import os
-import sys
-from pathlib import Path
-
-import fire
-import yaml
-
-from hermes_constants import OPENROUTER_BASE_URL, get_hermes_home
-
-# Load .env from ~/.hermes/.env first, then project root as dev fallback.
-# User-managed env files should override stale shell exports on restart.
-_hermes_home = get_hermes_home()
-_project_env = Path(__file__).parent / '.env'
-
-from hermes_cli.env_loader import load_hermes_dotenv
-
-_loaded_env_paths = load_hermes_dotenv(hermes_home=_hermes_home, project_env=_project_env)
-for _env_path in _loaded_env_paths:
- print(f"✅ Loaded environment variables from {_env_path}")
-
-# Set terminal working directory to tinker-atropos submodule
-# This ensures terminal commands run in the right context for RL work
-tinker_atropos_dir = Path(__file__).parent / 'tinker-atropos'
-if tinker_atropos_dir.exists():
- os.environ['TERMINAL_CWD'] = str(tinker_atropos_dir)
- os.environ['HERMES_QUIET'] = '1' # Disable temp subdirectory creation
- print(f"📂 Terminal working directory: {tinker_atropos_dir}")
-else:
- # Fall back to hermes-agent directory if submodule not found
- os.environ['TERMINAL_CWD'] = str(Path(__file__).parent)
- os.environ['HERMES_QUIET'] = '1'
- print(f"⚠️ tinker-atropos submodule not found, using: {Path(__file__).parent}")
-
-# Import agent and tools
-from run_agent import AIAgent
-from tools.rl_training_tool import get_missing_keys
-
-
-# ============================================================================
-# Config Loading
-# ============================================================================
-
-DEFAULT_MODEL = "anthropic/claude-opus-4.5"
-DEFAULT_BASE_URL = OPENROUTER_BASE_URL
-
-
-def load_hermes_config() -> dict:
- """
- Load configuration from ~/.hermes/config.yaml.
-
- Returns:
- dict: Configuration with model, base_url, etc.
- """
- config_path = _hermes_home / 'config.yaml'
-
- config = {
- "model": DEFAULT_MODEL,
- "base_url": DEFAULT_BASE_URL,
- }
-
- if config_path.exists():
- try:
- with open(config_path, "r", encoding='utf-8') as f:
- file_config = yaml.safe_load(f) or {}
-
- # Get model from config
- if "model" in file_config:
- if isinstance(file_config["model"], str):
- config["model"] = file_config["model"]
- elif isinstance(file_config["model"], dict):
- config["model"] = file_config["model"].get("default", DEFAULT_MODEL)
-
- # Get base_url if specified
- if "base_url" in file_config:
- config["base_url"] = file_config["base_url"]
-
- except Exception as e:
- print(f"⚠️ Warning: Failed to load config.yaml: {e}")
-
- return config
-
-
-# ============================================================================
-# RL-Specific Configuration
-# ============================================================================
-
-# Extended timeouts for long-running RL operations
-RL_MAX_ITERATIONS = 200 # Allow many more iterations for long workflows
-
-# RL-focused system prompt
-RL_SYSTEM_PROMPT = """You are an automated post-training engineer specializing in reinforcement learning for language models.
-
-## Your Capabilities
-
-You have access to RL training tools for running reinforcement learning on models through Tinker-Atropos:
-
-1. **DISCOVER**: Use `rl_list_environments` to see available RL environments
-2. **INSPECT**: Read environment files to understand how they work (verifiers, data loading, rewards)
-3. **INSPECT DATA**: Use terminal to explore HuggingFace datasets and understand their format
-4. **CREATE**: Copy existing environments as templates, modify for your needs
-5. **CONFIGURE**: Use `rl_select_environment` and `rl_edit_config` to set up training
-6. **TEST**: Always use `rl_test_inference` before full training to validate your setup
-7. **TRAIN**: Use `rl_start_training` to begin, `rl_check_status` to monitor
-8. **EVALUATE**: Use `rl_get_results` and analyze WandB metrics to assess performance
-
-## Environment Files
-
-Environment files are located in: `tinker-atropos/tinker_atropos/environments/`
-
-Study existing environments to learn patterns. Look for:
-- `load_dataset()` calls - how data is loaded
-- `score_answer()` / `score()` - verification logic
-- `get_next_item()` - prompt formatting
-- `system_prompt` - instruction format
-- `config_init()` - default configuration
-
-## Creating New Environments
-
-To create a new environment:
-1. Read an existing environment file (e.g., gsm8k_tinker.py)
-2. Use terminal to explore the target dataset format
-3. Copy the environment file as a template
-4. Modify the dataset loading, prompt formatting, and verifier logic
-5. Test with `rl_test_inference` before training
-
-## Important Guidelines
-
-- **Always test before training**: Training runs take hours - verify everything works first
-- **Monitor metrics**: Check WandB for reward/mean and percent_correct
-- **Status check intervals**: Wait at least 30 minutes between status checks
-- **Early stopping**: Stop training early if metrics look bad or stagnant
-- **Iterate quickly**: Start with small total_steps to validate, then scale up
-
-## Available Toolsets
-
-You have access to:
-- **RL tools**: Environment discovery, config management, training, testing
-- **Terminal**: Run commands, inspect files, explore datasets
-- **Web**: Search for information, documentation, papers
-- **File tools**: Read and modify code files
-
-When asked to train a model, follow this workflow:
-1. List available environments
-2. Select and configure the appropriate environment
-3. Test with sample prompts
-4. Start training with conservative settings
-5. Monitor progress and adjust as needed
-"""
-
-# Toolsets to enable for RL workflows
-RL_TOOLSETS = ["terminal", "web", "rl"]
-
-
-# ============================================================================
-# Helper Functions
-# ============================================================================
-
-def check_requirements():
- """Check that all required environment variables and services are available."""
- errors = []
-
- # Check API keys
- if not os.getenv("OPENROUTER_API_KEY"):
- errors.append("OPENROUTER_API_KEY not set - required for agent")
-
- missing_rl_keys = get_missing_keys()
- if missing_rl_keys:
- errors.append(f"Missing RL API keys: {', '.join(missing_rl_keys)}")
-
- if errors:
- print("❌ Missing requirements:")
- for error in errors:
- print(f" - {error}")
- print("\nPlease set these environment variables in your .env file or shell.")
- return False
-
- return True
-
-
-def check_tinker_atropos():
- """Check if tinker-atropos submodule is properly set up."""
- tinker_path = Path(__file__).parent / "tinker-atropos"
-
- if not tinker_path.exists():
- return False, "tinker-atropos submodule not found. Run: git submodule update --init"
-
- envs_path = tinker_path / "tinker_atropos" / "environments"
- if not envs_path.exists():
- return False, f"environments directory not found at {envs_path}"
-
- env_files = list(envs_path.glob("*.py"))
- env_files = [f for f in env_files if not f.name.startswith("_")]
-
- return True, {"path": str(tinker_path), "environments_count": len(env_files)}
-
-
-def list_environments_sync():
- """List available environments (synchronous wrapper)."""
- from tools.rl_training_tool import rl_list_environments
- import json
-
- async def _list():
- result = await rl_list_environments()
- return json.loads(result)
-
- return asyncio.run(_list())
-
-
-# ============================================================================
-# Main CLI
-# ============================================================================
-
-def main(
- task: str = None,
- model: str = None,
- api_key: str = None,
- base_url: str = None,
- max_iterations: int = RL_MAX_ITERATIONS,
- interactive: bool = False,
- list_environments: bool = False,
- check_server: bool = False,
- verbose: bool = False,
- save_trajectories: bool = True,
-):
- """
- RL Training CLI - Dedicated runner for RL training workflows.
-
- Args:
- task: The training task/goal (e.g., "Train a model on GSM8k for math")
- model: Model to use for the agent (reads from ~/.hermes/config.yaml if not provided)
- api_key: OpenRouter API key (uses OPENROUTER_API_KEY env var if not provided)
- base_url: API base URL (reads from config or defaults to OpenRouter)
- max_iterations: Maximum agent iterations (default: 200 for long workflows)
- interactive: Run in interactive mode (multiple conversations)
- list_environments: Just list available RL environments and exit
- check_server: Check if RL API server is running and exit
- verbose: Enable verbose logging
- save_trajectories: Save conversation trajectories (default: True for RL)
-
- Examples:
- # Train on a specific environment
- python rl_cli.py "Train a model on GSM8k math problems"
-
- # Interactive mode
- python rl_cli.py --interactive
-
- # List available environments
- python rl_cli.py --list-environments
-
- # Check server status
- python rl_cli.py --check-server
- """
- # Load config from ~/.hermes/config.yaml
- config = load_hermes_config()
-
- # Use config values if not explicitly provided
- if model is None:
- model = config["model"]
- if base_url is None:
- base_url = config["base_url"]
-
- print("🎯 RL Training Agent")
- print("=" * 60)
-
- # Handle setup check
- if check_server:
- print("\n🔍 Checking tinker-atropos setup...")
- ok, result = check_tinker_atropos()
- if ok:
- print("✅ tinker-atropos submodule found")
- print(f" Path: {result.get('path')}")
- print(f" Environments found: {result.get('environments_count', 0)}")
-
- # Also check API keys
- missing = get_missing_keys()
- if missing:
- print(f"\n⚠️ Missing API keys: {', '.join(missing)}")
- print(" Add them to ~/.hermes/.env")
- else:
- print("✅ API keys configured")
- else:
- print(f"❌ tinker-atropos not set up: {result}")
- print("\nTo set up:")
- print(" git submodule update --init")
- print(" pip install -e ./tinker-atropos")
- return
-
- # Handle environment listing
- if list_environments:
- print("\n📋 Available RL Environments:")
- print("-" * 40)
- try:
- data = list_environments_sync()
- if "error" in data:
- print(f"❌ Error: {data['error']}")
- return
-
- envs = data.get("environments", [])
- if not envs:
- print("No environments found.")
- print("\nMake sure tinker-atropos is set up:")
- print(" git submodule update --init")
- return
-
- for env in envs:
- print(f"\n 📦 {env['name']}")
- print(f" Class: {env['class_name']}")
- print(f" Path: {env['file_path']}")
- if env.get('description'):
- desc = env['description'][:100] + "..." if len(env.get('description', '')) > 100 else env.get('description', '')
- print(f" Description: {desc}")
-
- print(f"\n📊 Total: {len(envs)} environments")
- print("\nUse `rl_select_environment(name)` to select an environment for training.")
- except Exception as e:
- print(f"❌ Error listing environments: {e}")
- print("\nMake sure tinker-atropos is set up:")
- print(" git submodule update --init")
- print(" pip install -e ./tinker-atropos")
- return
-
- # Check requirements
- if not check_requirements():
- sys.exit(1)
-
- # Set default task if none provided
- if not task and not interactive:
- print("\n⚠️ No task provided. Use --interactive for interactive mode or provide a task.")
- print("\nExamples:")
- print(' python rl_cli.py "Train a model on GSM8k math problems"')
- print(' python rl_cli.py "Create an RL environment for code generation"')
- print(' python rl_cli.py --interactive')
- return
-
- # Get API key
- api_key = api_key or os.getenv("OPENROUTER_API_KEY")
- if not api_key:
- print("❌ No API key provided. Set OPENROUTER_API_KEY or pass --api-key")
- sys.exit(1)
-
- print(f"\n🤖 Model: {model}")
- print(f"🔧 Max iterations: {max_iterations}")
- print(f"📁 Toolsets: {', '.join(RL_TOOLSETS)}")
- print("=" * 60)
-
- # Create agent with RL configuration
- agent = AIAgent(
- base_url=base_url,
- api_key=api_key,
- model=model,
- max_iterations=max_iterations,
- enabled_toolsets=RL_TOOLSETS,
- save_trajectories=save_trajectories,
- verbose_logging=verbose,
- quiet_mode=False,
- ephemeral_system_prompt=RL_SYSTEM_PROMPT,
- )
-
- if interactive:
- # Interactive mode - multiple conversations
- print("\n🔄 Interactive RL Training Mode")
- print("Type 'quit' or 'exit' to end the session.")
- print("Type 'status' to check active training runs.")
- print("-" * 40)
-
- while True:
- try:
- user_input = input("\n🎯 RL Task> ").strip()
-
- if not user_input:
- continue
-
- if user_input.lower() in {'quit', 'exit', 'q'}:
- print("\n👋 Goodbye!")
- break
-
- if user_input.lower() == 'status':
- # Quick status check
- from tools.rl_training_tool import rl_list_runs
- import json
- result = asyncio.run(rl_list_runs())
- runs = json.loads(result)
- if isinstance(runs, list) and runs:
- print("\n📊 Active Runs:")
- for run in runs:
- print(f" - {run['run_id']}: {run['environment']} ({run['status']})")
- else:
- print("\nNo active runs.")
- continue
-
- # Run the agent
- print("\n" + "=" * 60)
- agent.run_conversation(user_input)
- print("\n" + "=" * 60)
-
- except KeyboardInterrupt:
- print("\n\n👋 Interrupted. Goodbye!")
- break
- except Exception as e:
- print(f"\n❌ Error: {e}")
- if verbose:
- import traceback
- traceback.print_exc()
- else:
- # Single task mode
- print(f"\n📝 Task: {task}")
- print("-" * 40)
-
- try:
- agent.run_conversation(task)
- print("\n" + "=" * 60)
- print("✅ Task completed")
- except KeyboardInterrupt:
- print("\n\n⚠️ Interrupted by user")
- except Exception as e:
- print(f"\n❌ Error: {e}")
- if verbose:
- import traceback
- traceback.print_exc()
- sys.exit(1)
-
-
-if __name__ == "__main__":
- fire.Fire(main)
diff --git a/scripts/install.ps1 b/scripts/install.ps1
index 36cdf76ec70..2cf81969beb 100644
--- a/scripts/install.ps1
+++ b/scripts/install.ps1
@@ -958,20 +958,6 @@ except Exception:
}
}
- # tinker-atropos (RL training) is optional and OFF by default. Matches the
- # Linux/macOS install.sh behavior. Reasons not to auto-install:
- # - tinker-atropos/pyproject.toml pulls atroposlib + tinker from git+https
- # (NousResearch/atropos + thinking-machines-lab/tinker) which can fail on
- # locked-down networks, flaky DNS, or rate-limited github.com and would
- # previously kill the whole install mid-flight on Windows.
- # - It's an RL training submodule, not part of the default agent surface.
- # Users who don't do RL training never need it.
- # Users who do want it can run the one-liner we print below.
- if (Test-Path "tinker-atropos\pyproject.toml") {
- Write-Info "tinker-atropos submodule found — skipping install (optional, for RL training)"
- Write-Info " To install later: $UvCmd pip install -e `".\tinker-atropos`""
- }
-
Pop-Location
Write-Success "All dependencies installed"
diff --git a/scripts/install.sh b/scripts/install.sh
index cf24912cc51..9c5db6b1c08 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -1051,11 +1051,6 @@ install_deps() {
log_info "Termux note: matrix e2ee and local faster-whisper extras are excluded from .[termux-all] due to upstream Android wheel/toolchain blockers."
log_info "Termux note: browser/WhatsApp tooling is not installed by default; see the Termux guide for optional follow-up steps."
- if [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then
- log_info "tinker-atropos submodule found — skipping install (optional, for RL training)"
- log_info " To install later: $PIP_PYTHON -m pip install -e \"./tinker-atropos\""
- fi
-
log_success "All dependencies installed"
return 0
fi
@@ -1243,13 +1238,6 @@ PY
log_success "Main package installed"
- # tinker-atropos (RL training) is optional — skip by default.
- # To enable RL tools: git submodule update --init tinker-atropos && uv pip install -e "./tinker-atropos"
- if [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then
- log_info "tinker-atropos submodule found — skipping install (optional, for RL training)"
- log_info " To install: $UV_CMD pip install -e \"./tinker-atropos\""
- fi
-
log_success "All dependencies installed"
}
diff --git a/setup-hermes.sh b/setup-hermes.sh
index 2aa773c1c9c..bdb8c1e9653 100755
--- a/setup-hermes.sh
+++ b/setup-hermes.sh
@@ -267,22 +267,6 @@ else
fi
# ============================================================================
-# Submodules (terminal backend + RL training)
-# ============================================================================
-
-echo -e "${CYAN}→${NC} Installing optional submodules..."
-
-# tinker-atropos (RL training backend)
-if is_termux; then
- echo -e "${CYAN}→${NC} Skipping tinker-atropos on Termux (not part of the tested Android path)"
-elif [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then
- $UV_CMD pip install -e "./tinker-atropos" && \
- echo -e "${GREEN}✓${NC} tinker-atropos installed" || \
- echo -e "${YELLOW}⚠${NC} tinker-atropos install failed (RL tools may not work)"
-else
- echo -e "${YELLOW}⚠${NC} tinker-atropos not found (run: git submodule update --init --recursive)"
-fi
-
# ============================================================================
# Optional: ripgrep (for faster file search)
# ============================================================================
diff --git a/tests/conftest.py b/tests/conftest.py
index d9ae0c86ea6..aa2b1b1fbcb 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -101,7 +101,6 @@ _CREDENTIAL_NAMES = frozenset({
"RETAINDB_API_KEY",
"HINDSIGHT_API_KEY",
"HINDSIGHT_LLM_API_KEY",
- "TINKER_API_KEY",
"DAYTONA_API_KEY",
"TWILIO_AUTH_TOKEN",
"TELEGRAM_BOT_TOKEN",
diff --git a/tests/environments/benchmarks/test_terminalbench2_env_security.py b/tests/environments/benchmarks/test_terminalbench2_env_security.py
deleted file mode 100644
index b2610757762..00000000000
--- a/tests/environments/benchmarks/test_terminalbench2_env_security.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""Security tests for Terminal-Bench 2 archive extraction."""
-
-import base64
-import importlib
-import io
-import sys
-import tarfile
-import types
-
-import pytest
-
-
-def _stub_module(name: str, **attrs):
- module = types.ModuleType(name)
- for key, value in attrs.items():
- setattr(module, key, value)
- return module
-
-
-def _load_terminalbench_module(monkeypatch):
- class _EvalHandlingEnum:
- STOP_TRAIN = "stop_train"
-
- class _APIServerConfig:
- def __init__(self, *args, **kwargs):
- self.args = args
- self.kwargs = kwargs
-
- class _AgentResult:
- pass
-
- class _HermesAgentLoop:
- pass
-
- class _HermesAgentBaseEnv:
- pass
-
- class _HermesAgentEnvConfig:
- pass
-
- class _ToolContext:
- pass
-
- stub_modules = {
- "atroposlib": _stub_module("atroposlib"),
- "atroposlib.envs": _stub_module("atroposlib.envs"),
- "atroposlib.envs.base": _stub_module(
- "atroposlib.envs.base",
- EvalHandlingEnum=_EvalHandlingEnum,
- ),
- "atroposlib.envs.server_handling": _stub_module("atroposlib.envs.server_handling"),
- "atroposlib.envs.server_handling.server_manager": _stub_module(
- "atroposlib.envs.server_handling.server_manager",
- APIServerConfig=_APIServerConfig,
- ),
- "environments.agent_loop": _stub_module(
- "environments.agent_loop",
- AgentResult=_AgentResult,
- HermesAgentLoop=_HermesAgentLoop,
- ),
- "environments.hermes_base_env": _stub_module(
- "environments.hermes_base_env",
- HermesAgentBaseEnv=_HermesAgentBaseEnv,
- HermesAgentEnvConfig=_HermesAgentEnvConfig,
- ),
- "environments.tool_context": _stub_module(
- "environments.tool_context",
- ToolContext=_ToolContext,
- ),
- "tools.terminal_tool": _stub_module(
- "tools.terminal_tool",
- register_task_env_overrides=lambda *args, **kwargs: None,
- clear_task_env_overrides=lambda *args, **kwargs: None,
- cleanup_vm=lambda *args, **kwargs: None,
- ),
- }
-
- stub_modules["atroposlib"].envs = stub_modules["atroposlib.envs"]
- stub_modules["atroposlib.envs"].base = stub_modules["atroposlib.envs.base"]
- stub_modules["atroposlib.envs"].server_handling = stub_modules["atroposlib.envs.server_handling"]
- stub_modules["atroposlib.envs.server_handling"].server_manager = stub_modules[
- "atroposlib.envs.server_handling.server_manager"
- ]
-
- for name, module in stub_modules.items():
- monkeypatch.setitem(sys.modules, name, module)
-
- module_name = "environments.benchmarks.terminalbench_2.terminalbench2_env"
- sys.modules.pop(module_name, None)
- return importlib.import_module(module_name)
-
-
-def _build_tar_b64(entries):
- buf = io.BytesIO()
- with tarfile.open(fileobj=buf, mode="w:gz") as tar:
- for entry in entries:
- kind = entry["kind"]
- info = tarfile.TarInfo(entry["name"])
-
- if kind == "dir":
- info.type = tarfile.DIRTYPE
- tar.addfile(info)
- continue
-
- if kind == "file":
- data = entry["data"].encode("utf-8")
- info.size = len(data)
- tar.addfile(info, io.BytesIO(data))
- continue
-
- if kind == "symlink":
- info.type = tarfile.SYMTYPE
- info.linkname = entry["target"]
- tar.addfile(info)
- continue
-
- raise ValueError(f"Unknown tar entry kind: {kind}")
-
- return base64.b64encode(buf.getvalue()).decode("ascii")
-
-
-def test_extract_base64_tar_allows_safe_files(tmp_path, monkeypatch):
- module = _load_terminalbench_module(monkeypatch)
- archive = _build_tar_b64(
- [
- {"kind": "dir", "name": "nested"},
- {"kind": "file", "name": "nested/hello.txt", "data": "hello"},
- ]
- )
-
- target = tmp_path / "extract"
- module._extract_base64_tar(archive, target)
-
- assert (target / "nested" / "hello.txt").read_text(encoding="utf-8") == "hello"
-
-
-def test_extract_base64_tar_rejects_path_traversal(tmp_path, monkeypatch):
- module = _load_terminalbench_module(monkeypatch)
- archive = _build_tar_b64(
- [
- {"kind": "file", "name": "../escape.txt", "data": "owned"},
- ]
- )
-
- target = tmp_path / "extract"
- with pytest.raises(ValueError, match="Unsafe archive member path"):
- module._extract_base64_tar(archive, target)
-
- assert not (tmp_path / "escape.txt").exists()
-
-
-def test_extract_base64_tar_rejects_symlinks(tmp_path, monkeypatch):
- module = _load_terminalbench_module(monkeypatch)
- archive = _build_tar_b64(
- [
- {"kind": "symlink", "name": "link", "target": "../../escape.txt"},
- ]
- )
-
- target = tmp_path / "extract"
- with pytest.raises(ValueError, match="Unsupported archive member type"):
- module._extract_base64_tar(archive, target)
-
- assert not (target / "link").exists()
diff --git a/tests/hermes_cli/test_set_config_value.py b/tests/hermes_cli/test_set_config_value.py
index 617a915e322..39faa83cf58 100644
--- a/tests/hermes_cli/test_set_config_value.py
+++ b/tests/hermes_cli/test_set_config_value.py
@@ -39,8 +39,6 @@ class TestExplicitAllowlist:
"OPENROUTER_API_KEY",
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
- "WANDB_API_KEY",
- "TINKER_API_KEY",
"HONCHO_API_KEY",
"FIRECRAWL_API_KEY",
"BROWSERBASE_API_KEY",
diff --git a/tests/hermes_cli/test_setup_hermes_script.py b/tests/hermes_cli/test_setup_hermes_script.py
index 7978e660a89..a4eb5ccb7d0 100644
--- a/tests/hermes_cli/test_setup_hermes_script.py
+++ b/tests/hermes_cli/test_setup_hermes_script.py
@@ -18,4 +18,3 @@ def test_setup_hermes_script_has_termux_path():
assert ".[termux]" in content
assert "constraints-termux.txt" in content
assert "$PREFIX/bin" in content
- assert "Skipping tinker-atropos on Termux" in content
diff --git a/tests/run_agent/test_agent_loop.py b/tests/run_agent/test_agent_loop.py
deleted file mode 100644
index bd9e41b91e2..00000000000
--- a/tests/run_agent/test_agent_loop.py
+++ /dev/null
@@ -1,505 +0,0 @@
-"""
-Tests for environments/agent_loop.py — HermesAgentLoop.
-
-Tests the multi-turn agent engine using mocked servers, without needing
-real API keys or running servers.
-"""
-
-import asyncio
-import json
-import sys
-from dataclasses import dataclass
-from pathlib import Path
-from typing import Any, Dict, List, Optional
-from unittest.mock import MagicMock
-
-import pytest
-
-# Ensure repo root is importable
-sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
-
-try:
- from environments.agent_loop import (
- AgentResult,
- HermesAgentLoop,
- ToolError,
- _extract_reasoning_from_message,
- resize_tool_pool,
- )
-except ImportError:
- pytest.skip("atroposlib not installed", allow_module_level=True)
-
-
-# ─── Mock server infrastructure ─────────────────────────────────────────
-
-
-@dataclass
-class MockFunction:
- name: str
- arguments: str
-
-
-@dataclass
-class MockToolCall:
- id: str
- function: MockFunction
- type: str = "function"
-
-
-@dataclass
-class MockMessage:
- content: Optional[str]
- role: str = "assistant"
- tool_calls: Optional[List[MockToolCall]] = None
- reasoning_content: Optional[str] = None
- reasoning: Optional[str] = None
- reasoning_details: Optional[list] = None
-
-
-@dataclass
-class MockChoice:
- message: MockMessage
- finish_reason: str = "stop"
- index: int = 0
-
-
-@dataclass
-class MockChatCompletion:
- choices: List[MockChoice]
- id: str = "chatcmpl-mock"
- model: str = "mock-model"
-
-
-class MockServer:
- """
- Mock server that returns pre-configured responses in sequence.
- Mimics the chat_completion() interface.
- """
-
- def __init__(self, responses: List[MockChatCompletion]):
- self.responses = responses
- self.call_count = 0
- self.call_history: List[Dict[str, Any]] = []
-
- async def chat_completion(self, **kwargs) -> MockChatCompletion:
- self.call_history.append(kwargs)
- if self.call_count >= len(self.responses):
- # Return a simple text response if we run out
- return MockChatCompletion(
- choices=[MockChoice(message=MockMessage(content="Done."))]
- )
- resp = self.responses[self.call_count]
- self.call_count += 1
- return resp
-
-
-def make_text_response(content: str) -> MockChatCompletion:
- """Create a simple text-only response (no tool calls)."""
- return MockChatCompletion(
- choices=[MockChoice(message=MockMessage(content=content))]
- )
-
-
-def make_tool_response(
- tool_name: str,
- arguments: dict,
- content: str = "",
- tool_call_id: str = "call_001",
-) -> MockChatCompletion:
- """Create a response with a single tool call."""
- return MockChatCompletion(
- choices=[
- MockChoice(
- message=MockMessage(
- content=content,
- tool_calls=[
- MockToolCall(
- id=tool_call_id,
- function=MockFunction(
- name=tool_name,
- arguments=json.dumps(arguments),
- ),
- )
- ],
- ),
- finish_reason="tool_calls",
- )
- ]
- )
-
-
-# ─── Tests ───────────────────────────────────────────────────────────────
-
-
-class TestAgentResult:
- def test_defaults(self):
- result = AgentResult(messages=[])
- assert result.messages == []
- assert result.managed_state is None
- assert result.turns_used == 0
- assert result.finished_naturally is False
- assert result.reasoning_per_turn == []
- assert result.tool_errors == []
-
-
-class TestExtractReasoning:
- def test_reasoning_content_field(self):
- msg = MockMessage(content="hello", reasoning_content="I think...")
- assert _extract_reasoning_from_message(msg) == "I think..."
-
- def test_reasoning_field(self):
- msg = MockMessage(content="hello", reasoning="Let me consider...")
- assert _extract_reasoning_from_message(msg) == "Let me consider..."
-
- def test_reasoning_details(self):
- detail = MagicMock()
- detail.text = "Detail reasoning"
- msg = MockMessage(content="hello", reasoning_details=[detail])
- assert _extract_reasoning_from_message(msg) == "Detail reasoning"
-
- def test_reasoning_details_dict_format(self):
- msg = MockMessage(
- content="hello",
- reasoning_details=[{"text": "Dict reasoning"}],
- )
- assert _extract_reasoning_from_message(msg) == "Dict reasoning"
-
- def test_no_reasoning(self):
- msg = MockMessage(content="hello")
- assert _extract_reasoning_from_message(msg) is None
-
- def test_reasoning_content_takes_priority(self):
- msg = MockMessage(
- content="hello",
- reasoning_content="First",
- reasoning="Second",
- )
- assert _extract_reasoning_from_message(msg) == "First"
-
-
-class TestHermesAgentLoop:
- """Test the agent loop with mock servers."""
-
- @pytest.fixture
- def basic_tools(self):
- """Minimal tool schema for testing."""
- return [
- {
- "type": "function",
- "function": {
- "name": "terminal",
- "description": "Run a command",
- "parameters": {
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "Command to run",
- }
- },
- "required": ["command"],
- },
- },
- },
- {
- "type": "function",
- "function": {
- "name": "read_file",
- "description": "Read a file",
- "parameters": {
- "type": "object",
- "properties": {
- "path": {"type": "string"},
- },
- "required": ["path"],
- },
- },
- },
- ]
-
- @pytest.fixture
- def valid_names(self):
- return {"terminal", "read_file", "todo"}
-
- @pytest.mark.asyncio
- async def test_simple_text_response(self, basic_tools, valid_names):
- """Model responds with text only, no tool calls."""
- server = MockServer([make_text_response("Hello! How can I help?")])
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=basic_tools,
- valid_tool_names=valid_names,
- max_turns=10,
- )
- messages = [{"role": "user", "content": "Hi"}]
- result = await agent.run(messages)
-
- assert result.finished_naturally is True
- assert result.turns_used == 1
- assert len(result.messages) >= 2 # user + assistant
- assert result.messages[-1]["role"] == "assistant"
- assert result.messages[-1]["content"] == "Hello! How can I help?"
-
- @pytest.mark.asyncio
- async def test_tool_call_then_text(self, basic_tools, valid_names):
- """Model calls a tool, then responds with text."""
- server = MockServer([
- make_tool_response("todo", {"todos": [{"id": "1", "content": "test", "status": "pending"}]}),
- make_text_response("I created a todo for you."),
- ])
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=basic_tools,
- valid_tool_names=valid_names,
- max_turns=10,
- )
- messages = [{"role": "user", "content": "Create a todo"}]
- result = await agent.run(messages)
-
- assert result.finished_naturally is True
- assert result.turns_used == 2
- # Should have: user, assistant (tool_call), tool (result), assistant (text)
- roles = [m["role"] for m in result.messages]
- assert roles == ["user", "assistant", "tool", "assistant"]
-
- @pytest.mark.asyncio
- async def test_max_turns_reached(self, basic_tools, valid_names):
- """Model keeps calling tools until max_turns is hit."""
- # Create responses that always call a tool
- responses = [
- make_tool_response("todo", {"todos": [{"id": str(i), "content": f"task {i}", "status": "pending"}]}, tool_call_id=f"call_{i}")
- for i in range(10)
- ]
- server = MockServer(responses)
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=basic_tools,
- valid_tool_names=valid_names,
- max_turns=3,
- )
- messages = [{"role": "user", "content": "Keep going"}]
- result = await agent.run(messages)
-
- assert result.finished_naturally is False
- assert result.turns_used == 3
-
- @pytest.mark.asyncio
- async def test_unknown_tool_name(self, basic_tools, valid_names):
- """Model calls a tool not in valid_tool_names."""
- server = MockServer([
- make_tool_response("nonexistent_tool", {"arg": "val"}),
- make_text_response("OK, that didn't work."),
- ])
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=basic_tools,
- valid_tool_names=valid_names,
- max_turns=10,
- )
- messages = [{"role": "user", "content": "Call something weird"}]
- result = await agent.run(messages)
-
- # Should record a tool error
- assert len(result.tool_errors) >= 1
- assert result.tool_errors[0].tool_name == "nonexistent_tool"
-
- @pytest.mark.asyncio
- async def test_empty_response(self, basic_tools, valid_names):
- """Server returns empty response."""
- server = MockServer([MockChatCompletion(choices=[])])
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=basic_tools,
- valid_tool_names=valid_names,
- max_turns=10,
- )
- messages = [{"role": "user", "content": "Hi"}]
- result = await agent.run(messages)
-
- assert result.finished_naturally is False
- assert result.turns_used == 1
-
- @pytest.mark.asyncio
- async def test_api_error_handling(self, basic_tools, valid_names):
- """Server raises an exception."""
-
- class FailingServer:
- async def chat_completion(self, **kwargs):
- raise ConnectionError("Server unreachable")
-
- agent = HermesAgentLoop(
- server=FailingServer(),
- tool_schemas=basic_tools,
- valid_tool_names=valid_names,
- max_turns=10,
- )
- messages = [{"role": "user", "content": "Hi"}]
- result = await agent.run(messages)
-
- assert result.finished_naturally is False
- assert result.turns_used == 1
-
- @pytest.mark.asyncio
- async def test_tools_passed_to_server(self, basic_tools, valid_names):
- """Verify tools are passed in the chat_completion kwargs."""
- server = MockServer([make_text_response("OK")])
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=basic_tools,
- valid_tool_names=valid_names,
- max_turns=10,
- )
- messages = [{"role": "user", "content": "Hi"}]
- await agent.run(messages)
-
- assert len(server.call_history) == 1
- assert "tools" in server.call_history[0]
- assert server.call_history[0]["tools"] == basic_tools
-
- @pytest.mark.asyncio
- async def test_extra_body_forwarded(self, basic_tools, valid_names):
- """extra_body should be forwarded to server."""
- extra = {"provider": {"ignore": ["DeepInfra"]}}
- server = MockServer([make_text_response("OK")])
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=basic_tools,
- valid_tool_names=valid_names,
- max_turns=10,
- extra_body=extra,
- )
- messages = [{"role": "user", "content": "Hi"}]
- await agent.run(messages)
-
- assert server.call_history[0].get("extra_body") == extra
-
- @pytest.mark.asyncio
- async def test_managed_state_returned(self, basic_tools, valid_names):
- """If server has get_state(), result should include managed_state."""
- server = MockServer([make_text_response("OK")])
- server.get_state = lambda: {"nodes": [{"test": True}]}
-
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=basic_tools,
- valid_tool_names=valid_names,
- max_turns=10,
- )
- messages = [{"role": "user", "content": "Hi"}]
- result = await agent.run(messages)
-
- assert result.managed_state is not None
- assert "nodes" in result.managed_state
-
- @pytest.mark.asyncio
- async def test_no_managed_state_without_get_state(self, basic_tools, valid_names):
- """Regular server without get_state() should return None managed_state."""
- server = MockServer([make_text_response("OK")])
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=basic_tools,
- valid_tool_names=valid_names,
- max_turns=10,
- )
- messages = [{"role": "user", "content": "Hi"}]
- result = await agent.run(messages)
-
- assert result.managed_state is None
-
- @pytest.mark.asyncio
- async def test_memory_tool_blocked(self, basic_tools):
- """Memory tool should return error in RL environments."""
- valid = {"terminal", "read_file", "todo", "memory"}
- server = MockServer([
- make_tool_response("memory", {"action": "add", "target": "user", "content": "test"}),
- make_text_response("Done"),
- ])
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=basic_tools,
- valid_tool_names=valid,
- max_turns=10,
- )
- messages = [{"role": "user", "content": "Remember this"}]
- result = await agent.run(messages)
-
- # Find the tool response
- tool_msgs = [m for m in result.messages if m["role"] == "tool"]
- assert len(tool_msgs) >= 1
- tool_result = json.loads(tool_msgs[0]["content"])
- assert "error" in tool_result
- assert "not available" in tool_result["error"].lower()
-
- @pytest.mark.asyncio
- async def test_session_search_blocked(self, basic_tools):
- """session_search should return error in RL environments."""
- valid = {"terminal", "read_file", "todo", "session_search"}
- server = MockServer([
- make_tool_response("session_search", {"query": "test"}),
- make_text_response("Done"),
- ])
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=basic_tools,
- valid_tool_names=valid,
- max_turns=10,
- )
- messages = [{"role": "user", "content": "Search sessions"}]
- result = await agent.run(messages)
-
- tool_msgs = [m for m in result.messages if m["role"] == "tool"]
- assert len(tool_msgs) >= 1
- tool_result = json.loads(tool_msgs[0]["content"])
- assert "error" in tool_result
-
- @pytest.mark.asyncio
- async def test_reasoning_content_preserved(self, basic_tools, valid_names):
- """Reasoning content should be extracted and preserved."""
- resp = MockChatCompletion(
- choices=[
- MockChoice(
- message=MockMessage(
- content="The answer is 42.",
- reasoning_content="Let me think about this step by step...",
- )
- )
- ]
- )
- server = MockServer([resp])
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=basic_tools,
- valid_tool_names=valid_names,
- max_turns=10,
- )
- messages = [{"role": "user", "content": "What is the meaning of life?"}]
- result = await agent.run(messages)
-
- assert len(result.reasoning_per_turn) == 1
- assert result.reasoning_per_turn[0] == "Let me think about this step by step..."
-
-
-class TestResizeToolPool:
- def test_resize_works(self):
- """resize_tool_pool should not raise."""
- resize_tool_pool(16) # Small pool for testing
- resize_tool_pool(128) # Restore default
-
- def test_resize_shuts_down_previous_executor(self, monkeypatch):
- """Replacing the global tool executor should shut down the old pool."""
- import environments.agent_loop as agent_loop_module
-
- old_executor = MagicMock()
- new_executor = MagicMock()
-
- monkeypatch.setattr(agent_loop_module, "_tool_executor", old_executor)
- monkeypatch.setattr(
- agent_loop_module.concurrent.futures,
- "ThreadPoolExecutor",
- MagicMock(return_value=new_executor),
- )
-
- resize_tool_pool(16)
-
- old_executor.shutdown.assert_called_once_with(wait=False)
- assert agent_loop_module._tool_executor is new_executor
diff --git a/tests/run_agent/test_agent_loop_tool_calling.py b/tests/run_agent/test_agent_loop_tool_calling.py
deleted file mode 100644
index 3b8d6ac5988..00000000000
--- a/tests/run_agent/test_agent_loop_tool_calling.py
+++ /dev/null
@@ -1,552 +0,0 @@
-"""Integration tests for HermesAgentLoop tool calling.
-
-Tests the full agent loop with real LLM calls via OpenRouter.
-Uses stepfun/step-3.5-flash:free by default (zero cost), falls back
-to anthropic/claude-sonnet-4 if the free model is unavailable.
-
-These tests verify:
-1. Single tool call: model calls a tool, gets result, responds
-2. Multi-tool call: model calls multiple tools in one turn
-3. Multi-turn: model calls tools across multiple turns
-4. Unknown tool rejection: model calling a non-existent tool gets an error
-5. Max turns: loop stops when max_turns is reached
-6. No tools: model responds without calling any tools
-7. Tool error handling: tool execution errors are captured
-
-Run:
- pytest tests/test_agent_loop_tool_calling.py -v
- pytest tests/test_agent_loop_tool_calling.py -v -k "single" # run one test
-"""
-
-import asyncio
-import json
-import os
-import sys
-from pathlib import Path
-from typing import Any, Dict, List, Set
-from unittest.mock import patch
-
-import pytest
-
-# pytestmark removed — tests skip gracefully via OPENROUTER_API_KEY check on line 59
-
-# Ensure repo root is importable
-_repo_root = Path(__file__).resolve().parent.parent.parent
-if str(_repo_root) not in sys.path:
- sys.path.insert(0, str(_repo_root))
-
-try:
- from environments.agent_loop import AgentResult, HermesAgentLoop
- from atroposlib.envs.server_handling.openai_server import OpenAIServer # noqa: F401
-except ImportError:
- pytest.skip("atroposlib not installed", allow_module_level=True)
-
-
-# =========================================================================
-# Test infrastructure
-# =========================================================================
-
-# Models to try, in order of preference (free first)
-_MODELS = [
- "stepfun/step-3.5-flash:free",
- "google/gemini-2.0-flash-001",
- "anthropic/claude-sonnet-4",
-]
-
-def _get_api_key():
- key = os.getenv("OPENROUTER_API_KEY", "")
- if not key:
- pytest.skip("OPENROUTER_API_KEY not set")
- return key
-
-
-def _make_server(model: str = None):
- """Create an OpenAI server for testing."""
- from atroposlib.envs.server_handling.openai_server import OpenAIServer
- from atroposlib.envs.server_handling.server_manager import APIServerConfig
-
- config = APIServerConfig(
- base_url="https://openrouter.ai/api/v1",
- model_name=model or _MODELS[0],
- server_type="openai",
- api_key=_get_api_key(),
- health_check=False,
- )
- return OpenAIServer(config)
-
-
-async def _try_models(test_fn):
- """Try running a test with each model until one works."""
- last_error = None
- for model in _MODELS:
- try:
- server = _make_server(model)
- return await test_fn(server, model)
- except Exception as e:
- last_error = e
- if "rate" in str(e).lower() or "limit" in str(e).lower():
- continue # Rate limited, try next model
- raise # Real error
- pytest.skip(f"All models failed. Last error: {last_error}")
-
-
-# =========================================================================
-# Fake tools for testing
-# =========================================================================
-
-# Simple calculator tool
-CALC_TOOL = {
- "type": "function",
- "function": {
- "name": "calculate",
- "description": "Calculate a math expression. Returns the numeric result.",
- "parameters": {
- "type": "object",
- "properties": {
- "expression": {
- "type": "string",
- "description": "Math expression to evaluate, e.g. '2 + 3'"
- }
- },
- "required": ["expression"],
- },
- },
-}
-
-# Weather lookup tool
-WEATHER_TOOL = {
- "type": "function",
- "function": {
- "name": "get_weather",
- "description": "Get the current weather for a city. Returns temperature and conditions.",
- "parameters": {
- "type": "object",
- "properties": {
- "city": {
- "type": "string",
- "description": "City name, e.g. 'Tokyo'"
- }
- },
- "required": ["city"],
- },
- },
-}
-
-# Lookup tool (always succeeds)
-LOOKUP_TOOL = {
- "type": "function",
- "function": {
- "name": "lookup",
- "description": "Look up a fact. Returns a short answer string.",
- "parameters": {
- "type": "object",
- "properties": {
- "query": {
- "type": "string",
- "description": "What to look up"
- }
- },
- "required": ["query"],
- },
- },
-}
-
-# Error tool (always fails)
-ERROR_TOOL = {
- "type": "function",
- "function": {
- "name": "failing_tool",
- "description": "A tool that always fails with an error.",
- "parameters": {
- "type": "object",
- "properties": {
- "input": {"type": "string"}
- },
- "required": ["input"],
- },
- },
-}
-
-
-def _fake_tool_handler(tool_name: str, args: Dict[str, Any], **kwargs) -> str:
- """Handle fake tool calls for testing."""
- if tool_name == "calculate":
- expr = args.get("expression", "0")
- try:
- # Safe eval for simple math
- result = eval(expr, {"__builtins__": {}}, {})
- return json.dumps({"result": result})
- except Exception as e:
- return json.dumps({"error": str(e)})
-
- elif tool_name == "get_weather":
- city = args.get("city", "Unknown")
- # Return canned weather
- return json.dumps({
- "city": city,
- "temperature": 22,
- "conditions": "sunny",
- "humidity": 45,
- })
-
- elif tool_name == "lookup":
- query = args.get("query", "")
- return json.dumps({"answer": f"The answer to '{query}' is 42."})
-
- elif tool_name == "failing_tool":
- raise RuntimeError("This tool always fails!")
-
- return json.dumps({"error": f"Unknown tool: {tool_name}"})
-
-
-# =========================================================================
-# Tests
-# =========================================================================
-
-@pytest.mark.asyncio
-async def test_single_tool_call():
- """Model should call a single tool, get the result, and respond."""
-
- async def _run(server, model):
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=[WEATHER_TOOL],
- valid_tool_names={"get_weather"},
- max_turns=5,
- temperature=0.0,
- max_tokens=500,
- )
-
- messages = [
- {"role": "user", "content": "What's the weather in Tokyo? Use the get_weather tool."},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- assert isinstance(result, AgentResult)
- assert result.turns_used >= 2, f"Expected at least 2 turns (tool call + response), got {result.turns_used}"
-
- # Verify a tool call happened
- tool_calls_found = False
- for msg in result.messages:
- if msg.get("role") == "assistant" and msg.get("tool_calls"):
- for tc in msg["tool_calls"]:
- if tc["function"]["name"] == "get_weather":
- tool_calls_found = True
- args = json.loads(tc["function"]["arguments"])
- assert "city" in args
- assert tool_calls_found, "Model should have called get_weather"
-
- # Verify tool result is in conversation
- tool_results = [m for m in result.messages if m.get("role") == "tool"]
- assert len(tool_results) >= 1, "Should have at least one tool result"
-
- # Verify the final response references the weather
- final_msg = result.messages[-1]
- assert final_msg["role"] == "assistant"
- assert final_msg["content"], "Final response should have content"
-
- return result
-
- await _try_models(_run)
-
-
-@pytest.mark.asyncio
-async def test_multi_tool_single_turn():
- """Model should call multiple tools in a single turn."""
-
- async def _run(server, model):
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=[WEATHER_TOOL, CALC_TOOL],
- valid_tool_names={"get_weather", "calculate"},
- max_turns=5,
- temperature=0.0,
- max_tokens=500,
- )
-
- messages = [
- {"role": "user", "content": (
- "I need two things at once: "
- "1) What's the weather in Paris? Use get_weather. "
- "2) What is 15 * 7? Use calculate. "
- "Call BOTH tools in a single response."
- )},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- # Count distinct tools called
- tools_called = set()
- for msg in result.messages:
- if msg.get("role") == "assistant" and msg.get("tool_calls"):
- for tc in msg["tool_calls"]:
- tools_called.add(tc["function"]["name"])
-
- # At minimum, both tools should have been called (maybe in different turns)
- assert "get_weather" in tools_called, f"get_weather not called. Called: {tools_called}"
- assert "calculate" in tools_called, f"calculate not called. Called: {tools_called}"
-
- return result
-
- await _try_models(_run)
-
-
-@pytest.mark.asyncio
-async def test_multi_turn_conversation():
- """Agent should handle multiple turns of tool calls."""
-
- async def _run(server, model):
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=[LOOKUP_TOOL, CALC_TOOL],
- valid_tool_names={"lookup", "calculate"},
- max_turns=10,
- temperature=0.0,
- max_tokens=500,
- )
-
- messages = [
- {"role": "user", "content": (
- "First, use the lookup tool to look up 'meaning of life'. "
- "Then use calculate to compute 6 * 7. "
- "Do these in separate tool calls, one at a time."
- )},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- # Should have used both tools
- tools_called = set()
- for msg in result.messages:
- if msg.get("role") == "assistant" and msg.get("tool_calls"):
- for tc in msg["tool_calls"]:
- tools_called.add(tc["function"]["name"])
-
- assert "lookup" in tools_called, f"lookup not called. Called: {tools_called}"
- assert "calculate" in tools_called, f"calculate not called. Called: {tools_called}"
-
- # Should finish naturally
- assert result.finished_naturally, "Should finish naturally after answering"
-
- return result
-
- await _try_models(_run)
-
-
-@pytest.mark.asyncio
-async def test_unknown_tool_rejected():
- """If the model calls a tool not in valid_tool_names, it gets an error."""
-
- async def _run(server, model):
- # Only allow "calculate" but give schema for both
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=[CALC_TOOL, WEATHER_TOOL],
- valid_tool_names={"calculate"}, # weather NOT allowed
- max_turns=5,
- temperature=0.0,
- max_tokens=500,
- )
-
- messages = [
- {"role": "user", "content": "What's the weather in London? Use get_weather."},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- # Check if get_weather was called and rejected
- if result.tool_errors:
- weather_errors = [e for e in result.tool_errors if e.tool_name == "get_weather"]
- assert len(weather_errors) > 0, "get_weather should have been rejected"
- assert "Unknown tool" in weather_errors[0].error
-
- return result
-
- await _try_models(_run)
-
-
-@pytest.mark.asyncio
-async def test_max_turns_limit():
- """Agent should stop after max_turns even if model keeps calling tools."""
-
- async def _run(server, model):
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=[LOOKUP_TOOL],
- valid_tool_names={"lookup"},
- max_turns=2, # Very low limit
- temperature=0.0,
- max_tokens=500,
- )
-
- messages = [
- {"role": "user", "content": (
- "Keep looking up facts. Look up 'fact 1', then 'fact 2', "
- "then 'fact 3', then 'fact 4'. Do them one at a time."
- )},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- assert result.turns_used <= 2, f"Should stop at max_turns=2, used {result.turns_used}"
- assert not result.finished_naturally, "Should NOT finish naturally (hit max_turns)"
-
- return result
-
- await _try_models(_run)
-
-
-@pytest.mark.asyncio
-async def test_no_tools_direct_response():
- """When no tools are useful, model should respond directly."""
-
- async def _run(server, model):
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=[WEATHER_TOOL],
- valid_tool_names={"get_weather"},
- max_turns=5,
- temperature=0.0,
- max_tokens=200,
- )
-
- messages = [
- {"role": "user", "content": "What is 2 + 2? Just answer directly, no tools needed."},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- assert result.finished_naturally, "Should finish naturally with a direct response"
- assert result.turns_used == 1, f"Should take exactly 1 turn for a direct answer, took {result.turns_used}"
-
- final = result.messages[-1]
- assert final["role"] == "assistant"
- assert final["content"], "Should have text content"
- assert "4" in final["content"], "Should contain the answer '4'"
-
- return result
-
- await _try_models(_run)
-
-
-@pytest.mark.asyncio
-async def test_tool_error_handling():
- """Tool execution errors should be captured and reported to the model."""
-
- async def _run(server, model):
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=[ERROR_TOOL],
- valid_tool_names={"failing_tool"},
- max_turns=5,
- temperature=0.0,
- max_tokens=500,
- )
-
- messages = [
- {"role": "user", "content": "Please call the failing_tool with input 'test'."},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- # The tool error should be recorded
- assert len(result.tool_errors) >= 1, "Should have at least one tool error"
- assert "RuntimeError" in result.tool_errors[0].error or "always fails" in result.tool_errors[0].error
-
- # The error should be in the conversation as a tool result
- tool_results = [m for m in result.messages if m.get("role") == "tool"]
- assert len(tool_results) >= 1
- error_result = json.loads(tool_results[0]["content"])
- assert "error" in error_result
-
- return result
-
- await _try_models(_run)
-
-
-@pytest.mark.asyncio
-async def test_agent_result_structure():
- """Verify the AgentResult has all expected fields populated."""
-
- async def _run(server, model):
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=[CALC_TOOL],
- valid_tool_names={"calculate"},
- max_turns=5,
- temperature=0.0,
- max_tokens=300,
- )
-
- messages = [
- {"role": "user", "content": "What is 3 + 4? Use the calculate tool."},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- # Structural checks
- assert isinstance(result, AgentResult)
- assert isinstance(result.messages, list)
- assert len(result.messages) >= 3, "Should have user + assistant(tool) + tool_result + assistant(final)"
- assert isinstance(result.turns_used, int)
- assert result.turns_used > 0
- assert isinstance(result.finished_naturally, bool)
- assert isinstance(result.tool_errors, list)
- assert isinstance(result.reasoning_per_turn, list)
-
- # Messages should follow OpenAI format
- for msg in result.messages:
- assert "role" in msg, f"Message missing 'role': {msg}"
- assert msg["role"] in ("system", "user", "assistant", "tool"), f"Invalid role: {msg['role']}"
-
- return result
-
- await _try_models(_run)
-
-
-@pytest.mark.asyncio
-async def test_conversation_history_preserved():
- """The full conversation history should be in result.messages."""
-
- async def _run(server, model):
- agent = HermesAgentLoop(
- server=server,
- tool_schemas=[WEATHER_TOOL],
- valid_tool_names={"get_weather"},
- max_turns=5,
- temperature=0.0,
- max_tokens=500,
- )
-
- messages = [
- {"role": "system", "content": "You are a helpful weather assistant."},
- {"role": "user", "content": "What's the weather in Berlin? Use get_weather."},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- # System message should be preserved
- assert result.messages[0]["role"] == "system"
- assert "weather assistant" in result.messages[0]["content"]
-
- # User message should be preserved
- assert result.messages[1]["role"] == "user"
- assert "Berlin" in result.messages[1]["content"]
-
- # Should have assistant + tool + assistant sequence
- roles = [m["role"] for m in result.messages]
- assert "tool" in roles, "Should have tool results in conversation"
-
- return result
-
- await _try_models(_run)
diff --git a/tests/run_agent/test_agent_loop_vllm.py b/tests/run_agent/test_agent_loop_vllm.py
deleted file mode 100644
index d4284909414..00000000000
--- a/tests/run_agent/test_agent_loop_vllm.py
+++ /dev/null
@@ -1,359 +0,0 @@
-"""Integration tests for HermesAgentLoop with a local vLLM server.
-
-Tests the full Phase 2 flow: ManagedServer + tool calling with a real
-vLLM backend, producing actual token IDs and logprobs for RL training.
-
-Requires a running vLLM server. Start one from the atropos directory:
-
- python -m example_trainer.vllm_api_server \
- --model Qwen/Qwen3-4B-Thinking-2507 \
- --port 9001 \
- --gpu-memory-utilization 0.8 \
- --max-model-len=32000
-
-Tests are automatically skipped if the server is not reachable.
-
-Run:
- pytest tests/test_agent_loop_vllm.py -v
- pytest tests/test_agent_loop_vllm.py -v -k "single"
-"""
-
-import asyncio
-import json
-import os
-import sys
-from pathlib import Path
-from typing import Any, Dict
-from unittest.mock import patch
-
-import pytest
-import requests
-
-# Ensure repo root is importable
-_repo_root = Path(__file__).resolve().parent.parent.parent
-if str(_repo_root) not in sys.path:
- sys.path.insert(0, str(_repo_root))
-
-try:
- from environments.agent_loop import AgentResult, HermesAgentLoop
-except ImportError:
- pytest.skip("atroposlib not installed", allow_module_level=True)
-
-
-# =========================================================================
-# Configuration
-# =========================================================================
-
-VLLM_HOST = "localhost"
-VLLM_PORT = 9001
-VLLM_BASE_URL = f"http://{VLLM_HOST}:{VLLM_PORT}"
-VLLM_MODEL = "Qwen/Qwen3-4B-Thinking-2507"
-
-
-def _vllm_is_running() -> bool:
- """Check if the vLLM server is reachable."""
- try:
- r = requests.get(f"{VLLM_BASE_URL}/health", timeout=3)
- return r.status_code == 200
- except Exception:
- return False
-
-
-# Skip all tests in this module if vLLM is not running
-pytestmark = pytest.mark.skipif(
- not _vllm_is_running(),
- reason=(
- f"vLLM server not reachable at {VLLM_BASE_URL}. "
- "Start it with: python -m example_trainer.vllm_api_server "
- f"--model {VLLM_MODEL} --port {VLLM_PORT} "
- "--gpu-memory-utilization 0.8 --max-model-len=32000"
- ),
-)
-
-
-# =========================================================================
-# Server setup
-# =========================================================================
-
-def _make_server_manager():
- """Create a ServerManager pointing to the local vLLM server."""
- from atroposlib.envs.server_handling.server_manager import (
- ServerManager,
- APIServerConfig,
- )
-
- config = APIServerConfig(
- base_url=VLLM_BASE_URL,
- model_name=VLLM_MODEL,
- server_type="vllm",
- health_check=False,
- )
- sm = ServerManager([config], tool_parser="hermes")
- sm.servers[0].server_healthy = True
- return sm
-
-
-def _get_tokenizer():
- """Load the tokenizer for the model."""
- from transformers import AutoTokenizer
- return AutoTokenizer.from_pretrained(VLLM_MODEL)
-
-
-# =========================================================================
-# Fake tools
-# =========================================================================
-
-WEATHER_TOOL = {
- "type": "function",
- "function": {
- "name": "get_weather",
- "description": "Get the current weather for a city. Returns temperature and conditions.",
- "parameters": {
- "type": "object",
- "properties": {
- "city": {
- "type": "string",
- "description": "City name, e.g. 'Tokyo'",
- }
- },
- "required": ["city"],
- },
- },
-}
-
-CALC_TOOL = {
- "type": "function",
- "function": {
- "name": "calculate",
- "description": "Calculate a math expression. Returns the numeric result.",
- "parameters": {
- "type": "object",
- "properties": {
- "expression": {
- "type": "string",
- "description": "Math expression, e.g. '2 + 3'",
- }
- },
- "required": ["expression"],
- },
- },
-}
-
-
-def _fake_tool_handler(tool_name: str, args: Dict[str, Any], **kwargs) -> str:
- """Handle fake tool calls for testing."""
- if tool_name == "get_weather":
- city = args.get("city", "Unknown")
- return json.dumps({
- "city": city,
- "temperature": 22,
- "conditions": "sunny",
- "humidity": 45,
- })
- elif tool_name == "calculate":
- expr = args.get("expression", "0")
- try:
- result = eval(expr, {"__builtins__": {}}, {})
- return json.dumps({"result": result})
- except Exception as e:
- return json.dumps({"error": str(e)})
- return json.dumps({"error": f"Unknown tool: {tool_name}"})
-
-
-# =========================================================================
-# Tests
-# =========================================================================
-
-@pytest.mark.asyncio
-async def test_vllm_single_tool_call():
- """vLLM model calls a tool, gets result, responds — full Phase 2 flow."""
- sm = _make_server_manager()
- tokenizer = _get_tokenizer()
-
- async with sm.managed_server(tokenizer=tokenizer) as managed:
- agent = HermesAgentLoop(
- server=managed,
- tool_schemas=[WEATHER_TOOL],
- valid_tool_names={"get_weather"},
- max_turns=5,
- temperature=0.6,
- max_tokens=1000,
- )
-
- messages = [
- {"role": "user", "content": "What's the weather in Tokyo? Use the get_weather tool."},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- assert isinstance(result, AgentResult)
- assert result.turns_used >= 2, f"Expected at least 2 turns, got {result.turns_used}"
-
- # Verify tool call happened
- tool_calls_found = False
- for msg in result.messages:
- if msg.get("role") == "assistant" and msg.get("tool_calls"):
- for tc in msg["tool_calls"]:
- if tc["function"]["name"] == "get_weather":
- tool_calls_found = True
- args = json.loads(tc["function"]["arguments"])
- assert "city" in args
- assert tool_calls_found, "Model should have called get_weather"
-
- # Verify tool results in conversation
- tool_results = [m for m in result.messages if m.get("role") == "tool"]
- assert len(tool_results) >= 1
-
-
-@pytest.mark.asyncio
-async def test_vllm_multi_tool_calls():
- """vLLM model calls multiple tools across turns."""
- sm = _make_server_manager()
- tokenizer = _get_tokenizer()
-
- async with sm.managed_server(tokenizer=tokenizer) as managed:
- agent = HermesAgentLoop(
- server=managed,
- tool_schemas=[WEATHER_TOOL, CALC_TOOL],
- valid_tool_names={"get_weather", "calculate"},
- max_turns=10,
- temperature=0.6,
- max_tokens=1000,
- )
-
- messages = [
- {"role": "user", "content": (
- "I need two things: "
- "1) What's the weather in Paris? Use get_weather. "
- "2) What is 15 * 7? Use calculate."
- )},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- # Both tools should be called
- tools_called = set()
- for msg in result.messages:
- if msg.get("role") == "assistant" and msg.get("tool_calls"):
- for tc in msg["tool_calls"]:
- tools_called.add(tc["function"]["name"])
-
- assert "get_weather" in tools_called, f"get_weather not called. Called: {tools_called}"
- assert "calculate" in tools_called, f"calculate not called. Called: {tools_called}"
-
-
-@pytest.mark.asyncio
-async def test_vllm_managed_server_produces_nodes():
- """ManagedServer should produce SequenceNodes with tokens and logprobs."""
- sm = _make_server_manager()
- tokenizer = _get_tokenizer()
-
- async with sm.managed_server(tokenizer=tokenizer) as managed:
- agent = HermesAgentLoop(
- server=managed,
- tool_schemas=[WEATHER_TOOL],
- valid_tool_names={"get_weather"},
- max_turns=5,
- temperature=0.6,
- max_tokens=1000,
- )
-
- messages = [
- {"role": "user", "content": "What's the weather in Berlin? Use get_weather."},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- # Get the managed state — should have SequenceNodes
- state = managed.get_state()
-
- assert state is not None, "ManagedServer should return state"
- nodes = state.get("nodes", [])
- assert len(nodes) >= 1, f"Should have at least 1 node, got {len(nodes)}"
-
- node = nodes[0]
- assert hasattr(node, "tokens"), "Node should have tokens"
- assert hasattr(node, "logprobs"), "Node should have logprobs"
- assert len(node.tokens) > 0, "Tokens should not be empty"
- assert len(node.logprobs) > 0, "Logprobs should not be empty"
- assert len(node.tokens) == len(node.logprobs), (
- f"Tokens ({len(node.tokens)}) and logprobs ({len(node.logprobs)}) should have same length"
- )
-
-
-@pytest.mark.asyncio
-async def test_vllm_no_tools_direct_response():
- """vLLM model should respond directly when no tools are needed."""
- sm = _make_server_manager()
- tokenizer = _get_tokenizer()
-
- async with sm.managed_server(tokenizer=tokenizer) as managed:
- agent = HermesAgentLoop(
- server=managed,
- tool_schemas=[WEATHER_TOOL],
- valid_tool_names={"get_weather"},
- max_turns=5,
- temperature=0.6,
- max_tokens=500,
- )
-
- messages = [
- {"role": "user", "content": "What is 2 + 2? Answer directly, no tools."},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- assert result.finished_naturally, "Should finish naturally"
- assert result.turns_used == 1, f"Should take 1 turn, took {result.turns_used}"
-
- final = result.messages[-1]
- assert final["role"] == "assistant"
- assert final["content"], "Should have content"
-
-
-@pytest.mark.asyncio
-async def test_vllm_thinking_content_extracted():
- """Qwen3-Thinking model should produce reasoning content."""
- sm = _make_server_manager()
- tokenizer = _get_tokenizer()
-
- async with sm.managed_server(
- tokenizer=tokenizer,
- preserve_think_blocks=True,
- ) as managed:
- agent = HermesAgentLoop(
- server=managed,
- tool_schemas=[CALC_TOOL],
- valid_tool_names={"calculate"},
- max_turns=5,
- temperature=0.6,
- max_tokens=1000,
- )
-
- messages = [
- {"role": "user", "content": "What is 123 * 456? Use the calculate tool."},
- ]
-
- with patch("environments.agent_loop.handle_function_call", side_effect=_fake_tool_handler):
- result = await agent.run(messages)
-
- # Qwen3-Thinking should generate blocks
- # Check if any content contains thinking markers
- has_thinking = False
- for msg in result.messages:
- content = msg.get("content", "") or ""
- if "" in content or "" in content:
- has_thinking = True
- break
-
- # Also check reasoning_per_turn
- has_reasoning = any(r for r in result.reasoning_per_turn if r)
-
- # At least one of these should be true for a thinking model
- assert has_thinking or has_reasoning, (
- "Qwen3-Thinking should produce blocks or reasoning content"
- )
diff --git a/tests/run_agent/test_streaming_tool_call_repair.py b/tests/run_agent/test_streaming_tool_call_repair.py
index dadfaec33e5..e85c0e22d18 100644
--- a/tests/run_agent/test_streaming_tool_call_repair.py
+++ b/tests/run_agent/test_streaming_tool_call_repair.py
@@ -23,7 +23,7 @@ class TestStreamingAssemblyRepair:
These tests verify the REPAIR FUNCTION itself works correctly for the
cases that arise during streaming assembly. Integration tests that
- exercise the full streaming path are in test_agent_loop_tool_calling.py.
+ exercise the full streaming path are in run_agent.py's streaming tests.
"""
# -- Truncation cases (most common streaming failure) --
diff --git a/tests/test_model_tools.py b/tests/test_model_tools.py
index 379aac2bbcf..beae3daa65e 100644
--- a/tests/test_model_tools.py
+++ b/tests/test_model_tools.py
@@ -278,7 +278,7 @@ class TestLegacyToolsetMap:
expected = [
"web_tools", "terminal_tools", "vision_tools", "moa_tools",
"image_tools", "skills_tools", "browser_tools", "cronjob_tools",
- "rl_tools", "file_tools", "tts_tools",
+ "file_tools", "tts_tools",
]
for name in expected:
assert name in _LEGACY_TOOLSET_MAP, f"Missing legacy toolset: {name}"
diff --git a/tests/tools/test_managed_server_tool_support.py b/tests/tools/test_managed_server_tool_support.py
deleted file mode 100644
index 5b917f3da89..00000000000
--- a/tests/tools/test_managed_server_tool_support.py
+++ /dev/null
@@ -1,178 +0,0 @@
-"""
-Tests for ManagedServer / tool-parser integration.
-
-Validates that:
-1. The installed atroposlib API still matches Hermes's expectations
-2. Hermes's parser registry remains compatible with ManagedServer parsing
-3. HermesAgentBaseEnv wires the selected parser into ServerManager correctly
-
-These tests verify the contract between hermes-agent's environments/ code
-and atroposlib's ManagedServer. They detect API incompatibilities early.
-"""
-
-import inspect
-import sys
-from pathlib import Path
-
-import pytest
-
-sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
-
-try:
- import atroposlib # noqa: F401
-except ImportError:
- pytest.skip("atroposlib not installed", allow_module_level=True)
-
-
-class TestManagedServerAPI:
- """Test that ManagedServer's API matches what hermes-agent expects."""
-
- def test_managed_server_init_signature(self):
- """ManagedServer should accept tool_call_parser parameter."""
- from atroposlib.envs.server_handling.managed_server import ManagedServer
-
- sig = inspect.signature(ManagedServer.__init__)
- params = list(sig.parameters.keys())
-
- # Core params that must exist
- assert "self" in params
- assert "server" in params
- assert "tokenizer" in params
- assert "track_tree" in params
-
- # tool_call_parser — required for tool_call_support branch
- # If this fails, atroposlib hasn't been updated to tool_call_support
- has_tool_parser = "tool_call_parser" in params
- if not has_tool_parser:
- pytest.skip(
- "ManagedServer does not have tool_call_parser param — "
- "baseline atroposlib (pre tool_call_support branch)"
- )
-
- def test_server_manager_managed_server_signature(self):
- """ServerManager.managed_server() should accept tool_call_parser."""
- from atroposlib.envs.server_handling.server_manager import ServerManager
-
- sig = inspect.signature(ServerManager.managed_server)
- params = list(sig.parameters.keys())
-
- assert "self" in params
- assert "tokenizer" in params
-
- has_tool_parser = "tool_call_parser" in params
- if not has_tool_parser:
- pytest.skip(
- "ServerManager.managed_server() does not have tool_call_parser param — "
- "baseline atroposlib (pre tool_call_support branch)"
- )
-
- def test_managed_server_chat_template_kwargs(self):
- """ManagedServer should have CHAT_TEMPLATE_KWARGS for forwarding tools/thinking."""
- from atroposlib.envs.server_handling.managed_server import ManagedServer
-
- if not hasattr(ManagedServer, "CHAT_TEMPLATE_KWARGS"):
- pytest.skip(
- "ManagedServer does not have CHAT_TEMPLATE_KWARGS — "
- "baseline atroposlib (pre tool_call_support branch)"
- )
-
- kwargs = ManagedServer.CHAT_TEMPLATE_KWARGS
- assert "tools" in kwargs, "tools must be in CHAT_TEMPLATE_KWARGS"
-
- def test_no_get_logprobs_method(self):
- """get_logprobs should be removed in tool_call_support branch."""
- from atroposlib.envs.server_handling.managed_server import ManagedServer
-
- # In baseline, get_logprobs exists. In tool_call_support, it's removed.
- # We just note the state — not a hard fail either way.
- has_get_logprobs = hasattr(ManagedServer, "get_logprobs")
- if has_get_logprobs:
- pytest.skip(
- "ManagedServer still has get_logprobs — baseline atroposlib"
- )
-
-
-class TestParserCompatibility:
- """Test that hermes-agent's parsers match ManagedServer's expectations."""
-
- def test_parser_parse_returns_correct_format(self):
- """
- ManagedServer expects parser.parse(text) -> (content, tool_calls)
- where tool_calls is a list of objects with .id, .function.name, .function.arguments
- """
- from environments.tool_call_parsers import get_parser
-
- parser = get_parser("hermes")
- text = '{"name": "terminal", "arguments": {"command": "ls"}}'
- content, tool_calls = parser.parse(text)
-
- assert tool_calls is not None
- assert len(tool_calls) == 1
-
- tc = tool_calls[0]
- # ManagedServer accesses these attrs directly
- assert hasattr(tc, "id")
- assert hasattr(tc, "function")
- assert hasattr(tc.function, "name")
- assert hasattr(tc.function, "arguments")
-
- def test_parser_no_tools_returns_none(self):
- """ManagedServer checks `if parsed_tool_calls:` — None should be falsy."""
- from environments.tool_call_parsers import get_parser
-
- parser = get_parser("hermes")
- content, tool_calls = parser.parse("Just text, no tools")
- assert tool_calls is None
-
- def test_parser_content_is_string_or_none(self):
- """ManagedServer uses `parsed_content or ""` — must be str or None."""
- from environments.tool_call_parsers import get_parser
-
- parser = get_parser("hermes")
-
- # With tool calls
- text = '{"name": "terminal", "arguments": {"command": "ls"}}'
- content, _ = parser.parse(text)
- assert content is None or isinstance(content, str)
-
- # Without tool calls
- content2, _ = parser.parse("Just text")
- assert isinstance(content2, str)
-
-
-class TestBaseEnvCompatibility:
- """Test that hermes_base_env.py's tool-parser wiring matches the current API."""
-
- def test_hermes_base_env_sets_server_manager_tool_parser(self):
- """Hermes wires parser selection through ServerManager.tool_parser."""
- import ast
-
- base_env_path = Path(__file__).parent.parent.parent / "environments" / "hermes_base_env.py"
- source = base_env_path.read_text()
- tree = ast.parse(source)
-
- found_assignment = False
- for node in ast.walk(tree):
- if isinstance(node, ast.Assign):
- for target in node.targets:
- if isinstance(target, ast.Attribute) and target.attr == "tool_parser":
- parent = target.value
- if (
- isinstance(parent, ast.Attribute)
- and parent.attr == "server"
- and isinstance(parent.value, ast.Name)
- and parent.value.id == "self"
- ):
- found_assignment = True
-
- assert found_assignment, (
- "hermes_base_env.py should set self.server.tool_parser from config.tool_call_parser"
- )
-
- def test_hermes_base_env_uses_config_tool_call_parser(self):
- """Verify hermes_base_env uses the config field rather than a local parser instance."""
- base_env_path = Path(__file__).parent.parent.parent / "environments" / "hermes_base_env.py"
- source = base_env_path.read_text()
-
- assert 'tool_call_parser: str = Field(' in source
- assert 'self.server.tool_parser = config.tool_call_parser' in source
diff --git a/tests/tools/test_rl_training_tool.py b/tests/tools/test_rl_training_tool.py
deleted file mode 100644
index 8b68ea8d946..00000000000
--- a/tests/tools/test_rl_training_tool.py
+++ /dev/null
@@ -1,142 +0,0 @@
-"""Tests for rl_training_tool.py — file handle lifecycle and cleanup.
-
-Verifies that _stop_training_run properly closes log file handles,
-terminates processes, and handles edge cases on failure paths.
-Inspired by PR #715 (0xbyt4).
-"""
-
-from unittest.mock import MagicMock
-
-import pytest
-
-from tools.rl_training_tool import RunState, _stop_training_run
-
-
-def _make_run_state(**overrides) -> RunState:
- """Create a minimal RunState for testing."""
- defaults = {
- "run_id": "test-run-001",
- "environment": "test_env",
- "config": {},
- }
- defaults.update(overrides)
- return RunState(**defaults)
-
-
-class TestStopTrainingRunFileHandles:
- """Verify that _stop_training_run closes log file handles stored as attributes."""
-
- def test_closes_all_log_file_handles(self):
- state = _make_run_state()
- files = {}
- for attr in ("api_log_file", "trainer_log_file", "env_log_file"):
- fh = MagicMock()
- setattr(state, attr, fh)
- files[attr] = fh
-
- _stop_training_run(state)
-
- for attr, fh in files.items():
- fh.close.assert_called_once()
- assert getattr(state, attr) is None
-
- def test_clears_file_attrs_to_none(self):
- state = _make_run_state()
- state.api_log_file = MagicMock()
-
- _stop_training_run(state)
-
- assert state.api_log_file is None
-
- def test_close_exception_does_not_propagate(self):
- """If a file handle .close() raises, it must not crash."""
- state = _make_run_state()
- bad_fh = MagicMock()
- bad_fh.close.side_effect = OSError("already closed")
- good_fh = MagicMock()
- state.api_log_file = bad_fh
- state.trainer_log_file = good_fh
-
- _stop_training_run(state) # should not raise
-
- bad_fh.close.assert_called_once()
- good_fh.close.assert_called_once()
-
- def test_handles_missing_file_attrs(self):
- """RunState without log file attrs should not crash."""
- state = _make_run_state()
- # No log file attrs set at all — getattr(..., None) should handle it
- _stop_training_run(state) # should not raise
-
-
-class TestStopTrainingRunProcesses:
- """Verify that _stop_training_run terminates processes correctly."""
-
- def test_terminates_running_processes(self):
- state = _make_run_state()
- for attr in ("api_process", "trainer_process", "env_process"):
- proc = MagicMock()
- proc.poll.return_value = None # still running
- setattr(state, attr, proc)
-
- _stop_training_run(state)
-
- for attr in ("api_process", "trainer_process", "env_process"):
- getattr(state, attr).terminate.assert_called_once()
-
- def test_does_not_terminate_exited_processes(self):
- state = _make_run_state()
- proc = MagicMock()
- proc.poll.return_value = 0 # already exited
- state.api_process = proc
-
- _stop_training_run(state)
-
- proc.terminate.assert_not_called()
-
- def test_handles_none_processes(self):
- state = _make_run_state()
- # All process attrs are None by default
- _stop_training_run(state) # should not raise
-
- def test_handles_mixed_running_and_exited_processes(self):
- state = _make_run_state()
- # api still running
- api = MagicMock()
- api.poll.return_value = None
- state.api_process = api
- # trainer already exited
- trainer = MagicMock()
- trainer.poll.return_value = 0
- state.trainer_process = trainer
- # env is None
- state.env_process = None
-
- _stop_training_run(state)
-
- api.terminate.assert_called_once()
- trainer.terminate.assert_not_called()
-
-
-class TestStopTrainingRunStatus:
- """Verify status transitions in _stop_training_run."""
-
- def test_sets_status_to_stopped_when_running(self):
- state = _make_run_state(status="running")
- _stop_training_run(state)
- assert state.status == "stopped"
-
- def test_does_not_change_status_when_failed(self):
- state = _make_run_state(status="failed")
- _stop_training_run(state)
- assert state.status == "failed"
-
- def test_does_not_change_status_when_pending(self):
- state = _make_run_state(status="pending")
- _stop_training_run(state)
- assert state.status == "pending"
-
- def test_no_crash_with_no_processes_and_no_files(self):
- state = _make_run_state()
- _stop_training_run(state) # should not raise
- assert state.status == "pending"
diff --git a/tests/tools/test_tool_call_parsers.py b/tests/tools/test_tool_call_parsers.py
deleted file mode 100644
index bdea75698a8..00000000000
--- a/tests/tools/test_tool_call_parsers.py
+++ /dev/null
@@ -1,274 +0,0 @@
-"""
-Tests for environments/tool_call_parsers/ — client-side tool call parsers.
-
-These parsers extract structured tool_calls from raw model output text.
-Used in Phase 2 (VLLM/generate) where the server returns raw tokens.
-"""
-
-import json
-import sys
-from pathlib import Path
-
-import pytest
-
-# Ensure repo root is importable
-sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
-
-try:
- from environments.tool_call_parsers import (
- ParseResult,
- ToolCallParser,
- get_parser,
- list_parsers,
- )
-except ImportError:
- pytest.skip("atroposlib not installed", allow_module_level=True)
-
-
-# ─── Registry tests ─────────────────────────────────────────────────────
-
-class TestParserRegistry:
- def test_list_parsers_returns_nonempty(self):
- parsers = list_parsers()
- assert len(parsers) > 0
-
- def test_hermes_parser_registered(self):
- parsers = list_parsers()
- assert "hermes" in parsers
-
- def test_get_parser_returns_instance(self):
- parser = get_parser("hermes")
- assert isinstance(parser, ToolCallParser)
-
- def test_get_parser_unknown_raises(self):
- with pytest.raises(KeyError):
- get_parser("nonexistent_parser_xyz")
-
- def test_all_registered_parsers_instantiate(self):
- """Every registered parser should be importable and instantiable."""
- for name in list_parsers():
- parser = get_parser(name)
- assert isinstance(parser, ToolCallParser)
- assert hasattr(parser, "parse")
-
-
-# ─── Hermes parser tests ────────────────────────────────────────────────
-
-class TestHermesParser:
- @pytest.fixture
- def parser(self):
- return get_parser("hermes")
-
- def test_no_tool_call(self, parser):
- text = "Hello, I can help you with that."
- content, tool_calls = parser.parse(text)
- assert content == text
- assert tool_calls is None
-
- def test_single_tool_call(self, parser):
- text = '{"name": "terminal", "arguments": {"command": "ls -la"}}'
- content, tool_calls = parser.parse(text)
- assert tool_calls is not None
- assert len(tool_calls) == 1
- assert tool_calls[0].function.name == "terminal"
- args = json.loads(tool_calls[0].function.arguments)
- assert args["command"] == "ls -la"
-
- def test_tool_call_with_surrounding_text(self, parser):
- text = 'Let me check that for you.\n{"name": "terminal", "arguments": {"command": "pwd"}}'
- content, tool_calls = parser.parse(text)
- assert tool_calls is not None
- assert len(tool_calls) == 1
- assert tool_calls[0].function.name == "terminal"
- # Content should have the surrounding text
- if content is not None:
- assert "check that" in content or content.strip() != ""
-
- def test_multiple_tool_calls(self, parser):
- text = (
- '{"name": "terminal", "arguments": {"command": "ls"}}\n'
- '{"name": "read_file", "arguments": {"path": "test.py"}}'
- )
- content, tool_calls = parser.parse(text)
- assert tool_calls is not None
- assert len(tool_calls) == 2
- names = {tc.function.name for tc in tool_calls}
- assert "terminal" in names
- assert "read_file" in names
-
- def test_tool_call_ids_are_unique(self, parser):
- text = (
- '{"name": "terminal", "arguments": {"command": "ls"}}\n'
- '{"name": "terminal", "arguments": {"command": "pwd"}}'
- )
- _, tool_calls = parser.parse(text)
- assert tool_calls is not None
- ids = [tc.id for tc in tool_calls]
- assert len(ids) == len(set(ids)), "Tool call IDs must be unique"
-
- def test_empty_string(self, parser):
- content, tool_calls = parser.parse("")
- assert tool_calls is None
-
- def test_malformed_json_in_tool_call(self, parser):
- text = 'not valid json'
- content, tool_calls = parser.parse(text)
- # Should either return None tool_calls or handle gracefully
- # (implementation may vary — some parsers return error tool calls)
-
- def test_truncated_tool_call(self, parser):
- """Test handling of unclosed tool_call tag (model truncated mid-generation)."""
- text = '{"name": "terminal", "arguments": {"command": "ls -la"}'
- content, tool_calls = parser.parse(text)
- # Parser should handle truncated output gracefully
- # Either parse it successfully or return None
-
-
-# ─── Parse result contract tests (applies to ALL parsers) ───────────────
-
-class TestParseResultContract:
- """Ensure all parsers conform to the ParseResult contract."""
-
- @pytest.fixture(params=["hermes"]) # Add more as needed
- def parser(self, request):
- return get_parser(request.param)
-
- def test_returns_tuple_of_two(self, parser):
- result = parser.parse("hello world")
- assert isinstance(result, tuple)
- assert len(result) == 2
-
- def test_no_tools_returns_none_tool_calls(self, parser):
- content, tool_calls = parser.parse("Just plain text, no tools.")
- assert tool_calls is None
- assert content is not None
-
- def test_tool_calls_are_proper_objects(self, parser):
- """When tool calls are found, they should be ChatCompletionMessageToolCall objects."""
- # Use hermes format since that's universal
- text = '{"name": "terminal", "arguments": {"command": "echo hi"}}'
- content, tool_calls = parser.parse(text)
- if tool_calls is not None:
- for tc in tool_calls:
- assert hasattr(tc, "id")
- assert hasattr(tc, "function")
- assert hasattr(tc.function, "name")
- assert hasattr(tc.function, "arguments")
- assert tc.id is not None
- assert isinstance(tc.function.name, str)
- assert isinstance(tc.function.arguments, str)
-
-
-# ─── DeepSeek V3 parser tests ───────────────────────────────────────────
-
-class TestDeepSeekV3Parser:
- @pytest.fixture
- def parser(self):
- return get_parser("deepseek_v3")
-
- def test_no_tool_call(self, parser):
- text = "Hello, how can I help you?"
- content, tool_calls = parser.parse(text)
- assert content == text
- assert tool_calls is None
-
- def test_single_tool_call(self, parser):
- text = (
- '<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_weather\n'
- '```json\n{"city": "London"}\n```<|tool▁call▁end|><|tool▁calls▁end|>'
- )
- content, tool_calls = parser.parse(text)
- assert tool_calls is not None
- assert len(tool_calls) == 1
- assert tool_calls[0].function.name == "get_weather"
- args = json.loads(tool_calls[0].function.arguments)
- assert args["city"] == "London"
-
- def test_multiple_tool_calls(self, parser):
- text = (
- '<|tool▁calls▁begin|>'
- '<|tool▁call▁begin|>function<|tool▁sep|>get_weather\n'
- '```json\n{"city": "London"}\n```<|tool▁call▁end|>'
- '<|tool▁call▁begin|>function<|tool▁sep|>get_time\n'
- '```json\n{"timezone": "UTC"}\n```<|tool▁call▁end|>'
- '<|tool▁calls▁end|>'
- )
- content, tool_calls = parser.parse(text)
- assert tool_calls is not None
- assert len(tool_calls) == 2, f"Expected 2 tool calls, got {len(tool_calls)}"
- names = [tc.function.name for tc in tool_calls]
- assert "get_weather" in names
- assert "get_time" in names
-
- def test_tool_call_with_preceding_text(self, parser):
- text = (
- 'Let me check that for you.\n'
- '<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>terminal\n'
- '```json\n{"command": "ls"}\n```<|tool▁call▁end|><|tool▁calls▁end|>'
- )
- content, tool_calls = parser.parse(text)
- assert tool_calls is not None
- assert len(tool_calls) == 1
-
-
-# ─── Mistral parser tests ───────────────────────────────────────────────
-
-class TestMistralParser:
- @pytest.fixture
- def parser(self):
- return get_parser("mistral")
-
- def test_no_tool_call(self, parser):
- text = "Hello, how can I help you?"
- content, tool_calls = parser.parse(text)
- assert content == text
- assert tool_calls is None
-
- def test_pre_v11_single_tool_call(self, parser):
- text = '[TOOL_CALLS] [{"name": "func", "arguments": {"key": "val"}}]'
- content, tool_calls = parser.parse(text)
- assert tool_calls is not None
- assert len(tool_calls) == 1
- assert tool_calls[0].function.name == "func"
- args = json.loads(tool_calls[0].function.arguments)
- assert args["key"] == "val"
-
- def test_pre_v11_nested_json(self, parser):
- text = '[TOOL_CALLS] [{"name": "func", "arguments": {"nested": {"deep": true}}}]'
- content, tool_calls = parser.parse(text)
- assert tool_calls is not None
- assert len(tool_calls) == 1
- assert tool_calls[0].function.name == "func"
- args = json.loads(tool_calls[0].function.arguments)
- assert args["nested"]["deep"] is True
-
- def test_v11_single_tool_call(self, parser):
- text = '[TOOL_CALLS]get_weather{"city": "London"}'
- content, tool_calls = parser.parse(text)
- assert tool_calls is not None
- assert len(tool_calls) == 1
- assert tool_calls[0].function.name == "get_weather"
- args = json.loads(tool_calls[0].function.arguments)
- assert args["city"] == "London"
-
- def test_v11_multiple_tool_calls(self, parser):
- text = '[TOOL_CALLS]func1{"a": 1}[TOOL_CALLS]func2{"b": 2}'
- content, tool_calls = parser.parse(text)
- assert tool_calls is not None
- assert len(tool_calls) == 2
- names = [tc.function.name for tc in tool_calls]
- assert "func1" in names
- assert "func2" in names
-
- def test_preceding_text_preserved(self, parser):
- text = 'Hello[TOOL_CALLS]func{"a": 1}'
- content, tool_calls = parser.parse(text)
- assert content == "Hello"
- assert tool_calls is not None
- assert len(tool_calls) == 1
- assert tool_calls[0].function.name == "func"
-
- def test_malformed_json_fallback(self, parser):
- text = "[TOOL_CALLS] not valid json"
- content, tool_calls = parser.parse(text)
- assert tool_calls is None
diff --git a/tinker-atropos b/tinker-atropos
deleted file mode 160000
index 65f084ee805..00000000000
--- a/tinker-atropos
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 65f084ee8054a5d02aeac76e24ed60388511c82b
diff --git a/tools/budget_config.py b/tools/budget_config.py
index 577e59442ee..093188d5c75 100644
--- a/tools/budget_config.py
+++ b/tools/budget_config.py
@@ -1,6 +1,5 @@
"""Configurable budget constants for tool result persistence.
-Overridable at the RL environment level via HermesAgentEnvConfig fields.
Per-tool resolution: pinned > config overrides > registry > default.
"""
diff --git a/tools/rl_training_tool.py b/tools/rl_training_tool.py
deleted file mode 100644
index c7acb8012e1..00000000000
--- a/tools/rl_training_tool.py
+++ /dev/null
@@ -1,1396 +0,0 @@
-#!/usr/bin/env python3
-"""
-RL Training Tools Module
-
-This module provides tools for running RL training through Tinker-Atropos.
-Directly manages training processes without requiring a separate API server.
-
-Features:
-- Environment discovery (AST-based scanning for BaseEnv subclasses)
-- Configuration management with locked infrastructure settings
-- Training run lifecycle via subprocess management
-- WandB metrics monitoring
-
-Required environment variables:
-- TINKER_API_KEY: API key for Tinker service
-- WANDB_API_KEY: API key for Weights & Biases metrics
-
-Usage:
- from tools.rl_training_tool import (
- rl_list_environments,
- rl_select_environment,
- rl_get_current_config,
- rl_edit_config,
- rl_start_training,
- rl_check_status,
- rl_stop_training,
- rl_get_results,
- )
-"""
-
-import ast
-import asyncio
-import importlib.util
-import json
-import os
-import subprocess
-import sys
-import time
-import uuid
-import logging
-from datetime import datetime
-import yaml
-from dataclasses import dataclass
-from pathlib import Path
-from typing import Any, Dict, List, Optional
-
-from hermes_constants import get_hermes_home
-
-logger = logging.getLogger(__name__)
-
-# ============================================================================
-# Path Configuration
-# ============================================================================
-
-# Path to tinker-atropos submodule (relative to hermes-agent root)
-HERMES_ROOT = Path(__file__).parent.parent
-TINKER_ATROPOS_ROOT = HERMES_ROOT / "tinker-atropos"
-ENVIRONMENTS_DIR = TINKER_ATROPOS_ROOT / "tinker_atropos" / "environments"
-CONFIGS_DIR = TINKER_ATROPOS_ROOT / "configs"
-LOGS_DIR = get_hermes_home() / "logs" / "rl_training"
-
-def _ensure_logs_dir():
- """Lazily create logs directory on first use (avoid side effects at import time)."""
- if TINKER_ATROPOS_ROOT.exists():
- LOGS_DIR.mkdir(exist_ok=True)
-
-# ============================================================================
-# Locked Configuration (Infrastructure Settings)
-# ============================================================================
-
-# These fields cannot be changed by the model - they're tuned for our infrastructure
-LOCKED_FIELDS = {
- "env": {
- "tokenizer_name": "Qwen/Qwen3-8B",
- "rollout_server_url": "http://localhost:8000",
- "use_wandb": True,
- "max_token_length": 8192,
- "max_num_workers": 2048,
- "worker_timeout": 3600,
- "total_steps": 2500,
- "steps_per_eval": 25,
- "max_batches_offpolicy": 3,
- "inference_weight": 1.0,
- "eval_limit_ratio": 0.1,
- },
- "openai": [
- {
- "model_name": "Qwen/Qwen3-8B",
- "base_url": "http://localhost:8001/v1",
- "api_key": "x",
- "weight": 1.0,
- "num_requests_for_eval": 256,
- "timeout": 3600,
- "server_type": "sglang", # Tinker uses sglang for actual training
- }
- ],
- "tinker": {
- "lora_rank": 32,
- "learning_rate": 0.00004,
- "max_token_trainer_length": 9000,
- "checkpoint_dir": "./temp/",
- "save_checkpoint_interval": 25,
- },
- "slurm": False,
- "testing": False,
-}
-
-LOCKED_FIELD_NAMES = set(LOCKED_FIELDS.get("env", {}).keys())
-
-
-# ============================================================================
-# State Management
-# ============================================================================
-
-@dataclass
-class EnvironmentInfo:
- """Information about a discovered environment."""
- name: str
- class_name: str
- file_path: str
- description: str = ""
- config_class: str = "BaseEnvConfig"
-
-
-@dataclass
-class RunState:
- """State for a training run."""
- run_id: str
- environment: str
- config: Dict[str, Any]
- status: str = "pending" # pending, starting, running, stopping, stopped, completed, failed
- error_message: str = ""
- wandb_project: str = ""
- wandb_run_name: str = ""
- start_time: float = 0.0
- # Process handles
- api_process: Optional[subprocess.Popen] = None
- trainer_process: Optional[subprocess.Popen] = None
- env_process: Optional[subprocess.Popen] = None
-
-
-# Global state
-_environments: List[EnvironmentInfo] = []
-_current_env: Optional[str] = None
-_current_config: Dict[str, Any] = {}
-_env_config_cache: Dict[str, Dict[str, Dict[str, Any]]] = {}
-_active_runs: Dict[str, RunState] = {}
-_last_status_check: Dict[str, float] = {}
-
-# Rate limiting for status checks (30 minutes)
-MIN_STATUS_CHECK_INTERVAL = 30 * 60
-
-
-# ============================================================================
-# Environment Discovery
-# ============================================================================
-
-def _scan_environments() -> List[EnvironmentInfo]:
- """
- Scan the environments directory for BaseEnv subclasses using AST.
- """
- environments = []
-
- if not ENVIRONMENTS_DIR.exists():
- return environments
-
- for py_file in ENVIRONMENTS_DIR.glob("*.py"):
- if py_file.name.startswith("_"):
- continue
-
- try:
- with open(py_file, "r", encoding="utf-8") as f:
- tree = ast.parse(f.read())
-
- for node in ast.walk(tree):
- if isinstance(node, ast.ClassDef):
- # Check if class has BaseEnv as base
- for base in node.bases:
- base_name = ""
- if isinstance(base, ast.Name):
- base_name = base.id
- elif isinstance(base, ast.Attribute):
- base_name = base.attr
-
- if base_name == "BaseEnv":
- # Extract name from class attribute if present
- env_name = py_file.stem
- description = ""
- config_class = "BaseEnvConfig"
-
- for item in node.body:
- if isinstance(item, ast.Assign):
- for target in item.targets:
- if isinstance(target, ast.Name):
- if target.id == "name" and isinstance(item.value, ast.Constant):
- env_name = item.value.value
- elif target.id == "env_config_cls" and isinstance(item.value, ast.Name):
- config_class = item.value.id
-
- # Get docstring
- if isinstance(item, ast.Expr) and isinstance(item.value, ast.Constant):
- if isinstance(item.value.value, str) and not description:
- description = item.value.value.split("\n")[0].strip()
-
- environments.append(EnvironmentInfo(
- name=env_name,
- class_name=node.name,
- file_path=str(py_file),
- description=description or f"Environment from {py_file.name}",
- config_class=config_class,
- ))
- break
- except Exception as e:
- logger.warning("Could not parse %s: %s", py_file, e)
-
- return environments
-
-
-def _get_env_config_fields(env_file_path: str) -> Dict[str, Dict[str, Any]]:
- """
- Dynamically import an environment and extract its config fields.
-
- Uses config_init() to get the actual config class, with fallback to
- directly importing BaseEnvConfig if config_init fails.
- """
- try:
- # Load the environment module
- spec = importlib.util.spec_from_file_location("env_module", env_file_path)
- module = importlib.util.module_from_spec(spec)
- sys.modules["env_module"] = module
- spec.loader.exec_module(module)
-
- # Find the BaseEnv subclass
- env_class = None
- for name, obj in vars(module).items():
- if isinstance(obj, type) and name != "BaseEnv":
- if hasattr(obj, "config_init") and callable(getattr(obj, "config_init")):
- env_class = obj
- break
-
- if not env_class:
- return {}
-
- # Try calling config_init to get the actual config class
- config_class = None
- try:
- env_config, server_configs = env_class.config_init()
- config_class = type(env_config)
- except Exception as config_error:
- # Fallback: try to import BaseEnvConfig directly from atroposlib
- logger.info("config_init failed (%s), using BaseEnvConfig defaults", config_error)
- try:
- from atroposlib.envs.base import BaseEnvConfig
- config_class = BaseEnvConfig
- except ImportError:
- return {}
-
- if not config_class:
- return {}
-
- # Helper to make values JSON-serializable (handle enums, etc.)
- def make_serializable(val):
- if val is None:
- return None
- if hasattr(val, 'value'): # Enum
- return val.value
- if hasattr(val, 'name') and hasattr(val, '__class__') and 'Enum' in str(type(val)):
- return val.name
- return val
-
- # Extract fields from the Pydantic model
- fields = {}
- for field_name, field_info in config_class.model_fields.items():
- field_type = field_info.annotation
- default = make_serializable(field_info.default)
- description = field_info.description or ""
-
- is_locked = field_name in LOCKED_FIELD_NAMES
-
- # Convert type to string
- type_name = getattr(field_type, "__name__", str(field_type))
- if hasattr(field_type, "__origin__"):
- type_name = str(field_type)
-
- locked_value = LOCKED_FIELDS.get("env", {}).get(field_name, default)
- current_value = make_serializable(locked_value) if is_locked else default
-
- fields[field_name] = {
- "type": type_name,
- "default": default,
- "description": description,
- "locked": is_locked,
- "current_value": current_value,
- }
-
- return fields
-
- except Exception as e:
- logger.warning("Could not introspect environment config: %s", e)
- return {}
-
-
-def _initialize_environments():
- """Initialize environment list on first use."""
- global _environments
- if not _environments:
- _environments = _scan_environments()
-
-
-# ============================================================================
-# Subprocess Management
-# ============================================================================
-
-async def _spawn_training_run(run_state: RunState, config_path: Path):
- """
- Spawn the three processes needed for training:
- 1. run-api (Atropos API server)
- 2. launch_training.py (Tinker trainer + inference server)
- 3. environment.py serve (the Atropos environment)
- """
- run_id = run_state.run_id
-
- _ensure_logs_dir()
-
- # Log file paths
- api_log = LOGS_DIR / f"api_{run_id}.log"
- trainer_log = LOGS_DIR / f"trainer_{run_id}.log"
- env_log = LOGS_DIR / f"env_{run_id}.log"
-
- try:
- # Step 1: Start the Atropos API server (run-api)
- logger.info("[%s] Starting Atropos API server (run-api)...", run_id)
-
- # File must stay open while the subprocess runs; we store the handle
- # on run_state so _stop_training_run() can close it when done.
- api_log_file = open(api_log, "w", encoding="utf-8") # closed by _stop_training_run
- run_state.api_log_file = api_log_file
- run_state.api_process = subprocess.Popen(
- ["run-api"],
- stdout=api_log_file,
- stderr=subprocess.STDOUT,
- cwd=str(TINKER_ATROPOS_ROOT),
- )
-
- # Wait for API to start
- await asyncio.sleep(5)
-
- if run_state.api_process.poll() is not None:
- run_state.status = "failed"
- run_state.error_message = f"API server exited with code {run_state.api_process.returncode}. Check {api_log}"
- _stop_training_run(run_state)
- return
-
- logger.info("[%s] Atropos API server started", run_id)
-
- # Step 2: Start the Tinker trainer
- logger.info("[%s] Starting Tinker trainer: launch_training.py --config %s", run_id, config_path)
-
- trainer_log_file = open(trainer_log, "w", encoding="utf-8") # closed by _stop_training_run
- run_state.trainer_log_file = trainer_log_file
- run_state.trainer_process = subprocess.Popen(
- [sys.executable, "launch_training.py", "--config", str(config_path)],
- stdout=trainer_log_file,
- stderr=subprocess.STDOUT,
- cwd=str(TINKER_ATROPOS_ROOT),
- env={**os.environ, "TINKER_API_KEY": os.getenv("TINKER_API_KEY", "")},
- )
-
- # Wait for trainer to initialize (it starts FastAPI inference server on 8001)
- logger.info("[%s] Waiting 30 seconds for trainer to initialize...", run_id)
- await asyncio.sleep(30)
-
- if run_state.trainer_process.poll() is not None:
- run_state.status = "failed"
- run_state.error_message = f"Trainer exited with code {run_state.trainer_process.returncode}. Check {trainer_log}"
- _stop_training_run(run_state)
- return
-
- logger.info("[%s] Trainer started, inference server on port 8001", run_id)
-
- # Step 3: Start the environment
- logger.info("[%s] Waiting 90 more seconds before starting environment...", run_id)
- await asyncio.sleep(90)
-
- # Find the environment file
- env_info = None
- for env in _environments:
- if env.name == run_state.environment:
- env_info = env
- break
-
- if not env_info:
- run_state.status = "failed"
- run_state.error_message = f"Environment '{run_state.environment}' not found"
- _stop_training_run(run_state)
- return
-
- logger.info("[%s] Starting environment: %s serve", run_id, env_info.file_path)
-
- env_log_file = open(env_log, "w", encoding="utf-8") # closed by _stop_training_run
- run_state.env_log_file = env_log_file
- run_state.env_process = subprocess.Popen(
- [sys.executable, str(env_info.file_path), "serve", "--config", str(config_path)],
- stdout=env_log_file,
- stderr=subprocess.STDOUT,
- cwd=str(TINKER_ATROPOS_ROOT),
- )
-
- # Wait for environment to connect
- await asyncio.sleep(10)
-
- if run_state.env_process.poll() is not None:
- run_state.status = "failed"
- run_state.error_message = f"Environment exited with code {run_state.env_process.returncode}. Check {env_log}"
- _stop_training_run(run_state)
- return
-
- run_state.status = "running"
- run_state.start_time = time.time()
- logger.info("[%s] Training run started successfully!", run_id)
-
- # Start background monitoring
- asyncio.create_task(_monitor_training_run(run_state))
-
- except Exception as e:
- run_state.status = "failed"
- run_state.error_message = str(e)
- _stop_training_run(run_state)
-
-
-async def _monitor_training_run(run_state: RunState):
- """Background task to monitor a training run."""
- while run_state.status == "running":
- await asyncio.sleep(30) # Check every 30 seconds
-
- # Check if any process has died
- if run_state.env_process and run_state.env_process.poll() is not None:
- exit_code = run_state.env_process.returncode
- if exit_code == 0:
- run_state.status = "completed"
- else:
- run_state.status = "failed"
- run_state.error_message = f"Environment process exited with code {exit_code}"
- _stop_training_run(run_state)
- break
-
- if run_state.trainer_process and run_state.trainer_process.poll() is not None:
- exit_code = run_state.trainer_process.returncode
- if exit_code == 0:
- run_state.status = "completed"
- else:
- run_state.status = "failed"
- run_state.error_message = f"Trainer process exited with code {exit_code}"
- _stop_training_run(run_state)
- break
-
- if run_state.api_process and run_state.api_process.poll() is not None:
- run_state.status = "failed"
- run_state.error_message = "API server exited unexpectedly"
- _stop_training_run(run_state)
- break
-
-
-def _stop_training_run(run_state: RunState):
- """Stop all processes for a training run."""
- # Stop in reverse order: env -> trainer -> api
- if run_state.env_process and run_state.env_process.poll() is None:
- logger.info("[%s] Stopping environment process...", run_state.run_id)
- run_state.env_process.terminate()
- try:
- run_state.env_process.wait(timeout=10)
- except subprocess.TimeoutExpired:
- run_state.env_process.kill()
-
- if run_state.trainer_process and run_state.trainer_process.poll() is None:
- logger.info("[%s] Stopping trainer process...", run_state.run_id)
- run_state.trainer_process.terminate()
- try:
- run_state.trainer_process.wait(timeout=10)
- except subprocess.TimeoutExpired:
- run_state.trainer_process.kill()
-
- if run_state.api_process and run_state.api_process.poll() is None:
- logger.info("[%s] Stopping API server...", run_state.run_id)
- run_state.api_process.terminate()
- try:
- run_state.api_process.wait(timeout=10)
- except subprocess.TimeoutExpired:
- run_state.api_process.kill()
-
- if run_state.status == "running":
- run_state.status = "stopped"
-
- # Close log file handles that were opened for subprocess stdout.
- for attr in ("env_log_file", "trainer_log_file", "api_log_file"):
- fh = getattr(run_state, attr, None)
- if fh is not None:
- try:
- fh.close()
- except Exception:
- pass
- setattr(run_state, attr, None)
-
-
-# ============================================================================
-# Environment Discovery Tools
-# ============================================================================
-
-async def rl_list_environments() -> str:
- """
- List all available RL environments.
-
- Scans tinker-atropos/tinker_atropos/environments/ for Python files
- containing classes that inherit from BaseEnv.
-
- Returns information about each environment including:
- - name: Environment identifier
- - class_name: Python class name
- - file_path: Path to the environment file
- - description: Brief description if available
-
- TIP: To create or modify RL environments:
- 1. Use terminal/file tools to inspect existing environments
- 2. Study how they load datasets, define verifiers, and structure rewards
- 3. Inspect HuggingFace datasets to understand data formats
- 4. Copy an existing environment as a template
-
- Returns:
- JSON string with list of environments
- """
- _initialize_environments()
-
- response = {
- "environments": [
- {
- "name": env.name,
- "class_name": env.class_name,
- "file_path": env.file_path,
- "description": env.description,
- }
- for env in _environments
- ],
- "count": len(_environments),
- "tips": [
- "Use rl_select_environment(name) to select an environment",
- "Read the file_path with file tools to understand how each environment works",
- "Look for load_dataset(), score_answer(), get_next_item() methods",
- ]
- }
-
- return json.dumps(response, indent=2)
-
-
-async def rl_select_environment(name: str) -> str:
- """
- Select an RL environment for training.
-
- This loads the environment's configuration fields into memory.
- After selecting, use rl_get_current_config() to see all configurable options
- and rl_edit_config() to modify specific fields.
-
- Args:
- name: Name of the environment to select (from rl_list_environments)
-
- Returns:
- JSON string with selection result, file path, and configurable field count
-
- TIP: Read the returned file_path to understand how the environment works.
- """
- global _current_env, _current_config
-
- _initialize_environments()
-
- env_info = None
- for env in _environments:
- if env.name == name:
- env_info = env
- break
-
- if not env_info:
- return json.dumps({
- "error": f"Environment '{name}' not found",
- "available": [e.name for e in _environments],
- }, indent=2)
-
- _current_env = name
-
- # Dynamically discover config fields
- config_fields = _get_env_config_fields(env_info.file_path)
- _env_config_cache[name] = config_fields
-
- # Initialize current config with defaults for non-locked fields
- _current_config = {}
- for field_name, field_info in config_fields.items():
- if not field_info.get("locked", False):
- _current_config[field_name] = field_info.get("default")
-
- # Auto-set wandb_name to "{env_name}-DATETIME" to avoid overlaps
- timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
- _current_config["wandb_name"] = f"{name}-{timestamp}"
-
- return json.dumps({
- "message": f"Selected environment: {name}",
- "environment": name,
- "file_path": env_info.file_path,
- }, indent=2)
-
-
-# ============================================================================
-# Configuration Tools
-# ============================================================================
-
-async def rl_get_current_config() -> str:
- """
- Get the current environment configuration.
-
- Returns all configurable fields for the selected environment.
- Each environment may have different configuration options.
-
- Fields are divided into:
- - configurable_fields: Can be changed with rl_edit_config()
- - locked_fields: Infrastructure settings that cannot be changed
-
- Returns:
- JSON string with configurable and locked fields
- """
- if not _current_env:
- return json.dumps({
- "error": "No environment selected. Use rl_select_environment(name) first.",
- }, indent=2)
-
- config_fields = _env_config_cache.get(_current_env, {})
-
- configurable = []
- locked = []
-
- for field_name, field_info in config_fields.items():
- field_data = {
- "name": field_name,
- "type": field_info.get("type", "unknown"),
- "default": field_info.get("default"),
- "description": field_info.get("description", ""),
- "current_value": _current_config.get(field_name, field_info.get("default")),
- }
-
- if field_info.get("locked", False):
- field_data["locked_value"] = LOCKED_FIELDS.get("env", {}).get(field_name)
- locked.append(field_data)
- else:
- configurable.append(field_data)
-
- return json.dumps({
- "environment": _current_env,
- "configurable_fields": configurable,
- "locked_fields": locked,
- "tip": "Use rl_edit_config(field, value) to change any configurable field.",
- }, indent=2)
-
-
-async def rl_edit_config(field: str, value: Any) -> str:
- """
- Update a configuration field.
-
- Use rl_get_current_config() first to see available fields for the
- selected environment. Each environment has different options.
-
- Locked fields (infrastructure settings) cannot be changed.
-
- Args:
- field: Name of the field to update (from rl_get_current_config)
- value: New value for the field
-
- Returns:
- JSON string with updated config or error message
- """
- if not _current_env:
- return json.dumps({
- "error": "No environment selected. Use rl_select_environment(name) first.",
- }, indent=2)
-
- config_fields = _env_config_cache.get(_current_env, {})
-
- if field not in config_fields:
- return json.dumps({
- "error": f"Unknown field '{field}'",
- "available_fields": list(config_fields.keys()),
- }, indent=2)
-
- field_info = config_fields[field]
- if field_info.get("locked", False):
- return json.dumps({
- "error": f"Field '{field}' is locked and cannot be changed",
- "locked_value": LOCKED_FIELDS.get("env", {}).get(field),
- }, indent=2)
-
- _current_config[field] = value
-
- return json.dumps({
- "message": f"Updated {field} = {value}",
- "field": field,
- "value": value,
- "config": _current_config,
- }, indent=2)
-
-
-# ============================================================================
-# Training Management Tools
-# ============================================================================
-
-async def rl_start_training() -> str:
- """
- Start a new RL training run with the current environment and config.
-
- Requires an environment to be selected first using rl_select_environment().
- Use rl_edit_config() to adjust configuration before starting.
-
- This spawns three processes:
- 1. run-api (Atropos trajectory API)
- 2. launch_training.py (Tinker trainer + inference server)
- 3. environment.py serve (the selected environment)
-
- WARNING: Training runs take hours. Use rl_check_status() to monitor
- progress (recommended: check every 30 minutes at most).
-
- Returns:
- JSON string with run_id and initial status
- """
- if not _current_env:
- return json.dumps({
- "error": "No environment selected. Use rl_select_environment(name) first.",
- }, indent=2)
-
- # Check API keys
- if not os.getenv("TINKER_API_KEY"):
- return json.dumps({
- "error": "TINKER_API_KEY not set. Add it to ~/.hermes/.env",
- }, indent=2)
-
- # Find environment file
- env_info = None
- for env in _environments:
- if env.name == _current_env:
- env_info = env
- break
-
- if not env_info or not Path(env_info.file_path).exists():
- return json.dumps({
- "error": f"Environment file not found for '{_current_env}'",
- }, indent=2)
-
- # Generate run ID
- run_id = str(uuid.uuid4())[:8]
-
- # Create config YAML
- CONFIGS_DIR.mkdir(exist_ok=True)
- config_path = CONFIGS_DIR / f"run_{run_id}.yaml"
-
- # Start with locked config as base
- import copy
- run_config = copy.deepcopy(LOCKED_FIELDS)
-
- if "env" not in run_config:
- run_config["env"] = {}
-
- # Apply configurable fields
- for field_name, value in _current_config.items():
- if value is not None and value != "":
- run_config["env"][field_name] = value
-
- # Set WandB settings
- wandb_project = _current_config.get("wandb_project", "atropos-tinker")
- if "tinker" not in run_config:
- run_config["tinker"] = {}
- run_config["tinker"]["wandb_project"] = wandb_project
- run_config["tinker"]["wandb_run_name"] = f"{_current_env}-{run_id}"
-
- if "wandb_name" in _current_config and _current_config["wandb_name"]:
- run_config["env"]["wandb_name"] = _current_config["wandb_name"]
-
- with open(config_path, "w", encoding="utf-8") as f:
- yaml.dump(run_config, f, default_flow_style=False)
-
- # Create run state
- run_state = RunState(
- run_id=run_id,
- environment=_current_env,
- config=_current_config.copy(),
- status="starting",
- wandb_project=wandb_project,
- wandb_run_name=f"{_current_env}-{run_id}",
- )
-
- _active_runs[run_id] = run_state
-
- # Start training in background
- asyncio.create_task(_spawn_training_run(run_state, config_path))
-
- return json.dumps({
- "run_id": run_id,
- "status": "starting",
- "environment": _current_env,
- "config": _current_config,
- "wandb_project": wandb_project,
- "wandb_run_name": f"{_current_env}-{run_id}",
- "config_path": str(config_path),
- "logs": {
- "api": str(LOGS_DIR / f"api_{run_id}.log"),
- "trainer": str(LOGS_DIR / f"trainer_{run_id}.log"),
- "env": str(LOGS_DIR / f"env_{run_id}.log"),
- },
- "message": "Training starting. Use rl_check_status(run_id) to monitor (recommended: every 30 minutes).",
- }, indent=2)
-
-
-async def rl_check_status(run_id: str) -> str:
- """
- Get status and metrics for a training run.
-
- RATE LIMITED: For long-running training, this function enforces a
- minimum 30-minute interval between checks for the same run_id.
-
- Args:
- run_id: The run ID returned by rl_start_training()
-
- Returns:
- JSON string with run status and metrics
- """
- # Check rate limiting
- now = time.time()
- if run_id in _last_status_check:
- elapsed = now - _last_status_check[run_id]
- if elapsed < MIN_STATUS_CHECK_INTERVAL:
- remaining = MIN_STATUS_CHECK_INTERVAL - elapsed
- return json.dumps({
- "rate_limited": True,
- "run_id": run_id,
- "message": f"Rate limited. Next check available in {remaining/60:.0f} minutes.",
- "next_check_in_seconds": remaining,
- }, indent=2)
-
- _last_status_check[run_id] = now
-
- if run_id not in _active_runs:
- return json.dumps({
- "error": f"Run '{run_id}' not found",
- "active_runs": list(_active_runs.keys()),
- }, indent=2)
-
- run_state = _active_runs[run_id]
-
- # Check process status
- processes = {
- "api": run_state.api_process.poll() if run_state.api_process else None,
- "trainer": run_state.trainer_process.poll() if run_state.trainer_process else None,
- "env": run_state.env_process.poll() if run_state.env_process else None,
- }
-
- running_time = time.time() - run_state.start_time if run_state.start_time else 0
-
- result = {
- "run_id": run_id,
- "status": run_state.status,
- "environment": run_state.environment,
- "running_time_minutes": running_time / 60,
- "processes": {
- name: "running" if code is None else f"exited ({code})"
- for name, code in processes.items()
- },
- "wandb_project": run_state.wandb_project,
- "wandb_run_name": run_state.wandb_run_name,
- "logs": {
- "api": str(LOGS_DIR / f"api_{run_id}.log"),
- "trainer": str(LOGS_DIR / f"trainer_{run_id}.log"),
- "env": str(LOGS_DIR / f"env_{run_id}.log"),
- },
- }
-
- if run_state.error_message:
- result["error"] = run_state.error_message
-
- # Try to get WandB metrics if available
- try:
- import wandb
- api = wandb.Api()
- runs = api.runs(
- f"{os.getenv('WANDB_ENTITY', 'nousresearch')}/{run_state.wandb_project}",
- filters={"display_name": run_state.wandb_run_name}
- )
- if runs:
- wandb_run = runs[0]
- result["wandb_url"] = wandb_run.url
- result["metrics"] = {
- "step": wandb_run.summary.get("_step", 0),
- "reward_mean": wandb_run.summary.get("train/reward_mean"),
- "percent_correct": wandb_run.summary.get("train/percent_correct"),
- "eval_percent_correct": wandb_run.summary.get("eval/percent_correct"),
- }
- except Exception as e:
- result["wandb_error"] = str(e)
-
- return json.dumps(result, indent=2)
-
-
-async def rl_stop_training(run_id: str) -> str:
- """
- Stop a running training job.
-
- Args:
- run_id: The run ID to stop
-
- Returns:
- JSON string with stop confirmation
- """
- if run_id not in _active_runs:
- return json.dumps({
- "error": f"Run '{run_id}' not found",
- "active_runs": list(_active_runs.keys()),
- }, indent=2)
-
- run_state = _active_runs[run_id]
-
- if run_state.status not in {"running", "starting"}:
- return json.dumps({
- "message": f"Run '{run_id}' is not running (status: {run_state.status})",
- }, indent=2)
-
- _stop_training_run(run_state)
-
- return json.dumps({
- "message": f"Stopped training run '{run_id}'",
- "run_id": run_id,
- "status": run_state.status,
- }, indent=2)
-
-
-async def rl_get_results(run_id: str) -> str:
- """
- Get final results and metrics for a training run.
-
- Args:
- run_id: The run ID to get results for
-
- Returns:
- JSON string with final results
- """
- if run_id not in _active_runs:
- return json.dumps({
- "error": f"Run '{run_id}' not found",
- }, indent=2)
-
- run_state = _active_runs[run_id]
-
- result = {
- "run_id": run_id,
- "status": run_state.status,
- "environment": run_state.environment,
- "wandb_project": run_state.wandb_project,
- "wandb_run_name": run_state.wandb_run_name,
- }
-
- # Get WandB metrics
- try:
- import wandb
- api = wandb.Api()
- runs = api.runs(
- f"{os.getenv('WANDB_ENTITY', 'nousresearch')}/{run_state.wandb_project}",
- filters={"display_name": run_state.wandb_run_name}
- )
- if runs:
- wandb_run = runs[0]
- result["wandb_url"] = wandb_run.url
- result["final_metrics"] = dict(wandb_run.summary)
- result["history"] = [dict(row) for row in wandb_run.history(samples=10)]
- except Exception as e:
- result["wandb_error"] = str(e)
-
- return json.dumps(result, indent=2)
-
-
-async def rl_list_runs() -> str:
- """
- List all training runs (active and completed).
-
- Returns:
- JSON string with list of runs and their status
- """
- runs = []
- for run_id, run_state in _active_runs.items():
- runs.append({
- "run_id": run_id,
- "environment": run_state.environment,
- "status": run_state.status,
- "wandb_run_name": run_state.wandb_run_name,
- })
-
- return json.dumps({
- "runs": runs,
- "count": len(runs),
- }, indent=2)
-
-
-# ============================================================================
-# Inference Testing (via Atropos `process` mode with OpenRouter)
-# ============================================================================
-
-# Test models at different scales for robustness testing
-# These are cheap, capable models on OpenRouter for testing parsing/scoring
-TEST_MODELS = [
- {"id": "qwen/qwen3-8b", "name": "Qwen3 8B", "scale": "small"},
- {"id": "z-ai/glm-4.7-flash", "name": "GLM-4.7 Flash", "scale": "medium"},
- {"id": "minimax/minimax-m2.7", "name": "MiniMax M2.7", "scale": "large"},
-]
-
-# Default test parameters - quick but representative
-DEFAULT_NUM_STEPS = 3 # Number of steps (items) to test
-DEFAULT_GROUP_SIZE = 16 # Completions per item (like training)
-
-
-async def rl_test_inference(
- num_steps: int = DEFAULT_NUM_STEPS,
- group_size: int = DEFAULT_GROUP_SIZE,
- models: Optional[List[str]] = None,
-) -> str:
- """
- Quick inference test for any environment using Atropos's `process` mode.
-
- Runs a few steps of inference + scoring to validate:
- - Environment loads correctly
- - Prompt construction works
- - Inference parsing is robust (tested with multiple model scales)
- - Verifier/scoring logic works
-
- Default: 3 steps × 16 completions = 48 total rollouts per model.
- Tests 3 models = 144 total rollouts. Quick sanity check.
-
- Test models (varying intelligence levels for robustness):
- - qwen/qwen3-8b (small)
- - zhipu-ai/glm-4-flash (medium)
- - minimax/minimax-m1 (large)
-
- Args:
- num_steps: Steps to run (default: 3, max recommended for testing)
- group_size: Completions per step (default: 16, like training)
- models: Optional model IDs to test. If None, uses all 3 test models.
-
- Returns:
- JSON with results per model: steps_tested, accuracy, scores
- """
- if not _current_env:
- return json.dumps({
- "error": "No environment selected. Use rl_select_environment(name) first.",
- }, indent=2)
-
- api_key = os.getenv("OPENROUTER_API_KEY")
- if not api_key:
- return json.dumps({
- "error": "OPENROUTER_API_KEY not set. Required for inference testing.",
- }, indent=2)
-
- # Find environment info
- env_info = None
- for env in _environments:
- if env.name == _current_env:
- env_info = env
- break
-
- if not env_info:
- return json.dumps({
- "error": f"Environment '{_current_env}' not found",
- }, indent=2)
-
- # Determine which models to test
- if models:
- test_models = [m for m in TEST_MODELS if m["id"] in models]
- if not test_models:
- test_models = [{"id": m, "name": m, "scale": "custom"} for m in models]
- else:
- test_models = TEST_MODELS
-
- # Calculate total rollouts for logging
- total_rollouts_per_model = num_steps * group_size
- total_rollouts = total_rollouts_per_model * len(test_models)
-
- results = {
- "environment": _current_env,
- "environment_file": env_info.file_path,
- "test_config": {
- "num_steps": num_steps,
- "group_size": group_size,
- "rollouts_per_model": total_rollouts_per_model,
- "total_rollouts": total_rollouts,
- },
- "models_tested": [],
- }
-
- # Create output directory for test results
- _ensure_logs_dir()
- test_output_dir = LOGS_DIR / "inference_tests"
- test_output_dir.mkdir(exist_ok=True)
-
- for model_info in test_models:
- model_id = model_info["id"]
- model_safe_name = model_id.replace("/", "_")
-
- print(f"\n{'='*60}")
- print(f"Testing with {model_info['name']} ({model_id})")
- print(f"{'='*60}")
-
- # Output file for this test run
- output_file = test_output_dir / f"test_{_current_env}_{model_safe_name}.jsonl"
-
- # Generate unique run ID for wandb
- test_run_id = str(uuid.uuid4())[:8]
- wandb_run_name = f"test_inference_RSIAgent_{_current_env}_{test_run_id}"
-
- # Build the process command using Atropos's built-in CLI
- # This runs the environment's actual code with OpenRouter as the inference backend
- # We pass our locked settings + test-specific overrides via CLI args
- cmd = [
- sys.executable, env_info.file_path, "process",
- # Test-specific overrides
- "--env.total_steps", str(num_steps),
- "--env.group_size", str(group_size),
- "--env.use_wandb", "true", # Enable wandb for test tracking
- "--env.wandb_name", wandb_run_name,
- "--env.data_path_to_save_groups", str(output_file),
- # Use locked settings from our config
- "--env.tokenizer_name", LOCKED_FIELDS["env"]["tokenizer_name"],
- "--env.max_token_length", str(LOCKED_FIELDS["env"]["max_token_length"]),
- "--env.max_num_workers", str(LOCKED_FIELDS["env"]["max_num_workers"]),
- "--env.max_batches_offpolicy", str(LOCKED_FIELDS["env"]["max_batches_offpolicy"]),
- # OpenRouter config for inference testing
- # IMPORTANT: Use server_type=openai for OpenRouter (not sglang)
- # sglang is only for actual training with Tinker's inference server
- "--openai.base_url", "https://openrouter.ai/api/v1",
- "--openai.api_key", api_key,
- "--openai.model_name", model_id,
- "--openai.server_type", "openai", # OpenRouter is OpenAI-compatible
- "--openai.health_check", "false", # OpenRouter doesn't have health endpoint
- ]
-
- # Debug: Print the full command
- cmd_str = " ".join(str(c) for c in cmd)
- # Hide API key in printed output
- cmd_display = cmd_str.replace(api_key, "***API_KEY***")
- print(f"Command: {cmd_display}")
- print(f"Working dir: {TINKER_ATROPOS_ROOT}")
- print(f"WandB run: {wandb_run_name}")
- print(f" {num_steps} steps × {group_size} completions = {total_rollouts_per_model} rollouts")
-
- model_results = {
- "model": model_id,
- "name": model_info["name"],
- "scale": model_info["scale"],
- "wandb_run": wandb_run_name,
- "output_file": str(output_file),
- "steps": [],
- "steps_tested": 0,
- "total_completions": 0,
- "correct_completions": 0,
- }
-
- try:
- # Run the process command with real-time output streaming
- process = await asyncio.create_subprocess_exec(
- *cmd,
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE,
- cwd=str(TINKER_ATROPOS_ROOT),
- )
-
- # Stream output in real-time while collecting for logs
- stdout_lines = []
- stderr_lines = []
- log_file = test_output_dir / f"test_{_current_env}_{model_safe_name}.log"
-
- async def read_stream(stream, lines_list, prefix=""):
- """Read stream line by line and print in real-time."""
- while True:
- line = await stream.readline()
- if not line:
- break
- decoded = line.decode().rstrip()
- lines_list.append(decoded)
- # Print progress-related lines in real-time
- if any(kw in decoded.lower() for kw in ['processing', 'group', 'step', 'progress', '%', 'completed']):
- print(f" {prefix}{decoded}")
-
- # Read both streams concurrently with timeout
- try:
- await asyncio.wait_for(
- asyncio.gather(
- read_stream(process.stdout, stdout_lines, "📊 "),
- read_stream(process.stderr, stderr_lines, "⚠️ "),
- ),
- timeout=600, # 10 minute timeout per model
- )
- except asyncio.TimeoutError:
- process.kill()
- raise
-
- await process.wait()
-
- # Combine output for logging
- stdout_text = "\n".join(stdout_lines)
- stderr_text = "\n".join(stderr_lines)
-
- # Write logs to files for inspection outside CLI
- with open(log_file, "w", encoding="utf-8") as f:
- f.write(f"Command: {cmd_display}\n")
- f.write(f"Working dir: {TINKER_ATROPOS_ROOT}\n")
- f.write(f"Return code: {process.returncode}\n")
- f.write(f"\n{'='*60}\n")
- f.write(f"STDOUT:\n{'='*60}\n")
- f.write(stdout_text or "(empty)\n")
- f.write(f"\n{'='*60}\n")
- f.write(f"STDERR:\n{'='*60}\n")
- f.write(stderr_text or "(empty)\n")
-
- print(f" Log file: {log_file}")
-
- if process.returncode != 0:
- model_results["error"] = f"Process exited with code {process.returncode}"
- model_results["stderr"] = stderr_text[-1000:]
- model_results["stdout"] = stdout_text[-1000:]
- model_results["log_file"] = str(log_file)
- print(f"\n ❌ Error: {model_results['error']}")
- # Print last few lines of stderr for debugging
- if stderr_lines:
- print(" Last errors:")
- for line in stderr_lines[-5:]:
- print(f" {line}")
- else:
- print("\n ✅ Process completed successfully")
- print(f" Output file: {output_file}")
- print(f" File exists: {output_file.exists()}")
-
- # Parse the output JSONL file
- if output_file.exists():
- # Read JSONL file (one JSON object per line = one step)
- with open(output_file, "r", encoding="utf-8") as f:
- for line in f:
- line = line.strip()
- if not line:
- continue
- try:
- item = json.loads(line)
- scores = item.get("scores", [])
- model_results["steps_tested"] += 1
- model_results["total_completions"] += len(scores)
- correct = sum(1 for s in scores if s > 0)
- model_results["correct_completions"] += correct
-
- model_results["steps"].append({
- "step": model_results["steps_tested"],
- "completions": len(scores),
- "correct": correct,
- "scores": scores,
- })
- except json.JSONDecodeError:
- continue
-
- print(f" Completed {model_results['steps_tested']} steps")
- else:
- model_results["error"] = f"Output file not created: {output_file}"
-
- except asyncio.TimeoutError:
- model_results["error"] = "Process timed out after 10 minutes"
- print(" Timeout!")
- except Exception as e:
- model_results["error"] = str(e)
- print(f" Error: {e}")
-
- # Calculate stats
- if model_results["total_completions"] > 0:
- model_results["accuracy"] = round(
- model_results["correct_completions"] / model_results["total_completions"], 3
- )
- else:
- model_results["accuracy"] = 0
-
- if model_results["steps_tested"] > 0:
- steps_with_correct = sum(1 for s in model_results["steps"] if s.get("correct", 0) > 0)
- model_results["steps_with_correct"] = steps_with_correct
- model_results["step_success_rate"] = round(
- steps_with_correct / model_results["steps_tested"], 3
- )
- else:
- model_results["steps_with_correct"] = 0
- model_results["step_success_rate"] = 0
-
- print(f" Results: {model_results['correct_completions']}/{model_results['total_completions']} correct")
- print(f" Accuracy: {model_results['accuracy']:.1%}")
-
- results["models_tested"].append(model_results)
-
- # Overall summary
- working_models = [m for m in results["models_tested"] if m.get("steps_tested", 0) > 0]
-
- results["summary"] = {
- "steps_requested": num_steps,
- "models_tested": len(test_models),
- "models_succeeded": len(working_models),
- "best_model": max(working_models, key=lambda x: x.get("accuracy", 0))["model"] if working_models else None,
- "avg_accuracy": round(
- sum(m.get("accuracy", 0) for m in working_models) / len(working_models), 3
- ) if working_models else 0,
- "environment_working": bool(working_models),
- "output_directory": str(test_output_dir),
- }
-
- return json.dumps(results, indent=2)
-
-
-# ============================================================================
-# Requirements Check
-# ============================================================================
-
-def check_rl_python_version() -> bool:
- """
- Check if Python version meets the minimum for RL tools.
-
- tinker-atropos depends on the 'tinker' package which requires Python >= 3.11.
- """
- return sys.version_info >= (3, 11)
-
-
-def check_rl_api_keys() -> bool:
- """
- Check if required API keys and Python version are available.
-
- RL training requires:
- - Python >= 3.11 (tinker package requirement)
- - TINKER_API_KEY for the Tinker training API
- - WANDB_API_KEY for Weights & Biases metrics
- """
- if not check_rl_python_version():
- return False
- tinker_key = os.getenv("TINKER_API_KEY")
- wandb_key = os.getenv("WANDB_API_KEY")
- return bool(tinker_key) and bool(wandb_key)
-
-
-def get_missing_keys() -> List[str]:
- """
- Get list of missing requirements for RL tools (API keys and Python version).
- """
- missing = []
- if not check_rl_python_version():
- missing.append(f"Python >= 3.11 (current: {sys.version_info.major}.{sys.version_info.minor})")
- if not os.getenv("TINKER_API_KEY"):
- missing.append("TINKER_API_KEY")
- if not os.getenv("WANDB_API_KEY"):
- missing.append("WANDB_API_KEY")
- return missing
-
-
-# ---------------------------------------------------------------------------
-# Schemas + Registry
-# ---------------------------------------------------------------------------
-from tools.registry import registry
-
-RL_LIST_ENVIRONMENTS_SCHEMA = {"name": "rl_list_environments", "description": "List all available RL environments. Returns environment names, paths, and descriptions. TIP: Read the file_path with file tools to understand how each environment works (verifiers, data loading, rewards).", "parameters": {"type": "object", "properties": {}, "required": []}}
-RL_SELECT_ENVIRONMENT_SCHEMA = {"name": "rl_select_environment", "description": "Select an RL environment for training. Loads the environment's default configuration. After selecting, use rl_get_current_config() to see settings and rl_edit_config() to modify them.", "parameters": {"type": "object", "properties": {"name": {"type": "string", "description": "Name of the environment to select (from rl_list_environments)"}}, "required": ["name"]}}
-RL_GET_CURRENT_CONFIG_SCHEMA = {"name": "rl_get_current_config", "description": "Get the current environment configuration. Returns only fields that can be modified: group_size, max_token_length, total_steps, steps_per_eval, use_wandb, wandb_name, max_num_workers.", "parameters": {"type": "object", "properties": {}, "required": []}}
-RL_EDIT_CONFIG_SCHEMA = {"name": "rl_edit_config", "description": "Update a configuration field. Use rl_get_current_config() first to see all available fields for the selected environment. Each environment has different configurable options. Infrastructure settings (tokenizer, URLs, lora_rank, learning_rate) are locked.", "parameters": {"type": "object", "properties": {"field": {"type": "string", "description": "Name of the field to update (get available fields from rl_get_current_config)"}, "value": {"description": "New value for the field"}}, "required": ["field", "value"]}}
-RL_START_TRAINING_SCHEMA = {"name": "rl_start_training", "description": "Start a new RL training run with the current environment and config. Most training parameters (lora_rank, learning_rate, etc.) are fixed. Use rl_edit_config() to set group_size, batch_size, wandb_project before starting. WARNING: Training takes hours.", "parameters": {"type": "object", "properties": {}, "required": []}}
-RL_CHECK_STATUS_SCHEMA = {"name": "rl_check_status", "description": "Get status and metrics for a training run. RATE LIMITED: enforces 30-minute minimum between checks for the same run. Returns WandB metrics: step, state, reward_mean, loss, percent_correct.", "parameters": {"type": "object", "properties": {"run_id": {"type": "string", "description": "The run ID from rl_start_training()"}}, "required": ["run_id"]}}
-RL_STOP_TRAINING_SCHEMA = {"name": "rl_stop_training", "description": "Stop a running training job. Use if metrics look bad, training is stagnant, or you want to try different settings.", "parameters": {"type": "object", "properties": {"run_id": {"type": "string", "description": "The run ID to stop"}}, "required": ["run_id"]}}
-RL_GET_RESULTS_SCHEMA = {"name": "rl_get_results", "description": "Get final results and metrics for a completed training run. Returns final metrics and path to trained weights.", "parameters": {"type": "object", "properties": {"run_id": {"type": "string", "description": "The run ID to get results for"}}, "required": ["run_id"]}}
-RL_LIST_RUNS_SCHEMA = {"name": "rl_list_runs", "description": "List all training runs (active and completed) with their status.", "parameters": {"type": "object", "properties": {}, "required": []}}
-RL_TEST_INFERENCE_SCHEMA = {"name": "rl_test_inference", "description": "Quick inference test for any environment. Runs a few steps of inference + scoring using OpenRouter. Default: 3 steps x 16 completions = 48 rollouts per model, testing 3 models = 144 total. Tests environment loading, prompt construction, inference parsing, and verifier logic. Use BEFORE training to catch issues.", "parameters": {"type": "object", "properties": {"num_steps": {"type": "integer", "description": "Number of steps to run (default: 3, recommended max for testing)", "default": 3}, "group_size": {"type": "integer", "description": "Completions per step (default: 16, like training)", "default": 16}, "models": {"type": "array", "items": {"type": "string"}, "description": "Optional list of OpenRouter model IDs. Default: qwen/qwen3-8b, z-ai/glm-4.7-flash, minimax/minimax-m2.7"}}, "required": []}}
-
-_rl_env = ["TINKER_API_KEY", "WANDB_API_KEY"]
-
-registry.register(name="rl_list_environments", emoji="🧪", toolset="rl", schema=RL_LIST_ENVIRONMENTS_SCHEMA,
- handler=lambda args, **kw: rl_list_environments(), check_fn=check_rl_api_keys, requires_env=_rl_env, is_async=True)
-registry.register(name="rl_select_environment", emoji="🧪", toolset="rl", schema=RL_SELECT_ENVIRONMENT_SCHEMA,
- handler=lambda args, **kw: rl_select_environment(name=args.get("name", "")), check_fn=check_rl_api_keys, requires_env=_rl_env, is_async=True)
-registry.register(name="rl_get_current_config", emoji="🧪", toolset="rl", schema=RL_GET_CURRENT_CONFIG_SCHEMA,
- handler=lambda args, **kw: rl_get_current_config(), check_fn=check_rl_api_keys, requires_env=_rl_env, is_async=True)
-registry.register(name="rl_edit_config", emoji="🧪", toolset="rl", schema=RL_EDIT_CONFIG_SCHEMA,
- handler=lambda args, **kw: rl_edit_config(field=args.get("field", ""), value=args.get("value")), check_fn=check_rl_api_keys, requires_env=_rl_env, is_async=True)
-registry.register(name="rl_start_training", emoji="🧪", toolset="rl", schema=RL_START_TRAINING_SCHEMA,
- handler=lambda args, **kw: rl_start_training(), check_fn=check_rl_api_keys, requires_env=_rl_env, is_async=True)
-registry.register(name="rl_check_status", emoji="🧪", toolset="rl", schema=RL_CHECK_STATUS_SCHEMA,
- handler=lambda args, **kw: rl_check_status(run_id=args.get("run_id", "")), check_fn=check_rl_api_keys, requires_env=_rl_env, is_async=True)
-registry.register(name="rl_stop_training", emoji="🧪", toolset="rl", schema=RL_STOP_TRAINING_SCHEMA,
- handler=lambda args, **kw: rl_stop_training(run_id=args.get("run_id", "")), check_fn=check_rl_api_keys, requires_env=_rl_env, is_async=True)
-registry.register(name="rl_get_results", emoji="🧪", toolset="rl", schema=RL_GET_RESULTS_SCHEMA,
- handler=lambda args, **kw: rl_get_results(run_id=args.get("run_id", "")), check_fn=check_rl_api_keys, requires_env=_rl_env, is_async=True)
-registry.register(name="rl_list_runs", emoji="🧪", toolset="rl", schema=RL_LIST_RUNS_SCHEMA,
- handler=lambda args, **kw: rl_list_runs(), check_fn=check_rl_api_keys, requires_env=_rl_env, is_async=True)
-registry.register(name="rl_test_inference", emoji="🧪", toolset="rl", schema=RL_TEST_INFERENCE_SCHEMA,
- handler=lambda args, **kw: rl_test_inference(num_steps=args.get("num_steps", 3), group_size=args.get("group_size", 16), models=args.get("models")),
- check_fn=check_rl_api_keys, requires_env=_rl_env, is_async=True)
diff --git a/toolsets.py b/toolsets.py
index c664136c52a..8ec45f11a2f 100644
--- a/toolsets.py
+++ b/toolsets.py
@@ -169,18 +169,7 @@ TOOLSETS = {
"tools": ["send_message"],
"includes": []
},
-
- "rl": {
- "description": "RL training tools for running reinforcement learning on Tinker-Atropos",
- "tools": [
- "rl_list_environments", "rl_select_environment",
- "rl_get_current_config", "rl_edit_config",
- "rl_start_training", "rl_check_status",
- "rl_stop_training", "rl_get_results",
- "rl_list_runs", "rl_test_inference"
- ],
- "includes": []
- },
+
"file": {
"description": "File manipulation tools: read, write, patch (with fuzzy matching), and search (content + files)",
@@ -390,7 +379,7 @@ TOOLSETS = {
# Mirrors hermes-cli so cron's "default" toolset is the same set of
# core tools users see interactively — then `hermes tools` filters
# them down per the platform config. _DEFAULT_OFF_TOOLSETS (moa,
- # homeassistant, rl) are excluded by _get_platform_tools() unless
+ # homeassistant) are excluded by _get_platform_tools() unless
# the user explicitly enables them.
"description": "Default cron toolset - same core tools as hermes-cli; gated by `hermes tools`",
"tools": _HERMES_CORE_TOOLS,
diff --git a/uv.lock b/uv.lock
index a519cc2b194..72cef3b0cdd 100644
--- a/uv.lock
+++ b/uv.lock
@@ -301,22 +301,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/72/9e/c394b4e2104766fb28a1e44e3ed36e4c7773b4d05c868e482be99d5635c9/alibabacloud_tea_util-0.3.14-py3-none-any.whl", hash = "sha256:10d3e5c340d8f7ec69dd27345eb2fc5a1dab07875742525edf07bbe86db93bfe", size = 6697, upload-time = "2025-11-19T06:01:07.355Z" },
]
-[[package]]
-name = "altair"
-version = "6.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "jinja2", marker = "python_full_version >= '3.12'" },
- { name = "jsonschema", marker = "python_full_version >= '3.12'" },
- { name = "narwhals", marker = "python_full_version >= '3.12'" },
- { name = "packaging", marker = "python_full_version >= '3.12'" },
- { name = "typing-extensions", marker = "python_full_version >= '3.12' and python_full_version < '3.15'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f7/c0/184a89bd5feba14ff3c41cfaf1dd8a82c05f5ceedbc92145e17042eb08a4/altair-6.0.0.tar.gz", hash = "sha256:614bf5ecbe2337347b590afb111929aa9c16c9527c4887d96c9bc7f6640756b4", size = 763834, upload-time = "2025-11-12T08:59:11.519Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/db/33/ef2f2409450ef6daa61459d5de5c08128e7d3edb773fefd0a324d1310238/altair-6.0.0-py3-none-any.whl", hash = "sha256:09ae95b53d5fe5b16987dccc785a7af8588f2dca50de1e7a156efa8a461515f8", size = 795410, upload-time = "2025-11-12T08:59:09.804Z" },
-]
-
[[package]]
name = "annotated-doc"
version = "0.0.4"
@@ -354,15 +338,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/63/5f/67db29c6e5d16c8c9c4652d3efb934d89cb750cad201539141781d8eae14/anthropic-0.86.0-py3-none-any.whl", hash = "sha256:9d2bbd339446acce98858c5627d33056efe01f70435b22b63546fe7edae0cd57", size = 469400, upload-time = "2026-03-18T18:43:06.526Z" },
]
-[[package]]
-name = "antlr4-python3-runtime"
-version = "4.13.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/33/5f/2cdf6f7aca3b20d3f316e9f505292e1f256a32089bd702034c29ebde6242/antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916", size = 117467, upload-time = "2024-08-03T19:00:12.757Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/89/03/a851e84fcbb85214dc637b6378121ef9a0dd61b4c65264675d8a5c9b1ae7/antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8", size = 144462, upload-time = "2024-08-03T19:00:11.134Z" },
-]
-
[[package]]
name = "anyio"
version = "4.12.1"
@@ -436,34 +411,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" },
]
-[[package]]
-name = "atroposlib"
-version = "0.4.0"
-source = { git = "https://github.com/NousResearch/atropos.git?rev=c20c85256e5a45ad31edf8b7276e9c5ee1995a30#c20c85256e5a45ad31edf8b7276e9c5ee1995a30" }
-dependencies = [
- { name = "aiofiles" },
- { name = "aiohttp" },
- { name = "datasets" },
- { name = "fastapi" },
- { name = "gymnasium" },
- { name = "hf-transfer" },
- { name = "jinja2" },
- { name = "jsonlines" },
- { name = "markdown" },
- { name = "math-verify" },
- { name = "nltk" },
- { name = "numpy" },
- { name = "openai" },
- { name = "polars" },
- { name = "pydantic-cli" },
- { name = "rich" },
- { name = "tenacity" },
- { name = "tqdm" },
- { name = "transformers" },
- { name = "uvicorn", extra = ["standard"] },
- { name = "wandb" },
-]
-
[[package]]
name = "attrs"
version = "25.4.0"
@@ -562,15 +509,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/45/ec96b29162a402fc4c1c5512d114d7b3787b9d1c2ec241d9568b4816ee23/base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2", size = 5621, upload-time = "2021-10-30T22:12:16.658Z" },
]
-[[package]]
-name = "blinker"
-version = "1.9.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
-]
-
[[package]]
name = "boto3"
version = "1.42.89"
@@ -599,15 +537,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/91/f1/90a7b8eda38b7c3a65ca7ee0075bdf310b6b471cb1b95fab6e8994323a50/botocore-1.42.89-py3-none-any.whl", hash = "sha256:d9b786c8d9db6473063b4cc5be0ba7e6a381082307bd6afb69d4216f9fa95f35", size = 14887287, upload-time = "2026-04-13T19:35:56.677Z" },
]
-[[package]]
-name = "cachetools"
-version = "5.5.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" },
-]
-
[[package]]
name = "cbor2"
version = "5.8.0"
@@ -809,15 +738,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
-[[package]]
-name = "cloudpickle"
-version = "3.1.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" },
-]
-
[[package]]
name = "colorama"
version = "0.4.6"
@@ -827,88 +747,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
-[[package]]
-name = "contourpy"
-version = "1.3.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "numpy", marker = "python_full_version >= '3.12'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
- { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
- { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
- { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
- { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
- { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
- { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
- { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
- { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
- { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
- { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
- { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
- { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
- { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
- { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
- { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
- { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
- { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
- { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
- { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
- { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
- { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
- { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
- { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
- { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
- { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
- { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
- { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
- { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
- { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
- { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
- { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
- { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
- { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
- { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
- { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
- { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
- { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
- { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
- { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
- { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
- { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
- { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
- { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
- { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
- { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
- { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
- { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
- { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
- { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
- { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
- { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
- { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
- { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
- { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
- { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
- { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
- { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
- { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
- { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
- { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
- { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
- { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
- { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
- { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
- { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
- { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
- { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
- { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
- { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
- { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
-]
-
[[package]]
name = "croniter"
version = "6.0.0"
@@ -1018,15 +856,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/5c/9fa0ad6462b62efd0fb5ac1100eee47bc96ecc198ff4e237c731e5473616/ctranslate2-4.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:dfb7657bdb7b8211c8f9ecb6f3b70bc0db0e0384d01a8b1808cb66fe7199df59", size = 19123451, upload-time = "2026-02-04T06:12:24.115Z" },
]
-[[package]]
-name = "cycler"
-version = "0.12.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
-]
-
[[package]]
name = "darabonba-core"
version = "1.0.5"
@@ -1040,31 +869,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/66/d3/a7daaee544c904548e665829b51a9fa2572acb82c73ad787a8ff90273002/darabonba_core-1.0.5-py3-none-any.whl", hash = "sha256:671ab8dbc4edc2a8f88013da71646839bb8914f1259efc069353243ef52ea27c", size = 24580, upload-time = "2025-12-12T07:53:59.494Z" },
]
-[[package]]
-name = "datasets"
-version = "4.8.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "dill" },
- { name = "filelock" },
- { name = "fsspec", extra = ["http"] },
- { name = "httpx" },
- { name = "huggingface-hub" },
- { name = "multiprocess" },
- { name = "numpy" },
- { name = "packaging" },
- { name = "pandas" },
- { name = "pyarrow" },
- { name = "pyyaml" },
- { name = "requests" },
- { name = "tqdm" },
- { name = "xxhash" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/22/22/73e46ac7a8c25e7ef0b3bd6f10da3465021d90219a32eb0b4d2afea4c56e/datasets-4.8.4.tar.gz", hash = "sha256:a1429ed853275ce7943a01c6d2e25475b4501eb758934362106a280470df3a52", size = 604382, upload-time = "2026-03-23T14:21:17.987Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b0/e5/247d094108e42ac26363ab8dc57f168840cf7c05774b40ffeb0d78868fcc/datasets-4.8.4-py3-none-any.whl", hash = "sha256:cdc8bee4698e549d78bf1fed6aea2eebc760b22b084f07e6fc020c6577a6ce6d", size = 526991, upload-time = "2026-03-23T14:21:15.89Z" },
-]
-
[[package]]
name = "davey"
version = "0.1.4"
@@ -1290,15 +1094,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
]
-[[package]]
-name = "dill"
-version = "0.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" },
-]
-
[[package]]
name = "dingtalk-stream"
version = "0.24.3"
@@ -1436,15 +1231,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/48/265c2935467ac1dbcb7c5b54cd8a2f579cbb263db6bfc0e0c8fe4bc79c02/fal_client-0.13.1-py3-none-any.whl", hash = "sha256:967a01f3a4112d485a30f8f3a0e678c6ff5b919eb9c5d480315cfc30a79fc037", size = 19265, upload-time = "2026-02-20T07:21:28.143Z" },
]
-[[package]]
-name = "farama-notifications"
-version = "0.0.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2e/2c/8384832b7a6b1fd6ba95bbdcae26e7137bb3eedc955c42fd5cdcc086cfbf/Farama-Notifications-0.0.4.tar.gz", hash = "sha256:13fceff2d14314cf80703c8266462ebf3733c7d165336eee998fc58e545efd18", size = 2131, upload-time = "2023-02-27T18:28:41.047Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/05/2c/ffc08c54c05cdce6fbed2aeebc46348dbe180c6d2c541c7af7ba0aa5f5f8/Farama_Notifications-0.0.4-py3-none-any.whl", hash = "sha256:14de931035a41961f7c056361dc7f980762a143d05791ef5794a751a2caf05ae", size = 2511, upload-time = "2023-02-27T18:28:39.447Z" },
-]
-
[[package]]
name = "fastapi"
version = "0.133.1"
@@ -1477,58 +1263,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/05/99/49ee85903dee060d9f08297b4a342e5e0bcfca2f027a07b4ee0a38ab13f9/faster_whisper-1.2.1-py3-none-any.whl", hash = "sha256:79a66ad50688c0b794dd501dc340a736992a6342f7f95e5811be60b5224a26a7", size = 1118909, upload-time = "2025-10-31T11:35:47.794Z" },
]
-[[package]]
-name = "fastuuid"
-version = "0.14.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" },
- { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" },
- { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" },
- { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" },
- { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" },
- { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" },
- { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" },
- { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" },
- { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" },
- { url = "https://files.pythonhosted.org/packages/9e/8c/a04d486ca55b5abb7eaa65b39df8d891b7b1635b22db2163734dc273579a/fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", size = 154804, upload-time = "2025-10-19T22:24:15.615Z" },
- { url = "https://files.pythonhosted.org/packages/9c/b2/2d40bf00820de94b9280366a122cbaa60090c8cf59e89ac3938cf5d75895/fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", size = 156099, upload-time = "2025-10-19T22:24:31.646Z" },
- { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" },
- { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" },
- { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" },
- { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" },
- { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" },
- { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" },
- { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" },
- { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" },
- { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" },
- { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" },
- { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" },
- { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" },
- { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" },
- { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" },
- { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" },
- { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" },
- { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" },
- { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" },
- { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" },
- { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" },
- { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" },
- { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" },
- { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" },
- { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" },
- { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" },
- { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" },
- { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" },
- { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" },
- { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" },
- { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" },
- { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" },
- { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" },
- { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" },
-]
-
[[package]]
name = "filelock"
version = "3.24.3"
@@ -1576,55 +1310,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" },
]
-[[package]]
-name = "fonttools"
-version = "4.62.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" },
- { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" },
- { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" },
- { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" },
- { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" },
- { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" },
- { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" },
- { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" },
- { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" },
- { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" },
- { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" },
- { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" },
- { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" },
- { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" },
- { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" },
- { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" },
- { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" },
- { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" },
- { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" },
- { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" },
- { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" },
- { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" },
- { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" },
- { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" },
- { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" },
- { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" },
- { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" },
- { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" },
- { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" },
- { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" },
- { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" },
- { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" },
- { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" },
- { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" },
- { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" },
- { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" },
- { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" },
- { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" },
- { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" },
- { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" },
- { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" },
-]
-
[[package]]
name = "frozenlist"
version = "1.8.0"
@@ -1739,35 +1424,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" },
]
-[package.optional-dependencies]
-http = [
- { name = "aiohttp" },
-]
-
-[[package]]
-name = "gitdb"
-version = "4.0.12"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "smmap" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" },
-]
-
-[[package]]
-name = "gitpython"
-version = "3.1.46"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "gitdb" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" },
-]
-
[[package]]
name = "google-api-core"
version = "2.30.3"
@@ -1851,53 +1507,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" },
]
-[[package]]
-name = "greenlet"
-version = "3.3.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" },
- { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" },
- { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" },
- { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" },
- { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" },
- { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" },
- { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" },
- { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" },
- { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" },
- { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" },
- { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" },
- { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" },
- { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" },
- { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" },
- { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" },
- { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" },
- { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" },
- { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" },
- { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" },
- { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" },
- { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" },
- { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" },
- { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" },
- { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" },
- { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" },
- { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" },
- { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" },
- { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" },
- { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" },
- { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" },
- { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" },
- { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" },
- { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" },
- { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" },
- { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" },
- { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" },
- { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" },
- { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" },
- { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" },
-]
-
[[package]]
name = "grpclib"
version = "0.4.9"
@@ -1911,21 +1520,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/90/b0cbbd9efcc82816c58f31a34963071aa19fb792a212a5d9caf8e0fc3097/grpclib-0.4.9-py3-none-any.whl", hash = "sha256:7762ec1c8ed94dfad597475152dd35cbd11aecaaca2f243e29702435ca24cf0e", size = 77063, upload-time = "2025-12-14T22:23:13.224Z" },
]
-[[package]]
-name = "gymnasium"
-version = "1.2.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cloudpickle" },
- { name = "farama-notifications" },
- { name = "numpy" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/76/59/653a9417d98ed3e29ef9734ba52c3495f6c6823b8d5c0c75369f25111708/gymnasium-1.2.3.tar.gz", hash = "sha256:2b2cb5b5fbbbdf3afb9f38ca952cc48aa6aa3e26561400d940747fda3ad42509", size = 829230, upload-time = "2025-12-18T16:51:10.234Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/56/d3/ea5f088e3638dbab12e5c20d6559d5b3bdaeaa1f2af74e526e6815836285/gymnasium-1.2.3-py3-none-any.whl", hash = "sha256:e6314bba8f549c7fdcc8677f7cd786b64908af6e79b57ddaa5ce1825bffb5373", size = 952113, upload-time = "2025-12-18T16:51:08.445Z" },
-]
-
[[package]]
name = "h11"
version = "0.16.0"
@@ -2084,13 +1678,6 @@ pty = [
{ name = "ptyprocess", marker = "sys_platform != 'win32'" },
{ name = "pywinpty", marker = "sys_platform == 'win32'" },
]
-rl = [
- { name = "atroposlib" },
- { name = "fastapi" },
- { name = "tinker" },
- { name = "uvicorn", extra = ["standard"] },
- { name = "wandb" },
-]
slack = [
{ name = "aiohttp" },
{ name = "slack-bolt" },
@@ -2138,9 +1725,6 @@ web = [
{ name = "fastapi" },
{ name = "uvicorn", extra = ["standard"] },
]
-yc-bench = [
- { name = "yc-bench", marker = "python_full_version >= '3.12'" },
-]
youtube = [
{ name = "youtube-transcript-api" },
]
@@ -2157,7 +1741,6 @@ requires-dist = [
{ name = "alibabacloud-dingtalk", marker = "extra == 'dingtalk'", specifier = "==2.2.42" },
{ name = "anthropic", marker = "extra == 'anthropic'", specifier = "==0.86.0" },
{ name = "asyncpg", marker = "extra == 'matrix'", specifier = "==0.31.0" },
- { name = "atroposlib", marker = "extra == 'rl'", git = "https://github.com/NousResearch/atropos.git?rev=c20c85256e5a45ad31edf8b7276e9c5ee1995a30" },
{ name = "boto3", marker = "extra == 'bedrock'", specifier = "==1.42.89" },
{ name = "croniter", specifier = "==6.0.0" },
{ name = "daytona", marker = "extra == 'daytona'", specifier = "==0.155.0" },
@@ -2168,7 +1751,6 @@ requires-dist = [
{ name = "elevenlabs", marker = "extra == 'tts-premium'", specifier = "==1.59.0" },
{ name = "exa-py", marker = "extra == 'exa'", specifier = "==2.10.2" },
{ name = "fal-client", marker = "extra == 'fal'", specifier = "==0.13.1" },
- { name = "fastapi", marker = "extra == 'rl'", specifier = "==0.133.1" },
{ name = "fastapi", marker = "extra == 'web'", specifier = "==0.133.1" },
{ name = "faster-whisper", marker = "extra == 'voice'", specifier = "==1.2.1" },
{ name = "fire", specifier = "==0.7.1" },
@@ -2240,49 +1822,13 @@ requires-dist = [
{ name = "slack-sdk", marker = "extra == 'slack'", specifier = "==3.40.1" },
{ name = "sounddevice", marker = "extra == 'voice'", specifier = "==0.5.5" },
{ name = "tenacity", specifier = "==9.1.4" },
- { name = "tinker", marker = "extra == 'rl'", git = "https://github.com/thinking-machines-lab/tinker.git?rev=30517b667f18a3dfb7ef33fb56cf686d5820ba2b" },
{ name = "ty", marker = "extra == 'dev'", specifier = "==0.0.21" },
{ name = "tzdata", marker = "sys_platform == 'win32'", specifier = "==2025.3" },
- { name = "uvicorn", extras = ["standard"], marker = "extra == 'rl'", specifier = "==0.41.0" },
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'web'", specifier = "==0.41.0" },
{ name = "vercel", marker = "extra == 'vercel'", specifier = "==0.5.7" },
- { name = "wandb", marker = "extra == 'rl'", specifier = "==0.25.1" },
- { name = "yc-bench", marker = "python_full_version >= '3.12' and extra == 'yc-bench'", git = "https://github.com/collinear-ai/yc-bench.git?rev=bfb0c88062450f46341bd9a5298903fc2e952a5c" },
{ name = "youtube-transcript-api", marker = "extra == 'youtube'", specifier = "==1.2.4" },
]
-provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "vercel", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "homeassistant", "sms", "computer-use", "acp", "bedrock", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "rl", "yc-bench", "all"]
-
-[[package]]
-name = "hf-transfer"
-version = "0.1.9"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1a/eb/8fc64f40388c29ce8ce3b2b180a089d4d6b25b1d0d232d016704cb852104/hf_transfer-0.1.9.tar.gz", hash = "sha256:035572865dab29d17e783fbf1e84cf1cb24f3fcf8f1b17db1cfc7fdf139f02bf", size = 25201, upload-time = "2025-01-07T10:05:12.947Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a4/78/0dce00208f585fae675f40033ef9a30dedfa83665d5ac79f16beb4a0a6c2/hf_transfer-0.1.9-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:6e94e8822da79573c9b6ae4d6b2f847c59a7a06c5327d7db20751b68538dc4f6", size = 1386084, upload-time = "2025-01-07T10:04:47.874Z" },
- { url = "https://files.pythonhosted.org/packages/ea/2e/3d60b1a9e9f29a2152aa66c823bf5e399ae7be3fef310ff0de86779c5d2d/hf_transfer-0.1.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ebc4ab9023414880c8b1d3c38174d1c9989eb5022d37e814fa91a3060123eb0", size = 1343558, upload-time = "2025-01-07T10:04:42.313Z" },
- { url = "https://files.pythonhosted.org/packages/fb/38/130a5ac3747f104033591bcac1c961cb1faadfdc91704f59b09c0b465ff2/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8674026f21ed369aa2a0a4b46000aca850fc44cd2b54af33a172ce5325b4fc82", size = 3726676, upload-time = "2025-01-07T10:04:11.539Z" },
- { url = "https://files.pythonhosted.org/packages/15/a1/f4e27c5ad17aac616ae0849e2aede5aae31db8267a948c6b3eeb9fd96446/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a736dfbb2c84f5a2c975478ad200c0c8bfcb58a25a35db402678fb87ce17fa4", size = 3062920, upload-time = "2025-01-07T10:04:16.297Z" },
- { url = "https://files.pythonhosted.org/packages/8d/0d/727abdfba39bc3f1132cfa4c970588c2c0bb0d82fe2d645cc10f4e2f8e0b/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:504b8427fd785dd8546d53b9fafe6e436bd7a3adf76b9dce556507650a7b4567", size = 3578681, upload-time = "2025-01-07T10:04:29.702Z" },
- { url = "https://files.pythonhosted.org/packages/50/d0/2b213eb1ea8b1252ccaf1a6c804d0aba03fea38aae4124df6a3acb70511a/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c7fc1b85f4d0f76e452765d7648c9f4bfd0aedb9ced2ae1ebfece2d8cfaf8e2", size = 3398837, upload-time = "2025-01-07T10:04:22.778Z" },
- { url = "https://files.pythonhosted.org/packages/8c/8a/79dbce9006e0bd6b74516f97451a7b7c64dbbb426df15d901dd438cfeee3/hf_transfer-0.1.9-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d991376f0eac70a60f0cbc95602aa708a6f7c8617f28b4945c1431d67b8e3c8", size = 3546986, upload-time = "2025-01-07T10:04:36.415Z" },
- { url = "https://files.pythonhosted.org/packages/a9/f7/9ac239b6ee6fe0bad130325d987a93ea58c4118e50479f0786f1733b37e8/hf_transfer-0.1.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ac4eddcd99575ed3735ed911ddf9d1697e2bd13aa3f0ad7e3904dd4863842e", size = 4071715, upload-time = "2025-01-07T10:04:53.224Z" },
- { url = "https://files.pythonhosted.org/packages/d8/a3/0ed697279f5eeb7a40f279bd783cf50e6d0b91f24120dcf66ef2cf8822b4/hf_transfer-0.1.9-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:57fd9880da1ee0f47250f735f791fab788f0aa1ee36afc49f761349869c8b4d9", size = 3388081, upload-time = "2025-01-07T10:04:57.818Z" },
- { url = "https://files.pythonhosted.org/packages/dc/eb/47e477bdf1d784f31c7540db6cc8c354b777e51a186897a7abda34517f36/hf_transfer-0.1.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:5d561f0520f493c66b016d99ceabe69c23289aa90be38dd802d2aef279f15751", size = 3658654, upload-time = "2025-01-07T10:05:03.168Z" },
- { url = "https://files.pythonhosted.org/packages/45/07/6661e43fbee09594a8a5e9bb778107d95fe38dac4c653982afe03d32bd4d/hf_transfer-0.1.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a5b366d34cd449fe9b20ef25941e6eef0460a2f74e7389f02e673e1f88ebd538", size = 3690551, upload-time = "2025-01-07T10:05:09.238Z" },
- { url = "https://files.pythonhosted.org/packages/81/f5/461d2e5f307e5048289b1168d5c642ae3bb2504e88dff1a38b92ed990a21/hf_transfer-0.1.9-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e66acf91df4a8b72f60223059df3003062a5ae111757187ed1a06750a30e911b", size = 1393046, upload-time = "2025-01-07T10:04:51.003Z" },
- { url = "https://files.pythonhosted.org/packages/41/ba/8d9fd9f1083525edfcb389c93738c802f3559cb749324090d7109c8bf4c2/hf_transfer-0.1.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:8669dbcc7a3e2e8d61d42cd24da9c50d57770bd74b445c65123291ca842a7e7a", size = 1348126, upload-time = "2025-01-07T10:04:45.712Z" },
- { url = "https://files.pythonhosted.org/packages/8e/a2/cd7885bc9959421065a6fae0fe67b6c55becdeda4e69b873e52976f9a9f0/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fd0167c4407a3bc4cdd0307e65ada2294ec04f1813d8a69a5243e379b22e9d8", size = 3728604, upload-time = "2025-01-07T10:04:14.173Z" },
- { url = "https://files.pythonhosted.org/packages/f6/2e/a072cf196edfeda3310c9a5ade0a0fdd785e6154b3ce24fc738c818da2a7/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee8b10afedcb75f71091bcc197c526a6ebf5c58bbbadb34fdeee6160f55f619f", size = 3064995, upload-time = "2025-01-07T10:04:18.663Z" },
- { url = "https://files.pythonhosted.org/packages/c2/84/aec9ef4c0fab93c1ea2b1badff38c78b4b2f86f0555b26d2051dbc920cde/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5828057e313de59300dd1abb489444bc452efe3f479d3c55b31a8f680936ba42", size = 3580908, upload-time = "2025-01-07T10:04:32.834Z" },
- { url = "https://files.pythonhosted.org/packages/29/63/b560d39651a56603d64f1a0212d0472a44cbd965db2fa62b99d99cb981bf/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc6bd19e1cc177c66bdef15ef8636ad3bde79d5a4f608c158021153b4573509d", size = 3400839, upload-time = "2025-01-07T10:04:26.122Z" },
- { url = "https://files.pythonhosted.org/packages/d6/d8/f87ea6f42456254b48915970ed98e993110521e9263472840174d32c880d/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdca9bfb89e6f8f281890cc61a8aff2d3cecaff7e1a4d275574d96ca70098557", size = 3552664, upload-time = "2025-01-07T10:04:40.123Z" },
- { url = "https://files.pythonhosted.org/packages/d6/56/1267c39b65fc8f4e2113b36297320f102718bf5799b544a6cbe22013aa1d/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89a23f58b7b7effbc047b8ca286f131b17728c99a9f972723323003ffd1bb916", size = 4073732, upload-time = "2025-01-07T10:04:55.624Z" },
- { url = "https://files.pythonhosted.org/packages/82/1a/9c748befbe3decf7cb415e34f8a0c3789a0a9c55910dea73d581e48c0ce5/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:dc7fff1345980d6c0ebb92c811d24afa4b98b3e07ed070c8e38cc91fd80478c5", size = 3390096, upload-time = "2025-01-07T10:04:59.98Z" },
- { url = "https://files.pythonhosted.org/packages/72/85/4c03da147b6b4b7cb12e074d3d44eee28604a387ed0eaf7eaaead5069c57/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1a6bd16c667ebe89a069ca163060127a794fa3a3525292c900b8c8cc47985b0d", size = 3664743, upload-time = "2025-01-07T10:05:05.416Z" },
- { url = "https://files.pythonhosted.org/packages/e7/6e/e597b04f753f1b09e6893075d53a82a30c13855cbaa791402695b01e369f/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d2fde99d502093ade3ab1b53f80da18480e9902aa960dab7f74fb1b9e5bc5746", size = 3695243, upload-time = "2025-01-07T10:05:11.411Z" },
- { url = "https://files.pythonhosted.org/packages/09/89/d4e234727a26b2546c8fb70a276cd924260d60135f2165bf8b9ed67bb9a4/hf_transfer-0.1.9-cp38-abi3-win32.whl", hash = "sha256:435cc3cdc8524ce57b074032b8fd76eed70a4224d2091232fa6a8cef8fd6803e", size = 1086605, upload-time = "2025-01-07T10:05:18.873Z" },
- { url = "https://files.pythonhosted.org/packages/a1/14/f1e15b851d1c2af5b0b1a82bf8eb10bda2da62d98180220ba6fd8879bb5b/hf_transfer-0.1.9-cp38-abi3-win_amd64.whl", hash = "sha256:16f208fc678911c37e11aa7b586bc66a37d02e636208f18b6bc53d29b5df40ad", size = 1160240, upload-time = "2025-01-07T10:05:14.324Z" },
-]
+provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "vercel", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "homeassistant", "sms", "computer-use", "acp", "bedrock", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"]
[[package]]
name = "hf-xet"
@@ -2433,9 +1979,6 @@ wheels = [
]
[package.optional-dependencies]
-http2 = [
- { name = "h2" },
-]
socks = [
{ name = "socksio" },
]
@@ -2615,27 +2158,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
]
-[[package]]
-name = "joblib"
-version = "1.5.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
-]
-
-[[package]]
-name = "jsonlines"
-version = "4.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/35/87/bcda8e46c88d0e34cad2f09ee2d0c7f5957bccdb9791b0b934ec84d84be4/jsonlines-4.0.0.tar.gz", hash = "sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74", size = 11359, upload-time = "2023-09-01T12:34:44.187Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f8/62/d9ba6323b9202dd2fe166beab8a86d29465c41a0288cbe229fac60c1ab8d/jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55", size = 8701, upload-time = "2023-09-01T12:34:42.563Z" },
-]
-
[[package]]
name = "jsonschema"
version = "4.26.0"
@@ -2663,112 +2185,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
]
-[[package]]
-name = "kiwisolver"
-version = "1.5.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" },
- { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" },
- { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" },
- { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" },
- { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" },
- { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" },
- { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" },
- { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" },
- { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" },
- { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" },
- { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" },
- { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" },
- { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" },
- { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" },
- { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" },
- { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" },
- { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" },
- { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" },
- { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" },
- { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" },
- { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" },
- { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" },
- { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" },
- { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" },
- { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" },
- { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" },
- { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" },
- { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" },
- { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" },
- { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" },
- { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" },
- { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" },
- { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" },
- { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" },
- { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" },
- { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" },
- { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" },
- { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" },
- { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" },
- { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" },
- { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" },
- { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" },
- { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" },
- { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" },
- { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" },
- { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" },
- { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" },
- { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" },
- { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" },
- { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" },
- { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" },
- { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" },
- { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" },
- { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" },
- { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" },
- { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" },
- { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" },
- { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" },
- { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" },
- { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" },
- { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" },
- { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" },
- { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" },
- { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" },
- { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" },
- { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" },
- { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" },
- { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" },
- { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" },
- { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" },
- { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" },
- { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" },
- { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" },
- { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" },
- { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" },
- { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" },
- { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" },
- { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" },
- { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" },
- { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" },
- { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" },
- { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" },
- { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" },
- { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" },
- { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" },
- { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" },
- { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" },
- { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" },
- { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" },
- { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" },
- { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" },
- { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" },
- { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" },
- { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" },
- { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" },
- { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" },
- { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" },
- { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" },
-]
-
[[package]]
name = "lark-oapi"
version = "1.5.3"
@@ -2784,42 +2200,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/ff/2ece5d735ebfa2af600a53176f2636ae47af2bf934e08effab64f0d1e047/lark_oapi-1.5.3-py3-none-any.whl", hash = "sha256:fda6b32bb38d21b6bdaae94979c600b94c7c521e985adade63a54e4b3e20cc36", size = 6993016, upload-time = "2026-01-27T08:21:49.307Z" },
]
-[[package]]
-name = "latex2sympy2-extended"
-version = "1.11.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "antlr4-python3-runtime" },
- { name = "sympy" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/30/75/456da2da05f6380ea96e6ea804ab2c03e41fc3ed80052307fe8efe6ea20e/latex2sympy2_extended-1.11.0.tar.gz", hash = "sha256:9695657c81b50abba2636638638618db59f4663ed2a4a12d62cef74a40e28fec", size = 207023, upload-time = "2026-01-10T01:43:21.319Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/61/f75cd1fa54d8434276126034aed54dd120747de9a8fa013cdd79545ccbeb/latex2sympy2_extended-1.11.0-py3-none-any.whl", hash = "sha256:aebb77d52ce269e25028e4bea89ddb14d242ba36bcf7b636496fb5fd9728d234", size = 209050, upload-time = "2026-01-10T01:43:19.458Z" },
-]
-
-[[package]]
-name = "litellm"
-version = "1.81.15"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "aiohttp", marker = "python_full_version >= '3.12'" },
- { name = "click", marker = "python_full_version >= '3.12'" },
- { name = "fastuuid", marker = "python_full_version >= '3.12'" },
- { name = "httpx", marker = "python_full_version >= '3.12'" },
- { name = "importlib-metadata", marker = "python_full_version >= '3.12'" },
- { name = "jinja2", marker = "python_full_version >= '3.12'" },
- { name = "jsonschema", marker = "python_full_version >= '3.12'" },
- { name = "openai", marker = "python_full_version >= '3.12'" },
- { name = "pydantic", marker = "python_full_version >= '3.12'" },
- { name = "python-dotenv", marker = "python_full_version >= '3.12'" },
- { name = "tiktoken", marker = "python_full_version >= '3.12'" },
- { name = "tokenizers", marker = "python_full_version >= '3.12'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/70/0c/62a0fdc5adae6d205338f9239175aa6a93818e58b75cf000a9c7214a3d9f/litellm-1.81.15.tar.gz", hash = "sha256:a8a6277a53280762051c5818ebc76dd5f036368b9426c6f21795ae7f1ac6ebdc", size = 16597039, upload-time = "2026-02-24T06:52:50.892Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/78/fd/da11826dda0d332e360b9ead6c0c992d612ecb85b00df494823843cfcda3/litellm-1.81.15-py3-none-any.whl", hash = "sha256:2fa253658702509ce09fe0e172e5a47baaadf697fb0f784c7fd4ff665ae76ae1", size = 14682123, upload-time = "2026-02-24T06:52:48.084Z" },
-]
-
[[package]]
name = "markdown"
version = "3.10.2"
@@ -2924,82 +2304,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/70/bb89f807a6a6704bdc4d6f850d5d32954f6c1965e3248e31455defdf2f30/marshmallow-4.2.2-py3-none-any.whl", hash = "sha256:084a9466111b7ec7183ca3a65aed758739af919fedc5ebdab60fb39d6b4dc121", size = 48454, upload-time = "2026-02-04T15:47:02.013Z" },
]
-[[package]]
-name = "math-verify"
-version = "0.9.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "latex2sympy2-extended" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/4f/12/b8d13b581e110ac2f724a2351a8361a70fa36d057eb945d6379e8747c256/math_verify-0.9.0.tar.gz", hash = "sha256:45ac6c61344ba056b9e99a660a4bc8d044ed408f730aed68c60435aa5eec4645", size = 60329, upload-time = "2026-01-10T01:48:33.056Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/62/76/6b4969bccc842b6567f7e6ee015684b9428a9b7fcbdf479e73716f43597f/math_verify-0.9.0-py3-none-any.whl", hash = "sha256:3703e7c4885354027fa84409d762a596a2906d1fd4deb78361876bd905a76194", size = 29967, upload-time = "2026-01-10T01:48:31.674Z" },
-]
-
-[[package]]
-name = "matplotlib"
-version = "3.10.8"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "contourpy", marker = "python_full_version >= '3.12'" },
- { name = "cycler", marker = "python_full_version >= '3.12'" },
- { name = "fonttools", marker = "python_full_version >= '3.12'" },
- { name = "kiwisolver", marker = "python_full_version >= '3.12'" },
- { name = "numpy", marker = "python_full_version >= '3.12'" },
- { name = "packaging", marker = "python_full_version >= '3.12'" },
- { name = "pillow", marker = "python_full_version >= '3.12'" },
- { name = "pyparsing", marker = "python_full_version >= '3.12'" },
- { name = "python-dateutil", marker = "python_full_version >= '3.12'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" },
- { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" },
- { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" },
- { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" },
- { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" },
- { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" },
- { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" },
- { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" },
- { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" },
- { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" },
- { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" },
- { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" },
- { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" },
- { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" },
- { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" },
- { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" },
- { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" },
- { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" },
- { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" },
- { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" },
- { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" },
- { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" },
- { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" },
- { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" },
- { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" },
- { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" },
- { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" },
- { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" },
- { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" },
- { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" },
- { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" },
- { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" },
- { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" },
- { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" },
- { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" },
- { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" },
- { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" },
- { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" },
- { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" },
- { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" },
- { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" },
- { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" },
- { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" },
- { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" },
- { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" },
-]
-
[[package]]
name = "mautrix"
version = "0.21.0"
@@ -3260,35 +2564,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
]
-[[package]]
-name = "multiprocess"
-version = "0.70.19"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "dill" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a2/f2/e783ac7f2aeeed14e9e12801f22529cc7e6b7ab80928d6dcce4e9f00922d/multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897", size = 2079989, upload-time = "2026-01-19T06:47:39.744Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/aa/714635c727dbfc251139226fa4eaf1b07f00dc12d9cd2eb25f931adaf873/multiprocess-0.70.19-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1bbf1b69af1cf64cd05f65337d9215b88079ec819cd0ea7bac4dab84e162efe7", size = 144743, upload-time = "2026-01-19T06:47:24.562Z" },
- { url = "https://files.pythonhosted.org/packages/0f/e1/155f6abf5e6b5d9cef29b6d0167c180846157a4aca9b9bee1a217f67c959/multiprocess-0.70.19-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5be9ec7f0c1c49a4f4a6fd20d5dda4aeabc2d39a50f4ad53720f1cd02b3a7c2e", size = 144738, upload-time = "2026-01-19T06:47:26.636Z" },
- { url = "https://files.pythonhosted.org/packages/af/cb/f421c2869d75750a4f32301cc20c4b63fab6376e9a75c8e5e655bdeb3d9b/multiprocess-0.70.19-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1c3dce098845a0db43b32a0b76a228ca059a668071cfeaa0f40c36c0b1585d45", size = 144741, upload-time = "2026-01-19T06:47:27.985Z" },
- { url = "https://files.pythonhosted.org/packages/e3/45/8004d1e6b9185c1a444d6b55ac5682acf9d98035e54386d967366035a03a/multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87", size = 134948, upload-time = "2026-01-19T06:47:32.325Z" },
- { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" },
- { url = "https://files.pythonhosted.org/packages/71/70/38998b950a97ea279e6bd657575d22d1a2047256caf707d9a10fbce4f065/multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28", size = 150281, upload-time = "2026-01-19T06:47:35.037Z" },
- { url = "https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl", hash = "sha256:8d5eb4ec5017ba2fab4e34a747c6d2c2b6fecfe9e7236e77988db91580ada952", size = 156414, upload-time = "2026-01-19T06:47:35.915Z" },
- { url = "https://files.pythonhosted.org/packages/a0/61/af9115673a5870fd885247e2f1b68c4f1197737da315b520a91c757a861a/multiprocess-0.70.19-py314-none-any.whl", hash = "sha256:e8cc7fbdff15c0613f0a1f1f8744bef961b0a164c0ca29bdff53e9d2d93c5e5f", size = 160318, upload-time = "2026-01-19T06:47:37.497Z" },
- { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" },
-]
-
-[[package]]
-name = "narwhals"
-version = "2.18.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/59/96/45218c2fdec4c9f22178f905086e85ef1a6d63862dcc3cd68eb60f1867f5/narwhals-2.18.1.tar.gz", hash = "sha256:652a1fcc9d432bbf114846688884c215f17eb118aa640b7419295d2f910d2a8b", size = 620578, upload-time = "2026-03-24T15:11:25.456Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl", hash = "sha256:a0a8bb80205323851338888ba3a12b4f65d352362c8a94be591244faf36504ad", size = 444952, upload-time = "2026-03-24T15:11:23.801Z" },
-]
-
[[package]]
name = "nest-asyncio"
version = "1.6.0"
@@ -3298,21 +2573,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
]
-[[package]]
-name = "nltk"
-version = "3.9.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "joblib" },
- { name = "regex" },
- { name = "tqdm" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" },
-]
-
[[package]]
name = "numpy"
version = "2.4.3"
@@ -3651,60 +2911,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
-[[package]]
-name = "pandas"
-version = "2.3.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "numpy" },
- { name = "python-dateutil" },
- { name = "pytz" },
- { name = "tzdata" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" },
- { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" },
- { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" },
- { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" },
- { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" },
- { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" },
- { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" },
- { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" },
- { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" },
- { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" },
- { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" },
- { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" },
- { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" },
- { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" },
- { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" },
- { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" },
- { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" },
- { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" },
- { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" },
- { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" },
- { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" },
- { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" },
- { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" },
- { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" },
- { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" },
- { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" },
- { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" },
- { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" },
- { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" },
- { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" },
- { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" },
- { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" },
- { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" },
- { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" },
- { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" },
- { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" },
- { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" },
- { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" },
- { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" },
- { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
-]
-
[[package]]
name = "parallel-web"
version = "0.4.2"
@@ -3722,115 +2928,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/3e/2218fa29637781b8e7ac35a928108ff2614ddd40879389d3af2caa725af5/parallel_web-0.4.2-py3-none-any.whl", hash = "sha256:aa3a4a9aecc08972c5ce9303271d4917903373dff4dd277d9a3e30f9cff53346", size = 144012, upload-time = "2026-03-09T22:24:33.979Z" },
]
-[[package]]
-name = "pillow"
-version = "12.1.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" },
- { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" },
- { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" },
- { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" },
- { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" },
- { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" },
- { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" },
- { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" },
- { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" },
- { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" },
- { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" },
- { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
- { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
- { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
- { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
- { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
- { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
- { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
- { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
- { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
- { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
- { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
- { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
- { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
- { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
- { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
- { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
- { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
- { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
- { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
- { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
- { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
- { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
- { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
- { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
- { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
- { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
- { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
- { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
- { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
- { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
- { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
- { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
- { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
- { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
- { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
- { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
- { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
- { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
- { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
- { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
- { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
- { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
- { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
- { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
- { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
- { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
- { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
- { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
- { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
- { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
- { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
- { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
- { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
- { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
- { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
- { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
- { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
- { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
- { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
- { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
- { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
- { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" },
- { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" },
- { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" },
- { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" },
- { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" },
- { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" },
- { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" },
-]
-
-[[package]]
-name = "platformdirs"
-version = "4.9.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" },
-]
-
-[[package]]
-name = "plotly"
-version = "6.6.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "narwhals", marker = "python_full_version >= '3.12'" },
- { name = "packaging", marker = "python_full_version >= '3.12'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/24/fb/41efe84970cfddefd4ccf025e2cbfafe780004555f583e93dba3dac2cdef/plotly-6.6.0.tar.gz", hash = "sha256:b897f15f3b02028d69f755f236be890ba950d0a42d7dfc619b44e2d8cea8748c", size = 7027956, upload-time = "2026-03-02T21:10:25.321Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/52/d2/c6e44dba74f17c6216ce1b56044a9b93a929f1c2d5bdaff892512b260f5e/plotly-6.6.0-py3-none-any.whl", hash = "sha256:8d6daf0f87412e0c0bfe72e809d615217ab57cc715899a1e5145135a7800d1d0", size = 9910315, upload-time = "2026-03-02T21:10:18.131Z" },
-]
-
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -3840,34 +2937,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
-[[package]]
-name = "polars"
-version = "1.39.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "polars-runtime-32" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/93/ab/f19e592fce9e000da49c96bf35e77cef67f9cb4b040bfa538a2764c0263e/polars-1.39.3.tar.gz", hash = "sha256:2e016c7f3e8d14fa777ef86fe0477cec6c67023a20ba4c94d6e8431eefe4a63c", size = 728987, upload-time = "2026-03-20T11:16:24.836Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b4/db/08f4ca10c5018813e7e0b59e4472302328b3d2ab1512f5a2157a814540e0/polars-1.39.3-py3-none-any.whl", hash = "sha256:c2b955ccc0a08a2bc9259785decf3d5c007b489b523bf2390cf21cec2bb82a56", size = 823985, upload-time = "2026-03-20T11:14:23.619Z" },
-]
-
-[[package]]
-name = "polars-runtime-32"
-version = "1.39.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/17/39/c8688696bc22b6c501e3b82ef3be10e543c07a785af5660f30997cd22dd2/polars_runtime_32-1.39.3.tar.gz", hash = "sha256:c728e4f469cafab501947585f36311b8fb222d3e934c6209e83791e0df20b29d", size = 2872335, upload-time = "2026-03-20T11:16:26.581Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/74/1b41205f7368c9375ab1dea91178eaa20435fe3eff036390a53a7660b416/polars_runtime_32-1.39.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:425c0b220b573fa097b4042edff73114cc6d23432a21dfd2dc41adf329d7d2e9", size = 45273243, upload-time = "2026-03-20T11:14:26.691Z" },
- { url = "https://files.pythonhosted.org/packages/90/bf/297716b3095fe719be20fcf7af1d2b6ab069c38199bbace2469608a69b3a/polars_runtime_32-1.39.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef5884711e3c617d7dc93519a7d038e242f5741cfe5fe9afd32d58845d86c562", size = 40842924, upload-time = "2026-03-20T11:14:31.154Z" },
- { url = "https://files.pythonhosted.org/packages/3d/3e/e65236d9d0d9babfa0ecba593413c06530fca60a8feb8f66243aa5dba92e/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06b47f535eb1f97a9a1e5b0053ef50db3a4276e241178e37bbb1a38b1fa53b14", size = 43220650, upload-time = "2026-03-20T11:14:35.458Z" },
- { url = "https://files.pythonhosted.org/packages/b0/15/fc3e43f3fdf3f20b7dfb5abe871ab6162cf8fb4aeabf4cfad822d5dc4c79/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bc9e13dc1d2e828331f2fe8ccbc9757554dc4933a8d3e85e906b988178f95ed", size = 46877498, upload-time = "2026-03-20T11:14:40.14Z" },
- { url = "https://files.pythonhosted.org/packages/3c/81/bd5f895919e32c6ab0a7786cd0c0ca961cb03152c47c3645808b54383f31/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:363d49e3a3e638fc943e2b9887940300a7d06789930855a178a4727949259dc2", size = 43380176, upload-time = "2026-03-20T11:14:45.566Z" },
- { url = "https://files.pythonhosted.org/packages/7a/3e/c86433c3b5ec0315bdfc7640d0c15d41f1216c0103a0eab9a9b5147d6c4c/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c206bdcc7bc62ea038d6adea8e44b02f0e675e0191a54c810703b4895208ea4", size = 46485933, upload-time = "2026-03-20T11:14:51.155Z" },
- { url = "https://files.pythonhosted.org/packages/54/ce/200b310cf91f98e652eb6ea09fdb3a9718aa0293ebf113dce325797c8572/polars_runtime_32-1.39.3-cp310-abi3-win_amd64.whl", hash = "sha256:d66ca522517554a883446957539c40dc7b75eb0c2220357fb28bc8940d305339", size = 46995458, upload-time = "2026-03-20T11:14:56.074Z" },
- { url = "https://files.pythonhosted.org/packages/da/76/2d48927e0aa2abbdde08cbf4a2536883b73277d47fbeca95e952de86df34/polars_runtime_32-1.39.3-cp310-abi3-win_arm64.whl", hash = "sha256:f49f51461de63f13e5dd4eb080421c8f23f856945f3f8bd5b2b1f59da52c2860", size = 41857648, upload-time = "2026-03-20T11:15:01.142Z" },
-]
-
[[package]]
name = "prompt-toolkit"
version = "3.0.52"
@@ -4043,56 +3112,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
]
-[[package]]
-name = "pyarrow"
-version = "23.0.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b0/41/8e6b6ef7e225d4ceead8459427a52afdc23379768f54dd3566014d7618c1/pyarrow-23.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", size = 34302230, upload-time = "2026-02-16T10:09:03.859Z" },
- { url = "https://files.pythonhosted.org/packages/bf/4a/1472c00392f521fea03ae93408bf445cc7bfa1ab81683faf9bc188e36629/pyarrow-23.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", size = 35850050, upload-time = "2026-02-16T10:09:11.877Z" },
- { url = "https://files.pythonhosted.org/packages/0c/b2/bd1f2f05ded56af7f54d702c8364c9c43cd6abb91b0e9933f3d77b4f4132/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd", size = 44491918, upload-time = "2026-02-16T10:09:18.144Z" },
- { url = "https://files.pythonhosted.org/packages/0b/62/96459ef5b67957eac38a90f541d1c28833d1b367f014a482cb63f3b7cd2d/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", size = 47562811, upload-time = "2026-02-16T10:09:25.792Z" },
- { url = "https://files.pythonhosted.org/packages/7d/94/1170e235add1f5f45a954e26cd0e906e7e74e23392dcb560de471f7366ec/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", size = 48183766, upload-time = "2026-02-16T10:09:34.645Z" },
- { url = "https://files.pythonhosted.org/packages/0e/2d/39a42af4570377b99774cdb47f63ee6c7da7616bd55b3d5001aa18edfe4f/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", size = 50607669, upload-time = "2026-02-16T10:09:44.153Z" },
- { url = "https://files.pythonhosted.org/packages/00/ca/db94101c187f3df742133ac837e93b1f269ebdac49427f8310ee40b6a58f/pyarrow-23.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", size = 27527698, upload-time = "2026-02-16T10:09:50.263Z" },
- { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" },
- { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" },
- { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" },
- { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" },
- { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" },
- { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" },
- { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" },
- { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066, upload-time = "2026-02-16T10:10:45.487Z" },
- { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526, upload-time = "2026-02-16T10:10:52.266Z" },
- { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279, upload-time = "2026-02-16T10:11:01.557Z" },
- { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798, upload-time = "2026-02-16T10:11:09.401Z" },
- { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446, upload-time = "2026-02-16T10:11:17.781Z" },
- { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972, upload-time = "2026-02-16T10:11:26.185Z" },
- { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749, upload-time = "2026-02-16T10:12:23.297Z" },
- { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544, upload-time = "2026-02-16T10:11:32.535Z" },
- { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911, upload-time = "2026-02-16T10:11:39.813Z" },
- { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337, upload-time = "2026-02-16T10:11:47.764Z" },
- { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944, upload-time = "2026-02-16T10:11:56.607Z" },
- { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269, upload-time = "2026-02-16T10:12:04.47Z" },
- { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794, upload-time = "2026-02-16T10:12:11.797Z" },
- { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642, upload-time = "2026-02-16T10:12:17.746Z" },
- { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755, upload-time = "2026-02-16T10:12:32.819Z" },
- { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826, upload-time = "2026-02-16T10:12:38.949Z" },
- { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859, upload-time = "2026-02-16T10:12:45.467Z" },
- { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443, upload-time = "2026-02-16T10:12:55.525Z" },
- { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991, upload-time = "2026-02-16T10:13:04.729Z" },
- { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077, upload-time = "2026-02-16T10:13:14.147Z" },
- { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271, upload-time = "2026-02-16T10:14:09.397Z" },
- { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692, upload-time = "2026-02-16T10:13:21.541Z" },
- { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383, upload-time = "2026-02-16T10:13:28.63Z" },
- { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119, upload-time = "2026-02-16T10:13:35.506Z" },
- { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199, upload-time = "2026-02-16T10:13:42.504Z" },
- { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435, upload-time = "2026-02-16T10:13:49.226Z" },
- { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149, upload-time = "2026-02-16T10:13:57.238Z" },
- { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" },
-]
-
[[package]]
name = "pyasn1"
version = "0.6.3"
@@ -4168,18 +3187,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
-[[package]]
-name = "pydantic-cli"
-version = "10.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pydantic" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/3d/45/b383f86c77e9f38360f66253a223f127a74a58aa46e22e52011093f83b3a/pydantic_cli-10.0.0.tar.gz", hash = "sha256:1439d1db73664177c838ca1b90ae8eca19c65ce3b119a79a7b6c6f07cb79874a", size = 34984, upload-time = "2025-10-16T07:00:45.091Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1c/41/5262fca75b48906b03bd1e156b99330699b59a198b220051128a23917e9a/pydantic_cli-10.0.0-py3-none-any.whl", hash = "sha256:e3778aed1e412c9962812af6a11d92ba514df6266bd60835f843b6332dae6eed", size = 43076, upload-time = "2025-10-16T07:00:43.705Z" },
-]
-
[[package]]
name = "pydantic-core"
version = "2.41.5"
@@ -4291,19 +3298,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" },
]
-[[package]]
-name = "pydeck"
-version = "0.9.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "jinja2", marker = "python_full_version >= '3.12'" },
- { name = "numpy", marker = "python_full_version >= '3.12'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240, upload-time = "2024-05-10T15:36:21.153Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403, upload-time = "2024-05-10T15:36:17.36Z" },
-]
-
[[package]]
name = "pygments"
version = "2.19.2"
@@ -4616,110 +3610,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
]
-[[package]]
-name = "regex"
-version = "2026.2.19"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ff/c0/d8079d4f6342e4cec5c3e7d7415b5cd3e633d5f4124f7a4626908dbe84c7/regex-2026.2.19.tar.gz", hash = "sha256:6fb8cb09b10e38f3ae17cc6dc04a1df77762bd0351b6ba9041438e7cc85ec310", size = 414973, upload-time = "2026-02-19T19:03:47.899Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6f/93/43f405a98f54cc59c786efb4fc0b644615ed2392fc89d57d30da11f35b5b/regex-2026.2.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:93b16a18cadb938f0f2306267161d57eb33081a861cee9ffcd71e60941eb5dfc", size = 488365, upload-time = "2026-02-19T19:00:17.857Z" },
- { url = "https://files.pythonhosted.org/packages/66/46/da0efce22cd8f5ae28eeb25ac69703f49edcad3331ac22440776f4ea0867/regex-2026.2.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78af1e499cab704131f6f4e2f155b7f54ce396ca2acb6ef21a49507e4752e0be", size = 290737, upload-time = "2026-02-19T19:00:19.869Z" },
- { url = "https://files.pythonhosted.org/packages/fb/19/f735078448132c1c974974d30d5306337bc297fe6b6f126164bff72c1019/regex-2026.2.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eb20c11aa4c3793c9ad04c19a972078cdadb261b8429380364be28e867a843f2", size = 288654, upload-time = "2026-02-19T19:00:21.307Z" },
- { url = "https://files.pythonhosted.org/packages/e2/3e/6d7c24a2f423c03ad03e3fbddefa431057186ac1c4cb4fa98b03c7f39808/regex-2026.2.19-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db5fd91eec71e7b08de10011a2223d0faa20448d4e1380b9daa179fa7bf58906", size = 793785, upload-time = "2026-02-19T19:00:22.926Z" },
- { url = "https://files.pythonhosted.org/packages/67/32/fdb8107504b3122a79bde6705ac1f9d495ed1fe35b87d7cfc1864471999a/regex-2026.2.19-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fdbade8acba71bb45057c2b72f477f0b527c4895f9c83e6cfc30d4a006c21726", size = 860731, upload-time = "2026-02-19T19:00:25.196Z" },
- { url = "https://files.pythonhosted.org/packages/9a/fd/cc8c6f05868defd840be6e75919b1c3f462357969ac2c2a0958363b4dc23/regex-2026.2.19-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:31a5f561eb111d6aae14202e7043fb0b406d3c8dddbbb9e60851725c9b38ab1d", size = 907350, upload-time = "2026-02-19T19:00:27.093Z" },
- { url = "https://files.pythonhosted.org/packages/b5/1b/4590db9caa8db3d5a3fe31197c4e42c15aab3643b549ef6a454525fa3a61/regex-2026.2.19-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4584a3ee5f257b71e4b693cc9be3a5104249399f4116fe518c3f79b0c6fc7083", size = 800628, upload-time = "2026-02-19T19:00:29.392Z" },
- { url = "https://files.pythonhosted.org/packages/76/05/513eaa5b96fa579fd0b813e19ec047baaaf573d7374ff010fa139b384bf7/regex-2026.2.19-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:196553ba2a2f47904e5dc272d948a746352e2644005627467e055be19d73b39e", size = 773711, upload-time = "2026-02-19T19:00:30.996Z" },
- { url = "https://files.pythonhosted.org/packages/95/65/5aed06d8c54563d37fea496cf888be504879a3981a7c8e12c24b2c92c209/regex-2026.2.19-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0c10869d18abb759a3317c757746cc913d6324ce128b8bcec99350df10419f18", size = 783186, upload-time = "2026-02-19T19:00:34.598Z" },
- { url = "https://files.pythonhosted.org/packages/2c/57/79a633ad90f2371b4ef9cd72ba3a69a1a67d0cfaab4fe6fa8586d46044ef/regex-2026.2.19-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e689fed279cbe797a6b570bd18ff535b284d057202692c73420cb93cca41aa32", size = 854854, upload-time = "2026-02-19T19:00:37.306Z" },
- { url = "https://files.pythonhosted.org/packages/eb/2d/0f113d477d9e91ec4545ec36c82e58be25038d06788229c91ad52da2b7f5/regex-2026.2.19-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0782bd983f19ac7594039c9277cd6f75c89598c1d72f417e4d30d874105eb0c7", size = 762279, upload-time = "2026-02-19T19:00:39.793Z" },
- { url = "https://files.pythonhosted.org/packages/39/cb/237e9fa4f61469fd4f037164dbe8e675a376c88cf73aaaa0aedfd305601c/regex-2026.2.19-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:dbb240c81cfed5d4a67cb86d7676d9f7ec9c3f186310bec37d8a1415210e111e", size = 846172, upload-time = "2026-02-19T19:00:42.134Z" },
- { url = "https://files.pythonhosted.org/packages/ac/7c/104779c5915cc4eb557a33590f8a3f68089269c64287dd769afd76c7ce61/regex-2026.2.19-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80d31c3f1fe7e4c6cd1831cd4478a0609903044dfcdc4660abfe6fb307add7f0", size = 789078, upload-time = "2026-02-19T19:00:43.908Z" },
- { url = "https://files.pythonhosted.org/packages/a8/4a/eae4e88b1317fb2ff57794915e0099198f51e760f6280b320adfa0ad396d/regex-2026.2.19-cp311-cp311-win32.whl", hash = "sha256:66e6a43225ff1064f8926adbafe0922b370d381c3330edaf9891cade52daa790", size = 266013, upload-time = "2026-02-19T19:00:47.274Z" },
- { url = "https://files.pythonhosted.org/packages/f9/29/ba89eb8fae79705e07ad1bd69e568f776159d2a8093c9dbc5303ee618298/regex-2026.2.19-cp311-cp311-win_amd64.whl", hash = "sha256:59a7a5216485a1896c5800e9feb8ff9213e11967b482633b6195d7da11450013", size = 277906, upload-time = "2026-02-19T19:00:49.011Z" },
- { url = "https://files.pythonhosted.org/packages/e3/1a/042d8f04b28e318df92df69d8becb0f42221eb3dd4fe5e976522f4337c76/regex-2026.2.19-cp311-cp311-win_arm64.whl", hash = "sha256:ec661807ffc14c8d14bb0b8c1bb3d5906e476bc96f98b565b709d03962ee4dd4", size = 270463, upload-time = "2026-02-19T19:00:50.988Z" },
- { url = "https://files.pythonhosted.org/packages/b3/73/13b39c7c9356f333e564ab4790b6cb0df125b8e64e8d6474e73da49b1955/regex-2026.2.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c1665138776e4ac1aa75146669236f7a8a696433ec4e525abf092ca9189247cc", size = 489541, upload-time = "2026-02-19T19:00:52.728Z" },
- { url = "https://files.pythonhosted.org/packages/15/77/fcc7bd9a67000d07fbcc11ed226077287a40d5c84544e62171d29d3ef59c/regex-2026.2.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d792b84709021945597e05656aac059526df4e0c9ef60a0eaebb306f8fafcaa8", size = 291414, upload-time = "2026-02-19T19:00:54.51Z" },
- { url = "https://files.pythonhosted.org/packages/f9/87/3997fc72dc59233426ef2e18dfdd105bb123812fff740ee9cc348f1a3243/regex-2026.2.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db970bcce4d63b37b3f9eb8c893f0db980bbf1d404a1d8d2b17aa8189de92c53", size = 289140, upload-time = "2026-02-19T19:00:56.841Z" },
- { url = "https://files.pythonhosted.org/packages/f3/d0/b7dd3883ed1cff8ee0c0c9462d828aaf12be63bf5dc55453cbf423523b13/regex-2026.2.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03d706fbe7dfec503c8c3cb76f9352b3e3b53b623672aa49f18a251a6c71b8e6", size = 798767, upload-time = "2026-02-19T19:00:59.014Z" },
- { url = "https://files.pythonhosted.org/packages/4a/7e/8e2d09103832891b2b735a2515abf377db21144c6dd5ede1fb03c619bf09/regex-2026.2.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dbff048c042beef60aa1848961384572c5afb9e8b290b0f1203a5c42cf5af65", size = 864436, upload-time = "2026-02-19T19:01:00.772Z" },
- { url = "https://files.pythonhosted.org/packages/8a/2e/afea8d23a6db1f67f45e3a0da3057104ce32e154f57dd0c8997274d45fcd/regex-2026.2.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccaaf9b907ea6b4223d5cbf5fa5dff5f33dc66f4907a25b967b8a81339a6e332", size = 912391, upload-time = "2026-02-19T19:01:02.865Z" },
- { url = "https://files.pythonhosted.org/packages/59/3c/ea5a4687adaba5e125b9bd6190153d0037325a0ba3757cc1537cc2c8dd90/regex-2026.2.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75472631eee7898e16a8a20998d15106cb31cfde21cdf96ab40b432a7082af06", size = 803702, upload-time = "2026-02-19T19:01:05.298Z" },
- { url = "https://files.pythonhosted.org/packages/dc/c5/624a0705e8473a26488ec1a3a4e0b8763ecfc682a185c302dfec71daea35/regex-2026.2.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d89f85a5ccc0cec125c24be75610d433d65295827ebaf0d884cbe56df82d4774", size = 775980, upload-time = "2026-02-19T19:01:07.047Z" },
- { url = "https://files.pythonhosted.org/packages/4d/4b/ed776642533232b5599b7c1f9d817fe11faf597e8a92b7a44b841daaae76/regex-2026.2.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9f81806abdca3234c3dd582b8a97492e93de3602c8772013cb4affa12d1668", size = 788122, upload-time = "2026-02-19T19:01:08.744Z" },
- { url = "https://files.pythonhosted.org/packages/8c/58/e93e093921d13b9784b4f69896b6e2a9e09580a265c59d9eb95e87d288f2/regex-2026.2.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9dadc10d1c2bbb1326e572a226d2ec56474ab8aab26fdb8cf19419b372c349a9", size = 858910, upload-time = "2026-02-19T19:01:10.488Z" },
- { url = "https://files.pythonhosted.org/packages/85/77/ff1d25a0c56cd546e0455cbc93235beb33474899690e6a361fa6b52d265b/regex-2026.2.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6bc25d7e15f80c9dc7853cbb490b91c1ec7310808b09d56bd278fe03d776f4f6", size = 764153, upload-time = "2026-02-19T19:01:12.156Z" },
- { url = "https://files.pythonhosted.org/packages/cd/ef/8ec58df26d52d04443b1dc56f9be4b409f43ed5ae6c0248a287f52311fc4/regex-2026.2.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:965d59792f5037d9138da6fed50ba943162160443b43d4895b182551805aff9c", size = 850348, upload-time = "2026-02-19T19:01:14.147Z" },
- { url = "https://files.pythonhosted.org/packages/f5/b3/c42fd5ed91639ce5a4225b9df909180fc95586db071f2bf7c68d2ccbfbe6/regex-2026.2.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:38d88c6ed4a09ed61403dbdf515d969ccba34669af3961ceb7311ecd0cef504a", size = 789977, upload-time = "2026-02-19T19:01:15.838Z" },
- { url = "https://files.pythonhosted.org/packages/b6/22/bc3b58ebddbfd6ca5633e71fd41829ee931963aad1ebeec55aad0c23044e/regex-2026.2.19-cp312-cp312-win32.whl", hash = "sha256:5df947cabab4b643d4791af5e28aecf6bf62e6160e525651a12eba3d03755e6b", size = 266381, upload-time = "2026-02-19T19:01:17.952Z" },
- { url = "https://files.pythonhosted.org/packages/fc/4a/6ff550b63e67603ee60e69dc6bd2d5694e85046a558f663b2434bdaeb285/regex-2026.2.19-cp312-cp312-win_amd64.whl", hash = "sha256:4146dc576ea99634ae9c15587d0c43273b4023a10702998edf0fa68ccb60237a", size = 277274, upload-time = "2026-02-19T19:01:19.826Z" },
- { url = "https://files.pythonhosted.org/packages/cc/29/9ec48b679b1e87e7bc8517dff45351eab38f74fbbda1fbcf0e9e6d4e8174/regex-2026.2.19-cp312-cp312-win_arm64.whl", hash = "sha256:cdc0a80f679353bd68450d2a42996090c30b2e15ca90ded6156c31f1a3b63f3b", size = 270509, upload-time = "2026-02-19T19:01:22.075Z" },
- { url = "https://files.pythonhosted.org/packages/d2/2d/a849835e76ac88fcf9e8784e642d3ea635d183c4112150ca91499d6703af/regex-2026.2.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8df08decd339e8b3f6a2eb5c05c687fe9d963ae91f352bc57beb05f5b2ac6879", size = 489329, upload-time = "2026-02-19T19:01:23.841Z" },
- { url = "https://files.pythonhosted.org/packages/da/aa/78ff4666d3855490bae87845a5983485e765e1f970da20adffa2937b241d/regex-2026.2.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3aa0944f1dc6e92f91f3b306ba7f851e1009398c84bfd370633182ee4fc26a64", size = 291308, upload-time = "2026-02-19T19:01:25.605Z" },
- { url = "https://files.pythonhosted.org/packages/cd/58/714384efcc07ae6beba528a541f6e99188c5cc1bc0295337f4e8a868296d/regex-2026.2.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c13228fbecb03eadbfd8f521732c5fda09ef761af02e920a3148e18ad0e09968", size = 289033, upload-time = "2026-02-19T19:01:27.243Z" },
- { url = "https://files.pythonhosted.org/packages/75/ec/6438a9344d2869cf5265236a06af1ca6d885e5848b6561e10629bc8e5a11/regex-2026.2.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d0e72703c60d68b18b27cde7cdb65ed2570ae29fb37231aa3076bfb6b1d1c13", size = 798798, upload-time = "2026-02-19T19:01:28.877Z" },
- { url = "https://files.pythonhosted.org/packages/c2/be/b1ce2d395e3fd2ce5f2fde2522f76cade4297cfe84cd61990ff48308749c/regex-2026.2.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46e69a4bf552e30e74a8aa73f473c87efcb7f6e8c8ece60d9fd7bf13d5c86f02", size = 864444, upload-time = "2026-02-19T19:01:30.933Z" },
- { url = "https://files.pythonhosted.org/packages/d5/97/a3406460c504f7136f140d9461960c25f058b0240e4424d6fb73c7a067ab/regex-2026.2.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8edda06079bd770f7f0cf7f3bba1a0b447b96b4a543c91fe0c142d034c166161", size = 912633, upload-time = "2026-02-19T19:01:32.744Z" },
- { url = "https://files.pythonhosted.org/packages/8b/d9/e5dbef95008d84e9af1dc0faabbc34a7fbc8daa05bc5807c5cf86c2bec49/regex-2026.2.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cbc69eae834afbf634f7c902fc72ff3e993f1c699156dd1af1adab5d06b7fe7", size = 803718, upload-time = "2026-02-19T19:01:34.61Z" },
- { url = "https://files.pythonhosted.org/packages/2f/e5/61d80132690a1ef8dc48e0f44248036877aebf94235d43f63a20d1598888/regex-2026.2.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bcf57d30659996ee5c7937999874504c11b5a068edc9515e6a59221cc2744dd1", size = 775975, upload-time = "2026-02-19T19:01:36.525Z" },
- { url = "https://files.pythonhosted.org/packages/05/32/ae828b3b312c972cf228b634447de27237d593d61505e6ad84723f8eabba/regex-2026.2.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8e6e77cd92216eb489e21e5652a11b186afe9bdefca8a2db739fd6b205a9e0a4", size = 788129, upload-time = "2026-02-19T19:01:38.498Z" },
- { url = "https://files.pythonhosted.org/packages/cb/25/d74f34676f22bec401eddf0e5e457296941e10cbb2a49a571ca7a2c16e5a/regex-2026.2.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b9ab8dec42afefa6314ea9b31b188259ffdd93f433d77cad454cd0b8d235ce1c", size = 858818, upload-time = "2026-02-19T19:01:40.409Z" },
- { url = "https://files.pythonhosted.org/packages/1e/eb/0bc2b01a6b0b264e1406e5ef11cae3f634c3bd1a6e61206fd3227ce8e89c/regex-2026.2.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:294c0fb2e87c6bcc5f577c8f609210f5700b993151913352ed6c6af42f30f95f", size = 764186, upload-time = "2026-02-19T19:01:43.009Z" },
- { url = "https://files.pythonhosted.org/packages/eb/37/5fe5a630d0d99ecf0c3570f8905dafbc160443a2d80181607770086c9812/regex-2026.2.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c0924c64b082d4512b923ac016d6e1dcf647a3560b8a4c7e55cbbd13656cb4ed", size = 850363, upload-time = "2026-02-19T19:01:45.015Z" },
- { url = "https://files.pythonhosted.org/packages/c3/45/ef68d805294b01ec030cfd388724ba76a5a21a67f32af05b17924520cb0b/regex-2026.2.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:790dbf87b0361606cb0d79b393c3e8f4436a14ee56568a7463014565d97da02a", size = 790026, upload-time = "2026-02-19T19:01:47.51Z" },
- { url = "https://files.pythonhosted.org/packages/d6/3a/40d3b66923dfc5aeba182f194f0ca35d09afe8c031a193e6ae46971a0a0e/regex-2026.2.19-cp313-cp313-win32.whl", hash = "sha256:43cdde87006271be6963896ed816733b10967baaf0e271d529c82e93da66675b", size = 266372, upload-time = "2026-02-19T19:01:49.469Z" },
- { url = "https://files.pythonhosted.org/packages/3d/f2/39082e8739bfd553497689e74f9d5e5bb531d6f8936d0b94f43e18f219c0/regex-2026.2.19-cp313-cp313-win_amd64.whl", hash = "sha256:127ea69273485348a126ebbf3d6052604d3c7da284f797bba781f364c0947d47", size = 277253, upload-time = "2026-02-19T19:01:51.208Z" },
- { url = "https://files.pythonhosted.org/packages/c2/c2/852b9600d53fb47e47080c203e2cdc0ac7e84e37032a57e0eaa37446033a/regex-2026.2.19-cp313-cp313-win_arm64.whl", hash = "sha256:5e56c669535ac59cbf96ca1ece0ef26cb66809990cda4fa45e1e32c3b146599e", size = 270505, upload-time = "2026-02-19T19:01:52.865Z" },
- { url = "https://files.pythonhosted.org/packages/a9/a2/e0b4575b93bc84db3b1fab24183e008691cd2db5c0ef14ed52681fbd94dd/regex-2026.2.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:93d881cab5afdc41a005dba1524a40947d6f7a525057aa64aaf16065cf62faa9", size = 492202, upload-time = "2026-02-19T19:01:54.816Z" },
- { url = "https://files.pythonhosted.org/packages/24/b5/b84fec8cbb5f92a7eed2b6b5353a6a9eed9670fee31817c2da9eb85dc797/regex-2026.2.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:80caaa1ddcc942ec7be18427354f9d58a79cee82dea2a6b3d4fd83302e1240d7", size = 292884, upload-time = "2026-02-19T19:01:58.254Z" },
- { url = "https://files.pythonhosted.org/packages/70/0c/fe89966dfae43da46f475362401f03e4d7dc3a3c955b54f632abc52669e0/regex-2026.2.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d793c5b4d2b4c668524cd1651404cfc798d40694c759aec997e196fe9729ec60", size = 291236, upload-time = "2026-02-19T19:01:59.966Z" },
- { url = "https://files.pythonhosted.org/packages/f2/f7/bda2695134f3e63eb5cccbbf608c2a12aab93d261ff4e2fe49b47fabc948/regex-2026.2.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5100acb20648d9efd3f4e7e91f51187f95f22a741dcd719548a6cf4e1b34b3f", size = 807660, upload-time = "2026-02-19T19:02:01.632Z" },
- { url = "https://files.pythonhosted.org/packages/11/56/6e3a4bf5e60d17326b7003d91bbde8938e439256dec211d835597a44972d/regex-2026.2.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5e3a31e94d10e52a896adaa3adf3621bd526ad2b45b8c2d23d1bbe74c7423007", size = 873585, upload-time = "2026-02-19T19:02:03.522Z" },
- { url = "https://files.pythonhosted.org/packages/35/5e/c90c6aa4d1317cc11839359479cfdd2662608f339e84e81ba751c8a4e461/regex-2026.2.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8497421099b981f67c99eba4154cf0dfd8e47159431427a11cfb6487f7791d9e", size = 915243, upload-time = "2026-02-19T19:02:05.608Z" },
- { url = "https://files.pythonhosted.org/packages/90/7c/981ea0694116793001496aaf9524e5c99e122ec3952d9e7f1878af3a6bf1/regex-2026.2.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e7a08622f7d51d7a068f7e4052a38739c412a3e74f55817073d2e2418149619", size = 812922, upload-time = "2026-02-19T19:02:08.115Z" },
- { url = "https://files.pythonhosted.org/packages/2d/be/9eda82afa425370ffdb3fa9f3ea42450b9ae4da3ff0a4ec20466f69e371b/regex-2026.2.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8abe671cf0f15c26b1ad389bf4043b068ce7d3b1c5d9313e12895f57d6738555", size = 781318, upload-time = "2026-02-19T19:02:10.072Z" },
- { url = "https://files.pythonhosted.org/packages/c6/d5/50f0bbe56a8199f60a7b6c714e06e54b76b33d31806a69d0703b23ce2a9e/regex-2026.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5a8f28dd32a4ce9c41758d43b5b9115c1c497b4b1f50c457602c1d571fa98ce1", size = 795649, upload-time = "2026-02-19T19:02:11.96Z" },
- { url = "https://files.pythonhosted.org/packages/c5/09/d039f081e44a8b0134d0bb2dd805b0ddf390b69d0b58297ae098847c572f/regex-2026.2.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:654dc41a5ba9b8cc8432b3f1aa8906d8b45f3e9502442a07c2f27f6c63f85db5", size = 868844, upload-time = "2026-02-19T19:02:14.043Z" },
- { url = "https://files.pythonhosted.org/packages/ef/53/e2903b79a19ec8557fe7cd21cd093956ff2dbc2e0e33969e3adbe5b184dd/regex-2026.2.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4a02faea614e7fdd6ba8b3bec6c8e79529d356b100381cec76e638f45d12ca04", size = 770113, upload-time = "2026-02-19T19:02:16.161Z" },
- { url = "https://files.pythonhosted.org/packages/8f/e2/784667767b55714ebb4e59bf106362327476b882c0b2f93c25e84cc99b1a/regex-2026.2.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d96162140bb819814428800934c7b71b7bffe81fb6da2d6abc1dcca31741eca3", size = 854922, upload-time = "2026-02-19T19:02:18.155Z" },
- { url = "https://files.pythonhosted.org/packages/59/78/9ef4356bd4aed752775bd18071034979b85f035fec51f3a4f9dea497a254/regex-2026.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c227f2922153ee42bbeb355fd6d009f8c81d9d7bdd666e2276ce41f53ed9a743", size = 799636, upload-time = "2026-02-19T19:02:20.04Z" },
- { url = "https://files.pythonhosted.org/packages/cf/54/fcfc9287f20c5c9bd8db755aafe3e8cf4d99a6a3f1c7162ee182e0ca9374/regex-2026.2.19-cp313-cp313t-win32.whl", hash = "sha256:a178df8ec03011153fbcd2c70cb961bc98cbbd9694b28f706c318bee8927c3db", size = 268968, upload-time = "2026-02-19T19:02:22.816Z" },
- { url = "https://files.pythonhosted.org/packages/1e/a0/ff24c6cb1273e42472706d277147fc38e1f9074a280fb6034b0fc9b69415/regex-2026.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:2c1693ca6f444d554aa246b592355b5cec030ace5a2729eae1b04ab6e853e768", size = 280390, upload-time = "2026-02-19T19:02:25.231Z" },
- { url = "https://files.pythonhosted.org/packages/1a/b6/a3f6ad89d780ffdeebb4d5e2e3e30bd2ef1f70f6a94d1760e03dd1e12c60/regex-2026.2.19-cp313-cp313t-win_arm64.whl", hash = "sha256:c0761d7ae8d65773e01515ebb0b304df1bf37a0a79546caad9cbe79a42c12af7", size = 271643, upload-time = "2026-02-19T19:02:27.175Z" },
- { url = "https://files.pythonhosted.org/packages/2d/e2/7ad4e76a6dddefc0d64dbe12a4d3ca3947a19ddc501f864a5df2a8222ddd/regex-2026.2.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:03d191a9bcf94d31af56d2575210cb0d0c6a054dbcad2ea9e00aa4c42903b919", size = 489306, upload-time = "2026-02-19T19:02:29.058Z" },
- { url = "https://files.pythonhosted.org/packages/14/95/ee1736135733afbcf1846c58671046f99c4d5170102a150ebb3dd8d701d9/regex-2026.2.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:516ee067c6c721d0d0bfb80a2004edbd060fffd07e456d4e1669e38fe82f922e", size = 291218, upload-time = "2026-02-19T19:02:31.083Z" },
- { url = "https://files.pythonhosted.org/packages/ef/08/180d1826c3d7065200a5168c6b993a44947395c7bb6e04b2c2a219c34225/regex-2026.2.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:997862c619994c4a356cb7c3592502cbd50c2ab98da5f61c5c871f10f22de7e5", size = 289097, upload-time = "2026-02-19T19:02:33.485Z" },
- { url = "https://files.pythonhosted.org/packages/28/93/0651924c390c5740f5f896723f8ddd946a6c63083a7d8647231c343912ff/regex-2026.2.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b9e1b8a7ebe2807cd7bbdf662510c8e43053a23262b9f46ad4fc2dfc9d204e", size = 799147, upload-time = "2026-02-19T19:02:35.669Z" },
- { url = "https://files.pythonhosted.org/packages/a7/00/2078bd8bcd37d58a756989adbfd9f1d0151b7ca4085a9c2a07e917fbac61/regex-2026.2.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6c8fb3b19652e425ff24169dad3ee07f99afa7996caa9dfbb3a9106cd726f49a", size = 865239, upload-time = "2026-02-19T19:02:38.012Z" },
- { url = "https://files.pythonhosted.org/packages/2a/13/75195161ec16936b35a365fa8c1dd2ab29fd910dd2587765062b174d8cfc/regex-2026.2.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50f1ee9488dd7a9fda850ec7c68cad7a32fa49fd19733f5403a3f92b451dcf73", size = 911904, upload-time = "2026-02-19T19:02:40.737Z" },
- { url = "https://files.pythonhosted.org/packages/96/72/ac42f6012179343d1c4bd0ffee8c948d841cb32ea188d37e96d80527fcc9/regex-2026.2.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab780092b1424d13200aa5a62996e95f65ee3db8509be366437439cdc0af1a9f", size = 803518, upload-time = "2026-02-19T19:02:42.923Z" },
- { url = "https://files.pythonhosted.org/packages/bc/d1/75a08e2269b007b9783f0f86aa64488e023141219cb5f14dc1e69cda56c6/regex-2026.2.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17648e1a88e72d88641b12635e70e6c71c5136ba14edba29bf8fc6834005a265", size = 775866, upload-time = "2026-02-19T19:02:45.189Z" },
- { url = "https://files.pythonhosted.org/packages/92/41/70e7d05faf6994c2ca7a9fcaa536da8f8e4031d45b0ec04b57040ede201f/regex-2026.2.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f914ae8c804c8a8a562fe216100bc156bfb51338c1f8d55fe32cf407774359a", size = 788224, upload-time = "2026-02-19T19:02:47.804Z" },
- { url = "https://files.pythonhosted.org/packages/c8/83/34a2dd601f9deb13c20545c674a55f4a05c90869ab73d985b74d639bac43/regex-2026.2.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c7e121a918bbee3f12ac300ce0a0d2f2c979cf208fb071ed8df5a6323281915c", size = 859682, upload-time = "2026-02-19T19:02:50.583Z" },
- { url = "https://files.pythonhosted.org/packages/8e/30/136db9a09a7f222d6e48b806f3730e7af6499a8cad9c72ac0d49d52c746e/regex-2026.2.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2fedd459c791da24914ecc474feecd94cf7845efb262ac3134fe27cbd7eda799", size = 764223, upload-time = "2026-02-19T19:02:52.777Z" },
- { url = "https://files.pythonhosted.org/packages/9e/ea/bb947743c78a16df481fa0635c50aa1a439bb80b0e6dc24cd4e49c716679/regex-2026.2.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ea8dfc99689240e61fb21b5fc2828f68b90abf7777d057b62d3166b7c1543c4c", size = 850101, upload-time = "2026-02-19T19:02:55.87Z" },
- { url = "https://files.pythonhosted.org/packages/25/27/e3bfe6e97a99f7393665926be02fef772da7f8aa59e50bc3134e4262a032/regex-2026.2.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fff45852160960f29e184ec8a5be5ab4063cfd0b168d439d1fc4ac3744bf29e", size = 789904, upload-time = "2026-02-19T19:02:58.523Z" },
- { url = "https://files.pythonhosted.org/packages/84/7b/7e2be6f00cea59d08761b027ad237002e90cac74b1607200ebaa2ba3d586/regex-2026.2.19-cp314-cp314-win32.whl", hash = "sha256:5390b130cce14a7d1db226a3896273b7b35be10af35e69f1cca843b6e5d2bb2d", size = 271784, upload-time = "2026-02-19T19:03:00.418Z" },
- { url = "https://files.pythonhosted.org/packages/f7/f6/639911530335773e7ec60bcaa519557b719586024c1d7eaad1daf87b646b/regex-2026.2.19-cp314-cp314-win_amd64.whl", hash = "sha256:e581f75d5c0b15669139ca1c2d3e23a65bb90e3c06ba9d9ea194c377c726a904", size = 280506, upload-time = "2026-02-19T19:03:02.302Z" },
- { url = "https://files.pythonhosted.org/packages/cd/ec/2582b56b4e036d46bb9b5d74a18548439ffa16c11cf59076419174d80f48/regex-2026.2.19-cp314-cp314-win_arm64.whl", hash = "sha256:7187fdee1be0896c1499a991e9bf7c78e4b56b7863e7405d7bb687888ac10c4b", size = 273557, upload-time = "2026-02-19T19:03:04.836Z" },
- { url = "https://files.pythonhosted.org/packages/49/0b/f901cfeb4efd83e4f5c3e9f91a6de77e8e5ceb18555698aca3a27e215ed3/regex-2026.2.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5ec1d7c080832fdd4e150c6f5621fe674c70c63b3ae5a4454cebd7796263b175", size = 492196, upload-time = "2026-02-19T19:03:08.188Z" },
- { url = "https://files.pythonhosted.org/packages/94/0a/349b959e3da874e15eda853755567b4cde7e5309dbb1e07bfe910cfde452/regex-2026.2.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8457c1bc10ee9b29cdfd897ccda41dce6bde0e9abd514bcfef7bcd05e254d411", size = 292878, upload-time = "2026-02-19T19:03:10.272Z" },
- { url = "https://files.pythonhosted.org/packages/98/b0/9d81b3c2c5ddff428f8c506713737278979a2c476f6e3675a9c51da0c389/regex-2026.2.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cce8027010d1ffa3eb89a0b19621cdc78ae548ea2b49fea1f7bfb3ea77064c2b", size = 291235, upload-time = "2026-02-19T19:03:12.5Z" },
- { url = "https://files.pythonhosted.org/packages/04/e7/be7818df8691dbe9508c381ea2cc4c1153e4fdb1c4b06388abeaa93bd712/regex-2026.2.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11c138febb40546ff9e026dbbc41dc9fb8b29e61013fa5848ccfe045f5b23b83", size = 807893, upload-time = "2026-02-19T19:03:15.064Z" },
- { url = "https://files.pythonhosted.org/packages/0c/b6/b898a8b983190cfa0276031c17beb73cfd1db07c03c8c37f606d80b655e2/regex-2026.2.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:74ff212aa61532246bb3036b3dfea62233414b0154b8bc3676975da78383cac3", size = 873696, upload-time = "2026-02-19T19:03:17.848Z" },
- { url = "https://files.pythonhosted.org/packages/1a/98/126ba671d54f19080ec87cad228fb4f3cc387fff8c4a01cb4e93f4ff9d94/regex-2026.2.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d00c95a2b6bfeb3ea1cb68d1751b1dfce2b05adc2a72c488d77a780db06ab867", size = 915493, upload-time = "2026-02-19T19:03:20.343Z" },
- { url = "https://files.pythonhosted.org/packages/b2/10/550c84a1a1a7371867fe8be2bea7df55e797cbca4709974811410e195c5d/regex-2026.2.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:311fcccb76af31be4c588d5a17f8f1a059ae8f4b097192896ebffc95612f223a", size = 813094, upload-time = "2026-02-19T19:03:23.287Z" },
- { url = "https://files.pythonhosted.org/packages/29/fb/ba221d2fc76a27b6b7d7a60f73a7a6a7bac21c6ba95616a08be2bcb434b0/regex-2026.2.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77cfd6b5e7c4e8bf7a39d243ea05882acf5e3c7002b0ef4756de6606893b0ecd", size = 781583, upload-time = "2026-02-19T19:03:26.872Z" },
- { url = "https://files.pythonhosted.org/packages/26/f1/af79231301297c9e962679efc04a31361b58dc62dec1fc0cb4b8dd95956a/regex-2026.2.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6380f29ff212ec922b6efb56100c089251940e0526a0d05aa7c2d9b571ddf2fe", size = 795875, upload-time = "2026-02-19T19:03:29.223Z" },
- { url = "https://files.pythonhosted.org/packages/a0/90/1e1d76cb0a2d0a4f38a039993e1c5cd971ae50435d751c5bae4f10e1c302/regex-2026.2.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:655f553a1fa3ab8a7fd570eca793408b8d26a80bfd89ed24d116baaf13a38969", size = 868916, upload-time = "2026-02-19T19:03:31.415Z" },
- { url = "https://files.pythonhosted.org/packages/9a/67/a1c01da76dbcfed690855a284c665cc0a370e7d02d1bd635cf9ff7dd74b8/regex-2026.2.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:015088b8558502f1f0bccd58754835aa154a7a5b0bd9d4c9b7b96ff4ae9ba876", size = 770386, upload-time = "2026-02-19T19:03:33.972Z" },
- { url = "https://files.pythonhosted.org/packages/49/6f/94842bf294f432ff3836bfd91032e2ecabea6d284227f12d1f935318c9c4/regex-2026.2.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9e6693b8567a59459b5dda19104c4a4dbbd4a1c78833eacc758796f2cfef1854", size = 855007, upload-time = "2026-02-19T19:03:36.238Z" },
- { url = "https://files.pythonhosted.org/packages/ff/93/393cd203ca0d1d368f05ce12d2c7e91a324bc93c240db2e6d5ada05835f4/regex-2026.2.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4071209fd4376ab5ceec72ad3507e9d3517c59e38a889079b98916477a871868", size = 799863, upload-time = "2026-02-19T19:03:38.497Z" },
- { url = "https://files.pythonhosted.org/packages/43/d9/35afda99bd92bf1a5831e55a4936d37ea4bed6e34c176a3c2238317faf4f/regex-2026.2.19-cp314-cp314t-win32.whl", hash = "sha256:2905ff4a97fad42f2d0834d8b1ea3c2f856ec209837e458d71a061a7d05f9f01", size = 274742, upload-time = "2026-02-19T19:03:40.804Z" },
- { url = "https://files.pythonhosted.org/packages/ae/42/7edc3344dcc87b698e9755f7f685d463852d481302539dae07135202d3ca/regex-2026.2.19-cp314-cp314t-win_amd64.whl", hash = "sha256:64128549b600987e0f335c2365879895f860a9161f283b14207c800a6ed623d3", size = 284443, upload-time = "2026-02-19T19:03:42.954Z" },
- { url = "https://files.pythonhosted.org/packages/3a/45/affdf2d851b42adf3d13fc5b3b059372e9bd299371fd84cf5723c45871fa/regex-2026.2.19-cp314-cp314t-win_arm64.whl", hash = "sha256:a09ae430e94c049dc6957f6baa35ee3418a3a77f3c12b6e02883bd80a2b679b0", size = 274932, upload-time = "2026-02-19T19:03:45.488Z" },
-]
-
[[package]]
name = "requests"
version = "2.33.0"
@@ -4978,41 +3868,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
]
-[[package]]
-name = "safetensors"
-version = "0.7.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" },
- { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" },
- { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" },
- { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" },
- { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" },
- { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" },
- { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" },
- { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" },
- { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" },
- { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" },
- { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" },
- { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" },
- { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" },
- { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" },
-]
-
-[[package]]
-name = "sentry-sdk"
-version = "2.56.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/de/df/5008954f5466085966468612a7d1638487596ee6d2fd7fb51783a85351bf/sentry_sdk-2.56.0.tar.gz", hash = "sha256:fdab72030b69625665b2eeb9738bdde748ad254e8073085a0ce95382678e8168", size = 426820, upload-time = "2026-03-24T09:56:36.575Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/cd/1a/b3a3e9f6520493fed7997af4d2de7965d71549c62f994a8fd15f2ecd519e/sentry_sdk-2.56.0-py2.py3-none-any.whl", hash = "sha256:5afafb744ceb91d22f4cc650c6bd048ac6af5f7412dcc6c59305a2e36f4dbc02", size = 451568, upload-time = "2026-03-24T09:56:34.807Z" },
-]
-
[[package]]
name = "setuptools"
version = "82.0.1"
@@ -5070,15 +3925,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/e1/bb81f93c9f403e3b573c429dd4838ec9b44e4ef35f3b0759eb49557ab6e3/slack_sdk-3.40.1-py2.py3-none-any.whl", hash = "sha256:cd8902252979aa248092b0d77f3a9ea3cc605bc5d53663ad728e892e26e14a65", size = 313687, upload-time = "2026-02-18T22:11:00.027Z" },
]
-[[package]]
-name = "smmap"
-version = "5.0.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" },
-]
-
[[package]]
name = "sniffio"
version = "1.3.1"
@@ -5113,59 +3959,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/39/a61d4b83a7746b70d23d9173be688c0c6bfc7173772344b7442c2c155497/sounddevice-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", size = 317115, upload-time = "2026-01-23T18:36:42.235Z" },
]
-[[package]]
-name = "sqlalchemy"
-version = "2.0.48"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "greenlet", marker = "(python_full_version >= '3.12' and platform_machine == 'AMD64') or (python_full_version >= '3.12' and platform_machine == 'WIN32') or (python_full_version >= '3.12' and platform_machine == 'aarch64') or (python_full_version >= '3.12' and platform_machine == 'amd64') or (python_full_version >= '3.12' and platform_machine == 'ppc64le') or (python_full_version >= '3.12' and platform_machine == 'win32') or (python_full_version >= '3.12' and platform_machine == 'x86_64')" },
- { name = "typing-extensions", marker = "python_full_version >= '3.12'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" },
- { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" },
- { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" },
- { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" },
- { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" },
- { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" },
- { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" },
- { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" },
- { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" },
- { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" },
- { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" },
- { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" },
- { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" },
- { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" },
- { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" },
- { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" },
- { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" },
- { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" },
- { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" },
- { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" },
- { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" },
- { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" },
- { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" },
- { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" },
- { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" },
- { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" },
- { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" },
- { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" },
- { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" },
- { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" },
- { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" },
- { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" },
- { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" },
- { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" },
- { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" },
- { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" },
- { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" },
- { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" },
- { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" },
- { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" },
- { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" },
-]
-
[[package]]
name = "sse-starlette"
version = "3.3.2"
@@ -5192,35 +3985,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
]
-[[package]]
-name = "streamlit"
-version = "1.55.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "altair", marker = "python_full_version >= '3.12'" },
- { name = "blinker", marker = "python_full_version >= '3.12'" },
- { name = "cachetools", marker = "python_full_version >= '3.12'" },
- { name = "click", marker = "python_full_version >= '3.12'" },
- { name = "gitpython", marker = "python_full_version >= '3.12'" },
- { name = "numpy", marker = "python_full_version >= '3.12'" },
- { name = "packaging", marker = "python_full_version >= '3.12'" },
- { name = "pandas", marker = "python_full_version >= '3.12'" },
- { name = "pillow", marker = "python_full_version >= '3.12'" },
- { name = "protobuf", marker = "python_full_version >= '3.12'" },
- { name = "pyarrow", marker = "python_full_version >= '3.12'" },
- { name = "pydeck", marker = "python_full_version >= '3.12'" },
- { name = "requests", marker = "python_full_version >= '3.12'" },
- { name = "tenacity", marker = "python_full_version >= '3.12'" },
- { name = "toml", marker = "python_full_version >= '3.12'" },
- { name = "tornado", marker = "python_full_version >= '3.12'" },
- { name = "typing-extensions", marker = "python_full_version >= '3.12'" },
- { name = "watchdog", marker = "python_full_version >= '3.12' and sys_platform != 'darwin'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/92/8e/f2b8b4fa8ba65aae251170c54f8ce198fb588fc348301c2b624f8c63efac/streamlit-1.55.0.tar.gz", hash = "sha256:015e512bbd02d000f4047e51118dc086b70e7d9c46b4a11a33c2509731379626", size = 8612008, upload-time = "2026-03-03T22:26:02.149Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/e6/412c1e1f200ca8c32ecf10201839183e261ad61ced3ede34a66f6d4be3cf/streamlit-1.55.0-py3-none-any.whl", hash = "sha256:1e4a16449c6131696180f4ddb40ea8c51834e89c2a43e1b0362bc9b1cfd9b415", size = 9075714, upload-time = "2026-03-03T22:25:59.126Z" },
-]
-
[[package]]
name = "sympy"
version = "1.14.0"
@@ -5272,77 +4036,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" },
]
-[[package]]
-name = "tiktoken"
-version = "0.12.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "regex", marker = "python_full_version >= '3.12'" },
- { name = "requests", marker = "python_full_version >= '3.12'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" },
- { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" },
- { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" },
- { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" },
- { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" },
- { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" },
- { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" },
- { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" },
- { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" },
- { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" },
- { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" },
- { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" },
- { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" },
- { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" },
- { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" },
- { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" },
- { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" },
- { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" },
- { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" },
- { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" },
- { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" },
- { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" },
- { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" },
- { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" },
- { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" },
- { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" },
- { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" },
- { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" },
- { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" },
- { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" },
- { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" },
- { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" },
- { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" },
- { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" },
- { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" },
- { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" },
- { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" },
- { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" },
- { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" },
- { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" },
- { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" },
- { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" },
-]
-
-[[package]]
-name = "tinker"
-version = "0.18.0"
-source = { git = "https://github.com/thinking-machines-lab/tinker.git?rev=30517b667f18a3dfb7ef33fb56cf686d5820ba2b#30517b667f18a3dfb7ef33fb56cf686d5820ba2b" }
-dependencies = [
- { name = "anyio" },
- { name = "click" },
- { name = "distro" },
- { name = "httpx", extra = ["http2"] },
- { name = "numpy" },
- { name = "pydantic" },
- { name = "rich" },
- { name = "sniffio" },
- { name = "transformers" },
- { name = "typing-extensions" },
-]
-
[[package]]
name = "tokenizers"
version = "0.22.2"
@@ -5407,26 +4100,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
]
-[[package]]
-name = "transformers"
-version = "5.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "huggingface-hub" },
- { name = "numpy" },
- { name = "packaging" },
- { name = "pyyaml" },
- { name = "regex" },
- { name = "safetensors" },
- { name = "tokenizers" },
- { name = "tqdm" },
- { name = "typer" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/fc/1a/70e830d53ecc96ce69cfa8de38f163712d2b43ac52fbd743f39f56025c31/transformers-5.3.0.tar.gz", hash = "sha256:009555b364029da9e2946d41f1c5de9f15e6b1df46b189b7293f33a161b9c557", size = 8830831, upload-time = "2026-03-04T17:41:46.119Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b8/88/ae8320064e32679a5429a2c9ebbc05c2bf32cefb6e076f9b07f6d685a9b4/transformers-5.3.0-py3-none-any.whl", hash = "sha256:50ac8c89c3c7033444fb3f9f53138096b997ebb70d4b5e50a2e810bf12d3d29a", size = 10661827, upload-time = "2026-03-04T17:41:42.722Z" },
-]
-
[[package]]
name = "ty"
version = "0.0.21"
@@ -5660,53 +4333,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/65/3a/0137d5b157845e1d41a70130d8dce8ba15d8712f34619693cda04ecb8f02/vercel_workers-0.0.16-py3-none-any.whl", hash = "sha256:542be839e46e236a68cc308695ccc3c970d76de72c978d7f416cc6ce09688896", size = 50141, upload-time = "2026-04-13T21:23:28.652Z" },
]
-[[package]]
-name = "wandb"
-version = "0.25.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "gitpython" },
- { name = "packaging" },
- { name = "platformdirs" },
- { name = "protobuf" },
- { name = "pydantic" },
- { name = "pyyaml" },
- { name = "requests" },
- { name = "sentry-sdk" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/60/bb/eb579bf9abac70934a014a9d4e45346aab307994f3021d201bebe5fa25ec/wandb-0.25.1.tar.gz", hash = "sha256:b2a95cd777ecbe7499599a43158834983448a0048329bc7210ef46ca18d21994", size = 43983308, upload-time = "2026-03-10T23:51:44.227Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e7/d8/873553b6818499d1b1de314067d528b892897baf0dc81fedc0e845abc2dd/wandb-0.25.1-py3-none-macosx_12_0_arm64.whl", hash = "sha256:9bb0679a3e2dcd96db9d9b6d3e17d046241d8d122974b24facb85cc93309a8c9", size = 23615900, upload-time = "2026-03-10T23:51:06.278Z" },
- { url = "https://files.pythonhosted.org/packages/71/ea/b131f319aaa5d0bf7572b6bfcff3dd89e1cf92b17eee443bbab71d12d74c/wandb-0.25.1-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:0fb13ed18914027523e7b4fc20380c520e0d10da0ee452f924a13f84509fbe12", size = 25576144, upload-time = "2026-03-10T23:51:11.527Z" },
- { url = "https://files.pythonhosted.org/packages/70/5f/81508581f0bb77b0495665c1c78e77606a48e66e855ca71ba7c8ae29efa4/wandb-0.25.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:cc4521eb5223429ddab5e8eee9b42fdf4caabdf0bc4e0e809042720e5fbef0ed", size = 23070425, upload-time = "2026-03-10T23:51:15.71Z" },
- { url = "https://files.pythonhosted.org/packages/f2/c7/445155ef010e2e35d190797d7c36ff441e062a5b566a6da4778e22233395/wandb-0.25.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:e73b4c55b947edae349232d5845204d30fac88e18eb4ad1d4b96bf7cf898405a", size = 25628142, upload-time = "2026-03-10T23:51:19.326Z" },
- { url = "https://files.pythonhosted.org/packages/d5/63/f5c55ee00cf481ef1ccd3c385a0585ad52e7840d08419d4f82ddbeeea959/wandb-0.25.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:22b84065aa398e1624d2e5ad79e08bc4d2af41a6db61697b03b3aaba332977c6", size = 23123172, upload-time = "2026-03-10T23:51:23.418Z" },
- { url = "https://files.pythonhosted.org/packages/3e/d9/19eb7974c0e9253bcbaee655222c0f0e1a52e63e9479ee711b4208f8ac31/wandb-0.25.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:005c4c6b5126ef8f4b4110e5372d950918b00637d6dc4b615ad17445f9739478", size = 25714479, upload-time = "2026-03-10T23:51:27.421Z" },
- { url = "https://files.pythonhosted.org/packages/11/19/466c1d03323a4a0ed7d4036a59b18d6b6f67cb5032e444205927e226b18d/wandb-0.25.1-py3-none-win32.whl", hash = "sha256:8f2d04f16b88d65bfba9d79fb945f6c64e2686215469a841936e0972be8ec6a5", size = 24967338, upload-time = "2026-03-10T23:51:31.833Z" },
- { url = "https://files.pythonhosted.org/packages/89/22/680d34c1587f3a979c701b66d71aa7c42b4ef2fdf0774f67034e618e834e/wandb-0.25.1-py3-none-win_amd64.whl", hash = "sha256:62db5166de14456156d7a85953a58733a631228e6d4248a753605f75f75fb845", size = 24967343, upload-time = "2026-03-10T23:51:36.026Z" },
- { url = "https://files.pythonhosted.org/packages/c4/e8/76836b75d401ff5912aaf513176e64557ceaec4c4946bfd38a698ff84d48/wandb-0.25.1-py3-none-win_arm64.whl", hash = "sha256:cc7c34b70cf4b7be4d395541e82e325fd9d2be978d62c9ec01f1a7141523b6bb", size = 22080774, upload-time = "2026-03-10T23:51:40.196Z" },
-]
-
-[[package]]
-name = "watchdog"
-version = "6.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
- { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
- { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
- { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
- { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
- { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
- { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
- { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
- { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
- { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
-]
-
[[package]]
name = "watchfiles"
version = "1.1.1"
@@ -5904,109 +4530,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
]
-[[package]]
-name = "xxhash"
-version = "3.6.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" },
- { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" },
- { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" },
- { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" },
- { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" },
- { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" },
- { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" },
- { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" },
- { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" },
- { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" },
- { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" },
- { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" },
- { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" },
- { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" },
- { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" },
- { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" },
- { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" },
- { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" },
- { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" },
- { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" },
- { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" },
- { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" },
- { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" },
- { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" },
- { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" },
- { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" },
- { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" },
- { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" },
- { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" },
- { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" },
- { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" },
- { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" },
- { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" },
- { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" },
- { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" },
- { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" },
- { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" },
- { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" },
- { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" },
- { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" },
- { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" },
- { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" },
- { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" },
- { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" },
- { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" },
- { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" },
- { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" },
- { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" },
- { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" },
- { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" },
- { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" },
- { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" },
- { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" },
- { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" },
- { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" },
- { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" },
- { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" },
- { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" },
- { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" },
- { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" },
- { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" },
- { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" },
- { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" },
- { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" },
- { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" },
- { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" },
- { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" },
- { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" },
- { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" },
- { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" },
- { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" },
- { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" },
- { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" },
- { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" },
- { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" },
- { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" },
- { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" },
- { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" },
- { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" },
- { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" },
- { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" },
- { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" },
- { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" },
- { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" },
- { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" },
- { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" },
- { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" },
- { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" },
- { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" },
- { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" },
- { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" },
- { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" },
- { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" },
- { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" },
- { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" },
-]
-
[[package]]
name = "yarl"
version = "1.22.0"
@@ -6117,21 +4640,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
]
-[[package]]
-name = "yc-bench"
-version = "0.1.0"
-source = { git = "https://github.com/collinear-ai/yc-bench.git?rev=bfb0c88062450f46341bd9a5298903fc2e952a5c#bfb0c88062450f46341bd9a5298903fc2e952a5c" }
-dependencies = [
- { name = "litellm", marker = "python_full_version >= '3.12'" },
- { name = "matplotlib", marker = "python_full_version >= '3.12'" },
- { name = "plotly", marker = "python_full_version >= '3.12'" },
- { name = "pydantic", marker = "python_full_version >= '3.12'" },
- { name = "python-dotenv", marker = "python_full_version >= '3.12'" },
- { name = "sqlalchemy", marker = "python_full_version >= '3.12'" },
- { name = "streamlit", marker = "python_full_version >= '3.12'" },
- { name = "typer", marker = "python_full_version >= '3.12'" },
-]
-
[[package]]
name = "youtube-transcript-api"
version = "1.2.4"
diff --git a/website/docs/developer-guide/architecture.md b/website/docs/developer-guide/architecture.md
index af2b0a2fd4b..b5e2add8993 100644
--- a/website/docs/developer-guide/architecture.md
+++ b/website/docs/developer-guide/architecture.md
@@ -127,7 +127,6 @@ hermes-agent/
├── cron/ # Scheduler (jobs.py, scheduler.py)
├── plugins/memory/ # Memory provider plugins
├── plugins/context_engine/ # Context engine plugins
-├── environments/ # RL training environments (Atropos)
├── skills/ # Bundled skills (always available)
├── optional-skills/ # Official optional skills (install explicitly)
├── website/ # Docusaurus documentation site
@@ -185,7 +184,6 @@ If you are new to the codebase:
8. **[Gateway Internals](./gateway-internals.md)** — messaging platform gateway
9. **[Context Compression & Prompt Caching](./context-compression-and-caching.md)** — compression and caching
10. **[ACP Internals](./acp-internals.md)** — IDE integration
-11. **[Environments, Benchmarks & Data Generation](./environments.md)** — RL training
## Major Subsystems
@@ -247,11 +245,11 @@ Exposes Hermes as an editor-native agent over stdio/JSON-RPC for VS Code, Zed, a
→ [ACP Internals](./acp-internals.md)
-### RL / Environments / Trajectories
+### Trajectories
-Full environment framework for evaluation and RL training. Integrates with Atropos, supports multiple tool-call parsers, and generates ShareGPT-format trajectories.
+Generates ShareGPT-format trajectories from agent sessions for training data generation.
-→ [Environments, Benchmarks & Data Generation](./environments.md), [Trajectories & Training Format](./trajectory-format.md)
+→ [Trajectories & Training Format](./trajectory-format.md)
## Design Principles
diff --git a/website/docs/developer-guide/contributing.md b/website/docs/developer-guide/contributing.md
index 6e00e367330..b3bf9799d71 100644
--- a/website/docs/developer-guide/contributing.md
+++ b/website/docs/developer-guide/contributing.md
@@ -50,9 +50,6 @@ export VIRTUAL_ENV="$(pwd)/venv"
# Install with all extras (messaging, cron, CLI menus, dev tools)
uv pip install -e ".[all,dev]"
-# tinker-atropos is a git submodule — needs `git submodule update --init` first
-# if you didn't clone with `--recurse-submodules`
-uv pip install -e "./tinker-atropos"
# Optional: browser tools
npm install
diff --git a/website/docs/developer-guide/environments.md b/website/docs/developer-guide/environments.md
deleted file mode 100644
index 0a5aa00ffff..00000000000
--- a/website/docs/developer-guide/environments.md
+++ /dev/null
@@ -1,520 +0,0 @@
----
-sidebar_position: 5
-title: "Environments, Benchmarks & Data Generation"
-description: "Building RL training environments, running evaluation benchmarks, and generating SFT data with the Hermes-Agent Atropos integration"
----
-
-# Environments, Benchmarks & Data Generation
-
-Hermes Agent includes a full environment framework that connects its tool-calling capabilities to the [Atropos](https://github.com/NousResearch/atropos) RL training framework. This enables three workflows:
-
-1. **RL Training** — Train language models on multi-turn agentic tasks with GRPO
-2. **Benchmarks** — Evaluate models on standardised agentic benchmarks
-3. **Data Generation** — Generate SFT training data from agent rollouts
-
-All three share the same core: an **environment** class that defines tasks, runs an agent loop, and scores the output.
-
-:::info Repo environments vs RL training tools
-The Python environment framework documented here lives under the repo's `environments/` directory and is the implementation-level API for Hermes/Atropos integration. This is separate from the user-facing `rl_*` tools, which operate as an orchestration surface for remote RL training workflows.
-:::
-
-:::tip Quick Links
-- **Want to run benchmarks?** Jump to [Available Benchmarks](#available-benchmarks)
-- **Want to train with RL?** See [RL Training Tools](/user-guide/features/rl-training) for the agent-driven interface, or [Running Environments](#running-environments) for manual execution
-- **Want to create a new environment?** See [Creating Environments](#creating-environments)
-:::
-
-## Architecture
-
-The environment system is built on a three-layer inheritance chain:
-
-```mermaid
-classDiagram
- class BaseEnv {
- Server management
- Worker scheduling
- Wandb logging
- CLI: serve / process / evaluate
- }
-
- class HermesAgentBaseEnv {
- Terminal backend configuration
- Tool resolution
- Agent loop engine
- ToolContext access
- }
-
- class TerminalTestEnv {
- Stack testing
- }
-
- class HermesSweEnv {
- SWE training
- }
-
- class TerminalBench2EvalEnv {
- Benchmark evaluation
- }
-
- class TBLiteEvalEnv {
- Fast benchmark
- }
-
- class YCBenchEvalEnv {
- Long-horizon benchmark
- }
-
- BaseEnv <|-- HermesAgentBaseEnv
- HermesAgentBaseEnv <|-- TerminalTestEnv
- HermesAgentBaseEnv <|-- HermesSweEnv
- HermesAgentBaseEnv <|-- TerminalBench2EvalEnv
- TerminalBench2EvalEnv <|-- TBLiteEvalEnv
- TerminalBench2EvalEnv <|-- YCBenchEvalEnv
-```
-
-### BaseEnv (Atropos)
-
-The foundation from `atroposlib`. Provides:
-- **Server management** — connects to OpenAI-compatible APIs (VLLM, SGLang, OpenRouter)
-- **Worker scheduling** — parallel rollout coordination
-- **Wandb integration** — metrics logging and rollout visualisation
-- **CLI interface** — three subcommands: `serve`, `process`, `evaluate`
-- **Eval logging** — `evaluate_log()` saves results to JSON + JSONL
-
-### HermesAgentBaseEnv
-
-The hermes-agent layer (`environments/hermes_base_env.py`). Adds:
-- **Terminal backend configuration** — sets `TERMINAL_ENV` for sandboxed execution (local, Docker, Modal, Daytona, SSH, Singularity)
-- **Tool resolution** — `_resolve_tools_for_group()` calls hermes-agent's `get_tool_definitions()` to get the right tool schemas based on enabled/disabled toolsets
-- **Agent loop integration** — `collect_trajectory()` runs `HermesAgentLoop` and scores the result
-- **Two-phase operation** — Phase 1 (OpenAI server) for eval/SFT, Phase 2 (VLLM ManagedServer) for full RL with logprobs
-- **Async safety patches** — monkey-patches Modal backend to work inside Atropos's event loop
-
-### Concrete Environments
-
-Your environment inherits from `HermesAgentBaseEnv` and implements five methods:
-
-| Method | Purpose |
-|--------|---------|
-| `setup()` | Load dataset, initialise state |
-| `get_next_item()` | Return the next item for rollout |
-| `format_prompt(item)` | Convert an item into the user message |
-| `compute_reward(item, result, ctx)` | Score the rollout (0.0–1.0) |
-| `evaluate()` | Periodic evaluation logic |
-
-## Core Components
-
-### Agent Loop
-
-`HermesAgentLoop` (`environments/agent_loop.py`) is the reusable multi-turn agent engine. It runs the same tool-calling pattern as hermes-agent's main loop:
-
-1. Send messages + tool schemas to the API via `server.chat_completion()`
-2. If the response contains `tool_calls`, dispatch each via `handle_function_call()`
-3. Append tool results to the conversation, go back to step 1
-4. If no `tool_calls`, the agent is done
-
-Tool calls execute in a thread pool (`ThreadPoolExecutor(128)`) so that async backends (Modal, Docker) don't deadlock inside Atropos's event loop.
-
-Returns an `AgentResult`:
-
-```python
-@dataclass
-class AgentResult:
- messages: List[Dict[str, Any]] # Full conversation history
- turns_used: int # Number of LLM calls made
- finished_naturally: bool # True if model stopped on its own
- reasoning_per_turn: List[Optional[str]] # Extracted reasoning content
- tool_errors: List[ToolError] # Errors encountered during tool dispatch
- managed_state: Optional[Dict] # VLLM ManagedServer state (Phase 2)
-```
-
-### Tool Context
-
-`ToolContext` (`environments/tool_context.py`) gives reward functions direct access to the **same sandbox** the model used during its rollout. The `task_id` scoping means all state (files, processes, browser tabs) is preserved.
-
-```python
-async def compute_reward(self, item, result, ctx: ToolContext):
- # Run tests in the model's terminal sandbox
- test = ctx.terminal("pytest -v")
- if test["exit_code"] == 0:
- return 1.0
-
- # Check if a file was created
- content = ctx.read_file("/workspace/solution.py")
- if content.get("content"):
- return 0.5
-
- # Download files for local verification
- ctx.download_file("/remote/output.bin", "/local/output.bin")
- return 0.0
-```
-
-Available methods:
-
-| Category | Methods |
-|----------|---------|
-| **Terminal** | `terminal(command, timeout)` |
-| **Files** | `read_file(path)`, `write_file(path, content)`, `search(query, path)` |
-| **Transfers** | `upload_file()`, `upload_dir()`, `download_file()`, `download_dir()` |
-| **Web** | `web_search(query)`, `web_extract(urls)` |
-| **Browser** | `browser_navigate(url)`, `browser_snapshot()` |
-| **Generic** | `call_tool(name, args)` — escape hatch for any hermes-agent tool |
-| **Cleanup** | `cleanup()` — release all resources |
-
-### Tool Call Parsers
-
-For **Phase 2** (VLLM ManagedServer), the server returns raw text without structured tool calls. Client-side parsers in `environments/tool_call_parsers/` extract `tool_calls` from raw output:
-
-```python
-from environments.tool_call_parsers import get_parser
-
-parser = get_parser("hermes") # or "mistral", "llama3_json", "qwen", "deepseek_v3", etc.
-content, tool_calls = parser.parse(raw_model_output)
-```
-
-Available parsers: `hermes`, `mistral`, `llama3_json`, `llama4_json`, `qwen`, `qwen3_coder`, `deepseek_v3`, `deepseek_v3_1` (alias `deepseek_v31`), `kimi_k2`, `longcat`, `glm45`, `glm47`.
-
-In Phase 1 (OpenAI server type), parsers are not needed — the server handles tool call parsing natively.
-
-## Available Benchmarks
-
-### TerminalBench2
-
-**89 challenging terminal tasks** with per-task Docker sandbox environments.
-
-| | |
-|---|---|
-| **What it tests** | Single-task coding/sysadmin ability |
-| **Scoring** | Binary pass/fail (test suite verification) |
-| **Sandbox** | Modal cloud sandboxes (per-task Docker images) |
-| **Tools** | `terminal` + `file` |
-| **Tasks** | 89 tasks across multiple categories |
-| **Cost** | ~$50–200 for full eval (parallel execution) |
-| **Time** | ~2–4 hours |
-
-```bash
-python environments/benchmarks/terminalbench_2/terminalbench2_env.py evaluate \
- --config environments/benchmarks/terminalbench_2/default.yaml
-
-# Run specific tasks
-python environments/benchmarks/terminalbench_2/terminalbench2_env.py evaluate \
- --config environments/benchmarks/terminalbench_2/default.yaml \
- --env.task_filter fix-git,git-multibranch
-```
-
-Dataset: [NousResearch/terminal-bench-2](https://huggingface.co/datasets/NousResearch/terminal-bench-2) on HuggingFace.
-
-### TBLite (OpenThoughts Terminal Bench Lite)
-
-**100 difficulty-calibrated tasks** — a faster proxy for TerminalBench2.
-
-| | |
-|---|---|
-| **What it tests** | Same as TB2 (coding/sysadmin), calibrated difficulty tiers |
-| **Scoring** | Binary pass/fail |
-| **Sandbox** | Modal cloud sandboxes |
-| **Tools** | `terminal` + `file` |
-| **Tasks** | 100 tasks: Easy (40), Medium (26), Hard (26), Extreme (8) |
-| **Correlation** | r=0.911 with full TB2 |
-| **Speed** | 2.6–8× faster than TB2 |
-
-```bash
-python environments/benchmarks/tblite/tblite_env.py evaluate \
- --config environments/benchmarks/tblite/default.yaml
-```
-
-TBLite is a thin subclass of TerminalBench2 — only the dataset and timeouts differ. Created by the OpenThoughts Agent team (Snorkel AI + Bespoke Labs). Dataset: [NousResearch/openthoughts-tblite](https://huggingface.co/datasets/NousResearch/openthoughts-tblite).
-
-### YC-Bench
-
-**Long-horizon strategic benchmark** — the agent plays CEO of an AI startup.
-
-| | |
-|---|---|
-| **What it tests** | Multi-turn strategic coherence over hundreds of turns |
-| **Scoring** | Composite: `0.5 × survival + 0.5 × normalised_funds` |
-| **Sandbox** | Local terminal (no Modal needed) |
-| **Tools** | `terminal` only |
-| **Runs** | 9 default (3 presets × 3 seeds), sequential |
-| **Cost** | ~$50–200 for full eval |
-| **Time** | ~3–6 hours |
-
-```bash
-# Install yc-bench (optional dependency)
-pip install "hermes-agent[yc-bench]"
-
-# Run evaluation
-bash environments/benchmarks/yc_bench/run_eval.sh
-
-# Or directly
-python environments/benchmarks/yc_bench/yc_bench_env.py evaluate \
- --config environments/benchmarks/yc_bench/default.yaml
-
-# Quick single-preset test
-python environments/benchmarks/yc_bench/yc_bench_env.py evaluate \
- --config environments/benchmarks/yc_bench/default.yaml \
- --env.presets '["fast_test"]' --env.seeds '[1]'
-```
-
-YC-Bench uses [collinear-ai/yc-bench](https://github.com/collinear-ai/yc-bench) — a deterministic simulation with 4 skill domains (research, inference, data_environment, training), prestige system, employee management, and financial pressure. Unlike TB2's per-task binary scoring, YC-Bench measures whether an agent can maintain coherent strategy over hundreds of compounding decisions.
-
-## Training Environments
-
-### TerminalTestEnv
-
-A minimal self-contained environment with inline tasks (no external dataset). Used for **validating the full stack** end-to-end. Each task asks the model to create a file at a known path; the verifier checks the content.
-
-```bash
-# Process mode (saves rollouts to JSONL, no training server needed)
-python environments/terminal_test_env/terminal_test_env.py process \
- --env.data_path_to_save_groups terminal_test_output.jsonl
-
-# Serve mode (connects to Atropos API for RL training)
-python environments/terminal_test_env/terminal_test_env.py serve
-```
-
-### HermesSweEnv
-
-SWE-bench style training environment. The model gets a coding task, uses terminal + file + web tools to solve it, and the reward function runs tests in the same Modal sandbox.
-
-```bash
-python environments/hermes_swe_env/hermes_swe_env.py serve \
- --openai.model_name YourModel \
- --env.dataset_name bigcode/humanevalpack \
- --env.terminal_backend modal
-```
-
-## Running Environments
-
-Every environment is a standalone Python script with three CLI subcommands:
-
-### `evaluate` — Run a benchmark
-
-For eval-only environments (benchmarks). Runs all items, computes metrics, logs to wandb.
-
-```bash
-python environments/benchmarks/tblite/tblite_env.py evaluate \
- --config environments/benchmarks/tblite/default.yaml \
- --openai.model_name anthropic/claude-sonnet-4.6
-```
-
-No training server or `run-api` needed. The environment handles everything.
-
-### `process` — Generate SFT data
-
-Runs rollouts and saves scored trajectories to JSONL. Useful for generating training data without a full RL loop.
-
-```bash
-python environments/terminal_test_env/terminal_test_env.py process \
- --env.data_path_to_save_groups output.jsonl \
- --openai.model_name anthropic/claude-sonnet-4.6
-```
-
-Output format: each line is a scored trajectory with the full conversation history, reward, and metadata.
-
-### `serve` — Connect to Atropos for RL training
-
-Connects the environment to a running Atropos API server (`run-api`). Used during live RL training.
-
-```bash
-# Terminal 1: Start the Atropos API
-run-api
-
-# Terminal 2: Start the environment
-python environments/hermes_swe_env/hermes_swe_env.py serve \
- --openai.model_name YourModel
-```
-
-The environment receives items from Atropos, runs agent rollouts, computes rewards, and sends scored trajectories back for training.
-
-## Two-Phase Operation
-
-### Phase 1: OpenAI Server (Eval / SFT)
-
-Uses `server.chat_completion()` with `tools=` parameter. The server (VLLM, SGLang, OpenRouter, OpenAI) handles tool call parsing natively. Returns `ChatCompletion` objects with structured `tool_calls`.
-
-- **Use for**: evaluation, SFT data generation, benchmarks, testing
-- **Placeholder tokens** are created for the Atropos pipeline (since real token IDs aren't available from the OpenAI API)
-
-### Phase 2: VLLM ManagedServer (Full RL)
-
-Uses ManagedServer for exact token IDs + logprobs via `/generate`. A client-side [tool call parser](#tool-call-parsers) reconstructs structured `tool_calls` from raw output.
-
-- **Use for**: full RL training with GRPO/PPO
-- **Real tokens**, masks, and logprobs flow through the pipeline
-- Set `tool_call_parser` in config to match your model's format (e.g., `"hermes"`, `"qwen"`, `"mistral"`)
-
-## Creating Environments
-
-### Training Environment
-
-```python
-from environments.hermes_base_env import HermesAgentBaseEnv, HermesAgentEnvConfig
-from atroposlib.envs.server_handling.server_manager import APIServerConfig
-
-class MyEnvConfig(HermesAgentEnvConfig):
- my_custom_field: str = "default_value"
-
-class MyEnv(HermesAgentBaseEnv):
- name = "my-env"
- env_config_cls = MyEnvConfig
-
- @classmethod
- def config_init(cls):
- env_config = MyEnvConfig(
- enabled_toolsets=["terminal", "file"],
- terminal_backend="modal",
- max_agent_turns=30,
- )
- server_configs = [APIServerConfig(
- base_url="https://openrouter.ai/api/v1",
- model_name="anthropic/claude-sonnet-4.6",
- server_type="openai",
- )]
- return env_config, server_configs
-
- async def setup(self):
- from datasets import load_dataset
- self.dataset = list(load_dataset("my-dataset", split="train"))
- self.iter = 0
-
- async def get_next_item(self):
- item = self.dataset[self.iter % len(self.dataset)]
- self.iter += 1
- return item
-
- def format_prompt(self, item):
- return item["instruction"]
-
- async def compute_reward(self, item, result, ctx):
- # ctx gives full tool access to the rollout's sandbox
- test = ctx.terminal("pytest -v")
- return 1.0 if test["exit_code"] == 0 else 0.0
-
- async def evaluate(self, *args, **kwargs):
- # Periodic evaluation during training
- pass
-
-if __name__ == "__main__":
- MyEnv.cli()
-```
-
-### Eval-Only Benchmark
-
-For benchmarks, follow the pattern used by TerminalBench2, TBLite, and YC-Bench:
-
-1. **Create under** `environments/benchmarks/your-benchmark/`
-2. **Set eval-only config**: `eval_handling=STOP_TRAIN`, `steps_per_eval=1`, `total_steps=1`
-3. **Stub training methods**: `collect_trajectories()` returns `(None, [])`, `score()` returns `None`
-4. **Implement** `rollout_and_score_eval(eval_item)` — the per-item agent loop + scoring
-5. **Implement** `evaluate()` — orchestrates all runs, computes aggregate metrics
-6. **Add streaming JSONL** for crash-safe result persistence
-7. **Add cleanup**: `KeyboardInterrupt` handling, `cleanup_all_environments()`, `_tool_executor.shutdown()`
-8. **Run with** `evaluate` subcommand
-
-See `environments/benchmarks/yc_bench/yc_bench_env.py` for a clean, well-documented reference implementation.
-
-## Configuration Reference
-
-### HermesAgentEnvConfig Fields
-
-| Field | Type | Default | Description |
-|-------|------|---------|-------------|
-| `enabled_toolsets` | `List[str]` | `None` (all) | Which hermes toolsets to enable |
-| `disabled_toolsets` | `List[str]` | `None` | Toolsets to filter out |
-| `distribution` | `str` | `None` | Probabilistic toolset distribution name |
-| `max_agent_turns` | `int` | `30` | Max LLM calls per rollout |
-| `agent_temperature` | `float` | `1.0` | Sampling temperature |
-| `system_prompt` | `str` | `None` | System message for the agent |
-| `terminal_backend` | `str` | `"local"` | `local`, `docker`, `modal`, `daytona`, `ssh`, `singularity` |
-| `terminal_timeout` | `int` | `120` | Seconds per terminal command |
-| `terminal_lifetime` | `int` | `3600` | Max sandbox lifetime |
-| `dataset_name` | `str` | `None` | HuggingFace dataset identifier |
-| `tool_pool_size` | `int` | `128` | Thread pool size for tool execution |
-| `tool_call_parser` | `str` | `"hermes"` | Parser for Phase 2 raw output |
-| `extra_body` | `Dict` | `None` | Extra params for OpenAI API (e.g., OpenRouter provider prefs) |
-| `eval_handling` | `Enum` | `STOP_TRAIN` | `STOP_TRAIN`, `LIMIT_TRAIN`, `NONE` |
-
-### YAML Configuration
-
-Environments can be configured via YAML files passed with `--config`:
-
-```yaml
-env:
- enabled_toolsets: ["terminal", "file"]
- max_agent_turns: 60
- max_token_length: 32000
- agent_temperature: 0.8
- terminal_backend: "modal"
- terminal_timeout: 300
- dataset_name: "NousResearch/terminal-bench-2"
- tokenizer_name: "NousResearch/Hermes-3-Llama-3.1-8B"
- use_wandb: true
- wandb_name: "my-benchmark"
-
-openai:
- base_url: "https://openrouter.ai/api/v1"
- model_name: "anthropic/claude-sonnet-4.6"
- server_type: "openai"
- health_check: false
-```
-
-YAML values override `config_init()` defaults. CLI arguments override YAML values:
-
-```bash
-python my_env.py evaluate \
- --config my_config.yaml \
- --openai.model_name anthropic/claude-opus-4.6 # overrides YAML
-```
-
-## Prerequisites
-
-### For all environments
-
-- Python >= 3.11
-- `atroposlib`: `pip install git+https://github.com/NousResearch/atropos.git`
-- An LLM API key (OpenRouter, OpenAI, or self-hosted VLLM/SGLang)
-
-### For Modal-sandboxed benchmarks (TB2, TBLite)
-
-- [Modal](https://modal.com) account and CLI: `pip install "hermes-agent[modal]"`
-- `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET` environment variables
-
-### For YC-Bench
-
-- `pip install "hermes-agent[yc-bench]"` (installs the yc-bench CLI + SQLAlchemy)
-- No Modal needed — runs with local terminal backend
-
-### For RL training
-
-- `TINKER_API_KEY` — API key for the [Tinker](https://tinker.computer) training service
-- `WANDB_API_KEY` — for Weights & Biases metrics tracking
-- The `tinker-atropos` submodule (at `tinker-atropos/` in the repo)
-
-See [RL Training](/user-guide/features/rl-training) for the agent-driven RL workflow.
-
-## Directory Structure
-
-```
-environments/
-├── hermes_base_env.py # Abstract base class (HermesAgentBaseEnv)
-├── agent_loop.py # Multi-turn agent engine (HermesAgentLoop)
-├── tool_context.py # Per-rollout tool access for reward functions
-├── patches.py # Async-safety patches for Modal backend
-│
-├── tool_call_parsers/ # Phase 2 client-side parsers
-│ ├── hermes_parser.py # Hermes/ChatML format
-│ ├── mistral_parser.py # Mistral [TOOL_CALLS] format
-│ ├── llama_parser.py # Llama 3 JSON tool calling
-│ ├── qwen_parser.py # Qwen format
-│ ├── deepseek_v3_parser.py # DeepSeek V3 format
-│ └── ... # + kimi_k2, longcat, glm45/47, etc.
-│
-├── terminal_test_env/ # Stack validation (inline tasks)
-├── hermes_swe_env/ # SWE-bench training environment
-│
-└── benchmarks/ # Evaluation benchmarks
- ├── terminalbench_2/ # 89 terminal tasks, Modal sandboxes
- ├── tblite/ # 100 calibrated tasks (fast TB2 proxy)
- └── yc_bench/ # Long-horizon strategic benchmark
-```
diff --git a/website/docs/getting-started/updating.md b/website/docs/getting-started/updating.md
index 55df5a7f640..aa2a426db99 100644
--- a/website/docs/getting-started/updating.md
+++ b/website/docs/getting-started/updating.md
@@ -123,13 +123,11 @@ If you installed manually (not via the quick installer):
cd /path/to/hermes-agent
export VIRTUAL_ENV="$(pwd)/venv"
-# Pull latest code and submodules
+# Pull latest code
git pull origin main
-git submodule update --init --recursive
# Reinstall (picks up new dependencies)
uv pip install -e ".[all]"
-uv pip install -e "./tinker-atropos"
# Check for new config options
hermes config check
diff --git a/website/docs/integrations/index.md b/website/docs/integrations/index.md
index 21235a12ba1..d80a61abd8c 100644
--- a/website/docs/integrations/index.md
+++ b/website/docs/integrations/index.md
@@ -97,5 +97,4 @@ See the [Messaging Gateway overview](/docs/user-guide/messaging) for the platfor
## Training & Evaluation
-- **[RL Training](/docs/user-guide/features/rl-training)** — Generate trajectory data from agent sessions for reinforcement learning and model fine-tuning. Supports Atropos environments with customizable reward functions.
- **[Batch Processing](/docs/user-guide/features/batch-processing)** — Run the agent across hundreds of prompts in parallel, generating structured ShareGPT-format trajectory data for training data generation or evaluation.
diff --git a/website/docs/integrations/providers.md b/website/docs/integrations/providers.md
index b53ab15ed84..af9e07814d7 100644
--- a/website/docs/integrations/providers.md
+++ b/website/docs/integrations/providers.md
@@ -1355,7 +1355,6 @@ You can switch between providers at any time with `hermes model` — no restart
| Premium TTS voices | [ElevenLabs](https://elevenlabs.io/) | `ELEVENLABS_API_KEY` |
| OpenAI TTS + voice transcription | [OpenAI](https://platform.openai.com/api-keys) | `VOICE_TOOLS_OPENAI_KEY` |
| Mistral TTS + voice transcription | [Mistral](https://console.mistral.ai/) | `MISTRAL_API_KEY` |
-| RL Training | [Tinker](https://tinker-console.thinkingmachines.ai/) + [WandB](https://wandb.ai/) | `TINKER_API_KEY`, `WANDB_API_KEY` |
| Cross-session user modeling | [Honcho](https://honcho.dev/) | `HONCHO_API_KEY` |
| Semantic long-term memory | [Supermemory](https://supermemory.ai) | `SUPERMEMORY_API_KEY` |
diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md
index 4b581877849..93107fba147 100644
--- a/website/docs/reference/environment-variables.md
+++ b/website/docs/reference/environment-variables.md
@@ -148,8 +148,6 @@ For native Anthropic auth, Hermes prefers Claude Code's own credential files whe
| `HONCHO_BASE_URL` | Base URL for self-hosted Honcho instances (default: Honcho cloud). No API key required for local instances |
| `HINDSIGHT_TIMEOUT` | Timeout in seconds for Hindsight memory-provider API calls (default: `60`). Bump this if your Hindsight instance is slow to respond during `/sync` or `on_session_switch` and you're seeing timeouts in `errors.log`. |
| `SUPERMEMORY_API_KEY` | Semantic long-term memory with profile recall and session ingest ([supermemory.ai](https://supermemory.ai)) |
-| `TINKER_API_KEY` | RL training ([tinker-console.thinkingmachines.ai](https://tinker-console.thinkingmachines.ai/)) |
-| `WANDB_API_KEY` | RL training metrics ([wandb.ai](https://wandb.ai/)) |
| `DAYTONA_API_KEY` | Daytona cloud sandboxes ([daytona.io](https://daytona.io/)) |
| `VERCEL_TOKEN` | Vercel Sandbox access token ([vercel.com](https://vercel.com/)) |
| `VERCEL_PROJECT_ID` | Vercel project ID (required with `VERCEL_TOKEN`) |
diff --git a/website/docs/reference/optional-skills-catalog.md b/website/docs/reference/optional-skills-catalog.md
index 40f9c5539c8..8c4c2f36432 100644
--- a/website/docs/reference/optional-skills-catalog.md
+++ b/website/docs/reference/optional-skills-catalog.md
@@ -120,7 +120,6 @@ hermes skills uninstall
| [**faiss**](/docs/user-guide/skills/optional/mlops/mlops-faiss) | Facebook's library for efficient similarity search and clustering of dense vectors. Supports billions of vectors, GPU acceleration, and various index types (Flat, IVF, HNSW). Use for fast k-NN search, large-scale vector retrieval, or whe... |
| [**optimizing-attention-flash**](/docs/user-guide/skills/optional/mlops/mlops-flash-attention) | Optimizes transformer attention with Flash Attention for 2-4x speedup and 10-20x memory reduction. Use when training/running transformers with long sequences (>512 tokens), encountering GPU memory issues with attention, or need faster in... |
| [**guidance**](/docs/user-guide/skills/optional/mlops/mlops-guidance) | Control LLM output with regex and grammars, guarantee valid JSON/XML/code generation, enforce structured formats, and build multi-step workflows with Guidance - Microsoft Research's constrained generation framework |
-| [**hermes-atropos-environments**](/docs/user-guide/skills/optional/mlops/mlops-hermes-atropos-environments) | Build, test, and debug Hermes Agent RL environments for Atropos training. Covers the HermesAgentBaseEnv interface, reward functions, agent loop integration, evaluation with tools, wandb logging, and the three CLI modes (serve/process/eva... |
| [**huggingface-tokenizers**](/docs/user-guide/skills/optional/mlops/mlops-huggingface-tokenizers) | Fast tokenizers optimized for research and production. Rust-based implementation tokenizes 1GB in <20 seconds. Supports BPE, WordPiece, and Unigram algorithms. Train custom vocabularies, track alignments, handle padding/truncation. Integ... |
| [**instructor**](/docs/user-guide/skills/optional/mlops/mlops-instructor) | Extract structured data from LLM responses with Pydantic validation, retry failed extractions automatically, parse complex JSON with type safety, and stream partial results with Instructor - battle-tested structured output library |
| [**lambda-labs-gpu-cloud**](/docs/user-guide/skills/optional/mlops/mlops-lambda-labs) | Reserved and on-demand GPU cloud instances for ML training and inference. Use when you need dedicated GPU instances with simple SSH access, persistent filesystems, or high-performance multi-node clusters for large-scale training. |
diff --git a/website/docs/reference/tools-reference.md b/website/docs/reference/tools-reference.md
index 5d0100de79d..03930264f8c 100644
--- a/website/docs/reference/tools-reference.md
+++ b/website/docs/reference/tools-reference.md
@@ -148,21 +148,6 @@ Registered only when the agent is spawned by the kanban dispatcher (`HERMES_KANB
|------|-------------|----------------------|
| `mixture_of_agents` | Route a hard problem through multiple frontier LLMs collaboratively. Makes 5 API calls (4 reference models + 1 aggregator) with maximum reasoning effort — use sparingly for genuinely difficult problems. Best for: complex math, advanced alg… | OPENROUTER_API_KEY |
-## `rl` toolset
-
-| Tool | Description | Requires environment |
-|------|-------------|----------------------|
-| `rl_check_status` | Get status and metrics for a training run. RATE LIMITED: enforces 30-minute minimum between checks for the same run. Returns WandB metrics: step, state, reward_mean, loss, percent_correct. | TINKER_API_KEY, WANDB_API_KEY |
-| `rl_edit_config` | Update a configuration field. Use rl_get_current_config() first to see all available fields for the selected environment. Each environment has different configurable options. Infrastructure settings (tokenizer, URLs, lora_rank, learning_ra… | TINKER_API_KEY, WANDB_API_KEY |
-| `rl_get_current_config` | Get the current environment configuration. Returns only fields that can be modified: group_size, max_token_length, total_steps, steps_per_eval, use_wandb, wandb_name, max_num_workers. | TINKER_API_KEY, WANDB_API_KEY |
-| `rl_get_results` | Get final results and metrics for a completed training run. Returns final metrics and path to trained weights. | TINKER_API_KEY, WANDB_API_KEY |
-| `rl_list_environments` | List all available RL environments. Returns environment names, paths, and descriptions. TIP: Read the file_path with file tools to understand how each environment works (verifiers, data loading, rewards). | TINKER_API_KEY, WANDB_API_KEY |
-| `rl_list_runs` | List all training runs (active and completed) with their status. | TINKER_API_KEY, WANDB_API_KEY |
-| `rl_select_environment` | Select an RL environment for training. Loads the environment's default configuration. After selecting, use rl_get_current_config() to see settings and rl_edit_config() to modify them. | TINKER_API_KEY, WANDB_API_KEY |
-| `rl_start_training` | Start a new RL training run with the current environment and config. Most training parameters (lora_rank, learning_rate, etc.) are fixed. Use rl_edit_config() to set group_size, batch_size, wandb_project before starting. WARNING: Training… | TINKER_API_KEY, WANDB_API_KEY |
-| `rl_stop_training` | Stop a running training job. Use if metrics look bad, training is stagnant, or you want to try different settings. | TINKER_API_KEY, WANDB_API_KEY |
-| `rl_test_inference` | Quick inference test for any environment. Runs a few steps of inference + scoring using OpenRouter. Default: 3 steps x 16 completions = 48 rollouts per model, testing 3 models = 144 total. Tests environment loading, prompt construction, in… | TINKER_API_KEY, WANDB_API_KEY |
-
## `session_search` toolset
| Tool | Description | Requires environment |
diff --git a/website/docs/reference/toolsets-reference.md b/website/docs/reference/toolsets-reference.md
index ce11d86cb41..5bf1f14260e 100644
--- a/website/docs/reference/toolsets-reference.md
+++ b/website/docs/reference/toolsets-reference.md
@@ -45,7 +45,7 @@ Or in-session:
```
/tools list
/tools disable browser
-/tools enable rl
+/tools enable homeassistant
```
## Core Toolsets
@@ -71,7 +71,6 @@ Or in-session:
| `memory` | `memory` | Persistent cross-session memory management. |
| `messaging` | `send_message` | Send messages to other platforms (Telegram, Discord, etc.) from within a session. |
| `moa` | `mixture_of_agents` | Multi-model consensus via Mixture of Agents. |
-| `rl` | `rl_check_status`, `rl_edit_config`, `rl_get_current_config`, `rl_get_results`, `rl_list_environments`, `rl_list_runs`, `rl_select_environment`, `rl_start_training`, `rl_stop_training`, `rl_test_inference` | RL training environment management (Atropos). |
| `safe` | `image_generate`, `vision_analyze`, `web_extract`, `web_search` (via `includes`) | Read-only research + media generation. No file writes, no terminal, no code execution. |
| `search` | `web_search` | Web search only (without extract). |
| `session_search` | `session_search` | Search past conversation sessions. |
diff --git a/website/docs/user-guide/features/rl-training.md b/website/docs/user-guide/features/rl-training.md
deleted file mode 100644
index 81fc6539b37..00000000000
--- a/website/docs/user-guide/features/rl-training.md
+++ /dev/null
@@ -1,234 +0,0 @@
----
-sidebar_position: 13
-title: "RL Training"
-description: "Reinforcement learning on agent behaviors with Tinker-Atropos — environment discovery, training, and evaluation"
----
-
-# RL Training
-
-Hermes Agent includes an integrated RL (Reinforcement Learning) training pipeline built on **Tinker-Atropos**. This enables training language models on environment-specific tasks using GRPO (Group Relative Policy Optimization) with LoRA adapters, orchestrated entirely through the agent's tool interface.
-
-## Overview
-
-The RL training system consists of three components:
-
-1. **[Atropos](https://github.com/NousResearch/atropos)** — A trajectory API server that coordinates environment interactions, manages rollout groups, and computes advantages
-2. **[Tinker](https://thinkingmachines.ai/tinker/)** — A training service that handles model weights, LoRA training, sampling/inference, and optimizer steps
-3. **Environments** — Python classes that define tasks, scoring, and reward functions (e.g., GSM8K math problems)
-
-The agent can discover environments, configure training parameters, launch training runs, and monitor metrics — all through a set of `rl_*` tools.
-
-## Requirements
-
-RL training requires:
-
-- **Python >= 3.11** (Tinker package requirement)
-- **TINKER_API_KEY** — API key for the Tinker training service
-- **WANDB_API_KEY** — API key for [Weights & Biases](https://wandb.ai/) metrics tracking
-- The `tinker-atropos` submodule (at `tinker-atropos/` relative to the Hermes root)
-
-```bash
-# Set up API keys
-hermes config set TINKER_API_KEY your-tinker-key
-hermes config set WANDB_API_KEY your-wandb-key
-```
-
-When both keys are present and Python >= 3.11 is available, the `rl` toolset is automatically enabled.
-
-## Available Tools
-
-| Tool | Description |
-|------|-------------|
-| `rl_list_environments` | Discover available RL environments |
-| `rl_select_environment` | Select an environment and load its config |
-| `rl_get_current_config` | View configurable and locked fields |
-| `rl_edit_config` | Modify configurable training parameters |
-| `rl_start_training` | Launch a training run (spawns 3 processes) |
-| `rl_check_status` | Monitor training progress and WandB metrics |
-| `rl_stop_training` | Stop a running training job |
-| `rl_get_results` | Get final metrics and model weights path |
-| `rl_list_runs` | List all active and completed runs |
-| `rl_test_inference` | Quick inference test using OpenRouter |
-
-## Workflow
-
-### 1. Discover Environments
-
-```
-List the available RL environments
-```
-
-The agent calls `rl_list_environments()` which scans `tinker-atropos/tinker_atropos/environments/` using AST parsing to find Python classes inheriting from `BaseEnv`. Each environment defines:
-
-- **Dataset loading** — where training data comes from (e.g., HuggingFace datasets)
-- **Prompt construction** — how to format items for the model
-- **Scoring/verification** — how to evaluate model outputs and assign rewards
-
-### 2. Select and Configure
-
-```
-Select the GSM8K environment and show me the configuration
-```
-
-The agent calls `rl_select_environment("gsm8k_tinker")`, then `rl_get_current_config()` to see all parameters.
-
-Configuration fields are divided into two categories:
-
-**Configurable fields** (can be modified):
-- `group_size` — Number of completions per item (default: 16)
-- `batch_size` — Training batch size (default: 128)
-- `wandb_name` — WandB run name (auto-set to `{env}-{timestamp}`)
-- Other environment-specific parameters
-
-**Locked fields** (infrastructure settings, cannot be changed):
-- `tokenizer_name` — Model tokenizer (e.g., `Qwen/Qwen3-8B`)
-- `rollout_server_url` — Atropos API URL (`http://localhost:8000`)
-- `max_token_length` — Maximum token length (8192)
-- `max_num_workers` — Maximum parallel workers (2048)
-- `total_steps` — Total training steps (2500)
-- `lora_rank` — LoRA adapter rank (32)
-- `learning_rate` — Learning rate (4e-5)
-- `max_token_trainer_length` — Max tokens for trainer (9000)
-
-### 3. Start Training
-
-```
-Start the training run
-```
-
-The agent calls `rl_start_training()` which:
-
-1. Generates a YAML config file merging locked settings with configurable overrides
-2. Creates a unique run ID
-3. Spawns three processes:
- - **Atropos API server** (`run-api`) — trajectory coordination
- - **Tinker trainer** (`launch_training.py`) — LoRA training + FastAPI inference server on port 8001
- - **Environment** (`environment.py serve`) — the selected environment connecting to Atropos
-
-The processes start with staggered delays (5s for API, 30s for trainer, 90s more for environment) to ensure proper initialization order.
-
-### 4. Monitor Progress
-
-```
-Check the status of training run abc12345
-```
-
-The agent calls `rl_check_status(run_id)` which reports:
-
-- Process status (running/exited for each of the 3 processes)
-- Running time
-- WandB metrics (step, reward mean, percent correct, eval accuracy)
-- Log file locations for debugging
-
-:::note Rate Limiting
-Status checks are rate-limited to once every **30 minutes** per run ID. This prevents excessive polling during long-running training jobs that take hours.
-:::
-
-### 5. Stop or Get Results
-
-```
-Stop the training run
-# or
-Get the final results for run abc12345
-```
-
-`rl_stop_training()` terminates all three processes in reverse order (environment → trainer → API). `rl_get_results()` retrieves final WandB metrics and training history.
-
-## Inference Testing
-
-Before committing to a full training run, you can test if an environment works correctly using `rl_test_inference`. This runs a few steps of inference and scoring using OpenRouter — no Tinker API needed, just an `OPENROUTER_API_KEY`.
-
-```
-Test the selected environment with inference
-```
-
-Default configuration:
-- **3 steps × 16 completions = 48 rollouts per model**
-- Tests 3 models at different scales for robustness:
- - `qwen/qwen3-8b` (small)
- - `z-ai/glm-4.7-flash` (medium)
- - `minimax/minimax-m2.7` (large)
-- Total: ~144 rollouts
-
-This validates:
-- Environment loads correctly
-- Prompt construction works
-- Inference response parsing is robust across model scales
-- Verifier/scoring logic produces valid rewards
-
-## Tinker API Integration
-
-The trainer uses the [Tinker](https://tinker.computer) API for model training operations:
-
-- **ServiceClient** — Creates training and sampling clients
-- **Training client** — Handles forward-backward passes with importance sampling loss, optimizer steps (Adam), and weight checkpointing
-- **Sampling client** — Provides inference using the latest trained weights
-
-The training loop:
-1. Fetches a batch of rollouts from Atropos (prompt + completions + scores)
-2. Converts to Tinker Datum objects with padded logprobs and advantages
-3. Runs forward-backward pass with importance sampling loss
-4. Takes an optimizer step (Adam: lr=4e-5, β1=0.9, β2=0.95)
-5. Saves weights and creates a new sampling client for next-step inference
-6. Logs metrics to WandB
-
-## Architecture Diagram
-
-```mermaid
-flowchart LR
- api["Atropos API
run-api
port 8000"]
- env["Environment
BaseEnv implementation"]
- infer["OpenAI / sglang
inference API
port 8001"]
- trainer["Tinker Trainer
LoRA training + FastAPI"]
-
- env <--> api
- env --> infer
- api -->|"batches: tokens, scores, logprobs"| trainer
- trainer -->|"serves inference"| infer
-```
-
-## Creating Custom Environments
-
-To create a new RL environment:
-
-1. Create a Python file in `tinker-atropos/tinker_atropos/environments/`
-2. Define a class that inherits from `BaseEnv`
-3. Implement the required methods:
- - `load_dataset()` — Load your training data
- - `get_next_item()` — Provide the next item to the model
- - `score_answer()` — Score model outputs and assign rewards
- - `collect_trajectories()` — Collect and return trajectories
-4. Optionally define a custom config class inheriting from `BaseEnvConfig`
-
-Study the existing `gsm8k_tinker.py` as a template. The agent can help you create new environments — it can read existing environment files, inspect HuggingFace datasets, and write new environment code.
-
-## WandB Metrics
-
-Training runs log to Weights & Biases with these key metrics:
-
-| Metric | Description |
-|--------|-------------|
-| `train/loss` | Training loss (importance sampling) |
-| `train/learning_rate` | Current learning rate |
-| `reward/mean` | Mean reward across groups |
-| `logprobs/mean` | Mean reference logprobs |
-| `logprobs/mean_training` | Mean training logprobs |
-| `logprobs/diff` | Logprob drift (reference - training) |
-| `advantages/mean` | Mean advantage values |
-| `advantages/std` | Advantage standard deviation |
-
-## Log Files
-
-Each training run generates log files in `~/.hermes/logs/rl_training/`:
-
-```
-logs/
-├── api_{run_id}.log # Atropos API server logs
-├── trainer_{run_id}.log # Tinker trainer logs
-├── env_{run_id}.log # Environment process logs
-└── inference_tests/ # Inference test results
- ├── test_{env}_{model}.jsonl
- └── test_{env}_{model}.log
-```
-
-These are invaluable for debugging when training fails or produces unexpected results.
diff --git a/website/docs/user-guide/skills/optional/mlops/mlops-hermes-atropos-environments.md b/website/docs/user-guide/skills/optional/mlops/mlops-hermes-atropos-environments.md
deleted file mode 100644
index 7cce92a7e0e..00000000000
--- a/website/docs/user-guide/skills/optional/mlops/mlops-hermes-atropos-environments.md
+++ /dev/null
@@ -1,323 +0,0 @@
----
-title: "Hermes Atropos Environments — Build, test, and debug Hermes Agent RL environments for Atropos training"
-sidebar_label: "Hermes Atropos Environments"
-description: "Build, test, and debug Hermes Agent RL environments for Atropos training"
----
-
-{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */}
-
-# Hermes Atropos Environments
-
-Build, test, and debug Hermes Agent RL environments for Atropos training. Covers the HermesAgentBaseEnv interface, reward functions, agent loop integration, evaluation with tools, wandb logging, and the three CLI modes (serve/process/evaluate). Use when creating, reviewing, or fixing RL environments in the hermes-agent repo.
-
-## Skill metadata
-
-| | |
-|---|---|
-| Source | Optional — install with `hermes skills install official/mlops/hermes-atropos-environments` |
-| Path | `optional-skills/mlops/hermes-atropos-environments` |
-| Version | `1.1.0` |
-| Author | Hermes Agent |
-| License | MIT |
-| Platforms | linux, macos, windows |
-| Tags | `atropos`, `rl`, `environments`, `training`, `reinforcement-learning`, `reward-functions` |
-| Related skills | [`axolotl`](/docs/user-guide/skills/optional/mlops/mlops-training-axolotl), [`fine-tuning-with-trl`](/docs/user-guide/skills/optional/mlops/mlops-training-trl-fine-tuning), `lm-evaluation-harness` |
-
-## Reference: full SKILL.md
-
-:::info
-The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active.
-:::
-
-# Hermes Agent Atropos Environments
-
-Guide for building RL environments in the hermes-agent repo that integrate with the Atropos training framework.
-
-## Architecture Overview
-
-
-```
-Atropos BaseEnv (atroposlib/envs/base.py)
- └── HermesAgentBaseEnv (environments/hermes_base_env.py)
- ├── Handles agent loop orchestration
- ├── Handles tool resolution per group
- ├── Handles ToolContext for reward verification
- └── YOUR ENVIRONMENT (environments/your_env.py)
- Only implements: setup, get_next_item, format_prompt,
- compute_reward, evaluate, wandb_log
-```
-
-
-Hermes environments are special because they run a **multi-turn agent loop with tool calling** — not just single-turn completions. The base env handles the loop; you implement the task and scoring.
-
-## File Locations
-
-| File | Purpose |
-|------|---------|
-| `environments/hermes_base_env.py` | Base class with agent loop + tool resolution |
-| `environments/agent_loop.py` | `HermesAgentLoop` + `AgentResult` dataclass |
-| `environments/tool_context.py` | `ToolContext` for reward verification |
-| `environments/tool_call_parsers.py` | Phase 2 tool call parsers (hermes, mistral, etc.) |
-| `environments/your_env.py` | Your environment implementation |
-
-## Inference Setup — Ask the User First
-
-**IMPORTANT:** Before running any test, evaluation, or data generation command, always ask the user how they want to handle inference. Do NOT assume OpenRouter or any specific endpoint. Present these options:
-
-1. **OpenRouter** — Ask which model they want to use (e.g., `anthropic/claude-sonnet-4.5`, `google/gemini-2.5-pro`, `meta-llama/llama-3.3-70b-instruct`, etc.). Requires `OPENROUTER_API_KEY` in environment.
-2. **Self-hosted VLLM endpoint** — Ask for their base URL (e.g., `http://localhost:8000/v1`) and model name. Set `--openai.server_type vllm`.
-3. **Other OpenAI-compatible API** — Ask for the base URL, model name, and any required API key. Set `--openai.server_type openai` and `--openai.health_check false`.
-4. **Local Atropos training server** — For `serve` mode with a live training loop. Default `http://localhost:8000/v1`.
-
-Once the user tells you their setup, use those values in all CLI commands for that session. Example prompts:
-
-> "Before I run this, how would you like to handle inference?
-> 1. OpenRouter (I'll need your preferred model, e.g. claude-sonnet-4.5)
-> 2. A self-hosted VLLM endpoint (give me the URL and model name)
-> 3. Another OpenAI-compatible API (give me the URL, model, and any auth details)
-> 4. Local Atropos training server (serve mode)"
-
-### Key flags by provider:
-
-| Provider | `--openai.server_type` | `--openai.health_check` | `--openai.api_key` |
-|----------|----------------------|------------------------|-------------------|
-| OpenRouter | `openai` | `false` | `$OPENROUTER_API_KEY` |
-| VLLM (self-hosted) | `vllm` | (default) | (not needed) |
-| Other OpenAI-compatible | `openai` | `false` | As needed |
-| Local Atropos | (default) | (default) | (not needed) |
-
-## Required Methods
-
-### 1. `setup()` — Load dataset and initialize state
-
-```python
-async def setup(self) -> None:
- """Called once at startup. Load datasets, initialize state."""
- # Try HuggingFace first, fallback to built-in samples
- try:
- from datasets import load_dataset
- ds = load_dataset("your/dataset", split="test")
- self._items = [...]
- except Exception:
- self._items = BUILTIN_SAMPLES
-
- # Always split into train/eval
- random.shuffle(self._items)
- eval_size = max(20, int(len(self._items) * 0.1))
- self._eval_items = self._items[:eval_size]
- self._items = self._items[eval_size:]
-```
-
-### 2. `get_next_item()` — Return next training item
-
-```python
-async def get_next_item(self) -> dict:
- """Return next item, cycling through dataset."""
- item = self._items[self._index % len(self._items)]
- self._index += 1
- return item
-```
-
-### 3. `format_prompt(item)` — Convert item to user message
-
-```python
-def format_prompt(self, item: dict) -> str:
- """Convert a dataset item into the user-facing prompt."""
- return f"Research this question: {item['question']}"
-```
-
-### 4. `compute_reward(item, result, ctx)` — Score the rollout
-
-**CRITICAL**: `result` is an `AgentResult`, NOT a dict. It has these attributes:
-- `result.messages` — List of message dicts (OpenAI format)
-- `result.turns_used` — Number of LLM calls made
-- `result.finished_naturally` — True if model stopped voluntarily
-- `result.tool_errors` — List of ToolError objects
-
-**AgentResult does NOT have**: `final_response`, `tool_calls`, `tools_used`.
-You must extract these from `result.messages`:
-
-```python
-async def compute_reward(self, item, result: AgentResult, ctx: ToolContext) -> float:
- # Extract final response (last assistant message with content)
- final_response = ""
- tools_used = []
- for msg in reversed(result.messages):
- if msg.get("role") == "assistant" and msg.get("content") and not final_response:
- final_response = msg["content"]
- if msg.get("role") == "assistant" and msg.get("tool_calls"):
- for tc in msg["tool_calls"]:
- fn = tc.get("function", {}) if isinstance(tc, dict) else {}
- name = fn.get("name", "")
- if name:
- tools_used.append(name)
-
- # Score using LLM judge, heuristic, or ToolContext verification
- correctness = await self._llm_judge(item, final_response)
- return correctness
-```
-
-`ctx` (ToolContext) gives you terminal/file access to the agent's sandbox for verification:
-```python
-# Run tests in the agent's sandbox
-result = ctx.terminal("pytest /workspace/test.py")
-return 1.0 if result["exit_code"] == 0 else 0.0
-```
-
-### 5. `evaluate()` — Periodic evaluation with full agent loop
-
-**MUST use the full agent loop with tools**, not single-turn chat_completion.
-The whole point of hermes-agent environments is agentic evaluation:
-
-```python
-async def evaluate(self, *args, **kwargs) -> None:
- import time, uuid
- from environments.agent_loop import HermesAgentLoop
- from environments.tool_context import ToolContext
-
- start_time = time.time()
- tools, valid_names = self._resolve_tools_for_group()
- samples = []
-
- for item in self._eval_items[:self.config.eval_size]:
- task_id = str(uuid.uuid4())
- messages = []
- if self.config.system_prompt:
- messages.append({"role": "system", "content": self.config.system_prompt})
- messages.append({"role": "user", "content": self.format_prompt(item)})
-
- agent = HermesAgentLoop(
- server=self.server,
- tool_schemas=tools,
- valid_tool_names=valid_names,
- max_turns=self.config.max_agent_turns,
- task_id=task_id,
- temperature=0.0, # Deterministic for eval
- max_tokens=self.config.max_token_length,
- extra_body=self.config.extra_body,
- )
- result = await agent.run(messages)
-
- ctx = ToolContext(task_id)
- try:
- reward = await self.compute_reward(item, result, ctx)
- finally:
- ctx.cleanup()
-
- samples.append({"prompt": ..., "response": ..., "reward": reward})
-
- eval_metrics = {"eval/mean_reward": ...}
- await self.evaluate_log(metrics=eval_metrics, samples=samples,
- start_time=start_time, end_time=time.time())
-```
-
-### 6. `wandb_log()` — Custom metrics logging
-
-Always call `super().wandb_log()` at the end:
-
-```python
-async def wandb_log(self, wandb_metrics=None):
- if wandb_metrics is None:
- wandb_metrics = {}
- if self._reward_buffer:
- n = len(self._reward_buffer)
- wandb_metrics["train/mean_reward"] = sum(self._reward_buffer) / n
- self._reward_buffer.clear()
- await super().wandb_log(wandb_metrics) # MUST call super
-```
-
-**Pitfall**: `compute_reward` appends to metric buffers. During eval, this pollutes training metrics. Roll back buffer entries added during eval.
-
-## Config Class
-
-Always create a custom config subclass with Pydantic Field descriptors. Key inherited fields you can tune: `enabled_toolsets`, `max_agent_turns`, `agent_temperature`, `system_prompt`, `terminal_backend`, `group_size`, `steps_per_eval`, `total_steps`.
-
-## config_init() — Default Configuration
-
-Classmethod returning `(YourEnvConfig, [APIServerConfig(...)])`. Set server_type to "openai" for OpenRouter/external APIs. Load API key from environment variable.
-
-## Three CLI Modes
-
-```bash
-# SERVE — Full training loop (connects to Atropos API server)
-python environments/my_env.py serve --openai.base_url http://localhost:8000/v1
-
-# PROCESS — Offline data generation (saves JSONL)
-python environments/my_env.py process --env.total_steps 10 --env.group_size 1 \
- --env.use_wandb false --env.data_path_to_save_groups output.jsonl \
- --openai.base_url "" \
- --openai.model_name "" \
- --openai.server_type --openai.health_check false
-
-# EVALUATE — Standalone eval (runs setup + evaluate only)
-python environments/my_env.py evaluate --env.eval_size 20 \
- --env.data_dir_to_save_evals /tmp/eval_results \
- --openai.base_url "" \
- --openai.model_name "" \
- --openai.server_type --openai.health_check false
-```
-
-Config priority: CLI args > YAML file > config_init() defaults.
-
-## Common Pitfalls
-
-1. **AgentResult has .messages, not .final_response** — Extract the final response by iterating reversed(result.messages) looking for the last assistant message with content.
-
-2. **evaluate() must use HermesAgentLoop, not chat_completion** — Single-turn chat_completion has no tools. The whole point of hermes-agent benchmarks is agentic evaluation with tool use.
-
-3. **Don't call _llm_judge twice** — If compute_reward already calls it, extract the score from the buffer instead of calling judge separately in evaluate().
-
-4. **Eval pollutes training buffers** — compute_reward appends to metric buffers. During eval, roll back buffer entries to keep training metrics clean.
-
-5. **Always set health_check=false for OpenRouter** — OpenRouter has no /health endpoint.
-
-6. **Set data_dir_to_save_evals in evaluate mode** — Without it, results aren't saved.
-
-7. **default_toolsets class variable vs enabled_toolsets config** — The class variable is a hint; the config field is what actually controls tool resolution.
-
-8. **Tool call parsing in messages** — Tool calls are dicts with `{"function": {"name": ..., "arguments": ...}}`. Always check `isinstance(tc, dict)`.
-
-9. **ToolContext.cleanup()** — Always call in a finally block to release sandbox resources.
-
-10. **server_type must be "openai" for external APIs** — Without it, Atropos assumes a local VLLM server.
-
-11. **Always ask the user for their inference setup** — Never hardcode or assume a specific provider/model. See the "Inference Setup" section above.
-
-## Reward Function Patterns
-
-### LLM Judge (for open-ended tasks)
-Use `self.server.chat_completion()` with a scoring prompt. Parse JSON response for score float. Always include a heuristic fallback (keyword overlap) for when the judge call fails.
-
-### Binary Verification (for code/terminal tasks)
-Use `ctx.terminal("pytest test.py -q")` to run tests in the agent's sandbox. Return 1.0 for pass, 0.0 for fail.
-
-### Multi-Signal (combine multiple indicators)
-Weight correctness (0.6) + tool usage (0.2) + efficiency (0.2) + optional bonuses. Clamp to [0, 1].
-
-## Testing Your Environment
-
-1. **Import test**: `python -c "from environments.my_env import MyEnv; print('OK')"`
-2. **Ask the user for inference setup** (see "Inference Setup" section above)
-3. **Process mode** (1 item): Verify JSONL output has valid tokens, masks, scores
-4. **Evaluate mode**: Verify full agent loop runs with tools, metrics logged correctly
-5. **Check reward range**: Scores should be in [0, 1], not all identical
-
-## Minimum Implementation Checklist
-
-```python
-class MyEnv(HermesAgentBaseEnv):
- name = "my-env"
- env_config_cls = MyEnvConfig
-
- @classmethod
- def config_init(cls): ... # Default server + env config
- async def setup(self): ... # Load dataset + train/eval split
- async def get_next_item(self): ... # Cycle through training items
- def format_prompt(self, item): ... # Item → user message string
- async def compute_reward(self, item, result, ctx): ... # Score rollout
- async def evaluate(self, *args, **kwargs): ... # Full agent loop eval
- async def wandb_log(self, metrics=None): ... # Custom metrics + super()
-
-if __name__ == "__main__":
- MyEnv.cli()
-```
diff --git a/website/sidebars.ts b/website/sidebars.ts
index 37557df8d11..a2977c87eef 100644
--- a/website/sidebars.ts
+++ b/website/sidebars.ts
@@ -103,7 +103,6 @@ const sidebars: SidebarsConfig = {
type: 'category',
label: 'Advanced',
items: [
- 'user-guide/features/rl-training',
'user-guide/features/spotify',
],
},
@@ -238,7 +237,6 @@ const sidebars: SidebarsConfig = {
'developer-guide/tools-runtime',
'developer-guide/acp-internals',
'developer-guide/cron-internals',
- 'developer-guide/environments',
'developer-guide/trajectory-format',
],
},