Spaces:
Sleeping
Sleeping
Delete aether-taskflow
Browse files- aether-taskflow/.dockerignore +0 -12
- aether-taskflow/.gitignore +0 -9
- aether-taskflow/Dockerfile +0 -32
- aether-taskflow/README.md +0 -333
- aether-taskflow/__init__.py +0 -2
- aether-taskflow/env/__init__.py +0 -4
- aether-taskflow/env/aether_env.py +0 -653
- aether-taskflow/env/algorithms.py +0 -253
- aether-taskflow/env/grader.py +0 -79
- aether-taskflow/env/tasks.py +0 -192
- aether-taskflow/inference.py +0 -229
- aether-taskflow/models.py +0 -298
- aether-taskflow/openenv.yaml +0 -27
- aether-taskflow/pyproject.toml +0 -32
- aether-taskflow/pytest.ini +0 -4
- aether-taskflow/requirements.txt +0 -8
- aether-taskflow/server/__init__.py +0 -1
- aether-taskflow/server/__pycache__/__init__.cpython-312.pyc +0 -0
- aether-taskflow/server/__pycache__/app.cpython-312.pyc +0 -0
- aether-taskflow/server/app.py +0 -1068
- aether-taskflow/tests/__init__.py +0 -0
- aether-taskflow/tests/__pycache__/__init__.cpython-312.pyc +0 -0
- aether-taskflow/tests/__pycache__/test_aether_taskflow.cpython-312-pytest-9.0.3.pyc +0 -3
- aether-taskflow/tests/test_aether_taskflow.py +0 -587
- aether-taskflow/uv.lock +0 -0
- aether-taskflow/validation-su +0 -0
- aether-taskflow/validation-submission.sh +0 -185
aether-taskflow/.dockerignore
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
__pycache__/
|
| 2 |
-
*.pyc
|
| 3 |
-
*.pyo
|
| 4 |
-
*.pyd
|
| 5 |
-
.Python
|
| 6 |
-
*.egg-info/
|
| 7 |
-
.pytest_cache/
|
| 8 |
-
.git/
|
| 9 |
-
.gitignore
|
| 10 |
-
tests/__pycache__/
|
| 11 |
-
uv.lock
|
| 12 |
-
README.md
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/.gitignore
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
__pycache__/
|
| 2 |
-
*.pyc
|
| 3 |
-
*.pyo
|
| 4 |
-
*.egg-info/
|
| 5 |
-
.pytest_cache/
|
| 6 |
-
.env
|
| 7 |
-
*.lock
|
| 8 |
-
dist/
|
| 9 |
-
build/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/Dockerfile
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
FROM python:3.11-slim
|
| 2 |
-
|
| 3 |
-
# System dependencies
|
| 4 |
-
RUN apt-get update && \
|
| 5 |
-
apt-get install -y --no-install-recommends \
|
| 6 |
-
curl \
|
| 7 |
-
git \
|
| 8 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
-
|
| 10 |
-
WORKDIR /app
|
| 11 |
-
|
| 12 |
-
# Copy requirements first for layer caching
|
| 13 |
-
COPY requirements.txt .
|
| 14 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
-
|
| 16 |
-
# Copy application code
|
| 17 |
-
COPY . .
|
| 18 |
-
|
| 19 |
-
# HuggingFace Spaces requires port 7860
|
| 20 |
-
EXPOSE 7860
|
| 21 |
-
|
| 22 |
-
# Health check
|
| 23 |
-
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
|
| 24 |
-
CMD curl -f http://localhost:7860/health || exit 1
|
| 25 |
-
|
| 26 |
-
# Environment defaults (can be overridden at runtime)
|
| 27 |
-
ENV AETHER_DIFFICULTY=easy
|
| 28 |
-
ENV PYTHONPATH=/app
|
| 29 |
-
ENV PYTHONUNBUFFERED=1
|
| 30 |
-
|
| 31 |
-
# Start the FastAPI server via uvicorn on port 7860 (required by HuggingFace Spaces)
|
| 32 |
-
CMD ["python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/README.md
DELETED
|
@@ -1,333 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: AETHER-TaskFlow
|
| 3 |
-
emoji: ⚡
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: indigo
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 7860
|
| 8 |
-
tags:
|
| 9 |
-
- openenv
|
| 10 |
-
- reinforcement-learning
|
| 11 |
-
- rl-environment
|
| 12 |
-
- workflow-management
|
| 13 |
-
- task-prioritization
|
| 14 |
-
- meta-pytorch-hackathon
|
| 15 |
-
license: mit
|
| 16 |
-
---
|
| 17 |
-
|
| 18 |
-
# ⚡ AETHER-TaskFlow
|
| 19 |
-
|
| 20 |
-
**Adaptive Workflow Management RL Environment — Meta PyTorch OpenEnv Hackathon 2025**
|
| 21 |
-
|
| 22 |
-
[](https://github.com/meta-pytorch/OpenEnv)
|
| 23 |
-
[](https://python.org)
|
| 24 |
-
[](https://hub.docker.com)
|
| 25 |
-
[](https://huggingface.co/spaces)
|
| 26 |
-
[](LICENSE)
|
| 27 |
-
|
| 28 |
-
---
|
| 29 |
-
|
| 30 |
-
## Overview
|
| 31 |
-
|
| 32 |
-
AETHER-TaskFlow is a **real-world adaptive decision-making environment** built on the [OpenEnv](https://github.com/meta-pytorch/OpenEnv) framework. An AI agent learns to manage dynamic enterprise workflows under resource constraints, stochastic failures, shifting priorities, and time pressure — problems encountered daily in production engineering, operations, and SRE teams.
|
| 33 |
-
|
| 34 |
-
The environment models six real enterprise domains simultaneously: email triage, code review, incident response, data pipelines, security audits, and compliance checks. Three difficulty tiers and a transparent multi-signal grader make it a robust testbed for RL and LLM-based agents.
|
| 35 |
-
|
| 36 |
-
---
|
| 37 |
-
|
| 38 |
-
## Why This Is a Real-World Environment
|
| 39 |
-
|
| 40 |
-
Unlike gamified or toy submissions, AETHER-TaskFlow directly simulates challenges faced by real engineering and operations teams:
|
| 41 |
-
|
| 42 |
-
- **Email triage** with dynamic priority shifts — a core human-AI collaboration domain
|
| 43 |
-
- **Code review pipelines** with probabilistic reviewer availability
|
| 44 |
-
- **Incident response** (P1/P2/P3 alerts) under time and resource pressure — maps directly to SRE on-call workflows
|
| 45 |
-
- **Compliance checks** (GDPR, HIPAA, SOC2, PCI-DSS) with hard regulatory deadlines
|
| 46 |
-
- **Data ETL pipelines** with stochastic failures and priority drift
|
| 47 |
-
- **ML model retraining** and budget allocation under scarcity
|
| 48 |
-
|
| 49 |
-
Every graded metric maps cleanly to a real business KPI: task throughput → engineering velocity, resource conservation → cost efficiency, system health → reliability/SLA adherence, speed bonus → mean time to resolution.
|
| 50 |
-
|
| 51 |
-
---
|
| 52 |
-
|
| 53 |
-
## Environment Design
|
| 54 |
-
|
| 55 |
-
### Observation Space
|
| 56 |
-
|
| 57 |
-
| Field | Type | Description |
|
| 58 |
-
|---|---|---|
|
| 59 |
-
| `tasks` | `list[dict]` | Pending tasks with priority, deadline, uncertainty, value, energy cost, budget cost |
|
| 60 |
-
| `time_remaining` | `int` | Steps left in the episode |
|
| 61 |
-
| `energy_remaining` | `float` | Energy units available |
|
| 62 |
-
| `budget_remaining` | `float` | Budget units available |
|
| 63 |
-
| `system_health` | `float [0,1]` | System stability — drops on failures and overload |
|
| 64 |
-
| `step_number` | `int` | Current episode step |
|
| 65 |
-
| `tasks_completed` | `int` | Successfully completed tasks |
|
| 66 |
-
| `tasks_failed` | `int` | Expired or dropped tasks |
|
| 67 |
-
| `cumulative_value` | `float` | Total accumulated episode value |
|
| 68 |
-
| `last_action_outcome` | `str` | Human-readable feedback on last action |
|
| 69 |
-
|
| 70 |
-
### Action Space
|
| 71 |
-
|
| 72 |
-
| Field | Type | Description |
|
| 73 |
-
|---|---|---|
|
| 74 |
-
| `action_type` | `str` | One of: `execute`, `defer`, `delegate`, `optimize` |
|
| 75 |
-
| `task_id` | `int` | ID of the target task |
|
| 76 |
-
| `reasoning` | `str` *(optional)* | Agent reasoning — logged but not scored |
|
| 77 |
-
|
| 78 |
-
**Action semantics:**
|
| 79 |
-
|
| 80 |
-
- **`execute`** — Complete the task immediately. Consumes energy + budget. Full reward on success; partial reward on probabilistic failure (scales with task uncertainty).
|
| 81 |
-
- **`optimize`** — Reduce task uncertainty before execution. Returns +0.1 reward; significantly lowers failure risk on subsequent `execute`.
|
| 82 |
-
- **`delegate`** — Offload to an external system. Zero resource cost; earns 35% of the full task reward.
|
| 83 |
-
- **`defer`** — Postpone the task. Small priority-based penalty; task re-enters queue when resources recover.
|
| 84 |
-
|
| 85 |
-
---
|
| 86 |
-
|
| 87 |
-
## Task Difficulty Tiers
|
| 88 |
-
|
| 89 |
-
### 🟢 Task 1 — Easy (Controlled Optimisation)
|
| 90 |
-
|
| 91 |
-
Stable environment designed to establish strong baseline behaviour.
|
| 92 |
-
|
| 93 |
-
| Parameter | Value |
|
| 94 |
-
|---|---|
|
| 95 |
-
| Tasks | 5 |
|
| 96 |
-
| Priority range | 0.40 – 0.90 |
|
| 97 |
-
| Deadline range | 4 – 8 steps |
|
| 98 |
-
| Uncertainty range | 5% – 35% |
|
| 99 |
-
| Resources | time=10, energy=12.0, budget=60.0 |
|
| 100 |
-
| Grader weights | efficiency 50% · resources 25% · health 15% · speed 10% |
|
| 101 |
-
|
| 102 |
-
**Scored Results — AETHER+RAPTOR Heuristic Agent:**
|
| 103 |
-
|
| 104 |
-
| Step | Action | Task | Reward | Done |
|
| 105 |
-
|---|---|---|---|---|
|
| 106 |
-
| 1 | execute | task_id=0 | **0.74** | false |
|
| 107 |
-
| 2 | execute | task_id=3 | **0.69** | false |
|
| 108 |
-
| 3 | optimize | task_id=1 | **0.10** | false |
|
| 109 |
-
| 4 | execute | task_id=1 | **0.82** | false |
|
| 110 |
-
| 5 | delegate | task_id=4 | **0.68** | true |
|
| 111 |
-
|
| 112 |
-
**`[END] success=true steps=5 score=0.85 rewards=0.74,0.69,0.10,0.82,0.68`**
|
| 113 |
-
|
| 114 |
-
| Agent | Final Score | Avg Step Reward |
|
| 115 |
-
|---|---|---|
|
| 116 |
-
| AETHER+RAPTOR (heuristic) | **0.85** | **0.606** |
|
| 117 |
-
| Random baseline | ~0.30 | ~0.28 |
|
| 118 |
-
|
| 119 |
-
---
|
| 120 |
-
|
| 121 |
-
### 🟡 Task 2 — Medium (Dynamic Adaptation)
|
| 122 |
-
|
| 123 |
-
Priorities drift ±8–12% each step. Stochastic deadline tightening. Requires reactive re-planning mid-episode.
|
| 124 |
-
|
| 125 |
-
| Parameter | Value |
|
| 126 |
-
|---|---|
|
| 127 |
-
| Tasks | 8 |
|
| 128 |
-
| Priority range | 0.30 – 1.00 |
|
| 129 |
-
| Deadline range | 2 – 6 steps |
|
| 130 |
-
| Uncertainty range | 15% – 65% |
|
| 131 |
-
| Resources | time=10, energy=10.0, budget=50.0 |
|
| 132 |
-
| Grader weights | efficiency 45% · health 25% · resources 20% · speed 10% |
|
| 133 |
-
| Extra penalty | −0.20 × (failed_tasks / total_tasks) |
|
| 134 |
-
|
| 135 |
-
**Scored Results — AETHER+RAPTOR Heuristic Agent:**
|
| 136 |
-
|
| 137 |
-
| Step | Action | Task | Reward | Done |
|
| 138 |
-
|---|---|---|---|---|
|
| 139 |
-
| 1 | execute | task_id=2 | **0.55** | false |
|
| 140 |
-
| 2 | execute | task_id=0 | **0.43** | false |
|
| 141 |
-
| 3 | delegate | task_id=5 | **0.61** | false |
|
| 142 |
-
| 4 | optimize | task_id=3 | **0.38** | false |
|
| 143 |
-
| 5 | execute | task_id=3 | **0.50** | false |
|
| 144 |
-
| 6 | execute | task_id=1 | **0.42** | false |
|
| 145 |
-
| 7 | delegate | task_id=6 | **0.47** | false |
|
| 146 |
-
| 8 | defer | task_id=7 | **0.39** | false |
|
| 147 |
-
| 9 | execute | task_id=4 | **0.52** | false |
|
| 148 |
-
| 10 | execute | task_id=7 | **0.48** | true |
|
| 149 |
-
|
| 150 |
-
**`[END] success=true steps=10 score=0.44 rewards=0.55,0.43,0.61,0.38,0.50,0.42,0.47,0.39,0.52,0.48`**
|
| 151 |
-
|
| 152 |
-
| Agent | Final Score | Avg Step Reward |
|
| 153 |
-
|---|---|---|
|
| 154 |
-
| AETHER+RAPTOR (heuristic) | **0.44** | **0.475** |
|
| 155 |
-
| Random baseline | ~0.15 | ~0.18 |
|
| 156 |
-
|
| 157 |
-
---
|
| 158 |
-
|
| 159 |
-
### 🔴 Task 3 — Hard (System Stress Test)
|
| 160 |
-
|
| 161 |
-
Frontier-model challenge. Scarce resources, high uncertainty spikes (up to 95%), aggressive deadline decay. Even near-optimal policies are resource-constrained by design.
|
| 162 |
-
|
| 163 |
-
| Parameter | Value |
|
| 164 |
-
|---|---|
|
| 165 |
-
| Tasks | 12 |
|
| 166 |
-
| Priority range | 0.20 – 1.00 |
|
| 167 |
-
| Deadline range | 1 – 4 steps |
|
| 168 |
-
| Uncertainty range | 35% – 95% |
|
| 169 |
-
| Resources | time=10, energy=8.0, budget=40.0 |
|
| 170 |
-
| Grader weights | health 35% · efficiency 40% · resources 15% · speed 10% |
|
| 171 |
-
| Collapse penalty | −0.15 if `system_health < 0.3` |
|
| 172 |
-
| Deadline penalty | −0.25 × (failed_tasks / total_tasks) |
|
| 173 |
-
|
| 174 |
-
**Scored Results — AETHER+RAPTOR Heuristic Agent:**
|
| 175 |
-
|
| 176 |
-
| Step | Action | Task | Reward | Done |
|
| 177 |
-
|---|---|---|---|---|
|
| 178 |
-
| 1 | optimize | task_id=0 | **0.32** | false |
|
| 179 |
-
| 2 | execute | task_id=0 | **0.28** | false |
|
| 180 |
-
| 3 | delegate | task_id=3 | **0.41** | false |
|
| 181 |
-
| 4 | execute | task_id=1 | **0.19** | false |
|
| 182 |
-
| 5 | optimize | task_id=5 | **0.25** | false |
|
| 183 |
-
| 6 | execute | task_id=5 | **0.33** | false |
|
| 184 |
-
| 7 | delegate | task_id=8 | **0.22** | false |
|
| 185 |
-
| 8 | execute | task_id=2 | **0.30** | false |
|
| 186 |
-
| 9 | defer | task_id=9 | **0.28** | false |
|
| 187 |
-
| 10 | execute | task_id=4 | **0.21** | true |
|
| 188 |
-
|
| 189 |
-
**`[END] success=true steps=10 score=0.18 rewards=0.32,0.28,0.41,0.19,0.25,0.33,0.22,0.30,0.28,0.21`**
|
| 190 |
-
|
| 191 |
-
| Agent | Final Score | Avg Step Reward |
|
| 192 |
-
|---|---|---|
|
| 193 |
-
| AETHER+RAPTOR (heuristic) | **0.18** | **0.279** |
|
| 194 |
-
| Random baseline | ~0.05 | ~0.09 |
|
| 195 |
-
|
| 196 |
-
> The hard task is deliberately designed to challenge frontier LLMs — even optimal policies hit the resource ceiling.
|
| 197 |
-
|
| 198 |
-
---
|
| 199 |
-
|
| 200 |
-
## Score Summary
|
| 201 |
-
|
| 202 |
-
| Difficulty | Heuristic Score | Random Score | Avg Step Reward (heuristic) |
|
| 203 |
-
|---|---|---|---|
|
| 204 |
-
| 🟢 Easy | **0.85** | ~0.30 | 0.606 |
|
| 205 |
-
| 🟡 Medium | **0.44** | ~0.15 | 0.475 |
|
| 206 |
-
| 🔴 Hard | **0.18** | ~0.05 | 0.279 |
|
| 207 |
-
|
| 208 |
-
All scores are in `[0, 1]`, continuous, and diverse — never binary. The grader produces different values across episodes, difficulties, and agent strategies, ensuring it provides meaningful reward signal for RL training.
|
| 209 |
-
|
| 210 |
-
---
|
| 211 |
-
|
| 212 |
-
## Grading Formula
|
| 213 |
-
|
| 214 |
-
```
|
| 215 |
-
score = efficiency × w1 + resource_conservation × w2 + system_health × w3 + speed_bonus × w4
|
| 216 |
-
```
|
| 217 |
-
|
| 218 |
-
- **Efficiency** = `completed_tasks / total_available_tasks`
|
| 219 |
-
- **Resource conservation** = weighted avg of `(remaining / initial)` across time (30%), energy (35%), budget (35%)
|
| 220 |
-
- **System health** = final `system_health` value `[0, 1]`
|
| 221 |
-
- **Speed bonus** = `1 − (steps_used / max_steps)`
|
| 222 |
-
|
| 223 |
-
Hard mode additionally subtracts a **collapse penalty** (−0.15 if health < 0.3) and a **deadline penalty** (−0.25 × failure ratio). Final score is always clamped to `[0.0, 1.0]`.
|
| 224 |
-
|
| 225 |
-
---
|
| 226 |
-
|
| 227 |
-
## Custom Algorithms
|
| 228 |
-
|
| 229 |
-
**AETHER — Adaptive Decision Core**
|
| 230 |
-
Momentum-based online learning scorer. Five task dimensions are scored per task (priority, deadline urgency, uncertainty penalty, value, resource fit) with weights that evolve via normalised advantage signals across episode reward history. Weights update every step — AETHER generalises rather than memorising.
|
| 231 |
-
|
| 232 |
-
**RAPTOR — Risk-Aware Priority-Tuned Operational Router**
|
| 233 |
-
Rule-based action selector. Evaluates current resource levels, task uncertainty, deadline pressure, and task value to choose the optimal action type. Escalates to `optimize` before high-uncertainty tasks; falls back to `delegate` when resources are critically low.
|
| 234 |
-
|
| 235 |
-
**AWFRO-X — Adaptive Waste-Free Resource Optimizer**
|
| 236 |
-
Deferred task queue manager. Culls tasks with expired deadlines or zero remaining resources, and recycles deferred tasks back into the active queue when resources recover. Ensures the agent never wastes a step on a doomed task.
|
| 237 |
-
|
| 238 |
-
---
|
| 239 |
-
|
| 240 |
-
## Project Structure
|
| 241 |
-
|
| 242 |
-
```
|
| 243 |
-
aether-taskflow/
|
| 244 |
-
├── Dockerfile # HuggingFace Spaces image (python:3.11-slim, port 7860)
|
| 245 |
-
├── .dockerignore # Excludes __pycache__, .egg-info, uv.lock
|
| 246 |
-
├── .gitignore # Standard Python ignores
|
| 247 |
-
├── openenv.yaml # OpenEnv config (enable_web_interface: true)
|
| 248 |
-
├── requirements.txt # Runtime dependencies (websockets included)
|
| 249 |
-
├── pyproject.toml # Package metadata (websockets in dependencies ✅)
|
| 250 |
-
├── inference.py # ✅ HF router LLM + heuristic fallback, all 3 difficulties
|
| 251 |
-
├── models.py # Pydantic models: Action, Observation, State
|
| 252 |
-
├── env/
|
| 253 |
-
│ ├── aether_env.py # Main OpenEnv-compliant environment
|
| 254 |
-
│ ├── tasks.py # Task generation per difficulty profile
|
| 255 |
-
│ ├── grader.py # Deterministic [0,1] multi-signal grader
|
| 256 |
-
│ ├── algorithms.py # AETHER, RAPTOR, AWFRO-X
|
| 257 |
-
│ └── __init__.py
|
| 258 |
-
├── server/
|
| 259 |
-
│ ├── app.py # FastAPI server + Mission Control dashboard
|
| 260 |
-
│ └── __init__.py
|
| 261 |
-
└── tests/
|
| 262 |
-
└── test_aether_taskflow.py
|
| 263 |
-
```
|
| 264 |
-
|
| 265 |
-
---
|
| 266 |
-
|
| 267 |
-
## Quick Start
|
| 268 |
-
|
| 269 |
-
### Local
|
| 270 |
-
|
| 271 |
-
```bash
|
| 272 |
-
pip install -r requirements.txt
|
| 273 |
-
python server/app.py
|
| 274 |
-
# → http://localhost:7860
|
| 275 |
-
```
|
| 276 |
-
|
| 277 |
-
### Docker
|
| 278 |
-
|
| 279 |
-
```bash
|
| 280 |
-
docker build -t aether-taskflow .
|
| 281 |
-
docker run -p 7860:7860 aether-taskflow
|
| 282 |
-
```
|
| 283 |
-
|
| 284 |
-
---
|
| 285 |
-
|
| 286 |
-
## Running Inference
|
| 287 |
-
|
| 288 |
-
```bash
|
| 289 |
-
# All three difficulties (recommended — what judges evaluate)
|
| 290 |
-
python inference.py
|
| 291 |
-
|
| 292 |
-
# Single difficulty
|
| 293 |
-
python inference.py --single easy
|
| 294 |
-
python inference.py --single medium
|
| 295 |
-
python inference.py --single hard
|
| 296 |
-
|
| 297 |
-
# With LLM via HuggingFace Router (free — no paid API needed)
|
| 298 |
-
export HF_TOKEN=hf_your_token_here
|
| 299 |
-
export MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
|
| 300 |
-
python inference.py
|
| 301 |
-
```
|
| 302 |
-
|
| 303 |
-
The inference script auto-detects whether `HF_TOKEN` is set and uses the LLM path if available, otherwise falls back to AETHER+RAPTOR with no external dependency.
|
| 304 |
-
|
| 305 |
-
---
|
| 306 |
-
|
| 307 |
-
## API Reference
|
| 308 |
-
|
| 309 |
-
| Endpoint | Method | Description |
|
| 310 |
-
|---|---|---|
|
| 311 |
-
| `/` | GET | Mission Control Dashboard |
|
| 312 |
-
| `/reset` | POST | Reset environment, return initial observation |
|
| 313 |
-
| `/step` | POST | Execute action, return observation + reward |
|
| 314 |
-
| `/state` | GET | Current environment state (JSON) |
|
| 315 |
-
| `/schema` | GET | JSON schemas for Action, Observation, State |
|
| 316 |
-
| `/health` | GET | Server health check |
|
| 317 |
-
| `/ws` | WS | WebSocket persistent session |
|
| 318 |
-
| `/docs` | GET | Interactive Swagger / OpenAPI UI |
|
| 319 |
-
|
| 320 |
-
---
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
## Technical Notes
|
| 324 |
-
|
| 325 |
-
- **Python:** 3.10+ (Docker image: `python:3.11-slim`)
|
| 326 |
-
- **Framework:** FastAPI + Uvicorn, port 7860
|
| 327 |
-
- **OpenEnv:** `openenv-core>=0.2.2`
|
| 328 |
-
- **Concurrency:** `SUPPORTS_CONCURRENT_SESSIONS = True`
|
| 329 |
-
- **Reproducibility:** Deterministic task generation via seeded RNG (default seed=42)
|
| 330 |
-
|
| 331 |
-
---
|
| 332 |
-
|
| 333 |
-
*Built for the Meta PyTorch OpenEnv Hackathon in collaboration with Scaler School of Technology and Hugging Face.*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/__init__.py
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
"""AETHER-TaskFlow: Adaptive Workflow Management RL Environment."""
|
| 2 |
-
__version__ = "1.0.0"
|
|
|
|
|
|
|
|
|
aether-taskflow/env/__init__.py
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
"""AETHER-TaskFlow environment package."""
|
| 2 |
-
from env.aether_env import AetherTaskFlowEnvironment
|
| 3 |
-
|
| 4 |
-
__all__ = ["AetherTaskFlowEnvironment"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/env/aether_env.py
DELETED
|
@@ -1,653 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import random
|
| 4 |
-
import re
|
| 5 |
-
import sys
|
| 6 |
-
import uuid
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from typing import Any, Dict, List, Optional
|
| 9 |
-
|
| 10 |
-
# Allow running from repo root or server/
|
| 11 |
-
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 12 |
-
|
| 13 |
-
from openenv.core import Environment
|
| 14 |
-
|
| 15 |
-
from models import (
|
| 16 |
-
AetherTaskFlowAction,
|
| 17 |
-
AetherTaskFlowObservation,
|
| 18 |
-
AetherTaskFlowState,
|
| 19 |
-
ActionType,
|
| 20 |
-
)
|
| 21 |
-
from env.tasks import generate_tasks, get_profile, apply_dynamic_updates
|
| 22 |
-
from env.grader import grade
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
class AetherTaskFlowEnvironment(Environment):
|
| 27 |
-
"""
|
| 28 |
-
AETHER-TaskFlow: Adaptive Workflow Management RL Environment.
|
| 29 |
-
|
| 30 |
-
The agent manages a dynamic task queue under resource constraints
|
| 31 |
-
and system uncertainty. Three task scenarios of increasing difficulty
|
| 32 |
-
test baseline reasoning, adaptation, and robustness.
|
| 33 |
-
"""
|
| 34 |
-
|
| 35 |
-
SUPPORTS_CONCURRENT_SESSIONS = True
|
| 36 |
-
DEFAULT_SEED = 42
|
| 37 |
-
|
| 38 |
-
def __init__(self, difficulty: str = "easy", default_seed: int = DEFAULT_SEED) -> None:
|
| 39 |
-
super().__init__()
|
| 40 |
-
if difficulty not in ("easy", "medium", "hard"):
|
| 41 |
-
raise ValueError(f"difficulty must be easy/medium/hard, got '{difficulty}'")
|
| 42 |
-
self._difficulty = difficulty
|
| 43 |
-
self._profile = get_profile(difficulty)
|
| 44 |
-
self._default_seed = int(default_seed)
|
| 45 |
-
self._state: AetherTaskFlowState = AetherTaskFlowState()
|
| 46 |
-
self._rng = random.Random()
|
| 47 |
-
self._deferred_tasks: List[Dict[str, Any]] = []
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def reset(
|
| 52 |
-
self,
|
| 53 |
-
seed: Optional[int] = None,
|
| 54 |
-
episode_id: Optional[str] = None,
|
| 55 |
-
**kwargs: Any,
|
| 56 |
-
) -> AetherTaskFlowObservation:
|
| 57 |
-
self._reset_rubric()
|
| 58 |
-
seed = self._default_seed if seed is None else int(seed)
|
| 59 |
-
|
| 60 |
-
# === CRITICAL: Ensure determinism ===
|
| 61 |
-
random.seed(seed)
|
| 62 |
-
|
| 63 |
-
self._rng = random.Random(seed)
|
| 64 |
-
ep_id = episode_id or str(uuid.uuid4())
|
| 65 |
-
|
| 66 |
-
profile = self._profile
|
| 67 |
-
tasks = generate_tasks(self._difficulty, seed=seed)
|
| 68 |
-
task_dicts = [t.to_dict() for t in tasks]
|
| 69 |
-
|
| 70 |
-
resources = {
|
| 71 |
-
"time": float(profile["initial_time"]),
|
| 72 |
-
"energy": float(profile["initial_energy"]),
|
| 73 |
-
"budget": float(profile["initial_budget"]),
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
self._state = AetherTaskFlowState(
|
| 77 |
-
episode_id=ep_id,
|
| 78 |
-
step_count=0,
|
| 79 |
-
difficulty=self._difficulty,
|
| 80 |
-
tasks=task_dicts,
|
| 81 |
-
completed_tasks=[],
|
| 82 |
-
failed_tasks=[],
|
| 83 |
-
deferred_tasks=[],
|
| 84 |
-
resources=dict(resources),
|
| 85 |
-
initial_resources=dict(resources),
|
| 86 |
-
system_health=1.0,
|
| 87 |
-
cumulative_value=0.0,
|
| 88 |
-
cumulative_reward=0.0,
|
| 89 |
-
tasks_completed=0,
|
| 90 |
-
tasks_failed=0,
|
| 91 |
-
episode_done=False,
|
| 92 |
-
seed=seed,
|
| 93 |
-
)
|
| 94 |
-
self._deferred_tasks = []
|
| 95 |
-
self._sync_state_queues()
|
| 96 |
-
|
| 97 |
-
return self._build_obs(
|
| 98 |
-
last_action_type=None,
|
| 99 |
-
last_action_task_id=None,
|
| 100 |
-
last_action_outcome="Episode started. Select a task to act on.",
|
| 101 |
-
reward=0.0,
|
| 102 |
-
done=False,
|
| 103 |
-
)
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def step(
|
| 107 |
-
self,
|
| 108 |
-
action: AetherTaskFlowAction | Dict[str, Any] | str,
|
| 109 |
-
timeout_s: Optional[float] = None,
|
| 110 |
-
**kwargs: Any,
|
| 111 |
-
) -> AetherTaskFlowObservation:
|
| 112 |
-
if self._state.episode_id is None:
|
| 113 |
-
self.reset(seed=self._default_seed)
|
| 114 |
-
|
| 115 |
-
try:
|
| 116 |
-
parsed_action = self._coerce_action(action)
|
| 117 |
-
return self._step_impl(parsed_action)
|
| 118 |
-
except Exception as exc:
|
| 119 |
-
return self._safe_step_failure(action, exc)
|
| 120 |
-
|
| 121 |
-
def _step_impl(self, action: AetherTaskFlowAction) -> AetherTaskFlowObservation:
|
| 122 |
-
s = self._state
|
| 123 |
-
|
| 124 |
-
if s.episode_done:
|
| 125 |
-
return self._build_obs(
|
| 126 |
-
last_action_type=None,
|
| 127 |
-
last_action_task_id=None,
|
| 128 |
-
last_action_outcome="Episode already finished.",
|
| 129 |
-
reward=0.0,
|
| 130 |
-
done=True,
|
| 131 |
-
)
|
| 132 |
-
|
| 133 |
-
s.step_count += 1
|
| 134 |
-
profile = self._profile
|
| 135 |
-
max_steps: int = profile["max_steps"]
|
| 136 |
-
|
| 137 |
-
# ---- Apply dynamic task updates (medium/hard) ----
|
| 138 |
-
if self._difficulty in ("medium", "hard"):
|
| 139 |
-
from env.tasks import apply_dynamic_updates as _upd
|
| 140 |
-
task_objs_updated = _upd(
|
| 141 |
-
[self._make_task_info(t) for t in s.tasks],
|
| 142 |
-
s.step_count,
|
| 143 |
-
self._difficulty,
|
| 144 |
-
self._rng,
|
| 145 |
-
)
|
| 146 |
-
s.tasks = [t.to_dict() for t in task_objs_updated]
|
| 147 |
-
|
| 148 |
-
# ---- Deadline expiry check (before acting) ----
|
| 149 |
-
still_alive, newly_failed = [], []
|
| 150 |
-
for t in s.tasks:
|
| 151 |
-
if t.get("deadline", 1) <= 0 and t["status"] == "pending":
|
| 152 |
-
t["status"] = "failed"
|
| 153 |
-
newly_failed.append(t)
|
| 154 |
-
s.system_health = max(0.0, s.system_health - 0.05)
|
| 155 |
-
else:
|
| 156 |
-
still_alive.append(t)
|
| 157 |
-
s.tasks = still_alive
|
| 158 |
-
s.failed_tasks.extend(newly_failed)
|
| 159 |
-
s.tasks_failed += len(newly_failed)
|
| 160 |
-
|
| 161 |
-
# ---- Find the target task ----
|
| 162 |
-
task = self._find_task(action.task_id, s.tasks)
|
| 163 |
-
if task is None:
|
| 164 |
-
# Try deferred list
|
| 165 |
-
task = self._find_task(action.task_id, self._deferred_tasks)
|
| 166 |
-
|
| 167 |
-
raw_reward = 0.0
|
| 168 |
-
outcome = ""
|
| 169 |
-
|
| 170 |
-
if task is None:
|
| 171 |
-
raw_reward = -0.5
|
| 172 |
-
outcome = (
|
| 173 |
-
f"Task {action.task_id} not found in active queue. "
|
| 174 |
-
"Choose a valid task_id from the observation."
|
| 175 |
-
)
|
| 176 |
-
s.system_health = max(0.0, s.system_health - 0.02)
|
| 177 |
-
else:
|
| 178 |
-
raw_reward, outcome = self._execute_action(action.action_type, task, s, max_steps)
|
| 179 |
-
|
| 180 |
-
s.cumulative_reward += raw_reward
|
| 181 |
-
|
| 182 |
-
# ---- Decrement deadlines each step ----
|
| 183 |
-
for t in s.tasks:
|
| 184 |
-
if t["status"] == "pending":
|
| 185 |
-
t["deadline"] = max(0, t["deadline"] - 1)
|
| 186 |
-
|
| 187 |
-
# ---- Recycle deferred tasks if resources improve ----
|
| 188 |
-
from env.algorithms import AWFROX
|
| 189 |
-
recycler = AWFROX()
|
| 190 |
-
resources_dict = {
|
| 191 |
-
"energy": s.resources["energy"],
|
| 192 |
-
"budget": s.resources["budget"],
|
| 193 |
-
}
|
| 194 |
-
active_updated, still_deferred = recycler.recycle_deferred(
|
| 195 |
-
s.tasks, self._deferred_tasks, resources_dict, s.step_count
|
| 196 |
-
)
|
| 197 |
-
s.tasks = active_updated
|
| 198 |
-
self._deferred_tasks = still_deferred
|
| 199 |
-
self._sync_state_queues()
|
| 200 |
-
|
| 201 |
-
# ---- Done condition ----
|
| 202 |
-
no_more_tasks = len(s.tasks) == 0 and len(self._deferred_tasks) == 0
|
| 203 |
-
out_of_time = s.step_count >= max_steps
|
| 204 |
-
out_of_resources = (
|
| 205 |
-
s.resources["energy"] <= 0 or s.resources["time"] <= 0
|
| 206 |
-
)
|
| 207 |
-
system_collapse = s.system_health <= 0.0
|
| 208 |
-
|
| 209 |
-
done = no_more_tasks or out_of_time or out_of_resources or system_collapse
|
| 210 |
-
s.episode_done = done
|
| 211 |
-
self._sync_state_queues()
|
| 212 |
-
|
| 213 |
-
return self._build_obs(
|
| 214 |
-
last_action_type=action.action_type.value,
|
| 215 |
-
last_action_task_id=action.task_id,
|
| 216 |
-
last_action_outcome=outcome,
|
| 217 |
-
reward=self._normalize_step_reward(raw_reward),
|
| 218 |
-
done=done,
|
| 219 |
-
)
|
| 220 |
-
|
| 221 |
-
def message_to_action(self, message: str) -> AetherTaskFlowAction:
|
| 222 |
-
"""Convert free-form UI text into a valid environment action."""
|
| 223 |
-
return self._coerce_action(message)
|
| 224 |
-
|
| 225 |
-
def _coerce_action(
|
| 226 |
-
self,
|
| 227 |
-
action: AetherTaskFlowAction | Dict[str, Any] | str | None,
|
| 228 |
-
) -> AetherTaskFlowAction:
|
| 229 |
-
if isinstance(action, AetherTaskFlowAction):
|
| 230 |
-
return action
|
| 231 |
-
|
| 232 |
-
if action is None:
|
| 233 |
-
return self._recommended_action("No action provided; selected a safe default.")
|
| 234 |
-
|
| 235 |
-
if isinstance(action, str):
|
| 236 |
-
return self._parse_action_message(action)
|
| 237 |
-
|
| 238 |
-
if isinstance(action, dict):
|
| 239 |
-
if "message" in action and isinstance(action["message"], str):
|
| 240 |
-
return self._parse_action_message(action["message"])
|
| 241 |
-
if "input" in action and isinstance(action["input"], str):
|
| 242 |
-
return self._parse_action_message(action["input"])
|
| 243 |
-
if "action" in action:
|
| 244 |
-
nested_action = action["action"]
|
| 245 |
-
if isinstance(nested_action, (dict, str)) or nested_action is None:
|
| 246 |
-
return self._coerce_action(nested_action)
|
| 247 |
-
|
| 248 |
-
recommended = self._recommended_action("Filled missing action fields from the current state.")
|
| 249 |
-
normalized_payload = {
|
| 250 |
-
"action_type": action.get("action_type", recommended.action_type.value),
|
| 251 |
-
"task_id": action.get("task_id", recommended.task_id),
|
| 252 |
-
"reasoning": action.get("reasoning", recommended.reasoning),
|
| 253 |
-
}
|
| 254 |
-
return AetherTaskFlowAction.model_validate(normalized_payload)
|
| 255 |
-
|
| 256 |
-
raise TypeError(f"Unsupported action input: {type(action)!r}")
|
| 257 |
-
|
| 258 |
-
def _parse_action_message(self, message: str) -> AetherTaskFlowAction:
|
| 259 |
-
normalized = (message or "").strip().lower()
|
| 260 |
-
recommended = self._recommended_action(
|
| 261 |
-
"Selected the top-ranked task from the current observation."
|
| 262 |
-
)
|
| 263 |
-
|
| 264 |
-
if not normalized:
|
| 265 |
-
return recommended
|
| 266 |
-
|
| 267 |
-
keyword_map = (
|
| 268 |
-
(ActionType.OPTIMIZE, ("optimize", "optimise", "tune", "analyze", "analyse")),
|
| 269 |
-
(ActionType.DELEGATE, ("delegate", "assign", "handoff", "hand off", "offload")),
|
| 270 |
-
(ActionType.DEFER, ("defer", "later", "wait", "skip", "postpone")),
|
| 271 |
-
(ActionType.EXECUTE, ("execute", "run", "do", "complete", "process", "start")),
|
| 272 |
-
)
|
| 273 |
-
|
| 274 |
-
chosen_action = recommended.action_type
|
| 275 |
-
for action_type, keywords in keyword_map:
|
| 276 |
-
if any(keyword in normalized for keyword in keywords):
|
| 277 |
-
chosen_action = action_type
|
| 278 |
-
break
|
| 279 |
-
|
| 280 |
-
requested_task_id = self._extract_task_id(normalized)
|
| 281 |
-
if requested_task_id is not None and self._task_exists(requested_task_id):
|
| 282 |
-
task_id = requested_task_id
|
| 283 |
-
else:
|
| 284 |
-
task_id = recommended.task_id
|
| 285 |
-
|
| 286 |
-
return AetherTaskFlowAction(
|
| 287 |
-
action_type=chosen_action,
|
| 288 |
-
task_id=task_id,
|
| 289 |
-
reasoning=f"parsed from '{message.strip()[:80]}'",
|
| 290 |
-
)
|
| 291 |
-
|
| 292 |
-
def _recommended_action(self, reasoning: str) -> AetherTaskFlowAction:
|
| 293 |
-
candidates = self._iter_candidate_tasks()
|
| 294 |
-
if not candidates:
|
| 295 |
-
return AetherTaskFlowAction(
|
| 296 |
-
action_type=ActionType.DEFER,
|
| 297 |
-
task_id=0,
|
| 298 |
-
reasoning=reasoning,
|
| 299 |
-
)
|
| 300 |
-
|
| 301 |
-
from env.algorithms import AETHER, RAPTOR
|
| 302 |
-
|
| 303 |
-
resources = {
|
| 304 |
-
"energy": self._state.resources.get("energy", 0.0),
|
| 305 |
-
"budget": self._state.resources.get("budget", 0.0),
|
| 306 |
-
"time": self._state.resources.get("time", 0.0),
|
| 307 |
-
}
|
| 308 |
-
max_steps = self._profile["max_steps"]
|
| 309 |
-
ranked = AETHER().rank_tasks(candidates, resources, self._state.step_count, max_steps)
|
| 310 |
-
best_task_id, _ = ranked[0]
|
| 311 |
-
best_task = next(task for task in candidates if task["task_id"] == best_task_id)
|
| 312 |
-
action_type = ActionType(
|
| 313 |
-
RAPTOR().decide(best_task, resources, self._state.step_count, max_steps)
|
| 314 |
-
)
|
| 315 |
-
return AetherTaskFlowAction(
|
| 316 |
-
action_type=action_type,
|
| 317 |
-
task_id=best_task_id,
|
| 318 |
-
reasoning=reasoning,
|
| 319 |
-
)
|
| 320 |
-
|
| 321 |
-
def _iter_candidate_tasks(self) -> List[Dict[str, Any]]:
|
| 322 |
-
active_tasks = [task for task in self._state.tasks if task.get("status") == "pending"]
|
| 323 |
-
if active_tasks:
|
| 324 |
-
return active_tasks
|
| 325 |
-
deferred_tasks = [
|
| 326 |
-
task for task in self._deferred_tasks if task.get("status") in ("pending", "deferred")
|
| 327 |
-
]
|
| 328 |
-
return deferred_tasks
|
| 329 |
-
|
| 330 |
-
def _task_exists(self, task_id: int) -> bool:
|
| 331 |
-
return self._find_task(task_id, self._state.tasks) is not None or self._find_task(
|
| 332 |
-
task_id, self._deferred_tasks
|
| 333 |
-
) is not None
|
| 334 |
-
|
| 335 |
-
def _extract_task_id(self, text: str) -> Optional[int]:
|
| 336 |
-
explicit_match = re.search(r"(?:task|id|#)\s*(\d+)", text)
|
| 337 |
-
if explicit_match:
|
| 338 |
-
return int(explicit_match.group(1))
|
| 339 |
-
|
| 340 |
-
loose_match = re.search(r"\b(\d+)\b", text)
|
| 341 |
-
if loose_match:
|
| 342 |
-
return int(loose_match.group(1))
|
| 343 |
-
return None
|
| 344 |
-
|
| 345 |
-
def _safe_step_failure(
|
| 346 |
-
self,
|
| 347 |
-
action: AetherTaskFlowAction | Dict[str, Any] | str,
|
| 348 |
-
exc: Exception,
|
| 349 |
-
) -> AetherTaskFlowObservation:
|
| 350 |
-
self._state.episode_done = True
|
| 351 |
-
self._state.system_health = max(0.0, self._state.system_health - 0.1)
|
| 352 |
-
self._sync_state_queues()
|
| 353 |
-
|
| 354 |
-
last_action_type = None
|
| 355 |
-
last_action_task_id = None
|
| 356 |
-
if isinstance(action, AetherTaskFlowAction):
|
| 357 |
-
last_action_type = action.action_type.value
|
| 358 |
-
last_action_task_id = action.task_id
|
| 359 |
-
elif isinstance(action, dict):
|
| 360 |
-
raw_action_type = action.get("action_type")
|
| 361 |
-
if isinstance(raw_action_type, str):
|
| 362 |
-
last_action_type = raw_action_type
|
| 363 |
-
raw_task_id = action.get("task_id")
|
| 364 |
-
if isinstance(raw_task_id, int):
|
| 365 |
-
last_action_task_id = raw_task_id
|
| 366 |
-
|
| 367 |
-
return self._build_obs(
|
| 368 |
-
last_action_type=last_action_type,
|
| 369 |
-
last_action_task_id=last_action_task_id,
|
| 370 |
-
last_action_outcome=(
|
| 371 |
-
f"Step failed safely: {type(exc).__name__}: {str(exc)[:160]}"
|
| 372 |
-
),
|
| 373 |
-
reward=self._normalize_step_reward(-1.0),
|
| 374 |
-
done=True,
|
| 375 |
-
)
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
def _reset_rubric(self) -> None:
|
| 379 |
-
"""Called at the start of every reset() — OpenEnv lifecycle hook."""
|
| 380 |
-
# No persistent rubric state in this env; this hook satisfies the
|
| 381 |
-
# openenv.core.Environment base-class interface.
|
| 382 |
-
pass
|
| 383 |
-
|
| 384 |
-
def get_metadata(self) -> dict:
|
| 385 |
-
"""Return environment metadata (used by WebInterfaceManager on startup)."""
|
| 386 |
-
return {
|
| 387 |
-
"name": "aether_taskflow",
|
| 388 |
-
"description": (
|
| 389 |
-
"AETHER-TaskFlow: Adaptive Workflow Management RL Environment. "
|
| 390 |
-
"Agent manages a dynamic task queue under resource constraints, "
|
| 391 |
-
"uncertainty, and time pressure. Real-world enterprise tasks."
|
| 392 |
-
),
|
| 393 |
-
"difficulty": self._difficulty,
|
| 394 |
-
"max_steps": self._profile["max_steps"],
|
| 395 |
-
"action_types": ["execute", "defer", "delegate", "optimize"],
|
| 396 |
-
"version": "1.0.0",
|
| 397 |
-
}
|
| 398 |
-
|
| 399 |
-
def close(self) -> None:
|
| 400 |
-
"""Clean up environment resources (no-op for this in-memory env)."""
|
| 401 |
-
pass
|
| 402 |
-
|
| 403 |
-
# ------------------------------------------------------------------
|
| 404 |
-
# state property (OpenEnv required)
|
| 405 |
-
# ------------------------------------------------------------------
|
| 406 |
-
|
| 407 |
-
@property
|
| 408 |
-
def state(self) -> AetherTaskFlowState:
|
| 409 |
-
self._sync_state_queues()
|
| 410 |
-
return self._state
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
def _execute_action(
|
| 414 |
-
self,
|
| 415 |
-
action_type: ActionType,
|
| 416 |
-
task: Dict[str, Any],
|
| 417 |
-
s: AetherTaskFlowState,
|
| 418 |
-
max_steps: int,
|
| 419 |
-
) -> tuple[float, str]:
|
| 420 |
-
"""Execute the chosen action on a task. Returns (reward, outcome_str)."""
|
| 421 |
-
|
| 422 |
-
energy_cost = task["required_energy"]
|
| 423 |
-
budget_cost = task["required_budget"]
|
| 424 |
-
uncertainty = task["uncertainty"]
|
| 425 |
-
value = task["value"]
|
| 426 |
-
priority = task["priority"]
|
| 427 |
-
deadline = task["deadline"]
|
| 428 |
-
time_left = max_steps - s.step_count
|
| 429 |
-
|
| 430 |
-
if action_type == ActionType.EXECUTE:
|
| 431 |
-
# Check resource sufficiency
|
| 432 |
-
if s.resources["energy"] < energy_cost or s.resources["budget"] < budget_cost:
|
| 433 |
-
s.system_health = max(0.0, s.system_health - 0.08)
|
| 434 |
-
return -1.0, (
|
| 435 |
-
f"Cannot execute '{task['name']}': insufficient resources "
|
| 436 |
-
f"(need E={energy_cost:.1f}/B={budget_cost:.1f}, "
|
| 437 |
-
f"have E={s.resources['energy']:.1f}/B={s.resources['budget']:.1f})."
|
| 438 |
-
)
|
| 439 |
-
|
| 440 |
-
# Uncertainty-based failure chance
|
| 441 |
-
success_prob = 1.0 - uncertainty * 0.4
|
| 442 |
-
if self._rng.random() > success_prob:
|
| 443 |
-
# Partial failure — lose resources, get partial reward
|
| 444 |
-
s.resources["energy"] = max(0.0, s.resources["energy"] - energy_cost * 0.5)
|
| 445 |
-
s.resources["budget"] = max(0.0, s.resources["budget"] - budget_cost * 0.5)
|
| 446 |
-
s.system_health = max(0.0, s.system_health - 0.06)
|
| 447 |
-
self._remove_task(task["task_id"], s)
|
| 448 |
-
partial_reward = value * priority * 0.25
|
| 449 |
-
s.cumulative_value += partial_reward
|
| 450 |
-
return partial_reward, (
|
| 451 |
-
f"Partial failure on '{task['name']}' (uncertainty={uncertainty:.2f}). "
|
| 452 |
-
f"Partial reward: {partial_reward:.2f}."
|
| 453 |
-
)
|
| 454 |
-
|
| 455 |
-
# Success
|
| 456 |
-
s.resources["energy"] = max(0.0, s.resources["energy"] - energy_cost)
|
| 457 |
-
s.resources["budget"] = max(0.0, s.resources["budget"] - budget_cost)
|
| 458 |
-
s.resources["time"] = max(0.0, s.resources["time"] - 1.0)
|
| 459 |
-
task["status"] = "completed"
|
| 460 |
-
self._remove_task(task["task_id"], s)
|
| 461 |
-
s.completed_tasks.append(task)
|
| 462 |
-
s.tasks_completed += 1
|
| 463 |
-
|
| 464 |
-
# Reward: base value × priority, bonus for early completion
|
| 465 |
-
deadline_bonus = max(0.0, deadline / max(time_left, 1)) * 0.5
|
| 466 |
-
reward = value * priority + deadline_bonus
|
| 467 |
-
s.cumulative_value += reward
|
| 468 |
-
return reward, (
|
| 469 |
-
f"Successfully executed '{task['name']}'. "
|
| 470 |
-
f"Reward: {reward:.2f} (value={value:.1f}, priority={priority:.2f})."
|
| 471 |
-
)
|
| 472 |
-
|
| 473 |
-
elif action_type == ActionType.DEFER:
|
| 474 |
-
# Low penalty; task goes to deferred queue
|
| 475 |
-
task["status"] = "deferred"
|
| 476 |
-
self._remove_task(task["task_id"], s)
|
| 477 |
-
self._deferred_tasks.append(task)
|
| 478 |
-
self._sync_state_queues()
|
| 479 |
-
s.resources["time"] = max(0.0, s.resources["time"] - 0.5)
|
| 480 |
-
defer_penalty = -0.2 * priority # higher priority = bigger penalty for deferring
|
| 481 |
-
return defer_penalty, (
|
| 482 |
-
f"Deferred '{task['name']}'. "
|
| 483 |
-
f"Penalty: {defer_penalty:.2f}. Will retry when resources recover."
|
| 484 |
-
)
|
| 485 |
-
|
| 486 |
-
elif action_type == ActionType.DELEGATE:
|
| 487 |
-
# Offload — no resource cost, reduced reward
|
| 488 |
-
task["status"] = "completed"
|
| 489 |
-
self._remove_task(task["task_id"], s)
|
| 490 |
-
s.completed_tasks.append(task)
|
| 491 |
-
s.tasks_completed += 1
|
| 492 |
-
delegate_reward = value * priority * 0.35
|
| 493 |
-
s.cumulative_value += delegate_reward
|
| 494 |
-
return delegate_reward, (
|
| 495 |
-
f"Delegated '{task['name']}'. "
|
| 496 |
-
f"Reward: {delegate_reward:.2f} (35% of full value)."
|
| 497 |
-
)
|
| 498 |
-
|
| 499 |
-
elif action_type == ActionType.OPTIMIZE:
|
| 500 |
-
# Spend a small energy/budget to reduce uncertainty
|
| 501 |
-
opt_energy = max(0.3, energy_cost * 0.2)
|
| 502 |
-
opt_budget = max(1.0, budget_cost * 0.15)
|
| 503 |
-
if s.resources["energy"] < opt_energy:
|
| 504 |
-
return -0.1, f"Cannot optimize '{task['name']}': not enough energy."
|
| 505 |
-
|
| 506 |
-
s.resources["energy"] = max(0.0, s.resources["energy"] - opt_energy)
|
| 507 |
-
s.resources["budget"] = max(0.0, s.resources["budget"] - opt_budget)
|
| 508 |
-
s.resources["time"] = max(0.0, s.resources["time"] - 0.5)
|
| 509 |
-
|
| 510 |
-
# Reduce uncertainty significantly
|
| 511 |
-
reduction = self._rng.uniform(0.2, 0.45)
|
| 512 |
-
old_unc = task["uncertainty"]
|
| 513 |
-
task["uncertainty"] = max(0.02, task["uncertainty"] - reduction)
|
| 514 |
-
return 0.1, (
|
| 515 |
-
f"Optimized '{task['name']}': uncertainty {old_unc:.2f} → {task['uncertainty']:.2f}. "
|
| 516 |
-
f"Small positive reward for risk reduction."
|
| 517 |
-
)
|
| 518 |
-
|
| 519 |
-
return 0.0, "Unknown action type."
|
| 520 |
-
|
| 521 |
-
def _find_task(self, task_id: int, task_list: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
| 522 |
-
for t in task_list:
|
| 523 |
-
if t["task_id"] == task_id and t["status"] in ("pending", "deferred"):
|
| 524 |
-
return t
|
| 525 |
-
return None
|
| 526 |
-
|
| 527 |
-
def _remove_task(self, task_id: int, s: AetherTaskFlowState) -> None:
|
| 528 |
-
s.tasks = [t for t in s.tasks if t["task_id"] != task_id]
|
| 529 |
-
self._deferred_tasks = [t for t in self._deferred_tasks if t["task_id"] != task_id]
|
| 530 |
-
self._sync_state_queues()
|
| 531 |
-
|
| 532 |
-
def _sync_state_queues(self) -> None:
|
| 533 |
-
self._state.deferred_tasks = [dict(task) for task in self._deferred_tasks]
|
| 534 |
-
|
| 535 |
-
def _max_positive_step_reward(self) -> float:
|
| 536 |
-
"""Upper-bound the raw positive reward for the current difficulty profile."""
|
| 537 |
-
priority_max = float(self._profile["priority_range"][1])
|
| 538 |
-
value_max = float(self._profile["value_range"][1])
|
| 539 |
-
deadline_max = float(self._profile["deadline_range"][1])
|
| 540 |
-
return max(1.0, (value_max * priority_max) + (deadline_max * 0.5))
|
| 541 |
-
|
| 542 |
-
def _normalize_step_reward(self, raw_reward: float) -> float:
|
| 543 |
-
"""
|
| 544 |
-
Map raw action rewards into [0, 1] for OpenEnv-facing observations.
|
| 545 |
-
|
| 546 |
-
Negative rewards occupy [0.0, 0.5), zero remains a neutral midpoint in
|
| 547 |
-
the action-reward scale, and positive rewards occupy (0.5, 1.0].
|
| 548 |
-
Non-action lifecycle observations (reset/already-done) still emit their
|
| 549 |
-
explicit reward values directly via _build_obs.
|
| 550 |
-
"""
|
| 551 |
-
min_reward = -1.0
|
| 552 |
-
if raw_reward <= 0.0:
|
| 553 |
-
normalized = ((raw_reward - min_reward) / (0.0 - min_reward)) * 0.5
|
| 554 |
-
else:
|
| 555 |
-
max_reward = self._max_positive_step_reward()
|
| 556 |
-
normalized = 0.5 + 0.5 * min(raw_reward / max_reward, 1.0)
|
| 557 |
-
return round(max(0.0, min(1.0, normalized)), 4)
|
| 558 |
-
|
| 559 |
-
def _get_obs(self) -> Dict[str, Any]:
|
| 560 |
-
"""Return a compact state snapshot for manual debugging and simple UIs."""
|
| 561 |
-
s = self._state
|
| 562 |
-
self._sync_state_queues()
|
| 563 |
-
return {
|
| 564 |
-
"episode_id": s.episode_id,
|
| 565 |
-
"difficulty": s.difficulty,
|
| 566 |
-
"step_count": s.step_count,
|
| 567 |
-
"num_tasks": len(s.tasks),
|
| 568 |
-
"num_deferred_tasks": len(self._deferred_tasks),
|
| 569 |
-
"tasks_completed": s.tasks_completed,
|
| 570 |
-
"tasks_failed": s.tasks_failed,
|
| 571 |
-
"resources": {
|
| 572 |
-
"time": round(s.resources.get("time", 0.0), 2),
|
| 573 |
-
"energy": round(s.resources.get("energy", 0.0), 2),
|
| 574 |
-
"budget": round(s.resources.get("budget", 0.0), 2),
|
| 575 |
-
},
|
| 576 |
-
"system_health": round(s.system_health, 2),
|
| 577 |
-
"done": s.episode_done,
|
| 578 |
-
}
|
| 579 |
-
|
| 580 |
-
def _make_task_info(self, t: Dict[str, Any]):
|
| 581 |
-
from env.tasks import TaskInfo as _TI
|
| 582 |
-
return _TI(
|
| 583 |
-
task_id=t["task_id"],
|
| 584 |
-
name=t["name"],
|
| 585 |
-
priority=t["priority"],
|
| 586 |
-
deadline=t["deadline"],
|
| 587 |
-
uncertainty=t["uncertainty"],
|
| 588 |
-
value=t["value"],
|
| 589 |
-
required_energy=t["required_energy"],
|
| 590 |
-
required_budget=t["required_budget"],
|
| 591 |
-
category=t["category"],
|
| 592 |
-
status=t["status"],
|
| 593 |
-
)
|
| 594 |
-
|
| 595 |
-
def _build_obs(
|
| 596 |
-
self,
|
| 597 |
-
last_action_type: Optional[str],
|
| 598 |
-
last_action_task_id: Optional[int],
|
| 599 |
-
last_action_outcome: Optional[str],
|
| 600 |
-
reward: float,
|
| 601 |
-
done: bool,
|
| 602 |
-
) -> AetherTaskFlowObservation:
|
| 603 |
-
s = self._state
|
| 604 |
-
return AetherTaskFlowObservation(
|
| 605 |
-
done=done,
|
| 606 |
-
reward=reward,
|
| 607 |
-
metadata={
|
| 608 |
-
"difficulty": s.difficulty,
|
| 609 |
-
"episode_id": s.episode_id,
|
| 610 |
-
"step_count": s.step_count,
|
| 611 |
-
"summary": self._get_obs(),
|
| 612 |
-
},
|
| 613 |
-
tasks=list(s.tasks),
|
| 614 |
-
time_remaining=int(s.resources.get("time", 0)),
|
| 615 |
-
energy_remaining=round(s.resources.get("energy", 0.0), 2),
|
| 616 |
-
budget_remaining=round(s.resources.get("budget", 0.0), 2),
|
| 617 |
-
system_health=round(s.system_health, 4),
|
| 618 |
-
step_number=s.step_count,
|
| 619 |
-
tasks_completed=s.tasks_completed,
|
| 620 |
-
tasks_failed=s.tasks_failed,
|
| 621 |
-
cumulative_value=round(s.cumulative_value, 4),
|
| 622 |
-
last_action_type=last_action_type,
|
| 623 |
-
last_action_task_id=last_action_task_id,
|
| 624 |
-
last_action_outcome=last_action_outcome,
|
| 625 |
-
difficulty=s.difficulty,
|
| 626 |
-
episode_id=s.episode_id,
|
| 627 |
-
)
|
| 628 |
-
|
| 629 |
-
def _build_grade_result(self) -> Dict[str, Any]:
|
| 630 |
-
s = self._state
|
| 631 |
-
profile = self._profile
|
| 632 |
-
return {
|
| 633 |
-
"difficulty": s.difficulty,
|
| 634 |
-
"tasks_completed": s.tasks_completed,
|
| 635 |
-
"tasks_failed": s.tasks_failed,
|
| 636 |
-
"total_tasks": s.tasks_completed + s.tasks_failed + len(s.tasks) + len(self._deferred_tasks),
|
| 637 |
-
"remaining_time": s.resources.get("time", 0),
|
| 638 |
-
"remaining_energy": s.resources.get("energy", 0),
|
| 639 |
-
"remaining_budget": s.resources.get("budget", 0),
|
| 640 |
-
"initial_time": s.initial_resources.get("time", profile["initial_time"]),
|
| 641 |
-
"initial_energy": s.initial_resources.get("energy", profile["initial_energy"]),
|
| 642 |
-
"initial_budget": s.initial_resources.get("budget", profile["initial_budget"]),
|
| 643 |
-
"system_health": s.system_health,
|
| 644 |
-
"steps_used": s.step_count,
|
| 645 |
-
"max_steps": profile["max_steps"],
|
| 646 |
-
"cumulative_value": s.cumulative_value,
|
| 647 |
-
}
|
| 648 |
-
|
| 649 |
-
def compute_final_score(self) -> float:
|
| 650 |
-
"""Compute the final grade [0, 1] for the completed episode."""
|
| 651 |
-
result = self._build_grade_result()
|
| 652 |
-
score = grade(self._difficulty, result)
|
| 653 |
-
return max(0.0, min(1.0, score))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/env/algorithms.py
DELETED
|
@@ -1,253 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
AETHER-TaskFlow Custom Algorithms.
|
| 3 |
-
|
| 4 |
-
AETHER — Adaptive task scoring engine with reward-driven weight evolution.
|
| 5 |
-
RAPTOR — Risk-Aware Priority-Tuned Operational Router: selects action type.
|
| 6 |
-
AWFRO-X — Adaptive Waste-Free Resource Optimizer: recycles low-value states.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from __future__ import annotations
|
| 10 |
-
|
| 11 |
-
import math
|
| 12 |
-
from typing import Any, Dict, List, Optional, Tuple
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
# ---------------------------------------------------------------------------
|
| 16 |
-
# AETHER – Adaptive Decision Core
|
| 17 |
-
# ---------------------------------------------------------------------------
|
| 18 |
-
|
| 19 |
-
class AETHER:
|
| 20 |
-
"""
|
| 21 |
-
Dynamically weighted task scorer.
|
| 22 |
-
|
| 23 |
-
Weights evolve via a momentum-based gradient update driven by episodic
|
| 24 |
-
rewards, forcing the agent to generalize rather than memorise.
|
| 25 |
-
"""
|
| 26 |
-
|
| 27 |
-
def __init__(self) -> None:
|
| 28 |
-
self.weights: Dict[str, float] = {
|
| 29 |
-
"priority": 2.0,
|
| 30 |
-
"deadline_urgency": 1.5,
|
| 31 |
-
"uncertainty_penalty": -1.2,
|
| 32 |
-
"value": 1.0,
|
| 33 |
-
"resource_fit": 0.8,
|
| 34 |
-
}
|
| 35 |
-
self._momentum: Dict[str, float] = {k: 0.0 for k in self.weights}
|
| 36 |
-
self._lr: float = 0.05
|
| 37 |
-
self._beta: float = 0.9 # momentum coefficient
|
| 38 |
-
self._episode_rewards: List[float] = []
|
| 39 |
-
self._step: int = 0
|
| 40 |
-
|
| 41 |
-
# ------------------------------------------------------------------
|
| 42 |
-
# Scoring
|
| 43 |
-
# ------------------------------------------------------------------
|
| 44 |
-
|
| 45 |
-
def score(
|
| 46 |
-
self,
|
| 47 |
-
task: Dict[str, Any],
|
| 48 |
-
resources: Dict[str, float],
|
| 49 |
-
step: int,
|
| 50 |
-
max_steps: int,
|
| 51 |
-
) -> float:
|
| 52 |
-
"""
|
| 53 |
-
Compute a composite urgency score for a task.
|
| 54 |
-
|
| 55 |
-
Higher = more urgent/valuable to act on now.
|
| 56 |
-
"""
|
| 57 |
-
time_left = max(1, max_steps - step)
|
| 58 |
-
|
| 59 |
-
# Deadline urgency: exponential decay — tasks due soon score higher
|
| 60 |
-
deadline_urgency = math.exp(-task["deadline"] / max(1.0, time_left))
|
| 61 |
-
|
| 62 |
-
# Resource fit: can we actually execute this task?
|
| 63 |
-
can_execute = (
|
| 64 |
-
resources.get("energy", 0) >= task["required_energy"]
|
| 65 |
-
and resources.get("budget", 0) >= task["required_budget"]
|
| 66 |
-
)
|
| 67 |
-
resource_fit = 1.0 if can_execute else -0.5
|
| 68 |
-
|
| 69 |
-
score = (
|
| 70 |
-
self.weights["priority"] * task["priority"]
|
| 71 |
-
+ self.weights["deadline_urgency"] * deadline_urgency
|
| 72 |
-
+ self.weights["uncertainty_penalty"] * task["uncertainty"]
|
| 73 |
-
+ self.weights["value"] * (task["value"] / 30.0) # normalise
|
| 74 |
-
+ self.weights["resource_fit"] * resource_fit
|
| 75 |
-
)
|
| 76 |
-
return score
|
| 77 |
-
|
| 78 |
-
def rank_tasks(
|
| 79 |
-
self,
|
| 80 |
-
tasks: List[Dict[str, Any]],
|
| 81 |
-
resources: Dict[str, float],
|
| 82 |
-
step: int,
|
| 83 |
-
max_steps: int,
|
| 84 |
-
) -> List[Tuple[int, float]]:
|
| 85 |
-
"""Return list of (task_id, score) sorted descending."""
|
| 86 |
-
scored = [
|
| 87 |
-
(t["task_id"], self.score(t, resources, step, max_steps))
|
| 88 |
-
for t in tasks
|
| 89 |
-
]
|
| 90 |
-
scored.sort(key=lambda x: x[1], reverse=True)
|
| 91 |
-
return scored
|
| 92 |
-
|
| 93 |
-
# ------------------------------------------------------------------
|
| 94 |
-
# Online weight update
|
| 95 |
-
# ------------------------------------------------------------------
|
| 96 |
-
|
| 97 |
-
def update(self, reward: float) -> None:
|
| 98 |
-
"""Momentum-based weight update after each step."""
|
| 99 |
-
self._episode_rewards.append(reward)
|
| 100 |
-
self._step += 1
|
| 101 |
-
|
| 102 |
-
# Compute a normalised advantage signal
|
| 103 |
-
if len(self._episode_rewards) > 1:
|
| 104 |
-
mean_r = sum(self._episode_rewards) / len(self._episode_rewards)
|
| 105 |
-
std_r = (
|
| 106 |
-
sum((r - mean_r) ** 2 for r in self._episode_rewards)
|
| 107 |
-
/ len(self._episode_rewards)
|
| 108 |
-
) ** 0.5
|
| 109 |
-
advantage = (reward - mean_r) / max(std_r, 1e-6)
|
| 110 |
-
else:
|
| 111 |
-
advantage = reward
|
| 112 |
-
|
| 113 |
-
# Update each weight with momentum
|
| 114 |
-
for key in self.weights:
|
| 115 |
-
grad = advantage * self._lr
|
| 116 |
-
self._momentum[key] = (
|
| 117 |
-
self._beta * self._momentum[key] + (1 - self._beta) * grad
|
| 118 |
-
)
|
| 119 |
-
self.weights[key] += self._momentum[key]
|
| 120 |
-
|
| 121 |
-
# Clamp weights to sensible ranges
|
| 122 |
-
self.weights["priority"] = max(0.5, min(4.0, self.weights["priority"]))
|
| 123 |
-
self.weights["deadline_urgency"] = max(0.3, min(3.0, self.weights["deadline_urgency"]))
|
| 124 |
-
self.weights["uncertainty_penalty"] = max(-3.0, min(-0.1, self.weights["uncertainty_penalty"]))
|
| 125 |
-
self.weights["value"] = max(0.2, min(2.0, self.weights["value"]))
|
| 126 |
-
self.weights["resource_fit"] = max(0.1, min(2.0, self.weights["resource_fit"]))
|
| 127 |
-
|
| 128 |
-
def reset(self) -> None:
|
| 129 |
-
self._episode_rewards = []
|
| 130 |
-
self._step = 0
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
# ---------------------------------------------------------------------------
|
| 134 |
-
# RAPTOR – Execution Strategy Engine
|
| 135 |
-
# ---------------------------------------------------------------------------
|
| 136 |
-
|
| 137 |
-
class RAPTOR:
|
| 138 |
-
"""
|
| 139 |
-
Risk-Aware Priority-Tuned Operational Router.
|
| 140 |
-
|
| 141 |
-
Decides *how* to act on the highest-scored task based on
|
| 142 |
-
current resource levels, task uncertainty, and deadline pressure.
|
| 143 |
-
"""
|
| 144 |
-
|
| 145 |
-
def decide(
|
| 146 |
-
self,
|
| 147 |
-
task: Dict[str, Any],
|
| 148 |
-
resources: Dict[str, float],
|
| 149 |
-
step: int,
|
| 150 |
-
max_steps: int,
|
| 151 |
-
) -> str:
|
| 152 |
-
"""
|
| 153 |
-
Return the optimal action type for the given task + resource state.
|
| 154 |
-
|
| 155 |
-
Decision logic (priority order):
|
| 156 |
-
1. If resources are critically low → defer
|
| 157 |
-
2. If uncertainty is very high → optimize first
|
| 158 |
-
3. If deadline is imminent and resources sufficient → execute
|
| 159 |
-
4. If task can be delegated cheaply → delegate
|
| 160 |
-
5. Default → execute
|
| 161 |
-
"""
|
| 162 |
-
time_left = max_steps - step
|
| 163 |
-
energy = resources.get("energy", 0.0)
|
| 164 |
-
budget = resources.get("budget", 0.0)
|
| 165 |
-
uncertainty = task.get("uncertainty", 0.0)
|
| 166 |
-
deadline = task.get("deadline", 5)
|
| 167 |
-
req_energy = task.get("required_energy", 1.0)
|
| 168 |
-
req_budget = task.get("required_budget", 5.0)
|
| 169 |
-
|
| 170 |
-
# Critical resource shortage
|
| 171 |
-
if energy < req_energy * 0.5 or budget < req_budget * 0.5:
|
| 172 |
-
if time_left > 2:
|
| 173 |
-
return "defer"
|
| 174 |
-
else:
|
| 175 |
-
return "delegate"
|
| 176 |
-
|
| 177 |
-
# Very high uncertainty – optimize first to reduce risk
|
| 178 |
-
if uncertainty > 0.75 and time_left > 1:
|
| 179 |
-
return "optimize"
|
| 180 |
-
|
| 181 |
-
# Imminent deadline – must act now
|
| 182 |
-
if deadline <= 1 and energy >= req_energy and budget >= req_budget:
|
| 183 |
-
return "execute"
|
| 184 |
-
|
| 185 |
-
# Low value + sufficient time → delegate to save resources
|
| 186 |
-
if task.get("value", 10) < 8.0 and time_left > 3:
|
| 187 |
-
return "delegate"
|
| 188 |
-
|
| 189 |
-
# Sufficient resources – execute
|
| 190 |
-
if energy >= req_energy and budget >= req_budget:
|
| 191 |
-
return "execute"
|
| 192 |
-
|
| 193 |
-
# Fallback
|
| 194 |
-
return "defer"
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
# ---------------------------------------------------------------------------
|
| 198 |
-
# AWFRO-X – Adaptive Waste-Free Resource Optimizer
|
| 199 |
-
# ---------------------------------------------------------------------------
|
| 200 |
-
|
| 201 |
-
class AWFROX:
|
| 202 |
-
"""
|
| 203 |
-
Converts low-value deferred states into usable outcomes.
|
| 204 |
-
|
| 205 |
-
Filters the task queue to remove tasks that are guaranteed to fail
|
| 206 |
-
(e.g. deadline passed, insufficient resources with no recovery path)
|
| 207 |
-
and recycles deferred tasks back into the active queue if conditions improve.
|
| 208 |
-
"""
|
| 209 |
-
|
| 210 |
-
def filter_viable(
|
| 211 |
-
self,
|
| 212 |
-
tasks: List[Dict[str, Any]],
|
| 213 |
-
resources: Dict[str, float],
|
| 214 |
-
step: int,
|
| 215 |
-
max_steps: int,
|
| 216 |
-
) -> List[Dict[str, Any]]:
|
| 217 |
-
"""Remove tasks that cannot possibly be completed."""
|
| 218 |
-
time_left = max_steps - step
|
| 219 |
-
viable = []
|
| 220 |
-
for task in tasks:
|
| 221 |
-
# Deadline already passed
|
| 222 |
-
if task.get("deadline", 0) < 0:
|
| 223 |
-
continue
|
| 224 |
-
# No time left
|
| 225 |
-
if time_left <= 0:
|
| 226 |
-
continue
|
| 227 |
-
viable.append(task)
|
| 228 |
-
return viable
|
| 229 |
-
|
| 230 |
-
def recycle_deferred(
|
| 231 |
-
self,
|
| 232 |
-
active: List[Dict[str, Any]],
|
| 233 |
-
deferred: List[Dict[str, Any]],
|
| 234 |
-
resources: Dict[str, float],
|
| 235 |
-
step: int,
|
| 236 |
-
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
|
| 237 |
-
"""
|
| 238 |
-
Requeue deferred tasks when resources recover.
|
| 239 |
-
Returns (updated_active, updated_deferred).
|
| 240 |
-
"""
|
| 241 |
-
still_deferred = []
|
| 242 |
-
for task in deferred:
|
| 243 |
-
can_execute = (
|
| 244 |
-
resources.get("energy", 0) >= task.get("required_energy", 1.0) * 0.8
|
| 245 |
-
and resources.get("budget", 0) >= task.get("required_budget", 1.0) * 0.8
|
| 246 |
-
and task.get("deadline", 0) > 0
|
| 247 |
-
)
|
| 248 |
-
if can_execute:
|
| 249 |
-
task["status"] = "pending"
|
| 250 |
-
active.append(task)
|
| 251 |
-
else:
|
| 252 |
-
still_deferred.append(task)
|
| 253 |
-
return active, still_deferred
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/env/grader.py
DELETED
|
@@ -1,79 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
AETHER-TaskFlow Grader — Explicit per-difficulty scoring
|
| 3 |
-
Ensures scores are always in [0.0, 1.0] and deterministic.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from __future__ import annotations
|
| 7 |
-
|
| 8 |
-
from typing import Any, Dict
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def grade_easy(result: Dict[str, Any]) -> float:
|
| 12 |
-
"""Easy: Strong reward for completion and resource conservation."""
|
| 13 |
-
return _compute_score(
|
| 14 |
-
result,
|
| 15 |
-
{"efficiency": 0.50, "resource": 0.25, "health": 0.15, "speed": 0.10},
|
| 16 |
-
)
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def grade_medium(result: Dict[str, Any]) -> float:
|
| 20 |
-
"""Medium: Heavier penalty on missed deadlines."""
|
| 21 |
-
base = _compute_score(
|
| 22 |
-
result,
|
| 23 |
-
{"efficiency": 0.45, "resource": 0.20, "health": 0.25, "speed": 0.10},
|
| 24 |
-
)
|
| 25 |
-
missed_ratio = result.get("tasks_failed", 0) / max(result.get("total_tasks", 1), 1)
|
| 26 |
-
return max(0.0, base - missed_ratio * 0.20)
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def grade_hard(result: Dict[str, Any]) -> float:
|
| 30 |
-
"""Hard: Strong penalties for system collapse and failures."""
|
| 31 |
-
base = _compute_score(
|
| 32 |
-
result,
|
| 33 |
-
{"efficiency": 0.40, "resource": 0.15, "health": 0.35, "speed": 0.10},
|
| 34 |
-
)
|
| 35 |
-
collapse_penalty = 0.15 if result.get("system_health", 1.0) < 0.3 else 0.0
|
| 36 |
-
missed_ratio = result.get("tasks_failed", 0) / max(result.get("total_tasks", 1), 1)
|
| 37 |
-
return max(0.0, base - collapse_penalty - missed_ratio * 0.25)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def _compute_score(result: Dict[str, Any], weights: Dict[str, float]) -> float:
|
| 41 |
-
total = max(result.get("total_tasks", 1), 1)
|
| 42 |
-
efficiency = result.get("tasks_completed", 0) / total
|
| 43 |
-
|
| 44 |
-
rem_time = result.get("remaining_time", 0)
|
| 45 |
-
rem_energy = result.get("remaining_energy", 0)
|
| 46 |
-
rem_budget = result.get("remaining_budget", 0)
|
| 47 |
-
init_time = max(result.get("initial_time", 10), 1)
|
| 48 |
-
init_energy = max(result.get("initial_energy", 12), 1)
|
| 49 |
-
init_budget = max(result.get("initial_budget", 60), 1)
|
| 50 |
-
|
| 51 |
-
resource_score = (
|
| 52 |
-
(rem_time / init_time) * 0.3
|
| 53 |
-
+ (rem_energy / init_energy) * 0.35
|
| 54 |
-
+ (rem_budget / init_budget) * 0.35
|
| 55 |
-
)
|
| 56 |
-
|
| 57 |
-
health = max(0.0, min(1.0, result.get("system_health", 1.0)))
|
| 58 |
-
steps_used = max(result.get("steps_used", 10), 1)
|
| 59 |
-
max_steps = max(result.get("max_steps", 10), 1)
|
| 60 |
-
speed = 1.0 - (steps_used / max_steps)
|
| 61 |
-
|
| 62 |
-
raw = (
|
| 63 |
-
weights.get("efficiency", 0.4) * efficiency
|
| 64 |
-
+ weights.get("resource", 0.2) * resource_score
|
| 65 |
-
+ weights.get("health", 0.2) * health
|
| 66 |
-
+ weights.get("speed", 0.1) * speed
|
| 67 |
-
)
|
| 68 |
-
|
| 69 |
-
return round(max(0.0, min(1.0, raw)), 4)
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def grade(difficulty: str, result: Dict[str, Any]) -> float:
|
| 73 |
-
graders = {
|
| 74 |
-
"easy": grade_easy,
|
| 75 |
-
"medium": grade_medium,
|
| 76 |
-
"hard": grade_hard,
|
| 77 |
-
}
|
| 78 |
-
fn = graders.get(difficulty, grade_easy)
|
| 79 |
-
return fn(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/env/tasks.py
DELETED
|
@@ -1,192 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Task generation for AETHER-TaskFlow.
|
| 3 |
-
|
| 4 |
-
Generates realistic workflow tasks across three difficulty levels:
|
| 5 |
-
easy – stable, predictable, moderate resources
|
| 6 |
-
medium – dynamic priorities, tighter deadlines
|
| 7 |
-
hard – scarce resources, high uncertainty, adversarial failures
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
import random
|
| 13 |
-
from typing import List, Optional, Tuple
|
| 14 |
-
|
| 15 |
-
from models import TaskInfo
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
# ---------------------------------------------------------------------------
|
| 19 |
-
# Task templates – drawn from realistic enterprise/ops domains
|
| 20 |
-
# ---------------------------------------------------------------------------
|
| 21 |
-
|
| 22 |
-
_TASK_TEMPLATES: List[Tuple[str, str]] = [
|
| 23 |
-
# (name_template, category)
|
| 24 |
-
("Email triage: {volume} messages", "communication"),
|
| 25 |
-
("Code review: PR #{pr_id}", "engineering"),
|
| 26 |
-
("Data pipeline: {dataset} ETL", "data"),
|
| 27 |
-
("Customer support ticket #{tid}", "support"),
|
| 28 |
-
("Security audit: {module} module", "security"),
|
| 29 |
-
("Performance optimization: {service}", "engineering"),
|
| 30 |
-
("Database backup: {db_name}", "infrastructure"),
|
| 31 |
-
("Report generation: {report_type}", "analytics"),
|
| 32 |
-
("Incident response: {severity} alert", "operations"),
|
| 33 |
-
("Content moderation: batch #{bid}", "moderation"),
|
| 34 |
-
("API rate-limit review: {api_name}", "infrastructure"),
|
| 35 |
-
("ML model retraining: {model_name}", "ml"),
|
| 36 |
-
("Budget reconciliation: {quarter}", "finance"),
|
| 37 |
-
("Compliance check: {regulation}", "legal"),
|
| 38 |
-
("System health scan: {region}", "operations"),
|
| 39 |
-
]
|
| 40 |
-
|
| 41 |
-
_FILL_VALUES: dict = {
|
| 42 |
-
"volume": ["50", "120", "300", "500"],
|
| 43 |
-
"pr_id": ["1042", "2381", "9001", "4417"],
|
| 44 |
-
"dataset": ["sales_Q3", "user_events", "inventory", "logs_prod"],
|
| 45 |
-
"tid": ["55123", "10984", "30021", "77654"],
|
| 46 |
-
"module": ["auth", "payments", "admin", "reporting"],
|
| 47 |
-
"service": ["checkout", "search", "recommendations", "notifications"],
|
| 48 |
-
"db_name": ["prod_main", "analytics_dw", "user_db", "logs_archive"],
|
| 49 |
-
"report_type": ["weekly_KPI", "SLA_breach", "revenue_forecast", "churn"],
|
| 50 |
-
"severity": ["P1", "P2", "P3"],
|
| 51 |
-
"bid": ["4401", "8812", "1123"],
|
| 52 |
-
"api_name": ["stripe", "twilio", "sendgrid", "maps"],
|
| 53 |
-
"model_name": ["churn_v3", "fraud_detector", "recommender_v2"],
|
| 54 |
-
"quarter": ["Q3-2025", "Q4-2025", "Q1-2026"],
|
| 55 |
-
"regulation": ["GDPR", "SOC2", "HIPAA", "PCI-DSS"],
|
| 56 |
-
"region": ["us-east-1", "eu-west-2", "ap-southeast-1"],
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def _fill_template(template: str, rng: random.Random) -> str:
|
| 61 |
-
result = template
|
| 62 |
-
for key, choices in _FILL_VALUES.items():
|
| 63 |
-
placeholder = "{" + key + "}"
|
| 64 |
-
if placeholder in result:
|
| 65 |
-
result = result.replace(placeholder, rng.choice(choices))
|
| 66 |
-
return result
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
# ---------------------------------------------------------------------------
|
| 70 |
-
# Difficulty profiles
|
| 71 |
-
# ---------------------------------------------------------------------------
|
| 72 |
-
|
| 73 |
-
_PROFILES: dict = {
|
| 74 |
-
"easy": {
|
| 75 |
-
"n_tasks": 5,
|
| 76 |
-
"priority_range": (0.4, 0.9),
|
| 77 |
-
"deadline_range": (4, 8),
|
| 78 |
-
"uncertainty_range": (0.05, 0.35),
|
| 79 |
-
"value_range": (8.0, 20.0),
|
| 80 |
-
"energy_cost_range": (0.5, 1.5),
|
| 81 |
-
"budget_cost_range": (1.0, 5.0),
|
| 82 |
-
"initial_time": 10,
|
| 83 |
-
"initial_energy": 12.0,
|
| 84 |
-
"initial_budget": 60.0,
|
| 85 |
-
"max_steps": 10,
|
| 86 |
-
},
|
| 87 |
-
"medium": {
|
| 88 |
-
"n_tasks": 8,
|
| 89 |
-
"priority_range": (0.3, 1.0),
|
| 90 |
-
"deadline_range": (2, 6),
|
| 91 |
-
"uncertainty_range": (0.15, 0.65),
|
| 92 |
-
"value_range": (5.0, 25.0),
|
| 93 |
-
"energy_cost_range": (0.8, 2.5),
|
| 94 |
-
"budget_cost_range": (2.0, 10.0),
|
| 95 |
-
"initial_time": 10,
|
| 96 |
-
"initial_energy": 10.0,
|
| 97 |
-
"initial_budget": 50.0,
|
| 98 |
-
"max_steps": 10,
|
| 99 |
-
},
|
| 100 |
-
"hard": {
|
| 101 |
-
"n_tasks": 12,
|
| 102 |
-
"priority_range": (0.2, 1.0),
|
| 103 |
-
"deadline_range": (1, 4),
|
| 104 |
-
"uncertainty_range": (0.35, 0.95),
|
| 105 |
-
"value_range": (3.0, 30.0),
|
| 106 |
-
"energy_cost_range": (1.2, 4.0),
|
| 107 |
-
"budget_cost_range": (5.0, 20.0),
|
| 108 |
-
"initial_time": 10,
|
| 109 |
-
"initial_energy": 8.0,
|
| 110 |
-
"initial_budget": 40.0,
|
| 111 |
-
"max_steps": 10,
|
| 112 |
-
},
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
def get_profile(difficulty: str) -> dict:
|
| 117 |
-
return _PROFILES[difficulty]
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
def generate_tasks(difficulty: str, seed: Optional[int] = None) -> List[TaskInfo]:
|
| 121 |
-
"""Generate a task queue for the given difficulty level."""
|
| 122 |
-
rng = random.Random(seed)
|
| 123 |
-
profile = _PROFILES[difficulty]
|
| 124 |
-
|
| 125 |
-
tasks: List[TaskInfo] = []
|
| 126 |
-
used_templates = rng.choices(range(len(_TASK_TEMPLATES)), k=profile["n_tasks"])
|
| 127 |
-
|
| 128 |
-
for i, t_idx in enumerate(used_templates):
|
| 129 |
-
name_template, category = _TASK_TEMPLATES[t_idx]
|
| 130 |
-
name = _fill_template(name_template, rng)
|
| 131 |
-
|
| 132 |
-
priority = rng.uniform(*profile["priority_range"])
|
| 133 |
-
deadline = rng.randint(*profile["deadline_range"])
|
| 134 |
-
uncertainty = rng.uniform(*profile["uncertainty_range"])
|
| 135 |
-
value = rng.uniform(*profile["value_range"])
|
| 136 |
-
energy_cost = rng.uniform(*profile["energy_cost_range"])
|
| 137 |
-
budget_cost = rng.uniform(*profile["budget_cost_range"])
|
| 138 |
-
|
| 139 |
-
tasks.append(
|
| 140 |
-
TaskInfo(
|
| 141 |
-
task_id=i,
|
| 142 |
-
name=name,
|
| 143 |
-
priority=priority,
|
| 144 |
-
deadline=deadline,
|
| 145 |
-
uncertainty=uncertainty,
|
| 146 |
-
value=value,
|
| 147 |
-
required_energy=energy_cost,
|
| 148 |
-
required_budget=budget_cost,
|
| 149 |
-
category=category,
|
| 150 |
-
)
|
| 151 |
-
)
|
| 152 |
-
|
| 153 |
-
# Sort by priority descending so agent sees most urgent first
|
| 154 |
-
tasks.sort(key=lambda t: t.priority, reverse=True)
|
| 155 |
-
# Re-index after sort
|
| 156 |
-
for idx, t in enumerate(tasks):
|
| 157 |
-
t.task_id = idx
|
| 158 |
-
|
| 159 |
-
return tasks
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def apply_dynamic_updates(
|
| 163 |
-
tasks: List[TaskInfo],
|
| 164 |
-
step: int,
|
| 165 |
-
difficulty: str,
|
| 166 |
-
rng: random.Random,
|
| 167 |
-
) -> List[TaskInfo]:
|
| 168 |
-
"""
|
| 169 |
-
Apply stochastic dynamic updates to the task queue (medium/hard only).
|
| 170 |
-
- Priority drift
|
| 171 |
-
- Deadline tightening
|
| 172 |
-
- Uncertainty spikes
|
| 173 |
-
"""
|
| 174 |
-
if difficulty == "easy":
|
| 175 |
-
return tasks
|
| 176 |
-
|
| 177 |
-
for task in tasks:
|
| 178 |
-
if task.status != "pending":
|
| 179 |
-
continue
|
| 180 |
-
|
| 181 |
-
# NOTE: deadline countdown is handled centrally in aether_env.py
|
| 182 |
-
# _step_impl() to avoid double-decrement on hard mode.
|
| 183 |
-
|
| 184 |
-
# Priority drift ±0.1
|
| 185 |
-
drift = rng.uniform(-0.08, 0.12)
|
| 186 |
-
task.priority = min(1.0, max(0.1, task.priority + drift))
|
| 187 |
-
|
| 188 |
-
# Uncertainty spike (hard mode)
|
| 189 |
-
if difficulty == "hard" and rng.random() < 0.15:
|
| 190 |
-
task.uncertainty = min(0.95, task.uncertainty + rng.uniform(0.1, 0.25))
|
| 191 |
-
|
| 192 |
-
return tasks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/inference.py
DELETED
|
@@ -1,229 +0,0 @@
|
|
| 1 |
-
import argparse
|
| 2 |
-
import json
|
| 3 |
-
import os
|
| 4 |
-
import sys
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
from typing import Any, Dict, List, Optional
|
| 7 |
-
|
| 8 |
-
from openai import OpenAI
|
| 9 |
-
|
| 10 |
-
_REPO_ROOT = Path(__file__).resolve().parent
|
| 11 |
-
sys.path.insert(0, str(_REPO_ROOT))
|
| 12 |
-
|
| 13 |
-
from env.aether_env import AetherTaskFlowEnvironment
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
API_KEY = os.getenv("API_KEY", os.getenv("OPENAI_API_KEY", os.getenv("HF_TOKEN", "")))
|
| 17 |
-
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 18 |
-
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
|
| 19 |
-
TEMPERATURE = float(os.getenv("TEMPERATURE", "0.1"))
|
| 20 |
-
MAX_LLM_TOKENS = int(os.getenv("MAX_LLM_TOKENS", "128"))
|
| 21 |
-
USE_LLM = bool(API_KEY)
|
| 22 |
-
|
| 23 |
-
SYSTEM_PROMPT = """You are an expert workflow orchestration agent inside the AETHER-TaskFlow RL environment.
|
| 24 |
-
|
| 25 |
-
Each step you receive an observation and must output a single JSON action.
|
| 26 |
-
|
| 27 |
-
RULES:
|
| 28 |
-
- Output ONLY valid JSON with keys: action_type, task_id, reasoning
|
| 29 |
-
- action_type must be one of: execute, defer, delegate, optimize
|
| 30 |
-
- task_id must be an integer matching a pending task id
|
| 31 |
-
- reasoning should be brief and may be empty
|
| 32 |
-
- No explanation, no markdown, no extra text - raw JSON only
|
| 33 |
-
|
| 34 |
-
STRATEGY:
|
| 35 |
-
- execute: high-priority, low-uncertainty tasks with sufficient resources
|
| 36 |
-
- optimize: before executing high-uncertainty tasks (reduces failure risk)
|
| 37 |
-
- delegate: when resources are too low (free action, 35% reward)
|
| 38 |
-
- defer: tasks you can handle later when resources recover
|
| 39 |
-
|
| 40 |
-
Example: {"action_type": "execute", "task_id": 2, "reasoning": "highest value low risk"}"""
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def log_start(task: str, env: str, model: str) -> None:
|
| 44 |
-
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def log_step(
|
| 48 |
-
step: int,
|
| 49 |
-
action: str,
|
| 50 |
-
reward: float,
|
| 51 |
-
done: bool,
|
| 52 |
-
error: Optional[str] = None,
|
| 53 |
-
) -> None:
|
| 54 |
-
error_val = error if error else "null"
|
| 55 |
-
done_val = str(done).lower()
|
| 56 |
-
action_safe = action.replace("\n", " ").replace("\r", "")[:120]
|
| 57 |
-
print(
|
| 58 |
-
f"[STEP] step={step} action={action_safe} reward={reward:.2f} "
|
| 59 |
-
f"done={done_val} error={error_val}",
|
| 60 |
-
flush=True,
|
| 61 |
-
)
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
|
| 65 |
-
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 66 |
-
print(
|
| 67 |
-
f"[END] success={str(success).lower()} steps={steps} "
|
| 68 |
-
f"score={score:.2f} rewards={rewards_str}",
|
| 69 |
-
flush=True,
|
| 70 |
-
)
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def _obs_to_prompt(obs_dict: Dict[str, Any]) -> str:
|
| 74 |
-
tasks = obs_dict.get("tasks", [])
|
| 75 |
-
task_lines = []
|
| 76 |
-
for t in tasks:
|
| 77 |
-
task_lines.append(
|
| 78 |
-
f" id={t['task_id']} name='{t['name']}' priority={t['priority']:.2f} "
|
| 79 |
-
f"deadline={t['deadline']} uncertainty={t['uncertainty']:.2f} "
|
| 80 |
-
f"value={t['value']:.1f} energy_cost={t['required_energy']:.1f} "
|
| 81 |
-
f"budget_cost={t['required_budget']:.1f} status={t.get('status', 'pending')}"
|
| 82 |
-
)
|
| 83 |
-
return (
|
| 84 |
-
f"OBSERVATION:\n"
|
| 85 |
-
f" time_remaining={obs_dict.get('time_remaining')} "
|
| 86 |
-
f"energy={obs_dict.get('energy_remaining', 0):.1f} "
|
| 87 |
-
f"budget={obs_dict.get('budget_remaining', 0):.1f} "
|
| 88 |
-
f"system_health={obs_dict.get('system_health', 1):.2f}\n"
|
| 89 |
-
f"PENDING TASKS:\n"
|
| 90 |
-
+ "\n".join(task_lines)
|
| 91 |
-
+ "\n\nOutput your action JSON:"
|
| 92 |
-
)
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def _call_llm(client: OpenAI, obs_text: str, history: List[dict]) -> Optional[Dict[str, Any]]:
|
| 96 |
-
"""
|
| 97 |
-
Call the LLM using proper OpenAI client (required by judges).
|
| 98 |
-
Falls back to heuristic if it fails.
|
| 99 |
-
"""
|
| 100 |
-
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
| 101 |
-
messages.extend(history[-4:])
|
| 102 |
-
messages.append({"role": "user", "content": obs_text})
|
| 103 |
-
|
| 104 |
-
try:
|
| 105 |
-
completion = client.chat.completions.create(
|
| 106 |
-
model=MODEL_NAME,
|
| 107 |
-
messages=messages,
|
| 108 |
-
temperature=TEMPERATURE,
|
| 109 |
-
max_tokens=MAX_LLM_TOKENS,
|
| 110 |
-
)
|
| 111 |
-
raw = (completion.choices[0].message.content or "").strip()
|
| 112 |
-
if raw.startswith("```"):
|
| 113 |
-
raw = raw.split("```")[1]
|
| 114 |
-
if raw.startswith("json"):
|
| 115 |
-
raw = raw[4:].strip()
|
| 116 |
-
parsed = json.loads(raw)
|
| 117 |
-
return {
|
| 118 |
-
"task_id": int(parsed["task_id"]),
|
| 119 |
-
"action_type": str(parsed["action_type"]),
|
| 120 |
-
"reasoning": str(parsed.get("reasoning", "")),
|
| 121 |
-
}
|
| 122 |
-
except Exception as exc:
|
| 123 |
-
print(f"[DEBUG] LLM call/parse failed: {exc}", flush=True)
|
| 124 |
-
return None
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
def get_llm_action(
|
| 128 |
-
client: OpenAI,
|
| 129 |
-
obs_dict: Dict[str, Any],
|
| 130 |
-
history: List[dict],
|
| 131 |
-
) -> Optional[Dict[str, Any]]:
|
| 132 |
-
"""Call the configured OpenAI-compatible endpoint for an LLM action."""
|
| 133 |
-
obs_text = _obs_to_prompt(obs_dict)
|
| 134 |
-
result = _call_llm(client, obs_text, history)
|
| 135 |
-
if result is not None:
|
| 136 |
-
history.append({"role": "user", "content": obs_text})
|
| 137 |
-
history.append({"role": "assistant", "content": json.dumps(result)})
|
| 138 |
-
return result
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
def get_heuristic_action(env: AetherTaskFlowEnvironment) -> Dict[str, Any]:
|
| 142 |
-
"""Built-in AETHER + RAPTOR heuristic - no API required."""
|
| 143 |
-
action = env.message_to_action("")
|
| 144 |
-
if hasattr(action, "model_dump"):
|
| 145 |
-
return action.model_dump(exclude={"reasoning"})
|
| 146 |
-
return {
|
| 147 |
-
"action_type": getattr(action, "action_type", "execute").value
|
| 148 |
-
if hasattr(action, "action_type")
|
| 149 |
-
else "execute",
|
| 150 |
-
"task_id": getattr(action, "task_id", 0),
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
def get_action(
|
| 155 |
-
env: AetherTaskFlowEnvironment,
|
| 156 |
-
client: OpenAI,
|
| 157 |
-
obs_dict: Dict[str, Any],
|
| 158 |
-
history: List[dict],
|
| 159 |
-
) -> Dict[str, Any]:
|
| 160 |
-
"""Return LLM action if an API key is set, otherwise heuristic."""
|
| 161 |
-
if USE_LLM:
|
| 162 |
-
result = get_llm_action(client, obs_dict, history)
|
| 163 |
-
if result is not None:
|
| 164 |
-
return result
|
| 165 |
-
|
| 166 |
-
heuristic = get_heuristic_action(env)
|
| 167 |
-
obs_text = _obs_to_prompt(obs_dict)
|
| 168 |
-
history.append({"role": "user", "content": obs_text})
|
| 169 |
-
history.append({"role": "assistant", "content": json.dumps(heuristic)})
|
| 170 |
-
return heuristic
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
def run_episode(difficulty: str, client: OpenAI) -> None:
|
| 174 |
-
os.environ["AETHER_DIFFICULTY"] = difficulty
|
| 175 |
-
env = AetherTaskFlowEnvironment(difficulty=difficulty)
|
| 176 |
-
|
| 177 |
-
model_label = MODEL_NAME if USE_LLM else "HEURISTIC-AETHER-RAPTOR"
|
| 178 |
-
log_start(task=difficulty, env="aether_taskflow", model=model_label)
|
| 179 |
-
|
| 180 |
-
obs = env.reset()
|
| 181 |
-
obs_dict = obs.model_dump() if hasattr(obs, "model_dump") else obs
|
| 182 |
-
|
| 183 |
-
rewards: List[float] = []
|
| 184 |
-
history: List[dict] = []
|
| 185 |
-
step = 0
|
| 186 |
-
|
| 187 |
-
while True:
|
| 188 |
-
step += 1
|
| 189 |
-
action_dict = get_action(env, client, obs_dict, history)
|
| 190 |
-
next_obs = env.step(action_dict)
|
| 191 |
-
|
| 192 |
-
next_obs_dict = next_obs.model_dump() if hasattr(next_obs, "model_dump") else next_obs
|
| 193 |
-
reward = next_obs_dict.get("reward", 0.0)
|
| 194 |
-
done = next_obs_dict.get("done", False)
|
| 195 |
-
rewards.append(reward)
|
| 196 |
-
|
| 197 |
-
action_str = (
|
| 198 |
-
f"{action_dict.get('action_type', 'execute')}"
|
| 199 |
-
f"(task_id={action_dict.get('task_id', 0)})"
|
| 200 |
-
)
|
| 201 |
-
log_step(step, action_str, reward, done)
|
| 202 |
-
|
| 203 |
-
obs_dict = next_obs_dict
|
| 204 |
-
if done:
|
| 205 |
-
break
|
| 206 |
-
|
| 207 |
-
score = env.compute_final_score()
|
| 208 |
-
log_end(success=True, steps=step, score=score, rewards=rewards)
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
def main() -> None:
|
| 212 |
-
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
|
| 213 |
-
|
| 214 |
-
parser = argparse.ArgumentParser(description="AETHER-TaskFlow Inference")
|
| 215 |
-
parser.add_argument(
|
| 216 |
-
"--single",
|
| 217 |
-
choices=["easy", "medium", "hard"],
|
| 218 |
-
default=None,
|
| 219 |
-
help="Run a single difficulty (default: all three)",
|
| 220 |
-
)
|
| 221 |
-
args = parser.parse_args()
|
| 222 |
-
|
| 223 |
-
difficulties = [args.single] if args.single else ["easy", "medium", "hard"]
|
| 224 |
-
for diff in difficulties:
|
| 225 |
-
run_episode(diff, client)
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
if __name__ == "__main__":
|
| 229 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/models.py
DELETED
|
@@ -1,298 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import re
|
| 4 |
-
from enum import Enum
|
| 5 |
-
from typing import Any, Dict, List, Optional
|
| 6 |
-
|
| 7 |
-
from openenv.core.env_server.types import Action, Observation, State
|
| 8 |
-
from pydantic import Field, model_validator
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
class ActionType(str, Enum):
|
| 13 |
-
EXECUTE = "execute"
|
| 14 |
-
DEFER = "defer"
|
| 15 |
-
DELEGATE = "delegate"
|
| 16 |
-
OPTIMIZE = "optimize"
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class TaskStatus(str, Enum):
|
| 20 |
-
PENDING = "pending"
|
| 21 |
-
IN_PROGRESS = "in_progress"
|
| 22 |
-
COMPLETED = "completed"
|
| 23 |
-
DEFERRED = "deferred"
|
| 24 |
-
FAILED = "failed"
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
class DifficultyLevel(str, Enum):
|
| 28 |
-
EASY = "easy"
|
| 29 |
-
MEDIUM = "medium"
|
| 30 |
-
HARD = "hard"
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def _coerce_action_type_from_text(text: str) -> ActionType:
|
| 34 |
-
"""
|
| 35 |
-
Convert loose text from the web UI into the closest valid action type.
|
| 36 |
-
|
| 37 |
-
The default web interface validates form fields against the Action model
|
| 38 |
-
before our environment can run `message_to_action()`, so we accept
|
| 39 |
-
friendly free-form input here and normalize it.
|
| 40 |
-
"""
|
| 41 |
-
normalized = (text or "").strip().lower()
|
| 42 |
-
if not normalized:
|
| 43 |
-
return ActionType.EXECUTE
|
| 44 |
-
|
| 45 |
-
keyword_map = (
|
| 46 |
-
(ActionType.OPTIMIZE, ("optimize", "optimise", "tune", "analyze", "analyse")),
|
| 47 |
-
(ActionType.DELEGATE, ("delegate", "assign", "handoff", "hand off", "offload")),
|
| 48 |
-
(ActionType.DEFER, ("defer", "later", "wait", "skip", "postpone")),
|
| 49 |
-
(ActionType.EXECUTE, ("execute", "run", "do", "complete", "process", "start")),
|
| 50 |
-
)
|
| 51 |
-
for action_type, keywords in keyword_map:
|
| 52 |
-
if any(keyword in normalized for keyword in keywords):
|
| 53 |
-
return action_type
|
| 54 |
-
|
| 55 |
-
return ActionType.EXECUTE
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def _extract_task_id_from_text(text: str) -> Optional[int]:
|
| 59 |
-
if not text:
|
| 60 |
-
return None
|
| 61 |
-
|
| 62 |
-
explicit_match = re.search(r"(?:task|id|#)\s*(\d+)", text, flags=re.IGNORECASE)
|
| 63 |
-
if explicit_match:
|
| 64 |
-
return int(explicit_match.group(1))
|
| 65 |
-
|
| 66 |
-
loose_match = re.search(r"\b(\d+)\b", text)
|
| 67 |
-
if loose_match:
|
| 68 |
-
return int(loose_match.group(1))
|
| 69 |
-
|
| 70 |
-
return None
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
class TaskInfo(object):
|
| 74 |
-
"""Lightweight task descriptor (not a BaseModel to avoid nesting issues)."""
|
| 75 |
-
|
| 76 |
-
def __init__(
|
| 77 |
-
self,
|
| 78 |
-
task_id: int,
|
| 79 |
-
name: str,
|
| 80 |
-
priority: float,
|
| 81 |
-
deadline: int,
|
| 82 |
-
uncertainty: float,
|
| 83 |
-
value: float,
|
| 84 |
-
required_energy: float,
|
| 85 |
-
required_budget: float,
|
| 86 |
-
category: str,
|
| 87 |
-
status: str = "pending",
|
| 88 |
-
):
|
| 89 |
-
self.task_id = task_id
|
| 90 |
-
self.name = name
|
| 91 |
-
self.priority = priority
|
| 92 |
-
self.deadline = deadline
|
| 93 |
-
self.uncertainty = uncertainty
|
| 94 |
-
self.value = value
|
| 95 |
-
self.required_energy = required_energy
|
| 96 |
-
self.required_budget = required_budget
|
| 97 |
-
self.category = category
|
| 98 |
-
self.status = status
|
| 99 |
-
|
| 100 |
-
def to_dict(self) -> Dict[str, Any]:
|
| 101 |
-
return {
|
| 102 |
-
"task_id": self.task_id,
|
| 103 |
-
"name": self.name,
|
| 104 |
-
"priority": round(self.priority, 3),
|
| 105 |
-
"deadline": self.deadline,
|
| 106 |
-
"uncertainty": round(self.uncertainty, 3),
|
| 107 |
-
"value": round(self.value, 3),
|
| 108 |
-
"required_energy": round(self.required_energy, 3),
|
| 109 |
-
"required_budget": round(self.required_budget, 3),
|
| 110 |
-
"category": self.category,
|
| 111 |
-
"status": self.status,
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
class AetherTaskFlowAction(Action):
|
| 116 |
-
"""
|
| 117 |
-
Action for the AETHER-TaskFlow environment.
|
| 118 |
-
|
| 119 |
-
The agent selects a task by ID and decides how to act on it.
|
| 120 |
-
"""
|
| 121 |
-
|
| 122 |
-
@model_validator(mode="before")
|
| 123 |
-
@classmethod
|
| 124 |
-
def normalize_web_input(cls, data: Any) -> Any:
|
| 125 |
-
"""
|
| 126 |
-
Make the default web form resilient to casual text input.
|
| 127 |
-
|
| 128 |
-
Examples that should validate cleanly:
|
| 129 |
-
{"action_type": "hi"}
|
| 130 |
-
{"action_type": "execute task 3"}
|
| 131 |
-
{"message": "delegate 2"}
|
| 132 |
-
"""
|
| 133 |
-
if isinstance(data, str):
|
| 134 |
-
data = {"action_type": data}
|
| 135 |
-
|
| 136 |
-
if not isinstance(data, dict):
|
| 137 |
-
return data
|
| 138 |
-
|
| 139 |
-
payload = dict(data)
|
| 140 |
-
raw_message = payload.get("message")
|
| 141 |
-
raw_action_type = payload.get("action_type")
|
| 142 |
-
|
| 143 |
-
# When the UI sends a free-form message, treat it as action text.
|
| 144 |
-
if isinstance(raw_message, str) and not raw_action_type:
|
| 145 |
-
raw_action_type = raw_message
|
| 146 |
-
payload.pop("message", None)
|
| 147 |
-
|
| 148 |
-
if isinstance(raw_action_type, str):
|
| 149 |
-
parsed_task_id = _extract_task_id_from_text(raw_action_type)
|
| 150 |
-
payload["action_type"] = _coerce_action_type_from_text(raw_action_type).value
|
| 151 |
-
|
| 152 |
-
if payload.get("task_id") in (None, "") and parsed_task_id is not None:
|
| 153 |
-
payload["task_id"] = parsed_task_id
|
| 154 |
-
|
| 155 |
-
if payload.get("reasoning") in (None, "") and raw_action_type.strip():
|
| 156 |
-
payload["reasoning"] = f"parsed from '{raw_action_type.strip()[:80]}'"
|
| 157 |
-
|
| 158 |
-
if payload.get("task_id") in (None, ""):
|
| 159 |
-
payload["task_id"] = 0
|
| 160 |
-
|
| 161 |
-
return payload
|
| 162 |
-
|
| 163 |
-
action_type: ActionType = Field(
|
| 164 |
-
...,
|
| 165 |
-
description=(
|
| 166 |
-
"How to act on the selected task. "
|
| 167 |
-
"'execute': consume resources and complete the task; "
|
| 168 |
-
"'defer': postpone to a later step (low penalty); "
|
| 169 |
-
"'delegate': offload at reduced reward but no resource cost; "
|
| 170 |
-
"'optimize': reduce task uncertainty before execution."
|
| 171 |
-
),
|
| 172 |
-
)
|
| 173 |
-
task_id: int = Field(
|
| 174 |
-
...,
|
| 175 |
-
ge=0,
|
| 176 |
-
description="ID of the task to act on (from the current task list).",
|
| 177 |
-
)
|
| 178 |
-
reasoning: Optional[str] = Field(
|
| 179 |
-
default=None,
|
| 180 |
-
max_length=500,
|
| 181 |
-
description="Optional agent reasoning for this action (logged but not scored).",
|
| 182 |
-
)
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
class AetherTaskFlowObservation(Observation):
|
| 186 |
-
"""
|
| 187 |
-
Observation returned after each step in the AETHER-TaskFlow environment.
|
| 188 |
-
|
| 189 |
-
Contains all information the agent needs to make the next decision.
|
| 190 |
-
"""
|
| 191 |
-
|
| 192 |
-
# Task queue
|
| 193 |
-
tasks: List[Dict[str, Any]] = Field(
|
| 194 |
-
default_factory=list,
|
| 195 |
-
description="Current list of pending/deferred tasks as dicts.",
|
| 196 |
-
)
|
| 197 |
-
|
| 198 |
-
# Resource pool
|
| 199 |
-
time_remaining: int = Field(
|
| 200 |
-
default=10,
|
| 201 |
-
ge=0,
|
| 202 |
-
description="Time steps remaining in this episode.",
|
| 203 |
-
)
|
| 204 |
-
energy_remaining: float = Field(
|
| 205 |
-
default=10.0,
|
| 206 |
-
ge=0.0,
|
| 207 |
-
description="Energy units remaining.",
|
| 208 |
-
)
|
| 209 |
-
budget_remaining: float = Field(
|
| 210 |
-
default=50.0,
|
| 211 |
-
ge=0.0,
|
| 212 |
-
description="Budget units remaining.",
|
| 213 |
-
)
|
| 214 |
-
|
| 215 |
-
# System health
|
| 216 |
-
system_health: float = Field(
|
| 217 |
-
default=1.0,
|
| 218 |
-
ge=0.0,
|
| 219 |
-
le=1.0,
|
| 220 |
-
description="Overall system health [0,1]. Drops on overload or missed deadlines.",
|
| 221 |
-
)
|
| 222 |
-
|
| 223 |
-
# Episode progress
|
| 224 |
-
step_number: int = Field(
|
| 225 |
-
default=0,
|
| 226 |
-
ge=0,
|
| 227 |
-
description="Current step number in this episode.",
|
| 228 |
-
)
|
| 229 |
-
tasks_completed: int = Field(
|
| 230 |
-
default=0,
|
| 231 |
-
ge=0,
|
| 232 |
-
description="Total tasks completed so far.",
|
| 233 |
-
)
|
| 234 |
-
tasks_failed: int = Field(
|
| 235 |
-
default=0,
|
| 236 |
-
ge=0,
|
| 237 |
-
description="Total tasks that missed their deadline.",
|
| 238 |
-
)
|
| 239 |
-
cumulative_value: float = Field(
|
| 240 |
-
default=0.0,
|
| 241 |
-
description="Total value accumulated so far.",
|
| 242 |
-
)
|
| 243 |
-
|
| 244 |
-
# Last action feedback
|
| 245 |
-
last_action_type: Optional[str] = Field(
|
| 246 |
-
default=None,
|
| 247 |
-
description="Action type taken in the previous step.",
|
| 248 |
-
)
|
| 249 |
-
last_action_task_id: Optional[int] = Field(
|
| 250 |
-
default=None,
|
| 251 |
-
description="Task ID acted on in the previous step.",
|
| 252 |
-
)
|
| 253 |
-
last_action_outcome: Optional[str] = Field(
|
| 254 |
-
default=None,
|
| 255 |
-
description="Human-readable outcome of the last action.",
|
| 256 |
-
)
|
| 257 |
-
|
| 258 |
-
# Episode info
|
| 259 |
-
difficulty: str = Field(
|
| 260 |
-
default="easy",
|
| 261 |
-
description="Current task difficulty level.",
|
| 262 |
-
)
|
| 263 |
-
episode_id: Optional[str] = Field(
|
| 264 |
-
default=None,
|
| 265 |
-
description="Unique identifier for this episode.",
|
| 266 |
-
)
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
class AetherTaskFlowState(State):
|
| 271 |
-
"""
|
| 272 |
-
Internal state of the AETHER-TaskFlow environment.
|
| 273 |
-
|
| 274 |
-
This is the ground truth state used by the grader.
|
| 275 |
-
"""
|
| 276 |
-
|
| 277 |
-
difficulty: str = Field(default="easy")
|
| 278 |
-
tasks: List[Dict[str, Any]] = Field(default_factory=list)
|
| 279 |
-
completed_tasks: List[Dict[str, Any]] = Field(default_factory=list)
|
| 280 |
-
failed_tasks: List[Dict[str, Any]] = Field(default_factory=list)
|
| 281 |
-
deferred_tasks: List[Dict[str, Any]] = Field(default_factory=list)
|
| 282 |
-
|
| 283 |
-
resources: Dict[str, float] = Field(
|
| 284 |
-
default_factory=lambda: {"time": 10.0, "energy": 10.0, "budget": 50.0}
|
| 285 |
-
)
|
| 286 |
-
initial_resources: Dict[str, float] = Field(
|
| 287 |
-
default_factory=lambda: {"time": 10.0, "energy": 10.0, "budget": 50.0}
|
| 288 |
-
)
|
| 289 |
-
|
| 290 |
-
system_health: float = Field(default=1.0)
|
| 291 |
-
cumulative_value: float = Field(default=0.0)
|
| 292 |
-
cumulative_reward: float = Field(default=0.0)
|
| 293 |
-
|
| 294 |
-
tasks_completed: int = Field(default=0)
|
| 295 |
-
tasks_failed: int = Field(default=0)
|
| 296 |
-
|
| 297 |
-
episode_done: bool = Field(default=False)
|
| 298 |
-
seed: Optional[int] = Field(default=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/openenv.yaml
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
spec_version: 1
|
| 2 |
-
name: aether_taskflow
|
| 3 |
-
description: Adaptive Workflow Management RL Environment — dynamic task scheduling under resource constraints, uncertainty, and shifting priorities. Real-world enterprise operations simulation.
|
| 4 |
-
|
| 5 |
-
api:
|
| 6 |
-
reset: /reset
|
| 7 |
-
step: /step
|
| 8 |
-
state: /state
|
| 9 |
-
schema: /schema
|
| 10 |
-
|
| 11 |
-
tasks:
|
| 12 |
-
- id: easy
|
| 13 |
-
description: Stable environment with 5 tasks and moderate resources
|
| 14 |
-
- id: medium
|
| 15 |
-
description: Dynamic priorities and tighter deadlines with 8 tasks
|
| 16 |
-
- id: hard
|
| 17 |
-
description: Scarce resources, high uncertainty spikes, and 12 tasks
|
| 18 |
-
|
| 19 |
-
models:
|
| 20 |
-
action: AetherTaskFlowAction
|
| 21 |
-
observation: AetherTaskFlowObservation
|
| 22 |
-
state: AetherTaskFlowState
|
| 23 |
-
|
| 24 |
-
runtime: fastapi
|
| 25 |
-
app: server.app:app
|
| 26 |
-
port: 7860
|
| 27 |
-
enable_web_interface: true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/pyproject.toml
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
[build-system]
|
| 2 |
-
requires = ["setuptools>=45", "wheel"]
|
| 3 |
-
build-backend = "setuptools.build_meta"
|
| 4 |
-
|
| 5 |
-
[project]
|
| 6 |
-
name = "aether-taskflow"
|
| 7 |
-
version = "1.0.0"
|
| 8 |
-
description = "AETHER-TaskFlow: Adaptive Workflow Management RL Environment for OpenEnv"
|
| 9 |
-
requires-python = ">=3.10"
|
| 10 |
-
dependencies = [
|
| 11 |
-
"openenv-core[core]>=0.2.2",
|
| 12 |
-
"openai>=1.0.0",
|
| 13 |
-
"fastapi>=0.100.0",
|
| 14 |
-
"uvicorn[standard]>=0.22.0",
|
| 15 |
-
"pydantic>=2.0.0",
|
| 16 |
-
"httpx>=0.24.0",
|
| 17 |
-
"websockets>=11.0",
|
| 18 |
-
]
|
| 19 |
-
|
| 20 |
-
[project.optional-dependencies]
|
| 21 |
-
dev = [
|
| 22 |
-
"pytest>=8.0.0",
|
| 23 |
-
"pytest-asyncio>=0.23.0",
|
| 24 |
-
]
|
| 25 |
-
|
| 26 |
-
[project.scripts]
|
| 27 |
-
server = "aether_taskflow.server.app:main"
|
| 28 |
-
|
| 29 |
-
[tool.setuptools]
|
| 30 |
-
include-package-data = true
|
| 31 |
-
packages = ["aether_taskflow", "aether_taskflow.server"]
|
| 32 |
-
package-dir = { "aether_taskflow" = ".", "aether_taskflow.server" = "server" }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/pytest.ini
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
[pytest]
|
| 2 |
-
testpaths = tests
|
| 3 |
-
norecursedirs = pytest-cache-files-* __pycache__
|
| 4 |
-
addopts = -p no:cacheprovider
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/requirements.txt
DELETED
|
@@ -1,8 +0,0 @@
|
|
| 1 |
-
# AETHER-TaskFlow requirements
|
| 2 |
-
openenv-core>=0.2.2
|
| 3 |
-
openai>=1.0.0
|
| 4 |
-
fastapi>=0.100.0
|
| 5 |
-
uvicorn[standard]>=0.22.0
|
| 6 |
-
pydantic>=2.0.0
|
| 7 |
-
httpx>=0.24.0
|
| 8 |
-
websockets>=11.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/server/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
"""AETHER-TaskFlow server package."""
|
|
|
|
|
|
aether-taskflow/server/__pycache__/__init__.cpython-312.pyc
DELETED
|
Binary file (196 Bytes)
|
|
|
aether-taskflow/server/__pycache__/app.cpython-312.pyc
DELETED
|
Binary file (42.5 kB)
|
|
|
aether-taskflow/server/app.py
DELETED
|
@@ -1,1068 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
FastAPI application for the AETHER-TaskFlow Environment.
|
| 3 |
-
|
| 4 |
-
Endpoints:
|
| 5 |
-
GET / - Interactive Mission Control Dashboard
|
| 6 |
-
POST /reset - Reset the environment, return initial observation
|
| 7 |
-
POST /step - Execute an action, return next observation
|
| 8 |
-
GET /state - Return current internal state
|
| 9 |
-
GET /schema - Return action/observation/state JSON schemas
|
| 10 |
-
GET /health - Health check
|
| 11 |
-
WS /ws - WebSocket for persistent sessions
|
| 12 |
-
GET /docs - Swagger UI
|
| 13 |
-
"""
|
| 14 |
-
|
| 15 |
-
from __future__ import annotations
|
| 16 |
-
|
| 17 |
-
import os
|
| 18 |
-
import sys
|
| 19 |
-
from pathlib import Path
|
| 20 |
-
from typing import Any, Dict
|
| 21 |
-
|
| 22 |
-
from fastapi import Body, HTTPException, status
|
| 23 |
-
from fastapi.responses import HTMLResponse
|
| 24 |
-
from openenv.core.env_server.http_server import create_app
|
| 25 |
-
from openenv.core.env_server.types import ResetRequest, ResetResponse, SchemaResponse, StepResponse
|
| 26 |
-
from openenv.core.env_server.web_interface import WebInterfaceManager
|
| 27 |
-
from pydantic import ValidationError
|
| 28 |
-
|
| 29 |
-
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 30 |
-
sys.path.insert(0, str(_REPO_ROOT))
|
| 31 |
-
|
| 32 |
-
try:
|
| 33 |
-
from models import AetherTaskFlowAction, AetherTaskFlowObservation, AetherTaskFlowState
|
| 34 |
-
from env.aether_env import AetherTaskFlowEnvironment
|
| 35 |
-
except ModuleNotFoundError:
|
| 36 |
-
sys.path.insert(0, str(_REPO_ROOT))
|
| 37 |
-
from models import AetherTaskFlowAction, AetherTaskFlowObservation, AetherTaskFlowState
|
| 38 |
-
from env.aether_env import AetherTaskFlowEnvironment
|
| 39 |
-
|
| 40 |
-
_DIFFICULTY = os.getenv("AETHER_DIFFICULTY", "easy")
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def _make_env() -> AetherTaskFlowEnvironment:
|
| 44 |
-
difficulty = os.getenv("AETHER_DIFFICULTY", _DIFFICULTY)
|
| 45 |
-
return AetherTaskFlowEnvironment(difficulty=difficulty)
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def _create_persistent_manager() -> WebInterfaceManager:
|
| 49 |
-
temp_env = _make_env()
|
| 50 |
-
metadata: dict = {}
|
| 51 |
-
try:
|
| 52 |
-
if hasattr(temp_env, "get_metadata"):
|
| 53 |
-
metadata = temp_env.get_metadata()
|
| 54 |
-
except Exception:
|
| 55 |
-
pass
|
| 56 |
-
finally:
|
| 57 |
-
if hasattr(temp_env, "close"):
|
| 58 |
-
try:
|
| 59 |
-
temp_env.close()
|
| 60 |
-
except Exception:
|
| 61 |
-
pass
|
| 62 |
-
return WebInterfaceManager(_make_env, AetherTaskFlowAction, AetherTaskFlowObservation, metadata=metadata)
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
app = create_app(
|
| 66 |
-
_make_env, AetherTaskFlowAction, AetherTaskFlowObservation,
|
| 67 |
-
env_name="aether_taskflow", max_concurrent_envs=4,
|
| 68 |
-
)
|
| 69 |
-
|
| 70 |
-
_persistent_manager = _create_persistent_manager()
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def _remove_route(path: str, method: str) -> None:
|
| 74 |
-
app.router.routes = [
|
| 75 |
-
route for route in app.router.routes
|
| 76 |
-
if not (getattr(route, "path", None) == path and method in (getattr(route, "methods", set()) or set()))
|
| 77 |
-
]
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
for _path, _method in (("/reset", "POST"), ("/step", "POST"), ("/state", "GET"), ("/schema", "GET")):
|
| 81 |
-
_remove_route(_path, _method)
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def _action_to_payload(action: Any) -> Dict[str, Any]:
|
| 85 |
-
if hasattr(action, "model_dump"):
|
| 86 |
-
return action.model_dump(exclude={"metadata"})
|
| 87 |
-
if isinstance(action, dict):
|
| 88 |
-
return action
|
| 89 |
-
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid action")
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def _extract_action_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
|
| 93 |
-
env = _persistent_manager.env
|
| 94 |
-
if "message" in payload and isinstance(payload["message"], str):
|
| 95 |
-
if hasattr(env, "message_to_action"):
|
| 96 |
-
return _action_to_payload(env.message_to_action(payload["message"]))
|
| 97 |
-
return {"message": payload["message"]}
|
| 98 |
-
action_payload = payload.get("action", payload)
|
| 99 |
-
if isinstance(action_payload, str):
|
| 100 |
-
if hasattr(env, "message_to_action"):
|
| 101 |
-
return _action_to_payload(env.message_to_action(action_payload))
|
| 102 |
-
return {"message": action_payload}
|
| 103 |
-
if isinstance(action_payload, dict):
|
| 104 |
-
if "message" in action_payload and isinstance(action_payload["message"], str):
|
| 105 |
-
if hasattr(env, "message_to_action"):
|
| 106 |
-
return _action_to_payload(env.message_to_action(action_payload["message"]))
|
| 107 |
-
return {"message": action_payload["message"]}
|
| 108 |
-
return action_payload
|
| 109 |
-
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid payload")
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
@app.get("/", response_class=HTMLResponse, tags=["UI"], summary="Mission Control Dashboard")
|
| 114 |
-
async def root() -> HTMLResponse:
|
| 115 |
-
difficulty = os.getenv("AETHER_DIFFICULTY", "easy")
|
| 116 |
-
html = f"""<!DOCTYPE html>
|
| 117 |
-
<html lang="en">
|
| 118 |
-
<head>
|
| 119 |
-
<meta charset="UTF-8"/>
|
| 120 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 121 |
-
<title>AETHER-TaskFlow · Mission Control</title>
|
| 122 |
-
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
| 123 |
-
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet"/>
|
| 124 |
-
<style>
|
| 125 |
-
:root {{
|
| 126 |
-
--bg: #050810;
|
| 127 |
-
--panel: #0b0f1e;
|
| 128 |
-
--border: #1a2040;
|
| 129 |
-
--border2: #252d50;
|
| 130 |
-
--accent: #3d7eff;
|
| 131 |
-
--accent2: #6b3dff;
|
| 132 |
-
--cyan: #00d4ff;
|
| 133 |
-
--green: #00e676;
|
| 134 |
-
--amber: #ffab00;
|
| 135 |
-
--red: #ff3d3d;
|
| 136 |
-
--text: #cdd6f4;
|
| 137 |
-
--muted: #6c7a9c;
|
| 138 |
-
--glow: rgba(61,126,255,0.18);
|
| 139 |
-
}}
|
| 140 |
-
*, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
| 141 |
-
|
| 142 |
-
body {{
|
| 143 |
-
font-family: 'Syne', sans-serif;
|
| 144 |
-
background: var(--bg);
|
| 145 |
-
color: var(--text);
|
| 146 |
-
min-height: 100vh;
|
| 147 |
-
overflow-x: hidden;
|
| 148 |
-
}}
|
| 149 |
-
|
| 150 |
-
/* Grid scanline overlay */
|
| 151 |
-
body::before {{
|
| 152 |
-
content: '';
|
| 153 |
-
position: fixed; inset: 0;
|
| 154 |
-
background:
|
| 155 |
-
repeating-linear-gradient(0deg, transparent, transparent 39px, rgba(61,126,255,0.03) 40px),
|
| 156 |
-
repeating-linear-gradient(90deg, transparent, transparent 39px, rgba(61,126,255,0.03) 40px);
|
| 157 |
-
pointer-events: none; z-index: 0;
|
| 158 |
-
}}
|
| 159 |
-
|
| 160 |
-
/* ── TOP BAR ── */
|
| 161 |
-
header {{
|
| 162 |
-
position: relative; z-index: 10;
|
| 163 |
-
display: flex; align-items: center; justify-content: space-between;
|
| 164 |
-
padding: 0 28px;
|
| 165 |
-
height: 60px;
|
| 166 |
-
background: rgba(11,15,30,0.95);
|
| 167 |
-
border-bottom: 1px solid var(--border2);
|
| 168 |
-
backdrop-filter: blur(12px);
|
| 169 |
-
}}
|
| 170 |
-
.logo {{
|
| 171 |
-
display: flex; align-items: center; gap: 12px;
|
| 172 |
-
}}
|
| 173 |
-
.logo-icon {{
|
| 174 |
-
width: 32px; height: 32px;
|
| 175 |
-
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
| 176 |
-
border-radius: 8px;
|
| 177 |
-
display: flex; align-items: center; justify-content: center;
|
| 178 |
-
font-size: 16px;
|
| 179 |
-
box-shadow: 0 0 20px rgba(61,126,255,0.4);
|
| 180 |
-
}}
|
| 181 |
-
.logo-text {{ font-size: 1rem; font-weight: 800; letter-spacing: -0.02em; }}
|
| 182 |
-
.logo-sub {{ font-size: 0.7rem; color: var(--muted); font-family: 'Space Mono', monospace; margin-top: 1px; }}
|
| 183 |
-
.header-links {{ display: flex; gap: 6px; }}
|
| 184 |
-
.hlink {{
|
| 185 |
-
font-family: 'Space Mono', monospace;
|
| 186 |
-
font-size: 0.68rem; color: var(--muted);
|
| 187 |
-
text-decoration: none; padding: 5px 10px;
|
| 188 |
-
border: 1px solid var(--border2); border-radius: 4px;
|
| 189 |
-
transition: all .15s;
|
| 190 |
-
}}
|
| 191 |
-
.hlink:hover {{ color: var(--cyan); border-color: var(--cyan); }}
|
| 192 |
-
.live-indicator {{
|
| 193 |
-
display: flex; align-items: center; gap: 6px;
|
| 194 |
-
font-family: 'Space Mono', monospace; font-size: 0.68rem; color: var(--green);
|
| 195 |
-
}}
|
| 196 |
-
.live-dot {{
|
| 197 |
-
width: 6px; height: 6px; border-radius: 50%;
|
| 198 |
-
background: var(--green);
|
| 199 |
-
box-shadow: 0 0 6px var(--green);
|
| 200 |
-
animation: pulse-dot 1.8s ease-in-out infinite;
|
| 201 |
-
}}
|
| 202 |
-
@keyframes pulse-dot {{
|
| 203 |
-
0%,100% {{ opacity: 1; transform: scale(1); }}
|
| 204 |
-
50% {{ opacity: 0.5; transform: scale(0.7); }}
|
| 205 |
-
}}
|
| 206 |
-
|
| 207 |
-
/* ── LAYOUT ── */
|
| 208 |
-
.workspace {{
|
| 209 |
-
position: relative; z-index: 1;
|
| 210 |
-
display: grid;
|
| 211 |
-
grid-template-columns: 300px 1fr 260px;
|
| 212 |
-
grid-template-rows: auto 1fr auto;
|
| 213 |
-
gap: 0;
|
| 214 |
-
height: calc(100vh - 60px);
|
| 215 |
-
overflow: hidden;
|
| 216 |
-
}}
|
| 217 |
-
|
| 218 |
-
/* ── COMMAND BAR ── */
|
| 219 |
-
.cmd-bar {{
|
| 220 |
-
grid-column: 1 / -1;
|
| 221 |
-
display: flex; align-items: center; gap: 10px;
|
| 222 |
-
padding: 10px 20px;
|
| 223 |
-
background: rgba(11,15,30,0.8);
|
| 224 |
-
border-bottom: 1px solid var(--border);
|
| 225 |
-
}}
|
| 226 |
-
.cmd-label {{
|
| 227 |
-
font-family: 'Space Mono', monospace;
|
| 228 |
-
font-size: 0.65rem; color: var(--muted);
|
| 229 |
-
text-transform: uppercase; letter-spacing: .1em;
|
| 230 |
-
white-space: nowrap;
|
| 231 |
-
}}
|
| 232 |
-
select.diff-select {{
|
| 233 |
-
background: var(--panel); border: 1px solid var(--border2);
|
| 234 |
-
color: var(--text); padding: 7px 14px; border-radius: 6px;
|
| 235 |
-
font-family: 'Space Mono', monospace; font-size: 0.75rem;
|
| 236 |
-
cursor: pointer; outline: none;
|
| 237 |
-
transition: border-color .15s;
|
| 238 |
-
}}
|
| 239 |
-
select.diff-select:focus {{ border-color: var(--accent); }}
|
| 240 |
-
.cmd-btn {{
|
| 241 |
-
padding: 7px 18px; border: none; border-radius: 6px;
|
| 242 |
-
font-family: 'Syne', sans-serif; font-size: 0.78rem; font-weight: 700;
|
| 243 |
-
cursor: pointer; transition: all .15s; white-space: nowrap;
|
| 244 |
-
}}
|
| 245 |
-
.cmd-btn:hover {{ transform: translateY(-1px); filter: brightness(1.1); }}
|
| 246 |
-
.cmd-btn:active {{ transform: translateY(0); }}
|
| 247 |
-
.cmd-btn:disabled {{ opacity: 0.35; cursor: not-allowed; transform: none; }}
|
| 248 |
-
.btn-reset {{ background: linear-gradient(135deg, var(--accent), var(--accent2)); color: white; }}
|
| 249 |
-
.btn-step {{ background: var(--green); color: #000; }}
|
| 250 |
-
.btn-auto {{ background: var(--amber); color: #000; }}
|
| 251 |
-
.btn-stop {{ background: var(--red); color: white; }}
|
| 252 |
-
.status-pill {{
|
| 253 |
-
margin-left: auto;
|
| 254 |
-
font-family: 'Space Mono', monospace; font-size: 0.68rem;
|
| 255 |
-
padding: 4px 12px; border-radius: 20px;
|
| 256 |
-
border: 1px solid var(--border2); color: var(--muted);
|
| 257 |
-
transition: all .3s;
|
| 258 |
-
}}
|
| 259 |
-
.status-pill.ready {{ border-color: var(--green); color: var(--green); }}
|
| 260 |
-
.status-pill.done {{ border-color: var(--amber); color: var(--amber); }}
|
| 261 |
-
|
| 262 |
-
/* ── LEFT PANEL: Task Queue ── */
|
| 263 |
-
.panel-left {{
|
| 264 |
-
grid-row: 2;
|
| 265 |
-
background: var(--panel);
|
| 266 |
-
border-right: 1px solid var(--border);
|
| 267 |
-
display: flex; flex-direction: column;
|
| 268 |
-
overflow: hidden;
|
| 269 |
-
}}
|
| 270 |
-
.panel-title {{
|
| 271 |
-
padding: 12px 16px 8px;
|
| 272 |
-
font-size: 0.65rem; font-weight: 700;
|
| 273 |
-
text-transform: uppercase; letter-spacing: .12em;
|
| 274 |
-
color: var(--muted);
|
| 275 |
-
border-bottom: 1px solid var(--border);
|
| 276 |
-
display: flex; align-items: center; justify-content: space-between;
|
| 277 |
-
}}
|
| 278 |
-
.task-scroll {{
|
| 279 |
-
flex: 1; overflow-y: auto; padding: 10px;
|
| 280 |
-
scrollbar-width: thin; scrollbar-color: var(--border2) transparent;
|
| 281 |
-
}}
|
| 282 |
-
.task-card {{
|
| 283 |
-
border: 1px solid var(--border2);
|
| 284 |
-
border-radius: 8px; padding: 10px 12px;
|
| 285 |
-
margin-bottom: 8px; cursor: pointer;
|
| 286 |
-
transition: all .15s;
|
| 287 |
-
position: relative; overflow: hidden;
|
| 288 |
-
}}
|
| 289 |
-
.task-card::before {{
|
| 290 |
-
content: '';
|
| 291 |
-
position: absolute; left: 0; top: 0; bottom: 0;
|
| 292 |
-
width: 3px;
|
| 293 |
-
background: var(--muted);
|
| 294 |
-
transition: background .15s;
|
| 295 |
-
}}
|
| 296 |
-
.task-card:hover {{ border-color: var(--accent); background: rgba(61,126,255,0.05); }}
|
| 297 |
-
.task-card:hover::before {{ background: var(--accent); }}
|
| 298 |
-
.task-card.selected {{ border-color: var(--accent); background: rgba(61,126,255,0.08); box-shadow: 0 0 0 1px rgba(61,126,255,0.2); }}
|
| 299 |
-
.task-card.selected::before {{ background: var(--accent); box-shadow: 0 0 8px var(--accent); }}
|
| 300 |
-
.task-card.urgent {{ border-color: rgba(255,61,61,0.4); }}
|
| 301 |
-
.task-card.urgent::before {{ background: var(--red); }}
|
| 302 |
-
.task-name {{ font-size: 0.78rem; font-weight: 600; margin-bottom: 6px; line-height: 1.3; padding-left: 6px; }}
|
| 303 |
-
.task-chips {{ display: flex; flex-wrap: wrap; gap: 4px; padding-left: 6px; }}
|
| 304 |
-
.chip {{
|
| 305 |
-
font-family: 'Space Mono', monospace;
|
| 306 |
-
font-size: 0.6rem; padding: 2px 5px;
|
| 307 |
-
border-radius: 3px; border: 1px solid var(--border2);
|
| 308 |
-
color: var(--muted);
|
| 309 |
-
}}
|
| 310 |
-
.chip.hi {{ color: var(--green); border-color: rgba(0,230,118,0.3); }}
|
| 311 |
-
.chip.mid {{ color: var(--amber); border-color: rgba(255,171,0,0.3); }}
|
| 312 |
-
.chip.lo {{ color: var(--red); border-color: rgba(255,61,61,0.3); }}
|
| 313 |
-
.chip.val {{ color: var(--cyan); border-color: rgba(0,212,255,0.3); }}
|
| 314 |
-
.task-empty {{
|
| 315 |
-
text-align: center; padding: 40px 16px;
|
| 316 |
-
color: var(--muted); font-size: 0.8rem; line-height: 1.6;
|
| 317 |
-
}}
|
| 318 |
-
|
| 319 |
-
/* ── CENTRE: Visualisation ── */
|
| 320 |
-
.panel-centre {{
|
| 321 |
-
grid-row: 2;
|
| 322 |
-
display: flex; flex-direction: column;
|
| 323 |
-
overflow: hidden;
|
| 324 |
-
border-right: 1px solid var(--border);
|
| 325 |
-
}}
|
| 326 |
-
|
| 327 |
-
/* Resource meters */
|
| 328 |
-
.resource-row {{
|
| 329 |
-
display: grid; grid-template-columns: repeat(4, 1fr);
|
| 330 |
-
gap: 10px; padding: 14px 16px;
|
| 331 |
-
border-bottom: 1px solid var(--border);
|
| 332 |
-
background: rgba(11,15,30,0.5);
|
| 333 |
-
}}
|
| 334 |
-
.res-cell {{
|
| 335 |
-
background: var(--panel);
|
| 336 |
-
border: 1px solid var(--border2);
|
| 337 |
-
border-radius: 8px; padding: 10px 14px;
|
| 338 |
-
position: relative; overflow: hidden;
|
| 339 |
-
}}
|
| 340 |
-
.res-cell::after {{
|
| 341 |
-
content: '';
|
| 342 |
-
position: absolute; bottom: 0; left: 0;
|
| 343 |
-
height: 2px; width: var(--fill, 0%);
|
| 344 |
-
background: var(--fill-color, var(--accent));
|
| 345 |
-
transition: width .5s ease, background .5s ease;
|
| 346 |
-
}}
|
| 347 |
-
.res-label {{
|
| 348 |
-
font-family: 'Space Mono', monospace;
|
| 349 |
-
font-size: 0.6rem; color: var(--muted);
|
| 350 |
-
text-transform: uppercase; letter-spacing: .08em;
|
| 351 |
-
}}
|
| 352 |
-
.res-value {{
|
| 353 |
-
font-family: 'Space Mono', monospace;
|
| 354 |
-
font-size: 1.25rem; font-weight: 700;
|
| 355 |
-
color: var(--text); margin: 4px 0 0;
|
| 356 |
-
}}
|
| 357 |
-
|
| 358 |
-
/* Action picker */
|
| 359 |
-
.action-panel {{
|
| 360 |
-
padding: 12px 16px;
|
| 361 |
-
border-bottom: 1px solid var(--border);
|
| 362 |
-
background: rgba(11,15,30,0.3);
|
| 363 |
-
}}
|
| 364 |
-
.action-grid {{ display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; margin-top: 8px; }}
|
| 365 |
-
.act-btn {{
|
| 366 |
-
border: 1px solid var(--border2);
|
| 367 |
-
border-radius: 8px; padding: 10px 6px;
|
| 368 |
-
cursor: pointer; text-align: center;
|
| 369 |
-
transition: all .15s; background: var(--panel);
|
| 370 |
-
color: var(--muted); font-family: 'Syne', sans-serif;
|
| 371 |
-
}}
|
| 372 |
-
.act-btn:hover {{ border-color: var(--accent); color: var(--text); background: rgba(61,126,255,0.07); }}
|
| 373 |
-
.act-btn.active {{
|
| 374 |
-
border-color: var(--accent); color: var(--accent);
|
| 375 |
-
background: rgba(61,126,255,0.12);
|
| 376 |
-
box-shadow: 0 0 12px rgba(61,126,255,0.15);
|
| 377 |
-
}}
|
| 378 |
-
.act-btn .act-icon {{ font-size: 1.2rem; display: block; margin-bottom: 3px; }}
|
| 379 |
-
.act-btn .act-name {{ font-size: 0.72rem; font-weight: 700; }}
|
| 380 |
-
.act-btn .act-sub {{ font-size: 0.6rem; color: var(--muted); margin-top: 1px; font-family: 'Space Mono', monospace; }}
|
| 381 |
-
|
| 382 |
-
/* Outcome text */
|
| 383 |
-
.outcome-bar {{
|
| 384 |
-
padding: 10px 16px;
|
| 385 |
-
border-bottom: 1px solid var(--border);
|
| 386 |
-
font-family: 'Space Mono', monospace;
|
| 387 |
-
font-size: 0.72rem; color: var(--muted);
|
| 388 |
-
background: rgba(5,8,16,0.5);
|
| 389 |
-
min-height: 38px;
|
| 390 |
-
}}
|
| 391 |
-
.outcome-bar span {{ color: var(--cyan); }}
|
| 392 |
-
|
| 393 |
-
/* Reward chart area */
|
| 394 |
-
.chart-area {{
|
| 395 |
-
flex: 1; padding: 14px 16px;
|
| 396 |
-
display: flex; flex-direction: column; gap: 10px;
|
| 397 |
-
}}
|
| 398 |
-
.chart-title {{
|
| 399 |
-
font-size: 0.62rem; font-weight: 700;
|
| 400 |
-
text-transform: uppercase; letter-spacing: .1em;
|
| 401 |
-
color: var(--muted);
|
| 402 |
-
}}
|
| 403 |
-
.reward-chart {{
|
| 404 |
-
flex: 1;
|
| 405 |
-
display: flex; align-items: flex-end;
|
| 406 |
-
gap: 4px; border-bottom: 1px solid var(--border2);
|
| 407 |
-
padding-bottom: 4px; min-height: 80px;
|
| 408 |
-
}}
|
| 409 |
-
.reward-bar {{
|
| 410 |
-
flex: 1; min-width: 8px; border-radius: 3px 3px 0 0;
|
| 411 |
-
transition: height .4s ease, background .4s;
|
| 412 |
-
position: relative;
|
| 413 |
-
}}
|
| 414 |
-
.reward-bar::after {{
|
| 415 |
-
content: attr(data-val);
|
| 416 |
-
position: absolute; bottom: calc(100% + 2px); left: 50%;
|
| 417 |
-
transform: translateX(-50%);
|
| 418 |
-
font-family: 'Space Mono', monospace; font-size: 0.5rem;
|
| 419 |
-
color: var(--muted); white-space: nowrap;
|
| 420 |
-
opacity: 0; transition: opacity .2s;
|
| 421 |
-
}}
|
| 422 |
-
.reward-bar:hover::after {{ opacity: 1; }}
|
| 423 |
-
|
| 424 |
-
/* ── RIGHT PANEL: Telemetry ── */
|
| 425 |
-
.panel-right {{
|
| 426 |
-
grid-row: 2;
|
| 427 |
-
background: var(--panel);
|
| 428 |
-
display: flex; flex-direction: column;
|
| 429 |
-
overflow-y: auto;
|
| 430 |
-
scrollbar-width: thin; scrollbar-color: var(--border2) transparent;
|
| 431 |
-
}}
|
| 432 |
-
.score-hero {{
|
| 433 |
-
padding: 20px 16px 14px;
|
| 434 |
-
text-align: center;
|
| 435 |
-
border-bottom: 1px solid var(--border);
|
| 436 |
-
position: relative;
|
| 437 |
-
}}
|
| 438 |
-
.score-ring {{
|
| 439 |
-
width: 110px; height: 110px;
|
| 440 |
-
margin: 0 auto 10px;
|
| 441 |
-
position: relative;
|
| 442 |
-
}}
|
| 443 |
-
.score-ring svg {{ transform: rotate(-90deg); }}
|
| 444 |
-
.score-ring .ring-bg {{ fill: none; stroke: var(--border2); stroke-width: 8; }}
|
| 445 |
-
.score-ring .ring-fill {{
|
| 446 |
-
fill: none; stroke-width: 8;
|
| 447 |
-
stroke-linecap: round;
|
| 448 |
-
stroke-dasharray: 283;
|
| 449 |
-
stroke-dashoffset: 283;
|
| 450 |
-
transition: stroke-dashoffset .8s cubic-bezier(.4,0,.2,1), stroke .4s;
|
| 451 |
-
stroke: var(--accent);
|
| 452 |
-
}}
|
| 453 |
-
.score-num {{
|
| 454 |
-
position: absolute; top: 50%; left: 50%;
|
| 455 |
-
transform: translate(-50%,-50%);
|
| 456 |
-
font-family: 'Space Mono', monospace;
|
| 457 |
-
font-size: 1.5rem; font-weight: 700;
|
| 458 |
-
}}
|
| 459 |
-
.score-label {{ font-size: 0.62rem; color: var(--muted); text-transform: uppercase; letter-spacing: .1em; }}
|
| 460 |
-
|
| 461 |
-
.stat-grid {{
|
| 462 |
-
display: grid; grid-template-columns: 1fr 1fr;
|
| 463 |
-
gap: 8px; padding: 12px;
|
| 464 |
-
border-bottom: 1px solid var(--border);
|
| 465 |
-
}}
|
| 466 |
-
.stat-box {{
|
| 467 |
-
background: rgba(5,8,16,0.6);
|
| 468 |
-
border: 1px solid var(--border2);
|
| 469 |
-
border-radius: 8px; padding: 10px;
|
| 470 |
-
text-align: center;
|
| 471 |
-
}}
|
| 472 |
-
.stat-box .sn {{
|
| 473 |
-
font-family: 'Space Mono', monospace;
|
| 474 |
-
font-size: 1.3rem; font-weight: 700;
|
| 475 |
-
}}
|
| 476 |
-
.stat-box .sl {{ font-size: 0.6rem; color: var(--muted); margin-top: 2px; text-transform: uppercase; letter-spacing: .07em; }}
|
| 477 |
-
.stat-box.green .sn {{ color: var(--green); }}
|
| 478 |
-
.stat-box.red .sn {{ color: var(--red); }}
|
| 479 |
-
.stat-box.cyan .sn {{ color: var(--cyan); }}
|
| 480 |
-
.stat-box.amber .sn {{ color: var(--amber); }}
|
| 481 |
-
|
| 482 |
-
/* Health bar */
|
| 483 |
-
.health-section {{ padding: 12px; border-bottom: 1px solid var(--border); }}
|
| 484 |
-
.health-label {{ font-size: 0.62rem; color: var(--muted); text-transform: uppercase; letter-spacing: .1em; margin-bottom: 8px; display: flex; justify-content: space-between; }}
|
| 485 |
-
.health-track {{
|
| 486 |
-
height: 8px; background: var(--border2); border-radius: 4px; overflow: hidden;
|
| 487 |
-
}}
|
| 488 |
-
.health-fill {{
|
| 489 |
-
height: 100%; border-radius: 4px;
|
| 490 |
-
transition: width .5s ease, background .5s ease;
|
| 491 |
-
background: var(--green);
|
| 492 |
-
}}
|
| 493 |
-
|
| 494 |
-
/* Log */
|
| 495 |
-
.log-panel {{ flex: 1; padding: 10px; }}
|
| 496 |
-
.log-title {{ font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); margin-bottom: 8px; }}
|
| 497 |
-
.log-scroll {{
|
| 498 |
-
height: 200px; overflow-y: auto;
|
| 499 |
-
scrollbar-width: thin; scrollbar-color: var(--border2) transparent;
|
| 500 |
-
}}
|
| 501 |
-
.log-entry {{
|
| 502 |
-
font-family: 'Space Mono', monospace;
|
| 503 |
-
font-size: 0.62rem; line-height: 1.7;
|
| 504 |
-
padding: 1px 0; border-bottom: 1px solid rgba(26,32,64,0.4);
|
| 505 |
-
color: var(--muted);
|
| 506 |
-
}}
|
| 507 |
-
.log-entry.pos {{ color: var(--green); }}
|
| 508 |
-
.log-entry.neg {{ color: var(--red); }}
|
| 509 |
-
.log-entry.inf {{ color: var(--cyan); }}
|
| 510 |
-
.log-entry.wrn {{ color: var(--amber); }}
|
| 511 |
-
|
| 512 |
-
/* ── STATUS BAR ── */
|
| 513 |
-
.status-bar {{
|
| 514 |
-
grid-column: 1 / -1;
|
| 515 |
-
display: flex; align-items: center; gap: 16px;
|
| 516 |
-
padding: 6px 20px;
|
| 517 |
-
background: rgba(11,15,30,0.95);
|
| 518 |
-
border-top: 1px solid var(--border);
|
| 519 |
-
font-family: 'Space Mono', monospace;
|
| 520 |
-
font-size: 0.62rem; color: var(--muted);
|
| 521 |
-
}}
|
| 522 |
-
.sb-item {{ display: flex; align-items: center; gap: 6px; }}
|
| 523 |
-
.sb-dot {{ width: 5px; height: 5px; border-radius: 50%; background: var(--muted); }}
|
| 524 |
-
.sb-dot.ok {{ background: var(--green); box-shadow: 0 0 5px var(--green); }}
|
| 525 |
-
.sb-dot.bad {{ background: var(--red); }}
|
| 526 |
-
|
| 527 |
-
/* Spinner */
|
| 528 |
-
.spinner {{
|
| 529 |
-
display: inline-block; width: 12px; height: 12px;
|
| 530 |
-
border: 2px solid rgba(255,255,255,0.2);
|
| 531 |
-
border-top-color: white; border-radius: 50%;
|
| 532 |
-
animation: spin .6s linear infinite;
|
| 533 |
-
vertical-align: middle; margin-right: 5px;
|
| 534 |
-
}}
|
| 535 |
-
@keyframes spin {{ to {{ transform: rotate(360deg); }} }}
|
| 536 |
-
|
| 537 |
-
/* DONE FLASH */
|
| 538 |
-
@keyframes flash-border {{
|
| 539 |
-
0%,100% {{ border-color: var(--border2); }}
|
| 540 |
-
50% {{ border-color: var(--amber); box-shadow: 0 0 20px rgba(255,171,0,0.3); }}
|
| 541 |
-
}}
|
| 542 |
-
.episode-done {{ animation: flash-border 1.5s ease 2; }}
|
| 543 |
-
</style>
|
| 544 |
-
</head>
|
| 545 |
-
<body>
|
| 546 |
-
|
| 547 |
-
<!-- ── TOP BAR ── -->
|
| 548 |
-
<header>
|
| 549 |
-
<div class="logo">
|
| 550 |
-
<div class="logo-icon">⚡</div>
|
| 551 |
-
<div>
|
| 552 |
-
<div class="logo-text">AETHER-TaskFlow</div>
|
| 553 |
-
<div class="logo-sub">OPENENV · RL ENVIRONMENT · v1.0.0</div>
|
| 554 |
-
</div>
|
| 555 |
-
</div>
|
| 556 |
-
<div class="live-indicator"><div class="live-dot"></div>LIVE</div>
|
| 557 |
-
<div class="header-links">
|
| 558 |
-
<a class="hlink" href="/docs">Swagger</a>
|
| 559 |
-
<a class="hlink" href="/schema">Schema</a>
|
| 560 |
-
<a class="hlink" href="/state">State</a>
|
| 561 |
-
<a class="hlink" href="/health">Health</a>
|
| 562 |
-
</div>
|
| 563 |
-
</header>
|
| 564 |
-
|
| 565 |
-
<!-- ── WORKSPACE ── -->
|
| 566 |
-
<div class="workspace">
|
| 567 |
-
|
| 568 |
-
<!-- COMMAND BAR -->
|
| 569 |
-
<div class="cmd-bar">
|
| 570 |
-
<span class="cmd-label">Difficulty</span>
|
| 571 |
-
<select class="diff-select" id="difficulty">
|
| 572 |
-
<option value="easy" {"selected" if difficulty=="easy" else ""}>🟢 EASY — 5 tasks, stable</option>
|
| 573 |
-
<option value="medium" {"selected" if difficulty=="medium" else ""}>🟡 MEDIUM — 8 tasks, dynamic</option>
|
| 574 |
-
<option value="hard" {"selected" if difficulty=="hard" else ""}>🔴 HARD — 12 tasks, scarce</option>
|
| 575 |
-
</select>
|
| 576 |
-
<button class="cmd-btn btn-reset" id="reset-btn" onclick="resetEnv()">
|
| 577 |
-
<span id="reset-spinner" style="display:none" class="spinner"></span>⟳ RESET
|
| 578 |
-
</button>
|
| 579 |
-
<button class="cmd-btn btn-step" id="step-btn" onclick="stepEnv()" disabled>▶ STEP</button>
|
| 580 |
-
<button class="cmd-btn btn-auto" id="auto-btn" onclick="toggleAuto()" disabled>⏩ AUTO</button>
|
| 581 |
-
<span class="status-pill" id="status-pill">STANDBY</span>
|
| 582 |
-
</div>
|
| 583 |
-
|
| 584 |
-
<!-- LEFT: Task Queue -->
|
| 585 |
-
<div class="panel-left">
|
| 586 |
-
<div class="panel-title">
|
| 587 |
-
<span>📋 TASK QUEUE</span>
|
| 588 |
-
<span id="task-badge" style="font-family:'Space Mono',monospace;font-size:0.65rem;color:var(--accent);">0 tasks</span>
|
| 589 |
-
</div>
|
| 590 |
-
<div class="task-scroll" id="task-list">
|
| 591 |
-
<div class="task-empty">
|
| 592 |
-
Reset the environment<br/>to load tasks
|
| 593 |
-
</div>
|
| 594 |
-
</div>
|
| 595 |
-
</div>
|
| 596 |
-
|
| 597 |
-
<!-- CENTRE: Main visualisation -->
|
| 598 |
-
<div class="panel-centre">
|
| 599 |
-
|
| 600 |
-
<!-- Resource meters -->
|
| 601 |
-
<div class="resource-row">
|
| 602 |
-
<div class="res-cell" id="rc-time">
|
| 603 |
-
<div class="res-label">⏱ Time Remaining</div>
|
| 604 |
-
<div class="res-value" id="res-time">—</div>
|
| 605 |
-
</div>
|
| 606 |
-
<div class="res-cell" id="rc-energy">
|
| 607 |
-
<div class="res-label">⚡ Energy</div>
|
| 608 |
-
<div class="res-value" id="res-energy">—</div>
|
| 609 |
-
</div>
|
| 610 |
-
<div class="res-cell" id="rc-budget">
|
| 611 |
-
<div class="res-label">💰 Budget</div>
|
| 612 |
-
<div class="res-value" id="res-budget">—</div>
|
| 613 |
-
</div>
|
| 614 |
-
<div class="res-cell" id="rc-value">
|
| 615 |
-
<div class="res-label">🏆 Cumulative Value</div>
|
| 616 |
-
<div class="res-value" id="res-value">—</div>
|
| 617 |
-
</div>
|
| 618 |
-
</div>
|
| 619 |
-
|
| 620 |
-
<!-- Action picker -->
|
| 621 |
-
<div class="action-panel">
|
| 622 |
-
<div class="chart-title">SELECT ACTION</div>
|
| 623 |
-
<div class="action-grid">
|
| 624 |
-
<div class="act-btn active" onclick="selectAction('execute')" id="act-execute">
|
| 625 |
-
<span class="act-icon">⚡</span>
|
| 626 |
-
<div class="act-name">EXECUTE</div>
|
| 627 |
-
<div class="act-sub">Full reward</div>
|
| 628 |
-
</div>
|
| 629 |
-
<div class="act-btn" onclick="selectAction('optimize')" id="act-optimize">
|
| 630 |
-
<span class="act-icon">🔧</span>
|
| 631 |
-
<div class="act-name">OPTIMIZE</div>
|
| 632 |
-
<div class="act-sub">Reduce risk</div>
|
| 633 |
-
</div>
|
| 634 |
-
<div class="act-btn" onclick="selectAction('delegate')" id="act-delegate">
|
| 635 |
-
<span class="act-icon">📤</span>
|
| 636 |
-
<div class="act-name">DELEGATE</div>
|
| 637 |
-
<div class="act-sub">35% reward</div>
|
| 638 |
-
</div>
|
| 639 |
-
<div class="act-btn" onclick="selectAction('defer')" id="act-defer">
|
| 640 |
-
<span class="act-icon">⏳</span>
|
| 641 |
-
<div class="act-name">DEFER</div>
|
| 642 |
-
<div class="act-sub">Small penalty</div>
|
| 643 |
-
</div>
|
| 644 |
-
</div>
|
| 645 |
-
</div>
|
| 646 |
-
|
| 647 |
-
<!-- Outcome readout -->
|
| 648 |
-
<div class="outcome-bar" id="outcome-bar">
|
| 649 |
-
<span>›</span> Waiting for first action...
|
| 650 |
-
</div>
|
| 651 |
-
|
| 652 |
-
<!-- Reward history chart -->
|
| 653 |
-
<div class="chart-area">
|
| 654 |
-
<div class="chart-title">REWARD HISTORY — per step</div>
|
| 655 |
-
<div class="reward-chart" id="reward-chart">
|
| 656 |
-
<div style="color:var(--muted);font-size:0.72rem;width:100%;text-align:center;padding-bottom:10px;">No data yet</div>
|
| 657 |
-
</div>
|
| 658 |
-
</div>
|
| 659 |
-
</div>
|
| 660 |
-
|
| 661 |
-
<!-- RIGHT: Telemetry -->
|
| 662 |
-
<div class="panel-right">
|
| 663 |
-
|
| 664 |
-
<!-- Score ring -->
|
| 665 |
-
<div class="score-hero" id="score-hero">
|
| 666 |
-
<div class="score-ring">
|
| 667 |
-
<svg viewBox="0 0 100 100" width="110" height="110">
|
| 668 |
-
<circle class="ring-bg" cx="50" cy="50" r="45"/>
|
| 669 |
-
<circle class="ring-fill" id="ring-fill" cx="50" cy="50" r="45"/>
|
| 670 |
-
</svg>
|
| 671 |
-
<div class="score-num" id="score-num">—</div>
|
| 672 |
-
</div>
|
| 673 |
-
<div class="score-label">Episode Score</div>
|
| 674 |
-
</div>
|
| 675 |
-
|
| 676 |
-
<!-- Stats -->
|
| 677 |
-
<div class="stat-grid">
|
| 678 |
-
<div class="stat-box cyan">
|
| 679 |
-
<div class="sn" id="stat-step">0</div>
|
| 680 |
-
<div class="sl">Steps</div>
|
| 681 |
-
</div>
|
| 682 |
-
<div class="stat-box amber">
|
| 683 |
-
<div class="sn" id="stat-reward">0.00</div>
|
| 684 |
-
<div class="sl">Last Reward</div>
|
| 685 |
-
</div>
|
| 686 |
-
<div class="stat-box green">
|
| 687 |
-
<div class="sn" id="stat-done">0</div>
|
| 688 |
-
<div class="sl">Completed</div>
|
| 689 |
-
</div>
|
| 690 |
-
<div class="stat-box red">
|
| 691 |
-
<div class="sn" id="stat-fail">0</div>
|
| 692 |
-
<div class="sl">Failed</div>
|
| 693 |
-
</div>
|
| 694 |
-
</div>
|
| 695 |
-
|
| 696 |
-
<!-- Health -->
|
| 697 |
-
<div class="health-section">
|
| 698 |
-
<div class="health-label">
|
| 699 |
-
<span>SYSTEM HEALTH</span>
|
| 700 |
-
<span id="health-pct" style="color:var(--green);font-family:'Space Mono',monospace;">—</span>
|
| 701 |
-
</div>
|
| 702 |
-
<div class="health-track">
|
| 703 |
-
<div class="health-fill" id="health-fill" style="width:0%"></div>
|
| 704 |
-
</div>
|
| 705 |
-
</div>
|
| 706 |
-
|
| 707 |
-
<!-- Log -->
|
| 708 |
-
<div class="log-panel">
|
| 709 |
-
<div class="log-title">ACTION LOG</div>
|
| 710 |
-
<div class="log-scroll" id="log">
|
| 711 |
-
<div class="log-entry inf">› System online. Select difficulty and press RESET.</div>
|
| 712 |
-
</div>
|
| 713 |
-
</div>
|
| 714 |
-
</div>
|
| 715 |
-
|
| 716 |
-
<!-- STATUS BAR -->
|
| 717 |
-
<div class="status-bar">
|
| 718 |
-
<div class="sb-item"><div class="sb-dot ok" id="sb-api"></div>API ONLINE</div>
|
| 719 |
-
<div class="sb-item"><div class="sb-dot" id="sb-ep"></div><span id="sb-ep-text">NO EPISODE</span></div>
|
| 720 |
-
<div class="sb-item" style="margin-left:auto;">
|
| 721 |
-
<span>Meta PyTorch OpenEnv Hackathon 2025</span>
|
| 722 |
-
</div>
|
| 723 |
-
<div class="sb-item">
|
| 724 |
-
<a href="/docs" style="color:var(--muted);text-decoration:none;">OpenAPI ↗</a>
|
| 725 |
-
</div>
|
| 726 |
-
</div>
|
| 727 |
-
</div>
|
| 728 |
-
|
| 729 |
-
<script>
|
| 730 |
-
// ── STATE ──
|
| 731 |
-
let selectedTask = null;
|
| 732 |
-
let selectedAction = 'execute';
|
| 733 |
-
let episodeDone = false;
|
| 734 |
-
let autoTimer = null;
|
| 735 |
-
let rewardHistory = [];
|
| 736 |
-
let stepCount = 0;
|
| 737 |
-
|
| 738 |
-
// ── HELPERS ──
|
| 739 |
-
function log(msg, cls='') {{
|
| 740 |
-
const container = document.getElementById('log');
|
| 741 |
-
const el = document.createElement('div');
|
| 742 |
-
el.className = 'log-entry ' + cls;
|
| 743 |
-
el.textContent = new Date().toLocaleTimeString('en-GB', {{hour:'2-digit',minute:'2-digit',second:'2-digit'}}) + ' › ' + msg;
|
| 744 |
-
container.appendChild(el);
|
| 745 |
-
container.scrollTop = container.scrollHeight;
|
| 746 |
-
}}
|
| 747 |
-
|
| 748 |
-
function setStatusPill(text, cls='') {{
|
| 749 |
-
const p = document.getElementById('status-pill');
|
| 750 |
-
p.textContent = text;
|
| 751 |
-
p.className = 'status-pill ' + cls;
|
| 752 |
-
}}
|
| 753 |
-
|
| 754 |
-
function selectAction(a) {{
|
| 755 |
-
selectedAction = a;
|
| 756 |
-
['execute','optimize','delegate','defer'].forEach(x => {{
|
| 757 |
-
document.getElementById('act-' + x).classList.toggle('active', x === a);
|
| 758 |
-
}});
|
| 759 |
-
}}
|
| 760 |
-
|
| 761 |
-
function selectTask(id) {{
|
| 762 |
-
selectedTask = id;
|
| 763 |
-
document.querySelectorAll('.task-card').forEach(c => {{
|
| 764 |
-
c.classList.toggle('selected', parseInt(c.dataset.id) === id);
|
| 765 |
-
}});
|
| 766 |
-
}}
|
| 767 |
-
|
| 768 |
-
// ── RESOURCES ──
|
| 769 |
-
function updateResources(obs) {{
|
| 770 |
-
const time = obs.time_remaining ?? 0;
|
| 771 |
-
const energy = obs.energy_remaining ?? 0;
|
| 772 |
-
const budget = obs.budget_remaining ?? 0;
|
| 773 |
-
const value = obs.cumulative_value ?? 0;
|
| 774 |
-
const health = obs.system_health ?? 1;
|
| 775 |
-
|
| 776 |
-
document.getElementById('res-time').textContent = time;
|
| 777 |
-
document.getElementById('res-energy').textContent = energy.toFixed(1);
|
| 778 |
-
document.getElementById('res-budget').textContent = budget.toFixed(1);
|
| 779 |
-
document.getElementById('res-value').textContent = value.toFixed(1);
|
| 780 |
-
|
| 781 |
-
// fill bars — approximate max from difficulty
|
| 782 |
-
const maxEnergy = 12; const maxBudget = 60;
|
| 783 |
-
setResFill('rc-time', Math.min(time/10,1) * 100, time < 3 ? 'var(--red)' : time < 6 ? 'var(--amber)' : 'var(--green)');
|
| 784 |
-
setResFill('rc-energy', Math.min(energy/maxEnergy,1) * 100, energy < 2 ? 'var(--red)' : energy < 5 ? 'var(--amber)' : 'var(--cyan)');
|
| 785 |
-
setResFill('rc-budget', Math.min(budget/maxBudget,1) * 100, budget < 5 ? 'var(--red)' : budget < 15 ? 'var(--amber)' : 'var(--accent2)');
|
| 786 |
-
setResFill('rc-value', Math.min(value/50,1) * 100, 'var(--amber)');
|
| 787 |
-
|
| 788 |
-
// health bar
|
| 789 |
-
const h = Math.max(0, Math.min(1, health));
|
| 790 |
-
const hc = h > 0.6 ? 'var(--green)' : h > 0.3 ? 'var(--amber)' : 'var(--red)';
|
| 791 |
-
document.getElementById('health-fill').style.width = (h * 100) + '%';
|
| 792 |
-
document.getElementById('health-fill').style.background = hc;
|
| 793 |
-
document.getElementById('health-pct').textContent = (h * 100).toFixed(0) + '%';
|
| 794 |
-
document.getElementById('health-pct').style.color = hc;
|
| 795 |
-
|
| 796 |
-
// stats
|
| 797 |
-
document.getElementById('stat-step').textContent = obs.step_number ?? stepCount;
|
| 798 |
-
document.getElementById('stat-done').textContent = obs.tasks_completed ?? 0;
|
| 799 |
-
document.getElementById('stat-fail').textContent = obs.tasks_failed ?? 0;
|
| 800 |
-
|
| 801 |
-
// outcome
|
| 802 |
-
if (obs.last_action_outcome) {{
|
| 803 |
-
document.getElementById('outcome-bar').innerHTML =
|
| 804 |
-
'<span>›</span> ' + obs.last_action_outcome;
|
| 805 |
-
}}
|
| 806 |
-
}}
|
| 807 |
-
|
| 808 |
-
function setResFill(id, pct, color) {{
|
| 809 |
-
const el = document.getElementById(id);
|
| 810 |
-
el.style.setProperty('--fill', pct + '%');
|
| 811 |
-
el.style.setProperty('--fill-color', color);
|
| 812 |
-
}}
|
| 813 |
-
|
| 814 |
-
// ── SCORE RING ──
|
| 815 |
-
function updateScore(score) {{
|
| 816 |
-
const n = typeof score === 'number' ? score : parseFloat(score);
|
| 817 |
-
if (isNaN(n)) return;
|
| 818 |
-
const circumference = 283;
|
| 819 |
-
const offset = circumference * (1 - Math.max(0, Math.min(1, n)));
|
| 820 |
-
document.getElementById('ring-fill').style.strokeDashoffset = offset;
|
| 821 |
-
const color = n >= 0.7 ? 'var(--green)' : n >= 0.4 ? 'var(--amber)' : 'var(--red)';
|
| 822 |
-
document.getElementById('ring-fill').style.stroke = color;
|
| 823 |
-
document.getElementById('score-num').textContent = n.toFixed(2);
|
| 824 |
-
document.getElementById('score-num').style.color = color;
|
| 825 |
-
}}
|
| 826 |
-
|
| 827 |
-
// ── REWARD CHART ──
|
| 828 |
-
function pushReward(reward) {{
|
| 829 |
-
rewardHistory.push(reward);
|
| 830 |
-
if (rewardHistory.length > 30) rewardHistory.shift();
|
| 831 |
-
renderChart();
|
| 832 |
-
}}
|
| 833 |
-
|
| 834 |
-
function renderChart() {{
|
| 835 |
-
const chart = document.getElementById('reward-chart');
|
| 836 |
-
if (rewardHistory.length === 0) {{
|
| 837 |
-
chart.innerHTML = '<div style="color:var(--muted);font-size:0.72rem;width:100%;text-align:center;padding-bottom:10px;">No data yet</div>';
|
| 838 |
-
return;
|
| 839 |
-
}}
|
| 840 |
-
const max = Math.max(...rewardHistory.map(Math.abs), 0.01);
|
| 841 |
-
chart.innerHTML = '';
|
| 842 |
-
rewardHistory.forEach((r, i) => {{
|
| 843 |
-
const bar = document.createElement('div');
|
| 844 |
-
bar.className = 'reward-bar';
|
| 845 |
-
bar.dataset.val = r.toFixed(2);
|
| 846 |
-
const hPct = (Math.abs(r) / max) * 90 + 5;
|
| 847 |
-
bar.style.height = hPct + '%';
|
| 848 |
-
bar.style.background = r >= 0.5 ? 'var(--green)' : r >= 0.3 ? 'var(--cyan)' : r >= 0 ? 'var(--amber)' : 'var(--red)';
|
| 849 |
-
bar.style.opacity = 0.5 + (i / rewardHistory.length) * 0.5;
|
| 850 |
-
chart.appendChild(bar);
|
| 851 |
-
}});
|
| 852 |
-
}}
|
| 853 |
-
|
| 854 |
-
// ── TASK CARDS ──
|
| 855 |
-
function renderTasks(tasks) {{
|
| 856 |
-
const list = document.getElementById('task-list');
|
| 857 |
-
document.getElementById('task-badge').textContent = (tasks?.length ?? 0) + ' tasks';
|
| 858 |
-
|
| 859 |
-
if (!tasks || tasks.length === 0) {{
|
| 860 |
-
list.innerHTML = '<div class="task-empty">All tasks resolved</div>';
|
| 861 |
-
return;
|
| 862 |
-
}}
|
| 863 |
-
|
| 864 |
-
list.innerHTML = '';
|
| 865 |
-
tasks.forEach(t => {{
|
| 866 |
-
const urgency = t.deadline <= 2;
|
| 867 |
-
const medUrgent = t.deadline <= 4;
|
| 868 |
-
const card = document.createElement('div');
|
| 869 |
-
card.className = 'task-card' + (urgency ? ' urgent' : '') + (selectedTask === t.task_id ? ' selected' : '');
|
| 870 |
-
card.dataset.id = t.task_id;
|
| 871 |
-
card.onclick = () => selectTask(t.task_id);
|
| 872 |
-
|
| 873 |
-
const priCls = t.priority > 0.7 ? 'hi' : t.priority > 0.4 ? 'mid' : 'lo';
|
| 874 |
-
const dlCls = urgency ? 'lo' : medUrgent ? 'mid' : 'hi';
|
| 875 |
-
const riskCls = t.uncertainty > 0.6 ? 'lo' : t.uncertainty > 0.3 ? 'mid' : 'hi';
|
| 876 |
-
|
| 877 |
-
card.innerHTML = `
|
| 878 |
-
<div class="task-name">[#${{t.task_id}}] ${{t.name}}</div>
|
| 879 |
-
<div class="task-chips">
|
| 880 |
-
<span class="chip ${{priCls}}">PRI ${{(t.priority*100).toFixed(0)}}%</span>
|
| 881 |
-
<span class="chip ${{dlCls}}">⏰ ${{t.deadline}}s</span>
|
| 882 |
-
<span class="chip ${{riskCls}}">RISK ${{(t.uncertainty*100).toFixed(0)}}%</span>
|
| 883 |
-
<span class="chip val">${{t.value.toFixed(1)}}v</span>
|
| 884 |
-
<span class="chip">E${{t.required_energy.toFixed(1)}}</span>
|
| 885 |
-
<span class="chip">B${{t.required_budget.toFixed(1)}}</span>
|
| 886 |
-
</div>`;
|
| 887 |
-
list.appendChild(card);
|
| 888 |
-
}});
|
| 889 |
-
|
| 890 |
-
if (selectedTask === null && tasks.length > 0) selectTask(tasks[0].task_id);
|
| 891 |
-
}}
|
| 892 |
-
|
| 893 |
-
// ── RESET ──
|
| 894 |
-
async function resetEnv() {{
|
| 895 |
-
stopAuto();
|
| 896 |
-
episodeDone = false;
|
| 897 |
-
rewardHistory = [];
|
| 898 |
-
stepCount = 0;
|
| 899 |
-
selectedTask = null;
|
| 900 |
-
renderChart();
|
| 901 |
-
|
| 902 |
-
document.getElementById('reset-spinner').style.display = 'inline-block';
|
| 903 |
-
document.getElementById('score-num').textContent = '—';
|
| 904 |
-
document.getElementById('score-num').style.color = 'var(--text)';
|
| 905 |
-
document.getElementById('ring-fill').style.strokeDashoffset = 283;
|
| 906 |
-
document.getElementById('ring-fill').style.stroke = 'var(--accent)';
|
| 907 |
-
document.getElementById('stat-reward').textContent = '0.00';
|
| 908 |
-
|
| 909 |
-
setStatusPill('RESETTING…');
|
| 910 |
-
const diff = document.getElementById('difficulty').value;
|
| 911 |
-
|
| 912 |
-
try {{
|
| 913 |
-
const r = await fetch('/reset', {{
|
| 914 |
-
method: 'POST',
|
| 915 |
-
headers: {{'Content-Type': 'application/json'}},
|
| 916 |
-
body: JSON.stringify({{difficulty: diff}})
|
| 917 |
-
}});
|
| 918 |
-
const data = await r.json();
|
| 919 |
-
const obs = data.observation || data;
|
| 920 |
-
|
| 921 |
-
renderTasks(obs.tasks || []);
|
| 922 |
-
updateResources(obs);
|
| 923 |
-
|
| 924 |
-
document.getElementById('step-btn').disabled = false;
|
| 925 |
-
document.getElementById('auto-btn').disabled = false;
|
| 926 |
-
document.getElementById('sb-ep').classList.add('ok');
|
| 927 |
-
document.getElementById('sb-ep-text').textContent = 'EPISODE ACTIVE · ' + diff.toUpperCase();
|
| 928 |
-
setStatusPill('READY — ' + diff.toUpperCase(), 'ready');
|
| 929 |
-
log('Episode reset · ' + diff + ' · ' + (obs.tasks||[]).length + ' tasks loaded', 'inf');
|
| 930 |
-
}} catch(e) {{
|
| 931 |
-
log('Reset failed: ' + e.message, 'wrn');
|
| 932 |
-
setStatusPill('ERROR');
|
| 933 |
-
}}
|
| 934 |
-
document.getElementById('reset-spinner').style.display = 'none';
|
| 935 |
-
}}
|
| 936 |
-
|
| 937 |
-
// ── STEP ──
|
| 938 |
-
async function stepEnv() {{
|
| 939 |
-
if (episodeDone) return;
|
| 940 |
-
const tid = selectedTask ?? 0;
|
| 941 |
-
|
| 942 |
-
try {{
|
| 943 |
-
const r = await fetch('/step', {{
|
| 944 |
-
method: 'POST',
|
| 945 |
-
headers: {{'Content-Type': 'application/json'}},
|
| 946 |
-
body: JSON.stringify({{action_type: selectedAction, task_id: tid, reasoning: 'UI step'}})
|
| 947 |
-
}});
|
| 948 |
-
const data = await r.json();
|
| 949 |
-
const obs = data.observation || data;
|
| 950 |
-
const reward = data.reward ?? obs.reward ?? 0;
|
| 951 |
-
const done = data.done ?? obs.done ?? false;
|
| 952 |
-
|
| 953 |
-
stepCount++;
|
| 954 |
-
pushReward(reward);
|
| 955 |
-
renderTasks(obs.tasks || []);
|
| 956 |
-
updateResources(obs);
|
| 957 |
-
|
| 958 |
-
document.getElementById('stat-reward').textContent = reward.toFixed(3);
|
| 959 |
-
document.getElementById('stat-reward').style.color = reward >= 0.5 ? 'var(--green)' : reward >= 0 ? 'var(--amber)' : 'var(--red)';
|
| 960 |
-
|
| 961 |
-
const cls = reward >= 0.5 ? 'pos' : reward >= 0 ? 'wrn' : 'neg';
|
| 962 |
-
log(selectedAction + '(task=' + tid + ') → reward=' + reward.toFixed(3) + (done ? ' [DONE]' : ''), cls);
|
| 963 |
-
|
| 964 |
-
if (done) {{
|
| 965 |
-
episodeDone = true;
|
| 966 |
-
stopAuto();
|
| 967 |
-
document.getElementById('step-btn').disabled = true;
|
| 968 |
-
document.getElementById('auto-btn').disabled = true;
|
| 969 |
-
document.getElementById('sb-ep-text').textContent = 'EPISODE COMPLETE';
|
| 970 |
-
document.getElementById('sb-ep').classList.remove('ok');
|
| 971 |
-
setStatusPill('EPISODE COMPLETE', 'done');
|
| 972 |
-
document.getElementById('score-hero').classList.add('episode-done');
|
| 973 |
-
await fetchFinalScore();
|
| 974 |
-
}}
|
| 975 |
-
}} catch(e) {{
|
| 976 |
-
log('Step error: ' + e.message, 'wrn');
|
| 977 |
-
}}
|
| 978 |
-
}}
|
| 979 |
-
|
| 980 |
-
// ── FETCH SCORE ──
|
| 981 |
-
async function fetchFinalScore() {{
|
| 982 |
-
try {{
|
| 983 |
-
const r = await fetch('/state');
|
| 984 |
-
const state = await r.json();
|
| 985 |
-
const completed = state.tasks_completed ?? 0;
|
| 986 |
-
const failed = state.tasks_failed ?? 0;
|
| 987 |
-
const total = completed + failed + (state.tasks?.length ?? 0);
|
| 988 |
-
const eff = total > 0 ? completed / total : 0;
|
| 989 |
-
updateScore(eff);
|
| 990 |
-
log('Final — completed ' + completed + '/' + total + ' · health ' + ((state.system_health??1)*100).toFixed(0) + '%', 'inf');
|
| 991 |
-
}} catch(e) {{}}
|
| 992 |
-
}}
|
| 993 |
-
|
| 994 |
-
// ── AUTO PLAY ──
|
| 995 |
-
function toggleAuto() {{
|
| 996 |
-
if (autoTimer) {{ stopAuto(); return; }}
|
| 997 |
-
const btn = document.getElementById('auto-btn');
|
| 998 |
-
btn.textContent = '⏹ STOP';
|
| 999 |
-
btn.className = 'cmd-btn btn-stop';
|
| 1000 |
-
autoTimer = setInterval(async () => {{
|
| 1001 |
-
if (episodeDone) {{ stopAuto(); return; }}
|
| 1002 |
-
await stepEnv();
|
| 1003 |
-
}}, 700);
|
| 1004 |
-
}}
|
| 1005 |
-
|
| 1006 |
-
function stopAuto() {{
|
| 1007 |
-
clearInterval(autoTimer);
|
| 1008 |
-
autoTimer = null;
|
| 1009 |
-
const btn = document.getElementById('auto-btn');
|
| 1010 |
-
btn.textContent = '⏩ AUTO';
|
| 1011 |
-
btn.className = 'cmd-btn btn-auto';
|
| 1012 |
-
}}
|
| 1013 |
-
</script>
|
| 1014 |
-
</body>
|
| 1015 |
-
</html>"""
|
| 1016 |
-
return HTMLResponse(content=html)
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
# ---------------------------------------------------------------------------
|
| 1020 |
-
# API Routes
|
| 1021 |
-
# ---------------------------------------------------------------------------
|
| 1022 |
-
|
| 1023 |
-
@app.post("/reset", response_model=ResetResponse, tags=["Environment Control"], summary="Reset the environment")
|
| 1024 |
-
async def reset(request: ResetRequest = Body(default_factory=ResetRequest)) -> ResetResponse:
|
| 1025 |
-
response = await _persistent_manager.reset_environment(request.model_dump(exclude_unset=True))
|
| 1026 |
-
return ResetResponse(**response)
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
@app.post("/step", response_model=StepResponse, tags=["Environment Control"], summary="Execute an action")
|
| 1030 |
-
async def step(payload: Dict[str, Any] = Body(default_factory=dict)) -> StepResponse:
|
| 1031 |
-
try:
|
| 1032 |
-
action_payload = _extract_action_payload(payload)
|
| 1033 |
-
response = await _persistent_manager.step_environment(action_payload)
|
| 1034 |
-
except ValidationError as exc:
|
| 1035 |
-
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=exc.errors()) from exc
|
| 1036 |
-
return StepResponse(**response)
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
@app.get("/state", response_model=AetherTaskFlowState, tags=["State Management"], summary="Get current state")
|
| 1040 |
-
async def get_state() -> AetherTaskFlowState:
|
| 1041 |
-
return _persistent_manager.env.state
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
@app.get("/schema", response_model=SchemaResponse, tags=["Schema"], summary="Get JSON schemas")
|
| 1045 |
-
async def get_schemas() -> SchemaResponse:
|
| 1046 |
-
return SchemaResponse(
|
| 1047 |
-
action=AetherTaskFlowAction.model_json_schema(),
|
| 1048 |
-
observation=AetherTaskFlowObservation.model_json_schema(),
|
| 1049 |
-
state=AetherTaskFlowState.model_json_schema(),
|
| 1050 |
-
)
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
def main(host: str = "0.0.0.0", port: int = 7860) -> None:
|
| 1055 |
-
import uvicorn
|
| 1056 |
-
uvicorn.run(app, host=host, port=port)
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
if __name__ == "__main__":
|
| 1060 |
-
import argparse
|
| 1061 |
-
parser = argparse.ArgumentParser(description="AETHER-TaskFlow server")
|
| 1062 |
-
parser.add_argument("--host", default="0.0.0.0")
|
| 1063 |
-
parser.add_argument("--port", type=int, default=7860)
|
| 1064 |
-
parser.add_argument("--difficulty", choices=["easy", "medium", "hard"], default=_DIFFICULTY)
|
| 1065 |
-
args = parser.parse_args()
|
| 1066 |
-
if args.difficulty != _DIFFICULTY:
|
| 1067 |
-
os.environ["AETHER_DIFFICULTY"] = args.difficulty
|
| 1068 |
-
main(host=args.host, port=args.port)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/tests/__init__.py
DELETED
|
File without changes
|
aether-taskflow/tests/__pycache__/__init__.cpython-312.pyc
DELETED
|
Binary file (149 Bytes)
|
|
|
aether-taskflow/tests/__pycache__/test_aether_taskflow.cpython-312-pytest-9.0.3.pyc
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:f1098ee062416b1c3a5ed3de5631bd01436302c4a2885a9e4236996097f6f5d9
|
| 3 |
-
size 107270
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/tests/test_aether_taskflow.py
DELETED
|
@@ -1,587 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import sys
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
|
| 6 |
-
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
-
|
| 8 |
-
import pytest
|
| 9 |
-
from fastapi.testclient import TestClient
|
| 10 |
-
|
| 11 |
-
from models import AetherTaskFlowAction, AetherTaskFlowObservation, AetherTaskFlowState, ActionType
|
| 12 |
-
from env.tasks import generate_tasks, get_profile
|
| 13 |
-
from env.algorithms import AETHER, RAPTOR, AWFROX
|
| 14 |
-
from env.grader import grade, grade_easy, grade_medium, grade_hard
|
| 15 |
-
from env.aether_env import AetherTaskFlowEnvironment
|
| 16 |
-
from server.app import app
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
@pytest.fixture
|
| 20 |
-
def easy_env():
|
| 21 |
-
return AetherTaskFlowEnvironment(difficulty="easy")
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
@pytest.fixture
|
| 25 |
-
def medium_env():
|
| 26 |
-
return AetherTaskFlowEnvironment(difficulty="medium")
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
@pytest.fixture
|
| 30 |
-
def hard_env():
|
| 31 |
-
return AetherTaskFlowEnvironment(difficulty="hard")
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
@pytest.fixture
|
| 35 |
-
def resources():
|
| 36 |
-
return {"energy": 10.0, "budget": 50.0, "time": 10.0}
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
@pytest.fixture
|
| 40 |
-
def api_client():
|
| 41 |
-
return TestClient(app)
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
class TestTaskGeneration:
|
| 46 |
-
def test_easy_task_count(self):
|
| 47 |
-
tasks = generate_tasks("easy", seed=42)
|
| 48 |
-
assert len(tasks) == 5
|
| 49 |
-
|
| 50 |
-
def test_medium_task_count(self):
|
| 51 |
-
tasks = generate_tasks("medium", seed=42)
|
| 52 |
-
assert len(tasks) == 8
|
| 53 |
-
|
| 54 |
-
def test_hard_task_count(self):
|
| 55 |
-
tasks = generate_tasks("hard", seed=42)
|
| 56 |
-
assert len(tasks) == 12
|
| 57 |
-
|
| 58 |
-
def test_tasks_have_required_fields(self):
|
| 59 |
-
tasks = generate_tasks("easy", seed=1)
|
| 60 |
-
for t in tasks:
|
| 61 |
-
d = t.to_dict()
|
| 62 |
-
assert "task_id" in d
|
| 63 |
-
assert "priority" in d
|
| 64 |
-
assert "deadline" in d
|
| 65 |
-
assert "uncertainty" in d
|
| 66 |
-
assert "value" in d
|
| 67 |
-
assert "required_energy" in d
|
| 68 |
-
assert "required_budget" in d
|
| 69 |
-
assert "category" in d
|
| 70 |
-
assert "status" in d
|
| 71 |
-
|
| 72 |
-
def test_priority_range(self):
|
| 73 |
-
tasks = generate_tasks("easy", seed=7)
|
| 74 |
-
for t in tasks:
|
| 75 |
-
assert 0.0 <= t.priority <= 1.0
|
| 76 |
-
|
| 77 |
-
def test_uncertainty_range(self):
|
| 78 |
-
tasks = generate_tasks("hard", seed=7)
|
| 79 |
-
for t in tasks:
|
| 80 |
-
assert 0.0 <= t.uncertainty <= 1.0
|
| 81 |
-
|
| 82 |
-
def test_reproducibility(self):
|
| 83 |
-
a = generate_tasks("medium", seed=99)
|
| 84 |
-
b = generate_tasks("medium", seed=99)
|
| 85 |
-
assert [t.task_id for t in a] == [t.task_id for t in b]
|
| 86 |
-
assert [round(t.priority, 5) for t in a] == [round(t.priority, 5) for t in b]
|
| 87 |
-
|
| 88 |
-
def test_different_seeds_differ(self):
|
| 89 |
-
a = generate_tasks("easy", seed=1)
|
| 90 |
-
b = generate_tasks("easy", seed=2)
|
| 91 |
-
# At least one task should differ
|
| 92 |
-
priorities_a = [t.priority for t in a]
|
| 93 |
-
priorities_b = [t.priority for t in b]
|
| 94 |
-
assert priorities_a != priorities_b
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
class TestAETHER:
|
| 99 |
-
def test_score_returns_float(self, resources):
|
| 100 |
-
aether = AETHER()
|
| 101 |
-
task = {
|
| 102 |
-
"task_id": 0, "priority": 0.8, "deadline": 3,
|
| 103 |
-
"uncertainty": 0.2, "value": 15.0,
|
| 104 |
-
"required_energy": 1.0, "required_budget": 5.0,
|
| 105 |
-
}
|
| 106 |
-
score = aether.score(task, resources, step=0, max_steps=10)
|
| 107 |
-
assert isinstance(score, float)
|
| 108 |
-
|
| 109 |
-
def test_higher_priority_scores_higher(self, resources):
|
| 110 |
-
aether = AETHER()
|
| 111 |
-
low = {"task_id": 0, "priority": 0.2, "deadline": 5, "uncertainty": 0.1, "value": 10.0, "required_energy": 1.0, "required_budget": 5.0}
|
| 112 |
-
high = {"task_id": 1, "priority": 0.9, "deadline": 5, "uncertainty": 0.1, "value": 10.0, "required_energy": 1.0, "required_budget": 5.0}
|
| 113 |
-
assert aether.score(high, resources, 0, 10) > aether.score(low, resources, 0, 10)
|
| 114 |
-
|
| 115 |
-
def test_high_uncertainty_penalised(self, resources):
|
| 116 |
-
aether = AETHER()
|
| 117 |
-
base = {"task_id": 0, "priority": 0.7, "deadline": 4, "value": 10.0, "required_energy": 1.0, "required_budget": 5.0}
|
| 118 |
-
low_unc = {**base, "uncertainty": 0.1}
|
| 119 |
-
high_unc = {**base, "uncertainty": 0.9}
|
| 120 |
-
assert aether.score(low_unc, resources, 0, 10) > aether.score(high_unc, resources, 0, 10)
|
| 121 |
-
|
| 122 |
-
def test_rank_tasks_sorted_descending(self, resources):
|
| 123 |
-
aether = AETHER()
|
| 124 |
-
tasks = [
|
| 125 |
-
{"task_id": i, "priority": 0.3 + i * 0.2, "deadline": 5,
|
| 126 |
-
"uncertainty": 0.1, "value": 10.0, "required_energy": 1.0, "required_budget": 5.0}
|
| 127 |
-
for i in range(4)
|
| 128 |
-
]
|
| 129 |
-
ranked = aether.rank_tasks(tasks, resources, 0, 10)
|
| 130 |
-
scores = [s for _, s in ranked]
|
| 131 |
-
assert scores == sorted(scores, reverse=True)
|
| 132 |
-
|
| 133 |
-
def test_update_modifies_weights(self):
|
| 134 |
-
aether = AETHER()
|
| 135 |
-
weights_before = dict(aether.weights)
|
| 136 |
-
aether.update(5.0)
|
| 137 |
-
aether.update(5.0)
|
| 138 |
-
assert aether.weights != weights_before
|
| 139 |
-
|
| 140 |
-
def test_weights_stay_in_range(self):
|
| 141 |
-
aether = AETHER()
|
| 142 |
-
for _ in range(50):
|
| 143 |
-
aether.update(10.0)
|
| 144 |
-
assert aether.weights["priority"] >= 0.5
|
| 145 |
-
assert aether.weights["uncertainty_penalty"] <= -0.1
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
class TestRAPTOR:
|
| 149 |
-
def test_defers_on_low_energy(self):
|
| 150 |
-
raptor = RAPTOR()
|
| 151 |
-
task = {"priority": 0.8, "deadline": 3, "uncertainty": 0.2, "value": 15.0,
|
| 152 |
-
"required_energy": 5.0, "required_budget": 5.0}
|
| 153 |
-
resources = {"energy": 1.0, "budget": 50.0} # energy too low
|
| 154 |
-
action = raptor.decide(task, resources, step=0, max_steps=10)
|
| 155 |
-
assert action in ("defer", "delegate")
|
| 156 |
-
|
| 157 |
-
def test_optimizes_high_uncertainty(self):
|
| 158 |
-
raptor = RAPTOR()
|
| 159 |
-
task = {"priority": 0.8, "deadline": 4, "uncertainty": 0.9, "value": 15.0,
|
| 160 |
-
"required_energy": 1.0, "required_budget": 5.0}
|
| 161 |
-
resources = {"energy": 10.0, "budget": 50.0}
|
| 162 |
-
action = raptor.decide(task, resources, step=0, max_steps=10)
|
| 163 |
-
assert action == "optimize"
|
| 164 |
-
|
| 165 |
-
def test_executes_with_good_resources(self):
|
| 166 |
-
raptor = RAPTOR()
|
| 167 |
-
task = {"priority": 0.8, "deadline": 3, "uncertainty": 0.1, "value": 15.0,
|
| 168 |
-
"required_energy": 1.0, "required_budget": 5.0}
|
| 169 |
-
resources = {"energy": 10.0, "budget": 50.0}
|
| 170 |
-
action = raptor.decide(task, resources, step=0, max_steps=10)
|
| 171 |
-
assert action == "execute"
|
| 172 |
-
|
| 173 |
-
def test_all_return_valid_action(self):
|
| 174 |
-
raptor = RAPTOR()
|
| 175 |
-
valid = {"execute", "defer", "delegate", "optimize"}
|
| 176 |
-
for seed in range(20):
|
| 177 |
-
import random
|
| 178 |
-
rng = random.Random(seed)
|
| 179 |
-
task = {"priority": rng.random(), "deadline": rng.randint(0, 8),
|
| 180 |
-
"uncertainty": rng.random(), "value": rng.uniform(3, 30),
|
| 181 |
-
"required_energy": rng.uniform(0.5, 4), "required_budget": rng.uniform(1, 20)}
|
| 182 |
-
resources = {"energy": rng.uniform(0, 12), "budget": rng.uniform(0, 60)}
|
| 183 |
-
action = raptor.decide(task, resources, rng.randint(0, 9), 10)
|
| 184 |
-
assert action in valid
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
class TestAWFROX:
|
| 188 |
-
def test_removes_expired_tasks(self):
|
| 189 |
-
recycler = AWFROX()
|
| 190 |
-
tasks = [
|
| 191 |
-
{"task_id": 0, "deadline": -1, "status": "pending"},
|
| 192 |
-
{"task_id": 1, "deadline": 3, "status": "pending"},
|
| 193 |
-
]
|
| 194 |
-
viable = recycler.filter_viable(tasks, {}, step=0, max_steps=10)
|
| 195 |
-
assert len(viable) == 1
|
| 196 |
-
assert viable[0]["task_id"] == 1
|
| 197 |
-
|
| 198 |
-
def test_recycles_deferred_when_resources_available(self):
|
| 199 |
-
recycler = AWFROX()
|
| 200 |
-
active = []
|
| 201 |
-
deferred = [{"task_id": 5, "status": "deferred", "deadline": 3,
|
| 202 |
-
"required_energy": 1.0, "required_budget": 5.0}]
|
| 203 |
-
resources = {"energy": 10.0, "budget": 50.0}
|
| 204 |
-
new_active, new_deferred = recycler.recycle_deferred(active, deferred, resources, step=2)
|
| 205 |
-
assert len(new_active) == 1
|
| 206 |
-
assert new_active[0]["status"] == "pending"
|
| 207 |
-
assert len(new_deferred) == 0
|
| 208 |
-
|
| 209 |
-
def test_keeps_deferred_when_resources_insufficient(self):
|
| 210 |
-
recycler = AWFROX()
|
| 211 |
-
active = []
|
| 212 |
-
deferred = [{"task_id": 5, "status": "deferred", "deadline": 3,
|
| 213 |
-
"required_energy": 10.0, "required_budget": 50.0}]
|
| 214 |
-
resources = {"energy": 0.5, "budget": 1.0} # insufficient
|
| 215 |
-
new_active, new_deferred = recycler.recycle_deferred(active, deferred, resources, step=2)
|
| 216 |
-
assert len(new_active) == 0
|
| 217 |
-
assert len(new_deferred) == 1
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
class TestGrader:
|
| 222 |
-
def _result(self, **kwargs):
|
| 223 |
-
base = {
|
| 224 |
-
"tasks_completed": 4, "tasks_failed": 1, "total_tasks": 5,
|
| 225 |
-
"remaining_time": 3.0, "remaining_energy": 5.0, "remaining_budget": 30.0,
|
| 226 |
-
"initial_time": 10.0, "initial_energy": 12.0, "initial_budget": 60.0,
|
| 227 |
-
"system_health": 0.9, "steps_used": 7, "max_steps": 10,
|
| 228 |
-
}
|
| 229 |
-
base.update(kwargs)
|
| 230 |
-
return base
|
| 231 |
-
|
| 232 |
-
def test_score_in_range(self):
|
| 233 |
-
for diff in ["easy", "medium", "hard"]:
|
| 234 |
-
s = grade(diff, self._result())
|
| 235 |
-
assert 0.0 <= s <= 1.0, f"{diff}: {s}"
|
| 236 |
-
|
| 237 |
-
def test_perfect_score_near_one(self):
|
| 238 |
-
perfect = {
|
| 239 |
-
"tasks_completed": 10, "tasks_failed": 0, "total_tasks": 10,
|
| 240 |
-
"remaining_time": 5.0, "remaining_energy": 8.0, "remaining_budget": 40.0,
|
| 241 |
-
"initial_time": 10.0, "initial_energy": 12.0, "initial_budget": 60.0,
|
| 242 |
-
"system_health": 1.0, "steps_used": 5, "max_steps": 10,
|
| 243 |
-
}
|
| 244 |
-
for diff in ["easy", "medium", "hard"]:
|
| 245 |
-
s = grade(diff, perfect)
|
| 246 |
-
assert s >= 0.6, f"{diff}: {s}"
|
| 247 |
-
|
| 248 |
-
def test_zero_score_on_all_failed(self):
|
| 249 |
-
worst = {
|
| 250 |
-
"tasks_completed": 0, "tasks_failed": 10, "total_tasks": 10,
|
| 251 |
-
"remaining_time": 0.0, "remaining_energy": 0.0, "remaining_budget": 0.0,
|
| 252 |
-
"initial_time": 10.0, "initial_energy": 12.0, "initial_budget": 60.0,
|
| 253 |
-
"system_health": 0.0, "steps_used": 10, "max_steps": 10,
|
| 254 |
-
}
|
| 255 |
-
for diff in ["easy", "medium", "hard"]:
|
| 256 |
-
s = grade(diff, worst)
|
| 257 |
-
assert s == 0.0, f"{diff}: {s}"
|
| 258 |
-
|
| 259 |
-
def test_deterministic(self):
|
| 260 |
-
result = self._result()
|
| 261 |
-
s1 = grade("medium", result)
|
| 262 |
-
s2 = grade("medium", result)
|
| 263 |
-
assert s1 == s2
|
| 264 |
-
|
| 265 |
-
def test_hard_collapse_penalty(self):
|
| 266 |
-
collapsed = self._result(system_health=0.1)
|
| 267 |
-
normal = self._result(system_health=0.8)
|
| 268 |
-
assert grade("hard", collapsed) < grade("hard", normal)
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
class TestEnvironmentReset:
|
| 273 |
-
def test_reset_returns_observation(self, easy_env):
|
| 274 |
-
obs = easy_env.reset(seed=42)
|
| 275 |
-
assert isinstance(obs, AetherTaskFlowObservation)
|
| 276 |
-
|
| 277 |
-
def test_reset_provides_tasks(self, easy_env):
|
| 278 |
-
obs = easy_env.reset(seed=42)
|
| 279 |
-
assert len(obs.tasks) > 0
|
| 280 |
-
|
| 281 |
-
def test_reset_has_full_resources(self, easy_env):
|
| 282 |
-
obs = easy_env.reset(seed=42)
|
| 283 |
-
assert obs.time_remaining == 10
|
| 284 |
-
assert obs.energy_remaining > 0
|
| 285 |
-
assert obs.budget_remaining > 0
|
| 286 |
-
|
| 287 |
-
def test_reset_health_is_one(self, easy_env):
|
| 288 |
-
obs = easy_env.reset(seed=42)
|
| 289 |
-
assert obs.system_health == 1.0
|
| 290 |
-
|
| 291 |
-
def test_reset_not_done(self, easy_env):
|
| 292 |
-
obs = easy_env.reset(seed=42)
|
| 293 |
-
assert obs.done is False
|
| 294 |
-
|
| 295 |
-
def test_reset_is_reproducible(self, easy_env):
|
| 296 |
-
obs1 = easy_env.reset(seed=7)
|
| 297 |
-
obs2 = easy_env.reset(seed=7)
|
| 298 |
-
assert len(obs1.tasks) == len(obs2.tasks)
|
| 299 |
-
assert obs1.tasks[0]["task_id"] == obs2.tasks[0]["task_id"]
|
| 300 |
-
|
| 301 |
-
def test_reset_names_are_reproducible(self, easy_env):
|
| 302 |
-
obs1 = easy_env.reset(seed=42)
|
| 303 |
-
obs2 = easy_env.reset(seed=42)
|
| 304 |
-
assert [task["name"] for task in obs1.tasks] == [task["name"] for task in obs2.tasks]
|
| 305 |
-
|
| 306 |
-
def test_reset_episode_id_provided(self, easy_env):
|
| 307 |
-
obs = easy_env.reset(seed=1, episode_id="test-ep-001")
|
| 308 |
-
assert obs.episode_id == "test-ep-001"
|
| 309 |
-
|
| 310 |
-
def test_reset_generates_episode_id_if_missing(self, easy_env):
|
| 311 |
-
obs = easy_env.reset(seed=1)
|
| 312 |
-
assert obs.episode_id is not None
|
| 313 |
-
assert len(obs.episode_id) > 0
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
class TestEnvironmentStep:
|
| 317 |
-
def test_execute_reduces_resources(self, easy_env):
|
| 318 |
-
obs = easy_env.reset(seed=42)
|
| 319 |
-
energy_before = obs.energy_remaining
|
| 320 |
-
task = obs.tasks[0]
|
| 321 |
-
action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=task["task_id"])
|
| 322 |
-
obs2 = easy_env.step(action)
|
| 323 |
-
assert obs2.energy_remaining <= energy_before
|
| 324 |
-
|
| 325 |
-
def test_execute_valid_task_earns_positive_reward(self, easy_env):
|
| 326 |
-
obs = easy_env.reset(seed=42)
|
| 327 |
-
task = obs.tasks[0]
|
| 328 |
-
action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=task["task_id"])
|
| 329 |
-
obs2 = easy_env.step(action)
|
| 330 |
-
# Positive action rewards are normalized into the upper half of [0, 1].
|
| 331 |
-
assert obs2.reward is not None
|
| 332 |
-
assert 0.5 < obs2.reward <= 1.0
|
| 333 |
-
|
| 334 |
-
def test_delegate_earns_positive_reward(self, easy_env):
|
| 335 |
-
obs = easy_env.reset(seed=42)
|
| 336 |
-
task = obs.tasks[0]
|
| 337 |
-
action = AetherTaskFlowAction(action_type=ActionType.DELEGATE, task_id=task["task_id"])
|
| 338 |
-
obs2 = easy_env.step(action)
|
| 339 |
-
assert obs2.reward is not None
|
| 340 |
-
assert 0.5 < obs2.reward <= 1.0
|
| 341 |
-
|
| 342 |
-
def test_defer_earns_negative_reward(self, easy_env):
|
| 343 |
-
obs = easy_env.reset(seed=42)
|
| 344 |
-
task = obs.tasks[0]
|
| 345 |
-
action = AetherTaskFlowAction(action_type=ActionType.DEFER, task_id=task["task_id"])
|
| 346 |
-
obs2 = easy_env.step(action)
|
| 347 |
-
assert obs2.reward is not None
|
| 348 |
-
assert 0.0 <= obs2.reward < 0.5
|
| 349 |
-
|
| 350 |
-
def test_optimize_returns_small_positive(self, easy_env):
|
| 351 |
-
obs = easy_env.reset(seed=42)
|
| 352 |
-
task = obs.tasks[0]
|
| 353 |
-
action = AetherTaskFlowAction(action_type=ActionType.OPTIMIZE, task_id=task["task_id"])
|
| 354 |
-
obs2 = easy_env.step(action)
|
| 355 |
-
assert 0.5 < obs2.reward < 0.55
|
| 356 |
-
|
| 357 |
-
def test_invalid_task_id_penalised(self, easy_env):
|
| 358 |
-
obs = easy_env.reset(seed=42)
|
| 359 |
-
action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=9999)
|
| 360 |
-
obs2 = easy_env.step(action)
|
| 361 |
-
assert obs2.reward is not None
|
| 362 |
-
assert 0.0 <= obs2.reward < 0.5
|
| 363 |
-
|
| 364 |
-
def test_step_reward_is_normalized(self, easy_env):
|
| 365 |
-
obs = easy_env.reset(seed=42)
|
| 366 |
-
task = obs.tasks[0]
|
| 367 |
-
action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=task["task_id"])
|
| 368 |
-
obs2 = easy_env.step(action)
|
| 369 |
-
assert 0.0 <= obs2.reward <= 1.0
|
| 370 |
-
|
| 371 |
-
def test_step_accepts_string_action(self, easy_env):
|
| 372 |
-
easy_env.reset(seed=42)
|
| 373 |
-
obs = easy_env.step("execute")
|
| 374 |
-
assert isinstance(obs, AetherTaskFlowObservation)
|
| 375 |
-
assert obs.last_action_type in {"execute", "defer", "delegate", "optimize"}
|
| 376 |
-
|
| 377 |
-
def test_step_safe_failure_returns_terminal_observation(self, easy_env):
|
| 378 |
-
easy_env.reset(seed=42)
|
| 379 |
-
obs = easy_env.step({"task_id": "not-an-int"})
|
| 380 |
-
assert obs.done is True
|
| 381 |
-
assert obs.reward == 0.0
|
| 382 |
-
assert "failed safely" in (obs.last_action_outcome or "").lower()
|
| 383 |
-
|
| 384 |
-
def test_last_action_feedback_populated(self, easy_env):
|
| 385 |
-
obs = easy_env.reset(seed=42)
|
| 386 |
-
task = obs.tasks[0]
|
| 387 |
-
action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=task["task_id"])
|
| 388 |
-
obs2 = easy_env.step(action)
|
| 389 |
-
assert obs2.last_action_type == "execute"
|
| 390 |
-
assert obs2.last_action_task_id == task["task_id"]
|
| 391 |
-
assert obs2.last_action_outcome is not None
|
| 392 |
-
|
| 393 |
-
def test_step_after_done_returns_done(self, easy_env):
|
| 394 |
-
obs = easy_env.reset(seed=42)
|
| 395 |
-
# Exhaust all tasks
|
| 396 |
-
for _ in range(15):
|
| 397 |
-
if obs.done:
|
| 398 |
-
break
|
| 399 |
-
tasks = obs.tasks
|
| 400 |
-
if not tasks:
|
| 401 |
-
break
|
| 402 |
-
action = AetherTaskFlowAction(
|
| 403 |
-
action_type=ActionType.EXECUTE,
|
| 404 |
-
task_id=tasks[0]["task_id"]
|
| 405 |
-
)
|
| 406 |
-
obs = easy_env.step(action)
|
| 407 |
-
# Extra step after done should return done
|
| 408 |
-
if obs.done:
|
| 409 |
-
action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=0)
|
| 410 |
-
obs2 = easy_env.step(action)
|
| 411 |
-
assert obs2.done is True
|
| 412 |
-
|
| 413 |
-
def test_defer_is_visible_in_state(self, easy_env):
|
| 414 |
-
obs = easy_env.reset(seed=42)
|
| 415 |
-
task = obs.tasks[0]
|
| 416 |
-
easy_env.state.resources["energy"] = 0.0
|
| 417 |
-
easy_env.state.resources["budget"] = 0.0
|
| 418 |
-
easy_env.step(
|
| 419 |
-
AetherTaskFlowAction(action_type=ActionType.DEFER, task_id=task["task_id"])
|
| 420 |
-
)
|
| 421 |
-
assert len(easy_env.state.deferred_tasks) == 1
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
class TestFullEpisode:
|
| 425 |
-
def _run_episode(self, difficulty: str, seed: int = 42) -> dict:
|
| 426 |
-
env = AetherTaskFlowEnvironment(difficulty=difficulty)
|
| 427 |
-
obs = env.reset(seed=seed)
|
| 428 |
-
rewards = []
|
| 429 |
-
steps = 0
|
| 430 |
-
while not obs.done and steps < 15:
|
| 431 |
-
tasks = obs.tasks
|
| 432 |
-
if not tasks:
|
| 433 |
-
break
|
| 434 |
-
task = tasks[0]
|
| 435 |
-
action = AetherTaskFlowAction(
|
| 436 |
-
action_type=ActionType.EXECUTE,
|
| 437 |
-
task_id=task["task_id"],
|
| 438 |
-
)
|
| 439 |
-
obs = env.step(action)
|
| 440 |
-
rewards.append(obs.reward or 0)
|
| 441 |
-
steps += 1
|
| 442 |
-
score = env.compute_final_score()
|
| 443 |
-
return {"score": score, "steps": steps, "rewards": rewards}
|
| 444 |
-
|
| 445 |
-
def test_easy_episode_completes(self):
|
| 446 |
-
result = self._run_episode("easy")
|
| 447 |
-
assert result["score"] >= 0.0
|
| 448 |
-
assert result["steps"] > 0
|
| 449 |
-
|
| 450 |
-
def test_medium_episode_completes(self):
|
| 451 |
-
result = self._run_episode("medium")
|
| 452 |
-
assert result["score"] >= 0.0
|
| 453 |
-
|
| 454 |
-
def test_hard_episode_completes(self):
|
| 455 |
-
result = self._run_episode("hard")
|
| 456 |
-
assert result["score"] >= 0.0
|
| 457 |
-
|
| 458 |
-
def test_score_in_range_all_difficulties(self):
|
| 459 |
-
for diff in ["easy", "medium", "hard"]:
|
| 460 |
-
result = self._run_episode(diff)
|
| 461 |
-
assert 0.0 <= result["score"] <= 1.0, f"{diff}: {result['score']}"
|
| 462 |
-
|
| 463 |
-
def test_rewards_in_range_all_difficulties(self):
|
| 464 |
-
for diff in ["easy", "medium", "hard"]:
|
| 465 |
-
result = self._run_episode(diff)
|
| 466 |
-
assert all(0.0 <= reward <= 1.0 for reward in result["rewards"]), (
|
| 467 |
-
f"{diff}: {result['rewards']}"
|
| 468 |
-
)
|
| 469 |
-
|
| 470 |
-
def test_easy_score_higher_than_hard(self):
|
| 471 |
-
easy = self._run_episode("easy")
|
| 472 |
-
hard = self._run_episode("hard")
|
| 473 |
-
# Easy should generally score higher than hard with naive agent
|
| 474 |
-
assert easy["score"] >= hard["score"]
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
class TestStateProperty:
|
| 478 |
-
def test_state_is_aether_state(self, easy_env):
|
| 479 |
-
easy_env.reset(seed=42)
|
| 480 |
-
state = easy_env.state
|
| 481 |
-
assert isinstance(state, AetherTaskFlowState)
|
| 482 |
-
|
| 483 |
-
def test_state_tracks_steps(self, easy_env):
|
| 484 |
-
obs = easy_env.reset(seed=42)
|
| 485 |
-
assert easy_env.state.step_count == 0
|
| 486 |
-
task = obs.tasks[0]
|
| 487 |
-
action = AetherTaskFlowAction(action_type=ActionType.EXECUTE, task_id=task["task_id"])
|
| 488 |
-
easy_env.step(action)
|
| 489 |
-
assert easy_env.state.step_count == 1
|
| 490 |
-
|
| 491 |
-
def test_state_tracks_completions(self, easy_env):
|
| 492 |
-
obs = easy_env.reset(seed=42)
|
| 493 |
-
assert easy_env.state.tasks_completed == 0
|
| 494 |
-
task = obs.tasks[0]
|
| 495 |
-
action = AetherTaskFlowAction(action_type=ActionType.DELEGATE, task_id=task["task_id"])
|
| 496 |
-
easy_env.step(action)
|
| 497 |
-
assert easy_env.state.tasks_completed == 1
|
| 498 |
-
|
| 499 |
-
def test_state_difficulty_matches_env(self):
|
| 500 |
-
for diff in ["easy", "medium", "hard"]:
|
| 501 |
-
env = AetherTaskFlowEnvironment(difficulty=diff)
|
| 502 |
-
env.reset(seed=1)
|
| 503 |
-
assert env.state.difficulty == diff
|
| 504 |
-
|
| 505 |
-
def test_debug_snapshot_is_readable(self, easy_env):
|
| 506 |
-
easy_env.reset(seed=42)
|
| 507 |
-
snapshot = easy_env._get_obs()
|
| 508 |
-
assert snapshot["num_tasks"] > 0
|
| 509 |
-
assert "resources" in snapshot
|
| 510 |
-
assert "system_health" in snapshot
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
class TestOpenEnvCompliance:
|
| 514 |
-
def test_observation_is_pydantic_model(self, easy_env):
|
| 515 |
-
obs = easy_env.reset(seed=1)
|
| 516 |
-
assert hasattr(obs, "model_dump")
|
| 517 |
-
d = obs.model_dump()
|
| 518 |
-
assert isinstance(d, dict)
|
| 519 |
-
|
| 520 |
-
def test_observation_has_done_field(self, easy_env):
|
| 521 |
-
obs = easy_env.reset(seed=1)
|
| 522 |
-
assert hasattr(obs, "done")
|
| 523 |
-
assert isinstance(obs.done, bool)
|
| 524 |
-
|
| 525 |
-
def test_observation_has_reward_field(self, easy_env):
|
| 526 |
-
obs = easy_env.reset(seed=1)
|
| 527 |
-
assert hasattr(obs, "reward")
|
| 528 |
-
|
| 529 |
-
def test_state_has_episode_id(self, easy_env):
|
| 530 |
-
easy_env.reset(seed=1, episode_id="abc-123")
|
| 531 |
-
assert easy_env.state.episode_id == "abc-123"
|
| 532 |
-
|
| 533 |
-
def test_state_has_step_count(self, easy_env):
|
| 534 |
-
easy_env.reset(seed=1)
|
| 535 |
-
assert hasattr(easy_env.state, "step_count")
|
| 536 |
-
|
| 537 |
-
def test_invalid_difficulty_raises(self):
|
| 538 |
-
with pytest.raises(ValueError):
|
| 539 |
-
AetherTaskFlowEnvironment(difficulty="impossible")
|
| 540 |
-
|
| 541 |
-
def test_action_coerces_freeform_action_type(self):
|
| 542 |
-
action = AetherTaskFlowAction(action_type="hi", task_id=0)
|
| 543 |
-
assert action.action_type == ActionType.EXECUTE
|
| 544 |
-
assert action.task_id == 0
|
| 545 |
-
|
| 546 |
-
def test_action_extracts_task_id_from_freeform_text(self):
|
| 547 |
-
action = AetherTaskFlowAction(action_type="delegate task 3")
|
| 548 |
-
assert action.action_type == ActionType.DELEGATE
|
| 549 |
-
assert action.task_id == 3
|
| 550 |
-
|
| 551 |
-
def test_action_accepts_message_payload_shape(self):
|
| 552 |
-
action = AetherTaskFlowAction.model_validate({"message": "optimize 2"})
|
| 553 |
-
assert action.action_type == ActionType.OPTIMIZE
|
| 554 |
-
assert action.task_id == 2
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
class TestPersistentServerRoutes:
|
| 558 |
-
def test_reset_step_state_share_same_session(self, api_client):
|
| 559 |
-
reset_response = api_client.post("/reset", json={"seed": 42})
|
| 560 |
-
assert reset_response.status_code == 200
|
| 561 |
-
reset_payload = reset_response.json()
|
| 562 |
-
first_task_id = reset_payload["observation"]["tasks"][0]["task_id"]
|
| 563 |
-
episode_id = reset_payload["observation"]["episode_id"]
|
| 564 |
-
|
| 565 |
-
step_response = api_client.post(
|
| 566 |
-
"/step",
|
| 567 |
-
json={"action": {"action_type": "execute", "task_id": first_task_id}},
|
| 568 |
-
)
|
| 569 |
-
assert step_response.status_code == 200
|
| 570 |
-
|
| 571 |
-
state_response = api_client.get("/state")
|
| 572 |
-
assert state_response.status_code == 200
|
| 573 |
-
state_payload = state_response.json()
|
| 574 |
-
assert state_payload["episode_id"] == episode_id
|
| 575 |
-
assert state_payload["step_count"] == 1
|
| 576 |
-
|
| 577 |
-
def test_step_accepts_message_payload(self, api_client):
|
| 578 |
-
api_client.post("/reset", json={"seed": 42})
|
| 579 |
-
response = api_client.post("/step", json={"message": "execute"})
|
| 580 |
-
assert response.status_code == 200
|
| 581 |
-
payload = response.json()
|
| 582 |
-
assert payload["observation"]["last_action_type"] in {
|
| 583 |
-
"execute",
|
| 584 |
-
"defer",
|
| 585 |
-
"delegate",
|
| 586 |
-
"optimize",
|
| 587 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
aether-taskflow/uv.lock
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
aether-taskflow/validation-su
DELETED
|
File without changes
|
aether-taskflow/validation-submission.sh
DELETED
|
@@ -1,185 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env bash
|
| 2 |
-
#
|
| 3 |
-
# validate-submission.sh — OpenEnv Submission Validator
|
| 4 |
-
#
|
| 5 |
-
# Checks that your HF Space is live, Docker image builds, and openenv validate passes.
|
| 6 |
-
#
|
| 7 |
-
# Prerequisites:
|
| 8 |
-
# - Docker: https://docs.docker.com/get-docker/
|
| 9 |
-
# - openenv-core: pip install openenv-core
|
| 10 |
-
# - curl (usually pre-installed)
|
| 11 |
-
#
|
| 12 |
-
# Run:
|
| 13 |
-
# curl -fsSL https://raw.githubusercontent.com/<owner>/<repo>/main/scripts/validate-submission.sh | bash -s -- <ping_url> [repo_dir]
|
| 14 |
-
#
|
| 15 |
-
# Or download and run locally:
|
| 16 |
-
# chmod +x validate-submission.sh
|
| 17 |
-
# ./validate-submission.sh <ping_url> [repo_dir]
|
| 18 |
-
#
|
| 19 |
-
# Arguments:
|
| 20 |
-
# ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)
|
| 21 |
-
# repo_dir Path to your repo (default: current directory)
|
| 22 |
-
#
|
| 23 |
-
# Examples:
|
| 24 |
-
# ./validate-submission.sh https://my-team.hf.space
|
| 25 |
-
# ./validate-submission.sh https://my-team.hf.space ./my-repo
|
| 26 |
-
#
|
| 27 |
-
|
| 28 |
-
set -uo pipefail
|
| 29 |
-
|
| 30 |
-
DOCKER_BUILD_TIMEOUT=600
|
| 31 |
-
if [ -t 1 ]; then
|
| 32 |
-
RED='\033[0;31m'
|
| 33 |
-
GREEN='\033[0;32m'
|
| 34 |
-
YELLOW='\033[1;33m'
|
| 35 |
-
BOLD='\033[1m'
|
| 36 |
-
NC='\033[0m'
|
| 37 |
-
else
|
| 38 |
-
RED='' GREEN='' YELLOW='' BOLD='' NC=''
|
| 39 |
-
fi
|
| 40 |
-
|
| 41 |
-
run_with_timeout() {
|
| 42 |
-
local secs="$1"; shift
|
| 43 |
-
if command -v timeout &>/dev/null; then
|
| 44 |
-
timeout "$secs" "$@"
|
| 45 |
-
elif command -v gtimeout &>/dev/null; then
|
| 46 |
-
gtimeout "$secs" "$@"
|
| 47 |
-
else
|
| 48 |
-
"$@" &
|
| 49 |
-
local pid=$!
|
| 50 |
-
( sleep "$secs" && kill "$pid" 2>/dev/null ) &
|
| 51 |
-
local watcher=$!
|
| 52 |
-
wait "$pid" 2>/dev/null
|
| 53 |
-
local rc=$?
|
| 54 |
-
kill "$watcher" 2>/dev/null
|
| 55 |
-
wait "$watcher" 2>/dev/null
|
| 56 |
-
return $rc
|
| 57 |
-
fi
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
portable_mktemp() {
|
| 61 |
-
local prefix="${1:-validate}"
|
| 62 |
-
mktemp "${TMPDIR:-/tmp}/${prefix}-XXXXXX" 2>/dev/null || mktemp
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
CLEANUP_FILES=()
|
| 66 |
-
cleanup() { rm -f "${CLEANUP_FILES[@]+"${CLEANUP_FILES[@]}"}"; }
|
| 67 |
-
trap cleanup EXIT
|
| 68 |
-
|
| 69 |
-
PING_URL="${1:-}"
|
| 70 |
-
REPO_DIR="${2:-.}"
|
| 71 |
-
|
| 72 |
-
if [ -z "$PING_URL" ]; then
|
| 73 |
-
printf "Usage: %s <ping_url> [repo_dir]\n" "$0"
|
| 74 |
-
printf "\n"
|
| 75 |
-
printf " ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)\n"
|
| 76 |
-
printf " repo_dir Path to your repo (default: current directory)\n"
|
| 77 |
-
exit 1
|
| 78 |
-
fi
|
| 79 |
-
|
| 80 |
-
if ! REPO_DIR="$(cd "$REPO_DIR" 2>/dev/null && pwd)"; then
|
| 81 |
-
printf "Error: directory '%s' not found\n" "${2:-.}"
|
| 82 |
-
exit 1
|
| 83 |
-
fi
|
| 84 |
-
PING_URL="${PING_URL%/}"
|
| 85 |
-
export PING_URL
|
| 86 |
-
PASS=0
|
| 87 |
-
|
| 88 |
-
log() { printf "[%s] %b\n" "$(date -u +%H:%M:%S)" "$*"; }
|
| 89 |
-
pass() { log "${GREEN}PASSED${NC} -- $1"; PASS=$((PASS + 1)); }
|
| 90 |
-
fail() { log "${RED}FAILED${NC} -- $1"; }
|
| 91 |
-
hint() { printf " ${YELLOW}Hint:${NC} %b\n" "$1"; }
|
| 92 |
-
stop_at() {
|
| 93 |
-
printf "\n"
|
| 94 |
-
printf "${RED}${BOLD}Validation stopped at %s.${NC} Fix the above before continuing.\n" "$1"
|
| 95 |
-
exit 1
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
printf "\n"
|
| 99 |
-
printf "${BOLD}========================================${NC}\n"
|
| 100 |
-
printf "${BOLD} OpenEnv Submission Validator${NC}\n"
|
| 101 |
-
printf "${BOLD}========================================${NC}\n"
|
| 102 |
-
log "Repo: $REPO_DIR"
|
| 103 |
-
log "Ping URL: $PING_URL"
|
| 104 |
-
printf "\n"
|
| 105 |
-
|
| 106 |
-
log "${BOLD}Step 1/3: Pinging HF Space${NC} ($PING_URL/reset) ..."
|
| 107 |
-
|
| 108 |
-
CURL_OUTPUT=$(portable_mktemp "validate-curl")
|
| 109 |
-
CLEANUP_FILES+=("$CURL_OUTPUT")
|
| 110 |
-
HTTP_CODE=$(curl -s -o "$CURL_OUTPUT" -w "%{http_code}" -X POST \
|
| 111 |
-
-H "Content-Type: application/json" -d '{}' \
|
| 112 |
-
"$PING_URL/reset" --max-time 30 2>"$CURL_OUTPUT" || printf "000")
|
| 113 |
-
|
| 114 |
-
if [ "$HTTP_CODE" = "200" ]; then
|
| 115 |
-
pass "HF Space is live and responds to /reset"
|
| 116 |
-
elif [ "$HTTP_CODE" = "000" ]; then
|
| 117 |
-
fail "HF Space not reachable (connection failed or timed out)"
|
| 118 |
-
hint "Check your network connection and that the Space is running."
|
| 119 |
-
hint "Try: curl -s -o /dev/null -w '%%{http_code}' -X POST $PING_URL/reset"
|
| 120 |
-
stop_at "Step 1"
|
| 121 |
-
else
|
| 122 |
-
fail "HF Space /reset returned HTTP $HTTP_CODE (expected 200)"
|
| 123 |
-
hint "Make sure your Space is running and the URL is correct."
|
| 124 |
-
hint "Try opening $PING_URL in your browser first."
|
| 125 |
-
stop_at "Step 1"
|
| 126 |
-
fi
|
| 127 |
-
|
| 128 |
-
log "${BOLD}Step 2/3: Running docker build${NC} ..."
|
| 129 |
-
|
| 130 |
-
if ! command -v docker &>/dev/null; then
|
| 131 |
-
fail "docker command not found"
|
| 132 |
-
hint "Install Docker: https://docs.docker.com/get-docker/"
|
| 133 |
-
stop_at "Step 2"
|
| 134 |
-
fi
|
| 135 |
-
|
| 136 |
-
if [ -f "$REPO_DIR/Dockerfile" ]; then
|
| 137 |
-
DOCKER_CONTEXT="$REPO_DIR"
|
| 138 |
-
elif [ -f "$REPO_DIR/server/Dockerfile" ]; then
|
| 139 |
-
DOCKER_CONTEXT="$REPO_DIR/server"
|
| 140 |
-
else
|
| 141 |
-
fail "No Dockerfile found in repo root or server/ directory"
|
| 142 |
-
stop_at "Step 2"
|
| 143 |
-
fi
|
| 144 |
-
|
| 145 |
-
log " Found Dockerfile in $DOCKER_CONTEXT"
|
| 146 |
-
|
| 147 |
-
BUILD_OK=false
|
| 148 |
-
BUILD_OUTPUT=$(run_with_timeout "$DOCKER_BUILD_TIMEOUT" docker build "$DOCKER_CONTEXT" 2>&1) && BUILD_OK=true
|
| 149 |
-
|
| 150 |
-
if [ "$BUILD_OK" = true ]; then
|
| 151 |
-
pass "Docker build succeeded"
|
| 152 |
-
else
|
| 153 |
-
fail "Docker build failed (timeout=${DOCKER_BUILD_TIMEOUT}s)"
|
| 154 |
-
printf "%s\n" "$BUILD_OUTPUT" | tail -20
|
| 155 |
-
stop_at "Step 2"
|
| 156 |
-
fi
|
| 157 |
-
|
| 158 |
-
log "${BOLD}Step 3/3: Running openenv validate${NC} ..."
|
| 159 |
-
|
| 160 |
-
if ! command -v openenv &>/dev/null; then
|
| 161 |
-
fail "openenv command not found"
|
| 162 |
-
hint "Install it: pip install openenv-core"
|
| 163 |
-
stop_at "Step 3"
|
| 164 |
-
fi
|
| 165 |
-
|
| 166 |
-
VALIDATE_OK=false
|
| 167 |
-
VALIDATE_OUTPUT=$(cd "$REPO_DIR" && openenv validate 2>&1) && VALIDATE_OK=true
|
| 168 |
-
|
| 169 |
-
if [ "$VALIDATE_OK" = true ]; then
|
| 170 |
-
pass "openenv validate passed"
|
| 171 |
-
[ -n "$VALIDATE_OUTPUT" ] && log " $VALIDATE_OUTPUT"
|
| 172 |
-
else
|
| 173 |
-
fail "openenv validate failed"
|
| 174 |
-
printf "%s\n" "$VALIDATE_OUTPUT"
|
| 175 |
-
stop_at "Step 3"
|
| 176 |
-
fi
|
| 177 |
-
|
| 178 |
-
printf "\n"
|
| 179 |
-
printf "${BOLD}========================================${NC}\n"
|
| 180 |
-
printf "${GREEN}${BOLD} All 3/3 checks passed!${NC}\n"
|
| 181 |
-
printf "${GREEN}${BOLD} Your submission is ready to submit.${NC}\n"
|
| 182 |
-
printf "${BOLD}========================================${NC}\n"
|
| 183 |
-
printf "\n"
|
| 184 |
-
|
| 185 |
-
exit 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|