Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- README.md +106 -190
- inference.py +109 -111
- server/Dockerfile +80 -80
- server/openenv_jayesh_environment.py +231 -320
README.md
CHANGED
|
@@ -1,248 +1,164 @@
|
|
| 1 |
---
|
| 2 |
-
title: OpenEnv Jayesh -
|
| 3 |
-
colorFrom:
|
| 4 |
-
colorTo:
|
| 5 |
sdk: docker
|
| 6 |
pinned: false
|
| 7 |
app_port: 8000
|
| 8 |
tags:
|
| 9 |
- openenv
|
| 10 |
- task-manager
|
| 11 |
-
- ai-agent
|
| 12 |
-
- planning
|
| 13 |
base_path: /web
|
| 14 |
---
|
| 15 |
|
| 16 |
-
#
|
| 17 |
|
| 18 |
-
|
| 19 |
|
| 20 |
-
|
| 21 |
-
- Python 3.10+
|
| 22 |
|
| 23 |
---
|
| 24 |
|
| 25 |
-
##
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
|
| 30 |
|
| 31 |
---
|
| 32 |
|
| 33 |
-
##
|
| 34 |
-
|
| 35 |
-
| Level | Goal | Key Constraints |
|
| 36 |
-
|-------|------|-----------------|
|
| 37 |
-
| **Easy** | Add 2-3 tasks, then `list` them | None -- basic task CRUD |
|
| 38 |
-
| **Medium** | Add 4 tasks with priorities & deadlines; complete all High-priority before deadline | Deadline enforcement, priority management |
|
| 39 |
-
| **Hard** | Add 5 tasks with priorities, deadlines, AND dependencies; complete in valid topological order | Dependency ordering + deadline enforcement + penalty accumulation |
|
| 40 |
-
|
| 41 |
-
---
|
| 42 |
-
|
| 43 |
-
## Action Space
|
| 44 |
|
| 45 |
```python
|
| 46 |
TaskManagerAction(
|
| 47 |
-
command
|
| 48 |
-
title
|
| 49 |
-
priority
|
| 50 |
-
deadline
|
| 51 |
-
depends_on=
|
| 52 |
)
|
| 53 |
```
|
| 54 |
|
| 55 |
-
##
|
| 56 |
-
| Command | Description |
|
| 57 |
-
|---------|-------------|
|
| 58 |
-
| `add` | Create a new task. Returns error if title already exists or dependency is unresolved. |
|
| 59 |
-
| `complete` | Mark a task as done. Checks deadline & dependency constraints and applies penalties. |
|
| 60 |
-
| `list` | Display all current tasks with status, priority, deadline, and dependency info. |
|
| 61 |
-
|
| 62 |
-
---
|
| 63 |
-
|
| 64 |
-
## Observation Space
|
| 65 |
|
| 66 |
```python
|
| 67 |
TaskManagerObservation(
|
| 68 |
-
success
|
| 69 |
-
message
|
| 70 |
-
tasks
|
| 71 |
-
violations
|
| 72 |
-
reward
|
| 73 |
-
done
|
| 74 |
-
metadata = {
|
| 75 |
-
"difficulty": "Hard",
|
| 76 |
-
"step": 7,
|
| 77 |
-
"tasks_added": 5,
|
| 78 |
-
"tasks_completed": 3,
|
| 79 |
-
"deadline_misses": 0,
|
| 80 |
-
"dependency_violations": 0
|
| 81 |
-
}
|
| 82 |
)
|
| 83 |
```
|
| 84 |
|
| 85 |
-
### Task Object Fields
|
| 86 |
-
| Field | Type | Description |
|
| 87 |
-
|-------|------|-------------|
|
| 88 |
-
| `title` | str | Task name |
|
| 89 |
-
| `priority` | str | `"Low"` / `"Normal"` / `"High"` |
|
| 90 |
-
| `deadline` | str | ISO-8601 date or `"none"` |
|
| 91 |
-
| `depends_on` | list[str] | Prerequisite task titles |
|
| 92 |
-
| `completed` | bool | Whether the task is done |
|
| 93 |
-
| `deadline_missed` | bool | True if completed after deadline |
|
| 94 |
-
| `dependency_violation` | bool | True if completed before all prerequisites |
|
| 95 |
-
|
| 96 |
---
|
| 97 |
|
| 98 |
-
##
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
---
|
| 128 |
|
| 129 |
-
## Quick
|
| 130 |
|
| 131 |
```bash
|
| 132 |
-
# Install dependencies
|
| 133 |
uv sync
|
| 134 |
-
|
| 135 |
-
# Start the server
|
| 136 |
uvicorn server.app:app --host 127.0.0.1 --port 8000
|
| 137 |
-
|
| 138 |
-
# In another terminal, run the inference demo
|
| 139 |
python inference.py
|
| 140 |
```
|
| 141 |
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
## Usage Examples
|
| 145 |
-
|
| 146 |
-
### Easy Mode
|
| 147 |
-
```python
|
| 148 |
-
env = OpenenvJayeshEnvironment()
|
| 149 |
-
obs = env.reset() # cycles to Easy
|
| 150 |
-
|
| 151 |
-
env.step(TaskManagerAction(command="add", title="Buy groceries", priority="Normal"))
|
| 152 |
-
# reward: 0.15
|
| 153 |
-
|
| 154 |
-
env.step(TaskManagerAction(command="add", title="Call dentist", priority="Low"))
|
| 155 |
-
# reward: 0.30
|
| 156 |
-
|
| 157 |
-
obs = env.step(TaskManagerAction(command="list"))
|
| 158 |
-
# reward: 1.0, done: True
|
| 159 |
-
```
|
| 160 |
-
|
| 161 |
-
### Medium Mode
|
| 162 |
-
```python
|
| 163 |
-
obs = env.reset() # cycles to Medium
|
| 164 |
-
|
| 165 |
-
env.step(TaskManagerAction(command="add", title="Fix critical bug", priority="High", deadline="2026-04-15"))
|
| 166 |
-
env.step(TaskManagerAction(command="add", title="Deploy hotfix", priority="High", deadline="2026-04-16"))
|
| 167 |
-
env.step(TaskManagerAction(command="add", title="Write release notes", priority="Normal", deadline="2026-04-22"))
|
| 168 |
-
env.step(TaskManagerAction(command="add", title="Team prep", priority="Low"))
|
| 169 |
-
|
| 170 |
-
env.step(TaskManagerAction(command="complete", title="Fix critical bug")) # +0.20 on-time
|
| 171 |
-
obs = env.step(TaskManagerAction(command="complete", title="Deploy hotfix")) # +0.20 -> done=True, reward=1.0
|
| 172 |
-
```
|
| 173 |
-
|
| 174 |
-
### Hard Mode (with dependencies)
|
| 175 |
-
```python
|
| 176 |
-
obs = env.reset() # cycles to Hard
|
| 177 |
-
|
| 178 |
-
env.step(TaskManagerAction(command="add", title="Reproduce bug", priority="High", deadline="2026-04-15"))
|
| 179 |
-
env.step(TaskManagerAction(command="add", title="Write tests", priority="Normal", deadline="2026-04-16"))
|
| 180 |
-
env.step(TaskManagerAction(command="add", title="Write fix", priority="High", deadline="2026-04-18",
|
| 181 |
-
depends_on=["Reproduce bug"]))
|
| 182 |
-
env.step(TaskManagerAction(command="add", title="Code review", priority="Normal", deadline="2026-04-20",
|
| 183 |
-
depends_on=["Write fix", "Write tests"]))
|
| 184 |
-
env.step(TaskManagerAction(command="add", title="Deploy", priority="Low", deadline="2026-04-22",
|
| 185 |
-
depends_on=["Code review"]))
|
| 186 |
-
|
| 187 |
-
# Complete in valid topological order
|
| 188 |
-
env.step(TaskManagerAction(command="complete", title="Reproduce bug")) # no deps
|
| 189 |
-
env.step(TaskManagerAction(command="complete", title="Write tests")) # no deps
|
| 190 |
-
env.step(TaskManagerAction(command="complete", title="Write fix")) # dep met
|
| 191 |
-
env.step(TaskManagerAction(command="complete", title="Code review")) # deps met
|
| 192 |
-
obs = env.step(TaskManagerAction(command="complete", title="Deploy")) # done=True, reward=1.0 + bonus
|
| 193 |
-
```
|
| 194 |
-
|
| 195 |
-
---
|
| 196 |
-
|
| 197 |
-
## Environment Design Rationale
|
| 198 |
-
|
| 199 |
-
### Why these three levels?
|
| 200 |
-
- **Easy** establishes baseline task CRUD competency -- can the agent perform basic operations?
|
| 201 |
-
- **Medium** adds time pressure and priority trade-offs -- a realistic proxy for real project management.
|
| 202 |
-
- **Hard** requires multi-step planning with constraint satisfaction -- approximates real dependency scheduling (e.g., CI/CD pipelines, project Gantt charts).
|
| 203 |
-
|
| 204 |
-
### Why partial rewards?
|
| 205 |
-
Smooth, dense reward signals (+0.15 per task, +0.10 per priority, etc.) enable reinforcement learning agents to make meaningful progress even without solving the full episode. This is superior to sparse reward environments where only terminal success counts.
|
| 206 |
-
|
| 207 |
-
### Why penalties?
|
| 208 |
-
- Deadline misses (-0.25) discourage agents from completing tasks arbitrarily late.
|
| 209 |
-
- Dependency violations (-0.30) teach agents that **order matters** -- a fundamental property of real-world task graphs.
|
| 210 |
-
|
| 211 |
-
---
|
| 212 |
-
|
| 213 |
-
## API Endpoints
|
| 214 |
|
| 215 |
| URL | Description |
|
| 216 |
|-----|-------------|
|
| 217 |
-
|
|
| 218 |
-
|
|
| 219 |
-
|
|
| 220 |
-
|
|
| 221 |
-
|
|
| 222 |
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
## Project Structure
|
| 226 |
|
| 227 |
```
|
| 228 |
openenv_jayesh/
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
| 238 |
```
|
| 239 |
|
| 240 |
-
---
|
| 241 |
-
|
| 242 |
## Deploy
|
| 243 |
|
| 244 |
```bash
|
| 245 |
openenv push --repo-id jayesh20/openenv_jayesh
|
| 246 |
```
|
| 247 |
-
|
| 248 |
-
Live space: https://huggingface.co/spaces/jayesh20/openenv_jayesh
|
|
|
|
| 1 |
---
|
| 2 |
+
title: OpenEnv Jayesh - Task Manager
|
| 3 |
+
colorFrom: blue
|
| 4 |
+
colorTo: green
|
| 5 |
sdk: docker
|
| 6 |
pinned: false
|
| 7 |
app_port: 8000
|
| 8 |
tags:
|
| 9 |
- openenv
|
| 10 |
- task-manager
|
|
|
|
|
|
|
| 11 |
base_path: /web
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# OpenEnv Jayesh - Task Manager Environment
|
| 15 |
|
| 16 |
+
An AI agent environment for managing tasks across three difficulty levels.
|
| 17 |
|
| 18 |
+
HF Space: https://huggingface.co/spaces/jayesh20/openenv_jayesh
|
|
|
|
| 19 |
|
| 20 |
---
|
| 21 |
|
| 22 |
+
## Difficulty levels
|
| 23 |
|
| 24 |
+
| Level | Goal | Scoring (summary) |
|
| 25 |
+
|-------|------|---------------------|
|
| 26 |
+
| **Easy** | Add **3** tasks with **at least two different** priority levels among them, then call **`list`** so all current tasks are shown. | Partial credit builds with **~+0.11** per add and **~+0.12** for `list`. **Full reward (1.0)** and **`done=True`** only when there are **3 tasks**, **`list`** was called, and **at least two distinct** priorities appear. If you `list` with three tasks but **only one** priority level, you get a **lower** goal bonus (no full 1.0 / no episode success). |
|
| 27 |
+
| **Medium** | Add **4** tasks, each with a **priority** and **deadline**. **Complete every High-priority task on time** (on or before its deadline). At least **one** High task must exist. | **Gradual** partial credit: small **~+0.07** steps for add / non-Normal / deadline (capped buckets), plus credit for **High** completions on time. **−0.15** per **deadline miss**. **`done=True`** and **1.0** when four tasks are in place, all Highs are done **without** being late, and there are **no** deadline misses. |
|
| 28 |
+
| **Hard** | Add **5** tasks with **priorities**, **deadlines**, and **dependencies**. Complete **all** tasks in valid **topological** order and **respect every deadline** (no dependency violations, no missed deadlines). | **Adds** use slightly smaller per-field steps than Medium (for smoother score), plus **~+0.012** per **dependency edge** when you set `depends_on`. **Clean completes** rise in steps, with a small extra when **dependencies were satisfied**; **topological bonus** when all five finish cleanly. **−0.15** per deadline miss, **−0.18** per dependency violation. **`done=True`** and **1.0** on a perfect run. |
|
| 29 |
|
| 30 |
+
Penalties are constants in `server/openenv_jayesh_environment.py`. Reward stays in **`[0.0, 1.0]`**.
|
| 31 |
|
| 32 |
---
|
| 33 |
|
| 34 |
+
## Action space
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
```python
|
| 37 |
TaskManagerAction(
|
| 38 |
+
command="add", # "add" | "complete" | "list"
|
| 39 |
+
title="Fix bug",
|
| 40 |
+
priority="High",
|
| 41 |
+
deadline="2026-04-01",
|
| 42 |
+
depends_on=["Other task"],
|
| 43 |
)
|
| 44 |
```
|
| 45 |
|
| 46 |
+
## Observation space
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
```python
|
| 49 |
TaskManagerObservation(
|
| 50 |
+
success=True,
|
| 51 |
+
message="...",
|
| 52 |
+
tasks=[...],
|
| 53 |
+
violations=[...],
|
| 54 |
+
reward=0.0,
|
| 55 |
+
done=False,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
)
|
| 57 |
```
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
---
|
| 60 |
|
| 61 |
+
## Sample inference output
|
| 62 |
+
|
| 63 |
+
```
|
| 64 |
+
======================================================================
|
| 65 |
+
Task Manager OpenEnv - Inference Runner
|
| 66 |
+
======================================================================
|
| 67 |
+
|
| 68 |
+
======================================================================
|
| 69 |
+
EASY MODE (perfect: 3 tasks + list)
|
| 70 |
+
======================================================================
|
| 71 |
+
Goal: EASY MODE
|
| 72 |
+
|
| 73 |
+
add 'Buy groceries' Low | score=0.110
|
| 74 |
+
add 'Call dentist' Normal | score=0.220
|
| 75 |
+
add 'Review PR' High | score=0.330
|
| 76 |
+
list (shows all tasks) | score=1.000 [DONE]
|
| 77 |
+
|
| 78 |
+
======================================================================
|
| 79 |
+
MEDIUM MODE (one deadline miss on a High task)
|
| 80 |
+
======================================================================
|
| 81 |
+
Goal: MEDIUM MODE
|
| 82 |
+
|
| 83 |
+
add 'Fix critical bug' High deadline=future | score=0.210
|
| 84 |
+
add 'Deploy hotfix' High deadline=PAST | score=0.420
|
| 85 |
+
add 'Write release notes' Normal | score=0.560
|
| 86 |
+
add 'Standup prep' Low | score=0.770
|
| 87 |
+
complete 'Fix critical bug' [on-time] | score=0.860
|
| 88 |
+
complete 'Deploy hotfix' [DEADLINE MISSED] | score=0.710
|
| 89 |
+
(!) DEADLINE MISSED: 'Deploy hotfix' was due 2020-01-01 (completed after deadline).
|
| 90 |
+
>> Score impact (Medium): -0.15 for this miss (total deadline-miss penalty: 0.15).
|
| 91 |
+
|
| 92 |
+
======================================================================
|
| 93 |
+
HARD MODE (perfect topological order, all deadlines met)
|
| 94 |
+
======================================================================
|
| 95 |
+
Goal: HARD MODE
|
| 96 |
+
|
| 97 |
+
add 'Reproduce bug' High | score=0.186
|
| 98 |
+
add 'Write tests' Normal | score=0.310
|
| 99 |
+
add 'Write fix' High (dep: Reproduce bug) | score=0.508
|
| 100 |
+
add 'Code review' (dep: Write fix, Write tests) | score=0.656
|
| 101 |
+
add 'Deploy' (dep: Code review) | score=0.804
|
| 102 |
+
|
| 103 |
+
complete 'Reproduce bug' | score=0.869
|
| 104 |
+
complete 'Write tests' | score=0.946
|
| 105 |
+
complete 'Write fix' | score=0.990
|
| 106 |
+
complete 'Code review' | score=0.990
|
| 107 |
+
complete 'Deploy' [goal] | score=1.000 [DONE]
|
| 108 |
+
|
| 109 |
+
-- Violation demo (fresh Hard episode) --
|
| 110 |
+
add 'Task A' High | score=0.186
|
| 111 |
+
add 'Task B' (dep: Task A) | score=0.322
|
| 112 |
+
WRONG: complete 'Task B' before 'Task A' | score=0.142
|
| 113 |
+
(!) DEP VIOLATION: 'Task B' completed before ['Task A'].
|
| 114 |
+
Score after dep violation: 0.188 (penalty applied)
|
| 115 |
+
|
| 116 |
+
======================================================================
|
| 117 |
+
EASY final score : 1.000
|
| 118 |
+
MEDIUM final score : 0.710 (deadline miss penalty)
|
| 119 |
+
HARD final score : 1.000
|
| 120 |
+
AVERAGE : 0.903
|
| 121 |
+
======================================================================
|
| 122 |
+
```
|
| 123 |
|
| 124 |
---
|
| 125 |
|
| 126 |
+
## Quick start
|
| 127 |
|
| 128 |
```bash
|
|
|
|
| 129 |
uv sync
|
|
|
|
|
|
|
| 130 |
uvicorn server.app:app --host 127.0.0.1 --port 8000
|
|
|
|
|
|
|
| 131 |
python inference.py
|
| 132 |
```
|
| 133 |
|
| 134 |
+
## API endpoints
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
| URL | Description |
|
| 137 |
|-----|-------------|
|
| 138 |
+
| http://127.0.0.1:8000/docs | Swagger UI |
|
| 139 |
+
| http://127.0.0.1:8000/health | Health check |
|
| 140 |
+
| POST /reset | Start new episode |
|
| 141 |
+
| POST /step | Execute action |
|
| 142 |
+
| GET /state | Current episode state |
|
| 143 |
|
| 144 |
+
## Project structure
|
|
|
|
|
|
|
| 145 |
|
| 146 |
```
|
| 147 |
openenv_jayesh/
|
| 148 |
+
├── Dockerfile
|
| 149 |
+
├── openenv.yaml
|
| 150 |
+
├── pyproject.toml
|
| 151 |
+
├── models.py
|
| 152 |
+
├── client.py
|
| 153 |
+
├── inference.py
|
| 154 |
+
└── server/
|
| 155 |
+
├── app.py
|
| 156 |
+
├── openenv_jayesh_environment.py
|
| 157 |
+
└── requirements.txt
|
| 158 |
```
|
| 159 |
|
|
|
|
|
|
|
| 160 |
## Deploy
|
| 161 |
|
| 162 |
```bash
|
| 163 |
openenv push --repo-id jayesh20/openenv_jayesh
|
| 164 |
```
|
|
|
|
|
|
inference.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
| 1 |
"""
|
| 2 |
-
inference.py -
|
| 3 |
|
| 4 |
-
Runs
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import sys
|
|
@@ -9,139 +13,133 @@ import os
|
|
| 9 |
|
| 10 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 11 |
|
| 12 |
-
from
|
| 13 |
from models import TaskManagerAction
|
| 14 |
|
| 15 |
-
# ---------------------------------------------------------------------------
|
| 16 |
-
# Helpers
|
| 17 |
-
# ---------------------------------------------------------------------------
|
| 18 |
-
|
| 19 |
-
def _step(client, action: TaskManagerAction, label: str):
|
| 20 |
-
res = client.step(action)
|
| 21 |
-
obs = res.observation
|
| 22 |
-
done_tag = " [DONE]" if res.done else ""
|
| 23 |
-
violation_tag = ""
|
| 24 |
-
if hasattr(obs, "violations") and obs.violations:
|
| 25 |
-
violation_tag = f"\n Violations: {obs.violations[-1]}"
|
| 26 |
-
print(
|
| 27 |
-
f" {label:<40} | Score: {res.reward:.3f}{done_tag}"
|
| 28 |
-
f"\n -> {obs.message}{violation_tag}"
|
| 29 |
-
)
|
| 30 |
-
return res
|
| 31 |
-
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
print("\n" + "=" * 70)
|
| 39 |
-
print("
|
| 40 |
print("=" * 70)
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
today = "2026-04-15"
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
| 50 |
|
| 51 |
-
print()
|
| 52 |
|
| 53 |
-
|
| 54 |
-
# ---------------------------------------------------------------------------
|
| 55 |
-
# Medium scenario
|
| 56 |
-
# ---------------------------------------------------------------------------
|
| 57 |
-
|
| 58 |
-
def run_medium(client):
|
| 59 |
print("\n" + "=" * 70)
|
| 60 |
-
print("
|
| 61 |
print("=" * 70)
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
today = "2026-04-15"
|
| 67 |
-
tomorrow = "2026-04-16"
|
| 68 |
-
next_week = "2026-04-22"
|
| 69 |
-
|
| 70 |
-
_step(client, TaskManagerAction(command="add", title="Fix critical bug", priority="High", deadline=today), "add 'Fix critical bug' High")
|
| 71 |
-
_step(client, TaskManagerAction(command="add", title="Deploy hotfix", priority="High", deadline=tomorrow), "add 'Deploy hotfix' High")
|
| 72 |
-
_step(client, TaskManagerAction(command="add", title="Write release notes", priority="Normal", deadline=next_week), "add 'Write release notes' Normal")
|
| 73 |
-
_step(client, TaskManagerAction(command="add", title="Team standup prep", priority="Low"), "add 'Team standup prep' Low")
|
| 74 |
-
|
| 75 |
-
# Complete High-priority tasks on time
|
| 76 |
-
_step(client, TaskManagerAction(command="complete", title="Fix critical bug"), "complete 'Fix critical bug' [High, on-time]")
|
| 77 |
-
_step(client, TaskManagerAction(command="complete", title="Deploy hotfix"), "complete 'Deploy hotfix' [High, on-time -> GOAL]")
|
| 78 |
|
| 79 |
-
|
|
|
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
-
# ---------------------------------------------------------------------------
|
| 83 |
-
# Hard scenario
|
| 84 |
-
# ---------------------------------------------------------------------------
|
| 85 |
|
| 86 |
-
def run_hard(
|
| 87 |
print("\n" + "=" * 70)
|
| 88 |
-
print("
|
| 89 |
print("=" * 70)
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
print()
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
_step(client, TaskManagerAction(command="complete", title="Deploy to production"), "complete 'Deploy to prod' [all deps met -> GOAL]")
|
| 115 |
|
| 116 |
print()
|
| 117 |
-
print(" --
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
print(f"
|
| 125 |
|
| 126 |
-
|
| 127 |
|
| 128 |
|
| 129 |
-
# ---------------------------------------------------------------------------
|
| 130 |
-
# Entry point
|
| 131 |
-
# ---------------------------------------------------------------------------
|
| 132 |
-
|
| 133 |
if __name__ == "__main__":
|
| 134 |
print("=" * 70)
|
| 135 |
-
print("
|
| 136 |
-
print("Connecting to http://127.0.0.1:8000 ...")
|
| 137 |
print("=" * 70)
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
inference.py - Task Manager OpenEnv standalone test runner.
|
| 3 |
|
| 4 |
+
Runs three scenarios (no server). Easy and Hard show successful runs; Medium shows one deadline miss.
|
| 5 |
+
Hard includes a separate violation demo episode.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python inference.py
|
| 9 |
"""
|
| 10 |
|
| 11 |
import sys
|
|
|
|
| 13 |
|
| 14 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 15 |
|
| 16 |
+
from server.openenv_jayesh_environment import OpenenvJayeshEnvironment
|
| 17 |
from models import TaskManagerAction
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
def step(env, action: TaskManagerAction, label: str):
|
| 21 |
+
obs = env.step(action)
|
| 22 |
+
done_tag = " [DONE]" if obs.done else ""
|
| 23 |
+
print(f" {label:<50} | score={obs.reward:.3f}{done_tag}")
|
| 24 |
+
msg = obs.message or ""
|
| 25 |
+
if obs.violations and (
|
| 26 |
+
"(!)" in msg
|
| 27 |
+
or "Score impact (Medium):" in msg
|
| 28 |
+
or "Score impact (Hard):" in msg
|
| 29 |
+
or "Dependency violation" in msg
|
| 30 |
+
):
|
| 31 |
+
print(f" (!) {obs.violations[-1]}")
|
| 32 |
+
for marker in ("Score impact (Medium):", "Score impact (Hard):"):
|
| 33 |
+
if marker in msg:
|
| 34 |
+
sub = msg[msg.index(marker) :]
|
| 35 |
+
if "). Task" in sub:
|
| 36 |
+
line = sub.split("). Task", 1)[0] + ")."
|
| 37 |
+
else:
|
| 38 |
+
line = sub.strip()
|
| 39 |
+
print(f" >> {line}")
|
| 40 |
+
break
|
| 41 |
+
return obs
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def run_easy():
|
| 45 |
print("\n" + "=" * 70)
|
| 46 |
+
print("EASY MODE (perfect: 3 tasks + list)")
|
| 47 |
print("=" * 70)
|
| 48 |
+
env = OpenenvJayeshEnvironment()
|
| 49 |
+
obs = env.reset()
|
| 50 |
+
print(f"Goal: {obs.message.splitlines()[0]}\n")
|
|
|
|
| 51 |
|
| 52 |
+
step(env, TaskManagerAction(command="add", title="Buy groceries", priority="Low"), "add 'Buy groceries' Low")
|
| 53 |
+
step(env, TaskManagerAction(command="add", title="Call dentist", priority="Normal"), "add 'Call dentist' Normal")
|
| 54 |
+
step(env, TaskManagerAction(command="add", title="Review PR", priority="High"), "add 'Review PR' High")
|
| 55 |
+
obs = step(env, TaskManagerAction(command="list"), "list (shows all tasks)")
|
| 56 |
+
return obs.reward
|
| 57 |
|
|
|
|
| 58 |
|
| 59 |
+
def run_medium():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
print("\n" + "=" * 70)
|
| 61 |
+
print("MEDIUM MODE (one deadline miss on a High task)")
|
| 62 |
print("=" * 70)
|
| 63 |
+
env = OpenenvJayeshEnvironment()
|
| 64 |
+
env._reset_count = 1
|
| 65 |
+
obs = env.reset()
|
| 66 |
+
print(f"Goal: {obs.message.splitlines()[0]}\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
+
future = "2099-12-31"
|
| 69 |
+
past = "2020-01-01"
|
| 70 |
|
| 71 |
+
step(env, TaskManagerAction(command="add", title="Fix critical bug", priority="High", deadline=future), "add 'Fix critical bug' High deadline=future")
|
| 72 |
+
step(env, TaskManagerAction(command="add", title="Deploy hotfix", priority="High", deadline=past), "add 'Deploy hotfix' High deadline=PAST")
|
| 73 |
+
step(env, TaskManagerAction(command="add", title="Write release notes", priority="Normal", deadline=future), "add 'Write release notes' Normal")
|
| 74 |
+
step(env, TaskManagerAction(command="add", title="Standup prep", priority="Low", deadline=future), "add 'Standup prep' Low")
|
| 75 |
+
step(env, TaskManagerAction(command="complete", title="Fix critical bug"), "complete 'Fix critical bug' [on-time]")
|
| 76 |
+
obs = step(env, TaskManagerAction(command="complete", title="Deploy hotfix"), "complete 'Deploy hotfix' [DEADLINE MISSED]")
|
| 77 |
+
return obs.reward
|
| 78 |
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
+
def run_hard():
|
| 81 |
print("\n" + "=" * 70)
|
| 82 |
+
print("HARD MODE (perfect topological order, all deadlines met)")
|
| 83 |
print("=" * 70)
|
| 84 |
+
env = OpenenvJayeshEnvironment()
|
| 85 |
+
env._reset_count = 2
|
| 86 |
+
obs = env.reset()
|
| 87 |
+
print(f"Goal: {obs.message.splitlines()[0]}\n")
|
| 88 |
+
|
| 89 |
+
future = "2099-12-31"
|
| 90 |
+
|
| 91 |
+
step(env, TaskManagerAction(command="add", title="Reproduce bug", priority="High", deadline=future), "add 'Reproduce bug' High")
|
| 92 |
+
step(env, TaskManagerAction(command="add", title="Write tests", priority="Normal", deadline=future), "add 'Write tests' Normal")
|
| 93 |
+
step(
|
| 94 |
+
env,
|
| 95 |
+
TaskManagerAction(command="add", title="Write fix", priority="High", deadline=future, depends_on=["Reproduce bug"]),
|
| 96 |
+
"add 'Write fix' High (dep: Reproduce bug)",
|
| 97 |
+
)
|
| 98 |
+
step(
|
| 99 |
+
env,
|
| 100 |
+
TaskManagerAction(command="add", title="Code review", priority="Normal", deadline=future, depends_on=["Write fix", "Write tests"]),
|
| 101 |
+
"add 'Code review' (dep: Write fix, Write tests)",
|
| 102 |
+
)
|
| 103 |
+
step(
|
| 104 |
+
env,
|
| 105 |
+
TaskManagerAction(command="add", title="Deploy to production", priority="Low", deadline=future, depends_on=["Code review"]),
|
| 106 |
+
"add 'Deploy' (dep: Code review)",
|
| 107 |
+
)
|
| 108 |
|
| 109 |
print()
|
| 110 |
+
step(env, TaskManagerAction(command="complete", title="Reproduce bug"), "complete 'Reproduce bug'")
|
| 111 |
+
step(env, TaskManagerAction(command="complete", title="Write tests"), "complete 'Write tests'")
|
| 112 |
+
step(env, TaskManagerAction(command="complete", title="Write fix"), "complete 'Write fix'")
|
| 113 |
+
step(env, TaskManagerAction(command="complete", title="Code review"), "complete 'Code review'")
|
| 114 |
+
obs = step(env, TaskManagerAction(command="complete", title="Deploy to production"), "complete 'Deploy' [goal]")
|
|
|
|
| 115 |
|
| 116 |
print()
|
| 117 |
+
print(" -- Violation demo (fresh Hard episode) --")
|
| 118 |
+
env2 = OpenenvJayeshEnvironment()
|
| 119 |
+
env2._reset_count = 2
|
| 120 |
+
env2.reset()
|
| 121 |
+
step(env2, TaskManagerAction(command="add", title="Task A", priority="High", deadline=future), "add 'Task A' High")
|
| 122 |
+
step(env2, TaskManagerAction(command="add", title="Task B", priority="Normal", deadline=future, depends_on=["Task A"]), "add 'Task B' (dep: Task A)")
|
| 123 |
+
obs2 = step(env2, TaskManagerAction(command="complete", title="Task B"), "WRONG: complete 'Task B' before 'Task A'")
|
| 124 |
+
print(f" Score after dep violation: {obs2.reward:.3f} (penalty applied)")
|
| 125 |
|
| 126 |
+
return obs.reward
|
| 127 |
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
if __name__ == "__main__":
|
| 130 |
print("=" * 70)
|
| 131 |
+
print(" Task Manager OpenEnv - Inference Runner")
|
|
|
|
| 132 |
print("=" * 70)
|
| 133 |
|
| 134 |
+
easy_score = run_easy()
|
| 135 |
+
medium_score = run_medium()
|
| 136 |
+
hard_score = run_hard()
|
| 137 |
+
|
| 138 |
+
avg = (easy_score + medium_score + hard_score) / 3
|
| 139 |
+
|
| 140 |
+
print("\n" + "=" * 70)
|
| 141 |
+
print(f" EASY final score : {easy_score:.3f}")
|
| 142 |
+
print(f" MEDIUM final score : {medium_score:.3f} (deadline miss penalty)")
|
| 143 |
+
print(f" HARD final score : {hard_score:.3f}")
|
| 144 |
+
print(f" AVERAGE : {avg:.3f}")
|
| 145 |
+
print("=" * 70)
|
server/Dockerfile
CHANGED
|
@@ -1,80 +1,80 @@
|
|
| 1 |
-
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
-
# All rights reserved.
|
| 3 |
-
#
|
| 4 |
-
# This source code is licensed under the BSD-style license found in the
|
| 5 |
-
# LICENSE file in the root directory of this source tree.
|
| 6 |
-
|
| 7 |
-
# Multi-stage build using openenv-base
|
| 8 |
-
# This Dockerfile is flexible and works for both:
|
| 9 |
-
# - In-repo environments (with local OpenEnv sources)
|
| 10 |
-
# - Standalone environments (with openenv from PyPI/Git)
|
| 11 |
-
# The build script (openenv build) handles context detection and sets appropriate build args.
|
| 12 |
-
|
| 13 |
-
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
|
| 14 |
-
FROM ${BASE_IMAGE} AS builder
|
| 15 |
-
|
| 16 |
-
WORKDIR /app
|
| 17 |
-
|
| 18 |
-
# Ensure git is available (required for installing dependencies from VCS)
|
| 19 |
-
RUN apt-get update && \
|
| 20 |
-
apt-get install -y --no-install-recommends git && \
|
| 21 |
-
rm -rf /var/lib/apt/lists/*
|
| 22 |
-
|
| 23 |
-
# Build argument to control whether we're building standalone or in-repo
|
| 24 |
-
ARG BUILD_MODE=in-repo
|
| 25 |
-
ARG ENV_NAME=openenv_jayesh
|
| 26 |
-
|
| 27 |
-
# Copy environment code (always at root of build context)
|
| 28 |
-
COPY . /app/env
|
| 29 |
-
|
| 30 |
-
# For in-repo builds, openenv is already vendored in the build context
|
| 31 |
-
# For standalone builds, openenv will be installed via pyproject.toml
|
| 32 |
-
WORKDIR /app/env
|
| 33 |
-
|
| 34 |
-
# Ensure uv is available (for local builds where base image lacks it)
|
| 35 |
-
RUN if ! command -v uv >/dev/null 2>&1; then \
|
| 36 |
-
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
| 37 |
-
mv /root/.local/bin/uv /usr/local/bin/uv && \
|
| 38 |
-
mv /root/.local/bin/uvx /usr/local/bin/uvx; \
|
| 39 |
-
fi
|
| 40 |
-
|
| 41 |
-
# Install dependencies using uv sync
|
| 42 |
-
# If uv.lock exists, use it; otherwise resolve on the fly
|
| 43 |
-
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 44 |
-
if [ -f uv.lock ]; then \
|
| 45 |
-
uv sync --frozen --no-install-project --no-editable; \
|
| 46 |
-
else \
|
| 47 |
-
uv sync --no-install-project --no-editable; \
|
| 48 |
-
fi
|
| 49 |
-
|
| 50 |
-
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 51 |
-
if [ -f uv.lock ]; then \
|
| 52 |
-
uv sync --frozen --no-editable; \
|
| 53 |
-
else \
|
| 54 |
-
uv sync --no-editable; \
|
| 55 |
-
fi
|
| 56 |
-
|
| 57 |
-
# Final runtime stage
|
| 58 |
-
FROM ${BASE_IMAGE}
|
| 59 |
-
|
| 60 |
-
WORKDIR /app
|
| 61 |
-
|
| 62 |
-
# Copy the virtual environment from builder
|
| 63 |
-
COPY --from=builder /app/env/.venv /app/.venv
|
| 64 |
-
|
| 65 |
-
# Copy the environment code
|
| 66 |
-
COPY --from=builder /app/env /app/env
|
| 67 |
-
|
| 68 |
-
# Set PATH to use the virtual environment
|
| 69 |
-
ENV PATH="/app/.venv/bin:$PATH"
|
| 70 |
-
|
| 71 |
-
# Set PYTHONPATH so imports work correctly
|
| 72 |
-
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 73 |
-
|
| 74 |
-
# Health check
|
| 75 |
-
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 76 |
-
CMD curl -f http://localhost:8000/health || exit 1
|
| 77 |
-
|
| 78 |
-
# Run the FastAPI server
|
| 79 |
-
# The module path is constructed to work with the /app/env structure
|
| 80 |
-
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
# Multi-stage build using openenv-base
|
| 8 |
+
# This Dockerfile is flexible and works for both:
|
| 9 |
+
# - In-repo environments (with local OpenEnv sources)
|
| 10 |
+
# - Standalone environments (with openenv from PyPI/Git)
|
| 11 |
+
# The build script (openenv build) handles context detection and sets appropriate build args.
|
| 12 |
+
|
| 13 |
+
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
|
| 14 |
+
FROM ${BASE_IMAGE} AS builder
|
| 15 |
+
|
| 16 |
+
WORKDIR /app
|
| 17 |
+
|
| 18 |
+
# Ensure git is available (required for installing dependencies from VCS)
|
| 19 |
+
RUN apt-get update && \
|
| 20 |
+
apt-get install -y --no-install-recommends git && \
|
| 21 |
+
rm -rf /var/lib/apt/lists/*
|
| 22 |
+
|
| 23 |
+
# Build argument to control whether we're building standalone or in-repo
|
| 24 |
+
ARG BUILD_MODE=in-repo
|
| 25 |
+
ARG ENV_NAME=openenv_jayesh
|
| 26 |
+
|
| 27 |
+
# Copy environment code (always at root of build context)
|
| 28 |
+
COPY . /app/env
|
| 29 |
+
|
| 30 |
+
# For in-repo builds, openenv is already vendored in the build context
|
| 31 |
+
# For standalone builds, openenv will be installed via pyproject.toml
|
| 32 |
+
WORKDIR /app/env
|
| 33 |
+
|
| 34 |
+
# Ensure uv is available (for local builds where base image lacks it)
|
| 35 |
+
RUN if ! command -v uv >/dev/null 2>&1; then \
|
| 36 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
| 37 |
+
mv /root/.local/bin/uv /usr/local/bin/uv && \
|
| 38 |
+
mv /root/.local/bin/uvx /usr/local/bin/uvx; \
|
| 39 |
+
fi
|
| 40 |
+
|
| 41 |
+
# Install dependencies using uv sync
|
| 42 |
+
# If uv.lock exists, use it; otherwise resolve on the fly
|
| 43 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 44 |
+
if [ -f uv.lock ]; then \
|
| 45 |
+
uv sync --frozen --no-install-project --no-editable; \
|
| 46 |
+
else \
|
| 47 |
+
uv sync --no-install-project --no-editable; \
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 51 |
+
if [ -f uv.lock ]; then \
|
| 52 |
+
uv sync --frozen --no-editable; \
|
| 53 |
+
else \
|
| 54 |
+
uv sync --no-editable; \
|
| 55 |
+
fi
|
| 56 |
+
|
| 57 |
+
# Final runtime stage
|
| 58 |
+
FROM ${BASE_IMAGE}
|
| 59 |
+
|
| 60 |
+
WORKDIR /app
|
| 61 |
+
|
| 62 |
+
# Copy the virtual environment from builder
|
| 63 |
+
COPY --from=builder /app/env/.venv /app/.venv
|
| 64 |
+
|
| 65 |
+
# Copy the environment code
|
| 66 |
+
COPY --from=builder /app/env /app/env
|
| 67 |
+
|
| 68 |
+
# Set PATH to use the virtual environment
|
| 69 |
+
ENV PATH="/app/.venv/bin:$PATH"
|
| 70 |
+
|
| 71 |
+
# Set PYTHONPATH so imports work correctly
|
| 72 |
+
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 73 |
+
|
| 74 |
+
# Health check
|
| 75 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 76 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 77 |
+
|
| 78 |
+
# Run the FastAPI server
|
| 79 |
+
# The module path is constructed to work with the /app/env structure
|
| 80 |
+
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
server/openenv_jayesh_environment.py
CHANGED
|
@@ -1,31 +1,12 @@
|
|
| 1 |
-
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
-
# All rights reserved.
|
| 3 |
-
|
| 4 |
"""
|
| 5 |
-
Smart Personal Task Manager - OpenEnv Environment
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
Medium (reset 1, 4, 7 ...)
|
| 14 |
-
Goal: Add 4 tasks with mixed priorities AND deadlines.
|
| 15 |
-
Complete ALL High-priority tasks before their deadlines.
|
| 16 |
-
Reward: +0.15 per task added (up to 4), +0.10 per correct priority label,
|
| 17 |
-
+0.20 per High-priority task completed on time.
|
| 18 |
-
-0.25 penalty per deadline miss.
|
| 19 |
-
Full 1.0 on completion.
|
| 20 |
-
|
| 21 |
-
Hard (reset 2, 5, 8 ...)
|
| 22 |
-
Goal: Add 5 tasks with priorities, deadlines, AND dependencies.
|
| 23 |
-
Complete tasks in valid dependency order; respect all deadlines.
|
| 24 |
-
Reward: +0.15 per task added (up to 5), +0.10 per correct priority,
|
| 25 |
-
+0.25 per task completed without violation.
|
| 26 |
-
Bonus +0.10 for achieving perfect (optimal) topological ordering.
|
| 27 |
-
-0.30 per dependency violation, -0.25 per deadline miss.
|
| 28 |
-
Full 1.0 on perfect completion.
|
| 29 |
"""
|
| 30 |
|
| 31 |
from __future__ import annotations
|
|
@@ -42,21 +23,19 @@ try:
|
|
| 42 |
except (ModuleNotFoundError, ImportError):
|
| 43 |
from models import TaskManagerAction, TaskManagerObservation
|
| 44 |
|
| 45 |
-
|
| 46 |
-
# ---------------------------------------------------------------------------
|
| 47 |
-
# Helpers
|
| 48 |
-
# ---------------------------------------------------------------------------
|
| 49 |
-
|
| 50 |
VALID_PRIORITIES = {"Low", "Normal", "High"}
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
|
| 54 |
-
def _parse_date(
|
| 55 |
-
|
| 56 |
-
if not dt_str:
|
| 57 |
return None
|
| 58 |
try:
|
| 59 |
-
return date.fromisoformat(
|
| 60 |
except ValueError:
|
| 61 |
return None
|
| 62 |
|
|
@@ -65,60 +44,38 @@ def _today() -> date:
|
|
| 65 |
return date.today()
|
| 66 |
|
| 67 |
|
| 68 |
-
# ---------------------------------------------------------------------------
|
| 69 |
-
# Environment
|
| 70 |
-
# ---------------------------------------------------------------------------
|
| 71 |
-
|
| 72 |
class OpenenvJayeshEnvironment(Environment):
|
| 73 |
-
"""
|
| 74 |
-
Smart Personal Task Manager with three distinct difficulty levels.
|
| 75 |
-
|
| 76 |
-
Easy -> add tasks + list
|
| 77 |
-
Medium -> deadlines + priority management
|
| 78 |
-
Hard -> deadlines + priority + dependency ordering
|
| 79 |
-
"""
|
| 80 |
|
| 81 |
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 82 |
|
| 83 |
-
# ------------------------------------------------------------------
|
| 84 |
-
# Lifecycle
|
| 85 |
-
# ------------------------------------------------------------------
|
| 86 |
-
|
| 87 |
def __init__(self) -> None:
|
| 88 |
super().__init__()
|
| 89 |
-
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 90 |
self._reset_count = 0
|
| 91 |
self._scenarios = ["Easy", "Medium", "Hard"]
|
| 92 |
-
self.
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
self.tasks: List[Dict[str, Any]] = []
|
| 97 |
-
self.difficulty
|
| 98 |
-
self.goal_completed
|
| 99 |
-
|
| 100 |
-
# Counters
|
| 101 |
-
self.tasks_added: int = 0
|
| 102 |
-
self.tasks_completed: int = 0
|
| 103 |
-
self.high_tasks_added: int = 0
|
| 104 |
-
self.high_tasks_completed_on_time: int = 0
|
| 105 |
-
self.deadline_misses: int = 0
|
| 106 |
-
self.dependency_violations: int = 0
|
| 107 |
-
self.completion_order: List[str] = [] # titles in completion order
|
| 108 |
-
|
| 109 |
-
# Episode-level flag
|
| 110 |
-
self.list_called: bool = False
|
| 111 |
-
|
| 112 |
-
# Violations log (for observation)
|
| 113 |
self.violations: List[str] = []
|
| 114 |
|
| 115 |
-
|
| 116 |
-
self.
|
| 117 |
-
self.
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
def reset(
|
| 124 |
self,
|
|
@@ -127,61 +84,41 @@ class OpenenvJayeshEnvironment(Environment):
|
|
| 127 |
**kwargs: Any,
|
| 128 |
) -> TaskManagerObservation:
|
| 129 |
self._state = State(episode_id=episode_id or str(uuid4()), step_count=0)
|
| 130 |
-
self.
|
| 131 |
|
| 132 |
-
|
| 133 |
-
self.difficulty = self._scenarios[idx]
|
| 134 |
self._reset_count += 1
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
"
|
| 141 |
-
"
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
"
|
| 151 |
-
"
|
| 152 |
-
"
|
| 153 |
-
"
|
| 154 |
-
"
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
else: # Hard
|
| 158 |
-
self.target_tasks = 5
|
| 159 |
-
self.target_high = 2
|
| 160 |
-
msg = (
|
| 161 |
-
"=== HARD MODE ===\n"
|
| 162 |
-
"Goal: Add 5 tasks with priorities, deadlines, AND dependencies.\n"
|
| 163 |
-
" Complete tasks in valid dependency order (dependencies first).\n"
|
| 164 |
-
" Respect all deadlines.\n"
|
| 165 |
-
"Tips: Use depends_on=['Task Title'] when adding a dependent task.\n"
|
| 166 |
-
" Complete prerequisite tasks before their dependents.\n"
|
| 167 |
-
"Rewards: +0.15/task, +0.10/priority, +0.25/completion without violation.\n"
|
| 168 |
-
" +0.10 bonus for perfect topological ordering.\n"
|
| 169 |
-
"Penalty: -0.30/dependency violation, -0.25/deadline miss."
|
| 170 |
-
)
|
| 171 |
-
|
| 172 |
return TaskManagerObservation(
|
| 173 |
success=True,
|
| 174 |
-
message=
|
| 175 |
tasks=[],
|
| 176 |
violations=[],
|
| 177 |
done=False,
|
| 178 |
reward=0.0,
|
| 179 |
)
|
| 180 |
|
| 181 |
-
# ------------------------------------------------------------------
|
| 182 |
-
# step
|
| 183 |
-
# ------------------------------------------------------------------
|
| 184 |
-
|
| 185 |
def step(
|
| 186 |
self,
|
| 187 |
action: TaskManagerAction,
|
|
@@ -190,25 +127,22 @@ class OpenenvJayeshEnvironment(Environment):
|
|
| 190 |
) -> TaskManagerObservation:
|
| 191 |
self._state.step_count += 1
|
| 192 |
cmd = (action.command or "").strip().lower()
|
| 193 |
-
success = True
|
| 194 |
-
message = ""
|
| 195 |
|
| 196 |
if cmd == "add":
|
| 197 |
-
success,
|
| 198 |
elif cmd == "complete":
|
| 199 |
-
success,
|
| 200 |
elif cmd == "list":
|
| 201 |
self.list_called = True
|
| 202 |
-
|
| 203 |
else:
|
| 204 |
-
success = False
|
| 205 |
-
message = f"Unknown command '{cmd}'. Valid commands: 'add', 'complete', 'list'."
|
| 206 |
|
| 207 |
-
reward = self.
|
| 208 |
|
| 209 |
return TaskManagerObservation(
|
| 210 |
success=success,
|
| 211 |
-
message=
|
| 212 |
tasks=list(self.tasks),
|
| 213 |
violations=list(self.violations),
|
| 214 |
done=self.goal_completed,
|
|
@@ -217,247 +151,224 @@ class OpenenvJayeshEnvironment(Environment):
|
|
| 217 |
"difficulty": self.difficulty,
|
| 218 |
"step": self._state.step_count,
|
| 219 |
"tasks_added": self.tasks_added,
|
| 220 |
-
"
|
|
|
|
| 221 |
"deadline_misses": self.deadline_misses,
|
| 222 |
-
"dependency_violations": self.dependency_violations,
|
| 223 |
},
|
| 224 |
)
|
| 225 |
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
def _handle_add(self, action: TaskManagerAction):
|
| 231 |
-
if not action.title or not action.title.strip():
|
| 232 |
-
return False, "Error: 'title' is required for the 'add' command."
|
| 233 |
-
|
| 234 |
-
title = action.title.strip()
|
| 235 |
-
|
| 236 |
-
# Duplicate check
|
| 237 |
if any(t["title"] == title for t in self.tasks):
|
| 238 |
-
return False, f"Error:
|
| 239 |
|
| 240 |
priority = (action.priority or "Normal").strip()
|
| 241 |
if priority not in VALID_PRIORITIES:
|
| 242 |
priority = "Normal"
|
| 243 |
|
| 244 |
-
deadline_str = action.deadline
|
| 245 |
-
deadline_obj = _parse_date(deadline_str)
|
| 246 |
|
| 247 |
depends_on: List[str] = []
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
False,
|
| 256 |
-
f"Error: Dependency '{dep}' does not exist yet. "
|
| 257 |
-
"Add prerequisite tasks first.",
|
| 258 |
-
)
|
| 259 |
|
| 260 |
task: Dict[str, Any] = {
|
| 261 |
"title": title,
|
| 262 |
"priority": priority,
|
| 263 |
-
"deadline": deadline_str
|
| 264 |
"depends_on": depends_on,
|
| 265 |
"completed": False,
|
| 266 |
"deadline_missed": False,
|
| 267 |
-
"
|
| 268 |
-
"added_step": self._state.step_count,
|
| 269 |
}
|
| 270 |
self.tasks.append(task)
|
| 271 |
self.tasks_added += 1
|
| 272 |
if priority == "High":
|
| 273 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
-
msg = (
|
| 276 |
-
f"Task added: '{title}' | Priority: {priority}"
|
| 277 |
-
+ (f" | Deadline: {deadline_str}" if deadline_str else "")
|
| 278 |
-
+ (f" | Depends on: {depends_on}" if depends_on else "")
|
| 279 |
-
)
|
| 280 |
-
return True, msg
|
| 281 |
-
|
| 282 |
-
def _handle_complete(self, action: TaskManagerAction):
|
| 283 |
-
if not action.title or not action.title.strip():
|
| 284 |
-
return False, "Error: 'title' is required for the 'complete' command."
|
| 285 |
-
|
| 286 |
-
title = action.title.strip()
|
| 287 |
task = next((t for t in self.tasks if t["title"] == title), None)
|
| 288 |
-
|
| 289 |
if task is None:
|
| 290 |
-
return False, f"Error:
|
| 291 |
if task["completed"]:
|
| 292 |
return False, f"Task '{title}' is already completed."
|
| 293 |
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
| 296 |
unmet = [
|
| 297 |
-
|
| 298 |
-
if not any(t["title"] ==
|
| 299 |
]
|
| 300 |
if unmet:
|
| 301 |
-
|
| 302 |
-
task["
|
| 303 |
-
self.
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
self.violations.append(violation_msg)
|
| 309 |
-
|
| 310 |
-
# ---- Deadline check ----
|
| 311 |
-
deadline_missed = False
|
| 312 |
dl = _parse_date(task["deadline"])
|
| 313 |
-
if dl
|
| 314 |
-
|
| 315 |
task["deadline_missed"] = True
|
| 316 |
self.deadline_misses += 1
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
|
| 323 |
-
# ---- Mark completed ----
|
| 324 |
task["completed"] = True
|
| 325 |
-
self.tasks_completed += 1
|
| 326 |
-
self.completion_order.append(title)
|
| 327 |
-
|
| 328 |
-
if task["priority"] == "High" and not deadline_missed:
|
| 329 |
-
self.high_tasks_completed_on_time += 1
|
| 330 |
-
|
| 331 |
-
msg_parts = [f"Task '{title}' marked complete."]
|
| 332 |
-
if dep_violation:
|
| 333 |
-
msg_parts.append("(!) Dependency violation penalty applied.")
|
| 334 |
-
if deadline_missed:
|
| 335 |
-
msg_parts.append("(!) Deadline miss penalty applied.")
|
| 336 |
-
if not dep_violation and not deadline_missed:
|
| 337 |
-
msg_parts.append("Clean completion -- no penalties.")
|
| 338 |
-
|
| 339 |
-
return True, " ".join(msg_parts)
|
| 340 |
-
|
| 341 |
-
# ------------------------------------------------------------------
|
| 342 |
-
# Reward calculation
|
| 343 |
-
# ------------------------------------------------------------------
|
| 344 |
-
|
| 345 |
-
def _calculate_reward(self) -> float:
|
| 346 |
-
self.goal_completed = False
|
| 347 |
-
reward = 0.0
|
| 348 |
|
| 349 |
-
if
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
else:
|
| 354 |
-
|
| 355 |
|
| 356 |
-
return
|
| 357 |
|
| 358 |
-
def
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
if self.list_called:
|
| 364 |
-
r += 0.
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
# +0.20 per High-priority task completed on time
|
| 382 |
-
r += self.high_tasks_completed_on_time * 0.20
|
| 383 |
-
# Penalties
|
| 384 |
-
r -= self.deadline_misses * 0.25
|
| 385 |
-
# Goal: >=4 tasks, >=2 High added, ALL High completed on time, no deadline misses
|
| 386 |
high_tasks = [t for t in self.tasks if t["priority"] == "High"]
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
and self.high_tasks_added >= 2
|
| 391 |
-
and all_high_done
|
| 392 |
-
and self.deadline_misses == 0
|
| 393 |
-
):
|
| 394 |
-
r = 1.0
|
| 395 |
-
self.goal_completed = True
|
| 396 |
-
return r
|
| 397 |
-
|
| 398 |
-
def _reward_hard(self) -> float:
|
| 399 |
-
r = 0.0
|
| 400 |
-
# +0.15 per task added, up to 5
|
| 401 |
-
r += min(5, self.tasks_added) * 0.15
|
| 402 |
-
# +0.10 per task with non-Normal or explicit priority
|
| 403 |
-
explicit_priority_tasks = sum(
|
| 404 |
-
1 for t in self.tasks if t["priority"] != "Normal"
|
| 405 |
)
|
| 406 |
-
|
| 407 |
-
# +0.25 per task completed without any violation
|
| 408 |
-
clean_completions = sum(
|
| 409 |
-
1 for t in self.tasks
|
| 410 |
-
if t["completed"] and not t["deadline_missed"] and not t["dependency_violation"]
|
| 411 |
-
)
|
| 412 |
-
r += clean_completions * 0.25
|
| 413 |
-
# Penalty
|
| 414 |
-
r -= self.dependency_violations * 0.30
|
| 415 |
-
r -= self.deadline_misses * 0.25
|
| 416 |
-
# Bonus: optimal ordering (no violations at all + all done)
|
| 417 |
-
all_done = all(t["completed"] for t in self.tasks)
|
| 418 |
-
if all_done and self.dependency_violations == 0 and self.deadline_misses == 0:
|
| 419 |
-
r += 0.10 # perfect-run bonus
|
| 420 |
-
# Goal: >=5 tasks, >=2 High, all completed, zero violations
|
| 421 |
-
high_tasks = [t for t in self.tasks if t["priority"] == "High"]
|
| 422 |
-
if (
|
| 423 |
-
self.tasks_added >= 5
|
| 424 |
-
and self.high_tasks_added >= 2
|
| 425 |
-
and len(self.tasks) == self.tasks_completed
|
| 426 |
-
and self.dependency_violations == 0
|
| 427 |
-
and self.deadline_misses == 0
|
| 428 |
-
):
|
| 429 |
-
r = 1.0
|
| 430 |
self.goal_completed = True
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
|
| 437 |
-
def
|
| 438 |
if not self.tasks:
|
| 439 |
-
return "No tasks
|
| 440 |
-
lines = [f"
|
| 441 |
for i, t in enumerate(self.tasks, 1):
|
| 442 |
status = "DONE" if t["completed"] else "PENDING"
|
| 443 |
flags = []
|
| 444 |
if t.get("deadline_missed"):
|
| 445 |
flags.append("LATE")
|
| 446 |
-
if t.get("
|
| 447 |
-
flags.append("DEP-
|
| 448 |
-
flag_str = f" [{',
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
lines.append(
|
| 452 |
-
f" {i}. [{status}]{flag_str} {t['title']} "
|
| 453 |
-
f"(Priority: {t['priority']}{dl_str}{dep_str})"
|
| 454 |
-
)
|
| 455 |
return "\n".join(lines)
|
| 456 |
|
| 457 |
-
# ------------------------------------------------------------------
|
| 458 |
-
# State property
|
| 459 |
-
# ------------------------------------------------------------------
|
| 460 |
-
|
| 461 |
@property
|
| 462 |
def state(self) -> State:
|
| 463 |
return self._state
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
Smart Personal Task Manager - OpenEnv Environment
|
| 3 |
+
|
| 4 |
+
Easy: three tasks + list; full 1.0 when at least 2 distinct priorities; else partial credit.
|
| 5 |
+
Medium: small +0.07 signals; deadline miss −0.15.
|
| 6 |
+
Hard: +0.018 per dep edge on add; rising clean completes; dep-satisfied micro-bonus; topo bonus;
|
| 7 |
+
deadline miss −0.15, dependency violation −0.18.
|
| 8 |
+
|
| 9 |
+
Reward clamp [0.0, 1.0].
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
from __future__ import annotations
|
|
|
|
| 23 |
except (ModuleNotFoundError, ImportError):
|
| 24 |
from models import TaskManagerAction, TaskManagerObservation
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
VALID_PRIORITIES = {"Low", "Normal", "High"}
|
| 27 |
+
|
| 28 |
+
# Fair penalties (requested ranges)
|
| 29 |
+
MEDIUM_DEADLINE_MISS_PENALTY = 0.15 # -0.12 … -0.18
|
| 30 |
+
HARD_DEADLINE_MISS_PENALTY = 0.15 # -0.12 … -0.18
|
| 31 |
+
HARD_DEP_VIOLATION_PENALTY = 0.18 # -0.15 … -0.20
|
| 32 |
|
| 33 |
|
| 34 |
+
def _parse_date(s: Optional[str]) -> Optional[date]:
|
| 35 |
+
if not s:
|
|
|
|
| 36 |
return None
|
| 37 |
try:
|
| 38 |
+
return date.fromisoformat(s)
|
| 39 |
except ValueError:
|
| 40 |
return None
|
| 41 |
|
|
|
|
| 44 |
return date.today()
|
| 45 |
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
class OpenenvJayeshEnvironment(Environment):
|
| 48 |
+
"""Task Manager with Easy / Medium / Hard difficulty levels."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
def __init__(self) -> None:
|
| 53 |
super().__init__()
|
|
|
|
| 54 |
self._reset_count = 0
|
| 55 |
self._scenarios = ["Easy", "Medium", "Hard"]
|
| 56 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 57 |
+
self._init_state()
|
| 58 |
+
|
| 59 |
+
def _init_state(self) -> None:
|
| 60 |
+
self.tasks: List[Dict[str, Any]] = []
|
| 61 |
+
self.difficulty = "Easy"
|
| 62 |
+
self.goal_completed = False
|
| 63 |
+
self.list_called = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
self.violations: List[str] = []
|
| 65 |
|
| 66 |
+
self.tasks_added = 0
|
| 67 |
+
self.high_added = 0
|
| 68 |
+
self.high_on_time = 0
|
| 69 |
+
self.clean_completions = 0
|
| 70 |
+
self.deadline_misses = 0
|
| 71 |
+
self.dep_violations = 0
|
| 72 |
|
| 73 |
+
self._r_add = 0.0
|
| 74 |
+
self._r_priority = 0.0
|
| 75 |
+
self._r_deadline_set = 0.0
|
| 76 |
+
self._r_complete = 0.0
|
| 77 |
+
self._r_hard_dep_ok = 0.0
|
| 78 |
+
self._r_hard_edge = 0.0
|
| 79 |
|
| 80 |
def reset(
|
| 81 |
self,
|
|
|
|
| 84 |
**kwargs: Any,
|
| 85 |
) -> TaskManagerObservation:
|
| 86 |
self._state = State(episode_id=episode_id or str(uuid4()), step_count=0)
|
| 87 |
+
self._init_state()
|
| 88 |
|
| 89 |
+
self.difficulty = self._scenarios[self._reset_count % 3]
|
|
|
|
| 90 |
self._reset_count += 1
|
| 91 |
|
| 92 |
+
msgs = {
|
| 93 |
+
"Easy": (
|
| 94 |
+
"EASY MODE\n"
|
| 95 |
+
"Goal: Add 3 tasks with at least 2 different priorities, then call 'list'.\n"
|
| 96 |
+
"Full score (1.0) when 3 tasks + list + 2+ priority levels; "
|
| 97 |
+
"otherwise partial credit if you list without enough variety."
|
| 98 |
+
),
|
| 99 |
+
"Medium": (
|
| 100 |
+
"MEDIUM MODE\n"
|
| 101 |
+
"Goal: Add 4 tasks with priorities and deadlines. Every High task on time.\n"
|
| 102 |
+
"Rewards: small steps (~+0.07 per signal) for smooth partial credit.\n"
|
| 103 |
+
f"Penalty: -{MEDIUM_DEADLINE_MISS_PENALTY:.2f} per deadline miss."
|
| 104 |
+
),
|
| 105 |
+
"Hard": (
|
| 106 |
+
"HARD MODE\n"
|
| 107 |
+
"Goal: 5 tasks with deps + deadlines; valid order; no misses.\n"
|
| 108 |
+
"Rewards: tiny edge credit when linking deps on add; rising clean completes; "
|
| 109 |
+
"small bonus when deps satisfied at complete.\n"
|
| 110 |
+
f"Penalties: -{HARD_DEP_VIOLATION_PENALTY:.2f} dependency, -{HARD_DEADLINE_MISS_PENALTY:.2f} deadline miss."
|
| 111 |
+
),
|
| 112 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
return TaskManagerObservation(
|
| 114 |
success=True,
|
| 115 |
+
message=msgs[self.difficulty],
|
| 116 |
tasks=[],
|
| 117 |
violations=[],
|
| 118 |
done=False,
|
| 119 |
reward=0.0,
|
| 120 |
)
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
def step(
|
| 123 |
self,
|
| 124 |
action: TaskManagerAction,
|
|
|
|
| 127 |
) -> TaskManagerObservation:
|
| 128 |
self._state.step_count += 1
|
| 129 |
cmd = (action.command or "").strip().lower()
|
|
|
|
|
|
|
| 130 |
|
| 131 |
if cmd == "add":
|
| 132 |
+
success, msg = self._add(action)
|
| 133 |
elif cmd == "complete":
|
| 134 |
+
success, msg = self._complete(action)
|
| 135 |
elif cmd == "list":
|
| 136 |
self.list_called = True
|
| 137 |
+
success, msg = True, self._fmt_list()
|
| 138 |
else:
|
| 139 |
+
success, msg = False, f"Unknown command '{cmd}'. Use: add | complete | list."
|
|
|
|
| 140 |
|
| 141 |
+
reward = self._score()
|
| 142 |
|
| 143 |
return TaskManagerObservation(
|
| 144 |
success=success,
|
| 145 |
+
message=msg,
|
| 146 |
tasks=list(self.tasks),
|
| 147 |
violations=list(self.violations),
|
| 148 |
done=self.goal_completed,
|
|
|
|
| 151 |
"difficulty": self.difficulty,
|
| 152 |
"step": self._state.step_count,
|
| 153 |
"tasks_added": self.tasks_added,
|
| 154 |
+
"clean_completions": self.clean_completions,
|
| 155 |
+
"dep_violations": self.dep_violations,
|
| 156 |
"deadline_misses": self.deadline_misses,
|
|
|
|
| 157 |
},
|
| 158 |
)
|
| 159 |
|
| 160 |
+
def _add(self, action: TaskManagerAction):
|
| 161 |
+
title = (action.title or "").strip()
|
| 162 |
+
if not title:
|
| 163 |
+
return False, "Error: 'title' is required for add."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
if any(t["title"] == title for t in self.tasks):
|
| 165 |
+
return False, f"Error: task '{title}' already exists."
|
| 166 |
|
| 167 |
priority = (action.priority or "Normal").strip()
|
| 168 |
if priority not in VALID_PRIORITIES:
|
| 169 |
priority = "Normal"
|
| 170 |
|
| 171 |
+
deadline_str = (action.deadline or "").strip() or None
|
|
|
|
| 172 |
|
| 173 |
depends_on: List[str] = []
|
| 174 |
+
for dep in (action.depends_on or []):
|
| 175 |
+
dep = dep.strip()
|
| 176 |
+
if not dep:
|
| 177 |
+
continue
|
| 178 |
+
if not any(t["title"] == dep for t in self.tasks):
|
| 179 |
+
return False, f"Error: dependency '{dep}' not found. Add it first."
|
| 180 |
+
depends_on.append(dep)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
task: Dict[str, Any] = {
|
| 183 |
"title": title,
|
| 184 |
"priority": priority,
|
| 185 |
+
"deadline": deadline_str,
|
| 186 |
"depends_on": depends_on,
|
| 187 |
"completed": False,
|
| 188 |
"deadline_missed": False,
|
| 189 |
+
"dep_violation": False,
|
|
|
|
| 190 |
}
|
| 191 |
self.tasks.append(task)
|
| 192 |
self.tasks_added += 1
|
| 193 |
if priority == "High":
|
| 194 |
+
self.high_added += 1
|
| 195 |
+
|
| 196 |
+
if self.difficulty == "Medium":
|
| 197 |
+
# Smaller per-signal steps (~0.07) so scores rise gradually (avoid large single-step jumps)
|
| 198 |
+
self._r_add += 0.07
|
| 199 |
+
if priority != "Normal":
|
| 200 |
+
self._r_priority += 0.07
|
| 201 |
+
if deadline_str:
|
| 202 |
+
self._r_deadline_set += 0.07
|
| 203 |
+
elif self.difficulty == "Hard":
|
| 204 |
+
# Slightly smaller per-signal steps than Medium to smooth the last adds (e.g. Deploy)
|
| 205 |
+
inc = 0.062
|
| 206 |
+
self._r_add += inc
|
| 207 |
+
if priority != "Normal":
|
| 208 |
+
self._r_priority += inc
|
| 209 |
+
if deadline_str:
|
| 210 |
+
self._r_deadline_set += inc
|
| 211 |
+
if depends_on:
|
| 212 |
+
self._r_hard_edge += 0.012 * float(len(depends_on))
|
| 213 |
+
|
| 214 |
+
parts = [f"Task '{title}' added (priority={priority}"]
|
| 215 |
+
if deadline_str:
|
| 216 |
+
parts[0] += f", deadline={deadline_str}"
|
| 217 |
+
if depends_on:
|
| 218 |
+
parts[0] += f", depends_on={depends_on}"
|
| 219 |
+
parts[0] += ")."
|
| 220 |
+
return True, " ".join(parts)
|
| 221 |
+
|
| 222 |
+
def _complete(self, action: TaskManagerAction):
|
| 223 |
+
title = (action.title or "").strip()
|
| 224 |
+
if not title:
|
| 225 |
+
return False, "Error: 'title' is required for complete."
|
| 226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
task = next((t for t in self.tasks if t["title"] == title), None)
|
|
|
|
| 228 |
if task is None:
|
| 229 |
+
return False, f"Error: task '{title}' not found."
|
| 230 |
if task["completed"]:
|
| 231 |
return False, f"Task '{title}' is already completed."
|
| 232 |
|
| 233 |
+
msgs = []
|
| 234 |
+
dep_viol = False
|
| 235 |
+
dl_miss = False
|
| 236 |
+
|
| 237 |
unmet = [
|
| 238 |
+
d for d in task["depends_on"]
|
| 239 |
+
if not any(t["title"] == d and t["completed"] for t in self.tasks)
|
| 240 |
]
|
| 241 |
if unmet:
|
| 242 |
+
dep_viol = True
|
| 243 |
+
task["dep_violation"] = True
|
| 244 |
+
self.dep_violations += 1
|
| 245 |
+
v = f"DEP VIOLATION: '{title}' completed before {unmet}."
|
| 246 |
+
self.violations.append(v)
|
| 247 |
+
msgs.append("(!) Dependency violation — penalty applied.")
|
| 248 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
dl = _parse_date(task["deadline"])
|
| 250 |
+
if dl and _today() > dl:
|
| 251 |
+
dl_miss = True
|
| 252 |
task["deadline_missed"] = True
|
| 253 |
self.deadline_misses += 1
|
| 254 |
+
v = f"DEADLINE MISSED: '{title}' was due {task['deadline']} (completed after deadline)."
|
| 255 |
+
self.violations.append(v)
|
| 256 |
+
msgs.append("(!) Deadline missed — penalty applied.")
|
| 257 |
+
if self.difficulty == "Medium":
|
| 258 |
+
msgs.append(
|
| 259 |
+
f"Score impact (Medium): -{MEDIUM_DEADLINE_MISS_PENALTY:.2f} for this miss "
|
| 260 |
+
f"(total deadline-miss penalty: "
|
| 261 |
+
f"{self.deadline_misses * MEDIUM_DEADLINE_MISS_PENALTY:.2f})."
|
| 262 |
+
)
|
| 263 |
+
elif self.difficulty == "Hard":
|
| 264 |
+
msgs.append(
|
| 265 |
+
f"Score impact (Hard): -{HARD_DEADLINE_MISS_PENALTY:.2f} for this miss "
|
| 266 |
+
f"(total: {self.deadline_misses * HARD_DEADLINE_MISS_PENALTY:.2f})."
|
| 267 |
+
)
|
| 268 |
|
|
|
|
| 269 |
task["completed"] = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
+
if not dep_viol and not dl_miss:
|
| 272 |
+
order_idx = self.clean_completions
|
| 273 |
+
self.clean_completions += 1
|
| 274 |
+
if task["priority"] == "High":
|
| 275 |
+
self.high_on_time += 1
|
| 276 |
+
msgs.append(f"Task '{title}' completed cleanly.")
|
| 277 |
+
if self.difficulty == "Medium":
|
| 278 |
+
if task["priority"] == "High":
|
| 279 |
+
self._r_complete += 0.09
|
| 280 |
+
elif self.difficulty == "Hard":
|
| 281 |
+
# Rising reward each clean step; tiny extra when prerequisites were satisfied
|
| 282 |
+
step_r = 0.065 + 0.012 * float(order_idx)
|
| 283 |
+
self._r_complete += min(0.125, step_r)
|
| 284 |
+
if task["depends_on"]:
|
| 285 |
+
self._r_hard_dep_ok += 0.025
|
| 286 |
else:
|
| 287 |
+
msgs.append(f"Task '{title}' completed with violations.")
|
| 288 |
|
| 289 |
+
return True, " ".join(msgs)
|
| 290 |
|
| 291 |
+
def _score(self) -> float:
|
| 292 |
+
if self.difficulty == "Easy":
|
| 293 |
+
return self._score_easy()
|
| 294 |
+
if self.difficulty == "Medium":
|
| 295 |
+
return self._score_medium()
|
| 296 |
+
return self._score_hard()
|
| 297 |
+
|
| 298 |
+
def _easy_has_two_priorities(self) -> bool:
|
| 299 |
+
if len(self.tasks) < 3:
|
| 300 |
+
return False
|
| 301 |
+
return len({t["priority"] for t in self.tasks}) >= 2
|
| 302 |
+
|
| 303 |
+
def _score_easy(self) -> float:
|
| 304 |
+
# Partial: ~+0.11/add, ~+0.12/list. Full 1.0 + done only with 2+ distinct priorities + list.
|
| 305 |
+
r = min(3, self.tasks_added) * 0.11
|
| 306 |
if self.list_called:
|
| 307 |
+
r += 0.12
|
| 308 |
+
if self.tasks_added >= 3 and self.list_called:
|
| 309 |
+
if self._easy_has_two_priorities():
|
| 310 |
+
r += 0.55
|
| 311 |
+
self.goal_completed = True
|
| 312 |
+
else:
|
| 313 |
+
r += 0.40
|
| 314 |
+
return round(min(1.0, r), 3)
|
| 315 |
+
|
| 316 |
+
def _score_medium(self) -> float:
|
| 317 |
+
# Softer caps match smaller increments (4×0.07 ≈ 0.28 per bucket)
|
| 318 |
+
r = min(0.28, self._r_add)
|
| 319 |
+
r += min(0.28, self._r_priority)
|
| 320 |
+
r += min(0.28, self._r_deadline_set)
|
| 321 |
+
r += min(0.22, self._r_complete)
|
| 322 |
+
r -= self.deadline_misses * MEDIUM_DEADLINE_MISS_PENALTY
|
| 323 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
high_tasks = [t for t in self.tasks if t["priority"] == "High"]
|
| 325 |
+
all_high_on_time = (
|
| 326 |
+
len(high_tasks) >= 1
|
| 327 |
+
and all(t["completed"] and not t["deadline_missed"] for t in high_tasks)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
)
|
| 329 |
+
if self.tasks_added >= 4 and all_high_on_time and self.deadline_misses == 0:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
self.goal_completed = True
|
| 331 |
+
return 1.0
|
| 332 |
+
return round(min(0.94, max(0.0, r)), 3)
|
| 333 |
+
|
| 334 |
+
def _score_hard(self) -> float:
|
| 335 |
+
# Caps aligned with 0.062 increments (~0.31 over 5 adds) to avoid one huge step on task 5
|
| 336 |
+
r = min(0.34, self._r_add)
|
| 337 |
+
r += min(0.26, self._r_priority)
|
| 338 |
+
r += min(0.26, self._r_deadline_set)
|
| 339 |
+
r += min(0.42, self._r_complete)
|
| 340 |
+
r += min(0.14, self._r_hard_dep_ok)
|
| 341 |
+
r += min(0.08, self._r_hard_edge)
|
| 342 |
+
topo_bonus = 0.0
|
| 343 |
+
if self.clean_completions == 5 and self.dep_violations == 0:
|
| 344 |
+
topo_bonus = 0.055
|
| 345 |
+
r += topo_bonus
|
| 346 |
+
r -= self.dep_violations * HARD_DEP_VIOLATION_PENALTY
|
| 347 |
+
r -= self.deadline_misses * HARD_DEADLINE_MISS_PENALTY
|
| 348 |
+
|
| 349 |
+
all_done = self.tasks_added >= 5 and all(t["completed"] for t in self.tasks)
|
| 350 |
+
if all_done and self.dep_violations == 0 and self.deadline_misses == 0:
|
| 351 |
+
self.goal_completed = True
|
| 352 |
+
return 1.0
|
| 353 |
+
return round(min(0.99, max(0.0, r)), 3)
|
| 354 |
|
| 355 |
+
def _fmt_list(self) -> str:
|
| 356 |
if not self.tasks:
|
| 357 |
+
return "No tasks yet."
|
| 358 |
+
lines = [f"Tasks ({len(self.tasks)}):"]
|
| 359 |
for i, t in enumerate(self.tasks, 1):
|
| 360 |
status = "DONE" if t["completed"] else "PENDING"
|
| 361 |
flags = []
|
| 362 |
if t.get("deadline_missed"):
|
| 363 |
flags.append("LATE")
|
| 364 |
+
if t.get("dep_violation"):
|
| 365 |
+
flags.append("DEP-ERR")
|
| 366 |
+
flag_str = f" [{','.join(flags)}]" if flags else ""
|
| 367 |
+
dl = f" due={t['deadline']}" if t["deadline"] else ""
|
| 368 |
+
dep = f" deps={t['depends_on']}" if t["depends_on"] else ""
|
| 369 |
+
lines.append(f" {i}. [{status}]{flag_str} {t['title']} ({t['priority']}{dl}{dep})")
|
|
|
|
|
|
|
|
|
|
| 370 |
return "\n".join(lines)
|
| 371 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
@property
|
| 373 |
def state(self) -> State:
|
| 374 |
return self._state
|