Spaces:
Sleeping
Sleeping
fix: stabilize deployment, action safety, and benchmark reproducibility
Browse files- .gitignore +4 -0
- Dockerfile +15 -11
- README.md +90 -137
- baseline.py +99 -43
- env/__init__.py +2 -1
- env/action_utils.py +95 -0
- env/dynamics.py +39 -6
- env/environment.py +52 -4
- env/reward.py +38 -12
- inference.py +10 -5
- models/schemas.py +18 -0
- pyproject.toml +8 -3
- requirements-train.txt +5 -0
- requirements.txt +0 -5
- scripts/benchmark.py +106 -0
- scripts/smoke_api.py +45 -0
- server/app.py +57 -11
- server/ecogrid_environment.py +30 -3
- tests/test_action_utils.py +34 -0
- tests/test_environment.py +32 -0
- tests/test_server_api.py +61 -0
- train_unsloth.py +40 -7
- uv.lock +254 -9
.gitignore
CHANGED
|
@@ -3,6 +3,10 @@ lora_adapter/
|
|
| 3 |
__pycache__/
|
| 4 |
*.pyc
|
| 5 |
venv/
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
.env
|
| 7 |
.pytest_cache/
|
| 8 |
pytest-cache-files-*/
|
|
|
|
| 3 |
__pycache__/
|
| 4 |
*.pyc
|
| 5 |
venv/
|
| 6 |
+
.venv/
|
| 7 |
+
.pydeps/
|
| 8 |
+
.uv-venv/
|
| 9 |
+
.uv-python/
|
| 10 |
.env
|
| 11 |
.pytest_cache/
|
| 12 |
pytest-cache-files-*/
|
Dockerfile
CHANGED
|
@@ -1,23 +1,27 @@
|
|
| 1 |
-
# Stage 1: Runtime
|
| 2 |
FROM python:3.10-slim
|
| 3 |
|
| 4 |
WORKDIR /app
|
| 5 |
|
| 6 |
-
#
|
| 7 |
-
RUN pip install uv
|
| 8 |
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
COPY pyproject.toml uv.lock ./
|
| 11 |
-
RUN uv sync
|
| 12 |
|
| 13 |
-
# Copy
|
| 14 |
COPY . .
|
|
|
|
| 15 |
|
| 16 |
-
# Expose server port (Hugging Face Spaces default)
|
| 17 |
EXPOSE 7860
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
|
| 22 |
-
|
| 23 |
-
CMD ["uv", "run", "--project", ".", "server", "--port", "7860", "--host", "0.0.0.0"]
|
|
|
|
|
|
|
| 1 |
FROM python:3.10-slim
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
# Stable uv install for lock-based, reproducible sync
|
| 6 |
+
RUN pip install --no-cache-dir uv
|
| 7 |
|
| 8 |
+
ENV UV_COMPILE_BYTECODE=1 \
|
| 9 |
+
UV_LINK_MODE=copy \
|
| 10 |
+
UV_PROJECT_ENVIRONMENT=/opt/venv \
|
| 11 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 12 |
+
PYTHONUNBUFFERED=1
|
| 13 |
+
|
| 14 |
+
# Install only locked runtime deps first for layer caching
|
| 15 |
COPY pyproject.toml uv.lock ./
|
| 16 |
+
RUN uv sync --frozen --no-dev --no-install-project
|
| 17 |
|
| 18 |
+
# Copy source and install project itself
|
| 19 |
COPY . .
|
| 20 |
+
RUN uv sync --frozen --no-dev
|
| 21 |
|
|
|
|
| 22 |
EXPOSE 7860
|
| 23 |
|
| 24 |
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
| 25 |
+
CMD /opt/venv/bin/python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:7860/health', timeout=3).read()" || exit 1
|
| 26 |
|
| 27 |
+
CMD ["/opt/venv/bin/python", "-m", "server.app"]
|
|
|
README.md
CHANGED
|
@@ -1,173 +1,126 @@
|
|
| 1 |
-
|
| 2 |
-
title: EcoGrid OpenEnv
|
| 3 |
-
emoji: 🌍
|
| 4 |
-
colorFrom: green
|
| 5 |
-
colorTo: blue
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 7860
|
| 8 |
-
---
|
| 9 |
-
# ⚡ EcoGrid-OpenEnv
|
| 10 |
-

|
| 11 |
-
|
| 12 |
-
**EcoGrid-OpenEnv** is a production-grade Reinforcement Learning environment for the OpenEnv framework. It simulates sustainable energy grid management where an AI agent must balance renewable sources, fossil fuels, and battery storage to meet demand while minimising cost and carbon emissions.
|
| 13 |
-
|
| 14 |
-
Built for the **Theme #3: World Modeling** track (Mercor Sub-theme).
|
| 15 |
-
|
| 16 |
-
---
|
| 17 |
-
|
| 18 |
-
## 🌍 The Problem
|
| 19 |
-
|
| 20 |
-
Modern power grids are facing unprecedented volatility. The transition to renewable energy introduces extreme supply variance (the sun doesn't always shine, the wind doesn't always blow), while electrification of transport causes unpredictable demand spikes.
|
| 21 |
-
|
| 22 |
-
Grid operators must solve a continuous, multi-objective optimization problem:
|
| 23 |
-
1. **Prevent Blackouts:** Meet demand perfectly.
|
| 24 |
-
2. **Minimise Cost:** Avoid expensive fossil fuels and spot-market emergency purchases.
|
| 25 |
-
3. **Cut Emissions:** Stay within strict carbon budgets.
|
| 26 |
-
|
| 27 |
-
This environment models that exact problem as a Reinforcement Learning Markov Decision Process (MDP).
|
| 28 |
-
|
| 29 |
-
---
|
| 30 |
-
|
| 31 |
-
## 🏗️ Environment Design
|
| 32 |
-
|
| 33 |
-
### State Space (Observation)
|
| 34 |
-
The agent receives a rich, dense state vector at every step:
|
| 35 |
-
```text
|
| 36 |
-
┌───────────────────────┐
|
| 37 |
-
│ GridState │
|
| 38 |
-
│ ├─ demand (MWh) │ ─> Varies wildly (morning/evening peaks)
|
| 39 |
-
│ ├─ solar_capacity │ ─> Predictable daytime curve + cloud noise
|
| 40 |
-
│ ├─ wind_capacity │ ─> Mean-reverting random walk + noise
|
| 41 |
-
│ ├─ battery_level │ ─> State of charge [0,1]
|
| 42 |
-
│ ├─ grid_stability │ ─> Momentum-based frequency indicator
|
| 43 |
-
│ ├─ carbon_budget │ ─> Remaining kgCO₂
|
| 44 |
-
│ ├─ price_signal │ ─> Surges when supply < demand
|
| 45 |
-
│ └─ time_step │
|
| 46 |
-
└───────────────────────┘
|
| 47 |
-
```
|
| 48 |
-
|
| 49 |
-
### Action Space
|
| 50 |
-
At each step, the agent outputs a continuous action vector:
|
| 51 |
-
```text
|
| 52 |
-
┌───────────────────────┐
|
| 53 |
-
│ GridAction │
|
| 54 |
-
│ ├─ renewable_ratio │ ─> [0, 1] Fraction of demand met by renewables
|
| 55 |
-
│ ├─ fossil_ratio │ ─> [0, 1] Fraction of demand met by fossil
|
| 56 |
-
│ └─ battery_action │ ─> [-1, 1] Discharge(-1) to Charge(+1)
|
| 57 |
-
└───────────────────────┘
|
| 58 |
-
```
|
| 59 |
-
*Constraint: `renewable_ratio + fossil_ratio <= 1.0`*
|
| 60 |
|
| 61 |
-
-
|
| 62 |
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
-
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
| **Stability** | 0.25 | `1 - blackout_risk` |
|
| 72 |
-
| **Green Bonus** | 0.15 | `renewable_ratio * stability_score` |
|
| 73 |
|
| 74 |
-
|
| 75 |
-
- `
|
| 76 |
-
- `-0.8` applied if carbon budget is exceeded (Hard task only).
|
| 77 |
|
| 78 |
-
|
| 79 |
|
| 80 |
-
|
|
|
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|------|------------|---------|------------|------|--------|
|
| 84 |
-
| `easy` | 1 | 48 steps | Stable solar, flat demand, no battery | Minimise Cost | `BasicGridBalance` |
|
| 85 |
-
| `medium` | 2 | 96 steps | Noisy renewables, demand spikes, small battery | Avoid Blackouts | `RenewableVariability` |
|
| 86 |
-
| `hard` | 3 | 96 steps | Strict carbon cap, 2x noise, limited storage | Survive within Carbon Cap | `CarbonConstrained` |
|
| 87 |
|
| 88 |
-
|
| 89 |
|
| 90 |
-
##
|
| 91 |
-
|
| 92 |
-
### 1. Installation
|
| 93 |
```bash
|
| 94 |
-
git clone https://github.com/dooti2325/EcoGrid.git
|
| 95 |
-
cd EcoGrid
|
| 96 |
-
python -m venv venv
|
| 97 |
-
source venv/bin/activate
|
| 98 |
pip install -r requirements.txt
|
| 99 |
```
|
| 100 |
|
| 101 |
-
###
|
| 102 |
```bash
|
| 103 |
-
|
| 104 |
-
# Output: ✅ Environment spec valid.
|
| 105 |
```
|
| 106 |
|
| 107 |
-
##
|
| 108 |
-
Run the deterministic heuristic baseline:
|
| 109 |
-
```bash
|
| 110 |
-
python baseline.py --task easy --agent heuristic
|
| 111 |
-
python baseline.py --task hard --agent heuristic
|
| 112 |
-
```
|
| 113 |
|
| 114 |
-
Run
|
| 115 |
```bash
|
| 116 |
-
|
| 117 |
-
python baseline.py --task medium --agent llm
|
| 118 |
```
|
| 119 |
|
| 120 |
-
-
|
| 121 |
-
|
| 122 |
-
|
|
|
|
| 123 |
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
|
| 127 |
|
|
|
|
| 128 |
```bash
|
| 129 |
-
|
| 130 |
-
python train_unsloth.py --task hard --epochs 3 --samples 500
|
| 131 |
```
|
| 132 |
-
This saves a LoRA adapter to `./lora_adapter/` and reward curves to `./logs/`.
|
| 133 |
-
|
| 134 |
-
---
|
| 135 |
-
|
| 136 |
-
## 📊 Training Evidence
|
| 137 |
-
|
| 138 |
-
The repository includes reward and loss plots from the GRPO training run:
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
## 📊 Baseline Scores
|
| 145 |
-
|
| 146 |
-
*Averaged over 5 random seeds.*
|
| 147 |
-
|
| 148 |
-
| Task | Random Agent | Heuristic Agent | Trained LLM |
|
| 149 |
-
|------|-------------|-----------------|------------------------|
|
| 150 |
-
| `easy` | 0.21 | 0.72 | 0.81 |
|
| 151 |
-
| `medium` | 0.16 | 0.58 | 0.75 |
|
| 152 |
-
| `hard` | 0.00 (fail) | 0.41 | 0.62 |
|
| 153 |
-
|
| 154 |
-
---
|
| 155 |
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
-
|
| 159 |
|
| 160 |
-
|
|
|
|
|
|
|
| 161 |
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
-
|
| 165 |
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
-
|
| 169 |
```bash
|
| 170 |
-
|
| 171 |
-
docker run -p 7860:7860 ecogrid
|
| 172 |
-
# Open http://localhost:7860
|
| 173 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# EcoGrid OpenEnv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
Production-ready RL environment and API for sustainable grid control.
|
| 4 |
|
| 5 |
+
This repo now ships with:
|
| 6 |
+
- Stable OpenEnv API server (`/health`, `/reset`, `/step`, `/schema`)
|
| 7 |
+
- Deterministic environment behavior for fixed seeds
|
| 8 |
+
- Action safety guards that prevent invalid action sums after rounding
|
| 9 |
+
- Hardened deployment path for Hugging Face Spaces (Docker + `uv.lock`)
|
| 10 |
+
- Reproducible benchmark script with before/after comparison output
|
| 11 |
|
| 12 |
+
## 1) What The Agent Controls
|
| 13 |
|
| 14 |
+
At each step the agent picks:
|
| 15 |
+
- `renewable_ratio` in `[0, 1]`
|
| 16 |
+
- `fossil_ratio` in `[0, 1]`
|
| 17 |
+
- `battery_action` in `[-1, 1]`
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
Safety constraint:
|
| 20 |
+
- `renewable_ratio + fossil_ratio <= 1.0` (enforced with normalization guards)
|
|
|
|
| 21 |
|
| 22 |
+
## 2) Runtime Modes
|
| 23 |
|
| 24 |
+
- API mode (default deployment): `python -m server.app`
|
| 25 |
+
- Dashboard mode (optional local demo): `streamlit run app.py`
|
| 26 |
|
| 27 |
+
HF Space Docker deployment uses API mode.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
## 3) Install
|
| 30 |
|
| 31 |
+
### Runtime only
|
|
|
|
|
|
|
| 32 |
```bash
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
pip install -r requirements.txt
|
| 34 |
```
|
| 35 |
|
| 36 |
+
### Runtime + training stack
|
| 37 |
```bash
|
| 38 |
+
pip install -r requirements-train.txt
|
|
|
|
| 39 |
```
|
| 40 |
|
| 41 |
+
## 4) Reproducible Benchmarks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
+
Run:
|
| 44 |
```bash
|
| 45 |
+
python scripts/benchmark.py --seeds 1,2,3,4,5 --out logs/benchmark_results.json
|
|
|
|
| 46 |
```
|
| 47 |
|
| 48 |
+
Current post-fix means (5 seeds):
|
| 49 |
+
- easy: random `0.2721`, heuristic `0.7595`
|
| 50 |
+
- medium: random `0.2545`, heuristic `0.7847`
|
| 51 |
+
- hard: random `0.0010`, heuristic `0.4000`
|
| 52 |
|
| 53 |
+
Reference pre-fix means used for delta tracking:
|
| 54 |
+
- easy: random `0.269`, heuristic `0.748`
|
| 55 |
+
- medium: random `0.251`, heuristic `0.376`
|
| 56 |
+
- hard: random `0.001`, heuristic `0.001`
|
| 57 |
|
| 58 |
+
## 5) API Smoke Test
|
| 59 |
|
| 60 |
+
Start server:
|
| 61 |
```bash
|
| 62 |
+
python -m server.app
|
|
|
|
| 63 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
Then:
|
| 66 |
+
```bash
|
| 67 |
+
python scripts/smoke_api.py --base-url http://127.0.0.1:7860
|
| 68 |
+
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
This checks:
|
| 71 |
+
- `GET /health`
|
| 72 |
+
- `POST /reset`
|
| 73 |
+
- `POST /step` (direct action payload compatibility)
|
| 74 |
|
| 75 |
+
## 6) Test Suite
|
| 76 |
|
| 77 |
+
```bash
|
| 78 |
+
pytest -q
|
| 79 |
+
```
|
| 80 |
|
| 81 |
+
Coverage includes:
|
| 82 |
+
- environment unit tests
|
| 83 |
+
- reward/grader tests
|
| 84 |
+
- action normalization regression tests
|
| 85 |
+
- API smoke/integration tests
|
| 86 |
+
- reproducibility checks (same seed => same trajectory)
|
| 87 |
|
| 88 |
+
## 7) Hugging Face Space Deployment
|
| 89 |
|
| 90 |
+
### Lock strategy
|
| 91 |
+
- Runtime dependencies live in `pyproject.toml` default deps.
|
| 92 |
+
- Heavy training deps are optional (`[project.optional-dependencies].train`).
|
| 93 |
+
- `uv.lock` is committed and used with `--frozen`.
|
| 94 |
|
| 95 |
+
Regenerate lock file:
|
| 96 |
```bash
|
| 97 |
+
uv lock
|
|
|
|
|
|
|
| 98 |
```
|
| 99 |
+
|
| 100 |
+
### Docker (Space) build path
|
| 101 |
+
The committed `Dockerfile` does:
|
| 102 |
+
1. `uv sync --frozen --no-dev --no-install-project`
|
| 103 |
+
2. copy source
|
| 104 |
+
3. `uv sync --frozen --no-dev`
|
| 105 |
+
4. run `/opt/venv/bin/python -m server.app`
|
| 106 |
+
|
| 107 |
+
Healthcheck:
|
| 108 |
+
- container-level health probe hits `http://127.0.0.1:7860/health`
|
| 109 |
+
|
| 110 |
+
## 8) Minimal Deployment Checklist
|
| 111 |
+
|
| 112 |
+
- `uv lock` succeeds locally
|
| 113 |
+
- `uv sync --frozen --no-dev` succeeds locally
|
| 114 |
+
- `pytest -q` passes
|
| 115 |
+
- `python scripts/smoke_api.py` passes against local server
|
| 116 |
+
- Docker build succeeds
|
| 117 |
+
- Space runtime reports healthy and serves `/health`, `/schema`, `/docs`
|
| 118 |
+
|
| 119 |
+
## 9) Project Structure
|
| 120 |
+
|
| 121 |
+
- `env/` core environment dynamics, rewards, action safety helpers
|
| 122 |
+
- `server/` OpenEnv/FastAPI serving layer
|
| 123 |
+
- `baseline.py` heuristic + optional LLM baseline runner
|
| 124 |
+
- `train_unsloth.py` deterministic training pipeline and metric logging
|
| 125 |
+
- `scripts/benchmark.py` reproducible benchmark runner
|
| 126 |
+
- `scripts/smoke_api.py` deployment smoke test runner
|
baseline.py
CHANGED
|
@@ -11,19 +11,32 @@ import os
|
|
| 11 |
import time
|
| 12 |
from typing import Literal
|
| 13 |
|
| 14 |
-
|
| 15 |
-
try:
|
| 16 |
-
import litellm
|
| 17 |
-
HAS_LITELLM = True
|
| 18 |
-
except ImportError:
|
| 19 |
-
HAS_LITELLM = False
|
| 20 |
|
| 21 |
from env.environment import EcoGridEnv
|
| 22 |
from env.tasks import BasicGridBalanceGrader, RenewableVariabilityGrader, CarbonConstrainedGrader
|
|
|
|
| 23 |
from models.schemas import GridAction, GridState
|
| 24 |
|
| 25 |
_trained_model = None
|
| 26 |
_trained_tokenizer = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
def load_trained_model():
|
| 29 |
"""Lazily load the LoRA model if available."""
|
|
@@ -93,56 +106,95 @@ def local_llm_agent(state: GridState, task_name: str) -> GridAction:
|
|
| 93 |
content = content[3:-3]
|
| 94 |
|
| 95 |
data = json.loads(content)
|
| 96 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
except Exception as e:
|
| 98 |
print(f"Local LLM Error: {e}. Falling back to heuristic.")
|
| 99 |
return heuristic_agent(state, task_name)
|
| 100 |
|
| 101 |
|
| 102 |
|
| 103 |
-
def
|
| 104 |
-
"""
|
| 105 |
-
|
| 106 |
avg_renewable_cap = (state.solar_capacity + state.wind_capacity) / 2.0
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
| 109 |
if state.demand > 0:
|
| 110 |
-
|
| 111 |
-
|
|
|
|
| 112 |
else:
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
#
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
if state.demand > 100 and state.battery_level > 0.2:
|
| 133 |
-
battery_action = -0.
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
)
|
| 142 |
|
| 143 |
|
| 144 |
def llm_agent(state: GridState, task_name: str) -> GridAction:
|
| 145 |
"""An agent that uses an LLM to make decisions via Chain-of-Thought."""
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
prompt = f"""
|
| 148 |
You are an expert energy grid operator managing a power grid.
|
|
@@ -182,7 +234,11 @@ Then, output ONLY a valid JSON object matching this schema, with no markdown fen
|
|
| 182 |
content = content[3:-3]
|
| 183 |
|
| 184 |
data = json.loads(content)
|
| 185 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
except Exception as e:
|
| 188 |
print(f"LLM Error: {e}. Falling back to heuristic.")
|
|
@@ -202,7 +258,7 @@ def main():
|
|
| 202 |
parser.add_argument("--agent", type=str, choices=["heuristic", "llm"], default="heuristic")
|
| 203 |
args = parser.parse_args()
|
| 204 |
|
| 205 |
-
if args.agent == "llm" and
|
| 206 |
console.print("[bold red]Error:[/bold red] litellm package not installed. Run: pip install litellm")
|
| 207 |
return
|
| 208 |
|
|
|
|
| 11 |
import time
|
| 12 |
from typing import Literal
|
| 13 |
|
| 14 |
+
HAS_LITELLM = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
from env.environment import EcoGridEnv
|
| 17 |
from env.tasks import BasicGridBalanceGrader, RenewableVariabilityGrader, CarbonConstrainedGrader
|
| 18 |
+
from env.action_utils import safe_grid_action
|
| 19 |
from models.schemas import GridAction, GridState
|
| 20 |
|
| 21 |
_trained_model = None
|
| 22 |
_trained_tokenizer = None
|
| 23 |
+
TASK_EPISODE_LENGTH = {"easy": 48, "medium": 96, "hard": 96}
|
| 24 |
+
FOSSIL_EMISSION_FACTOR = 0.5
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _get_litellm():
|
| 28 |
+
"""Lazily import litellm to avoid startup-time network side effects."""
|
| 29 |
+
global HAS_LITELLM
|
| 30 |
+
if HAS_LITELLM is False:
|
| 31 |
+
return None
|
| 32 |
+
try:
|
| 33 |
+
import litellm
|
| 34 |
+
|
| 35 |
+
HAS_LITELLM = True
|
| 36 |
+
return litellm
|
| 37 |
+
except ImportError:
|
| 38 |
+
HAS_LITELLM = False
|
| 39 |
+
return None
|
| 40 |
|
| 41 |
def load_trained_model():
|
| 42 |
"""Lazily load the LoRA model if available."""
|
|
|
|
| 106 |
content = content[3:-3]
|
| 107 |
|
| 108 |
data = json.loads(content)
|
| 109 |
+
return safe_grid_action(
|
| 110 |
+
renewable_ratio=data.get("renewable_ratio", 0.5),
|
| 111 |
+
fossil_ratio=data.get("fossil_ratio", 0.5),
|
| 112 |
+
battery_action=data.get("battery_action", 0.0),
|
| 113 |
+
)
|
| 114 |
except Exception as e:
|
| 115 |
print(f"Local LLM Error: {e}. Falling back to heuristic.")
|
| 116 |
return heuristic_agent(state, task_name)
|
| 117 |
|
| 118 |
|
| 119 |
|
| 120 |
+
def _constraint_aware_hard_controller(state: GridState) -> GridAction:
|
| 121 |
+
"""Hard-mode controller that enforces carbon budget pacing."""
|
| 122 |
+
remaining_steps = max(1, TASK_EPISODE_LENGTH["hard"] - state.time_step)
|
| 123 |
avg_renewable_cap = (state.solar_capacity + state.wind_capacity) / 2.0
|
| 124 |
+
avg_renewable_cap = min(1.0, max(0.0, avg_renewable_cap))
|
| 125 |
+
|
| 126 |
+
# Budget-aware fossil cap:
|
| 127 |
+
# carbon_per_step = fossil_ratio * demand * emission_factor
|
| 128 |
+
# => fossil_ratio <= carbon_budget_remaining / (remaining_steps * demand * emission_factor)
|
| 129 |
if state.demand > 0:
|
| 130 |
+
budget_fossil_cap = state.carbon_budget_remaining / (
|
| 131 |
+
remaining_steps * state.demand * FOSSIL_EMISSION_FACTOR
|
| 132 |
+
)
|
| 133 |
else:
|
| 134 |
+
budget_fossil_cap = 0.0
|
| 135 |
+
|
| 136 |
+
# Keep a safety margin to avoid late-episode budget collapse.
|
| 137 |
+
budget_fossil_cap = max(0.0, min(0.14, budget_fossil_cap * 0.92))
|
| 138 |
+
future_floor = remaining_steps * max(state.demand, 1.0) * FOSSIL_EMISSION_FACTOR * 0.08
|
| 139 |
+
if state.grid_stability < 0.75 and state.carbon_budget_remaining > future_floor:
|
| 140 |
+
budget_fossil_cap = min(0.18, budget_fossil_cap + 0.03)
|
| 141 |
+
|
| 142 |
+
renewable_ratio = min(0.9, max(0.62, avg_renewable_cap + 0.12))
|
| 143 |
+
fossil_ratio = min(max(0.02, 1.0 - renewable_ratio), budget_fossil_cap)
|
| 144 |
+
|
| 145 |
+
# Battery dispatch policy:
|
| 146 |
+
# - discharge on high demand or low stability
|
| 147 |
+
# - charge when demand is light and stability is healthy
|
| 148 |
+
if (state.demand > 100 or state.grid_stability < 0.8) and state.battery_level > 0.12:
|
| 149 |
+
battery_action = -0.9
|
| 150 |
+
elif state.demand < 78 and state.battery_level < 0.7 and avg_renewable_cap > 0.4:
|
| 151 |
+
battery_action = 0.6
|
| 152 |
+
else:
|
| 153 |
+
battery_action = 0.0
|
| 154 |
+
|
| 155 |
+
return safe_grid_action(
|
| 156 |
+
renewable_ratio=renewable_ratio,
|
| 157 |
+
fossil_ratio=fossil_ratio,
|
| 158 |
+
battery_action=battery_action,
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def heuristic_agent(state: GridState, task_name: str) -> GridAction:
|
| 163 |
+
"""Constraint-aware baseline agent with strict action validity guarantees."""
|
| 164 |
+
if task_name == "hard":
|
| 165 |
+
return _constraint_aware_hard_controller(state)
|
| 166 |
+
|
| 167 |
+
avg_renewable_cap = (state.solar_capacity + state.wind_capacity) / 2.0
|
| 168 |
+
avg_renewable_cap = min(1.0, max(0.0, avg_renewable_cap))
|
| 169 |
+
renewable_ratio = min(0.95, max(0.05, avg_renewable_cap))
|
| 170 |
+
fossil_ratio = max(0.0, 1.0 - avg_renewable_cap)
|
| 171 |
+
|
| 172 |
+
if task_name == "medium" and (state.grid_stability < 0.8 or state.demand > 105):
|
| 173 |
+
fossil_ratio = min(1.0, fossil_ratio + 0.05)
|
| 174 |
+
|
| 175 |
if state.demand > 100 and state.battery_level > 0.2:
|
| 176 |
+
battery_action = -0.9
|
| 177 |
+
if task_name == "medium":
|
| 178 |
+
fossil_ratio = max(0.0, fossil_ratio - 0.05)
|
| 179 |
+
elif state.demand < 70 and state.battery_level < 0.8 and avg_renewable_cap > 0.5:
|
| 180 |
+
battery_action = 0.7
|
| 181 |
+
if task_name == "medium":
|
| 182 |
+
fossil_ratio = min(1.0, fossil_ratio + 0.03)
|
| 183 |
+
else:
|
| 184 |
+
battery_action = 0.0
|
| 185 |
+
|
| 186 |
+
return safe_grid_action(
|
| 187 |
+
renewable_ratio=renewable_ratio,
|
| 188 |
+
fossil_ratio=fossil_ratio,
|
| 189 |
+
battery_action=battery_action,
|
| 190 |
)
|
| 191 |
|
| 192 |
|
| 193 |
def llm_agent(state: GridState, task_name: str) -> GridAction:
|
| 194 |
"""An agent that uses an LLM to make decisions via Chain-of-Thought."""
|
| 195 |
+
litellm = _get_litellm()
|
| 196 |
+
if litellm is None:
|
| 197 |
+
return heuristic_agent(state, task_name)
|
| 198 |
|
| 199 |
prompt = f"""
|
| 200 |
You are an expert energy grid operator managing a power grid.
|
|
|
|
| 234 |
content = content[3:-3]
|
| 235 |
|
| 236 |
data = json.loads(content)
|
| 237 |
+
return safe_grid_action(
|
| 238 |
+
renewable_ratio=data.get("renewable_ratio", 0.5),
|
| 239 |
+
fossil_ratio=data.get("fossil_ratio", 0.5),
|
| 240 |
+
battery_action=data.get("battery_action", 0.0),
|
| 241 |
+
)
|
| 242 |
|
| 243 |
except Exception as e:
|
| 244 |
print(f"LLM Error: {e}. Falling back to heuristic.")
|
|
|
|
| 258 |
parser.add_argument("--agent", type=str, choices=["heuristic", "llm"], default="heuristic")
|
| 259 |
args = parser.parse_args()
|
| 260 |
|
| 261 |
+
if args.agent == "llm" and _get_litellm() is None:
|
| 262 |
console.print("[bold red]Error:[/bold red] litellm package not installed. Run: pip install litellm")
|
| 263 |
return
|
| 264 |
|
env/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
"""EcoGrid-OpenEnv environment package."""
|
| 2 |
|
| 3 |
from env.environment import EcoGridEnv
|
|
|
|
| 4 |
|
| 5 |
-
__all__ = ["EcoGridEnv"]
|
|
|
|
| 1 |
"""EcoGrid-OpenEnv environment package."""
|
| 2 |
|
| 3 |
from env.environment import EcoGridEnv
|
| 4 |
+
from env.action_utils import safe_grid_action, coerce_grid_action
|
| 5 |
|
| 6 |
+
__all__ = ["EcoGridEnv", "safe_grid_action", "coerce_grid_action"]
|
env/action_utils.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Action utility helpers for safety and normalization.
|
| 3 |
+
|
| 4 |
+
These helpers keep action generation robust across:
|
| 5 |
+
- model output parsing
|
| 6 |
+
- heuristic controllers
|
| 7 |
+
- API payload conversion
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
from typing import Any, Optional, Tuple
|
| 13 |
+
|
| 14 |
+
from models.schemas import GridAction
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def normalize_action_components(
|
| 18 |
+
renewable_ratio: float,
|
| 19 |
+
fossil_ratio: float,
|
| 20 |
+
battery_action: float,
|
| 21 |
+
ndigits: int = 3,
|
| 22 |
+
) -> tuple[float, float, float]:
|
| 23 |
+
"""Clamp + normalize action components and keep sum <= 1 after rounding."""
|
| 24 |
+
ren = max(0.0, min(1.0, float(renewable_ratio)))
|
| 25 |
+
fos = max(0.0, min(1.0, float(fossil_ratio)))
|
| 26 |
+
bat = max(-1.0, min(1.0, float(battery_action)))
|
| 27 |
+
|
| 28 |
+
total = ren + fos
|
| 29 |
+
if total > 1.0 and total > 0:
|
| 30 |
+
ren /= total
|
| 31 |
+
fos /= total
|
| 32 |
+
|
| 33 |
+
# Round for consistent logging/UI while preserving constraints.
|
| 34 |
+
ren = round(ren, ndigits)
|
| 35 |
+
fos = round(fos, ndigits)
|
| 36 |
+
bat = round(bat, ndigits)
|
| 37 |
+
|
| 38 |
+
total_rounded = ren + fos
|
| 39 |
+
if total_rounded > 1.0:
|
| 40 |
+
overflow = round(total_rounded - 1.0, ndigits + 2)
|
| 41 |
+
# Remove overflow from fossil first, then renewable.
|
| 42 |
+
reduce_fos = min(fos, overflow)
|
| 43 |
+
fos = round(fos - reduce_fos, ndigits)
|
| 44 |
+
overflow = round(overflow - reduce_fos, ndigits + 2)
|
| 45 |
+
if overflow > 0:
|
| 46 |
+
ren = round(max(0.0, ren - overflow), ndigits)
|
| 47 |
+
|
| 48 |
+
return ren, fos, bat
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def safe_grid_action(
|
| 52 |
+
renewable_ratio: float,
|
| 53 |
+
fossil_ratio: float,
|
| 54 |
+
battery_action: float,
|
| 55 |
+
ndigits: int = 3,
|
| 56 |
+
) -> GridAction:
|
| 57 |
+
"""Build a validated GridAction after normalization."""
|
| 58 |
+
ren, fos, bat = normalize_action_components(
|
| 59 |
+
renewable_ratio=renewable_ratio,
|
| 60 |
+
fossil_ratio=fossil_ratio,
|
| 61 |
+
battery_action=battery_action,
|
| 62 |
+
ndigits=ndigits,
|
| 63 |
+
)
|
| 64 |
+
return GridAction(
|
| 65 |
+
renewable_ratio=ren,
|
| 66 |
+
fossil_ratio=fos,
|
| 67 |
+
battery_action=bat,
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def coerce_grid_action(
|
| 72 |
+
action_like: Any,
|
| 73 |
+
default_action: Optional[GridAction] = None,
|
| 74 |
+
) -> Tuple[GridAction, Optional[str]]:
|
| 75 |
+
"""Convert action payloads to a valid GridAction with graceful fallback."""
|
| 76 |
+
try:
|
| 77 |
+
if isinstance(action_like, GridAction):
|
| 78 |
+
return action_like, None
|
| 79 |
+
|
| 80 |
+
if isinstance(action_like, dict):
|
| 81 |
+
payload = action_like.get("action", action_like)
|
| 82 |
+
return safe_grid_action(
|
| 83 |
+
renewable_ratio=payload.get("renewable_ratio", 0.5),
|
| 84 |
+
fossil_ratio=payload.get("fossil_ratio", 0.5),
|
| 85 |
+
battery_action=payload.get("battery_action", 0.0),
|
| 86 |
+
), None
|
| 87 |
+
except Exception as exc:
|
| 88 |
+
if default_action is not None:
|
| 89 |
+
return default_action, f"invalid_action_payload: {type(exc).__name__}"
|
| 90 |
+
raise
|
| 91 |
+
|
| 92 |
+
if default_action is not None:
|
| 93 |
+
return default_action, "invalid_action_type"
|
| 94 |
+
|
| 95 |
+
raise ValueError("Unable to coerce action payload into GridAction.")
|
env/dynamics.py
CHANGED
|
@@ -92,12 +92,20 @@ def demand_curve(
|
|
| 92 |
# Evening peak (around hour 18)
|
| 93 |
evening = 40.0 * np.exp(-((hour - 18) ** 2) / 8.0)
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
# Random spike (occurs with probability proportional to volatility)
|
| 96 |
spike = 0.0
|
| 97 |
if volatility > 0 and rng.random() < 0.05 * volatility:
|
| 98 |
spike = rng.uniform(10, 40) * volatility
|
| 99 |
|
| 100 |
-
demand = base_demand + morning + evening + spike
|
| 101 |
return float(np.clip(demand, 0.0, 200.0))
|
| 102 |
|
| 103 |
|
|
@@ -106,6 +114,8 @@ def update_battery(
|
|
| 106 |
action: float,
|
| 107 |
capacity: float,
|
| 108 |
charge_rate: float = 0.15,
|
|
|
|
|
|
|
| 109 |
) -> float:
|
| 110 |
"""Update battery state of charge.
|
| 111 |
|
|
@@ -120,7 +130,12 @@ def update_battery(
|
|
| 120 |
"""
|
| 121 |
if capacity <= 0:
|
| 122 |
return 0.0
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
return float(np.clip(level + delta, 0.0, 1.0))
|
| 125 |
|
| 126 |
|
|
@@ -168,7 +183,10 @@ def compute_supply(
|
|
| 168 |
battery_level: float,
|
| 169 |
battery_capacity: float,
|
| 170 |
demand: float,
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
| 172 |
"""Compute total energy supply from all sources.
|
| 173 |
|
| 174 |
Args:
|
|
@@ -188,14 +206,28 @@ def compute_supply(
|
|
| 188 |
avg_renewable_cap = (solar_cap + wind_cap) / 2.0
|
| 189 |
renewable_supply = action_renewable * demand * min(1.0, avg_renewable_cap / max(action_renewable, 0.01))
|
| 190 |
|
| 191 |
-
# Fossil
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
# Battery can supplement supply when discharging
|
| 195 |
battery_supply = 0.0
|
| 196 |
if battery_action < 0 and battery_capacity > 0:
|
| 197 |
# Discharging: supply is proportional to discharge rate and level
|
| 198 |
-
battery_supply =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
total = renewable_supply + fossil_supply + battery_supply
|
| 201 |
return (
|
|
@@ -203,6 +235,7 @@ def compute_supply(
|
|
| 203 |
float(fossil_supply),
|
| 204 |
float(battery_supply),
|
| 205 |
float(total),
|
|
|
|
| 206 |
)
|
| 207 |
|
| 208 |
|
|
|
|
| 92 |
# Evening peak (around hour 18)
|
| 93 |
evening = 40.0 * np.exp(-((hour - 18) ** 2) / 8.0)
|
| 94 |
|
| 95 |
+
# Weekly operational cycle (business-day effect)
|
| 96 |
+
day_of_week = (time_step // 24) % 7
|
| 97 |
+
weekday_multiplier = 1.0 if day_of_week < 5 else 0.92
|
| 98 |
+
|
| 99 |
+
# Correlated weather/event noise on demand.
|
| 100 |
+
# Deterministic under the seeded RNG.
|
| 101 |
+
stochastic_component = rng.normal(0, 4.0 * max(0.2, volatility))
|
| 102 |
+
|
| 103 |
# Random spike (occurs with probability proportional to volatility)
|
| 104 |
spike = 0.0
|
| 105 |
if volatility > 0 and rng.random() < 0.05 * volatility:
|
| 106 |
spike = rng.uniform(10, 40) * volatility
|
| 107 |
|
| 108 |
+
demand = (base_demand + morning + evening + stochastic_component + spike) * weekday_multiplier
|
| 109 |
return float(np.clip(demand, 0.0, 200.0))
|
| 110 |
|
| 111 |
|
|
|
|
| 114 |
action: float,
|
| 115 |
capacity: float,
|
| 116 |
charge_rate: float = 0.15,
|
| 117 |
+
charge_efficiency: float = 0.94,
|
| 118 |
+
discharge_efficiency: float = 0.94,
|
| 119 |
) -> float:
|
| 120 |
"""Update battery state of charge.
|
| 121 |
|
|
|
|
| 130 |
"""
|
| 131 |
if capacity <= 0:
|
| 132 |
return 0.0
|
| 133 |
+
if action >= 0:
|
| 134 |
+
delta = action * charge_rate * capacity * charge_efficiency
|
| 135 |
+
else:
|
| 136 |
+
# Discharging removes more SoC than delivered energy because of losses.
|
| 137 |
+
eff = max(discharge_efficiency, 1e-6)
|
| 138 |
+
delta = action * charge_rate * capacity / eff
|
| 139 |
return float(np.clip(level + delta, 0.0, 1.0))
|
| 140 |
|
| 141 |
|
|
|
|
| 183 |
battery_level: float,
|
| 184 |
battery_capacity: float,
|
| 185 |
demand: float,
|
| 186 |
+
previous_fossil_ratio: float | None = None,
|
| 187 |
+
fossil_ramp_limit: float | None = None,
|
| 188 |
+
discharge_efficiency: float = 0.94,
|
| 189 |
+
) -> tuple[float, float, float, float, float]:
|
| 190 |
"""Compute total energy supply from all sources.
|
| 191 |
|
| 192 |
Args:
|
|
|
|
| 206 |
avg_renewable_cap = (solar_cap + wind_cap) / 2.0
|
| 207 |
renewable_supply = action_renewable * demand * min(1.0, avg_renewable_cap / max(action_renewable, 0.01))
|
| 208 |
|
| 209 |
+
# Fossil ramp-rate constraints emulate thermal plant limitations.
|
| 210 |
+
effective_fossil_ratio = action_fossil
|
| 211 |
+
if previous_fossil_ratio is not None and fossil_ramp_limit is not None:
|
| 212 |
+
lower = max(0.0, previous_fossil_ratio - fossil_ramp_limit)
|
| 213 |
+
upper = min(1.0, previous_fossil_ratio + fossil_ramp_limit)
|
| 214 |
+
effective_fossil_ratio = float(np.clip(action_fossil, lower, upper))
|
| 215 |
+
|
| 216 |
+
# Fossil supply (dispatchable but ramp-limited when configured)
|
| 217 |
+
fossil_supply = effective_fossil_ratio * demand
|
| 218 |
|
| 219 |
# Battery can supplement supply when discharging
|
| 220 |
battery_supply = 0.0
|
| 221 |
if battery_action < 0 and battery_capacity > 0:
|
| 222 |
# Discharging: supply is proportional to discharge rate and level
|
| 223 |
+
battery_supply = (
|
| 224 |
+
abs(battery_action)
|
| 225 |
+
* battery_level
|
| 226 |
+
* battery_capacity
|
| 227 |
+
* demand
|
| 228 |
+
* 0.2
|
| 229 |
+
* discharge_efficiency
|
| 230 |
+
)
|
| 231 |
|
| 232 |
total = renewable_supply + fossil_supply + battery_supply
|
| 233 |
return (
|
|
|
|
| 235 |
float(fossil_supply),
|
| 236 |
float(battery_supply),
|
| 237 |
float(total),
|
| 238 |
+
float(effective_fossil_ratio),
|
| 239 |
)
|
| 240 |
|
| 241 |
|
env/environment.py
CHANGED
|
@@ -11,6 +11,7 @@ import numpy as np
|
|
| 11 |
from typing import Literal, Optional
|
| 12 |
|
| 13 |
from models.schemas import GridState, GridAction, StepResult, TaskConfig
|
|
|
|
| 14 |
from env.dynamics import (
|
| 15 |
solar_output,
|
| 16 |
wind_output,
|
|
@@ -38,6 +39,9 @@ TASK_CONFIGS = {
|
|
| 38 |
demand_volatility=0.2,
|
| 39 |
carbon_strict=False,
|
| 40 |
volatility_multiplier=1.0,
|
|
|
|
|
|
|
|
|
|
| 41 |
description="Stable solar, flat demand, no battery. Goal: minimise cost.",
|
| 42 |
),
|
| 43 |
"medium": TaskConfig(
|
|
@@ -50,6 +54,9 @@ TASK_CONFIGS = {
|
|
| 50 |
demand_volatility=1.0,
|
| 51 |
carbon_strict=False,
|
| 52 |
volatility_multiplier=1.0,
|
|
|
|
|
|
|
|
|
|
| 53 |
description="Noisy solar+wind, demand spikes, small battery. Goal: avoid blackouts.",
|
| 54 |
),
|
| 55 |
"hard": TaskConfig(
|
|
@@ -62,6 +69,9 @@ TASK_CONFIGS = {
|
|
| 62 |
demand_volatility=1.5,
|
| 63 |
carbon_strict=True, # Episode ends on overrun
|
| 64 |
volatility_multiplier=2.0, # 2× noise on renewables
|
|
|
|
|
|
|
|
|
|
| 65 |
description="Strict carbon cap, high volatility, limited storage. Episode ends on overrun.",
|
| 66 |
),
|
| 67 |
}
|
|
@@ -95,6 +105,7 @@ class EcoGridEnv:
|
|
| 95 |
self._episode_log: list[StepResult] = []
|
| 96 |
self._previous_wind: float = 0.4
|
| 97 |
self._previous_stability: float = 0.9
|
|
|
|
| 98 |
|
| 99 |
def reset(
|
| 100 |
self,
|
|
@@ -117,6 +128,7 @@ class EcoGridEnv:
|
|
| 117 |
self._episode_log = []
|
| 118 |
self._previous_wind = 0.4
|
| 119 |
self._previous_stability = 0.9
|
|
|
|
| 120 |
|
| 121 |
# Generate initial state
|
| 122 |
config = self._task_config
|
|
@@ -143,7 +155,7 @@ class EcoGridEnv:
|
|
| 143 |
)
|
| 144 |
return self._state
|
| 145 |
|
| 146 |
-
def step(self, action: GridAction) -> StepResult:
|
| 147 |
"""Execute one timestep of the environment.
|
| 148 |
|
| 149 |
Args:
|
|
@@ -164,6 +176,15 @@ class EcoGridEnv:
|
|
| 164 |
assert config is not None
|
| 165 |
assert self._rng is not None
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
self._step_count += 1
|
| 168 |
prev_state = self._state
|
| 169 |
effective_noise = config.noise_level * config.volatility_multiplier
|
|
@@ -180,7 +201,13 @@ class EcoGridEnv:
|
|
| 180 |
)
|
| 181 |
|
| 182 |
# ── Compute supply from agent's action ──
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
action.renewable_ratio,
|
| 185 |
action.fossil_ratio,
|
| 186 |
action.battery_action,
|
|
@@ -189,20 +216,26 @@ class EcoGridEnv:
|
|
| 189 |
prev_state.battery_level,
|
| 190 |
config.battery_capacity,
|
| 191 |
prev_state.demand,
|
|
|
|
|
|
|
|
|
|
| 192 |
)
|
|
|
|
| 193 |
|
| 194 |
# ── Update battery ──
|
| 195 |
new_battery = update_battery(
|
| 196 |
prev_state.battery_level,
|
| 197 |
action.battery_action,
|
| 198 |
config.battery_capacity,
|
|
|
|
|
|
|
| 199 |
)
|
| 200 |
|
| 201 |
# ── Compute blackout risk ──
|
| 202 |
blackout = compute_blackout_risk(prev_state.demand, total_supply)
|
| 203 |
|
| 204 |
# ── Compute carbon emissions ──
|
| 205 |
-
emissions = carbon_emission(
|
| 206 |
new_carbon = prev_state.carbon_budget_remaining - emissions
|
| 207 |
|
| 208 |
# ── Compute grid stability ──
|
|
@@ -233,7 +266,19 @@ class EcoGridEnv:
|
|
| 233 |
|
| 234 |
# ── Compute reward ──
|
| 235 |
reward, breakdown = compute_reward(
|
| 236 |
-
prev_state,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
)
|
| 238 |
|
| 239 |
# ── Check termination conditions ──
|
|
@@ -258,7 +303,10 @@ class EcoGridEnv:
|
|
| 258 |
"blackout_risk": round(blackout, 4),
|
| 259 |
"carbon_emitted_step": round(emissions, 2),
|
| 260 |
"termination_reason": termination_reason,
|
|
|
|
| 261 |
}
|
|
|
|
|
|
|
| 262 |
|
| 263 |
result = StepResult(
|
| 264 |
observation=next_state,
|
|
|
|
| 11 |
from typing import Literal, Optional
|
| 12 |
|
| 13 |
from models.schemas import GridState, GridAction, StepResult, TaskConfig
|
| 14 |
+
from env.action_utils import coerce_grid_action
|
| 15 |
from env.dynamics import (
|
| 16 |
solar_output,
|
| 17 |
wind_output,
|
|
|
|
| 39 |
demand_volatility=0.2,
|
| 40 |
carbon_strict=False,
|
| 41 |
volatility_multiplier=1.0,
|
| 42 |
+
fossil_ramp_limit=1.0,
|
| 43 |
+
battery_charge_efficiency=0.95,
|
| 44 |
+
battery_discharge_efficiency=0.95,
|
| 45 |
description="Stable solar, flat demand, no battery. Goal: minimise cost.",
|
| 46 |
),
|
| 47 |
"medium": TaskConfig(
|
|
|
|
| 54 |
demand_volatility=1.0,
|
| 55 |
carbon_strict=False,
|
| 56 |
volatility_multiplier=1.0,
|
| 57 |
+
fossil_ramp_limit=0.35,
|
| 58 |
+
battery_charge_efficiency=0.94,
|
| 59 |
+
battery_discharge_efficiency=0.94,
|
| 60 |
description="Noisy solar+wind, demand spikes, small battery. Goal: avoid blackouts.",
|
| 61 |
),
|
| 62 |
"hard": TaskConfig(
|
|
|
|
| 69 |
demand_volatility=1.5,
|
| 70 |
carbon_strict=True, # Episode ends on overrun
|
| 71 |
volatility_multiplier=2.0, # 2× noise on renewables
|
| 72 |
+
fossil_ramp_limit=0.22,
|
| 73 |
+
battery_charge_efficiency=0.93,
|
| 74 |
+
battery_discharge_efficiency=0.93,
|
| 75 |
description="Strict carbon cap, high volatility, limited storage. Episode ends on overrun.",
|
| 76 |
),
|
| 77 |
}
|
|
|
|
| 105 |
self._episode_log: list[StepResult] = []
|
| 106 |
self._previous_wind: float = 0.4
|
| 107 |
self._previous_stability: float = 0.9
|
| 108 |
+
self._previous_fossil_ratio: float = 0.0
|
| 109 |
|
| 110 |
def reset(
|
| 111 |
self,
|
|
|
|
| 128 |
self._episode_log = []
|
| 129 |
self._previous_wind = 0.4
|
| 130 |
self._previous_stability = 0.9
|
| 131 |
+
self._previous_fossil_ratio = 0.0
|
| 132 |
|
| 133 |
# Generate initial state
|
| 134 |
config = self._task_config
|
|
|
|
| 155 |
)
|
| 156 |
return self._state
|
| 157 |
|
| 158 |
+
def step(self, action: GridAction | dict) -> StepResult:
|
| 159 |
"""Execute one timestep of the environment.
|
| 160 |
|
| 161 |
Args:
|
|
|
|
| 176 |
assert config is not None
|
| 177 |
assert self._rng is not None
|
| 178 |
|
| 179 |
+
action, action_warning = coerce_grid_action(
|
| 180 |
+
action_like=action,
|
| 181 |
+
default_action=GridAction(
|
| 182 |
+
renewable_ratio=0.5,
|
| 183 |
+
fossil_ratio=0.5,
|
| 184 |
+
battery_action=0.0,
|
| 185 |
+
),
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
self._step_count += 1
|
| 189 |
prev_state = self._state
|
| 190 |
effective_noise = config.noise_level * config.volatility_multiplier
|
|
|
|
| 201 |
)
|
| 202 |
|
| 203 |
# ── Compute supply from agent's action ──
|
| 204 |
+
(
|
| 205 |
+
renewable_supply,
|
| 206 |
+
fossil_supply,
|
| 207 |
+
battery_supply,
|
| 208 |
+
total_supply,
|
| 209 |
+
effective_fossil_ratio,
|
| 210 |
+
) = compute_supply(
|
| 211 |
action.renewable_ratio,
|
| 212 |
action.fossil_ratio,
|
| 213 |
action.battery_action,
|
|
|
|
| 216 |
prev_state.battery_level,
|
| 217 |
config.battery_capacity,
|
| 218 |
prev_state.demand,
|
| 219 |
+
previous_fossil_ratio=self._previous_fossil_ratio,
|
| 220 |
+
fossil_ramp_limit=config.fossil_ramp_limit,
|
| 221 |
+
discharge_efficiency=config.battery_discharge_efficiency,
|
| 222 |
)
|
| 223 |
+
self._previous_fossil_ratio = effective_fossil_ratio
|
| 224 |
|
| 225 |
# ── Update battery ──
|
| 226 |
new_battery = update_battery(
|
| 227 |
prev_state.battery_level,
|
| 228 |
action.battery_action,
|
| 229 |
config.battery_capacity,
|
| 230 |
+
charge_efficiency=config.battery_charge_efficiency,
|
| 231 |
+
discharge_efficiency=config.battery_discharge_efficiency,
|
| 232 |
)
|
| 233 |
|
| 234 |
# ── Compute blackout risk ──
|
| 235 |
blackout = compute_blackout_risk(prev_state.demand, total_supply)
|
| 236 |
|
| 237 |
# ── Compute carbon emissions ──
|
| 238 |
+
emissions = carbon_emission(effective_fossil_ratio, prev_state.demand)
|
| 239 |
new_carbon = prev_state.carbon_budget_remaining - emissions
|
| 240 |
|
| 241 |
# ── Compute grid stability ──
|
|
|
|
| 266 |
|
| 267 |
# ── Compute reward ──
|
| 268 |
reward, breakdown = compute_reward(
|
| 269 |
+
prev_state,
|
| 270 |
+
action,
|
| 271 |
+
next_state,
|
| 272 |
+
config.model_dump(),
|
| 273 |
+
actual_supply=(
|
| 274 |
+
renewable_supply,
|
| 275 |
+
fossil_supply,
|
| 276 |
+
battery_supply,
|
| 277 |
+
total_supply,
|
| 278 |
+
effective_fossil_ratio,
|
| 279 |
+
),
|
| 280 |
+
actual_blackout_risk=blackout,
|
| 281 |
+
actual_emissions=emissions,
|
| 282 |
)
|
| 283 |
|
| 284 |
# ── Check termination conditions ──
|
|
|
|
| 303 |
"blackout_risk": round(blackout, 4),
|
| 304 |
"carbon_emitted_step": round(emissions, 2),
|
| 305 |
"termination_reason": termination_reason,
|
| 306 |
+
"effective_fossil_ratio": round(effective_fossil_ratio, 4),
|
| 307 |
}
|
| 308 |
+
if action_warning:
|
| 309 |
+
info["action_warning"] = action_warning
|
| 310 |
|
| 311 |
result = StepResult(
|
| 312 |
observation=next_state,
|
env/reward.py
CHANGED
|
@@ -20,6 +20,9 @@ def compute_reward(
|
|
| 20 |
action: GridAction,
|
| 21 |
next_state: GridState,
|
| 22 |
task_config: Dict[str, Any],
|
|
|
|
|
|
|
|
|
|
| 23 |
) -> Tuple[float, Dict[str, float]]:
|
| 24 |
"""Compute dense reward for a single timestep.
|
| 25 |
|
|
@@ -35,16 +38,31 @@ def compute_reward(
|
|
| 35 |
# ── 1. Base Components ──
|
| 36 |
|
| 37 |
# Recompute supply to get exact fossil/renewable usage for this step
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
# Cost Score (Weight: 0.30)
|
| 50 |
# Fossil fuels are expensive. Grid purchases (for unmet demand) are very expensive.
|
|
@@ -62,7 +80,11 @@ def compute_reward(
|
|
| 62 |
|
| 63 |
# Carbon Score (Weight: 0.30)
|
| 64 |
# Penalise carbon emissions relative to total demand.
|
| 65 |
-
emissions =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
if state.demand > 0:
|
| 67 |
# Normalise by worst case (100% fossil generation)
|
| 68 |
worst_case_emissions = carbon_emission(1.0, state.demand)
|
|
@@ -73,7 +95,11 @@ def compute_reward(
|
|
| 73 |
|
| 74 |
# Stability Score (Weight: 0.25)
|
| 75 |
# Directly inversely proportional to blackout risk
|
| 76 |
-
blackout_risk =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
stability_score = 1.0 - blackout_risk
|
| 78 |
|
| 79 |
# Renewable Bonus (Weight: 0.15)
|
|
|
|
| 20 |
action: GridAction,
|
| 21 |
next_state: GridState,
|
| 22 |
task_config: Dict[str, Any],
|
| 23 |
+
actual_supply: Tuple[float, float, float, float, float] | None = None,
|
| 24 |
+
actual_blackout_risk: float | None = None,
|
| 25 |
+
actual_emissions: float | None = None,
|
| 26 |
) -> Tuple[float, Dict[str, float]]:
|
| 27 |
"""Compute dense reward for a single timestep.
|
| 28 |
|
|
|
|
| 38 |
# ── 1. Base Components ──
|
| 39 |
|
| 40 |
# Recompute supply to get exact fossil/renewable usage for this step
|
| 41 |
+
if actual_supply is None:
|
| 42 |
+
(
|
| 43 |
+
renewable_supply,
|
| 44 |
+
fossil_supply,
|
| 45 |
+
battery_supply,
|
| 46 |
+
total_supply,
|
| 47 |
+
effective_fossil_ratio,
|
| 48 |
+
) = compute_supply(
|
| 49 |
+
action.renewable_ratio,
|
| 50 |
+
action.fossil_ratio,
|
| 51 |
+
action.battery_action,
|
| 52 |
+
state.solar_capacity,
|
| 53 |
+
state.wind_capacity,
|
| 54 |
+
state.battery_level,
|
| 55 |
+
task_config["battery_capacity"],
|
| 56 |
+
state.demand,
|
| 57 |
+
)
|
| 58 |
+
else:
|
| 59 |
+
(
|
| 60 |
+
renewable_supply,
|
| 61 |
+
fossil_supply,
|
| 62 |
+
battery_supply,
|
| 63 |
+
total_supply,
|
| 64 |
+
effective_fossil_ratio,
|
| 65 |
+
) = actual_supply
|
| 66 |
|
| 67 |
# Cost Score (Weight: 0.30)
|
| 68 |
# Fossil fuels are expensive. Grid purchases (for unmet demand) are very expensive.
|
|
|
|
| 80 |
|
| 81 |
# Carbon Score (Weight: 0.30)
|
| 82 |
# Penalise carbon emissions relative to total demand.
|
| 83 |
+
emissions = (
|
| 84 |
+
actual_emissions
|
| 85 |
+
if actual_emissions is not None
|
| 86 |
+
else carbon_emission(effective_fossil_ratio, state.demand)
|
| 87 |
+
)
|
| 88 |
if state.demand > 0:
|
| 89 |
# Normalise by worst case (100% fossil generation)
|
| 90 |
worst_case_emissions = carbon_emission(1.0, state.demand)
|
|
|
|
| 95 |
|
| 96 |
# Stability Score (Weight: 0.25)
|
| 97 |
# Directly inversely proportional to blackout risk
|
| 98 |
+
blackout_risk = (
|
| 99 |
+
actual_blackout_risk
|
| 100 |
+
if actual_blackout_risk is not None
|
| 101 |
+
else compute_blackout_risk(state.demand, total_supply)
|
| 102 |
+
)
|
| 103 |
stability_score = 1.0 - blackout_risk
|
| 104 |
|
| 105 |
# Renewable Bonus (Weight: 0.15)
|
inference.py
CHANGED
|
@@ -19,6 +19,7 @@ from typing import List, Optional
|
|
| 19 |
from openai import OpenAI
|
| 20 |
|
| 21 |
from env.environment import EcoGridEnv
|
|
|
|
| 22 |
from env.tasks import BasicGridBalanceGrader, RenewableVariabilityGrader, CarbonConstrainedGrader
|
| 23 |
from models.schemas import GridAction, GridState
|
| 24 |
|
|
@@ -100,10 +101,10 @@ def _fallback_action(task_name: str, state: GridState) -> GridAction:
|
|
| 100 |
elif state.demand < 60 and state.battery_level < 0.8:
|
| 101 |
battery_action = 0.8
|
| 102 |
|
| 103 |
-
return
|
| 104 |
-
renewable_ratio=
|
| 105 |
-
fossil_ratio=
|
| 106 |
-
battery_action=
|
| 107 |
)
|
| 108 |
|
| 109 |
|
|
@@ -156,7 +157,11 @@ Then, output ONLY a valid JSON object matching this schema, with no markdown fen
|
|
| 156 |
content = content[3:-3]
|
| 157 |
|
| 158 |
data = json.loads(content)
|
| 159 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
|
| 162 |
# ---------------------------------------------------------------------------
|
|
|
|
| 19 |
from openai import OpenAI
|
| 20 |
|
| 21 |
from env.environment import EcoGridEnv
|
| 22 |
+
from env.action_utils import safe_grid_action
|
| 23 |
from env.tasks import BasicGridBalanceGrader, RenewableVariabilityGrader, CarbonConstrainedGrader
|
| 24 |
from models.schemas import GridAction, GridState
|
| 25 |
|
|
|
|
| 101 |
elif state.demand < 60 and state.battery_level < 0.8:
|
| 102 |
battery_action = 0.8
|
| 103 |
|
| 104 |
+
return safe_grid_action(
|
| 105 |
+
renewable_ratio=renewable_ratio,
|
| 106 |
+
fossil_ratio=fossil_ratio,
|
| 107 |
+
battery_action=battery_action,
|
| 108 |
)
|
| 109 |
|
| 110 |
|
|
|
|
| 157 |
content = content[3:-3]
|
| 158 |
|
| 159 |
data = json.loads(content)
|
| 160 |
+
return safe_grid_action(
|
| 161 |
+
renewable_ratio=data.get("renewable_ratio", preferred.renewable_ratio),
|
| 162 |
+
fossil_ratio=data.get("fossil_ratio", preferred.fossil_ratio),
|
| 163 |
+
battery_action=data.get("battery_action", preferred.battery_action),
|
| 164 |
+
)
|
| 165 |
|
| 166 |
|
| 167 |
# ---------------------------------------------------------------------------
|
models/schemas.py
CHANGED
|
@@ -177,4 +177,22 @@ class TaskConfig(BaseModel):
|
|
| 177 |
ge=0,
|
| 178 |
description="Multiplier for renewable noise (2x for hard task)",
|
| 179 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
description: str = Field(default="", description="Human-readable task description")
|
|
|
|
| 177 |
ge=0,
|
| 178 |
description="Multiplier for renewable noise (2x for hard task)",
|
| 179 |
)
|
| 180 |
+
fossil_ramp_limit: float = Field(
|
| 181 |
+
default=0.35,
|
| 182 |
+
ge=0,
|
| 183 |
+
le=1,
|
| 184 |
+
description="Max allowed step-to-step change in fossil ratio",
|
| 185 |
+
)
|
| 186 |
+
battery_charge_efficiency: float = Field(
|
| 187 |
+
default=0.94,
|
| 188 |
+
ge=0.5,
|
| 189 |
+
le=1.0,
|
| 190 |
+
description="Battery charge efficiency",
|
| 191 |
+
)
|
| 192 |
+
battery_discharge_efficiency: float = Field(
|
| 193 |
+
default=0.94,
|
| 194 |
+
ge=0.5,
|
| 195 |
+
le=1.0,
|
| 196 |
+
description="Battery discharge efficiency",
|
| 197 |
+
)
|
| 198 |
description: str = Field(default="", description="Human-readable task description")
|
pyproject.toml
CHANGED
|
@@ -18,12 +18,17 @@ dependencies = [
|
|
| 18 |
"plotly>=5.18.0",
|
| 19 |
"openai>=1.10.0",
|
| 20 |
"litellm>=1.0.0",
|
| 21 |
-
"rich>=13.0.0"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
"transformers>=4.40.0",
|
| 23 |
"peft>=0.11.0",
|
| 24 |
-
"accelerate>=0.30.0"
|
|
|
|
| 25 |
]
|
| 26 |
-
requires-python = ">=3.10"
|
| 27 |
|
| 28 |
[project.scripts]
|
| 29 |
server = "server.app:main"
|
|
|
|
| 18 |
"plotly>=5.18.0",
|
| 19 |
"openai>=1.10.0",
|
| 20 |
"litellm>=1.0.0",
|
| 21 |
+
"rich>=13.0.0"
|
| 22 |
+
]
|
| 23 |
+
requires-python = ">=3.10"
|
| 24 |
+
|
| 25 |
+
[project.optional-dependencies]
|
| 26 |
+
train = [
|
| 27 |
"transformers>=4.40.0",
|
| 28 |
"peft>=0.11.0",
|
| 29 |
+
"accelerate>=0.30.0",
|
| 30 |
+
"trl>=0.24.0",
|
| 31 |
]
|
|
|
|
| 32 |
|
| 33 |
[project.scripts]
|
| 34 |
server = "server.app:main"
|
requirements-train.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-r requirements.txt
|
| 2 |
+
transformers>=4.40.0
|
| 3 |
+
peft>=0.11.0
|
| 4 |
+
accelerate>=0.30.0
|
| 5 |
+
trl>=0.24.0
|
requirements.txt
CHANGED
|
@@ -7,8 +7,3 @@ plotly>=5.18.0
|
|
| 7 |
openai>=1.10.0
|
| 8 |
litellm>=1.0.0
|
| 9 |
rich>=13.0.0
|
| 10 |
-
# trl, unsloth, torch are heavy and omitted for the web dashboard deployment
|
| 11 |
-
# they should be installed locally for training
|
| 12 |
-
transformers>=4.40.0
|
| 13 |
-
peft>=0.11.0
|
| 14 |
-
accelerate>=0.30.0
|
|
|
|
| 7 |
openai>=1.10.0
|
| 8 |
litellm>=1.0.0
|
| 9 |
rich>=13.0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/benchmark.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic benchmark runner for EcoGrid policies."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import json
|
| 7 |
+
import random
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from statistics import mean
|
| 10 |
+
|
| 11 |
+
from baseline import heuristic_agent
|
| 12 |
+
from env.environment import EcoGridEnv
|
| 13 |
+
from env.tasks import (
|
| 14 |
+
BasicGridBalanceGrader,
|
| 15 |
+
CarbonConstrainedGrader,
|
| 16 |
+
RenewableVariabilityGrader,
|
| 17 |
+
)
|
| 18 |
+
from models.schemas import GridAction
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
TASKS = ("easy", "medium", "hard")
|
| 22 |
+
AGENTS = ("random", "heuristic")
|
| 23 |
+
|
| 24 |
+
# Historical reference numbers from pre-fix evaluation snapshot.
|
| 25 |
+
REFERENCE_MEAN = {
|
| 26 |
+
"easy": {"random": 0.269, "heuristic": 0.748},
|
| 27 |
+
"medium": {"random": 0.251, "heuristic": 0.376},
|
| 28 |
+
"hard": {"random": 0.001, "heuristic": 0.001},
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def grade_episode(task: str, episode_log):
|
| 33 |
+
if task == "easy":
|
| 34 |
+
return BasicGridBalanceGrader.grade(episode_log).score
|
| 35 |
+
if task == "medium":
|
| 36 |
+
return RenewableVariabilityGrader.grade(episode_log).score
|
| 37 |
+
return CarbonConstrainedGrader.grade(episode_log).score
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def choose_action(agent: str, task: str, state, rng: random.Random) -> GridAction:
|
| 41 |
+
if agent == "heuristic":
|
| 42 |
+
return heuristic_agent(state, task)
|
| 43 |
+
|
| 44 |
+
renewable_ratio = rng.random()
|
| 45 |
+
fossil_ratio = rng.random() * (1.0 - renewable_ratio)
|
| 46 |
+
battery_action = rng.uniform(-1.0, 1.0)
|
| 47 |
+
return GridAction(
|
| 48 |
+
renewable_ratio=renewable_ratio,
|
| 49 |
+
fossil_ratio=fossil_ratio,
|
| 50 |
+
battery_action=battery_action,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def run_episode(task: str, agent: str, seed: int) -> float:
|
| 55 |
+
rng = random.Random(seed)
|
| 56 |
+
env = EcoGridEnv()
|
| 57 |
+
state = env.reset(task=task, seed=seed)
|
| 58 |
+
while not env.is_done:
|
| 59 |
+
action = choose_action(agent, task, state, rng)
|
| 60 |
+
result = env.step(action)
|
| 61 |
+
state = result.observation
|
| 62 |
+
return grade_episode(task, env.get_episode_log())
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def run_benchmarks(seeds: list[int]) -> dict:
|
| 66 |
+
out = {
|
| 67 |
+
"metadata": {"seeds": seeds, "agents": list(AGENTS), "tasks": list(TASKS)},
|
| 68 |
+
"results": {},
|
| 69 |
+
}
|
| 70 |
+
for task in TASKS:
|
| 71 |
+
out["results"][task] = {}
|
| 72 |
+
for agent in AGENTS:
|
| 73 |
+
scores = [run_episode(task, agent, seed) for seed in seeds]
|
| 74 |
+
avg = float(mean(scores))
|
| 75 |
+
ref = REFERENCE_MEAN[task][agent]
|
| 76 |
+
out["results"][task][agent] = {
|
| 77 |
+
"scores": [round(x, 6) for x in scores],
|
| 78 |
+
"mean": round(avg, 6),
|
| 79 |
+
"reference_mean": ref,
|
| 80 |
+
"delta_vs_reference": round(avg - ref, 6),
|
| 81 |
+
}
|
| 82 |
+
return out
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def main():
|
| 86 |
+
parser = argparse.ArgumentParser(description="Run EcoGrid reproducible benchmark suite.")
|
| 87 |
+
parser.add_argument("--seeds", default="1,2,3,4,5", help="Comma-separated integer seeds")
|
| 88 |
+
parser.add_argument(
|
| 89 |
+
"--out",
|
| 90 |
+
default="logs/benchmark_results.json",
|
| 91 |
+
help="Path to save benchmark results JSON",
|
| 92 |
+
)
|
| 93 |
+
args = parser.parse_args()
|
| 94 |
+
|
| 95 |
+
seeds = [int(x.strip()) for x in args.seeds.split(",") if x.strip()]
|
| 96 |
+
results = run_benchmarks(seeds)
|
| 97 |
+
|
| 98 |
+
out_path = Path(args.out)
|
| 99 |
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 100 |
+
out_path.write_text(json.dumps(results, indent=2), encoding="utf-8")
|
| 101 |
+
print(json.dumps(results, indent=2))
|
| 102 |
+
print(f"\nSaved benchmark report to {out_path}")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
main()
|
scripts/smoke_api.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deployment smoke checks for EcoGrid OpenEnv server."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import json
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
import requests
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def check(base_url: str):
|
| 13 |
+
t0 = time.perf_counter()
|
| 14 |
+
health = requests.get(f"{base_url}/health", timeout=5)
|
| 15 |
+
cold_start_ms = (time.perf_counter() - t0) * 1000.0
|
| 16 |
+
health.raise_for_status()
|
| 17 |
+
|
| 18 |
+
reset = requests.post(f"{base_url}/reset", json={"task": "easy", "seed": 42}, timeout=10)
|
| 19 |
+
reset.raise_for_status()
|
| 20 |
+
|
| 21 |
+
step = requests.post(
|
| 22 |
+
f"{base_url}/step",
|
| 23 |
+
json={"renewable_ratio": 0.6, "fossil_ratio": 0.35, "battery_action": 0.0},
|
| 24 |
+
timeout=10,
|
| 25 |
+
)
|
| 26 |
+
step.raise_for_status()
|
| 27 |
+
|
| 28 |
+
return {
|
| 29 |
+
"health_status": health.status_code,
|
| 30 |
+
"reset_status": reset.status_code,
|
| 31 |
+
"step_status": step.status_code,
|
| 32 |
+
"cold_start_ms": round(cold_start_ms, 2),
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def main():
|
| 37 |
+
parser = argparse.ArgumentParser(description="Run API smoke checks against EcoGrid server.")
|
| 38 |
+
parser.add_argument("--base-url", default="http://127.0.0.1:7860")
|
| 39 |
+
args = parser.parse_args()
|
| 40 |
+
result = check(args.base_url.rstrip("/"))
|
| 41 |
+
print(json.dumps(result, indent=2))
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
if __name__ == "__main__":
|
| 45 |
+
main()
|
server/app.py
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
try:
|
| 2 |
from openenv.core.env_server.http_server import create_app
|
| 3 |
except ImportError as e:
|
| 4 |
raise ImportError("openenv-core>=0.2.0 is required for the server.") from e
|
| 5 |
|
|
|
|
|
|
|
|
|
|
| 6 |
from models.schemas import GridAction
|
| 7 |
from server.ecogrid_environment import ServerEcoGridEnv, ServerObservation
|
| 8 |
|
|
@@ -14,17 +20,57 @@ app = create_app(
|
|
| 14 |
max_concurrent_envs=10,
|
| 15 |
)
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
import uvicorn
|
|
|
|
|
|
|
|
|
|
| 19 |
uvicorn.run(app, host=host, port=port)
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
parser.add_argument("--port", type=int, default=7860)
|
| 25 |
-
args = parser.parse_args()
|
| 26 |
-
|
| 27 |
-
# Satisfy naive validator check for 'main()' string
|
| 28 |
-
if False: main()
|
| 29 |
-
|
| 30 |
-
main(port=args.port)
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
try:
|
| 5 |
from openenv.core.env_server.http_server import create_app
|
| 6 |
except ImportError as e:
|
| 7 |
raise ImportError("openenv-core>=0.2.0 is required for the server.") from e
|
| 8 |
|
| 9 |
+
from fastapi import Request
|
| 10 |
+
from fastapi.responses import JSONResponse
|
| 11 |
+
|
| 12 |
from models.schemas import GridAction
|
| 13 |
from server.ecogrid_environment import ServerEcoGridEnv, ServerObservation
|
| 14 |
|
|
|
|
| 20 |
max_concurrent_envs=10,
|
| 21 |
)
|
| 22 |
|
| 23 |
+
|
| 24 |
+
@app.middleware("http")
|
| 25 |
+
async def normalize_step_payload(request: Request, call_next):
|
| 26 |
+
"""Allow /step payloads with either wrapped or direct action JSON."""
|
| 27 |
+
if request.method == "POST" and request.url.path == "/step":
|
| 28 |
+
body = await request.body()
|
| 29 |
+
if body:
|
| 30 |
+
try:
|
| 31 |
+
payload = json.loads(body)
|
| 32 |
+
except json.JSONDecodeError:
|
| 33 |
+
return JSONResponse(status_code=422, content={"detail": "Invalid JSON body"})
|
| 34 |
+
|
| 35 |
+
if isinstance(payload, dict) and "action" not in payload:
|
| 36 |
+
wrapped = json.dumps({"action": payload}).encode("utf-8")
|
| 37 |
+
request._body = wrapped
|
| 38 |
+
|
| 39 |
+
async def _receive():
|
| 40 |
+
return {"type": "http.request", "body": wrapped, "more_body": False}
|
| 41 |
+
|
| 42 |
+
request._receive = _receive
|
| 43 |
+
|
| 44 |
+
return await call_next(request)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@app.get("/", include_in_schema=False)
|
| 48 |
+
def root():
|
| 49 |
+
"""Landing route for judges/operators."""
|
| 50 |
+
return JSONResponse(
|
| 51 |
+
{
|
| 52 |
+
"name": "eco-grid-openenv",
|
| 53 |
+
"status": "ok",
|
| 54 |
+
"docs": "/docs",
|
| 55 |
+
"health": "/health",
|
| 56 |
+
"schema": "/schema",
|
| 57 |
+
"version": "/version",
|
| 58 |
+
}
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@app.get("/version", include_in_schema=False)
|
| 63 |
+
def version():
|
| 64 |
+
return {"version": "1.1.0-stabilized"}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def main():
|
| 68 |
import uvicorn
|
| 69 |
+
|
| 70 |
+
host = os.getenv("HOST", "0.0.0.0")
|
| 71 |
+
port = int(os.getenv("PORT", "7860"))
|
| 72 |
uvicorn.run(app, host=host, port=port)
|
| 73 |
|
| 74 |
+
|
| 75 |
+
if __name__ == "__main__":
|
| 76 |
+
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/ecogrid_environment.py
CHANGED
|
@@ -5,6 +5,7 @@ from pydantic import BaseModel, Field
|
|
| 5 |
from openenv.core.env_server.interfaces import Environment
|
| 6 |
from openenv.core.env_server.types import State
|
| 7 |
|
|
|
|
| 8 |
from env.environment import EcoGridEnv
|
| 9 |
from models.schemas import GridAction, GridState
|
| 10 |
|
|
@@ -37,14 +38,40 @@ class ServerEcoGridEnv(Environment):
|
|
| 37 |
info={}
|
| 38 |
)
|
| 39 |
|
| 40 |
-
def step(self, action: GridAction) -> ServerObservation:
|
| 41 |
self._oe_state.step_count += 1
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
return ServerObservation(
|
| 44 |
observation=result.observation,
|
| 45 |
reward=result.reward,
|
| 46 |
done=result.done,
|
| 47 |
-
info=
|
| 48 |
)
|
| 49 |
|
| 50 |
@property
|
|
|
|
| 5 |
from openenv.core.env_server.interfaces import Environment
|
| 6 |
from openenv.core.env_server.types import State
|
| 7 |
|
| 8 |
+
from env.action_utils import coerce_grid_action
|
| 9 |
from env.environment import EcoGridEnv
|
| 10 |
from models.schemas import GridAction, GridState
|
| 11 |
|
|
|
|
| 38 |
info={}
|
| 39 |
)
|
| 40 |
|
| 41 |
+
def step(self, action: GridAction | dict) -> ServerObservation:
|
| 42 |
self._oe_state.step_count += 1
|
| 43 |
+
safe_default = GridAction(
|
| 44 |
+
renewable_ratio=0.5,
|
| 45 |
+
fossil_ratio=0.5,
|
| 46 |
+
battery_action=0.0,
|
| 47 |
+
)
|
| 48 |
+
parsed_action, action_warning = coerce_grid_action(
|
| 49 |
+
action_like=action,
|
| 50 |
+
default_action=safe_default,
|
| 51 |
+
)
|
| 52 |
+
try:
|
| 53 |
+
result = self._env.step(parsed_action)
|
| 54 |
+
except Exception as exc:
|
| 55 |
+
# Never crash the API on malformed/edge payloads.
|
| 56 |
+
fallback_state = self._env.state() if not self._env.is_done else self._env.reset()
|
| 57 |
+
return ServerObservation(
|
| 58 |
+
observation=fallback_state,
|
| 59 |
+
reward=0.001,
|
| 60 |
+
done=self._env.is_done,
|
| 61 |
+
info={
|
| 62 |
+
"error": f"step_failed:{type(exc).__name__}",
|
| 63 |
+
"detail": str(exc),
|
| 64 |
+
"action_warning": action_warning or "step_exception_fallback",
|
| 65 |
+
},
|
| 66 |
+
)
|
| 67 |
+
info = dict(result.info)
|
| 68 |
+
if action_warning:
|
| 69 |
+
info["action_warning"] = action_warning
|
| 70 |
return ServerObservation(
|
| 71 |
observation=result.observation,
|
| 72 |
reward=result.reward,
|
| 73 |
done=result.done,
|
| 74 |
+
info=info,
|
| 75 |
)
|
| 76 |
|
| 77 |
@property
|
tests/test_action_utils.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from env.action_utils import coerce_grid_action, normalize_action_components, safe_grid_action
|
| 2 |
+
from models.schemas import GridAction
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def test_normalize_action_components_rounding_never_exceeds_one():
|
| 6 |
+
# Regression case from baseline heuristic:
|
| 7 |
+
# 0.396 + 0.605 -> 1.001 after rounding.
|
| 8 |
+
renewable, fossil, battery = normalize_action_components(
|
| 9 |
+
renewable_ratio=0.396,
|
| 10 |
+
fossil_ratio=0.605,
|
| 11 |
+
battery_action=0.0,
|
| 12 |
+
)
|
| 13 |
+
assert renewable + fossil <= 1.0
|
| 14 |
+
assert -1.0 <= battery <= 1.0
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def test_safe_grid_action_clamps_and_normalizes():
|
| 18 |
+
action = safe_grid_action(
|
| 19 |
+
renewable_ratio=1.7,
|
| 20 |
+
fossil_ratio=0.8,
|
| 21 |
+
battery_action=-1.7,
|
| 22 |
+
)
|
| 23 |
+
assert isinstance(action, GridAction)
|
| 24 |
+
assert 0.0 <= action.renewable_ratio <= 1.0
|
| 25 |
+
assert 0.0 <= action.fossil_ratio <= 1.0
|
| 26 |
+
assert action.renewable_ratio + action.fossil_ratio <= 1.0
|
| 27 |
+
assert -1.0 <= action.battery_action <= 1.0
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_coerce_grid_action_invalid_payload_falls_back():
|
| 31 |
+
default = GridAction(renewable_ratio=0.5, fossil_ratio=0.5, battery_action=0.0)
|
| 32 |
+
action, warning = coerce_grid_action({"renewable_ratio": "invalid"}, default_action=default)
|
| 33 |
+
assert action == default
|
| 34 |
+
assert warning is not None
|
tests/test_environment.py
CHANGED
|
@@ -82,3 +82,35 @@ def test_carbon_overrun_termination():
|
|
| 82 |
break
|
| 83 |
|
| 84 |
assert done is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
break
|
| 83 |
|
| 84 |
assert done is True
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def test_step_accepts_dict_action_and_adds_warning_for_invalid_payload():
|
| 88 |
+
env = EcoGridEnv()
|
| 89 |
+
env.reset(task="medium", seed=42)
|
| 90 |
+
|
| 91 |
+
# Invalid dict payload should be coerced to safe fallback action
|
| 92 |
+
result = env.step({"renewable_ratio": "bad_value"})
|
| 93 |
+
|
| 94 |
+
assert result.done is False
|
| 95 |
+
assert "action_warning" in result.info
|
| 96 |
+
assert result.observation.time_step == 1
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def test_full_episode_reproducibility_same_seed_same_trajectory():
|
| 100 |
+
env1 = EcoGridEnv()
|
| 101 |
+
env2 = EcoGridEnv()
|
| 102 |
+
env1.reset(task="hard", seed=123)
|
| 103 |
+
env2.reset(task="hard", seed=123)
|
| 104 |
+
|
| 105 |
+
action = GridAction(renewable_ratio=0.6, fossil_ratio=0.3, battery_action=0.0)
|
| 106 |
+
rewards_1 = []
|
| 107 |
+
rewards_2 = []
|
| 108 |
+
|
| 109 |
+
for _ in range(10):
|
| 110 |
+
r1 = env1.step(action)
|
| 111 |
+
r2 = env2.step(action)
|
| 112 |
+
rewards_1.append(r1.reward)
|
| 113 |
+
rewards_2.append(r2.reward)
|
| 114 |
+
assert r1.observation.model_dump() == r2.observation.model_dump()
|
| 115 |
+
|
| 116 |
+
assert rewards_1 == rewards_2
|
tests/test_server_api.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi.testclient import TestClient
|
| 2 |
+
|
| 3 |
+
from server.app import app
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_health_endpoint():
|
| 7 |
+
client = TestClient(app)
|
| 8 |
+
response = client.get("/health")
|
| 9 |
+
assert response.status_code == 200
|
| 10 |
+
body = response.json()
|
| 11 |
+
assert body.get("status") in {"ok", "healthy"}
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_reset_and_step_smoke_wrapped_payload():
|
| 15 |
+
client = TestClient(app)
|
| 16 |
+
reset_resp = client.post("/reset", json={"task": "easy", "seed": 42})
|
| 17 |
+
assert reset_resp.status_code == 200
|
| 18 |
+
reset_body = reset_resp.json()
|
| 19 |
+
assert "observation" in reset_body
|
| 20 |
+
|
| 21 |
+
step_resp = client.post(
|
| 22 |
+
"/step",
|
| 23 |
+
json={"action": {"renewable_ratio": 0.6, "fossil_ratio": 0.3, "battery_action": 0.0}},
|
| 24 |
+
)
|
| 25 |
+
assert step_resp.status_code == 200
|
| 26 |
+
step_body = step_resp.json()
|
| 27 |
+
assert "observation" in step_body
|
| 28 |
+
assert "reward" in step_body
|
| 29 |
+
assert "done" in step_body
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_step_accepts_unwrapped_action_payload():
|
| 33 |
+
client = TestClient(app)
|
| 34 |
+
client.post("/reset", json={"task": "medium", "seed": 7})
|
| 35 |
+
step_resp = client.post(
|
| 36 |
+
"/step",
|
| 37 |
+
json={"renewable_ratio": 0.55, "fossil_ratio": 0.35, "battery_action": 0.0},
|
| 38 |
+
)
|
| 39 |
+
assert step_resp.status_code == 200
|
| 40 |
+
assert "observation" in step_resp.json()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_step_invalid_payload_returns_422_not_500():
|
| 44 |
+
client = TestClient(app)
|
| 45 |
+
client.post("/reset", json={"task": "easy", "seed": 11})
|
| 46 |
+
bad_resp = client.post(
|
| 47 |
+
"/step",
|
| 48 |
+
json={"renewable_ratio": 1.5, "fossil_ratio": 1.5, "battery_action": 2.0},
|
| 49 |
+
)
|
| 50 |
+
assert bad_resp.status_code == 422
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_step_stress_multiple_interactions():
|
| 54 |
+
client = TestClient(app)
|
| 55 |
+
client.post("/reset", json={"task": "hard", "seed": 21})
|
| 56 |
+
for _ in range(20):
|
| 57 |
+
resp = client.post(
|
| 58 |
+
"/step",
|
| 59 |
+
json={"renewable_ratio": 0.6, "fossil_ratio": 0.3, "battery_action": 0.0},
|
| 60 |
+
)
|
| 61 |
+
assert resp.status_code == 200
|
train_unsloth.py
CHANGED
|
@@ -11,6 +11,8 @@ import os
|
|
| 11 |
import random
|
| 12 |
from typing import List, Dict
|
| 13 |
|
|
|
|
|
|
|
| 14 |
try:
|
| 15 |
import torch
|
| 16 |
from datasets import Dataset
|
|
@@ -87,15 +89,28 @@ Output ONLY a valid JSON object:
|
|
| 87 |
]
|
| 88 |
|
| 89 |
|
| 90 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
"""Generate a dataset of random grid states for training."""
|
| 92 |
print(f"Generating {num_samples} training states for task '{task}'...")
|
| 93 |
env = EcoGridEnv()
|
|
|
|
| 94 |
|
| 95 |
prompts = []
|
| 96 |
# We just run the environment randomly to generate a variety of states
|
| 97 |
# Note: We don't need target actions because GRPO learns through trial and error!
|
| 98 |
-
state = env.reset(task=task, seed=
|
| 99 |
|
| 100 |
for _ in range(num_samples):
|
| 101 |
state_dict = state.model_dump()
|
|
@@ -103,9 +118,9 @@ def generate_training_data(num_samples: int, task: str) -> Dataset:
|
|
| 103 |
|
| 104 |
# Take a random valid action to advance the environment
|
| 105 |
action = GridAction(
|
| 106 |
-
renewable_ratio=
|
| 107 |
-
fossil_ratio=
|
| 108 |
-
battery_action=
|
| 109 |
)
|
| 110 |
|
| 111 |
try:
|
|
@@ -113,7 +128,7 @@ def generate_training_data(num_samples: int, task: str) -> Dataset:
|
|
| 113 |
state = result.observation
|
| 114 |
except Exception:
|
| 115 |
# If done or errored, reset
|
| 116 |
-
state = env.reset(task=task, seed=
|
| 117 |
|
| 118 |
return Dataset.from_dict({"prompt": prompts})
|
| 119 |
|
|
@@ -133,6 +148,7 @@ def main():
|
|
| 133 |
return
|
| 134 |
|
| 135 |
print(f"Initializing Unsloth GRPO training on {args.model}")
|
|
|
|
| 136 |
|
| 137 |
# 1. Load Model
|
| 138 |
model, tokenizer = FastLanguageModel.from_pretrained(
|
|
@@ -207,7 +223,7 @@ def main():
|
|
| 207 |
return rewards
|
| 208 |
|
| 209 |
# 3. Prepare Dataset
|
| 210 |
-
dataset = generate_training_data(args.samples, args.task)
|
| 211 |
|
| 212 |
# 4. Configure Trainer
|
| 213 |
training_args = GRPOConfig(
|
|
@@ -255,8 +271,25 @@ def main():
|
|
| 255 |
os.makedirs("./logs", exist_ok=True)
|
| 256 |
with open("./logs/reward_curve.json", "w") as f:
|
| 257 |
json.dump(reward_curve, f, indent=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
print("Saved reward curve to ./logs/reward_curve.json")
|
|
|
|
| 260 |
|
| 261 |
if __name__ == "__main__":
|
| 262 |
main()
|
|
|
|
| 11 |
import random
|
| 12 |
from typing import List, Dict
|
| 13 |
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
try:
|
| 17 |
import torch
|
| 18 |
from datasets import Dataset
|
|
|
|
| 89 |
]
|
| 90 |
|
| 91 |
|
| 92 |
+
def set_global_seed(seed: int) -> None:
|
| 93 |
+
"""Set all available RNG seeds for reproducible training."""
|
| 94 |
+
random.seed(seed)
|
| 95 |
+
np.random.seed(seed)
|
| 96 |
+
os.environ["PYTHONHASHSEED"] = str(seed)
|
| 97 |
+
if HAS_UNSLOTH:
|
| 98 |
+
torch.manual_seed(seed)
|
| 99 |
+
if torch.cuda.is_available():
|
| 100 |
+
torch.cuda.manual_seed_all(seed)
|
| 101 |
+
torch.use_deterministic_algorithms(True, warn_only=True)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def generate_training_data(num_samples: int, task: str, seed: int) -> Dataset:
|
| 105 |
"""Generate a dataset of random grid states for training."""
|
| 106 |
print(f"Generating {num_samples} training states for task '{task}'...")
|
| 107 |
env = EcoGridEnv()
|
| 108 |
+
rng = random.Random(seed)
|
| 109 |
|
| 110 |
prompts = []
|
| 111 |
# We just run the environment randomly to generate a variety of states
|
| 112 |
# Note: We don't need target actions because GRPO learns through trial and error!
|
| 113 |
+
state = env.reset(task=task, seed=seed)
|
| 114 |
|
| 115 |
for _ in range(num_samples):
|
| 116 |
state_dict = state.model_dump()
|
|
|
|
| 118 |
|
| 119 |
# Take a random valid action to advance the environment
|
| 120 |
action = GridAction(
|
| 121 |
+
renewable_ratio=rng.uniform(0, 0.8),
|
| 122 |
+
fossil_ratio=rng.uniform(0, 0.2),
|
| 123 |
+
battery_action=rng.uniform(-1, 1),
|
| 124 |
)
|
| 125 |
|
| 126 |
try:
|
|
|
|
| 128 |
state = result.observation
|
| 129 |
except Exception:
|
| 130 |
# If done or errored, reset
|
| 131 |
+
state = env.reset(task=task, seed=rng.randint(0, 10000))
|
| 132 |
|
| 133 |
return Dataset.from_dict({"prompt": prompts})
|
| 134 |
|
|
|
|
| 148 |
return
|
| 149 |
|
| 150 |
print(f"Initializing Unsloth GRPO training on {args.model}")
|
| 151 |
+
set_global_seed(args.seed)
|
| 152 |
|
| 153 |
# 1. Load Model
|
| 154 |
model, tokenizer = FastLanguageModel.from_pretrained(
|
|
|
|
| 223 |
return rewards
|
| 224 |
|
| 225 |
# 3. Prepare Dataset
|
| 226 |
+
dataset = generate_training_data(args.samples, args.task, args.seed)
|
| 227 |
|
| 228 |
# 4. Configure Trainer
|
| 229 |
training_args = GRPOConfig(
|
|
|
|
| 271 |
os.makedirs("./logs", exist_ok=True)
|
| 272 |
with open("./logs/reward_curve.json", "w") as f:
|
| 273 |
json.dump(reward_curve, f, indent=2)
|
| 274 |
+
|
| 275 |
+
with open("./logs/training_metrics.json", "w") as f:
|
| 276 |
+
json.dump(
|
| 277 |
+
{
|
| 278 |
+
"task": args.task,
|
| 279 |
+
"seed": args.seed,
|
| 280 |
+
"epochs": args.epochs,
|
| 281 |
+
"samples": args.samples,
|
| 282 |
+
"model": args.model,
|
| 283 |
+
"reward_curve": reward_curve,
|
| 284 |
+
"log_history": log_history,
|
| 285 |
+
},
|
| 286 |
+
f,
|
| 287 |
+
indent=2,
|
| 288 |
+
default=str,
|
| 289 |
+
)
|
| 290 |
|
| 291 |
print("Saved reward curve to ./logs/reward_curve.json")
|
| 292 |
+
print("Saved training metrics to ./logs/training_metrics.json")
|
| 293 |
|
| 294 |
if __name__ == "__main__":
|
| 295 |
main()
|
uv.lock
CHANGED
|
@@ -812,6 +812,42 @@ wheels = [
|
|
| 812 |
{ url = "https://files.pythonhosted.org/packages/7c/37/197db187c260d24d4be1f09d427f59f3fb9a89bcf1354e23865c7bff7607/cyclopts-4.11.0-py3-none-any.whl", hash = "sha256:34318e3823b44b5baa754a5e37ec70a5c17dc81c65e4295ed70e17bc1aeae50d", size = 208494, upload-time = "2026-04-23T00:23:34.948Z" },
|
| 813 |
]
|
| 814 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 815 |
[[package]]
|
| 816 |
name = "distro"
|
| 817 |
version = "1.9.0"
|
|
@@ -853,36 +889,44 @@ name = "eco-grid-openenv"
|
|
| 853 |
version = "1.0.0"
|
| 854 |
source = { editable = "." }
|
| 855 |
dependencies = [
|
| 856 |
-
{ name = "accelerate" },
|
| 857 |
{ name = "litellm" },
|
| 858 |
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
| 859 |
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
| 860 |
{ name = "openai" },
|
| 861 |
{ name = "openenv-core" },
|
| 862 |
-
{ name = "pandas" },
|
| 863 |
-
{ name = "
|
| 864 |
{ name = "plotly" },
|
| 865 |
{ name = "pydantic" },
|
| 866 |
{ name = "rich" },
|
| 867 |
{ name = "streamlit" },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 868 |
{ name = "transformers" },
|
|
|
|
| 869 |
]
|
| 870 |
|
| 871 |
[package.metadata]
|
| 872 |
requires-dist = [
|
| 873 |
-
{ name = "accelerate", specifier = ">=0.30.0" },
|
| 874 |
{ name = "litellm", specifier = ">=1.0.0" },
|
| 875 |
{ name = "numpy", specifier = ">=1.24.0" },
|
| 876 |
{ name = "openai", specifier = ">=1.10.0" },
|
| 877 |
{ name = "openenv-core", specifier = ">=0.2.3" },
|
| 878 |
{ name = "pandas", specifier = ">=2.0.0" },
|
| 879 |
-
{ name = "peft", specifier = ">=0.11.0" },
|
| 880 |
{ name = "plotly", specifier = ">=5.18.0" },
|
| 881 |
{ name = "pydantic", specifier = ">=2.0.0" },
|
| 882 |
{ name = "rich", specifier = ">=13.0.0" },
|
| 883 |
{ name = "streamlit", specifier = ">=1.30.0" },
|
| 884 |
-
{ name = "transformers", specifier = ">=4.40.0" },
|
|
|
|
| 885 |
]
|
|
|
|
| 886 |
|
| 887 |
[[package]]
|
| 888 |
name = "email-validator"
|
|
@@ -1153,11 +1197,16 @@ wheels = [
|
|
| 1153 |
|
| 1154 |
[[package]]
|
| 1155 |
name = "fsspec"
|
| 1156 |
-
version = "2026.
|
| 1157 |
source = { registry = "https://pypi.org/simple" }
|
| 1158 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 1159 |
wheels = [
|
| 1160 |
-
{ url = "https://files.pythonhosted.org/packages/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1161 |
]
|
| 1162 |
|
| 1163 |
[[package]]
|
|
@@ -1941,6 +1990,29 @@ wheels = [
|
|
| 1941 |
{ 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" },
|
| 1942 |
]
|
| 1943 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1944 |
[[package]]
|
| 1945 |
name = "narwhals"
|
| 1946 |
version = "2.20.0"
|
|
@@ -4100,6 +4172,22 @@ wheels = [
|
|
| 4100 |
{ url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" },
|
| 4101 |
]
|
| 4102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4103 |
[[package]]
|
| 4104 |
name = "typer"
|
| 4105 |
version = "0.23.1"
|
|
@@ -4366,6 +4454,163 @@ wheels = [
|
|
| 4366 |
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
|
| 4367 |
]
|
| 4368 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4369 |
[[package]]
|
| 4370 |
name = "yarl"
|
| 4371 |
version = "1.23.0"
|
|
|
|
| 812 |
{ url = "https://files.pythonhosted.org/packages/7c/37/197db187c260d24d4be1f09d427f59f3fb9a89bcf1354e23865c7bff7607/cyclopts-4.11.0-py3-none-any.whl", hash = "sha256:34318e3823b44b5baa754a5e37ec70a5c17dc81c65e4295ed70e17bc1aeae50d", size = 208494, upload-time = "2026-04-23T00:23:34.948Z" },
|
| 813 |
]
|
| 814 |
|
| 815 |
+
[[package]]
|
| 816 |
+
name = "datasets"
|
| 817 |
+
version = "4.8.4"
|
| 818 |
+
source = { registry = "https://pypi.org/simple" }
|
| 819 |
+
dependencies = [
|
| 820 |
+
{ name = "dill" },
|
| 821 |
+
{ name = "filelock" },
|
| 822 |
+
{ name = "fsspec", extra = ["http"] },
|
| 823 |
+
{ name = "httpx" },
|
| 824 |
+
{ name = "huggingface-hub" },
|
| 825 |
+
{ name = "multiprocess" },
|
| 826 |
+
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
| 827 |
+
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
| 828 |
+
{ name = "packaging" },
|
| 829 |
+
{ name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
| 830 |
+
{ name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
| 831 |
+
{ name = "pyarrow" },
|
| 832 |
+
{ name = "pyyaml" },
|
| 833 |
+
{ name = "requests" },
|
| 834 |
+
{ name = "tqdm" },
|
| 835 |
+
{ name = "xxhash" },
|
| 836 |
+
]
|
| 837 |
+
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" }
|
| 838 |
+
wheels = [
|
| 839 |
+
{ 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" },
|
| 840 |
+
]
|
| 841 |
+
|
| 842 |
+
[[package]]
|
| 843 |
+
name = "dill"
|
| 844 |
+
version = "0.4.1"
|
| 845 |
+
source = { registry = "https://pypi.org/simple" }
|
| 846 |
+
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" }
|
| 847 |
+
wheels = [
|
| 848 |
+
{ 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" },
|
| 849 |
+
]
|
| 850 |
+
|
| 851 |
[[package]]
|
| 852 |
name = "distro"
|
| 853 |
version = "1.9.0"
|
|
|
|
| 889 |
version = "1.0.0"
|
| 890 |
source = { editable = "." }
|
| 891 |
dependencies = [
|
|
|
|
| 892 |
{ name = "litellm" },
|
| 893 |
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
| 894 |
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
| 895 |
{ name = "openai" },
|
| 896 |
{ name = "openenv-core" },
|
| 897 |
+
{ name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
| 898 |
+
{ name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
| 899 |
{ name = "plotly" },
|
| 900 |
{ name = "pydantic" },
|
| 901 |
{ name = "rich" },
|
| 902 |
{ name = "streamlit" },
|
| 903 |
+
]
|
| 904 |
+
|
| 905 |
+
[package.optional-dependencies]
|
| 906 |
+
train = [
|
| 907 |
+
{ name = "accelerate" },
|
| 908 |
+
{ name = "peft" },
|
| 909 |
{ name = "transformers" },
|
| 910 |
+
{ name = "trl" },
|
| 911 |
]
|
| 912 |
|
| 913 |
[package.metadata]
|
| 914 |
requires-dist = [
|
| 915 |
+
{ name = "accelerate", marker = "extra == 'train'", specifier = ">=0.30.0" },
|
| 916 |
{ name = "litellm", specifier = ">=1.0.0" },
|
| 917 |
{ name = "numpy", specifier = ">=1.24.0" },
|
| 918 |
{ name = "openai", specifier = ">=1.10.0" },
|
| 919 |
{ name = "openenv-core", specifier = ">=0.2.3" },
|
| 920 |
{ name = "pandas", specifier = ">=2.0.0" },
|
| 921 |
+
{ name = "peft", marker = "extra == 'train'", specifier = ">=0.11.0" },
|
| 922 |
{ name = "plotly", specifier = ">=5.18.0" },
|
| 923 |
{ name = "pydantic", specifier = ">=2.0.0" },
|
| 924 |
{ name = "rich", specifier = ">=13.0.0" },
|
| 925 |
{ name = "streamlit", specifier = ">=1.30.0" },
|
| 926 |
+
{ name = "transformers", marker = "extra == 'train'", specifier = ">=4.40.0" },
|
| 927 |
+
{ name = "trl", marker = "extra == 'train'", specifier = ">=0.24.0" },
|
| 928 |
]
|
| 929 |
+
provides-extras = ["train"]
|
| 930 |
|
| 931 |
[[package]]
|
| 932 |
name = "email-validator"
|
|
|
|
| 1197 |
|
| 1198 |
[[package]]
|
| 1199 |
name = "fsspec"
|
| 1200 |
+
version = "2026.2.0"
|
| 1201 |
source = { registry = "https://pypi.org/simple" }
|
| 1202 |
+
sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" }
|
| 1203 |
wheels = [
|
| 1204 |
+
{ 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" },
|
| 1205 |
+
]
|
| 1206 |
+
|
| 1207 |
+
[package.optional-dependencies]
|
| 1208 |
+
http = [
|
| 1209 |
+
{ name = "aiohttp" },
|
| 1210 |
]
|
| 1211 |
|
| 1212 |
[[package]]
|
|
|
|
| 1990 |
{ 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" },
|
| 1991 |
]
|
| 1992 |
|
| 1993 |
+
[[package]]
|
| 1994 |
+
name = "multiprocess"
|
| 1995 |
+
version = "0.70.19"
|
| 1996 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1997 |
+
dependencies = [
|
| 1998 |
+
{ name = "dill" },
|
| 1999 |
+
]
|
| 2000 |
+
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" }
|
| 2001 |
+
wheels = [
|
| 2002 |
+
{ url = "https://files.pythonhosted.org/packages/8b/b6/10832f96b499690854e574360be342a282f5f7dba58eff791299ff6c0637/multiprocess-0.70.19-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:02e5c35d7d6cd2bdc89c1858867f7bde4012837411023a4696c148c1bdd7c80e", size = 135131, upload-time = "2026-01-19T06:47:20.479Z" },
|
| 2003 |
+
{ url = "https://files.pythonhosted.org/packages/99/50/faef2d8106534b0dc4a0b772668a1a99682696ebf17d3c0f13f2ed6a656a/multiprocess-0.70.19-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:79576c02d1207ec405b00cabf2c643c36070800cca433860e14539df7818b2aa", size = 135131, upload-time = "2026-01-19T06:47:21.879Z" },
|
| 2004 |
+
{ url = "https://files.pythonhosted.org/packages/94/b1/0b71d18b76bf423c2e8ee00b31db37d17297ab3b4db44e188692afdca628/multiprocess-0.70.19-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6b6d78d43a03b68014ca1f0b7937d965393a670c5de7c29026beb2258f2f896", size = 135134, upload-time = "2026-01-19T06:47:23.262Z" },
|
| 2005 |
+
{ 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" },
|
| 2006 |
+
{ 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" },
|
| 2007 |
+
{ 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" },
|
| 2008 |
+
{ 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" },
|
| 2009 |
+
{ 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" },
|
| 2010 |
+
{ 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" },
|
| 2011 |
+
{ 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" },
|
| 2012 |
+
{ 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" },
|
| 2013 |
+
{ 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" },
|
| 2014 |
+
]
|
| 2015 |
+
|
| 2016 |
[[package]]
|
| 2017 |
name = "narwhals"
|
| 2018 |
version = "2.20.0"
|
|
|
|
| 4172 |
{ url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" },
|
| 4173 |
]
|
| 4174 |
|
| 4175 |
+
[[package]]
|
| 4176 |
+
name = "trl"
|
| 4177 |
+
version = "1.2.0"
|
| 4178 |
+
source = { registry = "https://pypi.org/simple" }
|
| 4179 |
+
dependencies = [
|
| 4180 |
+
{ name = "accelerate" },
|
| 4181 |
+
{ name = "datasets" },
|
| 4182 |
+
{ name = "jinja2" },
|
| 4183 |
+
{ name = "packaging" },
|
| 4184 |
+
{ name = "transformers" },
|
| 4185 |
+
]
|
| 4186 |
+
sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/2d3d876917d43537afea7b502abb318ae071295e4accac222741b548399e/trl-1.2.0.tar.gz", hash = "sha256:f5038b21295d2559992a087ea8d9ca10f74cde23e6760861def209811ab45d00", size = 583112, upload-time = "2026-04-17T01:04:17.706Z" }
|
| 4187 |
+
wheels = [
|
| 4188 |
+
{ url = "https://files.pythonhosted.org/packages/02/82/bad1a22ff4b21d8080f7a64d8313c1ac0e791b455b98f2efca1ef3e14b8f/trl-1.2.0-py3-none-any.whl", hash = "sha256:f6ddfa162ac92d25973070d9e3f6cff71b32c52edc34539e4294722f9dc0a6d6", size = 697449, upload-time = "2026-04-17T01:04:16.007Z" },
|
| 4189 |
+
]
|
| 4190 |
+
|
| 4191 |
[[package]]
|
| 4192 |
name = "typer"
|
| 4193 |
version = "0.23.1"
|
|
|
|
| 4454 |
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
|
| 4455 |
]
|
| 4456 |
|
| 4457 |
+
[[package]]
|
| 4458 |
+
name = "xxhash"
|
| 4459 |
+
version = "3.7.0"
|
| 4460 |
+
source = { registry = "https://pypi.org/simple" }
|
| 4461 |
+
sdist = { url = "https://files.pythonhosted.org/packages/24/2f/e183a1b407002f5af81822bee18b61cdb94b8670208ef34734d8d2b8ebe9/xxhash-3.7.0.tar.gz", hash = "sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae", size = 82022, upload-time = "2026-04-25T11:10:32.553Z" }
|
| 4462 |
+
wheels = [
|
| 4463 |
+
{ url = "https://files.pythonhosted.org/packages/92/49/e4b575b4ed170a7f640c8bd69cfadfa81c7b700191fde5e72228762b9f73/xxhash-3.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd8ab85c916a58d5c8656ea15e3ce9df836fe2f120a74c296e01d69fab2614b4", size = 33426, upload-time = "2026-04-25T11:05:15.702Z" },
|
| 4464 |
+
{ url = "https://files.pythonhosted.org/packages/07/61/40f0155b0b09988eb6cdbfc52652f2f371810b0c58163208cb05667757bd/xxhash-3.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:85f5c0e26d945b5bb475e0a3d95193117498130baa7619357bdc7869c2391b5a", size = 30859, upload-time = "2026-04-25T11:05:17.708Z" },
|
| 4465 |
+
{ url = "https://files.pythonhosted.org/packages/12/bd/2902b7aad574e43cd85fd84849cfbce48c52cb02c7d6902b8a2b3f6e668e/xxhash-3.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7ffeaada9f8699be63d639536b0b60dff73b7d3325b7475c5bc8fdbf4eed47f", size = 193839, upload-time = "2026-04-25T11:05:19.364Z" },
|
| 4466 |
+
{ url = "https://files.pythonhosted.org/packages/48/df/343ce8fd09e47ba8fba43b3bad3283ddf0deca799d5a27b084c3aa2ce502/xxhash-3.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee88dfaa6b1b2bfadd3c031fa5f05584870e62fb05dc500942e9900c44fcfda", size = 212896, upload-time = "2026-04-25T11:05:21.131Z" },
|
| 4467 |
+
{ url = "https://files.pythonhosted.org/packages/79/cf/703e8422a8b52407864281fb4eb52c605e9f33180413b4458f05de110eba/xxhash-3.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7426ff0dfa76eb47efc2cc59d4a717bfa9dc9938bff5e49e748bca749f6aa616", size = 235896, upload-time = "2026-04-25T11:05:22.988Z" },
|
| 4468 |
+
{ url = "https://files.pythonhosted.org/packages/ed/bc/d4b039edbd426575add5f217abeeb2bf870e2c510d35445df81b4f457901/xxhash-3.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8ff6ec73110f610425caef3ea875afbfc34caa542f01df3a80f45aadeb9f906", size = 211665, upload-time = "2026-04-25T11:05:24.799Z" },
|
| 4469 |
+
{ url = "https://files.pythonhosted.org/packages/42/24/c6f81361796814b92399a88bf079d3b65e617f531819128fcf1bd6ef0571/xxhash-3.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d23fd49fdc5c8af61fb7104f1ad247954499140f6cb6045b3aa5c99dadbbf28", size = 444929, upload-time = "2026-04-25T11:05:26.245Z" },
|
| 4470 |
+
{ url = "https://files.pythonhosted.org/packages/a4/db/268012153eb7f6bf2c8a0491fdcde11e093f166990821a2ab754fe95537d/xxhash-3.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c249621af6d50a05d9f10af894b404157b15819878e18f75fcbb0213a77d07", size = 193271, upload-time = "2026-04-25T11:05:28.282Z" },
|
| 4471 |
+
{ url = "https://files.pythonhosted.org/packages/0a/86/1d0d905d659850dad7f59c807c130249fdb204dc6f71f1fb36268f3f3e61/xxhash-3.7.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6741564a923f082f3c2941c8bb920462ed5b25eaebdd1e161f162233c9a10bc5", size = 284580, upload-time = "2026-04-25T11:05:30.116Z" },
|
| 4472 |
+
{ url = "https://files.pythonhosted.org/packages/1f/52/fc01ca7ff425a9bdb38d9e3a17f2630447ce3b45d45a929a6cd94d469334/xxhash-3.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4fd8acc6e32596350619896feb372033c0920975992d29837c32853bb1feacd", size = 210193, upload-time = "2026-04-25T11:05:31.969Z" },
|
| 4473 |
+
{ url = "https://files.pythonhosted.org/packages/ec/96/122e0c6a3537a54b30752031dca557182576bae1a4171c0be8c532c84496/xxhash-3.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:646a69b56d8145d85f7fd2289d14fba07880c8a5bda406aa256b407481a61f35", size = 241094, upload-time = "2026-04-25T11:05:33.651Z" },
|
| 4474 |
+
{ url = "https://files.pythonhosted.org/packages/d8/17/92e33338db8c18add33a46b56c2b7d5dcc6cc2ac076c45389f6017b1bf37/xxhash-3.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:11dd69b1a34b7b9af29012f390825b0cdb0617c0966560e227ca74daa7478ba9", size = 197721, upload-time = "2026-04-25T11:05:35.387Z" },
|
| 4475 |
+
{ url = "https://files.pythonhosted.org/packages/c7/04/fd4114a0820913f336bef5c82ef851bde8d06270982ebd7b2a859961bbf2/xxhash-3.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:01cf5c5333aed26cc8d5eea33b8d6398e085e365a704b7372fabdf7ab06441a9", size = 210073, upload-time = "2026-04-25T11:05:37.405Z" },
|
| 4476 |
+
{ url = "https://files.pythonhosted.org/packages/dd/eb/a2472b8b81cd576a9af3a4889ad8ba5784e8c5a04592587056cdaededd6c/xxhash-3.7.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f1e65d52c2d526734abecb98372c256b7eacce8fdc42e0df8570417fb39e2772", size = 274960, upload-time = "2026-04-25T11:05:39.224Z" },
|
| 4477 |
+
{ url = "https://files.pythonhosted.org/packages/3d/d3/493afc544aae50b5fb2844ceaeb3697283bb59695db1a7cb40448636de05/xxhash-3.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8ff00fcc3eb436617ed8556cf15daf76c2b501248361a065625a588af78a0a02", size = 413113, upload-time = "2026-04-25T11:05:40.669Z" },
|
| 4478 |
+
{ url = "https://files.pythonhosted.org/packages/50/6a/002800845a22bff32bcf5fd09caceb4d3f5c3da6b754c46edb9743ce908b/xxhash-3.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b5cd29840505631c6f7dbb8a5d34b742b5e6bbda38fe0b9f54e825f3ea6b61dc", size = 190677, upload-time = "2026-04-25T11:05:42.403Z" },
|
| 4479 |
+
{ url = "https://files.pythonhosted.org/packages/f4/0f/86ee514622a381c0dc49167c8d431a22aa93518a4063559c3e36e4b82bc8/xxhash-3.7.0-cp310-cp310-win32.whl", hash = "sha256:5bf2f1940499839b39fef1561b5ecb6ede9ac34ef4457474e1337fc7ef07c2f3", size = 30627, upload-time = "2026-04-25T11:05:44.022Z" },
|
| 4480 |
+
{ url = "https://files.pythonhosted.org/packages/86/45/2ef2310803efb4a2d07844e8098d797e25702024793aa2e85858623a43b5/xxhash-3.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:d41fcda2fa8ca682ebca134a2f2dc02575ba549267585597e73061565795f475", size = 31463, upload-time = "2026-04-25T11:05:45.218Z" },
|
| 4481 |
+
{ url = "https://files.pythonhosted.org/packages/9e/75/40dbf8f142baf8993c38cd988c8d8f51fe0c51e6c84c5769a3c0280a651d/xxhash-3.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:a845a59664d5c531525a467470220f8edc37959e0a6f8e734ffb6654da5c4bee", size = 27747, upload-time = "2026-04-25T11:05:46.422Z" },
|
| 4482 |
+
{ url = "https://files.pythonhosted.org/packages/3b/f4/7bd35089ff1f8e2c96baa2dce05775a122aacd2e3830a73165e27a4d0848/xxhash-3.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fdc7d06929ae28dda98297a18eef7b0fd38991a3b405d8d7b55c9ef24c296958", size = 33423, upload-time = "2026-04-25T11:05:47.628Z" },
|
| 4483 |
+
{ url = "https://files.pythonhosted.org/packages/a3/26/4e00c88a6a2c8a759cfb77d2a9a405f901e8aa66e60ef1fd0aeb35edda48/xxhash-3.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea6daa712f4e094a30830cf01e9b47d03b24d05cc9dab8609f0d9a9db8454712", size = 30857, upload-time = "2026-04-25T11:05:49.189Z" },
|
| 4484 |
+
{ url = "https://files.pythonhosted.org/packages/82/2f/eeb942c17a5a761a8f01cb9180a0b76bfb62a2c39e6f46b1f9001899027a/xxhash-3.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9e6c0d843f1daf85ea23aeb053579135552bde575b7b98af20bfc667b6e4548d", size = 194702, upload-time = "2026-04-25T11:05:50.457Z" },
|
| 4485 |
+
{ url = "https://files.pythonhosted.org/packages/0e/fd/96f132c08b1e5951c68691d3b9ec351ec2edc028f6a01fcd294f46b9d9f0/xxhash-3.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:363c139bf15e1ac5f136b981d3c077eb551299b1effede7f12faa010b8590a60", size = 213613, upload-time = "2026-04-25T11:05:52.571Z" },
|
| 4486 |
+
{ url = "https://files.pythonhosted.org/packages/82/89/d4e92b796c5ed052d29ed324dbfc1dc1188e0c4bf64bebbf0f8fc20698df/xxhash-3.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a778b25874cb0f862eaab5986bff4ca49ffb0def7c0a34c237b948b3c6c775b2", size = 236726, upload-time = "2026-04-25T11:05:54.395Z" },
|
| 4487 |
+
{ url = "https://files.pythonhosted.org/packages/40/f1/81fc4361921dc6e557a9c60cb3712f36d244d06eeeb71cd2f4252ac42678/xxhash-3.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e1860f1e43d40e9d904cf22d93e587ea42e010ebce4160877e46bcab4bc232a", size = 212443, upload-time = "2026-04-25T11:05:56.334Z" },
|
| 4488 |
+
{ url = "https://files.pythonhosted.org/packages/6a/d0/afeddd4cff50a332f50d4b8a2e8857673153ab0564ef472fcdeb0b5430df/xxhash-3.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9122ad6f867c4a0f5e655f5c3bdf89103852009dbb442a3d23e688b9e699e800", size = 445793, upload-time = "2026-04-25T11:05:58.953Z" },
|
| 4489 |
+
{ url = "https://files.pythonhosted.org/packages/f7/d0/3c91e4e6a05ca4d7df8e39ec3a75b713609258ec84705ab34be6430826a1/xxhash-3.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d9110d0c3fb02679972837a033251fd186c529aa62f19c132fc909c74052b8", size = 193937, upload-time = "2026-04-25T11:06:00.546Z" },
|
| 4490 |
+
{ url = "https://files.pythonhosted.org/packages/4e/3a/a6b0772d9801dd4bea4ca4fd34734d6e9b51a711c8a611a24a79de26a878/xxhash-3.7.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:347a93f2b4ce67ce61959665e32a7447c380f8347e55e100daa23766baacf0e5", size = 285188, upload-time = "2026-04-25T11:06:01.96Z" },
|
| 4491 |
+
{ url = "https://files.pythonhosted.org/packages/6c/f8/cf8e31fd7282230fe7367cd501a2e75b4b67b222bfc7eacccfc20d2652cb/xxhash-3.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:acbb48679ddf3852c45280c10ff10d52ca2cd1da2e552fb81db1ff786c75d0e4", size = 210966, upload-time = "2026-04-25T11:06:03.453Z" },
|
| 4492 |
+
{ url = "https://files.pythonhosted.org/packages/cc/f0/fd36cc4a81bf52ee5633275daae2b93dd958aace67fd4f5d466ec83b5f35/xxhash-3.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fe14c356f8b23ad811dc026077a6d4abccdaa7bce5ca98579605550657b6fcfb", size = 241994, upload-time = "2026-04-25T11:06:05.264Z" },
|
| 4493 |
+
{ url = "https://files.pythonhosted.org/packages/08/e1/67f5d9c9369be42eaf99ba02c01bf14c5ecd67087b02567960bfcee43b63/xxhash-3.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f420ad3d41e38194353a498bbc9561fd5a9973a27b536ce46d8583479cf44335", size = 198707, upload-time = "2026-04-25T11:06:07.044Z" },
|
| 4494 |
+
{ url = "https://files.pythonhosted.org/packages/50/17/a4c865ca22d2da6b1bc7d739bf88cab209533cf52ba06ca9da27c3039bee/xxhash-3.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:693d02c6dc7d1aa0a45921d54cd8c1ff629e09dfdc2238471507af1f7a1c6f04", size = 210917, upload-time = "2026-04-25T11:06:08.853Z" },
|
| 4495 |
+
{ url = "https://files.pythonhosted.org/packages/49/8b/453b35810d697abac3c96bde3528bece685869227da274eb80a4a4d4a119/xxhash-3.7.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:14bf7a54e43825ec131ee7fe3c60e142e7c2c1e676ad0f93fc893432d15414af", size = 275772, upload-time = "2026-04-25T11:06:10.645Z" },
|
| 4496 |
+
{ url = "https://files.pythonhosted.org/packages/b5/ad/4eed7eab07fd3ee6678f416190f0413d097ab5d7c1278906bf1e9549d789/xxhash-3.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ae3a39a4d96bdb6f8d154fd7f490c4ad06f0532fcd2bb656052a9a7762cf5d31", size = 414068, upload-time = "2026-04-25T11:06:12.511Z" },
|
| 4497 |
+
{ url = "https://files.pythonhosted.org/packages/d3/4e/fd6f8a680ba248fdb83054fa71a8bfa3891225200de1708b888ef2c49829/xxhash-3.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1cc07c639e3a77ef1d32987464d3e408565b8a3be57b545d3542b191054d9923", size = 191459, upload-time = "2026-04-25T11:06:14.07Z" },
|
| 4498 |
+
{ url = "https://files.pythonhosted.org/packages/50/7c/8cb34b3bed4f44ca6827a534d50833f9bc6c006e83b0eb410ac9fa0793bd/xxhash-3.7.0-cp311-cp311-win32.whl", hash = "sha256:3281ba1d1e60ee7a382a7b958513ba03c2c0d5fcbd9a6f7517c0a81251a23422", size = 30628, upload-time = "2026-04-25T11:06:15.802Z" },
|
| 4499 |
+
{ url = "https://files.pythonhosted.org/packages/0b/47/a49767bd7b40782bedae9ff0721bfe1d7e4dd9dc1585dea684e57ba67c20/xxhash-3.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:a7f25baec4c5d851d40718d6fae52285b31683093d4ff5207e63ab306ccf14a5", size = 31461, upload-time = "2026-04-25T11:06:17.104Z" },
|
| 4500 |
+
{ url = "https://files.pythonhosted.org/packages/7c/c6/3957bfacfb706bd687be246dfa8dd60f8df97c44186d229f7fd6e26c4b7e/xxhash-3.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:4c2454448ce847c72635827bb75c15c5a3434b03ee1afd28cb6dc6fb2597d830", size = 27746, upload-time = "2026-04-25T11:06:18.716Z" },
|
| 4501 |
+
{ url = "https://files.pythonhosted.org/packages/f2/8a/51a14cdef4728c6c2337db8a7d8704422cc65676d9199d77215464c880af/xxhash-3.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:082c87bfdd2b9f457606c7a4a53457f4c4b48b0cdc48de0277f4349d79bb3d7a", size = 33357, upload-time = "2026-04-25T11:06:20.44Z" },
|
| 4502 |
+
{ url = "https://files.pythonhosted.org/packages/b9/1b/0c2c933809421ffd9bf42b59315552c143c755db5d9a816b2f1ae273e884/xxhash-3.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e7ce913b61f35b0c1c839a49ac9c8e75dd8d860150688aed353b0ce1bf409d8", size = 30869, upload-time = "2026-04-25T11:06:21.989Z" },
|
| 4503 |
+
{ url = "https://files.pythonhosted.org/packages/03/a8/89d5fdd6ee12d70ba99451de46dd0e8010167468dcd913ec855653f4dd50/xxhash-3.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3beb1de3b1e9694fcdd853e570ee64c631c7062435d2f8c69c1adf809bc086f0", size = 194100, upload-time = "2026-04-25T11:06:23.586Z" },
|
| 4504 |
+
{ url = "https://files.pythonhosted.org/packages/87/ee/2f9f2ed993e77206d1e66991290a1ebe22e843351ca3ebec8e49e01ba186/xxhash-3.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3e7b689c3bce16699efcf736066f5c6cc4472c3840fe4b22bd8279daf4abdac", size = 212977, upload-time = "2026-04-25T11:06:25.019Z" },
|
| 4505 |
+
{ url = "https://files.pythonhosted.org/packages/de/60/5a91644615a9e9d4e42c2e9925f1908e3a24e4e691d9de7340d565bea024/xxhash-3.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a6545e6b409e3d5cbafc850fb84c55a1ca26ed15a6b11e3bf07a0e0cd84517c8", size = 236373, upload-time = "2026-04-25T11:06:26.482Z" },
|
| 4506 |
+
{ url = "https://files.pythonhosted.org/packages/22/c0/f3a9384eaaed9d14d4d062a5d953aa0da489bfe9747877aa994caa87cd0b/xxhash-3.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:31ab1461c77a11461d703c88eb949e132a1c6515933cf675d97ec680f4bd18de", size = 212229, upload-time = "2026-04-25T11:06:28.065Z" },
|
| 4507 |
+
{ url = "https://files.pythonhosted.org/packages/2e/67/02f07a9fd79726804190f2172c4894c3ed9a4ebccaca05653c84beb58025/xxhash-3.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c4d596b7676f811172687ec567cbafb9e4dea2f9be1bbb4f622410cb7f40f40", size = 445462, upload-time = "2026-04-25T11:06:30.048Z" },
|
| 4508 |
+
{ url = "https://files.pythonhosted.org/packages/40/37/558f5a90c0672fc9b4402dc25d87ac5b7406616e8969430c9ca4e52ee74d/xxhash-3.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13805f0461cba0a857924e70ff91ae6d52d2598f79a884e788db80532614a4a1", size = 193932, upload-time = "2026-04-25T11:06:31.857Z" },
|
| 4509 |
+
{ url = "https://files.pythonhosted.org/packages/d5/90/aaa09cd58661d32044dbbad7df55bbe22a623032b810e7ed3b8c569a2a6f/xxhash-3.7.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d398f372496152f1c6933a33566373f8d1b37b98b8c9d608fa6edc0976f23b2", size = 284807, upload-time = "2026-04-25T11:06:33.697Z" },
|
| 4510 |
+
{ url = "https://files.pythonhosted.org/packages/d6/f3/53df3719ab127a02c174f0c1c74924fcd110866e89c966bc7909cfa8fa84/xxhash-3.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d610aa62cdb7d4d497740741772a24a794903bf3e79eaa51d2e800082abe11e5", size = 210445, upload-time = "2026-04-25T11:06:35.488Z" },
|
| 4511 |
+
{ url = "https://files.pythonhosted.org/packages/72/33/d219975c0e8b6fa2eb9ccd486fe47e21bf1847985b878dd2fbc3126e0d5c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:073c23900a9fbf3d26616c17c830db28af9803677cd5b33aea3224d824111514", size = 241273, upload-time = "2026-04-25T11:06:37.24Z" },
|
| 4512 |
+
{ url = "https://files.pythonhosted.org/packages/3e/50/49b1afe610eb3964cedcb90a4d4c3d46a261ee8669cbd4f060652619ae3c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:418a463c3e6a590c0cdc890f8be19adb44a8c8acd175ca5b2a6de77e61d0b386", size = 197950, upload-time = "2026-04-25T11:06:39.148Z" },
|
| 4513 |
+
{ url = "https://files.pythonhosted.org/packages/c6/75/5f42a1a4c78717d906a4b6a140c6dbf837ab1f547a54d23c4e2903310936/xxhash-3.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:03f8ff4474ee61c845758ce00711d7087a770d77efb36f7e74a6e867301000b8", size = 210709, upload-time = "2026-04-25T11:06:40.958Z" },
|
| 4514 |
+
{ url = "https://files.pythonhosted.org/packages/8a/85/237e446c25abced71e9c53d269f2cef5bab8a82b3f88a12e00c5368e7368/xxhash-3.7.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:44fba4a5f1d179b7ddc7b3dc40f56f9209046421679b57025d4d8821b376fd8d", size = 275345, upload-time = "2026-04-25T11:06:42.525Z" },
|
| 4515 |
+
{ url = "https://files.pythonhosted.org/packages/62/34/c2c26c0a6a9cc739bc2a5f0ae03ba8b87deb12b8bce35f7ac495e790dc6d/xxhash-3.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31e3516a0f829d06ded4a2c0f3c7c5561993256bfa1c493975fb9dc7bfa828a1", size = 414056, upload-time = "2026-04-25T11:06:44.343Z" },
|
| 4516 |
+
{ url = "https://files.pythonhosted.org/packages/a0/aa/5c58e9bc8071b8afd8dcf297ff362f723c4892168faba149f19904132bf4/xxhash-3.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b59ee2ac81de57771a09ecad09191e840a1d2fae1ef684208320591055768f83", size = 191485, upload-time = "2026-04-25T11:06:46.262Z" },
|
| 4517 |
+
{ url = "https://files.pythonhosted.org/packages/d4/69/a929cf9d1e2e65a48b818cdce72cb6b69eab2e6877f21436d0a1942aff43/xxhash-3.7.0-cp312-cp312-win32.whl", hash = "sha256:74bbd92f8c7fcc397ba0a11bfdc106bc72ad7f11e3a60277753f87e7532b4d81", size = 30671, upload-time = "2026-04-25T11:06:48.039Z" },
|
| 4518 |
+
{ url = "https://files.pythonhosted.org/packages/b9/1b/104b41a8947f4e1d4a66ce1e628eea752f37d1890bfd7453559ca7a3d950/xxhash-3.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:7bd7bc82dd4f185f28f35193c2e968ef46131628e3cac62f639dadf321cba4d1", size = 31514, upload-time = "2026-04-25T11:06:49.279Z" },
|
| 4519 |
+
{ url = "https://files.pythonhosted.org/packages/98/a0/1fd0ea1f1b886d9e7c73f0397571e22333a7d79e31da6d7127c2a4a71d75/xxhash-3.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:7d7148180ec99ba36585b42c8c5de25e9b40191613bc4be68909b4d25a77a852", size = 27761, upload-time = "2026-04-25T11:06:50.448Z" },
|
| 4520 |
+
{ url = "https://files.pythonhosted.org/packages/c1/ca/d5174b4c36d10f64d4ca7050563138c5a599efb01a765858ddefc9c1202a/xxhash-3.7.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:4b6d6b33f141158692bd4eafbb96edbc5aa0dabdb593a962db01a91983d4f8fa", size = 36813, upload-time = "2026-04-25T11:06:51.73Z" },
|
| 4521 |
+
{ url = "https://files.pythonhosted.org/packages/41/d0/abc6c9d347ba1f1e1e1d98125d0881a0452c7f9a76a9dd03a7b5d2197f23/xxhash-3.7.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:845d347df254d6c619f616afa921331bada8614b8d373d58725c663ba97c3605", size = 35121, upload-time = "2026-04-25T11:06:53.048Z" },
|
| 4522 |
+
{ url = "https://files.pythonhosted.org/packages/bf/11/4cc834eb3d79f2f2b3a6ef7324195208bcdfbdcf7534d2b17267aa5f3a8f/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fddbbb69a6fff4f421e7a0d1fa28f894b20112e9e3fab306af451e2dfd0e459b", size = 29624, upload-time = "2026-04-25T11:06:54.311Z" },
|
| 4523 |
+
{ url = "https://files.pythonhosted.org/packages/23/83/e97d3e7b635fe73a1dfb1e91f805324dd6d930bb42041cbf18f183bc0b6d/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:54876a4e45101cec2bf8f31a973cda073a23e2e108538dad224ba07f85f22487", size = 30638, upload-time = "2026-04-25T11:06:55.864Z" },
|
| 4524 |
+
{ url = "https://files.pythonhosted.org/packages/f4/40/d84951d80c35db1f4c40a29a64a8520eea5d56e764c603906b4fe763580f/xxhash-3.7.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:0c72fe9c7e3d6dfd7f1e21e224a877917fa09c465694ba4e06464b9511b65544", size = 33323, upload-time = "2026-04-25T11:06:57.336Z" },
|
| 4525 |
+
{ url = "https://files.pythonhosted.org/packages/89/cc/c7dc6558d97e9ab023f663d69ab28b340ed9bf4d2d94f2c259cf896bb354/xxhash-3.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a6d73a830b17ef49bc04e00182bd839164c1b3c59c127cd7c54fcb10c7ed8ee8", size = 33362, upload-time = "2026-04-25T11:06:58.656Z" },
|
| 4526 |
+
{ url = "https://files.pythonhosted.org/packages/2a/6e/46b84017b1301d54091430353d4ad5901654a3e0871649877a416f7f1644/xxhash-3.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c3b07cf3362086d8f126c6aecd8e5e9396ad8b2f2219ea7e49a8250c318acd", size = 30874, upload-time = "2026-04-25T11:06:59.834Z" },
|
| 4527 |
+
{ url = "https://files.pythonhosted.org/packages/df/5e/8f9158e3ab906ad3fec51e09b5ea0093e769f12207bfa42a368ca204e7ab/xxhash-3.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50e879ebbac351c81565ca108db766d7832f5b8b6a5b14b8c0151f7190028e3d", size = 194185, upload-time = "2026-04-25T11:07:01.658Z" },
|
| 4528 |
+
{ url = "https://files.pythonhosted.org/packages/f3/29/a804ded9f5d3d3758292678d23e7528b08fda7b7e750688d08b052322475/xxhash-3.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:921c14e93817842dd0dd9f372890a0f0c72e534650b6ab13c5be5cd0db11d47e", size = 213033, upload-time = "2026-04-25T11:07:03.606Z" },
|
| 4529 |
+
{ url = "https://files.pythonhosted.org/packages/8b/91/1ce5a7d2fdc975267320e2c78fc1cecfe7ab735ccbcf6993ec5dd541cb2c/xxhash-3.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e64a7c9d7dfca3e0fafcbc5e455519090706a3e36e95d655cec3e04e79f95aaa", size = 236140, upload-time = "2026-04-25T11:07:05.396Z" },
|
| 4530 |
+
{ url = "https://files.pythonhosted.org/packages/34/04/fd595a4fd8617b05fa27bd9b684ecb4985bfed27917848eea85d54036d06/xxhash-3.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2220af08163baf5fa36c2b8af079dc2cbe6e66ae061385267f9472362dfd53c6", size = 212291, upload-time = "2026-04-25T11:07:06.966Z" },
|
| 4531 |
+
{ url = "https://files.pythonhosted.org/packages/03/fb/f1a379cbc372ae5b9f4ab36154c48a849ca6ebe3ac477067a57865bf3bc6/xxhash-3.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f14bb8b22a4a91325813e3d553b8963c10cf8c756cff65ee50c194431296c655", size = 445532, upload-time = "2026-04-25T11:07:08.525Z" },
|
| 4532 |
+
{ url = "https://files.pythonhosted.org/packages/65/59/172424b79f8cfd4b6d8a122b2193e6b8ad4b11f7159bb3b6f9b3191329bb/xxhash-3.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496736f86a9bedaf64b0dc70e3539d0766df01c71ea22032698e88f3f04a1ce9", size = 193990, upload-time = "2026-04-25T11:07:10.315Z" },
|
| 4533 |
+
{ url = "https://files.pythonhosted.org/packages/b9/19/aeac22161d953f139f07ba5586cb4a17c5b7b6dff985122803bb12933500/xxhash-3.7.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0ff71596bd79816975b3de7130ab1ff4541410285a3c084584eeb1c8239996fd", size = 284876, upload-time = "2026-04-25T11:07:12.15Z" },
|
| 4534 |
+
{ url = "https://files.pythonhosted.org/packages/77/d5/4fd0b59e7a02242953da05ff679fbb961b0a4368eac97a217e11dae110c1/xxhash-3.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1ad86695c19b1d46fe106925db3c7a37f16be37669dcf58dcc70a9dd6e324676", size = 210495, upload-time = "2026-04-25T11:07:13.952Z" },
|
| 4535 |
+
{ url = "https://files.pythonhosted.org/packages/aa/fb/976a3165c728c7faf74aa1b5ab3cf6a85e6d731612894741840524c7d28c/xxhash-3.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:970f9f8c50961d639cbd0d988c96f80ddf66006de93641719282c4fe7a87c5e6", size = 241331, upload-time = "2026-04-25T11:07:15.557Z" },
|
| 4536 |
+
{ url = "https://files.pythonhosted.org/packages/4a/2c/6763d5901d53ac9e6ba296e5717ae599025c9d268396e8faa8b4b0a8e0ac/xxhash-3.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5886ad85e9e347911783760a1d16cb6b393e8f9e3b52c982568226cb56927bdc", size = 198037, upload-time = "2026-04-25T11:07:17.563Z" },
|
| 4537 |
+
{ url = "https://files.pythonhosted.org/packages/61/2b/876e722d533833f5f9a83473e6ba993e48745701096944e77bbecf29b2c3/xxhash-3.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6e934bbae1e0ec74e27d5f0d7f37ef547ce5ff9f0a7e63fb39e559fc99526734", size = 210744, upload-time = "2026-04-25T11:07:19.055Z" },
|
| 4538 |
+
{ url = "https://files.pythonhosted.org/packages/21/e6/d7e7baef7ce24166b4668d3c48557bb35a23b92ecadcac7e7718d099ab69/xxhash-3.7.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3b6b3d28228af044ebcded71c4a3dd86e1dbd7e2f4645bf40f7b5da65bb5fb5a", size = 275406, upload-time = "2026-04-25T11:07:20.908Z" },
|
| 4539 |
+
{ url = "https://files.pythonhosted.org/packages/92/fe/198b3763b2e01ca908f2154969a2352ec99bda892b574a11a9a151c5ede4/xxhash-3.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6be4d70d9ab76c9f324ead9c01af6ff52c324745ea0c3731682a0cf99720f1fe", size = 414125, upload-time = "2026-04-25T11:07:23.037Z" },
|
| 4540 |
+
{ url = "https://files.pythonhosted.org/packages/3a/6d/019a11affd5a5499137cacca53808659964785439855b5aa40dfd3412916/xxhash-3.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:151d7520838d4465461a0b7f4ae488b3b00de16183dd3214c1a6b14bf89d7fb6", size = 191555, upload-time = "2026-04-25T11:07:24.991Z" },
|
| 4541 |
+
{ url = "https://files.pythonhosted.org/packages/76/21/b96d58568df2d01533244c3e0e5cbdd0c8b2b25c4bec4d72f19259a292d7/xxhash-3.7.0-cp313-cp313-win32.whl", hash = "sha256:d798c1e291bffb8e37b5bbe0dda77fc767cd19e89cadaf66e6ed5d0ff88c9fe6", size = 30668, upload-time = "2026-04-25T11:07:26.665Z" },
|
| 4542 |
+
{ url = "https://files.pythonhosted.org/packages/99/57/d849a8d3afa1f8f4bc6a831cd89f49f9706fbbad94d2975d6140a171988c/xxhash-3.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:875811ba23c543b1a1c3143c926e43996eb27ebb8f52d3500744aa608c275aed", size = 31524, upload-time = "2026-04-25T11:07:27.92Z" },
|
| 4543 |
+
{ url = "https://files.pythonhosted.org/packages/81/52/bacc753e92dee78b058af8dcef0a50815f5f860986c664a92d75f965b6a5/xxhash-3.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:54a675cb300dda83d71daae2a599389d22db8021a0f8db0dd659e14626eb3ecc", size = 27768, upload-time = "2026-04-25T11:07:29.113Z" },
|
| 4544 |
+
{ url = "https://files.pythonhosted.org/packages/1c/47/ddbd683b7fc7e592c1a8d9d65f73ce9ab513f082b3967eee2baf549b8fc6/xxhash-3.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a3b19a42111c4057c1547a4a1396a53961dca576a0f6b82bfa88a2d1561764b2", size = 33576, upload-time = "2026-04-25T11:07:30.469Z" },
|
| 4545 |
+
{ url = "https://files.pythonhosted.org/packages/07/f2/36d3310161db7f72efb4562aadde0ed429f1d0531782dd6345b12d2da527/xxhash-3.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8f4608a06e4d61b7a3425665a46d00e0579122e1a2fae97a0c52953a3aad9aa3", size = 31123, upload-time = "2026-04-25T11:07:31.989Z" },
|
| 4546 |
+
{ url = "https://files.pythonhosted.org/packages/0d/3f/75937a5c69556ed213021e43cbedd84c8e0279d0d74e7d41a255d84ba4b1/xxhash-3.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad37c7792479e49cf96c1ab25517d7003fe0d93687a772ba19a097d235bbe41e", size = 196491, upload-time = "2026-04-25T11:07:33.358Z" },
|
| 4547 |
+
{ url = "https://files.pythonhosted.org/packages/22/29/f10d7ff8c7a733d4403a43b9de18c8fabc005f98cec054644f04418659ee/xxhash-3.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc026e3b89d98e30a8288c95cb696e77d150b3f0fb7a51f73dcd49ee6b5577fa", size = 215793, upload-time = "2026-04-25T11:07:34.919Z" },
|
| 4548 |
+
{ url = "https://files.pythonhosted.org/packages/8b/fd/778f60aa295f58907938f030a8b514611f391405614a525cccd2ffc00eb5/xxhash-3.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9b31ab1f28b078a6a1ac1a54eb35e7d5390deddd56870d0be3a0a733d1c321c", size = 237993, upload-time = "2026-04-25T11:07:36.638Z" },
|
| 4549 |
+
{ url = "https://files.pythonhosted.org/packages/70/f5/736db5de387b4a540e37a05b84b40dc58a1ce974bfd2b4e5754ce29b68c3/xxhash-3.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bb5fd680c038fd5229e44e9c493782f90df9bef632fd0499d442374688ff70b", size = 214887, upload-time = "2026-04-25T11:07:38.564Z" },
|
| 4550 |
+
{ url = "https://files.pythonhosted.org/packages/4d/aa/09a095f22fdb9a27fbb716841fbff52119721f9ca4261952d07a912f7839/xxhash-3.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030c0fd688fce3569fbb49a2feefd4110cbb0b650186fb4610759ecfac677548", size = 448407, upload-time = "2026-04-25T11:07:40.552Z" },
|
| 4551 |
+
{ url = "https://files.pythonhosted.org/packages/74/8a/b745efeeca9e34a91c26fdc97ad8514c43d5a81ac78565cba80a1353870a/xxhash-3.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b1bde10324f4c31812ae0d0502e92d916ae8917cad7209353f122b8b8f610c3", size = 196119, upload-time = "2026-04-25T11:07:42.101Z" },
|
| 4552 |
+
{ url = "https://files.pythonhosted.org/packages/8a/5c/0cfceb024af90c191f665c7933b1f318ee234f4797858383bebd1881d52f/xxhash-3.7.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:503722d52a615f2604f5e7611de7d43878df010dc0053094ef91cb9a9ac3d987", size = 286751, upload-time = "2026-04-25T11:07:43.568Z" },
|
| 4553 |
+
{ url = "https://files.pythonhosted.org/packages/0b/0a/0793e405dc3cf8f4ebe2c1acec1e4e4608cd9e7e50ea691dabbc2a95ccbb/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c72500a3b6d6c30ebfc135035bcace9eb5884f2dc220804efcaaba43e9f611dd", size = 212961, upload-time = "2026-04-25T11:07:45.388Z" },
|
| 4554 |
+
{ url = "https://files.pythonhosted.org/packages/0c/7e/721118ffc63bfff94aa565bcf2555a820f9f4bdb0f001e0d609bdfad70de/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:43475925a766d01ca8cd9a857fd87f3d50406983c8506a4c07c4df12adcc867f", size = 243703, upload-time = "2026-04-25T11:07:47.053Z" },
|
| 4555 |
+
{ url = "https://files.pythonhosted.org/packages/6e/18/16f6267160488b8276fd3d449d425712512add292ba545c1b6946bfdb7dd/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8d09dfd2ab135b985daf868b594315ebe11ad86cd9fea46e6c69f19b28f7d25a", size = 200894, upload-time = "2026-04-25T11:07:48.657Z" },
|
| 4556 |
+
{ url = "https://files.pythonhosted.org/packages/2d/94/80ba841287fd97e3e9cac1d228788c8ef623746f570404961eec748ecb5c/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c50269d0055ac1faecfd559886d2cbe4b730de236585aba0e873f9d9dadbe585", size = 213357, upload-time = "2026-04-25T11:07:50.257Z" },
|
| 4557 |
+
{ url = "https://files.pythonhosted.org/packages/a1/7e/106d4067130c59f1e18a55ffadcd876d8c68534883a1e02685b29d3d8153/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1910df4756a5ab58cfad8744fc2d0f23926e3efcc346ee76e87b974abab922f4", size = 277600, upload-time = "2026-04-25T11:07:51.745Z" },
|
| 4558 |
+
{ url = "https://files.pythonhosted.org/packages/c5/86/a081dd30da71d720b2612a792bfd55e45fa9a07ac76a0507f60487473c25/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d006faf3b491957efcb433489be3c149efe4787b7063d5cddb8ddaefdc60e0c1", size = 416980, upload-time = "2026-04-25T11:07:53.504Z" },
|
| 4559 |
+
{ url = "https://files.pythonhosted.org/packages/35/29/1a95221a029a3c1293773869e1ab47b07cbbdd82444a42809e8c60156626/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:abb65b4e947e958f7b3b0d71db3ce447d1bc5f37f5eab871ce7223bda8768a04", size = 193840, upload-time = "2026-04-25T11:07:55.103Z" },
|
| 4560 |
+
{ url = "https://files.pythonhosted.org/packages/c5/e0/db909dd0823285de2286f67e10ee4d81e96ad35d7d8e964ecb07fccd8af9/xxhash-3.7.0-cp313-cp313t-win32.whl", hash = "sha256:178959906cb1716a1ce08e0d69c82886c70a15a6f2790fc084fdd146ca30cd49", size = 30966, upload-time = "2026-04-25T11:07:56.524Z" },
|
| 4561 |
+
{ url = "https://files.pythonhosted.org/packages/7b/ff/d705b15b22f21ee106adce239cb65d35067a158c630b240270f09b17c2e6/xxhash-3.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2524a1e20d4c231d13b50f7cf39e44265b055669a64a7a4b9a2a44faa03f19b6", size = 31784, upload-time = "2026-04-25T11:07:57.758Z" },
|
| 4562 |
+
{ url = "https://files.pythonhosted.org/packages/a2/1f/b2cf83c3638fd0588e0b17f22e5a9400bdfb1a3e3755324ac0aee2250b88/xxhash-3.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:37d994d0ffe81ef087bb330d392caa809bb5853c77e22ea3f71db024a0543dba", size = 27932, upload-time = "2026-04-25T11:07:59.109Z" },
|
| 4563 |
+
{ url = "https://files.pythonhosted.org/packages/0e/cc/431db584f6fbb9312e40a173af027644e5580d39df1f73603cbb9dca4d6b/xxhash-3.7.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:8c5fcfd806c335bfa2adf1cd0b3110a44fc7b6995c3a648c27489bae85801465", size = 36644, upload-time = "2026-04-25T11:08:00.658Z" },
|
| 4564 |
+
{ url = "https://files.pythonhosted.org/packages/bc/01/255ec513e0a705d1f9a61413e78dfce4e3235203f0ed525a24c2b4b56345/xxhash-3.7.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:506a0b488f190f0a06769575e30caf71615c898ed93ab18b0dbcb6dec5c3713c", size = 35003, upload-time = "2026-04-25T11:08:02.338Z" },
|
| 4565 |
+
{ url = "https://files.pythonhosted.org/packages/68/70/c55fc33c93445b44d8fc5a17b41ed99e3cebe92bcf8396809e63fc9a1165/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ec68dbba21532c0173a9872298e65c89749f7c9d21538c3a78b5bb6105871568", size = 29655, upload-time = "2026-04-25T11:08:03.701Z" },
|
| 4566 |
+
{ url = "https://files.pythonhosted.org/packages/c2/72/ff8de73df000d74467d12a59ce6d6e2b2a368b978d41ab7b1fba5ed442be/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa77e7ec1450d415d20129961814787c9abd9a07f98872f070b1fe96c5084611", size = 30664, upload-time = "2026-04-25T11:08:05.011Z" },
|
| 4567 |
+
{ url = "https://files.pythonhosted.org/packages/b6/91/08416d9bd9bc3bf39d831abe8a5631ac2db5141dfd6fe81c3fe59a1f9264/xxhash-3.7.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fe32736295ea38e43e7d9424053c8c47c9f64fecfc7c895fb3da9b30b131c9ee", size = 33317, upload-time = "2026-04-25T11:08:06.413Z" },
|
| 4568 |
+
{ url = "https://files.pythonhosted.org/packages/0e/3b/86b1caa4dee10a99f4bf9521e623359341c5e50d05158fa10c275b2bd079/xxhash-3.7.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ab9dd2c83c4bbd63e422181a76f13502d049d3ddcac9a1bdc29196263d692bb8", size = 33457, upload-time = "2026-04-25T11:08:08.099Z" },
|
| 4569 |
+
{ url = "https://files.pythonhosted.org/packages/ed/38/98ea14ad1517e1461292a65906951458d520689782bfbae111050145bdba/xxhash-3.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3afec3a336a2286601a437cb07562ab0227685e6fbb9ec17e8c18457ff348ecf", size = 30894, upload-time = "2026-04-25T11:08:09.429Z" },
|
| 4570 |
+
{ url = "https://files.pythonhosted.org/packages/61/a2/074654d0b893606541199993c7db70067d9fc63b748e0d60020a52a1bd36/xxhash-3.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:565df64437a9390f84465dcca33e7377114c7ede8d05cd2cf20081f831ea788e", size = 194409, upload-time = "2026-04-25T11:08:10.91Z" },
|
| 4571 |
+
{ url = "https://files.pythonhosted.org/packages/e2/26/6d2a1afc468189f77ca28c32e1c83e1b9da1178231e05641dbc1b350e332/xxhash-3.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12eca820a5d558633d423bf8bb78ce72a55394823f64089247f788a7e0ae691e", size = 213135, upload-time = "2026-04-25T11:08:12.575Z" },
|
| 4572 |
+
{ url = "https://files.pythonhosted.org/packages/8e/0e/d8aecf95e09c42547453137be74d2f7b8b14e08f5177fa2fab6144a19061/xxhash-3.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f262b8f7599516567e070abf607b9af649052b2c4bd6f9be02b0cb41b7024805", size = 236379, upload-time = "2026-04-25T11:08:14.206Z" },
|
| 4573 |
+
{ url = "https://files.pythonhosted.org/packages/f2/74/8140e8210536b3dd0cc816c4faaeb5ba6e63e8125ab25af4bcddd6a037b3/xxhash-3.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1598916cb197681e03e601901e4ab96a9a963de398c59d0964f8a6f44a2b361", size = 212447, upload-time = "2026-04-25T11:08:15.79Z" },
|
| 4574 |
+
{ url = "https://files.pythonhosted.org/packages/a0/d2/462001d2903b4bee5a5689598a0a55e5e7cd1ac7f4247a5545cff10d3ebb/xxhash-3.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:322b2f0622230f526aeb1738149948a7ae357a9e2ceb1383c6fd1fdaecdafa16", size = 445660, upload-time = "2026-04-25T11:08:17.441Z" },
|
| 4575 |
+
{ url = "https://files.pythonhosted.org/packages/23/09/2bd1ed7f8689b20e51727952cac8329d50c694dc32b2eba06ba5bc742b37/xxhash-3.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cc22070880cc57b830a65cde4e65fa884c6d9b28ae4803b5ee05911e7bafba", size = 194076, upload-time = "2026-04-25T11:08:19.134Z" },
|
| 4576 |
+
{ url = "https://files.pythonhosted.org/packages/c9/6e/692302cd0a5f4ac4e6289f37fa888dc2e1e07750b68fe3e4bfe939b8cea3/xxhash-3.7.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb5a888a968b2434abf9ecda357b5d43f10d7b5a6da6fdbbe036208473aff0e2", size = 284990, upload-time = "2026-04-25T11:08:20.618Z" },
|
| 4577 |
+
{ url = "https://files.pythonhosted.org/packages/05/d9/e54b159b3d9df7999d2a7c676ce7b323d1b5588a64f8f51ed8172567bd87/xxhash-3.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a999771ff97bec27d18341be4f3a36b163bb1ac41ec17bef6d2dabd84acd33c7", size = 210590, upload-time = "2026-04-25T11:08:22.24Z" },
|
| 4578 |
+
{ url = "https://files.pythonhosted.org/packages/50/93/0e0df1a3a196ced4ca71de76d65ead25d8e87bbfb87b64306ea47a40c00d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ed4a6efe2dee1655adb73e7ad40c6aa955a6892422b1e3b95de6a34de56e3cbb", size = 241442, upload-time = "2026-04-25T11:08:23.844Z" },
|
| 4579 |
+
{ url = "https://files.pythonhosted.org/packages/9a/a9/d917a7a814e90b218f8a0d37967105eea91bf752c3303683c99a1f7bfc1f/xxhash-3.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fd17f14ac0faa12126c2f9ca774a8cf342957265ec3c8669c144e5e6cdb478c", size = 198356, upload-time = "2026-04-25T11:08:25.99Z" },
|
| 4580 |
+
{ url = "https://files.pythonhosted.org/packages/89/5e/f2ba1877c39469abbefc72991d6ebdcbd4c0880db01ae8cb1f553b0c537d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:05fd1254268c59b5cb2a029dfc204275e9fc52de2913f1e53aa8d01442c96b4d", size = 210898, upload-time = "2026-04-25T11:08:27.608Z" },
|
| 4581 |
+
{ url = "https://files.pythonhosted.org/packages/90/c6/be56b58e73de531f39a10de1355bb77ceb663900dc4bf2d6d3002a9c3f9e/xxhash-3.7.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a2eae53197c6276d5b317f75a1be226bbf440c20b58bf525f36b5d0e1f657ca6", size = 275519, upload-time = "2026-04-25T11:08:29.301Z" },
|
| 4582 |
+
{ url = "https://files.pythonhosted.org/packages/92/e2/17ddc85d5765b9c709f192009ed8f5a1fc876f4eb35bba7c307b5b1169f9/xxhash-3.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bfe6f92e3522dcbe8c4281efd74fa7542a336cb00b0e3272c4ec0edabeaeaf67", size = 414191, upload-time = "2026-04-25T11:08:31.16Z" },
|
| 4583 |
+
{ url = "https://files.pythonhosted.org/packages/9c/42/85f5b79f4bf1ec7ba052491164adfd4f4e9519f5dc7246de4fbd64a1bd56/xxhash-3.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7ab9a49c410d8c6c786ab99e79c529938d894c01433130353dd0fe999111077a", size = 191604, upload-time = "2026-04-25T11:08:32.862Z" },
|
| 4584 |
+
{ url = "https://files.pythonhosted.org/packages/b8/d0/6127b623aa4cca18d8b7743592b048d689fd6c6e37ff26a22cddf6cd9d7f/xxhash-3.7.0-cp314-cp314-win32.whl", hash = "sha256:040ea63668f9185b92bc74942df09c7e65703deed71431333678fc6e739a9955", size = 31271, upload-time = "2026-04-25T11:08:34.651Z" },
|
| 4585 |
+
{ url = "https://files.pythonhosted.org/packages/64/4f/44fc4788568004c43921701cbc127f48218a1eede2c9aea231115323564d/xxhash-3.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2a61e2a3fb23c892496d587b470dee7fa1b58b248a187719c65ea8e94ec13257", size = 32284, upload-time = "2026-04-25T11:08:35.987Z" },
|
| 4586 |
+
{ url = "https://files.pythonhosted.org/packages/6d/77/18bb895eb60a49453d16e17d67990e5caff557c78eafc90ad4e2eabf4570/xxhash-3.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:c7741c7524961d8c0cb4d4c21b28957ff731a3fd5b5cd8b856dc80a40e9e5acc", size = 28701, upload-time = "2026-04-25T11:08:37.767Z" },
|
| 4587 |
+
{ url = "https://files.pythonhosted.org/packages/45/a0/46f72244570c550fbbb7db1ef554183dd5ebe9136385f30e032b781ae8f6/xxhash-3.7.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:fc84bf7aa7592f31ec63a3e7b11d624f468a3f19f5238cec7282a42e838ab1d7", size = 33646, upload-time = "2026-04-25T11:08:39.109Z" },
|
| 4588 |
+
{ url = "https://files.pythonhosted.org/packages/4a/3a/453846a7eceea11e75def361eed01ec6a0205b9822c19927ed364ccae7cc/xxhash-3.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f1563fdc8abfc389748e6932c7e4e99c89a53e4ec37d4563c24fc06f5e5644b", size = 31125, upload-time = "2026-04-25T11:08:40.467Z" },
|
| 4589 |
+
{ url = "https://files.pythonhosted.org/packages/bd/3e/49434aba738885d512f9e486db1bdd19db28dfa40372b56da26ef7a4e738/xxhash-3.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d415f18becf6f153046ab6adc97da77e3643a0ee205dae61c4012604113a020", size = 196633, upload-time = "2026-04-25T11:08:41.943Z" },
|
| 4590 |
+
{ url = "https://files.pythonhosted.org/packages/a4/e9/006cb6127baeb9f8abe6d15e62faa01349f09b34e2bfd65175b2422d026b/xxhash-3.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb16aa13ed175bc9be5c2491ba031b85a9b51c4ed90e0b3d4ebe63cf3fb54f8e", size = 215899, upload-time = "2026-04-25T11:08:43.645Z" },
|
| 4591 |
+
{ url = "https://files.pythonhosted.org/packages/27/e4/cc57d72e66df0ae29b914335f1c6dcf61e8f3746ddf0ae3c471aa4f15e00/xxhash-3.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f9fd595f1e5941b3d7863e4774e4b30caa6731fc34b9277da032295aa5656ee5", size = 238116, upload-time = "2026-04-25T11:08:45.698Z" },
|
| 4592 |
+
{ url = "https://files.pythonhosted.org/packages/af/78/3531d4a3fd8a0038cc6be1f265a69c1b3587f557a10b677dd736de2202c1/xxhash-3.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1295325c5a98d552333fa53dc2b026b0ef0ec9c8e73ca3a952990b4c7d65d459", size = 215012, upload-time = "2026-04-25T11:08:47.355Z" },
|
| 4593 |
+
{ url = "https://files.pythonhosted.org/packages/b4/f6/259fb1eaaec921f59b17203b0daee69829761226d3b980d5191d7723dd83/xxhash-3.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3573a651d146912da9daa9e29e5fbc45994420daaa9ef1e2fa5823e1dc485513", size = 448534, upload-time = "2026-04-25T11:08:49.149Z" },
|
| 4594 |
+
{ url = "https://files.pythonhosted.org/packages/7b/16/a66d0eaf6a7e68532c07714361ddc904c663ec940f3b028c1ae4a21a7b9d/xxhash-3.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ec1e080a3d02d94ea9335bfab0e3374b877e25411422c18f51a943fa4b46381", size = 196217, upload-time = "2026-04-25T11:08:50.805Z" },
|
| 4595 |
+
{ url = "https://files.pythonhosted.org/packages/8d/ef/d2efc7fc51756dc52509109d1a25cefc859d74bc4b19a167b12dbd8c2786/xxhash-3.7.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84415265192072d8638a3afc3c1bc5995e310570cd9acb54dc46d3939e364fe0", size = 286906, upload-time = "2026-04-25T11:08:52.418Z" },
|
| 4596 |
+
{ url = "https://files.pythonhosted.org/packages/fc/67/25decd1d4a4018582ec4db2a868a2b7e40640f4adb20dfeb19ac923aa825/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d4dea659b57443989ef32f4295104fd6912c73d0bf26d1d148bb88a9f159b02", size = 213057, upload-time = "2026-04-25T11:08:54.105Z" },
|
| 4597 |
+
{ url = "https://files.pythonhosted.org/packages/0d/5d/17651eb29d06786cdc40c60ae3d27d645aa5d61d2eca6237a7ba0b94789b/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05ece0fe4d9c9c2728912d1981ae1566cfc83a011571b24732cbf76e1fb70dca", size = 243886, upload-time = "2026-04-25T11:08:56.109Z" },
|
| 4598 |
+
{ url = "https://files.pythonhosted.org/packages/8a/d4/174d9cf7502243d586e6a9ae842b1ae23026620995114f85f1380e588bc9/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fd880353cf1ffaf321bc18dd663e111976dbd0d3bbd8a66d58d2b470dfa7f396", size = 201015, upload-time = "2026-04-25T11:08:57.777Z" },
|
| 4599 |
+
{ url = "https://files.pythonhosted.org/packages/91/8c/2254e2d06c3ac5e6fe22eaf3da791b87ea823ae9f2c17b4af66755c5752d/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4e15cc9e2817f6481160f930c62842b3ff419e20e13072bcbab12230943092bc", size = 213457, upload-time = "2026-04-25T11:08:59.826Z" },
|
| 4600 |
+
{ url = "https://files.pythonhosted.org/packages/79/a2/e3daa762545921173e3360f3b4ff7fc63c2d27359f7230ec1a7a74e117f6/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:90b9d1a8bd37d768ffc92a1f651ec69afc532a96fa1ac2ea7abbed5d630b3237", size = 277738, upload-time = "2026-04-25T11:09:01.423Z" },
|
| 4601 |
+
{ url = "https://files.pythonhosted.org/packages/e1/4c/e186da2c46b87f5204640e008d42730bf3c1ee9f0efb71ae1ebcdfeac681/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:157c49475b34ecea8809e51123d9769a534e139d1247942f7a4bc67710bb2533", size = 417127, upload-time = "2026-04-25T11:09:03.592Z" },
|
| 4602 |
+
{ url = "https://files.pythonhosted.org/packages/17/28/3798e15007a3712d0da3d3fe70f8e11916569858b5cc371053bc26270832/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a6ddec83325685e729ca119d1f5c518ec39294212ecd770e60693cdc5f7eb79", size = 193962, upload-time = "2026-04-25T11:09:06.228Z" },
|
| 4603 |
+
{ url = "https://files.pythonhosted.org/packages/ad/95/a26baa93b5241fd7630998816a4ec47a5a0bad193b3f8fc8f3593e1a4a67/xxhash-3.7.0-cp314-cp314t-win32.whl", hash = "sha256:a04a6cab47e2166435aaf5b9e5ee41d1532cc8300efdef87f2a4d0acb7db19ed", size = 31643, upload-time = "2026-04-25T11:09:08.153Z" },
|
| 4604 |
+
{ url = "https://files.pythonhosted.org/packages/44/36/5454f13c447e395f9b06a3e91274c59f503d31fad84e1836efe3bdb71f6a/xxhash-3.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8653dd7c2eda020545bb2c71c7f7039b53fe7434d0fc1a0a9deb79ab3f1a4fc1", size = 32522, upload-time = "2026-04-25T11:09:09.534Z" },
|
| 4605 |
+
{ url = "https://files.pythonhosted.org/packages/74/35/698e7e3ff38e22992ea24870a511d8762474fb6783627a2910ff22a185c2/xxhash-3.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:468f0fc114faaa4b36699f8e328bbc3bb11dc418ba94ac52c26dd736d4b6c637", size = 28807, upload-time = "2026-04-25T11:09:11.234Z" },
|
| 4606 |
+
{ url = "https://files.pythonhosted.org/packages/54/c1/e57ac7317b1f58a92bab692da6d497e2a7ce44735b224e296347a7ecc754/xxhash-3.7.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad3aa71e12ee634f22b39a0ff439357583706e50765f17f05550f92dbf128a23", size = 31232, upload-time = "2026-04-25T11:10:21.51Z" },
|
| 4607 |
+
{ url = "https://files.pythonhosted.org/packages/4f/4e/075559bd712bc62e84915ea46bbee859f935d285659082c129bdbff679dd/xxhash-3.7.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5de686e73690cdaf72b96d4fa083c230ec9020bcc2627ce6316138e2cf2fe2d1", size = 28553, upload-time = "2026-04-25T11:10:23.1Z" },
|
| 4608 |
+
{ url = "https://files.pythonhosted.org/packages/92/ca/a9c78cb384d4b033b0c58196bd5c8509873cabe76389e195127b0302a741/xxhash-3.7.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7fbec49f5341bbdea0c471f7d1e2fb41ae8925af9b6f28025c28defd8eb94274", size = 41109, upload-time = "2026-04-25T11:10:25.022Z" },
|
| 4609 |
+
{ url = "https://files.pythonhosted.org/packages/bd/b1/dfe2629f7c77eb2fa234c72ff537cdd64939763df704e256446ed364a16d/xxhash-3.7.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48b542c347c2089f43dc5a6db31d2a6f3cdb04ee33505ec6e9f653834dbb0bde", size = 36307, upload-time = "2026-04-25T11:10:26.949Z" },
|
| 4610 |
+
{ url = "https://files.pythonhosted.org/packages/e7/f7/5a484afce0f48dd8083208b42e4911f290a82c7b52458ef2927e4d421a45/xxhash-3.7.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a169a036bed0995e090d1493b283cc2cc8a6f5046821086b843abefff80643bc", size = 32534, upload-time = "2026-04-25T11:10:29.01Z" },
|
| 4611 |
+
{ url = "https://files.pythonhosted.org/packages/0f/5f/4acfcd490db9780cf36c58534d828003c564cde5350220a1c783c4d10776/xxhash-3.7.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ec101643395d7f21405b640f728f6f627e6986557027d740f2f9b220955edafe", size = 31552, upload-time = "2026-04-25T11:10:30.727Z" },
|
| 4612 |
+
]
|
| 4613 |
+
|
| 4614 |
[[package]]
|
| 4615 |
name = "yarl"
|
| 4616 |
version = "1.23.0"
|