priyaaaaaasharmaaaaa commited on
Commit
35e9f20
·
verified ·
1 Parent(s): 4c3f78e

Upload trained LoRA adapter from Kaggle

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +7 -0
  2. .gitattributes +10 -10
  3. .gitignore +6 -0
  4. Dockerfile +14 -0
  5. GUIDE.md +245 -0
  6. README.md +283 -52
  7. SETUP.md +266 -0
  8. client.py +134 -0
  9. data/project_data.json +231 -0
  10. data/sprint_data.json +65 -0
  11. evaluate.py +203 -0
  12. evaluate_r2.py +457 -0
  13. huggingface_tokenizers_cache/CACHEDIR.TAG +4 -0
  14. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/.no_exist/3d254dbee5e3beae81bb8a717ad3a03427a09d26/chat_template.jinja +0 -0
  15. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/31349551d90c7606f325fe0f11bbb8bd5fa0d7c7 +0 -0
  16. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/4783fe10ac3adce15ac8f358ef5462739852c569 +0 -0
  17. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/482ced4679301bf287ebb310bdd1790eb4514232 +24 -0
  18. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/76ac30de06e0158be805f42a4e63934bedb5a169 +58 -0
  19. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/9b8043f10c758210957b050c77f14d6282f33a52 +31 -0
  20. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/9c5ae00e602b8860cbd784ba82a8aa14e8feecec692e7076590d014d7b7fdafa +3 -0
  21. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/a5de2eb6cac65b89846ac1bcfb837a3d83459f40 +209 -0
  22. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/refs/main +1 -0
  23. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/added_tokens.json +24 -0
  24. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/config.json +58 -0
  25. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/merges.txt +0 -0
  26. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/special_tokens_map.json +31 -0
  27. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/tokenizer.json +3 -0
  28. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/tokenizer_config.json +209 -0
  29. huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/vocab.json +0 -0
  30. inference.py +220 -0
  31. inference_r2.py +855 -0
  32. openenv.yaml +88 -0
  33. plot_results.py +421 -0
  34. project_client.py +289 -0
  35. pyproject.toml +24 -0
  36. requirements.env +0 -0
  37. requirements.txt +0 -0
  38. results/phase1/README.md +73 -0
  39. results/phase1/adapter_config.json +50 -0
  40. results/phase1/adapter_model.safetensors +3 -0
  41. results/phase1/chat_template.jinja +54 -0
  42. results/phase1/checkpoint-100/README.md +210 -0
  43. results/phase1/checkpoint-100/adapter_config.json +50 -0
  44. results/phase1/checkpoint-100/adapter_model.safetensors +3 -0
  45. results/phase1/checkpoint-100/chat_template.jinja +54 -0
  46. results/phase1/checkpoint-100/optimizer.pt +3 -0
  47. results/phase1/checkpoint-100/rng_state.pth +3 -0
  48. results/phase1/checkpoint-100/scaler.pt +3 -0
  49. results/phase1/checkpoint-100/scheduler.pt +3 -0
  50. results/phase1/checkpoint-100/tokenizer.json +3 -0
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ venv/
2
+ **/__pycache__/
3
+ *.pyc
4
+ .env
5
+ *.log
6
+ .git/
7
+ results/
.gitattributes CHANGED
@@ -33,13 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
- checkpoint-100/tokenizer.json filter=lfs diff=lfs merge=lfs -text
37
- checkpoint-150/tokenizer.json filter=lfs diff=lfs merge=lfs -text
38
- checkpoint-200/tokenizer.json filter=lfs diff=lfs merge=lfs -text
39
- checkpoint-250/tokenizer.json filter=lfs diff=lfs merge=lfs -text
40
- checkpoint-300/tokenizer.json filter=lfs diff=lfs merge=lfs -text
41
- checkpoint-350/tokenizer.json filter=lfs diff=lfs merge=lfs -text
42
- checkpoint-400/tokenizer.json filter=lfs diff=lfs merge=lfs -text
43
- checkpoint-449/tokenizer.json filter=lfs diff=lfs merge=lfs -text
44
- checkpoint-50/tokenizer.json filter=lfs diff=lfs merge=lfs -text
45
- tokenizer.json filter=lfs diff=lfs merge=lfs -text
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.sh text eol=lf
37
+ *.lock text eol=lf
38
+ huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/9c5ae00e602b8860cbd784ba82a8aa14e8feecec692e7076590d014d7b7fdafa filter=lfs diff=lfs merge=lfs -text
39
+ huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/tokenizer.json filter=lfs diff=lfs merge=lfs -text
40
+ results/phase1/checkpoint-100/tokenizer.json filter=lfs diff=lfs merge=lfs -text
41
+ results/phase1/checkpoint-150/tokenizer.json filter=lfs diff=lfs merge=lfs -text
42
+ results/phase1/checkpoint-200/tokenizer.json filter=lfs diff=lfs merge=lfs -text
43
+ results/phase1/checkpoint-222/tokenizer.json filter=lfs diff=lfs merge=lfs -text
44
+ results/phase1/checkpoint-50/tokenizer.json filter=lfs diff=lfs merge=lfs -text
45
+ results/phase1/tokenizer.json filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ *.log
6
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+ EXPOSE 8000
12
+
13
+ # Startup script runs both services
14
+ CMD ["sh", "start.sh"]
GUIDE.md ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 📖 Complete Guide — AI Sprint Manager OpenEnv
2
+
3
+ **What we built, how to test it, how to demo it, and how to explain it.**
4
+
5
+ ---
6
+
7
+ ## 🤔 What Are We Building — Plain English
8
+
9
+ Imagine you're the Tech Lead of a software company. Every 2 weeks (called a "sprint"), your team gets a list of tasks — new features, bug fixes, tech debt. Your job is to decide:
10
+
11
+ - Which developer gets which task?
12
+ - What's most urgent when a new bug appears?
13
+ - What do you do when someone calls in sick?
14
+ - How do you avoid burning out your best developer?
15
+
16
+ **We built a simulation of this scenario** so an AI agent can practice making these decisions thousands of times and learn to get better — just like how AlphaGo learned to play Go by playing millions of games against itself.
17
+
18
+ The AI plays the role of the Tech Lead. It looks at the sprint state and decides what to do. The environment tells it how well it did (reward). Over time, it learns better strategies.
19
+
20
+ ---
21
+
22
+ ## 🏗️ How It's Built — Layer by Layer
23
+
24
+ ```
25
+ YOU / AI AGENT
26
+ ↓ makes decisions (assign, skip, reprioritize...)
27
+ FASTAPI SERVER (ui.py / server/app.py)
28
+ ↓ receives actions, returns results
29
+ SPRINT ENVIRONMENT (sprint_env/environment.py)
30
+ ↓ core logic: tracks tasks, devs, days, rewards
31
+ DATA (data/sprint_data.json)
32
+ ↓ tasks and developers — fully customizable
33
+ GRADIO UI (ui.py)
34
+ visual sprint board, charts, controls
35
+ ```
36
+
37
+ ---
38
+
39
+ ## 📁 What Each File Does
40
+
41
+ | File | Purpose | Change it to... |
42
+ |------|---------|----------------|
43
+ | `data/sprint_data.json` | All scenario data | Add your own tasks/devs |
44
+ | `sprint_env/models.py` | Data contracts (Action/Observation/State) | Add new fields |
45
+ | `sprint_env/tasks.py` | Task & Developer classes | Add new task types |
46
+ | `sprint_env/environment.py` | Core RL logic | Change simulation rules |
47
+ | `sprint_env/graders.py` | Scoring (easy/medium/hard) | Change scoring weights |
48
+ | `sprint_env/data_loader.py` | Loads JSON data with caching | Point to custom data |
49
+ | `server/app.py` | OpenEnv HTTP API entry point | Add new endpoints |
50
+ | `client.py` | Typed Python client for RL training | Use in training scripts |
51
+ | `ui.py` | Gradio UI + combined server | Change UI layout |
52
+ | `inference.py` | Baseline LLM agent | Change model/strategy |
53
+ | `openenv.yaml` | OpenEnv spec metadata | Update task list |
54
+
55
+ ---
56
+
57
+ ## 🔄 What Happens Each Step
58
+
59
+ ```
60
+ Day 1 → Day 2 → Day 3 → ... → Day 10 → DONE
61
+ ↑ ↑ ↑
62
+ agent agent agent
63
+ acts acts acts
64
+ ```
65
+
66
+ **One step = one day in the sprint:**
67
+
68
+ 1. Agent receives observation (all tasks, all devs, current day)
69
+ 2. Agent picks an action (e.g. "assign T1 to dev1")
70
+ 3. Environment validates the action
71
+ 4. Developers work on assigned tasks — progress increases
72
+ 5. Random events fire (dev goes sick, new bug appears)
73
+ 6. Reward is calculated and returned
74
+ 7. Repeat until Day 10 or all tasks resolved
75
+
76
+ ---
77
+
78
+ ## 💰 Reward Design — Why It Works for RL
79
+
80
+ The reward function is **shaped** (signal at every step) not **sparse** (only at the end):
81
+
82
+ ```
83
+ Good actions → positive reward immediately
84
+ Bad actions → negative reward immediately
85
+ Task done on time → bonus
86
+ Task missed deadline → penalty
87
+ Sprint ends → final_score × 10 bonus
88
+ ```
89
+
90
+ This means a learning agent gets feedback on every single decision — critical for efficient RL training with GRPO, PPO, or any policy gradient algorithm.
91
+
92
+ ---
93
+
94
+ ## ✅ How To Know Everything Is Working
95
+
96
+ ### Quick 60-second check
97
+
98
+ ```bash
99
+ # 1. Start server
100
+ python ui.py
101
+
102
+ # 2. In another terminal — health check
103
+ curl http://localhost:7860/health
104
+ # Expected: {"status":"ok","env":"ai-sprint-manager"}
105
+
106
+ # 3. Reset
107
+ curl -X POST http://localhost:7860/reset \
108
+ -H "Content-Type: application/json" \
109
+ -d '{"task_name":"easy_sprint","seed":42}'
110
+ # Expected: JSON with current_day=1, 5 tasks in backlog
111
+
112
+ # 4. Step
113
+ curl -X POST http://localhost:7860/step \
114
+ -H "Content-Type: application/json" \
115
+ -d '{"action":{"action_type":"assign","task_id":"T1","dev_id":"dev1"}}'
116
+ # Expected: reward around +1.2, T1 now in_progress
117
+
118
+ # 5. Validate
119
+ openenv validate
120
+ # Expected: [OK] ai-sprint-manager: Ready for multi-mode deployment
121
+
122
+ # 6. Run inference
123
+ python inference.py
124
+ # Expected: [START]/[STEP]/[END] lines, scores for all 3 tasks
125
+ ```
126
+
127
+ ### Full test checklist
128
+
129
+ | # | Test | How | Pass Condition |
130
+ |---|------|-----|---------------|
131
+ | 1 | Server health | `GET /health` | `{"status":"ok"}` |
132
+ | 2 | Reset works | `POST /reset` | day=1, tasks in backlog |
133
+ | 3 | Assign works | `POST /step` assign T1→dev1 | reward +1.2, T1 in_progress |
134
+ | 4 | Skill mismatch rejected | Assign backend task to frontend dev | reward -0.15, error message |
135
+ | 5 | Sprint ends | 10 skip steps | `done: true` |
136
+ | 6 | Grader runs | Check final_score in info | value between 0.0 and 1.0 |
137
+ | 7 | OpenEnv valid | `openenv validate` | `[OK]` message |
138
+ | 8 | Inference output | `python inference.py` | `[START]` `[STEP]` `[END]` lines |
139
+ | 9 | Docker build | `docker build .` | Exit code 0 |
140
+ | 10 | Docker run | `docker run -p 7860:7860 ...` then health | `{"status":"ok"}` |
141
+ | 11 | Live Space | `curl https://sejal-k-ai-sprint-manager.hf.space/health` | `{"status":"ok"}` |
142
+ | 12 | UI loads | Open http://localhost:7860 | Gradio UI visible |
143
+ | 13 | UI reset | Click Reset Sprint | Sprint board populates |
144
+ | 14 | Auto-assign | Click Auto-Assign All | Tasks move to in_progress |
145
+ | 15 | Reward chart | Take 3+ actions | Sparkline appears |
146
+
147
+ ---
148
+
149
+ ## 🎤 Project Demo Script (10 minutes)
150
+
151
+ ### Before Demo
152
+ ```bash
153
+ python ui.py
154
+ # Open http://localhost:7860 in browser — full screen
155
+ # Have terminal with inference.py output ready
156
+ ```
157
+
158
+ ### [0:00 — 1:30] The Problem
159
+ > "Software teams waste hours every sprint on planning. Which developer gets which task? What happens when someone goes sick? What if a critical bug appears on day 5? These decisions directly affect delivery speed and developer burnout."
160
+
161
+ > "We built an RL environment that simulates exactly this — so an AI agent can learn to make these decisions better."
162
+
163
+ ### [1:30 — 3:00] Show the UI
164
+ - Select `easy_sprint` → **🔄 Reset Sprint**
165
+ - Point to sprint board: *"5 tasks in backlog, 3 developers, 10 day sprint"*
166
+ - Point to Skill Guide: *"This tells you which dev is right for which task. Backend tasks need Alice, frontend tasks need Bob."*
167
+
168
+ ### [3:00 — 4:30] Manual Play — Good vs Bad Decision
169
+ - Assign T3 (frontend) → dev1 (backend): *"Wrong skill — negative reward, task rejected"*
170
+ - Assign T3 (frontend) → dev2 (frontend): *"Correct match — positive reward, task starts"*
171
+ - Point to reward chart: *"See the reward signal? This is exactly what the AI learns from."*
172
+
173
+ ### [4:30 — 6:00] Auto-Assign
174
+ - Click **🤖 Auto-Assign All**
175
+ - *"Rule-based auto-assign picks the best skill match for every task."*
176
+ - Click **▶️ Take Action** (skip) a few times
177
+ - *"Each day the sprint advances, tasks progress, deadlines approach."*
178
+
179
+ ### [6:00 — 7:30] Hard Sprint
180
+ - Reset with `hard_sprint`
181
+ - *"12 tasks, 5 developers, random events — developers go sick, urgent bugs appear mid-sprint."*
182
+ - Auto-assign, then skip a few times
183
+ - When a 🚨 event fires: *"There — urgent bug on day 4. A trained agent needs to react and reassign resources."*
184
+
185
+ ### [7:30 — 9:00] Inference Output
186
+ - Show terminal with inference.py output
187
+ - *"This is our Llama 3.1 baseline running against all 3 scenarios automatically."*
188
+ - Point to structured output: *"`[START]` `[STEP]` `[END]` — machine-parseable format the judges require."*
189
+ - Point to scores: *"Easy sprint perfect score of 1.0 — validates the environment works. Hard sprint 0.0 — shows it genuinely challenges frontier models."*
190
+
191
+ ### [9:00 — 10:00] Technical Highlights
192
+ > "What makes this submission stand out:"
193
+
194
+ - **Real-world domain** — not CartPole, not a game — actual software engineering
195
+ - **External data file** — `data/sprint_data.json` — anyone can plug in their own team
196
+ - **Typed Python client** — `client.py` makes it plug-and-play with TRL, Stable-Baselines3
197
+ - **OpenEnv compliant** — passes all 3 validation checks
198
+ - **Shaped rewards** — signal at every step, enables efficient RL training
199
+
200
+ ---
201
+
202
+ ## 🐛 Common Issues & Fixes
203
+
204
+ | Problem | Cause | Fix |
205
+ |---------|-------|-----|
206
+ | `ModuleNotFoundError` | Missing package | `pip install -r requirements.txt` |
207
+ | Port 7860 in use | Other process | Kill it or change port in ui.py |
208
+ | `401 Unauthorized` in inference | Bad HF token | Regenerate at hf.co/settings/tokens |
209
+ | Validate step 3 fails | openenv not in PATH | Activate venv before running script |
210
+ | Tasks not progressing | No devs assigned | Auto-Assign or assign manually |
211
+ | Score always 0.0 | All tasks missed | Assign earlier, prioritize urgent tasks |
212
+ | Docker timeout | venv in context | Check `.dockerignore` has `venv/` |
213
+
214
+ ---
215
+
216
+ ## 🔬 Is This a Real RL Environment?
217
+
218
+ | Criterion | Our Environment |
219
+ |-----------|----------------|
220
+ | Sequential decisions | ✅ Each day depends on previous assignments |
221
+ | Large state space | ✅ Tasks × Developers × Day — combinatorial |
222
+ | Non-trivial action space | ✅ 5 types × 12 tasks × 5 devs |
223
+ | Shaped reward | ✅ Signal every step, not just episode end |
224
+ | Stochastic transitions | ✅ Random dev absences, mid-sprint bugs |
225
+ | Clean episode boundaries | ✅ reset() gives fresh state every time |
226
+ | Partial observability | ✅ Agent can't predict future events |
227
+ | Trainable with RL | ✅ GRPO / PPO / any policy gradient |
228
+
229
+ **A trained RL agent (not zero-shot LLM) should score 0.7+ on medium and 0.4+ on hard** after sufficient training — currently it scores 0.42 and 0.0 with baseline Llama 3.1. That's the gap RL training is meant to close.
230
+
231
+ ---
232
+
233
+ ## 📊 Baseline Score Interpretation
234
+
235
+ ```
236
+ easy_sprint: 1.00 ← LLM figured out skill matching perfectly
237
+ medium_sprint: 0.42 ← Partial success, random events hurt performance
238
+ hard_sprint: 0.00 ← Cascade failures overwhelm baseline LLM
239
+ average: 0.47
240
+ ```
241
+
242
+ These scores are intentional and show the difficulty curve works correctly:
243
+ - Easy solvable by any agent → environment is correct
244
+ - Medium shows partial success → reward shaping works
245
+ - Hard challenges frontier models → difficulty is genuine
README.md CHANGED
@@ -1,73 +1,304 @@
1
  ---
2
- base_model: unsloth/qwen2.5-1.5b-instruct-unsloth-bnb-4bit
3
- library_name: peft
4
- model_name: trained_model
5
- tags:
6
- - base_model:adapter:unsloth/qwen2.5-1.5b-instruct-unsloth-bnb-4bit
7
- - grpo
8
- - lora
9
- - transformers
10
- - trl
11
- - unsloth
12
- licence: license
13
- pipeline_tag: text-generation
14
  ---
15
 
16
- # Model Card for trained_model
17
 
18
- This model is a fine-tuned version of [unsloth/qwen2.5-1.5b-instruct-unsloth-bnb-4bit](https://huggingface.co/unsloth/qwen2.5-1.5b-instruct-unsloth-bnb-4bit).
19
- It has been trained using [TRL](https://github.com/huggingface/trl).
20
 
21
- ## Quick start
22
 
23
- ```python
24
- from transformers import pipeline
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- question = "If you had a time machine, but could only go to the past or the future once and never return, which would you choose and why?"
27
- generator = pipeline("text-generation", model="None", device="cuda")
28
- output = generator([{"role": "user", "content": question}], max_new_tokens=128, return_full_text=False)[0]
29
- print(output["generated_text"])
30
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- ## Training procedure
33
 
34
-
35
 
 
 
 
 
 
36
 
37
- This model was trained with GRPO, a method introduced in [DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models](https://huggingface.co/papers/2402.03300).
38
 
39
- ### Framework versions
40
 
41
- - PEFT 0.18.1
42
- - TRL: 0.24.0
43
- - Transformers: 5.5.0
44
- - Pytorch: 2.10.0+cu128
45
- - Datasets: 4.3.0
46
- - Tokenizers: 0.22.2
47
 
48
- ## Citations
49
 
50
- Cite GRPO as:
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- ```bibtex
53
- @article{shao2024deepseekmath,
54
- title = {{DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models}},
55
- author = {Zhihong Shao and Peiyi Wang and Qihao Zhu and Runxin Xu and Junxiao Song and Mingchuan Zhang and Y. K. Li and Y. Wu and Daya Guo},
56
- year = 2024,
57
- eprint = {arXiv:2402.03300},
58
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  ```
61
 
62
- Cite TRL as:
63
-
64
- ```bibtex
65
- @misc{vonwerra2022trl,
66
- title = {{TRL: Transformer Reinforcement Learning}},
67
- author = {Leandro von Werra and Younes Belkada and Lewis Tunstall and Edward Beeching and Tristan Thrush and Nathan Lambert and Shengyi Huang and Kashif Rasul and Quentin Gallou{\'e}dec},
68
- year = 2020,
69
- journal = {GitHub repository},
70
- publisher = {GitHub},
71
- howpublished = {\url{https://github.com/huggingface/trl}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: AI Sprint Manager
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ tags: [openenv, reinforcement-learning, agile, sprint-management, fastapi, gradio]
 
 
 
 
 
9
  ---
10
 
11
+ # 🤖 AI Sprint Manager — OpenEnv
12
 
13
+ > **A reinforcement learning environment where an AI agent acts as a Tech Lead managing agile software sprints.**
 
14
 
15
+ ---
16
 
17
+ ## 🎯 What Is This?
18
+
19
+ Modern software teams spend enormous time on sprint planning decisions:
20
+ - Which developer gets which task?
21
+ - What do you do when someone goes sick mid-sprint?
22
+ - How do you handle an urgent production bug that appears on day 5?
23
+
24
+ This environment simulates these real-world decisions so an AI agent can **learn optimal sprint management strategies** through reinforcement learning.
25
+
26
+ The agent plays the role of a Tech Lead. Each step it observes the full sprint state (tasks, developers, workloads, deadlines) and takes an action. The environment responds with a reward signal that guides learning.
27
+
28
+ ---
29
+
30
+ ## 🏗️ Architecture
31
 
 
 
 
 
32
  ```
33
+ ┌─────────────────────────────────────────┐
34
+ │ RL Agent / LLM / Training Loop │
35
+ │ (uses client.py) │
36
+ └──────────────────┬──────────────────────┘
37
+ │ HTTP reset / step / state
38
+
39
+ ┌─────────────────────────────────────────┐
40
+ │ FastAPI Server (port 7860) │
41
+ │ /reset /step /state /health │
42
+ └──────────────────┬──────────────────────┘
43
+
44
+
45
+ ┌─────────────────────────────────────────┐
46
+ │ Sprint Environment (core logic) │
47
+ │ • Task/developer simulation │
48
+ │ • Reward calculation │
49
+ │ • Random events (bugs, absences) │
50
+ │ • 3 graders: easy / medium / hard │
51
+ └──────────────────┬──────────────────────┘
52
+ │ data loaded from
53
+
54
+ ┌─────────────────────────────────────────┐
55
+ │ data/sprint_data.json │
56
+ │ (customizable — bring your own data!) │
57
+ └─────────────────────────────────────────┘
58
+ ```
59
+
60
+ ---
61
 
62
+ ## 🎮 Live Demo
63
 
 
64
 
65
+ 1. Select a sprint scenario (easy / medium / hard)
66
+ 2. Click **🔄 Reset Sprint**
67
+ 3. Use the **Skill → Dev Guide** to assign tasks correctly
68
+ 4. Or click **🤖 Auto-Assign All** to let the system decide
69
+ 5. Watch the reward history and task status update in real time
70
 
71
+ ---
72
 
73
+ ## 📐 Action Space
74
 
75
+ | Field | Type | Values |
76
+ |---|---|---|
77
+ | `action_type` | string | `assign`, `reassign`, `reprioritize`, `unblock`, `skip` |
78
+ | `task_id` | string | Task ID e.g. `"T1"`, `"T6"` |
79
+ | `dev_id` | string | Developer ID e.g. `"dev1"`, `"dev3"` |
80
+ | `new_priority` | int | 1–5 (1=highest, for reprioritize only) |
81
 
82
+ ## 📊 Observation Space
83
 
84
+ | Field | Type | Description |
85
+ |---|---|---|
86
+ | `current_day` | int | Day in sprint (1–10) |
87
+ | `sprint_length` | int | Total sprint length |
88
+ | `developers` | list | Each dev's skill, capacity, load, tasks, availability |
89
+ | `tasks` | list | Each task's type, priority, effort, deadline, status, progress |
90
+ | `reward` | float | Step reward |
91
+ | `cumulative_reward` | float | Total reward this episode |
92
+ | `tasks_completed/missed/in_progress/backlog` | int | Status counts |
93
+ | `workload_balance_score` | float | 0=unbalanced, 1=perfect |
94
+ | `events` | list | Events that just happened (completions, misses, absences) |
95
+ | `done` | bool | Whether episode is complete |
96
 
97
+ ---
98
+
99
+ ## 🎯 Tasks (Scenarios)
100
+
101
+ | ID | Difficulty | Devs | Tasks | Random Events |
102
+ |---|---|---|---|---|
103
+ | `easy_sprint` | 🟢 Easy | 3 | 5 | None |
104
+ | `medium_sprint` | 🟡 Medium | 4 | 8 | Dev absences, bugs expire |
105
+ | `hard_sprint` | 🔴 Hard | 5 | 12 | Urgent bugs mid-sprint, cascading failures |
106
+
107
+ ### Baseline Scores (meta-llama/Llama-3.1-8B-Instruct)
108
+
109
+ | Task | Score |
110
+ |---|---|
111
+ | `easy_sprint` | 0.01 |
112
+ | `medium_sprint` | 0.46 ████████ |
113
+ | `hard_sprint` | 0.01 |
114
+ | **Average** | **0.16** |
115
+
116
+ ---
117
 
118
+ ## 💰 Reward Function
119
+
120
+ | Event | Reward |
121
+ |---|---|
122
+ | Assign task (skill match) | +0.8 to +1.3 |
123
+ | Assign task (skill mismatch penalty) | +0.1 to +0.6 |
124
+ | Wrong skill / over capacity | -0.15 |
125
+ | Task completed on time | +0.5 to +2.5 |
126
+ | Task completed late | +0.1 |
127
+ | Task missed deadline | -0.3 to -1.5 |
128
+ | Urgent bug missed | -0.25 extra |
129
+ | Skip (no action) | -0.05 |
130
+ | Final score bonus | score × 10.0 |
131
+
132
+ ---
133
+
134
+ ## 🔌 API Reference
135
+
136
+ ```bash
137
+ # Health check
138
+ GET /health → {"status": "ok", "env": "ai-sprint-manager"}
139
+
140
+ # Start new episode
141
+ POST /reset
142
+ Body: {"task_name": "easy_sprint", "seed": 42}
143
+
144
+ # Take one action
145
+ POST /step
146
+ Body: {"action": {"action_type": "assign", "task_id": "T1", "dev_id": "dev1"}}
147
+
148
+ # Get full state
149
+ GET /state
150
+
151
+ # List scenarios
152
+ GET /tasks
153
  ```
154
 
155
+ ---
156
+
157
+ ## 🐍 Python Client Usage
158
+
159
+ ```python
160
+ from client import SprintEnvClient
161
+ from sprint_env.models import SprintAction
162
+
163
+ # Connect to live Space
164
+ with SprintEnvClient(base_url="https://sejal-k-ai-sprint-manager.hf.space") as env:
165
+ # Reset
166
+ obs = env.reset(task_name="medium_sprint", seed=42)
167
+
168
+ # Agent loop
169
+ while not obs["done"]:
170
+ action = SprintAction(
171
+ action_type="assign",
172
+ task_id="T1",
173
+ dev_id="dev1",
174
+ )
175
+ result = env.step(action)
176
+ print(result) # StepResult(reward=+1.20, done=False, day=2, completed=0)
177
+ obs = result.observation
178
+ ```
179
+
180
+ ---
181
+
182
+ ## 🗂️ Project Structure
183
+
184
+ ```
185
+ ai-sprint-manager-openenv/
186
+ ├── openenv.yaml # OpenEnv spec metadata
187
+ ├── pyproject.toml # Project dependencies
188
+ ├── Dockerfile # Container definition
189
+ ├── requirements.txt # Python dependencies
190
+ ├── inference.py # Baseline LLM agent script
191
+ ├── client.py # Typed Python client (for RL training)
192
+ ├── ui.py # Gradio UI + FastAPI combined server
193
+ ├── start.sh # Container startup script
194
+
195
+ ├── data/
196
+ │ └── sprint_data.json # All scenario data (customizable!)
197
+
198
+ ├── sprint_env/
199
+ │ ├── __init__.py
200
+ │ ├── models.py # Pydantic Action/Observation/State
201
+ │ ├── tasks.py # Task & Developer dataclasses
202
+ │ ├── environment.py # Core RL environment logic
203
+ │ ├── graders.py # Scoring functions (easy/medium/hard)
204
+ │ └── data_loader.py # JSON data loader with caching
205
+
206
+ └── server/
207
+ ├── __init__.py
208
+ └── app.py # OpenEnv-compliant FastAPI server entry
209
+ ```
210
+
211
+ ---
212
+
213
+ ## 🔧 Bring Your Own Data
214
+
215
+ Don't want to use our sample scenarios? Edit `data/sprint_data.json`:
216
+
217
+ ```json
218
+ {
219
+ "scenarios": {
220
+ "my_custom_sprint": {
221
+ "description": "My team's actual sprint",
222
+ "difficulty": "medium",
223
+ "developers": [
224
+ {"id": "dev1", "name": "Your Name", "skill": "backend", "capacity": 5, "productivity": 1.0}
225
+ ],
226
+ "tasks": [
227
+ {"id": "T1", "name": "Your Task", "task_type": "feature", "priority": 1,
228
+ "effort": 3, "deadline": 5, "required_skill": "backend"}
229
+ ]
230
+ }
231
+ }
232
  }
233
+ ```
234
+
235
+ Or point to your own file:
236
+ ```bash
237
+ export SPRINT_DATA_PATH=/path/to/your/data.json
238
+ python ui.py
239
+ ```
240
+
241
+ ---
242
+
243
+ ## 🚀 Setup & Run
244
+
245
+ ```bash
246
+ # Clone
247
+ git clone https://github.com/sejalsksagar/ai-sprint-manager-openenv.git
248
+ cd ai-sprint-manager-openenv
249
+
250
+ # Install
251
+ python -m venv venv
252
+ source venv/bin/activate # Windows: venv\Scripts\activate
253
+ pip install -r requirements.txt
254
+
255
+ # Configure
256
+ cp .env.example .env
257
+ # Edit .env with your HF_TOKEN
258
+
259
+ # Run locally
260
+ python ui.py
261
+ # Open http://localhost:7860
262
+
263
+ # Docker
264
+ docker build -t ai-sprint-manager .
265
+ docker run -p 7860:7860 ai-sprint-manager
266
+
267
+ # Run inference
268
+ python inference.py
269
+ ```
270
+
271
+ ---
272
+
273
+ ## 🤖 Can an RL Agent Learn From This?
274
+
275
+ Yes. The environment is designed for policy gradient training (GRPO, PPO):
276
+
277
+ ```python
278
+ # Example training loop skeleton (TRL/GRPO compatible)
279
+ from client import SprintEnvClient
280
+ from sprint_env.models import SprintAction
281
+
282
+ env = SprintEnvClient(base_url="http://localhost:7860")
283
+
284
+ for episode in range(1000):
285
+ obs = env.reset(task_name="medium_sprint")
286
+ trajectory = []
287
+
288
+ while not obs["done"]:
289
+ action = policy.sample(obs) # your policy here
290
+ result = env.step(action)
291
+ trajectory.append((obs, action, result.reward))
292
+ obs = result.observation
293
+
294
+ policy.update(trajectory) # GRPO/PPO update
295
+ ```
296
+
297
+ The shaped reward function provides learning signal at every step — not just at episode end — which is critical for efficient RL training.
298
+
299
+
300
+ ---
301
+
302
+ ## 👥 Team
303
+
304
+ Built for the **Meta PyTorch OpenEnv Hackathon x SST | India AI Hackathon '26**
SETUP.md ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🛠️ Team Setup Guide — AI Sprint Manager OpenEnv
2
+
3
+ Complete setup instructions for Windows and Mac teammates.
4
+
5
+ ---
6
+
7
+ ## 📋 Prerequisites
8
+
9
+ ### Windows
10
+
11
+ 1. **Install Python 3.11**
12
+ - Download from https://www.python.org/downloads/
13
+ - ✅ Check **"Add Python to PATH"** during install
14
+ - Verify: open PowerShell and run `python --version`
15
+
16
+ 2. **Install Git**
17
+ - Download from https://git-scm.com/download/win
18
+ - Use default options during install
19
+ - Verify: `git --version`
20
+
21
+ 3. **Install Docker Desktop**
22
+ - Download from https://www.docker.com/products/docker-desktop/
23
+ - Requires Windows 10/11 with WSL2 enabled
24
+ - After install, open Docker Desktop and wait for it to start
25
+ - Verify: `docker --version`
26
+
27
+ 4. **Install VS Code** (recommended)
28
+ - Download from https://code.visualstudio.com/
29
+
30
+ ### Mac
31
+
32
+ 1. **Install Homebrew** (package manager)
33
+ ```bash
34
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
35
+ ```
36
+
37
+ 2. **Install Python 3.11**
38
+ ```bash
39
+ brew install python@3.11
40
+ ```
41
+ Verify: `python3.11 --version`
42
+
43
+ 3. **Install Git**
44
+ ```bash
45
+ brew install git
46
+ ```
47
+ Verify: `git --version`
48
+
49
+ 4. **Install Docker Desktop**
50
+ - Download from https://www.docker.com/products/docker-desktop/
51
+ - Choose Apple Silicon or Intel depending on your Mac
52
+ - Open Docker Desktop and wait for it to start
53
+ - Verify: `docker --version`
54
+
55
+ ---
56
+
57
+ ## 📥 Clone the GitHub Repo
58
+
59
+ ```bash
60
+ # Both Windows (PowerShell) and Mac (Terminal)
61
+ git clone https://github.com/YOUR_GITHUB_USERNAME/ai-sprint-manager-openenv.git
62
+ cd ai-sprint-manager-openenv
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 🌿 Create Your Own Branch
68
+
69
+ ```bash
70
+ # Replace "yourname" with your actual name
71
+ git checkout -b feature/yourname-improvements
72
+ ```
73
+
74
+ Verify you're on your branch:
75
+ ```bash
76
+ git branch
77
+ # Should show * feature/yourname-improvements
78
+ ```
79
+
80
+ ---
81
+
82
+ ## 🐍 Set Up Virtual Environment
83
+
84
+ ### Windows
85
+ ```powershell
86
+ python -m venv venv
87
+ venv\Scripts\activate
88
+ pip install --upgrade pip
89
+ pip install -r requirements.txt
90
+ pip install python-dotenv
91
+ ```
92
+
93
+ ### Mac
94
+ ```bash
95
+ python3 -m venv venv
96
+ source venv/bin/activate
97
+ pip install --upgrade pip
98
+ pip install -r requirements.txt
99
+ pip install python-dotenv
100
+ ```
101
+
102
+ ---
103
+
104
+ ## ⚙️ Create Your `.env` File
105
+
106
+ Create a file called `.env` in the project root (never commit this!):
107
+
108
+ ```
109
+ HF_TOKEN=hf_your_token_here
110
+ API_BASE_URL=https://router.huggingface.co/v1
111
+ MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct
112
+ ENV_BASE_URL=http://localhost:7860
113
+ ```
114
+
115
+ We'll get the HF_TOKEN in the next section.
116
+
117
+ ---
118
+
119
+ ## 🤗 Create Hugging Face Account & Token
120
+
121
+ 1. Go to https://huggingface.co and sign up
122
+ 2. Go to https://huggingface.co/settings/tokens
123
+ 3. Click **New token**
124
+ 4. Name: `sprint-manager-token`
125
+ 5. Type: **Read**
126
+ 6. Click **Create token** and copy it
127
+ 7. Paste it into your `.env` file as `HF_TOKEN=hf_...`
128
+
129
+ ---
130
+
131
+ ## 🚀 Run Locally & Test
132
+
133
+ ### Terminal 1 — Start the API + UI server
134
+ #### Windows
135
+ ```powershell
136
+ python ui.py
137
+ ```
138
+ #### Mac
139
+ ```bash
140
+ python ui.py
141
+ ```
142
+
143
+ ### Terminal 2 — Test the API
144
+ #### Windows
145
+ ```powershell
146
+ Invoke-WebRequest -Uri http://localhost:7860/health -Method GET
147
+ ```
148
+ #### Mac
149
+ ```bash
150
+ curl http://localhost:7860/health
151
+ # Expected: {"status":"ok","env":"ai-sprint-manager"}
152
+ ```
153
+
154
+ ### Test via Browser
155
+ Open http://localhost:7860 — you should see the Gradio sprint board UI.
156
+
157
+ Try:
158
+ 1. Select `easy_sprint`, click **🔄 Reset Sprint**
159
+ 2. Set Action=`assign`, Task ID=`T1`, Dev ID=`dev1`
160
+ 3. Click **▶️ Take Action**
161
+ 4. You should see a positive reward and T1 assigned to Alice
162
+
163
+ ### Run Inference Locally
164
+ ```bash
165
+ # Make sure .env file has your HF_TOKEN
166
+ python inference.py
167
+ ```
168
+
169
+ ---
170
+
171
+ ## 🐳 Test with Docker
172
+
173
+ ```bash
174
+ docker build -t ai-sprint-manager .
175
+ ```
176
+
177
+ ### Windows
178
+ ```powershell
179
+ docker run -p 7860:7860 ai-sprint-manager
180
+ ```
181
+ ### Mac
182
+ ```bash
183
+ docker run -p 7860:7860 ai-sprint-manager
184
+ ```
185
+
186
+ Open http://localhost:7860 to verify.
187
+
188
+ ---
189
+
190
+ ## ☁️ Create Your Own HF Space & Deploy
191
+
192
+ 1. Go to https://huggingface.co/new-space
193
+ 2. Fill in:
194
+ - **Space name:** `ai-sprint-manager-yourname`
195
+ - **SDK:** Docker
196
+ - **Visibility:** Public
197
+ 3. Click **Create Space**
198
+
199
+ 4. Add your HF token as a Secret:
200
+ - Go to Space → **Settings** → **Variables and secrets**
201
+ - Add secret: Name=`HF_TOKEN`, Value=your token
202
+
203
+ 5. Add HF as a git remote and push your branch:
204
+ ```bash
205
+ git remote add myspace https://huggingface.co/spaces/YOUR_HF_USERNAME/ai-sprint-manager-yourname
206
+ git push myspace feature/yourname-improvements:main
207
+ ```
208
+
209
+ 6. Wait 2-3 minutes for build. Test your live Space:
210
+
211
+ ### Windows
212
+ ```powershell
213
+ Invoke-WebRequest -Uri "https://YOUR_HF_USERNAME-ai-sprint-manager-yourname.hf.space/health" -Method GET
214
+ ```
215
+ ### Mac
216
+ ```bash
217
+ curl https://YOUR_HF_USERNAME-ai-sprint-manager-yourname.hf.space/health
218
+ ```
219
+
220
+ ---
221
+
222
+ ## 🔧 Run OpenEnv Validation
223
+
224
+ ```bash
225
+ pip install openenv-core uv
226
+ uv lock
227
+ openenv validate
228
+ # Expected: [OK] ai-sprint-manager: Ready for multi-mode deployment
229
+ ```
230
+
231
+ ---
232
+
233
+ ## 💡 Suggested Improvements for Teammates
234
+
235
+ ### 🟢 Easy (Good for getting started)
236
+
237
+ 1. **Better skill matching UI** — Show a skill-to-dev mapping table in the Gradio UI so users know which dev to pick
238
+ 2. **Sprint history chart** — Add a reward-over-time line chart using `gr.Plot`
239
+ 3. **Add more tasks** — Expand `tasks.py` with more realistic task names and types
240
+ 4. **Auto-assign button** — Add a Gradio button that automatically assigns all backlog tasks using a simple rule (best skill match)
241
+
242
+ ### 🟡 Medium
243
+
244
+ 5. **Session isolation** — Currently all users share one env instance. Add session IDs so multiple users can use the UI simultaneously
245
+ 6. **Sprint replay** — Save the full episode history and add a "replay" feature to the UI
246
+ 7. **Better reward visualization** — Add a bar chart showing per-task completion status
247
+ 8. **Configurable sprint length** — Let users set sprint length (5/10/15 days) in the UI
248
+
249
+ ### 🔴 Hard (Advanced)
250
+
251
+ 9. **Real RL training loop** — Add a `train.py` script using Stable-Baselines3 or TRL+GRPO to actually train a policy
252
+ 10. **Multi-agent support** — Let multiple AI agents collaborate on sprint management
253
+ 11. **WebSocket support** — Upgrade from HTTP to WebSocket for real-time updates per OpenEnv spec
254
+ 12. **Custom sprint builder** — Let users define their own tasks and team in the UI
255
+
256
+ ---
257
+
258
+ ## 📤 Submit Your Changes
259
+
260
+ ```bash
261
+ git add .
262
+ git commit -m "feat: your improvement description"
263
+ git push origin feature/yourname-improvements
264
+ ```
265
+
266
+ Then open a Pull Request on GitHub to merge into `main`.
client.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Sprint Manager — OpenEnv Client
3
+
4
+ This is what RL researchers import in their training code.
5
+ It provides a clean typed interface to the environment server.
6
+
7
+ Usage:
8
+ import requests
9
+ from client import SprintEnvClient, SprintAction
10
+
11
+ client = SprintEnvClient(base_url="https://sejal-k-ai-sprint-manager.hf.space")
12
+ obs = client.reset(task_name="easy_sprint")
13
+ result = client.step(SprintAction(action_type="assign", task_id="T1", dev_id="dev1"))
14
+ state = client.state()
15
+ client.close()
16
+
17
+ # Or as context manager:
18
+ with SprintEnvClient(base_url="http://localhost:7860") as client:
19
+ obs = client.reset(task_name="medium_sprint", seed=42)
20
+ while not obs["done"]:
21
+ result = client.step(SprintAction(action_type="skip"))
22
+ obs = result["observation"]
23
+ """
24
+ from __future__ import annotations
25
+ import requests
26
+ from typing import Optional, Any
27
+ from sprint_env.models import SprintAction
28
+
29
+
30
+ class StepResult:
31
+ """Typed result from a step() call."""
32
+ def __init__(self, payload: dict):
33
+ self.observation: dict = payload["observation"]
34
+ self.reward: float = payload["reward"]
35
+ self.done: bool = payload["done"]
36
+ self.info: dict = payload.get("info", {})
37
+
38
+ def __repr__(self):
39
+ return (
40
+ f"StepResult(reward={self.reward:+.2f}, done={self.done}, "
41
+ f"day={self.observation.get('current_day')}, "
42
+ f"completed={self.observation.get('tasks_completed')})"
43
+ )
44
+
45
+
46
+ class SprintEnvClient:
47
+ """
48
+ HTTP client for the Sprint Manager OpenEnv environment.
49
+
50
+ Wraps the REST API into a clean Python interface.
51
+ Use this in RL training loops, notebooks, or evaluation scripts.
52
+ """
53
+
54
+ def __init__(self, base_url: str = "http://localhost:7860", timeout: int = 30):
55
+ self.base_url = base_url.rstrip("/")
56
+ self.timeout = timeout
57
+ self._session = requests.Session()
58
+
59
+ def reset(
60
+ self,
61
+ task_name: str = "easy_sprint",
62
+ seed: Optional[int] = None,
63
+ episode_id: Optional[str] = None,
64
+ ) -> dict:
65
+ """
66
+ Reset the environment and return initial observation.
67
+
68
+ Args:
69
+ task_name: One of "easy_sprint", "medium_sprint", "hard_sprint"
70
+ seed: Random seed for reproducibility
71
+ episode_id: Optional episode identifier
72
+
73
+ Returns:
74
+ Observation dict
75
+ """
76
+ payload = {"task_name": task_name}
77
+ if seed is not None:
78
+ payload["seed"] = seed
79
+ if episode_id is not None:
80
+ payload["episode_id"] = episode_id
81
+
82
+ resp = self._session.post(
83
+ f"{self.base_url}/reset", json=payload, timeout=self.timeout
84
+ )
85
+ resp.raise_for_status()
86
+ return resp.json()
87
+
88
+ def step(self, action: SprintAction) -> StepResult:
89
+ """
90
+ Take one action and advance the sprint by one day.
91
+
92
+ Args:
93
+ action: SprintAction with action_type, task_id, dev_id, new_priority
94
+
95
+ Returns:
96
+ StepResult with observation, reward, done, info
97
+ """
98
+ payload = {"action": action.model_dump()}
99
+ resp = self._session.post(
100
+ f"{self.base_url}/step", json=payload, timeout=self.timeout
101
+ )
102
+ resp.raise_for_status()
103
+ return StepResult(resp.json())
104
+
105
+ def state(self) -> dict:
106
+ """Return the full current environment state."""
107
+ resp = self._session.get(f"{self.base_url}/state", timeout=self.timeout)
108
+ resp.raise_for_status()
109
+ return resp.json()
110
+
111
+ def health(self) -> dict:
112
+ """Check server health."""
113
+ resp = self._session.get(f"{self.base_url}/health", timeout=self.timeout)
114
+ resp.raise_for_status()
115
+ return resp.json()
116
+
117
+ def list_tasks(self) -> list[dict]:
118
+ """List all available sprint scenarios."""
119
+ resp = self._session.get(f"{self.base_url}/tasks", timeout=self.timeout)
120
+ resp.raise_for_status()
121
+ return resp.json()["tasks"]
122
+
123
+ def close(self):
124
+ """Close the HTTP session."""
125
+ self._session.close()
126
+
127
+ def __enter__(self):
128
+ return self
129
+
130
+ def __exit__(self, *args):
131
+ self.close()
132
+
133
+ def __repr__(self):
134
+ return f"SprintEnvClient(base_url='{self.base_url}')"
data/project_data.json ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "scenarios": {
3
+ "project_easy": {
4
+ "description": "Small stable team, clear backlog, no cascade failures. Good for baseline LLM testing on multi-sprint horizon.",
5
+ "difficulty": "easy",
6
+ "num_sprints": 6,
7
+ "days_per_sprint": 10,
8
+ "developers": [
9
+ {"id": "dev1", "name": "Alice", "skill": "backend", "capacity": 5, "productivity": 1.2},
10
+ {"id": "dev2", "name": "Bob", "skill": "frontend", "capacity": 4, "productivity": 1.0},
11
+ {"id": "dev3", "name": "Carol", "skill": "fullstack", "capacity": 6, "productivity": 1.1},
12
+ {"id": "dev4", "name": "Dave", "skill": "devops", "capacity": 4, "productivity": 1.0}
13
+ ],
14
+ "tasks": [
15
+ {"id": "T01", "name": "User Auth API", "task_type": "feature", "priority": 1, "effort": 3, "sprint": 1, "deadline_day": 8, "required_skill": "backend", "depends_on": []},
16
+ {"id": "T02", "name": "Login UI", "task_type": "feature", "priority": 1, "effort": 3, "sprint": 1, "deadline_day": 9, "required_skill": "frontend", "depends_on": []},
17
+ {"id": "T03", "name": "CI/CD Setup", "task_type": "feature", "priority": 2, "effort": 3, "sprint": 1, "deadline_day": 10, "required_skill": "devops", "depends_on": []},
18
+ {"id": "T04", "name": "Database Schema v1", "task_type": "feature", "priority": 1, "effort": 4, "sprint": 1, "deadline_day": 9, "required_skill": "backend", "depends_on": []},
19
+
20
+ {"id": "T05", "name": "User Profile API", "task_type": "feature", "priority": 1, "effort": 4, "sprint": 2, "deadline_day": 18, "required_skill": "backend", "depends_on": ["T01"]},
21
+ {"id": "T06", "name": "Profile Page UI", "task_type": "feature", "priority": 2, "effort": 3, "sprint": 2, "deadline_day": 19, "required_skill": "frontend", "depends_on": ["T02"]},
22
+ {"id": "T07", "name": "Fix Login CSS Bug", "task_type": "bug", "priority": 1, "effort": 1, "sprint": 2, "deadline_day": 13, "required_skill": "frontend", "depends_on": []},
23
+ {"id": "T08", "name": "Docker Deployment", "task_type": "feature", "priority": 2, "effort": 3, "sprint": 2, "deadline_day": 20, "required_skill": "devops", "depends_on": ["T03"]},
24
+
25
+ {"id": "T09", "name": "Search API", "task_type": "feature", "priority": 2, "effort": 4, "sprint": 3, "deadline_day": 28, "required_skill": "backend", "depends_on": ["T04"]},
26
+ {"id": "T10", "name": "Search UI", "task_type": "feature", "priority": 2, "effort": 3, "sprint": 3, "deadline_day": 29, "required_skill": "frontend", "depends_on": []},
27
+ {"id": "T11", "name": "Email Notifications", "task_type": "feature", "priority": 3, "effort": 3, "sprint": 3, "deadline_day": 30, "required_skill": "backend", "depends_on": ["T05"]},
28
+ {"id": "T12", "name": "Staging Environment", "task_type": "feature", "priority": 2, "effort": 2, "sprint": 3, "deadline_day": 27, "required_skill": "devops", "depends_on": ["T08"]},
29
+
30
+ {"id": "T13", "name": "Payment Integration", "task_type": "feature", "priority": 1, "effort": 5, "sprint": 4, "deadline_day": 38, "required_skill": "backend", "depends_on": ["T05"]},
31
+ {"id": "T14", "name": "Checkout UI", "task_type": "feature", "priority": 1, "effort": 4, "sprint": 4, "deadline_day": 39, "required_skill": "frontend", "depends_on": ["T10"]},
32
+ {"id": "T15", "name": "API Rate Limiting", "task_type": "tech_debt", "priority": 3, "effort": 3, "sprint": 4, "deadline_day": 40, "required_skill": "backend", "depends_on": []},
33
+ {"id": "T16", "name": "Load Balancer Config", "task_type": "feature", "priority": 2, "effort": 3, "sprint": 4, "deadline_day": 38, "required_skill": "devops", "depends_on": ["T12"]},
34
+
35
+ {"id": "T17", "name": "Analytics Dashboard", "task_type": "feature", "priority": 2, "effort": 4, "sprint": 5, "deadline_day": 49, "required_skill": "fullstack", "depends_on": ["T13"]},
36
+ {"id": "T18", "name": "Accessibility Audit", "task_type": "tech_debt", "priority": 3, "effort": 3, "sprint": 5, "deadline_day": 50, "required_skill": "frontend", "depends_on": []},
37
+ {"id": "T19", "name": "Performance Optimisation", "task_type": "tech_debt", "priority": 3, "effort": 3, "sprint": 5, "deadline_day": 48, "required_skill": "backend", "depends_on": []},
38
+ {"id": "T20", "name": "Monitoring & Alerting", "task_type": "feature", "priority": 2, "effort": 3, "sprint": 5, "deadline_day": 50, "required_skill": "devops", "depends_on": ["T16"]},
39
+
40
+ {"id": "T21", "name": "Final Security Hardening", "task_type": "bug", "priority": 1, "effort": 4, "sprint": 6, "deadline_day": 58, "required_skill": "backend", "depends_on": ["T15"]},
41
+ {"id": "T22", "name": "Production Release UI", "task_type": "feature", "priority": 1, "effort": 3, "sprint": 6, "deadline_day": 59, "required_skill": "frontend", "depends_on": ["T14", "T18"]},
42
+ {"id": "T23", "name": "Runbook Documentation", "task_type": "tech_debt", "priority": 3, "effort": 2, "sprint": 6, "deadline_day": 60, "required_skill": "fullstack", "depends_on": []},
43
+ {"id": "T24", "name": "Production Deploy", "task_type": "feature", "priority": 1, "effort": 3, "sprint": 6, "deadline_day": 60, "required_skill": "devops", "depends_on": ["T20", "T21"]}
44
+ ],
45
+ "instructions": [
46
+ {"id": "I01", "release_day": 1, "text": "Prioritise auth tasks above all others in sprint 1.", "target_sprint": 1, "affects_tasks": ["T01", "T02"]},
47
+ {"id": "I02", "release_day": 5, "text": "Ensure CI/CD is completed before sprint 1 ends.", "target_sprint": 1, "affects_tasks": ["T03"]},
48
+ {"id": "I03", "release_day": 11, "text": "The login CSS bug is blocking QA — resolve first in sprint 2.", "target_sprint": 2, "affects_tasks": ["T07"]},
49
+ {"id": "I04", "release_day": 14, "text": "Docker deployment must be validated by day 20.", "target_sprint": 2, "affects_tasks": ["T08"]},
50
+ {"id": "I05", "release_day": 21, "text": "Search must be demo-ready by end of sprint 3.", "target_sprint": 3, "affects_tasks": ["T09", "T10"]},
51
+ {"id": "I06", "release_day": 25, "text": "Email notifications are now a stakeholder requirement for sprint 3.", "target_sprint": 3, "affects_tasks": ["T11"]},
52
+ {"id": "I07", "release_day": 31, "text": "Payment integration is the top priority for sprint 4 — assign best backend dev.", "target_sprint": 4, "affects_tasks": ["T13"]},
53
+ {"id": "I08", "release_day": 35, "text": "API rate limiting must ship in sprint 4 before production scaling.", "target_sprint": 4, "affects_tasks": ["T15"]},
54
+ {"id": "I09", "release_day": 41, "text": "Analytics dashboard needs to be interactive — fullstack only.", "target_sprint": 5, "affects_tasks": ["T17"]},
55
+ {"id": "I10", "release_day": 45, "text": "Accessibility audit result must be attached to sprint 5 report.", "target_sprint": 5, "affects_tasks": ["T18"]},
56
+ {"id": "I11", "release_day": 51, "text": "Security hardening is a release gate — cannot ship without it.", "target_sprint": 6, "affects_tasks": ["T21"]},
57
+ {"id": "I12", "release_day": 55, "text": "Production deploy is on day 60 — no delays accepted.", "target_sprint": 6, "affects_tasks": ["T24"]}
58
+ ]
59
+ },
60
+
61
+ "project_medium": {
62
+ "description": "Mid-size team with developer absences, mid-sprint urgent bugs, and instructions that conflict with prior plans.",
63
+ "difficulty": "medium",
64
+ "num_sprints": 6,
65
+ "days_per_sprint": 10,
66
+ "developers": [
67
+ {"id": "dev1", "name": "Alice", "skill": "backend", "capacity": 5, "productivity": 1.1},
68
+ {"id": "dev2", "name": "Bob", "skill": "frontend", "capacity": 4, "productivity": 0.9},
69
+ {"id": "dev3", "name": "Carol", "skill": "devops", "capacity": 5, "productivity": 1.0},
70
+ {"id": "dev4", "name": "Dave", "skill": "fullstack", "capacity": 6, "productivity": 1.2},
71
+ {"id": "dev5", "name": "Eve", "skill": "backend", "capacity": 5, "productivity": 1.0}
72
+ ],
73
+ "absences": [
74
+ {"dev_id": "dev2", "day_start": 14, "day_end": 16, "reason": "sick leave"},
75
+ {"dev_id": "dev4", "day_start": 33, "day_end": 35, "reason": "conference"},
76
+ {"dev_id": "dev1", "day_start": 52, "day_end": 54, "reason": "vacation"}
77
+ ],
78
+ "tasks": [
79
+ {"id": "T01", "name": "Auth Microservice", "task_type": "feature", "priority": 1, "effort": 5, "sprint": 1, "deadline_day": 8, "required_skill": "backend", "depends_on": []},
80
+ {"id": "T02", "name": "Login & Register UI", "task_type": "feature", "priority": 1, "effort": 4, "sprint": 1, "deadline_day": 9, "required_skill": "frontend", "depends_on": []},
81
+ {"id": "T03", "name": "Kubernetes Base Setup", "task_type": "feature", "priority": 2, "effort": 5, "sprint": 1, "deadline_day": 10, "required_skill": "devops", "depends_on": []},
82
+ {"id": "T04", "name": "DB Schema & Migrations", "task_type": "feature", "priority": 1, "effort": 4, "sprint": 1, "deadline_day": 9, "required_skill": "backend", "depends_on": []},
83
+ {"id": "T05", "name": "API Gateway Setup", "task_type": "feature", "priority": 2, "effort": 4, "sprint": 1, "deadline_day": 10, "required_skill": "fullstack", "depends_on": []},
84
+
85
+ {"id": "T06", "name": "User Profile Service", "task_type": "feature", "priority": 1, "effort": 5, "sprint": 2, "deadline_day": 18, "required_skill": "backend", "depends_on": ["T01"]},
86
+ {"id": "T07", "name": "Profile & Settings UI", "task_type": "feature", "priority": 2, "effort": 4, "sprint": 2, "deadline_day": 19, "required_skill": "frontend", "depends_on": ["T02"]},
87
+ {"id": "T08", "name": "Prod DB Crash Fix", "task_type": "urgent_bug", "priority": 1, "effort": 2, "sprint": 2, "deadline_day": 12, "required_skill": "backend", "depends_on": []},
88
+ {"id": "T09", "name": "Helm Chart Deployment", "task_type": "feature", "priority": 2, "effort": 4, "sprint": 2, "deadline_day": 20, "required_skill": "devops", "depends_on": ["T03"]},
89
+ {"id": "T10", "name": "Inter-Service Auth", "task_type": "feature", "priority": 1, "effort": 4, "sprint": 2, "deadline_day": 19, "required_skill": "fullstack", "depends_on": ["T05"]},
90
+
91
+ {"id": "T11", "name": "Notification Service", "task_type": "feature", "priority": 2, "effort": 5, "sprint": 3, "deadline_day": 28, "required_skill": "backend", "depends_on": ["T06"]},
92
+ {"id": "T12", "name": "Notification Preference UI", "task_type": "feature", "priority": 2, "effort": 3, "sprint": 3, "deadline_day": 29, "required_skill": "frontend", "depends_on": ["T07"]},
93
+ {"id": "T13", "name": "Payment Gateway API", "task_type": "feature", "priority": 1, "effort": 6, "sprint": 3, "deadline_day": 30, "required_skill": "backend", "depends_on": ["T04"]},
94
+ {"id": "T14", "name": "Service Mesh (Istio)", "task_type": "feature", "priority": 2, "effort": 5, "sprint": 3, "deadline_day": 28, "required_skill": "devops", "depends_on": ["T09"]},
95
+ {"id": "T15", "name": "Search & Filter API", "task_type": "feature", "priority": 2, "effort": 4, "sprint": 3, "deadline_day": 29, "required_skill": "fullstack", "depends_on": ["T10"]},
96
+
97
+ {"id": "T16", "name": "Checkout & Orders API", "task_type": "feature", "priority": 1, "effort": 5, "sprint": 4, "deadline_day": 38, "required_skill": "backend", "depends_on": ["T13"]},
98
+ {"id": "T17", "name": "Checkout Flow UI", "task_type": "feature", "priority": 1, "effort": 5, "sprint": 4, "deadline_day": 39, "required_skill": "frontend", "depends_on": ["T12"]},
99
+ {"id": "T18", "name": "Payment Security Audit", "task_type": "bug", "priority": 1, "effort": 3, "sprint": 4, "deadline_day": 36, "required_skill": "backend", "depends_on": ["T13"]},
100
+ {"id": "T19", "name": "Canary Deployment Setup", "task_type": "feature", "priority": 2, "effort": 4, "sprint": 4, "deadline_day": 39, "required_skill": "devops", "depends_on": ["T14"]},
101
+ {"id": "T20", "name": "Full-Text Search (ES)", "task_type": "feature", "priority": 3, "effort": 5, "sprint": 4, "deadline_day": 40, "required_skill": "fullstack", "depends_on": ["T15"]},
102
+
103
+ {"id": "T21", "name": "Reporting & Analytics API", "task_type": "feature", "priority": 2, "effort": 5, "sprint": 5, "deadline_day": 49, "required_skill": "backend", "depends_on": ["T16"]},
104
+ {"id": "T22", "name": "Analytics Dashboard UI", "task_type": "feature", "priority": 2, "effort": 4, "sprint": 5, "deadline_day": 49, "required_skill": "frontend", "depends_on": ["T17"]},
105
+ {"id": "T23", "name": "SLA & SLO Monitoring", "task_type": "tech_debt", "priority": 3, "effort": 3, "sprint": 5, "deadline_day": 50, "required_skill": "devops", "depends_on": ["T19"]},
106
+ {"id": "T24", "name": "Mobile API Optimisation", "task_type": "tech_debt", "priority": 3, "effort": 4, "sprint": 5, "deadline_day": 50, "required_skill": "fullstack", "depends_on": ["T20"]},
107
+ {"id": "T25", "name": "Data Privacy Compliance", "task_type": "tech_debt", "priority": 2, "effort": 3, "sprint": 5, "deadline_day": 48, "required_skill": "backend", "depends_on": []},
108
+
109
+ {"id": "T26", "name": "Pen Test & Remediation", "task_type": "bug", "priority": 1, "effort": 5, "sprint": 6, "deadline_day": 57, "required_skill": "backend", "depends_on": ["T18", "T25"]},
110
+ {"id": "T27", "name": "Final UI Polish & QA", "task_type": "feature", "priority": 1, "effort": 4, "sprint": 6, "deadline_day": 59, "required_skill": "frontend", "depends_on": ["T22"]},
111
+ {"id": "T28", "name": "Blue-Green Production Swap", "task_type": "feature", "priority": 1, "effort": 4, "sprint": 6, "deadline_day": 60, "required_skill": "devops", "depends_on": ["T23"]},
112
+ {"id": "T29", "name": "Runbooks & Postmortem Docs", "task_type": "tech_debt", "priority": 3, "effort": 3, "sprint": 6, "deadline_day": 60, "required_skill": "fullstack", "depends_on": []},
113
+ {"id": "T30", "name": "Stakeholder Demo Prep", "task_type": "feature", "priority": 2, "effort": 2, "sprint": 6, "deadline_day": 60, "required_skill": "fullstack", "depends_on": ["T21", "T22"]}
114
+ ],
115
+ "instructions": [
116
+ {"id": "I01", "release_day": 1, "text": "Auth microservice is a hard dependency for all downstream services — finish before day 8.", "target_sprint": 1, "affects_tasks": ["T01"]},
117
+ {"id": "I02", "release_day": 3, "text": "API gateway must be validated with at least one downstream service by end of sprint 1.", "target_sprint": 1, "affects_tasks": ["T05"]},
118
+ {"id": "I03", "release_day": 7, "text": "Kubernetes base setup is blocking the DevOps track — escalate if behind.", "target_sprint": 1, "affects_tasks": ["T03"]},
119
+ {"id": "I04", "release_day": 11, "text": "Production DB crash is P0 — drop everything and assign a backend dev immediately.", "target_sprint": 2, "affects_tasks": ["T08"]},
120
+ {"id": "I05", "release_day": 15, "text": "Bob is on sick leave — redistribute frontend tasks for sprint 2.", "target_sprint": 2, "affects_tasks": ["T07"]},
121
+ {"id": "I06", "release_day": 18, "text": "Inter-service auth must be ready before notification service starts in sprint 3.", "target_sprint": 2, "affects_tasks": ["T10"]},
122
+ {"id": "I07", "release_day": 21, "text": "Payment gateway is now a sprint 3 commitment to the business — no slip.", "target_sprint": 3, "affects_tasks": ["T13"]},
123
+ {"id": "I08", "release_day": 24, "text": "Service mesh complexity is underestimated — pair Carol with Dave on Istio.", "target_sprint": 3, "affects_tasks": ["T14"]},
124
+ {"id": "I09", "release_day": 27, "text": "Add payment security audit as a sprint 3 exit gate before sprint 4 checkout work begins.", "target_sprint": 3, "affects_tasks": ["T18"]},
125
+ {"id": "I10", "release_day": 31, "text": "Checkout flow is top priority — nothing ships in sprint 4 before T16 and T17 are green.", "target_sprint": 4, "affects_tasks": ["T16", "T17"]},
126
+ {"id": "I11", "release_day": 33, "text": "Dave is at conference days 33-35 — replan sprint 4 fullstack tasks.", "target_sprint": 4, "affects_tasks": ["T20"]},
127
+ {"id": "I12", "release_day": 36, "text": "Canary deployment must be validated before blue-green swap in sprint 6.", "target_sprint": 4, "affects_tasks": ["T19"]},
128
+ {"id": "I13", "release_day": 41, "text": "Data privacy compliance is now a legal requirement — must complete sprint 5.", "target_sprint": 5, "affects_tasks": ["T25"]},
129
+ {"id": "I14", "release_day": 44, "text": "Analytics dashboard needs real-time updates — scope increase noted.", "target_sprint": 5, "affects_tasks": ["T22"]},
130
+ {"id": "I15", "release_day": 48, "text": "SLA monitoring must include payment service endpoints.", "target_sprint": 5, "affects_tasks": ["T23"]},
131
+ {"id": "I16", "release_day": 51, "text": "Alice is on vacation days 52-54 — replan sprint 6 backend tasks.", "target_sprint": 6, "affects_tasks": ["T26"]},
132
+ {"id": "I17", "release_day": 53, "text": "Pen test results are back — two critical findings must be fixed before blue-green swap.", "target_sprint": 6, "affects_tasks": ["T26", "T28"]},
133
+ {"id": "I18", "release_day": 56, "text": "Stakeholder demo is on day 60 — runbooks and demo prep are release gates.", "target_sprint": 6, "affects_tasks": ["T29", "T30"]}
134
+ ]
135
+ },
136
+
137
+ "project_hard": {
138
+ "description": "Large team, cascading failures, urgent mid-project pivots, conflicting instructions, and hard delivery deadline with zero tolerance for missed dependencies.",
139
+ "difficulty": "hard",
140
+ "num_sprints": 6,
141
+ "days_per_sprint": 10,
142
+ "developers": [
143
+ {"id": "dev1", "name": "Alice", "skill": "backend", "capacity": 5, "productivity": 1.3},
144
+ {"id": "dev2", "name": "Bob", "skill": "frontend", "capacity": 4, "productivity": 0.8},
145
+ {"id": "dev3", "name": "Carol", "skill": "devops", "capacity": 5, "productivity": 1.0},
146
+ {"id": "dev4", "name": "Dave", "skill": "fullstack", "capacity": 7, "productivity": 1.1},
147
+ {"id": "dev5", "name": "Eve", "skill": "backend", "capacity": 5, "productivity": 0.9},
148
+ {"id": "dev6", "name": "Frank", "skill": "frontend", "capacity": 4, "productivity": 1.0},
149
+ {"id": "dev7", "name": "Grace", "skill": "devops", "capacity": 5, "productivity": 1.1}
150
+ ],
151
+ "absences": [
152
+ {"dev_id": "dev2", "day_start": 12, "day_end": 15, "reason": "sick leave"},
153
+ {"dev_id": "dev5", "day_start": 23, "day_end": 26, "reason": "family emergency"},
154
+ {"dev_id": "dev4", "day_start": 33, "day_end": 37, "reason": "conference"},
155
+ {"dev_id": "dev1", "day_start": 51, "day_end": 55, "reason": "vacation"},
156
+ {"dev_id": "dev3", "day_start": 43, "day_end": 45, "reason": "sick leave"}
157
+ ],
158
+ "tasks": [
159
+ {"id": "T01", "name": "Auth & IAM Service", "task_type": "feature", "priority": 1, "effort": 6, "sprint": 1, "deadline_day": 7, "required_skill": "backend", "depends_on": []},
160
+ {"id": "T02", "name": "API Gateway + Rate Limiting", "task_type": "feature", "priority": 1, "effort": 5, "sprint": 1, "deadline_day": 8, "required_skill": "fullstack", "depends_on": []},
161
+ {"id": "T03", "name": "Design System Setup", "task_type": "feature", "priority": 2, "effort": 4, "sprint": 1, "deadline_day": 9, "required_skill": "frontend", "depends_on": []},
162
+ {"id": "T04", "name": "Kubernetes + Helm Base", "task_type": "feature", "priority": 1, "effort": 6, "sprint": 1, "deadline_day": 9, "required_skill": "devops", "depends_on": []},
163
+ {"id": "T05", "name": "Core DB Schema & Seed Data", "task_type": "feature", "priority": 1, "effort": 4, "sprint": 1, "deadline_day": 8, "required_skill": "backend", "depends_on": []},
164
+ {"id": "T06", "name": "Service-to-Service Auth", "task_type": "feature", "priority": 1, "effort": 4, "sprint": 1, "deadline_day": 10, "required_skill": "fullstack", "depends_on": ["T01"]},
165
+ {"id": "T07", "name": "CI/CD Pipelines", "task_type": "feature", "priority": 2, "effort": 4, "sprint": 1, "deadline_day": 10, "required_skill": "devops", "depends_on": []},
166
+
167
+ {"id": "T08", "name": "User Management API", "task_type": "feature", "priority": 1, "effort": 5, "sprint": 2, "deadline_day": 17, "required_skill": "backend", "depends_on": ["T01", "T05"]},
168
+ {"id": "T09", "name": "User Management UI", "task_type": "feature", "priority": 1, "effort": 4, "sprint": 2, "deadline_day": 18, "required_skill": "frontend", "depends_on": ["T03"]},
169
+ {"id": "T10", "name": "Istio Service Mesh", "task_type": "feature", "priority": 2, "effort": 6, "sprint": 2, "deadline_day": 20, "required_skill": "devops", "depends_on": ["T04"]},
170
+ {"id": "T11", "name": "Event Bus (Kafka)", "task_type": "feature", "priority": 1, "effort": 6, "sprint": 2, "deadline_day": 19, "required_skill": "fullstack", "depends_on": ["T06"]},
171
+ {"id": "T12", "name": "Prod DB Failure — Hotfix", "task_type": "urgent_bug", "priority": 1, "effort": 3, "sprint": 2, "deadline_day": 12, "required_skill": "backend", "depends_on": []},
172
+ {"id": "T13", "name": "Payment Compliance Spike", "task_type": "tech_debt", "priority": 2, "effort": 3, "sprint": 2, "deadline_day": 20, "required_skill": "backend", "depends_on": []},
173
+
174
+ {"id": "T14", "name": "Payment Gateway Service", "task_type": "feature", "priority": 1, "effort": 7, "sprint": 3, "deadline_day": 28, "required_skill": "backend", "depends_on": ["T08", "T13"]},
175
+ {"id": "T15", "name": "Checkout UI — Multi-step", "task_type": "feature", "priority": 1, "effort": 6, "sprint": 3, "deadline_day": 29, "required_skill": "frontend", "depends_on": ["T09"]},
176
+ {"id": "T16", "name": "Canary + Rollback Automation", "task_type": "feature", "priority": 2, "effort": 5, "sprint": 3, "deadline_day": 30, "required_skill": "devops", "depends_on": ["T10"]},
177
+ {"id": "T17", "name": "Notification & Event Service", "task_type": "feature", "priority": 2, "effort": 5, "sprint": 3, "deadline_day": 29, "required_skill": "fullstack", "depends_on": ["T11"]},
178
+ {"id": "T18", "name": "Inventory & Orders API", "task_type": "feature", "priority": 1, "effort": 6, "sprint": 3, "deadline_day": 28, "required_skill": "backend", "depends_on": ["T05"]},
179
+ {"id": "T19", "name": "Payment Security Audit", "task_type": "bug", "priority": 1, "effort": 3, "sprint": 3, "deadline_day": 27, "required_skill": "backend", "depends_on": ["T14"]},
180
+
181
+ {"id": "T20", "name": "Order Management UI", "task_type": "feature", "priority": 1, "effort": 5, "sprint": 4, "deadline_day": 37, "required_skill": "frontend", "depends_on": ["T15", "T18"]},
182
+ {"id": "T21", "name": "Fraud Detection Service", "task_type": "feature", "priority": 1, "effort": 6, "sprint": 4, "deadline_day": 38, "required_skill": "fullstack", "depends_on": ["T14", "T17"]},
183
+ {"id": "T22", "name": "Full-Text Search (ES)", "task_type": "feature", "priority": 2, "effort": 5, "sprint": 4, "deadline_day": 39, "required_skill": "backend", "depends_on": ["T18"]},
184
+ {"id": "T23", "name": "Multi-Region Failover", "task_type": "feature", "priority": 2, "effort": 6, "sprint": 4, "deadline_day": 40, "required_skill": "devops", "depends_on": ["T16"]},
185
+ {"id": "T24", "name": "Mid-Sprint Scope Pivot — Recs", "task_type": "feature", "priority": 2, "effort": 5, "sprint": 4, "deadline_day": 39, "required_skill": "fullstack", "depends_on": []},
186
+ {"id": "T25", "name": "Payment Gateway Bug Surge", "task_type": "urgent_bug", "priority": 1, "effort": 4, "sprint": 4, "deadline_day": 34, "required_skill": "backend", "depends_on": []},
187
+
188
+ {"id": "T26", "name": "Reporting & BI API", "task_type": "feature", "priority": 2, "effort": 5, "sprint": 5, "deadline_day": 48, "required_skill": "backend", "depends_on": ["T22"]},
189
+ {"id": "T27", "name": "Executive Dashboard UI", "task_type": "feature", "priority": 2, "effort": 5, "sprint": 5, "deadline_day": 49, "required_skill": "frontend", "depends_on": ["T20"]},
190
+ {"id": "T28", "name": "SLO / SLA Monitoring", "task_type": "tech_debt", "priority": 2, "effort": 4, "sprint": 5, "deadline_day": 49, "required_skill": "devops", "depends_on": ["T23"]},
191
+ {"id": "T29", "name": "Recommendation Engine v1", "task_type": "feature", "priority": 2, "effort": 6, "sprint": 5, "deadline_day": 50, "required_skill": "fullstack", "depends_on": ["T24"]},
192
+ {"id": "T30", "name": "GDPR / Data Privacy Pass", "task_type": "tech_debt", "priority": 1, "effort": 4, "sprint": 5, "deadline_day": 48, "required_skill": "backend", "depends_on": []},
193
+ {"id": "T31", "name": "Mobile API Adaptation Layer", "task_type": "feature", "priority": 3, "effort": 4, "sprint": 5, "deadline_day": 50, "required_skill": "fullstack", "depends_on": ["T21"]},
194
+
195
+ {"id": "T32", "name": "Penetration Test & Fixes", "task_type": "bug", "priority": 1, "effort": 6, "sprint": 6, "deadline_day": 56, "required_skill": "backend", "depends_on": ["T19", "T30"]},
196
+ {"id": "T33", "name": "Final UI QA & Accessibility", "task_type": "bug", "priority": 1, "effort": 4, "sprint": 6, "deadline_day": 58, "required_skill": "frontend", "depends_on": ["T27"]},
197
+ {"id": "T34", "name": "Blue-Green Production Deploy", "task_type": "feature", "priority": 1, "effort": 5, "sprint": 6, "deadline_day": 60, "required_skill": "devops", "depends_on": ["T28", "T32"]},
198
+ {"id": "T35", "name": "Fraud Rule Tuning", "task_type": "tech_debt", "priority": 2, "effort": 3, "sprint": 6, "deadline_day": 58, "required_skill": "fullstack", "depends_on": ["T21"]},
199
+ {"id": "T36", "name": "Runbooks & DR Documentation", "task_type": "tech_debt", "priority": 2, "effort": 3, "sprint": 6, "deadline_day": 59, "required_skill": "fullstack", "depends_on": []},
200
+ {"id": "T37", "name": "Stakeholder Demo & Handoff", "task_type": "feature", "priority": 1, "effort": 2, "sprint": 6, "deadline_day": 60, "required_skill": "fullstack", "depends_on": ["T33", "T35", "T36"]}
201
+ ],
202
+ "instructions": [
203
+ {"id": "I01", "release_day": 1, "text": "Auth & IAM must be done before day 7 — it gates all other services.", "target_sprint": 1, "affects_tasks": ["T01"]},
204
+ {"id": "I02", "release_day": 2, "text": "API gateway rate limiting is a launch requirement — not optional.", "target_sprint": 1, "affects_tasks": ["T02"]},
205
+ {"id": "I03", "release_day": 5, "text": "Service-to-service auth must be validated in sprint 1 before Kafka work begins.", "target_sprint": 1, "affects_tasks": ["T06"]},
206
+ {"id": "I04", "release_day": 8, "text": "Kubernetes must be ready for sprint 2 deployments — escalate any blockers.", "target_sprint": 1, "affects_tasks": ["T04"]},
207
+ {"id": "I05", "release_day": 11, "text": "Prod DB failure is P0 — assign Alice immediately, push T08 if needed.", "target_sprint": 2, "affects_tasks": ["T12"]},
208
+ {"id": "I06", "release_day": 12, "text": "Bob is on sick leave days 12-15 — redistribute T09 to Frank.", "target_sprint": 2, "affects_tasks": ["T09"]},
209
+ {"id": "I07", "release_day": 16, "text": "Kafka event bus is a hard dependency for notification service in sprint 3.", "target_sprint": 2, "affects_tasks": ["T11"]},
210
+ {"id": "I08", "release_day": 19, "text": "Payment compliance spike must close before sprint 3 payment work starts.", "target_sprint": 2, "affects_tasks": ["T13"]},
211
+ {"id": "I09", "release_day": 21, "text": "Payment gateway is the sprint 3 critical path — assign Eve and Alice.", "target_sprint": 3, "affects_tasks": ["T14"]},
212
+ {"id": "I10", "release_day": 23, "text": "Eve is out days 23-26 — replan sprint 3 backend with Alice carrying payment.", "target_sprint": 3, "affects_tasks": ["T14", "T18"]},
213
+ {"id": "I11", "release_day": 25, "text": "Payment security audit is a release gate — must pass before checkout UI is signed off.", "target_sprint": 3, "affects_tasks": ["T19"]},
214
+ {"id": "I12", "release_day": 28, "text": "Canary rollback automation must be validated before multi-region failover in sprint 4.", "target_sprint": 3, "affects_tasks": ["T16"]},
215
+ {"id": "I13", "release_day": 31, "text": "Fraud detection is now a sprint 4 commitment — do not descope.", "target_sprint": 4, "affects_tasks": ["T21"]},
216
+ {"id": "I14", "release_day": 33, "text": "Dave is at conference days 33-37 — all fullstack sprint 4 tasks need replan.", "target_sprint": 4, "affects_tasks": ["T21", "T24"]},
217
+ {"id": "I15", "release_day": 33, "text": "Payment bug surge T25 is P0 — assign best available backend dev.", "target_sprint": 4, "affects_tasks": ["T25"]},
218
+ {"id": "I16", "release_day": 36, "text": "Recommendation engine scope has been greenlit — T24 must ship in sprint 4.", "target_sprint": 4, "affects_tasks": ["T24"]},
219
+ {"id": "I17", "release_day": 39, "text": "Multi-region failover must be tested before sprint 6 production deploy.", "target_sprint": 4, "affects_tasks": ["T23"]},
220
+ {"id": "I18", "release_day": 41, "text": "GDPR pass is now a legal requirement — must complete in sprint 5 no exceptions.", "target_sprint": 5, "affects_tasks": ["T30"]},
221
+ {"id": "I19", "release_day": 43, "text": "Carol is sick days 43-45 — DevOps SLO monitoring may slip, flag risk.", "target_sprint": 5, "affects_tasks": ["T28"]},
222
+ {"id": "I20", "release_day": 45, "text": "Executive dashboard needs live data feed — scope increase, coordinate with backend.", "target_sprint": 5, "affects_tasks": ["T27", "T26"]},
223
+ {"id": "I21", "release_day": 47, "text": "Mobile API layer has been reprioritised by CPO — must not miss sprint 5.", "target_sprint": 5, "affects_tasks": ["T31"]},
224
+ {"id": "I22", "release_day": 51, "text": "Alice is on vacation days 51-55 — pen test fixes must be pre-assigned.", "target_sprint": 6, "affects_tasks": ["T32"]},
225
+ {"id": "I23", "release_day": 53, "text": "Pen test returned 3 critical CVEs — T32 scope has increased, adjust sprint 6 plan.", "target_sprint": 6, "affects_tasks": ["T32"]},
226
+ {"id": "I24", "release_day": 55, "text": "Blue-green deploy is hard-locked to day 60 — no rescheduling.", "target_sprint": 6, "affects_tasks": ["T34"]},
227
+ {"id": "I25", "release_day": 57, "text": "Stakeholder demo requires fraud rule tuning and runbooks to be complete first.", "target_sprint": 6, "affects_tasks": ["T35", "T36", "T37"]}
228
+ ]
229
+ }
230
+ }
231
+ }
data/sprint_data.json ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "scenarios": {
3
+ "easy_sprint": {
4
+ "description": "Small team, clear tasks, no surprises. Good for baseline testing.",
5
+ "difficulty": "easy",
6
+ "developers": [
7
+ {"id": "dev1", "name": "Alice", "skill": "backend", "capacity": 5, "productivity": 1.2},
8
+ {"id": "dev2", "name": "Bob", "skill": "frontend", "capacity": 4, "productivity": 1.0},
9
+ {"id": "dev3", "name": "Carol", "skill": "fullstack","capacity": 6, "productivity": 1.1}
10
+ ],
11
+ "tasks": [
12
+ {"id": "T1", "name": "User Login API", "task_type": "feature", "priority": 2, "effort": 3, "deadline": 5, "required_skill": "backend"},
13
+ {"id": "T2", "name": "Dashboard UI", "task_type": "feature", "priority": 2, "effort": 3, "deadline": 6, "required_skill": "frontend"},
14
+ {"id": "T3", "name": "Fix CSS Layout Bug", "task_type": "bug", "priority": 1, "effort": 1, "deadline": 3, "required_skill": "frontend"},
15
+ {"id": "T4", "name": "Database Migration", "task_type": "feature", "priority": 3, "effort": 4, "deadline": 7, "required_skill": "backend"},
16
+ {"id": "T5", "name": "Setup CI/CD Pipeline", "task_type": "feature", "priority": 3, "effort": 3, "deadline": 8, "required_skill": "devops"}
17
+ ]
18
+ },
19
+ "medium_sprint": {
20
+ "description": "Mid-size team with bugs and random dev absences.",
21
+ "difficulty": "medium",
22
+ "developers": [
23
+ {"id": "dev1", "name": "Alice", "skill": "backend", "capacity": 5, "productivity": 1.1},
24
+ {"id": "dev2", "name": "Bob", "skill": "frontend", "capacity": 4, "productivity": 0.9},
25
+ {"id": "dev3", "name": "Carol", "skill": "devops", "capacity": 5, "productivity": 1.0},
26
+ {"id": "dev4", "name": "Dave", "skill": "fullstack","capacity": 6, "productivity": 1.2}
27
+ ],
28
+ "tasks": [
29
+ {"id": "T1", "name": "Auth Service", "task_type": "feature", "priority": 1, "effort": 5, "deadline": 5, "required_skill": "backend"},
30
+ {"id": "T2", "name": "Profile Page UI", "task_type": "feature", "priority": 2, "effort": 4, "deadline": 6, "required_skill": "frontend"},
31
+ {"id": "T3", "name": "Payment Integration", "task_type": "feature", "priority": 1, "effort": 6, "deadline": 7, "required_skill": "backend"},
32
+ {"id": "T4", "name": "Docker Deployment", "task_type": "feature", "priority": 2, "effort": 3, "deadline": 5, "required_skill": "devops"},
33
+ {"id": "T5", "name": "Login Page", "task_type": "feature", "priority": 2, "effort": 3, "deadline": 5, "required_skill": "frontend"},
34
+ {"id": "T6", "name": "Prod DB Crash Fix", "task_type": "bug", "priority": 1, "effort": 2, "deadline": 3, "required_skill": "backend"},
35
+ {"id": "T7", "name": "API Rate Limiting", "task_type": "tech_debt", "priority": 4, "effort": 3, "deadline": 9, "required_skill": "backend"},
36
+ {"id": "T8", "name": "Mobile Layout Fix", "task_type": "bug", "priority": 2, "effort": 2, "deadline": 4, "required_skill": "frontend"}
37
+ ]
38
+ },
39
+ "hard_sprint": {
40
+ "description": "Large team, many tasks, urgent bugs appear mid-sprint, dev absences cascade.",
41
+ "difficulty": "hard",
42
+ "developers": [
43
+ {"id": "dev1", "name": "Alice", "skill": "backend", "capacity": 5, "productivity": 1.3},
44
+ {"id": "dev2", "name": "Bob", "skill": "frontend", "capacity": 4, "productivity": 0.8},
45
+ {"id": "dev3", "name": "Carol", "skill": "devops", "capacity": 5, "productivity": 1.0},
46
+ {"id": "dev4", "name": "Dave", "skill": "fullstack","capacity": 7, "productivity": 1.1},
47
+ {"id": "dev5", "name": "Eve", "skill": "backend", "capacity": 5, "productivity": 0.9}
48
+ ],
49
+ "tasks": [
50
+ {"id": "T1", "name": "Microservices Refactor", "task_type": "feature", "priority": 1, "effort": 8, "deadline": 5, "required_skill": "backend"},
51
+ {"id": "T2", "name": "Real-time Notifications", "task_type": "feature", "priority": 1, "effort": 6, "deadline": 5, "required_skill": "fullstack"},
52
+ {"id": "T3", "name": "Security Audit Fixes", "task_type": "bug", "priority": 1, "effort": 5, "deadline": 4, "required_skill": "backend"},
53
+ {"id": "T4", "name": "Kubernetes Migration", "task_type": "feature", "priority": 2, "effort": 7, "deadline": 6, "required_skill": "devops"},
54
+ {"id": "T5", "name": "Homepage Redesign", "task_type": "feature", "priority": 2, "effort": 5, "deadline": 6, "required_skill": "frontend"},
55
+ {"id": "T6", "name": "Payment Gateway Bug", "task_type": "urgent_bug", "priority": 1, "effort": 3, "deadline": 3, "required_skill": "backend"},
56
+ {"id": "T7", "name": "Search Performance", "task_type": "tech_debt", "priority": 3, "effort": 4, "deadline": 7, "required_skill": "backend"},
57
+ {"id": "T8", "name": "A/B Testing Framework", "task_type": "feature", "priority": 3, "effort": 5, "deadline": 8, "required_skill": "fullstack"},
58
+ {"id": "T9", "name": "CDN Integration", "task_type": "feature", "priority": 3, "effort": 3, "deadline": 7, "required_skill": "devops"},
59
+ {"id": "T10", "name": "OAuth2 Provider", "task_type": "feature", "priority": 2, "effort": 6, "deadline": 6, "required_skill": "backend"},
60
+ {"id": "T11", "name": "Analytics Dashboard", "task_type": "feature", "priority": 3, "effort": 5, "deadline": 8, "required_skill": "fullstack"},
61
+ {"id": "T12", "name": "Accessibility Audit", "task_type": "tech_debt", "priority": 4, "effort": 3, "deadline": 9, "required_skill": "frontend"}
62
+ ]
63
+ }
64
+ }
65
+ }
evaluate.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ evaluate.py — Compare baseline vs trained RL policy
3
+ =====================================================
4
+ Run after train.py completes.
5
+ """
6
+ from __future__ import annotations
7
+ import os
8
+ import json
9
+ import requests
10
+ from openai import OpenAI
11
+ from dotenv import load_dotenv
12
+
13
+ load_dotenv()
14
+
15
+ ENV_URL = os.getenv("ENV_BASE_URL", "https://sejal-k-ai-sprint-manager.hf.space")
16
+ BASELINE_MODEL = os.getenv("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct")
17
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
18
+ API_KEY = os.getenv("HF_TOKEN", "dummy")
19
+ POLICY_PATH = "./results/best_policy.json"
20
+ TASKS = ["easy_sprint", "medium_sprint", "hard_sprint"]
21
+ N_SEEDS = 5
22
+
23
+ SYSTEM = """You are a Tech Lead. Output ONLY JSON.
24
+ {"action_type": "assign", "task_id": "T1", "dev_id": "dev1", "new_priority": null}
25
+ Only assign backlog tasks to available skill-matched developers."""
26
+
27
+
28
+ # ── Shared helpers ────────────────────────────────────────────────────────────
29
+
30
+ def env_reset(task_name, seed=42):
31
+ r = requests.post(f"{ENV_URL}/reset",
32
+ json={"task_name": task_name, "seed": seed}, timeout=30)
33
+ r.raise_for_status()
34
+ return r.json()
35
+
36
+ def env_step(action):
37
+ r = requests.post(f"{ENV_URL}/step", json={"action": {
38
+ "action_type": action.get("action_type", "skip"),
39
+ "task_id": action.get("task_id"),
40
+ "dev_id": action.get("dev_id"),
41
+ "new_priority": action.get("new_priority"),
42
+ }}, timeout=30)
43
+ r.raise_for_status()
44
+ return r.json()
45
+
46
+ def run_episode(act_fn, task_name, seed=42):
47
+ obs = env_reset(task_name, seed)
48
+ result = {"info": {}}
49
+ for _ in range(12):
50
+ if obs.get("done"):
51
+ break
52
+ action = act_fn(obs)
53
+ result = env_step(action)
54
+ obs = result["observation"]
55
+ return max(0.01, min(0.99, result.get("info", {}).get("final_score", 0.01)))
56
+
57
+ def evaluate(act_fn, name):
58
+ print(f"\n {name}")
59
+ print(f" {'─'*45}")
60
+ scores = {}
61
+ for task in TASKS:
62
+ vals = []
63
+ for seed in range(N_SEEDS):
64
+ try:
65
+ vals.append(run_episode(act_fn, task, seed=seed*13+42))
66
+ except Exception:
67
+ vals.append(0.01)
68
+ avg = sum(vals) / len(vals)
69
+ scores[task] = round(avg, 4)
70
+ bar = "█" * int(avg * 20)
71
+ print(f" {task:<20} {avg:.4f} {bar}")
72
+ overall = sum(scores.values()) / len(scores)
73
+ scores["average"] = round(overall, 4)
74
+ print(f" {'AVERAGE':<20} {overall:.4f}")
75
+ return scores
76
+
77
+
78
+ # ── Trained policy agent ──────────────────────────────────────────────────────
79
+
80
+ class TrainedPolicy:
81
+ def __init__(self, path):
82
+ with open(path) as f:
83
+ w = json.load(f)
84
+ self.priority_weight = w["priority_weight"]
85
+ self.deadline_weight = w["deadline_weight"]
86
+ self.skill_weight = w["skill_weight"]
87
+ self.load_weight = w["load_weight"]
88
+
89
+ def act(self, obs):
90
+ day = obs.get("current_day", 1)
91
+ backlog = [t for t in obs["tasks"] if t["status"] == "backlog"]
92
+ avail = [d for d in obs["developers"]
93
+ if d["is_available"] and d["current_load"] < d["capacity"]]
94
+ if not backlog or not avail:
95
+ return {"action_type": "skip", "task_id": None,
96
+ "dev_id": None, "new_priority": None}
97
+
98
+ best, bt, bd = float("-inf"), None, None
99
+ for t in backlog:
100
+ for d in avail:
101
+ s = self.priority_weight * (6 - t["priority"])
102
+ s += self.deadline_weight * (10 / max(1, t["deadline"] - day))
103
+ if d["skill"] == t["required_skill"]:
104
+ s += self.skill_weight * 3
105
+ elif d["skill"] == "fullstack":
106
+ s += self.skill_weight * 2
107
+ else:
108
+ s -= self.skill_weight * 2
109
+ s -= self.load_weight * (d["current_load"] / max(d["capacity"], 1)) * 2
110
+ if s > best:
111
+ best, bt, bd = s, t, d
112
+ if bt and bd:
113
+ return {"action_type": "assign", "task_id": bt["id"],
114
+ "dev_id": bd["id"], "new_priority": None}
115
+ return {"action_type": "skip", "task_id": None,
116
+ "dev_id": None, "new_priority": None}
117
+
118
+
119
+ # ── Baseline LLM agent ────────────────────────────────────────────────────────
120
+
121
+ def make_baseline_act():
122
+ client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
123
+
124
+ def act(obs):
125
+ backlog = sorted([t for t in obs["tasks"] if t["status"] == "backlog"],
126
+ key=lambda t: (t["priority"], t["deadline"]))
127
+ avail = [d for d in obs["developers"]
128
+ if d["is_available"] and d["current_load"] < d["capacity"]]
129
+ prompt = (
130
+ f"Day {obs['current_day']}/{obs['sprint_length']}\n"
131
+ f"Backlog: {[t['id']+':'+t['required_skill'] for t in backlog[:4]]}\n"
132
+ f"Devs: {[d['id']+':'+d['skill'] for d in avail]}\n"
133
+ f"JSON action:"
134
+ )
135
+ try:
136
+ resp = client.chat.completions.create(
137
+ model=BASELINE_MODEL,
138
+ messages=[
139
+ {"role": "system", "content": SYSTEM},
140
+ {"role": "user", "content": prompt},
141
+ ],
142
+ temperature=0.1, max_tokens=80,
143
+ )
144
+ text = resp.choices[0].message.content or ""
145
+ a = json.loads(text.strip())
146
+ if a.get("action_type") in ("assign","reassign","skip","unblock"):
147
+ return a
148
+ except Exception:
149
+ pass
150
+ return {"action_type": "skip", "task_id": None,
151
+ "dev_id": None, "new_priority": None}
152
+ return act
153
+
154
+
155
+ # ── Main ──────────────────────────────────────────────────────────────────────
156
+
157
+ def main():
158
+ try:
159
+ r = requests.get(f"{ENV_URL}/health", timeout=10)
160
+ print(f"Server: {r.json()}")
161
+ except Exception:
162
+ print("ERROR: Start server first: python ui.py")
163
+ return
164
+
165
+ print("\n" + "="*55)
166
+ print(" EVALUATION: Baseline vs Trained RL Policy")
167
+ print("="*55)
168
+
169
+ results = {}
170
+
171
+ # Baseline
172
+ results["baseline"] = evaluate(make_baseline_act(), f"Baseline LLM ({BASELINE_MODEL})")
173
+
174
+ # Trained policy
175
+ if os.path.exists(POLICY_PATH):
176
+ policy = TrainedPolicy(POLICY_PATH)
177
+ results["trained"] = evaluate(policy.act, f"Trained RL Policy ({POLICY_PATH})")
178
+
179
+ # Comparison table
180
+ print(f"\n{'='*55}")
181
+ print(" IMPROVEMENT SUMMARY")
182
+ print(f"{'='*55}")
183
+ print(f" {'Task':<20} {'Baseline':>10} {'Trained':>10} {'Delta':>10}")
184
+ print(f" {'─'*48}")
185
+ for task in TASKS + ["average"]:
186
+ b = results["baseline"].get(task, 0)
187
+ t = results["trained"].get(task, 0)
188
+ delta = t - b
189
+ sign = "+" if delta >= 0 else ""
190
+ print(f" {task:<20} {b:>10.4f} {t:>10.4f} {sign}{delta:>9.4f}")
191
+ else:
192
+ print(f"\n No trained policy at {POLICY_PATH}")
193
+ print(" Run python train.py first.")
194
+
195
+ # Save
196
+ os.makedirs("results", exist_ok=True)
197
+ with open("results/evaluation.json", "w") as f:
198
+ json.dump(results, f, indent=2)
199
+ print(f"\n Saved → results/evaluation.json")
200
+
201
+
202
+ if __name__ == "__main__":
203
+ main()
evaluate_r2.py ADDED
@@ -0,0 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ evaluate_r2.py — Round 2 Evaluation Script
3
+ ============================================
4
+ Compares baseline (rule-based) vs trained LLM on both R1 and R2 tasks.
5
+ Produces the before/after improvement table judges want to see.
6
+ Saves results to results/r2_evaluation.json.
7
+
8
+ Usage:
9
+ # Baseline only (rule-based, no model needed):
10
+ python evaluate_r2.py --baseline-only
11
+
12
+ # Full comparison (trained model vs baseline):
13
+ python evaluate_r2.py --model results/trained_model
14
+
15
+ # Quick 1-episode-per-task run:
16
+ python evaluate_r2.py --model results/trained_model --episodes 1
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import os
24
+ import sys
25
+ import time
26
+ from pathlib import Path
27
+
28
+ import requests
29
+
30
+ ENV_BASE_URL = os.getenv("ENV_BASE_URL", "https://sejal-k-ai-sprint-manager.hf.space")
31
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
32
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
33
+ # NOTE: MODEL_NAME here is for INFERENCE comparison only.
34
+ # For TRAINING, use Qwen/Qwen2.5-1.5B-Instruct (loaded locally in train_llm.py).
35
+ MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-1.5B-Instruct")
36
+
37
+ RESULTS_DIR = Path("results")
38
+ RESULTS_DIR.mkdir(exist_ok=True)
39
+
40
+ R1_TASKS = ["easy_sprint", "medium_sprint", "hard_sprint"]
41
+ R2_TASKS = ["project_easy", "project_medium", "project_hard"]
42
+
43
+ # ── Measured baselines (FINAL — do not change) ────────────────────────────────
44
+ # R1: Llama-3.1-8B zero-shot inference (inference.py), measured 2025-01
45
+ LLAMA_BASELINE_R1 = {
46
+ "easy_sprint": 0.0100,
47
+ "medium_sprint": 0.4583,
48
+ "hard_sprint": 0.0100,
49
+ "average": 0.1594,
50
+ }
51
+
52
+ # R2: Llama-3.1-8B zero-shot inference (inference_r2.py), measured 2025-01
53
+ LLAMA_BASELINE_R2 = {
54
+ "project_easy": 0.3198,
55
+ "project_medium": 0.2443,
56
+ "project_hard": 0.2520,
57
+ "average": 0.2720,
58
+ }
59
+
60
+ # Training model: Qwen/Qwen2.5-1.5B-Instruct (GRPO, local 4-bit QLoRA)
61
+ TRAINING_MODEL = "Qwen/Qwen2.5-1.5B-Instruct"
62
+
63
+
64
+ # ── Rule-based policies ───────────────────────────────────────────────────────
65
+
66
+ def rule_based_r1(obs: dict) -> dict:
67
+ tasks = obs.get("tasks", [])
68
+ devs = obs.get("developers", [])
69
+ avail = [d for d in devs if d["is_available"] and d["current_load"] < d["capacity"]]
70
+ backlog = sorted([t for t in tasks if t["status"] == "backlog"],
71
+ key=lambda t: (t["priority"], t["deadline"]))
72
+ for task in backlog:
73
+ match = [d for d in avail if d["skill"] == task.get("required_skill") or d["skill"] == "fullstack"]
74
+ dev = match[0] if match else (avail[0] if avail else None)
75
+ if dev:
76
+ return {"action_type": "assign", "task_id": task["id"],
77
+ "dev_id": dev["id"], "new_priority": None}
78
+ return {"action_type": "skip", "task_id": None, "dev_id": None, "new_priority": None}
79
+
80
+
81
+ def rule_based_r2(obs: dict) -> dict:
82
+ tasks = obs.get("tasks", [])
83
+ devs = obs.get("developers", [])
84
+ done_ids = {t["id"] for t in tasks if t["status"] == "done"}
85
+ avail = [d for d in devs if d["is_available"] and d["current_load"] < d["capacity"] * 2]
86
+
87
+ def best_dev(task):
88
+ m = [d for d in avail if d["skill"] == task.get("required_skill") or d["skill"] == "fullstack"]
89
+ return m[0] if m else (avail[0] if avail else None)
90
+
91
+ for inst in [i for i in obs.get("instruction_queue", []) if not i.get("followed", False)]:
92
+ for tid in inst.get("affects_tasks", []):
93
+ t = next((t for t in tasks if t["id"] == tid and t["status"] == "backlog"), None)
94
+ if t and all(d in done_ids for d in t.get("metadata", {}).get("depends_on", [])):
95
+ dev = best_dev(t)
96
+ if dev:
97
+ return {"action_type": "assign", "task_id": t["id"],
98
+ "dev_id": dev["id"], "new_priority": None}
99
+
100
+ backlog = sorted([t for t in tasks if t["status"] == "backlog"],
101
+ key=lambda t: (t["priority"], t["deadline"]))
102
+ for t in backlog:
103
+ if all(d in done_ids for d in t.get("metadata", {}).get("depends_on", [])):
104
+ dev = best_dev(t)
105
+ if dev:
106
+ return {"action_type": "assign", "task_id": t["id"],
107
+ "dev_id": dev["id"], "new_priority": None}
108
+
109
+ return {"action_type": "skip", "task_id": None, "dev_id": None, "new_priority": None}
110
+
111
+
112
+ # ── Score calculators ─────────────────────────────────────────────────────────
113
+
114
+ def score_r1_obs(obs: dict) -> float:
115
+ """Extract R1 final score from terminal observation."""
116
+ done = sum(1 for t in obs.get("tasks", []) if t["status"] == "done")
117
+ total = len(obs.get("tasks", [])) or 1
118
+ missed = sum(1 for t in obs.get("tasks", []) if t["status"] == "missed")
119
+ raw = done / total - missed / total * 0.3
120
+ return round(max(0.01, min(0.99, raw)), 4)
121
+
122
+
123
+ def score_r2_obs(obs: dict) -> float:
124
+ """Compute R2 project score from terminal observation.
125
+ Formula: delivery×0.55 + instruction_following×0.30 + team_health×0.15
126
+ """
127
+ tasks_total = len(obs.get("tasks", [])) or 1
128
+ tasks_done = obs.get("tasks_completed", 0)
129
+ inst_score = obs.get("instruction_following_score", 0.01)
130
+ delivery_rate = tasks_done / tasks_total
131
+ debt_count = len(obs.get("tech_debt", []))
132
+ team_health = max(0.01, 1.0 - debt_count * 0.02)
133
+ raw = delivery_rate * 0.55 + inst_score * 0.30 + team_health * 0.15
134
+ return round(max(0.01, min(0.99, raw)), 4)
135
+
136
+
137
+ # ── Episode runners ───────────────────────────────────────────────────────────
138
+
139
+ def run_r1_episode(r1_client, task_name: str, policy_fn) -> dict:
140
+ """Run one R1 episode. Calls /step directly as dict to avoid model_dump() issue."""
141
+ import requests as _req
142
+ obs = r1_client.reset(task_name=task_name, seed=42)
143
+ rewards, actions = [], []
144
+ base_url = r1_client.base_url
145
+ for _ in range(12):
146
+ if obs.get("done", False):
147
+ break
148
+ action = policy_fn(obs)
149
+ resp = _req.post(f"{base_url}/step", json={"action": action}, timeout=30)
150
+ resp.raise_for_status()
151
+ result = resp.json()
152
+ obs = result["observation"]
153
+ rewards.append(result["reward"])
154
+ actions.append(action["action_type"])
155
+ if result["done"]:
156
+ break
157
+ return {
158
+ "task": task_name,
159
+ "score": score_r1_obs(obs),
160
+ "cumulative_reward": round(sum(rewards), 4),
161
+ "steps": len(rewards),
162
+ "tasks_completed": obs.get("tasks_completed", 0),
163
+ "tasks_missed": obs.get("tasks_missed", 0),
164
+ "action_breakdown": {a: actions.count(a) for a in set(actions)},
165
+ }
166
+
167
+
168
+ def run_r2_episode(r2_client, task_name: str, policy_fn) -> dict:
169
+ obs = r2_client.reset(task_name=task_name, seed=42)
170
+ rewards, actions, sprint_rewards = [], [], []
171
+ for _ in range(60):
172
+ if obs.get("done", False):
173
+ break
174
+ action = policy_fn(obs)
175
+ result = r2_client.step(action)
176
+ obs = result.observation if hasattr(result, "observation") else result["observation"]
177
+ rew = result.reward if hasattr(result, "reward") else result["reward"]
178
+ done = result.done if hasattr(result, "done") else result["done"]
179
+ rewards.append(rew)
180
+ actions.append(action["action_type"])
181
+ sprint_rewards = obs.get("sprint_rewards", [])
182
+ if done:
183
+ break
184
+ return {
185
+ "task": task_name,
186
+ "score": score_r2_obs(obs),
187
+ "cumulative_reward": round(sum(rewards), 4),
188
+ "steps": len(rewards),
189
+ "tasks_completed": obs.get("tasks_completed", 0),
190
+ "tasks_missed": obs.get("tasks_missed", 0),
191
+ "instruction_following_score": obs.get("instruction_following_score", 0.0),
192
+ "tech_debt_count": len(obs.get("tech_debt", [])),
193
+ "sprint_rewards": sprint_rewards,
194
+ "action_breakdown": {a: actions.count(a) for a in set(actions)},
195
+ }
196
+
197
+
198
+ # ── LLM policy builders ───────────────────────────────────────────────────────
199
+
200
+ def _build_api_policy(model_id: str, system_prompt: str):
201
+ """Build an LLM policy that calls the HF router API."""
202
+ from openai import OpenAI
203
+ client = OpenAI(api_key=HF_TOKEN, base_url=API_BASE_URL)
204
+
205
+ def policy(obs: dict) -> dict:
206
+ import json as _json
207
+ user_msg = f"Current state:\n{_json.dumps(obs, indent=2)}\nOutput JSON action only."
208
+ try:
209
+ resp = client.chat.completions.create(
210
+ model=model_id,
211
+ messages=[{"role": "system", "content": system_prompt},
212
+ {"role": "user", "content": user_msg}],
213
+ max_tokens=60,
214
+ temperature=0.1,
215
+ )
216
+ raw = resp.choices[0].message.content.strip()
217
+ start = raw.find("{")
218
+ end = raw.rfind("}") + 1
219
+ if start >= 0 and end > start:
220
+ return _json.loads(raw[start:end])
221
+ except Exception:
222
+ pass
223
+ return {"action_type": "skip", "task_id": None, "dev_id": None, "new_priority": None}
224
+
225
+ return policy
226
+
227
+
228
+ def build_llm_policy(model_path: str, system_prompt: str):
229
+ """Build R2 LLM policy — tries local model first, falls back to API."""
230
+ # Try local model (after training)
231
+ local_path = Path(model_path)
232
+ if local_path.exists():
233
+ try:
234
+ from transformers import AutoModelForCausalLM, AutoTokenizer
235
+ import torch, json as _json
236
+ tokenizer = AutoTokenizer.from_pretrained(str(local_path))
237
+ model = AutoModelForCausalLM.from_pretrained(
238
+ str(local_path), torch_dtype=torch.float16, device_map="auto"
239
+ )
240
+ print(f"[INFO] Loaded local model from {local_path}", flush=True)
241
+
242
+ def local_policy(obs: dict) -> dict:
243
+ prompt = f"{system_prompt}\n\nState:\n{_json.dumps(obs)}\nAction:"
244
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
245
+ with torch.no_grad():
246
+ outputs = model.generate(**inputs, max_new_tokens=60, temperature=0.1, do_sample=True)
247
+ raw = tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
248
+ try:
249
+ start = raw.find("{"); end = raw.rfind("}") + 1
250
+ if start >= 0 and end > start:
251
+ return _json.loads(raw[start:end])
252
+ except Exception:
253
+ pass
254
+ return {"action_type": "skip", "task_id": None, "dev_id": None, "new_priority": None}
255
+
256
+ return local_policy
257
+ except Exception as e:
258
+ print(f"[WARN] Could not load local model: {e}", flush=True)
259
+
260
+ # Fall back to HF API (model_path is an HF model ID like "sejal-k/ai-sprint-manager-trained")
261
+ print(f"[INFO] Using HF API for model {model_path}", flush=True)
262
+ return _build_api_policy(model_path, system_prompt)
263
+
264
+
265
+ # ── Main evaluation ───────────────────────────────────────────────────────────
266
+
267
+ def evaluate(model_path: str | None = None, n_episodes: int = 3, baseline_only: bool = False):
268
+ from client import SprintEnvClient
269
+ from project_client import ProjectEnvClient
270
+
271
+ print(f"\n{'='*60}", flush=True)
272
+ print(f" AI Sprint Manager — Evaluation", flush=True)
273
+ print(f" Env: {ENV_BASE_URL}", flush=True)
274
+ print(f" Model: {model_path or 'rule-based only'}", flush=True)
275
+ print(f" Training model: {TRAINING_MODEL}", flush=True)
276
+ print(f"{'='*60}", flush=True)
277
+
278
+ # Health check
279
+ try:
280
+ r = requests.get(f"{ENV_BASE_URL}/health", timeout=10)
281
+ r.raise_for_status()
282
+ r2 = requests.get(f"{ENV_BASE_URL}/project/health", timeout=10)
283
+ r2.raise_for_status()
284
+ print(f"[OK] Environment is live", flush=True)
285
+ except Exception as e:
286
+ print(f"[ERROR] Server unreachable: {e}", flush=True)
287
+ sys.exit(1)
288
+
289
+ results = {
290
+ "metadata": {
291
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
292
+ "model": model_path or "rule-based",
293
+ "training_model": TRAINING_MODEL,
294
+ "env_url": ENV_BASE_URL,
295
+ "n_episodes": n_episodes,
296
+ "baseline_only": baseline_only,
297
+ },
298
+ # Measured Llama-3.1-8B zero-shot baselines (FINAL)
299
+ "r1_llama_baseline": LLAMA_BASELINE_R1,
300
+ "r2_llama_baseline": LLAMA_BASELINE_R2,
301
+ # Live run results
302
+ "r1_rule_based": {},
303
+ "r1_llm": {},
304
+ "r2_rule_based": {},
305
+ "r2_llm": {},
306
+ "improvement": {},
307
+ }
308
+
309
+ r1_client = SprintEnvClient(base_url=ENV_BASE_URL)
310
+ r2_client = ProjectEnvClient(base_url=ENV_BASE_URL)
311
+
312
+ # ── R1 rule-based baseline ────────────────────────────────────────────────
313
+ print(f"\n{'─'*55}", flush=True)
314
+ print(f" R1 — Rule-based baseline", flush=True)
315
+ print(f"{'─'*55}", flush=True)
316
+ for task in R1_TASKS:
317
+ ep_results = []
318
+ for ep in range(n_episodes):
319
+ r = run_r1_episode(r1_client, task, rule_based_r1)
320
+ ep_results.append(r)
321
+ print(f" {task} ep{ep+1}: score={r['score']:.4f} "
322
+ f"done={r['tasks_completed']} reward={r['cumulative_reward']:.2f}", flush=True)
323
+ avg_score = sum(r["score"] for r in ep_results) / n_episodes
324
+ results["r1_rule_based"][task] = {
325
+ "avg_score": round(avg_score, 4),
326
+ "episodes": ep_results,
327
+ }
328
+
329
+ if not baseline_only and model_path:
330
+ from inference_r2 import R2_SYSTEM_PROMPT
331
+
332
+ R1_SYSTEM_PROMPT = (
333
+ "You are an expert Tech Lead managing an agile sprint. "
334
+ "Output a JSON action: {\"action_type\":\"<assign|reassign|reprioritize|unblock|skip>\","
335
+ "\"task_id\":\"<id or null>\",\"dev_id\":\"<id or null>\",\"new_priority\":<1-5 or null>}. "
336
+ "Only output JSON. Assign backlog tasks to available developers, skill match preferred."
337
+ )
338
+
339
+ llm_r1_policy = _build_api_policy(model_path, R1_SYSTEM_PROMPT)
340
+ llm_r2_policy = build_llm_policy(model_path, R2_SYSTEM_PROMPT)
341
+
342
+ # ── R1 LLM ───────────────────────────────────────────────────────────
343
+ print(f"\n{'─'*55}", flush=True)
344
+ print(f" R1 — LLM ({model_path})", flush=True)
345
+ print(f"{'─'*55}", flush=True)
346
+ for task in R1_TASKS:
347
+ ep_results = []
348
+ for ep in range(n_episodes):
349
+ r = run_r1_episode(r1_client, task, llm_r1_policy)
350
+ ep_results.append(r)
351
+ print(f" {task} ep{ep+1}: score={r['score']:.4f}", flush=True)
352
+ avg_score = sum(r["score"] for r in ep_results) / n_episodes
353
+ results["r1_llm"][task] = {
354
+ "avg_score": round(avg_score, 4),
355
+ "episodes": ep_results,
356
+ }
357
+
358
+ # ── R2 LLM ───────────────────────────────────────────────────────────
359
+ print(f"\n{'─'*55}", flush=True)
360
+ print(f" R2 — LLM ({model_path})", flush=True)
361
+ print(f"{'─'*55}", flush=True)
362
+ for task in R2_TASKS:
363
+ ep_results = []
364
+ for ep in range(n_episodes):
365
+ r = run_r2_episode(r2_client, task, llm_r2_policy)
366
+ ep_results.append(r)
367
+ print(f" {task} ep{ep+1}: score={r['score']:.4f} "
368
+ f"inst={r['instruction_following_score']:.2f}", flush=True)
369
+ avg_score = sum(r["score"] for r in ep_results) / n_episodes
370
+ results["r2_llm"][task] = {
371
+ "avg_score": round(avg_score, 4),
372
+ "episodes": ep_results,
373
+ }
374
+
375
+ # ── Improvement table ─────────────────────────────────────────────────
376
+ for task in R2_TASKS:
377
+ base_llama = LLAMA_BASELINE_R2.get(task, 0)
378
+ llm = results["r2_llm"].get(task, {}).get("avg_score", base_llama)
379
+ delta_vs_llama = round(llm - base_llama, 4)
380
+ results["improvement"][task] = {
381
+ "llama_baseline": base_llama,
382
+ "trained_llm": llm,
383
+ "delta_vs_llama": delta_vs_llama,
384
+ "pct_gain_vs_llama": round(delta_vs_llama / max(base_llama, 0.01) * 100, 1),
385
+ }
386
+
387
+ r1_client.close()
388
+ r2_client.close()
389
+
390
+ _print_summary(results, baseline_only)
391
+
392
+ out_path = RESULTS_DIR / "r2_evaluation.json"
393
+ with open(out_path, "w") as f:
394
+ json.dump(results, f, indent=2)
395
+ print(f"\n[INFO] Results saved to {out_path}", flush=True)
396
+ return results
397
+
398
+
399
+ def _print_summary(results: dict, baseline_only: bool):
400
+ print(f"\n{'='*65}", flush=True)
401
+ print(f" EVALUATION SUMMARY", flush=True)
402
+ print(f"{'='*65}", flush=True)
403
+
404
+ print(f"\n{'R1 SCORES (Llama-3.1-8B zero-shot — measured baseline)':─<65}", flush=True)
405
+ print(f" {'Task':<22} {'Llama Baseline':>15} {'LLM Trained':>12}", flush=True)
406
+ for task in ["easy_sprint", "medium_sprint", "hard_sprint"]:
407
+ llama = results["r1_llama_baseline"].get(task, 0)
408
+ llm = results["r1_llm"].get(task, {}).get("avg_score", 0)
409
+ llm_s = f"{llm:.4f}" if llm else "—"
410
+ print(f" {task:<22} {llama:>15.4f} {llm_s:>12}", flush=True)
411
+ avg = results["r1_llama_baseline"].get("average", 0)
412
+ print(f" {'AVERAGE':<22} {avg:>15.4f}", flush=True)
413
+
414
+ print(f"\n{'R2 SCORES':─<65}", flush=True)
415
+ print(f" {'Task':<22} {'Llama Baseline':>15} {'LLM Trained':>12} {'Δ vs Llama':>10}", flush=True)
416
+ for task in ["project_easy", "project_medium", "project_hard"]:
417
+ llama = results["r2_llama_baseline"].get(task, 0)
418
+ llm = results["r2_llm"].get(task, {}).get("avg_score", 0)
419
+ imp = results["improvement"].get(task, {})
420
+ delta = imp.get("delta_vs_llama", 0)
421
+ llm_s = f"{llm:.4f}" if llm else "—"
422
+ delta_s = f"+{delta:.4f}" if delta > 0 else (f"{delta:.4f}" if delta else "—")
423
+ print(f" {task:<22} {llama:>15.4f} {llm_s:>12} {delta_s:>10}", flush=True)
424
+ avg_r2 = results["r2_llama_baseline"].get("average", 0)
425
+ print(f" {'AVERAGE':<22} {avg_r2:>15.4f}", flush=True)
426
+
427
+ print(f"\n{'='*65}", flush=True)
428
+ print(f" Training model: Qwen/Qwen2.5-1.5B-Instruct (GRPO, 4-bit QLoRA)", flush=True)
429
+ print(f" Baselines: Llama-3.1-8B zero-shot (via HF Router)", flush=True)
430
+ print(f"{'='*65}", flush=True)
431
+
432
+
433
+ # ── CLI ───────────────────────────────────────────────────────────────────────
434
+
435
+ def main():
436
+ parser = argparse.ArgumentParser(description="Evaluate R1+R2 before/after training")
437
+ parser.add_argument("--model", type=str, default=None,
438
+ help="Path to trained model dir or HF model ID")
439
+ parser.add_argument("--baseline-only", action="store_true",
440
+ help="Run rule-based baseline only (no model needed)")
441
+ parser.add_argument("--episodes", type=int, default=3,
442
+ help="Episodes per task (default: 3)")
443
+ args = parser.parse_args()
444
+
445
+ if not args.baseline_only and not args.model:
446
+ print("[INFO] No --model specified. Running baseline-only evaluation.", flush=True)
447
+ args.baseline_only = True
448
+
449
+ evaluate(
450
+ model_path=args.model,
451
+ n_episodes=args.episodes,
452
+ baseline_only=args.baseline_only,
453
+ )
454
+
455
+
456
+ if __name__ == "__main__":
457
+ main()
huggingface_tokenizers_cache/CACHEDIR.TAG ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag created by huggingface_hub.
3
+ # For information about cache directory tags, see:
4
+ # https://bford.info/cachedir/
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/.no_exist/3d254dbee5e3beae81bb8a717ad3a03427a09d26/chat_template.jinja ADDED
File without changes
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/31349551d90c7606f325fe0f11bbb8bd5fa0d7c7 ADDED
The diff for this file is too large to render. See raw diff
 
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/4783fe10ac3adce15ac8f358ef5462739852c569 ADDED
The diff for this file is too large to render. See raw diff
 
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/482ced4679301bf287ebb310bdd1790eb4514232 ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "</tool_call>": 151658,
3
+ "<tool_call>": 151657,
4
+ "<|box_end|>": 151649,
5
+ "<|box_start|>": 151648,
6
+ "<|endoftext|>": 151643,
7
+ "<|file_sep|>": 151664,
8
+ "<|fim_middle|>": 151660,
9
+ "<|fim_pad|>": 151662,
10
+ "<|fim_prefix|>": 151659,
11
+ "<|fim_suffix|>": 151661,
12
+ "<|im_end|>": 151645,
13
+ "<|im_start|>": 151644,
14
+ "<|image_pad|>": 151655,
15
+ "<|object_ref_end|>": 151647,
16
+ "<|object_ref_start|>": 151646,
17
+ "<|quad_end|>": 151651,
18
+ "<|quad_start|>": 151650,
19
+ "<|repo_name|>": 151663,
20
+ "<|video_pad|>": 151656,
21
+ "<|vision_end|>": 151653,
22
+ "<|vision_pad|>": 151654,
23
+ "<|vision_start|>": 151652
24
+ }
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/76ac30de06e0158be805f42a4e63934bedb5a169 ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_name_or_path": "Qwen/Qwen2.5-1.5B-Instruct",
3
+ "architectures": [
4
+ "Qwen2ForCausalLM"
5
+ ],
6
+ "attention_dropout": 0.0,
7
+ "eos_token_id": 151645,
8
+ "hidden_act": "silu",
9
+ "hidden_size": 1536,
10
+ "initializer_range": 0.02,
11
+ "intermediate_size": 8960,
12
+ "max_position_embeddings": 32768,
13
+ "max_window_layers": 21,
14
+ "model_type": "qwen2",
15
+ "num_attention_heads": 12,
16
+ "num_hidden_layers": 28,
17
+ "num_key_value_heads": 2,
18
+ "pad_token_id": 151654,
19
+ "quantization_config": {
20
+ "_load_in_4bit": true,
21
+ "_load_in_8bit": false,
22
+ "bnb_4bit_compute_dtype": "bfloat16",
23
+ "bnb_4bit_quant_storage": "uint8",
24
+ "bnb_4bit_quant_type": "nf4",
25
+ "bnb_4bit_use_double_quant": true,
26
+ "llm_int8_enable_fp32_cpu_offload": false,
27
+ "llm_int8_has_fp16_weight": false,
28
+ "llm_int8_skip_modules": [
29
+ "lm_head",
30
+ "multi_modal_projector",
31
+ "merger",
32
+ "modality_projection",
33
+ "model.layers.0.self_attn",
34
+ "model.layers.1.mlp",
35
+ "model.layers.2.mlp",
36
+ "model.layers.3.mlp",
37
+ "model.layers.7.mlp",
38
+ "model.layers.24.mlp",
39
+ "model.layers.26.mlp",
40
+ "model.layers.15.self_attn"
41
+ ],
42
+ "llm_int8_threshold": 6.0,
43
+ "load_in_4bit": true,
44
+ "load_in_8bit": false,
45
+ "quant_method": "bitsandbytes"
46
+ },
47
+ "rms_norm_eps": 1e-06,
48
+ "rope_scaling": null,
49
+ "rope_theta": 1000000.0,
50
+ "sliding_window": null,
51
+ "tie_word_embeddings": true,
52
+ "torch_dtype": "bfloat16",
53
+ "transformers_version": "4.49.0.dev0",
54
+ "unsloth_fixed": true,
55
+ "use_cache": true,
56
+ "use_sliding_window": false,
57
+ "vocab_size": 151936
58
+ }
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/9b8043f10c758210957b050c77f14d6282f33a52 ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ "<|im_start|>",
4
+ "<|im_end|>",
5
+ "<|object_ref_start|>",
6
+ "<|object_ref_end|>",
7
+ "<|box_start|>",
8
+ "<|box_end|>",
9
+ "<|quad_start|>",
10
+ "<|quad_end|>",
11
+ "<|vision_start|>",
12
+ "<|vision_end|>",
13
+ "<|vision_pad|>",
14
+ "<|image_pad|>",
15
+ "<|video_pad|>"
16
+ ],
17
+ "eos_token": {
18
+ "content": "<|im_end|>",
19
+ "lstrip": false,
20
+ "normalized": false,
21
+ "rstrip": false,
22
+ "single_word": false
23
+ },
24
+ "pad_token": {
25
+ "content": "<|vision_pad|>",
26
+ "lstrip": false,
27
+ "normalized": false,
28
+ "rstrip": false,
29
+ "single_word": false
30
+ }
31
+ }
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/9c5ae00e602b8860cbd784ba82a8aa14e8feecec692e7076590d014d7b7fdafa ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c5ae00e602b8860cbd784ba82a8aa14e8feecec692e7076590d014d7b7fdafa
3
+ size 11421896
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/blobs/a5de2eb6cac65b89846ac1bcfb837a3d83459f40 ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_bos_token": false,
3
+ "add_prefix_space": false,
4
+ "added_tokens_decoder": {
5
+ "151643": {
6
+ "content": "<|endoftext|>",
7
+ "lstrip": false,
8
+ "normalized": false,
9
+ "rstrip": false,
10
+ "single_word": false,
11
+ "special": true
12
+ },
13
+ "151644": {
14
+ "content": "<|im_start|>",
15
+ "lstrip": false,
16
+ "normalized": false,
17
+ "rstrip": false,
18
+ "single_word": false,
19
+ "special": true
20
+ },
21
+ "151645": {
22
+ "content": "<|im_end|>",
23
+ "lstrip": false,
24
+ "normalized": false,
25
+ "rstrip": false,
26
+ "single_word": false,
27
+ "special": true
28
+ },
29
+ "151646": {
30
+ "content": "<|object_ref_start|>",
31
+ "lstrip": false,
32
+ "normalized": false,
33
+ "rstrip": false,
34
+ "single_word": false,
35
+ "special": true
36
+ },
37
+ "151647": {
38
+ "content": "<|object_ref_end|>",
39
+ "lstrip": false,
40
+ "normalized": false,
41
+ "rstrip": false,
42
+ "single_word": false,
43
+ "special": true
44
+ },
45
+ "151648": {
46
+ "content": "<|box_start|>",
47
+ "lstrip": false,
48
+ "normalized": false,
49
+ "rstrip": false,
50
+ "single_word": false,
51
+ "special": true
52
+ },
53
+ "151649": {
54
+ "content": "<|box_end|>",
55
+ "lstrip": false,
56
+ "normalized": false,
57
+ "rstrip": false,
58
+ "single_word": false,
59
+ "special": true
60
+ },
61
+ "151650": {
62
+ "content": "<|quad_start|>",
63
+ "lstrip": false,
64
+ "normalized": false,
65
+ "rstrip": false,
66
+ "single_word": false,
67
+ "special": true
68
+ },
69
+ "151651": {
70
+ "content": "<|quad_end|>",
71
+ "lstrip": false,
72
+ "normalized": false,
73
+ "rstrip": false,
74
+ "single_word": false,
75
+ "special": true
76
+ },
77
+ "151652": {
78
+ "content": "<|vision_start|>",
79
+ "lstrip": false,
80
+ "normalized": false,
81
+ "rstrip": false,
82
+ "single_word": false,
83
+ "special": true
84
+ },
85
+ "151653": {
86
+ "content": "<|vision_end|>",
87
+ "lstrip": false,
88
+ "normalized": false,
89
+ "rstrip": false,
90
+ "single_word": false,
91
+ "special": true
92
+ },
93
+ "151654": {
94
+ "content": "<|vision_pad|>",
95
+ "lstrip": false,
96
+ "normalized": false,
97
+ "rstrip": false,
98
+ "single_word": false,
99
+ "special": true
100
+ },
101
+ "151655": {
102
+ "content": "<|image_pad|>",
103
+ "lstrip": false,
104
+ "normalized": false,
105
+ "rstrip": false,
106
+ "single_word": false,
107
+ "special": true
108
+ },
109
+ "151656": {
110
+ "content": "<|video_pad|>",
111
+ "lstrip": false,
112
+ "normalized": false,
113
+ "rstrip": false,
114
+ "single_word": false,
115
+ "special": true
116
+ },
117
+ "151657": {
118
+ "content": "<tool_call>",
119
+ "lstrip": false,
120
+ "normalized": false,
121
+ "rstrip": false,
122
+ "single_word": false,
123
+ "special": false
124
+ },
125
+ "151658": {
126
+ "content": "</tool_call>",
127
+ "lstrip": false,
128
+ "normalized": false,
129
+ "rstrip": false,
130
+ "single_word": false,
131
+ "special": false
132
+ },
133
+ "151659": {
134
+ "content": "<|fim_prefix|>",
135
+ "lstrip": false,
136
+ "normalized": false,
137
+ "rstrip": false,
138
+ "single_word": false,
139
+ "special": false
140
+ },
141
+ "151660": {
142
+ "content": "<|fim_middle|>",
143
+ "lstrip": false,
144
+ "normalized": false,
145
+ "rstrip": false,
146
+ "single_word": false,
147
+ "special": false
148
+ },
149
+ "151661": {
150
+ "content": "<|fim_suffix|>",
151
+ "lstrip": false,
152
+ "normalized": false,
153
+ "rstrip": false,
154
+ "single_word": false,
155
+ "special": false
156
+ },
157
+ "151662": {
158
+ "content": "<|fim_pad|>",
159
+ "lstrip": false,
160
+ "normalized": false,
161
+ "rstrip": false,
162
+ "single_word": false,
163
+ "special": false
164
+ },
165
+ "151663": {
166
+ "content": "<|repo_name|>",
167
+ "lstrip": false,
168
+ "normalized": false,
169
+ "rstrip": false,
170
+ "single_word": false,
171
+ "special": false
172
+ },
173
+ "151664": {
174
+ "content": "<|file_sep|>",
175
+ "lstrip": false,
176
+ "normalized": false,
177
+ "rstrip": false,
178
+ "single_word": false,
179
+ "special": false
180
+ }
181
+ },
182
+ "additional_special_tokens": [
183
+ "<|im_start|>",
184
+ "<|im_end|>",
185
+ "<|object_ref_start|>",
186
+ "<|object_ref_end|>",
187
+ "<|box_start|>",
188
+ "<|box_end|>",
189
+ "<|quad_start|>",
190
+ "<|quad_end|>",
191
+ "<|vision_start|>",
192
+ "<|vision_end|>",
193
+ "<|vision_pad|>",
194
+ "<|image_pad|>",
195
+ "<|video_pad|>"
196
+ ],
197
+ "bos_token": null,
198
+ "chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- message.content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n",
199
+ "clean_up_tokenization_spaces": false,
200
+ "eos_token": "<|im_end|>",
201
+ "errors": "replace",
202
+ "extra_special_tokens": {},
203
+ "model_max_length": 32768,
204
+ "pad_token": "<|vision_pad|>",
205
+ "padding_side": "left",
206
+ "split_special_tokens": false,
207
+ "tokenizer_class": "Qwen2Tokenizer",
208
+ "unk_token": null
209
+ }
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/refs/main ADDED
@@ -0,0 +1 @@
 
 
1
+ 3d254dbee5e3beae81bb8a717ad3a03427a09d26
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/added_tokens.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "</tool_call>": 151658,
3
+ "<tool_call>": 151657,
4
+ "<|box_end|>": 151649,
5
+ "<|box_start|>": 151648,
6
+ "<|endoftext|>": 151643,
7
+ "<|file_sep|>": 151664,
8
+ "<|fim_middle|>": 151660,
9
+ "<|fim_pad|>": 151662,
10
+ "<|fim_prefix|>": 151659,
11
+ "<|fim_suffix|>": 151661,
12
+ "<|im_end|>": 151645,
13
+ "<|im_start|>": 151644,
14
+ "<|image_pad|>": 151655,
15
+ "<|object_ref_end|>": 151647,
16
+ "<|object_ref_start|>": 151646,
17
+ "<|quad_end|>": 151651,
18
+ "<|quad_start|>": 151650,
19
+ "<|repo_name|>": 151663,
20
+ "<|video_pad|>": 151656,
21
+ "<|vision_end|>": 151653,
22
+ "<|vision_pad|>": 151654,
23
+ "<|vision_start|>": 151652
24
+ }
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/config.json ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_name_or_path": "Qwen/Qwen2.5-1.5B-Instruct",
3
+ "architectures": [
4
+ "Qwen2ForCausalLM"
5
+ ],
6
+ "attention_dropout": 0.0,
7
+ "eos_token_id": 151645,
8
+ "hidden_act": "silu",
9
+ "hidden_size": 1536,
10
+ "initializer_range": 0.02,
11
+ "intermediate_size": 8960,
12
+ "max_position_embeddings": 32768,
13
+ "max_window_layers": 21,
14
+ "model_type": "qwen2",
15
+ "num_attention_heads": 12,
16
+ "num_hidden_layers": 28,
17
+ "num_key_value_heads": 2,
18
+ "pad_token_id": 151654,
19
+ "quantization_config": {
20
+ "_load_in_4bit": true,
21
+ "_load_in_8bit": false,
22
+ "bnb_4bit_compute_dtype": "bfloat16",
23
+ "bnb_4bit_quant_storage": "uint8",
24
+ "bnb_4bit_quant_type": "nf4",
25
+ "bnb_4bit_use_double_quant": true,
26
+ "llm_int8_enable_fp32_cpu_offload": false,
27
+ "llm_int8_has_fp16_weight": false,
28
+ "llm_int8_skip_modules": [
29
+ "lm_head",
30
+ "multi_modal_projector",
31
+ "merger",
32
+ "modality_projection",
33
+ "model.layers.0.self_attn",
34
+ "model.layers.1.mlp",
35
+ "model.layers.2.mlp",
36
+ "model.layers.3.mlp",
37
+ "model.layers.7.mlp",
38
+ "model.layers.24.mlp",
39
+ "model.layers.26.mlp",
40
+ "model.layers.15.self_attn"
41
+ ],
42
+ "llm_int8_threshold": 6.0,
43
+ "load_in_4bit": true,
44
+ "load_in_8bit": false,
45
+ "quant_method": "bitsandbytes"
46
+ },
47
+ "rms_norm_eps": 1e-06,
48
+ "rope_scaling": null,
49
+ "rope_theta": 1000000.0,
50
+ "sliding_window": null,
51
+ "tie_word_embeddings": true,
52
+ "torch_dtype": "bfloat16",
53
+ "transformers_version": "4.49.0.dev0",
54
+ "unsloth_fixed": true,
55
+ "use_cache": true,
56
+ "use_sliding_window": false,
57
+ "vocab_size": 151936
58
+ }
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/special_tokens_map.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ "<|im_start|>",
4
+ "<|im_end|>",
5
+ "<|object_ref_start|>",
6
+ "<|object_ref_end|>",
7
+ "<|box_start|>",
8
+ "<|box_end|>",
9
+ "<|quad_start|>",
10
+ "<|quad_end|>",
11
+ "<|vision_start|>",
12
+ "<|vision_end|>",
13
+ "<|vision_pad|>",
14
+ "<|image_pad|>",
15
+ "<|video_pad|>"
16
+ ],
17
+ "eos_token": {
18
+ "content": "<|im_end|>",
19
+ "lstrip": false,
20
+ "normalized": false,
21
+ "rstrip": false,
22
+ "single_word": false
23
+ },
24
+ "pad_token": {
25
+ "content": "<|vision_pad|>",
26
+ "lstrip": false,
27
+ "normalized": false,
28
+ "rstrip": false,
29
+ "single_word": false
30
+ }
31
+ }
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c5ae00e602b8860cbd784ba82a8aa14e8feecec692e7076590d014d7b7fdafa
3
+ size 11421896
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/tokenizer_config.json ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_bos_token": false,
3
+ "add_prefix_space": false,
4
+ "added_tokens_decoder": {
5
+ "151643": {
6
+ "content": "<|endoftext|>",
7
+ "lstrip": false,
8
+ "normalized": false,
9
+ "rstrip": false,
10
+ "single_word": false,
11
+ "special": true
12
+ },
13
+ "151644": {
14
+ "content": "<|im_start|>",
15
+ "lstrip": false,
16
+ "normalized": false,
17
+ "rstrip": false,
18
+ "single_word": false,
19
+ "special": true
20
+ },
21
+ "151645": {
22
+ "content": "<|im_end|>",
23
+ "lstrip": false,
24
+ "normalized": false,
25
+ "rstrip": false,
26
+ "single_word": false,
27
+ "special": true
28
+ },
29
+ "151646": {
30
+ "content": "<|object_ref_start|>",
31
+ "lstrip": false,
32
+ "normalized": false,
33
+ "rstrip": false,
34
+ "single_word": false,
35
+ "special": true
36
+ },
37
+ "151647": {
38
+ "content": "<|object_ref_end|>",
39
+ "lstrip": false,
40
+ "normalized": false,
41
+ "rstrip": false,
42
+ "single_word": false,
43
+ "special": true
44
+ },
45
+ "151648": {
46
+ "content": "<|box_start|>",
47
+ "lstrip": false,
48
+ "normalized": false,
49
+ "rstrip": false,
50
+ "single_word": false,
51
+ "special": true
52
+ },
53
+ "151649": {
54
+ "content": "<|box_end|>",
55
+ "lstrip": false,
56
+ "normalized": false,
57
+ "rstrip": false,
58
+ "single_word": false,
59
+ "special": true
60
+ },
61
+ "151650": {
62
+ "content": "<|quad_start|>",
63
+ "lstrip": false,
64
+ "normalized": false,
65
+ "rstrip": false,
66
+ "single_word": false,
67
+ "special": true
68
+ },
69
+ "151651": {
70
+ "content": "<|quad_end|>",
71
+ "lstrip": false,
72
+ "normalized": false,
73
+ "rstrip": false,
74
+ "single_word": false,
75
+ "special": true
76
+ },
77
+ "151652": {
78
+ "content": "<|vision_start|>",
79
+ "lstrip": false,
80
+ "normalized": false,
81
+ "rstrip": false,
82
+ "single_word": false,
83
+ "special": true
84
+ },
85
+ "151653": {
86
+ "content": "<|vision_end|>",
87
+ "lstrip": false,
88
+ "normalized": false,
89
+ "rstrip": false,
90
+ "single_word": false,
91
+ "special": true
92
+ },
93
+ "151654": {
94
+ "content": "<|vision_pad|>",
95
+ "lstrip": false,
96
+ "normalized": false,
97
+ "rstrip": false,
98
+ "single_word": false,
99
+ "special": true
100
+ },
101
+ "151655": {
102
+ "content": "<|image_pad|>",
103
+ "lstrip": false,
104
+ "normalized": false,
105
+ "rstrip": false,
106
+ "single_word": false,
107
+ "special": true
108
+ },
109
+ "151656": {
110
+ "content": "<|video_pad|>",
111
+ "lstrip": false,
112
+ "normalized": false,
113
+ "rstrip": false,
114
+ "single_word": false,
115
+ "special": true
116
+ },
117
+ "151657": {
118
+ "content": "<tool_call>",
119
+ "lstrip": false,
120
+ "normalized": false,
121
+ "rstrip": false,
122
+ "single_word": false,
123
+ "special": false
124
+ },
125
+ "151658": {
126
+ "content": "</tool_call>",
127
+ "lstrip": false,
128
+ "normalized": false,
129
+ "rstrip": false,
130
+ "single_word": false,
131
+ "special": false
132
+ },
133
+ "151659": {
134
+ "content": "<|fim_prefix|>",
135
+ "lstrip": false,
136
+ "normalized": false,
137
+ "rstrip": false,
138
+ "single_word": false,
139
+ "special": false
140
+ },
141
+ "151660": {
142
+ "content": "<|fim_middle|>",
143
+ "lstrip": false,
144
+ "normalized": false,
145
+ "rstrip": false,
146
+ "single_word": false,
147
+ "special": false
148
+ },
149
+ "151661": {
150
+ "content": "<|fim_suffix|>",
151
+ "lstrip": false,
152
+ "normalized": false,
153
+ "rstrip": false,
154
+ "single_word": false,
155
+ "special": false
156
+ },
157
+ "151662": {
158
+ "content": "<|fim_pad|>",
159
+ "lstrip": false,
160
+ "normalized": false,
161
+ "rstrip": false,
162
+ "single_word": false,
163
+ "special": false
164
+ },
165
+ "151663": {
166
+ "content": "<|repo_name|>",
167
+ "lstrip": false,
168
+ "normalized": false,
169
+ "rstrip": false,
170
+ "single_word": false,
171
+ "special": false
172
+ },
173
+ "151664": {
174
+ "content": "<|file_sep|>",
175
+ "lstrip": false,
176
+ "normalized": false,
177
+ "rstrip": false,
178
+ "single_word": false,
179
+ "special": false
180
+ }
181
+ },
182
+ "additional_special_tokens": [
183
+ "<|im_start|>",
184
+ "<|im_end|>",
185
+ "<|object_ref_start|>",
186
+ "<|object_ref_end|>",
187
+ "<|box_start|>",
188
+ "<|box_end|>",
189
+ "<|quad_start|>",
190
+ "<|quad_end|>",
191
+ "<|vision_start|>",
192
+ "<|vision_end|>",
193
+ "<|vision_pad|>",
194
+ "<|image_pad|>",
195
+ "<|video_pad|>"
196
+ ],
197
+ "bos_token": null,
198
+ "chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n {%- else %}\n {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}\n {%- endif %}\n {{- \"\\n\\n# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0]['role'] == 'system' %}\n {{- '<|im_start|>system\\n' + messages[0]['content'] + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- for message in messages %}\n {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) or (message.role == \"assistant\" and not message.tool_calls) %}\n {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role }}\n {%- if message.content %}\n {{- '\\n' + message.content }}\n {%- endif %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '\\n<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {{- tool_call.arguments | tojson }}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- message.content }}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n",
199
+ "clean_up_tokenization_spaces": false,
200
+ "eos_token": "<|im_end|>",
201
+ "errors": "replace",
202
+ "extra_special_tokens": {},
203
+ "model_max_length": 32768,
204
+ "pad_token": "<|vision_pad|>",
205
+ "padding_side": "left",
206
+ "split_special_tokens": false,
207
+ "tokenizer_class": "Qwen2Tokenizer",
208
+ "unk_token": null
209
+ }
huggingface_tokenizers_cache/models--unsloth--qwen2.5-1.5b-instruct-unsloth-bnb-4bit/snapshots/3d254dbee5e3beae81bb8a717ad3a03427a09d26/vocab.json ADDED
The diff for this file is too large to render. See raw diff
 
inference.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Inference Script — AI Sprint Manager OpenEnv
3
+ ============================================================
4
+ MANDATORY:
5
+ API_BASE_URL : LLM endpoint
6
+ MODEL_NAME : Model identifier
7
+ HF_TOKEN : Hugging Face / API key
8
+ """
9
+ from __future__ import annotations
10
+ import os
11
+ import json
12
+ import time
13
+ import sys
14
+ import requests
15
+ from dotenv import load_dotenv
16
+ from openai import OpenAI
17
+
18
+ load_dotenv()
19
+
20
+ # ── Config ────────────────────────────────────────────────────────────────────
21
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
22
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY", "dummy")
23
+ MODEL_NAME = os.getenv("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct")
24
+ ENV_BASE_URL = os.getenv("ENV_BASE_URL", "https://sejal-k-ai-sprint-manager.hf.space")
25
+
26
+ MAX_STEPS = 12
27
+ TEMPERATURE = 0.2
28
+ MAX_TOKENS = 300
29
+ TASKS = ["easy_sprint", "medium_sprint", "hard_sprint"]
30
+
31
+ client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
32
+
33
+ SYSTEM_PROMPT = """You are an expert Tech Lead managing an agile sprint.
34
+ Your goal: maximize task completion, balance developer workload, and meet deadlines.
35
+
36
+ Each step output a JSON action with this exact schema:
37
+ {
38
+ "action_type": "<assign|reassign|reprioritize|unblock|skip>",
39
+ "task_id": "<task id or null>",
40
+ "dev_id": "<developer id or null>",
41
+ "new_priority": <1-5 or null>
42
+ }
43
+
44
+ Rules:
45
+ - assign: put a backlog task onto an available developer
46
+ - reassign: move an in-progress task to a different developer
47
+ - reprioritize: change a task priority (1=highest)
48
+ - unblock: unblock a blocked task
49
+ - skip: do nothing
50
+
51
+ Output ONLY the JSON object. No explanation."""
52
+
53
+
54
+ def build_user_prompt(obs: dict) -> str:
55
+ tasks_summary = "\n".join(
56
+ f" [{t['id']}] {t['name']} | {t['task_type']} | P{t['priority']} | "
57
+ f"effort={t['effort']} | due=Day{t['deadline']} | status={t['status']} | "
58
+ f"dev={t['assigned_to']} | progress={t['progress']:.0%}"
59
+ for t in obs["tasks"]
60
+ )
61
+ devs_summary = "\n".join(
62
+ f" [{d['id']}] {d['name']} | skill={d['skill']} | "
63
+ f"load={d['current_load']}/{d['capacity']} | available={d['is_available']}"
64
+ for d in obs["developers"]
65
+ )
66
+ events_str = "\n ".join(obs.get("events", [])) or "None"
67
+ return f"""Day: {obs['current_day']}/{obs['sprint_length']}
68
+ Done:{obs['tasks_completed']} Missed:{obs['tasks_missed']} InProgress:{obs['tasks_in_progress']} Backlog:{obs['tasks_backlog']}
69
+ Cumulative Reward: {obs['cumulative_reward']:.2f}
70
+
71
+ Events: {events_str}
72
+
73
+ TASKS:
74
+ {tasks_summary}
75
+
76
+ DEVELOPERS:
77
+ {devs_summary}
78
+
79
+ Output your JSON action:"""
80
+
81
+
82
+ def call_env(endpoint: str, payload: dict = None, method: str = "POST") -> dict:
83
+ url = f"{ENV_BASE_URL}/{endpoint}"
84
+ if method == "GET":
85
+ resp = requests.get(url, timeout=30)
86
+ else:
87
+ resp = requests.post(url, json=payload or {}, timeout=30)
88
+ resp.raise_for_status()
89
+ return resp.json()
90
+
91
+
92
+ def get_rule_based_action(obs: dict) -> str:
93
+ """Fallback rule-based action when LLM unavailable."""
94
+ tasks = obs.get("tasks", [])
95
+ devs = obs.get("developers", [])
96
+ backlog = sorted(
97
+ [t for t in tasks if t["status"] == "backlog"],
98
+ key=lambda t: (t["priority"], t["deadline"])
99
+ )
100
+ if not backlog:
101
+ return '{"action_type": "skip"}'
102
+ task = backlog[0]
103
+ available = [d for d in devs if d["is_available"] and d["current_load"] < d["capacity"]]
104
+ skill_match = [d for d in available if d["skill"] == task["required_skill"] or d["skill"] == "fullstack"]
105
+ dev = skill_match[0] if skill_match else (available[0] if available else None)
106
+ if not dev:
107
+ return '{"action_type": "skip"}'
108
+ return json.dumps({"action_type": "assign", "task_id": task["id"], "dev_id": dev["id"], "new_priority": None})
109
+
110
+
111
+ def parse_action(text: str) -> dict:
112
+ text = text.strip()
113
+ if "```" in text:
114
+ lines = [l for l in text.split("\n") if not l.strip().startswith("```")]
115
+ text = "\n".join(lines)
116
+ try:
117
+ return json.loads(text)
118
+ except json.JSONDecodeError:
119
+ start, end = text.find("{"), text.rfind("}") + 1
120
+ if start >= 0 and end > start:
121
+ try:
122
+ return json.loads(text[start:end])
123
+ except Exception:
124
+ pass
125
+ return {"action_type": "skip", "task_id": None, "dev_id": None, "new_priority": None}
126
+
127
+
128
+ def run_episode(task_name: str) -> float:
129
+ """Run one complete episode and return final score."""
130
+
131
+ # ── [START] block ─────────────────────────────────────────────────────────
132
+ print(f"[START] task={task_name}", flush=True)
133
+
134
+ obs = call_env("reset", {"task_name": task_name, "seed": 42})
135
+ final_score = 0.0
136
+ step_num = 0
137
+
138
+ for step_num in range(1, MAX_STEPS + 1):
139
+ if obs.get("done", False):
140
+ break
141
+
142
+ try:
143
+ completion = client.chat.completions.create(
144
+ model=MODEL_NAME,
145
+ messages=[
146
+ {"role": "system", "content": SYSTEM_PROMPT},
147
+ {"role": "user", "content": build_user_prompt(obs)},
148
+ ],
149
+ temperature=TEMPERATURE,
150
+ max_tokens=MAX_TOKENS,
151
+ )
152
+ response_text = completion.choices[0].message.content or ""
153
+ except Exception as e:
154
+ response_text = get_rule_based_action(obs)
155
+
156
+ action = parse_action(response_text)
157
+ result = call_env("step", {"action": action})
158
+ obs = result["observation"]
159
+ reward = result["reward"]
160
+ done = result["done"]
161
+ info = result.get("info", {})
162
+
163
+ # ── [STEP] block ──────────────────────────────────────────────────────
164
+ print(
165
+ f"[STEP] task={task_name} step={step_num} "
166
+ f"action={action.get('action_type')} reward={reward:.4f} "
167
+ f"cumulative={obs.get('cumulative_reward', 0):.4f} done={done}",
168
+ flush=True
169
+ )
170
+
171
+ if done:
172
+ final_score = max(0.01, min(0.99, info.get("final_score", 0.01)))
173
+ break
174
+
175
+ # ── [END] block ───────────────────────────────────────────────────────────
176
+ print(
177
+ f"[END] task={task_name} score={final_score:.4f} steps={step_num}",
178
+ flush=True
179
+ )
180
+ return final_score
181
+
182
+
183
+ def main():
184
+ print(f"[INFO] model={MODEL_NAME} server={ENV_BASE_URL}", flush=True)
185
+
186
+ try:
187
+ health = call_env("health", method="GET")
188
+ print(f"[INFO] health={health}", flush=True)
189
+ except Exception as e:
190
+ print(f"[ERROR] Cannot reach env server: {e}", flush=True)
191
+ sys.exit(1)
192
+
193
+ scores = {}
194
+ start_time = time.time()
195
+
196
+ for task in TASKS:
197
+ try:
198
+ score = run_episode(task)
199
+ scores[task] = score
200
+ except Exception as e:
201
+ print(f"[ERROR] task={task} error={e}", flush=True)
202
+ scores[task] = 0.0
203
+
204
+ elapsed = time.time() - start_time
205
+
206
+ # Human-readable summary
207
+ print("\n" + "="*60, flush=True)
208
+ print(" BASELINE SCORES", flush=True)
209
+ print("="*60, flush=True)
210
+ for task, score in scores.items():
211
+ bar = "█" * int(score * 20)
212
+ print(f" {task:<20} {score:.4f} {bar}", flush=True)
213
+ avg = sum(scores.values()) / len(scores) if scores else 0.0
214
+ print(f" {'AVERAGE':<20} {avg:.4f}", flush=True)
215
+ print(f"\n Runtime: {elapsed:.1f}s", flush=True)
216
+ print("="*60, flush=True)
217
+
218
+
219
+ if __name__ == "__main__":
220
+ main()
inference_r2.py ADDED
@@ -0,0 +1,855 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ inference_r2.py — Round 2 LLM agent for AI Sprint Manager
3
+ ==========================================================
4
+
5
+ ROOT-CAUSE FIXES in this version:
6
+ [FIX-I1] PROMPT ALIGNMENT — system prompt and user prompt now exactly match
7
+ train_llm.py (R2_SYSTEM_PROMPT + _build_r2_prompt format).
8
+ The fine-tuned model conditions strongly on the EXACT wording it
9
+ saw during training. The previous version used a completely different
10
+ system prompt ("You are a tech lead managing a 6-sprint...") and a
11
+ different user prompt format (day=/sprint= vs D/60 S/6).
12
+ This single mismatch is the biggest cause of poor inference scores.
13
+
14
+ [FIX-I2] LOCAL MODEL LOADING — sejal-k/ai-sprint-manager-trained is a LoRA
15
+ adapter, not a full model. The HF Router cannot serve it. The previous
16
+ version was routing all calls to the base Qwen model via HF Router,
17
+ producing base-model scores instead of fine-tuned scores.
18
+ Fix: load the adapter locally via Unsloth (preferred) or PEFT.
19
+ Set LOCAL_MODEL_PATH env var to the adapter path/HF repo ID.
20
+
21
+ [FIX-I3] LLM_CALL_EVERY=1 — was 3, meaning 2 out of every 3 steps used the
22
+ rule-based fallback regardless of model quality. With a local model
23
+ there is no rate-limit, so every step uses the LLM.
24
+
25
+ [FIX-I4] TEMPERATURE=0.3 for inference — was 0.1 (too greedy) and 0.8 (too
26
+ random). 0.3 gives slight diversity while staying near peak-prob output.
27
+
28
+ [FIX-I5] METADATA BUG — sort_key in smart_fallback used getattr(t, "metadata")
29
+ on plain dicts. Fixed to t.get("metadata", {}).
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import json
35
+ import os
36
+ import re
37
+ import time
38
+ import random
39
+ from typing import Optional, Tuple
40
+
41
+ import requests
42
+
43
+ # ─── Config ───────────────────────────────────────────────────────────────────
44
+ # Set LOCAL_MODEL_PATH to use the fine-tuned adapter. Examples:
45
+ # export LOCAL_MODEL_PATH=results/trained_model (local checkpoint)
46
+ # export LOCAL_MODEL_PATH=sejal-k/ai-sprint-manager-trained (HF Hub adapter)
47
+ LOCAL_MODEL_PATH = os.getenv("LOCAL_MODEL_PATH", "priyaaaaaasharmaaaaa/trial1")
48
+ MODEL_NAME = os.getenv("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct")
49
+ ENV_BASE_URL = os.getenv("ENV_BASE_URL", "https://sejal-k-ai-sprint-manager.hf.space")
50
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
51
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
52
+
53
+ # Inference control: USE_LLM=0 → smart_fallback only (sanity vs LLM).
54
+ _use_llm_raw = os.getenv("USE_LLM", "1").strip().lower()
55
+ USE_LLM = _use_llm_raw not in ("0", "false", "no", "off", "")
56
+ LLM_COOLDOWN_STEPS = int(os.getenv("LLM_COOLDOWN_STEPS", "15"))
57
+ MAX_LLM_SOFT_FAIL_STREAK = int(os.getenv("MAX_LLM_SOFT_FAIL_STREAK", "3"))
58
+ MAX_LLM_SKIP_STREAK = int(os.getenv("MAX_LLM_SKIP_STREAK", "4"))
59
+ MAX_SAME_BAD_ASSIGN_STREAK = int(os.getenv("MAX_SAME_BAD_ASSIGN_STREAK", "2"))
60
+
61
+ MAX_TOKENS = 96 # matches train_llm.py max_completion_length
62
+ MAX_RETRIES = 2
63
+ LLM_CALL_EVERY = 1 # [FIX-I3] every step — no rate limit with local model
64
+ TEMPERATURE = 0.3 # [FIX-I4]
65
+
66
+ TASK_ID_RE = re.compile(r"^T\d+$")
67
+ RETRYABLE_CODES = {429, 500, 502, 503, 504}
68
+
69
+
70
+ # ─── [FIX-I1] Prompts — EXACTLY matching train_llm.py ────────────────────────
71
+ # These strings must stay in sync with R2_SYSTEM_PROMPT and _build_r2_prompt()
72
+ # in train_llm.py. Any wording difference causes the fine-tuned model to produce
73
+ # lower-quality output because it no longer recognises its training context.
74
+
75
+ R2_SYSTEM_PROMPT = """You are an Engineering Manager running a 60-day software project.
76
+ Each step you MUST output exactly ONE JSON object and nothing else.
77
+
78
+ Schema (use null for unused fields):
79
+ {"action_type":"<assign|reassign|reprioritize|unblock|skip>","task_id":"<id or null>","dev_id":"<id or null>","new_priority":<1-5 or null>}
80
+
81
+ Rules (follow in order):
82
+ 1. If ACTIVE INSTRUCTIONS exist, assign THEIR tasks first.
83
+ 2. Only assign tasks with status=backlog (never in_progress or done).
84
+ 3. Only assign if all dependency markers show ✓.
85
+ 4. Only assign to an AVAILABLE developer with matching or fullstack skill.
86
+ 5. Use unblock ONLY for explicitly blocked tasks whose deps are ✓.
87
+ 6. skip is last resort.
88
+
89
+ Output ONLY the JSON. No explanation."""
90
+
91
+
92
+ def build_user_prompt(
93
+ obs: dict,
94
+ *,
95
+ assigned_this_episode: Optional[set] = None,
96
+ assign_attempted_episode: Optional[set] = None,
97
+ ) -> str:
98
+ """
99
+ Core layout matches _build_r2_prompt() in train_llm.py.
100
+ Optional MEMORY block is inference-only: steers the model away from repeat
101
+ assigns and lists live IN_PROGRESS ids (training export can omit kwargs).
102
+ """
103
+ current_sprint = obs.get("current_sprint", 1)
104
+ current_day = obs.get("current_day", 1)
105
+ days_left = max(0, current_sprint * 10 - current_day + 1)
106
+ tasks = obs.get("tasks", [])
107
+ done_ids = {t["id"] for t in tasks if t.get("status") == "done"}
108
+
109
+ active_insts = [i for i in obs.get("instruction_queue", []) if not i.get("followed", False)]
110
+ inst_section = (
111
+ "⚡FOLLOW: " + " | ".join(f"[{i['id']}] {i['text'][:50]}" for i in active_insts[:2])
112
+ ) if active_insts else "No instructions."
113
+
114
+ debt_raw = obs.get("tech_debt", [])
115
+ debt_count = len(debt_raw) if isinstance(debt_raw, list) else int(debt_raw or 0)
116
+
117
+ backlog = sorted(
118
+ [t for t in tasks if t.get("status") == "backlog"],
119
+ key=lambda t: (t.get("priority", 9), t.get("deadline", 99))
120
+ )
121
+ in_prog = [t for t in tasks if t.get("status") == "in_progress"]
122
+
123
+ def fmt(t: dict) -> str:
124
+ meta = t.get("metadata", {}) or {} # [FIX-I5]
125
+ deps = t.get("depends_on", []) or meta.get("depends_on", [])
126
+ dep_ok = "✓" if all(d in done_ids for d in deps) else "✗"
127
+ return (f"[{t['id']}]P{t.get('priority','?')} "
128
+ f"{str(t.get('required_skill','?'))[:4]} {dep_ok} "
129
+ f"D{t.get('deadline', t.get('deadline_day','?'))}")
130
+
131
+ backlog_str = " ".join(fmt(t) for t in backlog[:6])
132
+ if len(backlog) > 6:
133
+ backlog_str += f" +{len(backlog)-6}"
134
+
135
+ inprog_str = " ".join(f"[{t['id']}]→{t.get('assigned_to','?')}" for t in in_prog) or "none"
136
+ avail_devs = [d for d in obs.get("developers", []) if d.get("is_available", False)]
137
+ devs_str = " ".join(
138
+ f"[{d['id']}]{str(d.get('name','?'))[:4]}({str(d.get('skill','?'))[:3]}) "
139
+ f"{d.get('current_load',0)}/{d.get('capacity',5)}"
140
+ for d in avail_devs
141
+ )
142
+
143
+ memory_lines: list[str] = []
144
+ in_prog_ids = [t["id"] for t in in_prog]
145
+ if in_prog_ids:
146
+ memory_lines.append(
147
+ "NO_REASSIGN_UNTIL_BACKLOG: " + " ".join(in_prog_ids)
148
+ + " — do NOT assign/reassign these while they stay in_progress."
149
+ )
150
+ if assigned_this_episode:
151
+ memory_lines.append(
152
+ "ASSIGNED_OK_THIS_EP: " + " ".join(sorted(assigned_this_episode))
153
+ + " — already started; do not assign again."
154
+ )
155
+ if assign_attempted_episode:
156
+ extra = assign_attempted_episode - set(assigned_this_episode or ())
157
+ if extra:
158
+ memory_lines.append(
159
+ "ASSIGN_ALREADY_TRIED: " + " ".join(sorted(extra))
160
+ + " — pick a different backlog task if still open."
161
+ )
162
+ memory_block = ("EPISODE_MEMORY:\n" + "\n".join(memory_lines) + "\n") if memory_lines else ""
163
+
164
+ return (
165
+ f"D{current_day}/60 S{current_sprint}/6 {days_left}d "
166
+ f"done={obs.get('tasks_completed',0)} miss={obs.get('tasks_missed',0)} "
167
+ f"inst={obs.get('instruction_following_score',0):.2f} debt={debt_count}\n"
168
+ f"{inst_section}\n"
169
+ f"BACKLOG(✓=deps_ok): {backlog_str}\n"
170
+ f"IN_PROG: {inprog_str}\n"
171
+ f"DEVS(avail): {devs_str}\n"
172
+ f"{memory_block}"
173
+ f"JSON:"
174
+ )
175
+
176
+
177
+ def _completed_ids(obs: dict) -> set:
178
+ return {t["id"] for t in obs.get("tasks", []) if t.get("status") == "done"}
179
+
180
+
181
+ def _deps_met_task(obs: dict, task: dict) -> bool:
182
+ done_ids = _completed_ids(obs)
183
+ meta = task.get("metadata", {}) or {}
184
+ deps = task.get("depends_on", []) or meta.get("depends_on", [])
185
+ return all(d in done_ids for d in deps)
186
+
187
+
188
+ def _dev_by_id(obs: dict, dev_id: object) -> Optional[dict]:
189
+ sid = str(dev_id)
190
+ for d in obs.get("developers", []):
191
+ if str(d.get("id")) == sid:
192
+ return d
193
+ return None
194
+
195
+
196
+ def validate_llm_action(
197
+ obs: dict,
198
+ action: Optional[dict],
199
+ assigned_this_episode: set,
200
+ ) -> Tuple[bool, str]:
201
+ """
202
+ Hard gate: reject LLM output that violates env rules (backlog, deps, dev).
203
+ """
204
+ if not action:
205
+ return False, "empty"
206
+ at = action.get("action_type")
207
+ if at not in {"assign", "reassign", "reprioritize", "unblock", "skip"}:
208
+ return False, "bad_type"
209
+
210
+ if at == "skip":
211
+ return True, "ok"
212
+
213
+ tid = action.get("task_id")
214
+ if not tid:
215
+ return False, "missing_task_id"
216
+
217
+ by_id = {t["id"]: t for t in obs.get("tasks", [])}
218
+ task = by_id.get(tid)
219
+ if task is None:
220
+ return False, "unknown_task"
221
+
222
+ if at == "unblock":
223
+ if task.get("status") != "blocked":
224
+ return False, "unblock_not_blocked"
225
+ if not _deps_met_task(obs, task):
226
+ return False, "unblock_deps"
227
+ return True, "ok"
228
+
229
+ if at == "reprioritize":
230
+ if task.get("status") != "backlog":
231
+ return False, "reprioritize_not_backlog"
232
+ if not _deps_met_task(obs, task):
233
+ return False, "reprioritize_deps"
234
+ np = action.get("new_priority")
235
+ if np is None:
236
+ return False, "reprioritize_no_priority"
237
+ try:
238
+ npi = int(np)
239
+ except (TypeError, ValueError):
240
+ return False, "reprioritize_bad_priority"
241
+ if not (1 <= npi <= 5):
242
+ return False, "reprioritize_range"
243
+ return True, "ok"
244
+
245
+ if at in ("assign", "reassign"):
246
+ st = task.get("status")
247
+ if st != "backlog":
248
+ return False, f"assign_bad_status:{st}"
249
+ if not _deps_met_task(obs, task):
250
+ return False, "assign_deps"
251
+ if tid in assigned_this_episode:
252
+ return False, "assign_already_started_episode"
253
+ did = action.get("dev_id")
254
+ if not did:
255
+ return False, "assign_no_dev"
256
+ dev = _dev_by_id(obs, did)
257
+ if dev is None:
258
+ return False, "assign_unknown_dev"
259
+ if not dev.get("is_available", False):
260
+ return False, "assign_dev_unavailable"
261
+ try:
262
+ rem = int(dev.get("remaining_capacity", dev.get("capacity", 1)))
263
+ if rem <= 0:
264
+ return False, "assign_dev_no_capacity"
265
+ except (TypeError, ValueError):
266
+ pass
267
+ skill = task.get("required_skill", "")
268
+ dskill = dev.get("skill", "")
269
+ if dskill not in (skill, "fullstack"):
270
+ return False, "assign_skill_mismatch"
271
+ return True, "ok"
272
+
273
+ return False, "unhandled"
274
+
275
+
276
+ # ─── [FIX-I2] Local fine-tuned model loader ───────────────────────────────────
277
+
278
+ _local_model = None
279
+ _local_tokenizer = None
280
+ _local_backend = None
281
+
282
+
283
+ def _load_local_model(model_path: str) -> bool:
284
+ """
285
+ [FIX-I2] Load LoRA adapter locally. HF Router cannot serve LoRA adapters —
286
+ it requires fully merged weights. Unsloth first, PEFT+bitsandbytes fallback.
287
+ """
288
+ global _local_model, _local_tokenizer, _local_backend
289
+ if _local_model is not None:
290
+ return True
291
+
292
+ print(f"[INFO] Loading fine-tuned model: {model_path}", flush=True)
293
+
294
+ unsloth_err: Optional[BaseException] = None
295
+ # Attempt 1: Unsloth (fastest, native 4-bit, same library used for training)
296
+ try:
297
+ from unsloth import FastLanguageModel
298
+ model, tokenizer = FastLanguageModel.from_pretrained(
299
+ model_name=model_path, max_seq_length=2048, dtype=None,
300
+ load_in_4bit=True, token=HF_TOKEN or None,
301
+ )
302
+ FastLanguageModel.for_inference(model)
303
+ _local_model, _local_tokenizer, _local_backend = model, tokenizer, "unsloth"
304
+ print("[INFO] Loaded via Unsloth (fast 4-bit inference).", flush=True)
305
+ return True
306
+ except Exception as e:
307
+ unsloth_err = e
308
+ print(f"[WARN] Unsloth failed: {e}", flush=True)
309
+
310
+ # Attempt 2: PEFT + bitsandbytes
311
+ try:
312
+ import torch
313
+ from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
314
+ from peft import PeftModel
315
+ import huggingface_hub
316
+
317
+ try:
318
+ cfg_path = huggingface_hub.hf_hub_download(
319
+ model_path, "adapter_config.json", token=HF_TOKEN or None
320
+ )
321
+ except Exception:
322
+ cfg_path = os.path.join(model_path, "adapter_config.json")
323
+
324
+ with open(cfg_path) as f:
325
+ adapter_cfg = json.load(f)
326
+ base_id = adapter_cfg.get("base_model_name_or_path", "Qwen/Qwen2.5-1.5B-Instruct")
327
+ print(f"[INFO] Base model: {base_id}", flush=True)
328
+
329
+ bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16)
330
+ tok = AutoTokenizer.from_pretrained(base_id, token=HF_TOKEN or None)
331
+ base = AutoModelForCausalLM.from_pretrained(
332
+ base_id, quantization_config=bnb, device_map="auto", token=HF_TOKEN or None
333
+ )
334
+ model = PeftModel.from_pretrained(base, model_path, token=HF_TOKEN or None)
335
+ model.eval()
336
+ _local_model, _local_tokenizer, _local_backend = model, tok, "peft"
337
+ print("[INFO] Loaded via PEFT + bitsandbytes 4-bit.", flush=True)
338
+ return True
339
+ except Exception as e2:
340
+ print(
341
+ f"[ERROR] Cannot load local model.\n Unsloth: {unsloth_err}\n PEFT: {e2}",
342
+ flush=True,
343
+ )
344
+ return False
345
+
346
+
347
+ def _call_local_model(user_prompt: str) -> Optional[dict]:
348
+ """Run one inference step on the locally loaded fine-tuned model."""
349
+ if _local_model is None:
350
+ return None
351
+ import torch
352
+
353
+ messages = [
354
+ {"role": "system", "content": R2_SYSTEM_PROMPT},
355
+ {"role": "user", "content": user_prompt},
356
+ ]
357
+ tok = _local_tokenizer
358
+ try:
359
+ if hasattr(tok, "apply_chat_template"):
360
+ prompt_text = tok.apply_chat_template(
361
+ messages, tokenize=False, add_generation_prompt=True
362
+ )
363
+ else:
364
+ prompt_text = "\n".join(
365
+ f"<|{m['role']}|>\n{m['content']}" for m in messages
366
+ ) + "\n<|assistant|>\n"
367
+
368
+ inputs = tok(prompt_text, return_tensors="pt").to(_local_model.device)
369
+ inp_len = inputs["input_ids"].shape[1]
370
+
371
+ with torch.no_grad():
372
+ outputs = _local_model.generate(
373
+ **inputs, max_new_tokens=MAX_TOKENS,
374
+ temperature=TEMPERATURE, do_sample=True,
375
+ pad_token_id=tok.pad_token_id or tok.eos_token_id,
376
+ )
377
+
378
+ completion = tok.decode(outputs[0][inp_len:], skip_special_tokens=True).strip()
379
+ return parse_action(completion)
380
+
381
+ except Exception as e:
382
+ print(f" [WARN] Local model inference error: {e}", flush=True)
383
+ return None
384
+
385
+
386
+ # ─── HF Router client (fallback when no local model) ─────────────────────────
387
+
388
+ def _call_api_model(user_prompt: str) -> Optional[dict]:
389
+ """Use HF Router — only for full (non-adapter) models."""
390
+ try:
391
+ from openai import OpenAI, APIStatusError
392
+ except ImportError:
393
+ return None
394
+ if not HF_TOKEN:
395
+ return None
396
+
397
+ client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
398
+ messages = [
399
+ {"role": "system", "content": R2_SYSTEM_PROMPT},
400
+ {"role": "user", "content": user_prompt},
401
+ ]
402
+ for attempt in range(1, MAX_RETRIES + 2):
403
+ try:
404
+ resp = client.chat.completions.create(
405
+ model=MODEL_NAME, messages=messages,
406
+ max_tokens=MAX_TOKENS, temperature=TEMPERATURE,
407
+ )
408
+ return parse_action(resp.choices[0].message.content or "")
409
+ except APIStatusError as e:
410
+ if e.status_code not in RETRYABLE_CODES:
411
+ return None
412
+ if attempt <= MAX_RETRIES:
413
+ time.sleep(2 ** attempt + random.uniform(0, 0.5))
414
+ else:
415
+ return None
416
+ except Exception:
417
+ if attempt <= MAX_RETRIES:
418
+ time.sleep(2 ** attempt)
419
+ else:
420
+ return None
421
+ return None
422
+
423
+
424
+ def call_llm_user_prompt(user_prompt: str) -> Optional[dict]:
425
+ """Prefer local fine-tuned model; fall back to HF Router."""
426
+ if _local_model is not None:
427
+ return _call_local_model(user_prompt)
428
+ return _call_api_model(user_prompt)
429
+
430
+
431
+ # ─── Action parser ────────────────────────────────────────────────────────────
432
+
433
+ def parse_action(raw: str) -> Optional[dict]:
434
+ if not raw:
435
+ return None
436
+ raw = raw.strip()
437
+ raw = re.sub(r"^```[a-z]*\s*", "", raw)
438
+ raw = re.sub(r"\s*```$", "", raw)
439
+
440
+ # Find last balanced JSON object (handles CoT prefix the model might emit)
441
+ depth = 0; obj_start = -1; last_start = -1; last_end = -1
442
+ for i, ch in enumerate(raw):
443
+ if ch == "{":
444
+ if depth == 0: obj_start = i
445
+ depth += 1
446
+ elif ch == "}":
447
+ depth -= 1
448
+ if depth == 0 and obj_start >= 0:
449
+ last_start, last_end = obj_start, i + 1
450
+
451
+ obj = None
452
+ if last_start >= 0:
453
+ try:
454
+ obj = json.loads(raw[last_start:last_end])
455
+ except json.JSONDecodeError:
456
+ pass
457
+ if obj is None:
458
+ try:
459
+ obj = json.loads(raw)
460
+ except Exception:
461
+ return None
462
+
463
+ action_type = obj.get("action_type", "")
464
+ if action_type not in {"assign", "reassign", "reprioritize", "unblock", "skip"}:
465
+ return None
466
+
467
+ null_vals = {"null", "none", "None", "Null", "", "undefined", "N/A", "nil"}
468
+ for key in ("task_id", "dev_id", "new_priority"):
469
+ v = obj.get(key)
470
+ if v is not None and str(v).strip() in null_vals:
471
+ obj[key] = None
472
+
473
+ task_id = obj.get("task_id")
474
+ if task_id is not None and not TASK_ID_RE.match(str(task_id)):
475
+ print(f" [INVALID] task_id={repr(task_id)} rejected", flush=True)
476
+ return None
477
+
478
+ if action_type in {"assign", "reassign"}:
479
+ if not task_id or not obj.get("dev_id"):
480
+ return None
481
+
482
+ return {
483
+ "action_type": action_type,
484
+ "task_id": task_id,
485
+ "dev_id": obj.get("dev_id"),
486
+ "new_priority": obj.get("new_priority"),
487
+ }
488
+
489
+
490
+ # Aliases for evaluate_r2 / train naming
491
+ _build_r2_prompt = build_user_prompt
492
+ _parse_action = parse_action
493
+
494
+
495
+ # ─── Smart fallback ───────────────────────────────────────────────────────────
496
+
497
+ def smart_fallback(obs: dict, assigned_this_episode: set, last_dev_idx: list) -> dict:
498
+ tasks = obs.get("tasks", [])
499
+ devs = obs.get("developers", [])
500
+ instructions = obs.get("instruction_queue", [])
501
+ current_sprint = obs.get("current_sprint", 1)
502
+ completed_ids = {t["id"] for t in tasks if t.get("status") == "done"}
503
+
504
+ def deps_met(task: dict) -> bool:
505
+ meta = task.get("metadata", {}) or {} # [FIX-I5]
506
+ deps = task.get("depends_on", []) or meta.get("depends_on", [])
507
+ return all(dep in completed_ids for dep in deps)
508
+
509
+ skip = {"action_type": "skip", "task_id": None, "dev_id": None, "new_priority": None}
510
+
511
+ # Tier 1: unblock blocked tasks with met deps
512
+ for task in tasks:
513
+ if task.get("status") == "blocked" and deps_met(task):
514
+ return {"action_type": "unblock", "task_id": task["id"],
515
+ "dev_id": None, "new_priority": None}
516
+
517
+ instruction_task_ids: set = set()
518
+ for inst in instructions:
519
+ if not inst.get("followed", False):
520
+ for tid in inst.get("affects_tasks", []):
521
+ instruction_task_ids.add(tid)
522
+
523
+ # Tier 2: reprioritize low-priority instruction tasks
524
+ for task in tasks:
525
+ if (task.get("status") == "backlog"
526
+ and task["id"] in instruction_task_ids
527
+ and task.get("priority", 9) > 2
528
+ and deps_met(task)):
529
+ return {"action_type": "reprioritize", "task_id": task["id"],
530
+ "dev_id": None, "new_priority": 1}
531
+
532
+ # Tier 3: assign
533
+ assignable = [t for t in tasks if t.get("status") == "backlog" and deps_met(t)]
534
+ if not assignable:
535
+ return skip
536
+
537
+ def sort_key(t: dict) -> tuple: # [FIX-I5]
538
+ meta = t.get("metadata", {}) or {}
539
+ sprint_target = meta.get("sprint", current_sprint + 99)
540
+ in_inst = 0 if t["id"] in instruction_task_ids else 1
541
+ return (in_inst, sprint_target, t.get("priority", 99))
542
+
543
+ assignable.sort(key=sort_key)
544
+ task = assignable[0]
545
+
546
+ available_devs = [
547
+ d for d in devs
548
+ if d.get("is_available", False)
549
+ and d.get("remaining_capacity", d.get("capacity", 1)) > 0
550
+ ]
551
+ if not available_devs:
552
+ available_devs = [d for d in devs if d.get("is_available", False)]
553
+ if not available_devs:
554
+ available_devs = devs
555
+
556
+ skill = task.get("required_skill", "")
557
+ skilled_devs = [d for d in available_devs
558
+ if d.get("skill") == skill or d.get("skill") == "fullstack"]
559
+ pool = skilled_devs if skilled_devs else available_devs
560
+ idx = last_dev_idx[0] % len(pool)
561
+ dev = pool[idx]
562
+ last_dev_idx[0] = (idx + 1) % len(pool)
563
+
564
+ return {"action_type": "assign", "task_id": task["id"],
565
+ "dev_id": dev["id"], "new_priority": None}
566
+
567
+
568
+ # ─── Environment helpers ──────────────────────────────────────────────────────
569
+
570
+ def _post(url: str, payload: dict, timeout: int = 15) -> dict:
571
+ resp = requests.post(url, json=payload, timeout=timeout)
572
+ resp.raise_for_status()
573
+ return resp.json()
574
+
575
+
576
+ def step_env(action: dict) -> dict:
577
+ return _post(f"{ENV_BASE_URL}/project/step", {"action": action})
578
+
579
+
580
+ def reset_env(scenario: str, seed: int = 42) -> dict:
581
+ return _post(f"{ENV_BASE_URL}/project/reset", {"task_name": scenario, "seed": seed})
582
+
583
+
584
+ def health() -> dict:
585
+ resp = requests.get(f"{ENV_BASE_URL}/project/health", timeout=10)
586
+ resp.raise_for_status()
587
+ return resp.json()
588
+
589
+
590
+ # ─── Episode runner ───────────────────────────────────────────────────────────
591
+
592
+ def run_episode(scenario: str, seed: int = 42) -> dict:
593
+ obs_data = reset_env(scenario, seed)
594
+ obs = obs_data.get("observation", obs_data)
595
+
596
+ assigned_this_episode: set = set()
597
+ assign_attempted_episode: set = set()
598
+ last_dev_idx = [0]
599
+ cumulative = 0.0
600
+ step_num = 0
601
+
602
+ llm_skip_streak = 0
603
+ llm_soft_fail_streak = 0
604
+ bad_assign_tid: Optional[str] = None
605
+ bad_assign_streak = 0
606
+ llm_cooldown_until = 0
607
+
608
+ MAX_STEPS = 200 # safety cap so your code doesn’t spiral forever
609
+ MAX_STEP_RETRIES = 3
610
+
611
+ print(f"\n[START] task={scenario}", flush=True)
612
+
613
+ while True:
614
+ step_num += 1
615
+
616
+ if step_num > MAX_STEPS:
617
+ print("[WARN] Max steps reached — forcing termination", flush=True)
618
+ break
619
+
620
+ day = obs.get("current_day", step_num)
621
+ sprint = obs.get("current_sprint", 1)
622
+
623
+ router_ok = _local_model is not None or bool(HF_TOKEN)
624
+ allow_llm = (
625
+ USE_LLM
626
+ and (step_num % LLM_CALL_EVERY == 1)
627
+ and router_ok
628
+ and step_num > llm_cooldown_until
629
+ )
630
+
631
+ action = None
632
+
633
+ if allow_llm:
634
+ user_prompt = build_user_prompt(
635
+ obs,
636
+ assigned_this_episode=assigned_this_episode,
637
+ assign_attempted_episode=assign_attempted_episode,
638
+ )
639
+ proposed = call_llm_user_prompt(user_prompt)
640
+
641
+ if proposed is None:
642
+ llm_soft_fail_streak += 1
643
+ print(" [LLM] no parse / API fail → fallback", flush=True)
644
+ else:
645
+ ok, reason = validate_llm_action(obs, proposed, assigned_this_episode)
646
+ if ok:
647
+ if proposed.get("action_type") == "skip":
648
+ llm_skip_streak += 1
649
+ if llm_skip_streak >= MAX_LLM_SKIP_STREAK:
650
+ action = None
651
+ llm_cooldown_until = max(
652
+ llm_cooldown_until, step_num + LLM_COOLDOWN_STEPS
653
+ )
654
+ llm_skip_streak = 0
655
+ llm_soft_fail_streak += 1
656
+ print(
657
+ " [COOLDOWN] skip-spam — rule-based this step + "
658
+ f"{LLM_COOLDOWN_STEPS}-step LLM pause",
659
+ flush=True,
660
+ )
661
+ else:
662
+ action = proposed
663
+ llm_soft_fail_streak = 0
664
+ bad_assign_tid = None
665
+ bad_assign_streak = 0
666
+ else:
667
+ llm_skip_streak = 0
668
+ action = proposed
669
+ llm_soft_fail_streak = 0
670
+ bad_assign_tid = None
671
+ bad_assign_streak = 0
672
+ else:
673
+ llm_soft_fail_streak += 1
674
+ print(
675
+ f" [REJECT] LLM action invalid ({reason}) → fallback",
676
+ flush=True,
677
+ )
678
+ if proposed.get("action_type") in ("assign", "reassign"):
679
+ tidp = proposed.get("task_id")
680
+ tid_key = str(tidp) if tidp is not None else None
681
+ if tid_key == bad_assign_tid:
682
+ bad_assign_streak += 1
683
+ else:
684
+ bad_assign_tid = tid_key
685
+ bad_assign_streak = 1
686
+ else:
687
+ bad_assign_tid = None
688
+ bad_assign_streak = 0
689
+
690
+ if bad_assign_streak >= MAX_SAME_BAD_ASSIGN_STREAK:
691
+ llm_cooldown_until = max(
692
+ llm_cooldown_until, step_num + LLM_COOLDOWN_STEPS
693
+ )
694
+ bad_assign_streak = 0
695
+ bad_assign_tid = None
696
+ print(
697
+ f" [COOLDOWN] repeated bad assign → rule-based for {LLM_COOLDOWN_STEPS} steps",
698
+ flush=True,
699
+ )
700
+
701
+ if llm_soft_fail_streak >= MAX_LLM_SOFT_FAIL_STREAK:
702
+ llm_cooldown_until = max(
703
+ llm_cooldown_until, step_num + LLM_COOLDOWN_STEPS
704
+ )
705
+ llm_soft_fail_streak = 0
706
+ print(
707
+ f" [COOLDOWN] repeated LLM invalid → rule-based for {LLM_COOLDOWN_STEPS} steps",
708
+ flush=True,
709
+ )
710
+
711
+ if action is None:
712
+ action = smart_fallback(obs, assigned_this_episode, last_dev_idx)
713
+
714
+ if action.get("action_type") in ("assign", "reassign") and action.get("task_id"):
715
+ assign_attempted_episode.add(action["task_id"])
716
+
717
+ # ─── STEP WITH RETRY + STATE VALIDATION ───────────────────────
718
+ success = False
719
+
720
+ for attempt in range(MAX_STEP_RETRIES):
721
+ result = step_env(action)
722
+ new_obs = result.get("observation", result)
723
+
724
+ # 🚨 HARD GUARD: detect time regression
725
+ if (
726
+ new_obs.get("current_day", 0) < obs.get("current_day", 0) or
727
+ new_obs.get("current_sprint", 0) < obs.get("current_sprint", 0)
728
+ ):
729
+ print(f"[ERROR] State regression detected (attempt {attempt+1}) — retrying", flush=True)
730
+ time.sleep(0.5)
731
+ continue
732
+
733
+ success = True
734
+ break
735
+
736
+ if not success:
737
+ print("[FATAL] Repeated environment corruption — aborting episode", flush=True)
738
+ break
739
+
740
+ reward = result.get("reward", 0.0)
741
+ obs = new_obs
742
+ done = result.get("done", obs.get("done", False))
743
+ cumulative += reward
744
+
745
+ inst_score = obs.get("instruction_following_score", 0.0)
746
+ debt_raw = obs.get("tech_debt", 0)
747
+
748
+ # Track tasks successfully moved to in_progress (assign / reassign)
749
+ if action.get("action_type") in ("assign", "reassign") and action.get("task_id"):
750
+ tid_sent = action["task_id"]
751
+ post_s = {t["id"]: t.get("status") for t in obs.get("tasks", [])}
752
+ if post_s.get(tid_sent) == "in_progress":
753
+ assigned_this_episode.add(tid_sent)
754
+
755
+ debt_d = len(debt_raw) if isinstance(debt_raw, list) else int(debt_raw or 0)
756
+
757
+ print(
758
+ f"[STEP] task={scenario} step={step_num} day={day} sprint={sprint} "
759
+ f"action={action.get('action_type','?')} "
760
+ f"task_id={action.get('task_id','None')} "
761
+ f"dev={action.get('dev_id','None')} "
762
+ f"reward={reward:.4f} cumulative={cumulative:.4f} "
763
+ f"inst_score={inst_score:.3f} debt={debt_d} done={done}",
764
+ flush=True,
765
+ )
766
+
767
+ if done:
768
+ break
769
+
770
+ # ─── FINAL METRICS ───────────────────────────────────────────────
771
+ tasks = obs.get("tasks", [])
772
+ completed = sum(1 for t in tasks if t.get("status") == "done")
773
+ missed = sum(1 for t in tasks if t.get("status") == "missed")
774
+ inst_score = obs.get("instruction_following_score", 0.0)
775
+
776
+ debt_raw = obs.get("tech_debt", 0)
777
+ debt_count = len(debt_raw) if isinstance(debt_raw, list) else int(debt_raw or 0)
778
+
779
+ total = len(tasks) or 1
780
+
781
+ final_score = max(0.01, min(0.99,
782
+ (completed / total) * 0.55 +
783
+ inst_score * 0.30 +
784
+ max(0.01, 1.0 - debt_count * 0.02) * 0.15
785
+ ))
786
+
787
+ print(
788
+ f"[END] task={scenario} score={final_score:.4f} steps={step_num} "
789
+ f"completed={completed} missed={missed} "
790
+ f"inst_score={inst_score:.3f} debt={debt_count}",
791
+ flush=True,
792
+ )
793
+
794
+ return {
795
+ "scenario": scenario,
796
+ "score": final_score,
797
+ "completed": completed,
798
+ "missed": missed,
799
+ "inst_score": inst_score,
800
+ "debt": debt_count,
801
+ "steps": step_num
802
+ }
803
+
804
+ # ─── Main ─────────────────────────────────────────────────────────────────────
805
+
806
+ def main():
807
+ scenarios = ["project_easy", "project_medium", "project_hard"]
808
+
809
+ local_ok = False
810
+ if LOCAL_MODEL_PATH:
811
+ local_ok = _load_local_model(LOCAL_MODEL_PATH)
812
+ if not local_ok:
813
+ print("[WARN] Local model load failed — scores will reflect BASE model!", flush=True)
814
+
815
+ if not USE_LLM:
816
+ mode = "rule-based-only (USE_LLM=0)"
817
+ else:
818
+ mode = ("local-finetuned" if local_ok
819
+ else ("hf-router-base" if HF_TOKEN else "rule-based-only"))
820
+
821
+ print(f"[INFO] model={LOCAL_MODEL_PATH if local_ok else MODEL_NAME}", flush=True)
822
+ print(f"[INFO] inference mode={mode}", flush=True)
823
+ print(f"[INFO] USE_LLM={USE_LLM} cooldown_steps={LLM_COOLDOWN_STEPS}", flush=True)
824
+ print(f"[INFO] server={ENV_BASE_URL}", flush=True)
825
+
826
+ try:
827
+ print(f"[INFO] health={health()}", flush=True)
828
+ except Exception as e:
829
+ print(f"[WARN] health check failed: {e}", flush=True)
830
+
831
+ results = {}
832
+ t0 = time.time()
833
+ for scenario in scenarios:
834
+ try:
835
+ results[scenario] = run_episode(scenario)
836
+ except Exception as e:
837
+ print(f"[ERROR] {scenario}: {e}", flush=True)
838
+ results[scenario] = {"score": 0.01, "error": str(e)}
839
+
840
+ scores = [results[s].get("score", 0) for s in scenarios if s in results]
841
+ avg = sum(scores) / len(scores) if scores else 0
842
+
843
+ print("\n" + "=" * 62, flush=True)
844
+ print(f" ROUND 2 — SCORES [{mode}]", flush=True)
845
+ print("=" * 62, flush=True)
846
+ for s in scenarios:
847
+ sc = results.get(s, {}).get("score", 0)
848
+ print(f" {s:<22} {sc:.4f} {'█' * int(sc * 20)}", flush=True)
849
+ print(f"\n AVERAGE {avg:.4f}", flush=True)
850
+ print(f"\n Runtime: {time.time()-t0:.1f}s", flush=True)
851
+ print("=" * 62, flush=True)
852
+
853
+
854
+ if __name__ == "__main__":
855
+ main()
openenv.yaml ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: ai-sprint-manager
3
+ type: environment
4
+ runtime: python
5
+ app: server:app
6
+ port: 8000
7
+ description: >
8
+ An RL environment where an AI agent acts as a Tech Lead managing
9
+ agile sprints — assigning tasks to developers, balancing workload,
10
+ and handling unexpected events to maximize team productivity.
11
+ tasks:
12
+ - id: easy_sprint
13
+ name: "Easy Sprint: Small Team, Clear Tasks"
14
+ difficulty: easy
15
+ - id: medium_sprint
16
+ name: "Medium Sprint: Bugs + Delays"
17
+ difficulty: medium
18
+ - id: hard_sprint
19
+ name: "Hard Sprint: Cascading Failures & Scope Changes"
20
+ difficulty: hard
21
+
22
+ - id: project_easy
23
+ description: >
24
+ AI Engineering Manager — 6-sprint project (60 days), small stable team,
25
+ clear backlog, no cascade failures. Tests long-horizon planning baseline.
26
+ difficulty: easy
27
+ reset_endpoint: /project/reset
28
+ step_endpoint: /project/step
29
+ state_endpoint: /project/state
30
+ health_endpoint: /project/health
31
+ reset_payload:
32
+ task_name: project_easy
33
+ seed: 42
34
+ max_steps: 60
35
+ score_field: instruction_following_score
36
+ metadata:
37
+ round: 2
38
+ num_sprints: 6
39
+ total_days: 60
40
+ num_tasks: 24
41
+ num_instructions: 12
42
+ theme: long_horizon_planning
43
+
44
+ - id: project_medium
45
+ description: >
46
+ AI Engineering Manager — 6-sprint project (60 days), mid-size team with
47
+ developer absences, urgent bugs, and conflicting instructions mid-sprint.
48
+ difficulty: medium
49
+ reset_endpoint: /project/reset
50
+ step_endpoint: /project/step
51
+ state_endpoint: /project/state
52
+ health_endpoint: /project/health
53
+ reset_payload:
54
+ task_name: project_medium
55
+ seed: 42
56
+ max_steps: 60
57
+ score_field: instruction_following_score
58
+ metadata:
59
+ round: 2
60
+ num_sprints: 6
61
+ total_days: 60
62
+ num_tasks: 30
63
+ num_instructions: 18
64
+ theme: long_horizon_planning
65
+
66
+ - id: project_hard
67
+ description: >
68
+ AI Engineering Manager — 6-sprint project (60 days), large team with
69
+ cascading failures, scope pivots, conflicting instructions, and hard
70
+ delivery deadline. Scale AI bonus prize target scenario.
71
+ difficulty: hard
72
+ reset_endpoint: /project/reset
73
+ step_endpoint: /project/step
74
+ state_endpoint: /project/state
75
+ health_endpoint: /project/health
76
+ reset_payload:
77
+ task_name: project_hard
78
+ seed: 42
79
+ max_steps: 60
80
+ score_field: instruction_following_score
81
+ metadata:
82
+ round: 2
83
+ num_sprints: 6
84
+ total_days: 60
85
+ num_tasks: 37
86
+ num_instructions: 25
87
+ theme: long_horizon_planning
88
+ scale_ai_bonus: true
plot_results.py ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ plot_results.py — Results Charts for Hackathon Presentation
3
+ ============================================================
4
+ Generates publication-ready charts from evaluation JSON data.
5
+ Saves all charts to results/charts/ as both PNG and SVG.
6
+
7
+ BASELINE CONSTANTS (FINAL — measured, do not change):
8
+ R1 Llama-3.1-8B zero-shot: easy=0.0100, medium=0.4583, hard=0.0100, avg=0.1594
9
+ R2 Llama-3.1-8B zero-shot: easy=0.3198, medium=0.2443, hard=0.2520, avg=0.2720
10
+ Training model: Qwen/Qwen2.5-1.5B-Instruct (GRPO, 4-bit QLoRA)
11
+
12
+ Charts produced:
13
+ 1. r1_scores_comparison.png — R1 Llama baseline vs trained bar chart
14
+ 2. r2_scores_comparison.png — R2 Llama zero-shot vs trained bar chart (no rule-based)
15
+ 3. sprint_rewards.png — Sprint-by-sprint reward for each R2 scenario
16
+ 4. improvement_summary.png — Combined before/after delta chart (main slide chart)
17
+ 5. training_curve.png — GRPO training loss/reward curve (if trainer_state.json present)
18
+
19
+ Usage:
20
+ # After running evaluate_r2.py --baseline-only:
21
+ python plot_results.py --eval results/r2_evaluation.json
22
+
23
+ # With training curve (after train_llm.py):
24
+ python plot_results.py --eval results/r2_evaluation.json \\
25
+ --trainer results/trained_model/trainer_state.json
26
+
27
+ # Hardcode known scores for presentation (no eval file needed):
28
+ python plot_results.py --demo
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import json
35
+ import os
36
+ from pathlib import Path
37
+
38
+ CHARTS_DIR = Path("results/charts")
39
+ CHARTS_DIR.mkdir(parents=True, exist_ok=True)
40
+
41
+ # ── Measured baselines (FINAL) ────────────────────────────────────────────────
42
+ LLAMA_BASELINE_R1 = {
43
+ "easy_sprint": 0.0100,
44
+ "medium_sprint": 0.4583,
45
+ "hard_sprint": 0.0100,
46
+ "average": 0.1594,
47
+ }
48
+ LLAMA_BASELINE_R2 = {
49
+ "project_easy": 0.3198,
50
+ "project_medium": 0.2443,
51
+ "project_hard": 0.2520,
52
+ "average": 0.2720,
53
+ }
54
+ TRAINING_MODEL = "Qwen/Qwen2.5-1.5B-Instruct"
55
+
56
+ # ── Colour palette ────────────────────────────────────────────────────────────
57
+ C_LLAMA = "#6B7280" # grey — Llama zero-shot baseline
58
+ C_RULE = "#3B82F6" # blue — rule-based baseline
59
+ C_TRAINED = "#10B981" # green — trained Qwen (post-GRPO)
60
+ C_EASY = "#60A5FA"
61
+ C_MEDIUM = "#F59E0B"
62
+ C_HARD = "#EF4444"
63
+ C_BG = "#F9FAFB"
64
+ C_TEXT = "#111827"
65
+
66
+
67
+ def _setup_matplotlib():
68
+ import matplotlib
69
+ matplotlib.use("Agg") # headless
70
+ import matplotlib.pyplot as plt
71
+ import matplotlib.patches as mpatches
72
+ plt.rcParams.update({
73
+ "figure.facecolor": C_BG,
74
+ "axes.facecolor": C_BG,
75
+ "axes.edgecolor": "#D1D5DB",
76
+ "axes.labelcolor": C_TEXT,
77
+ "text.color": C_TEXT,
78
+ "xtick.color": C_TEXT,
79
+ "ytick.color": C_TEXT,
80
+ "grid.color": "#E5E7EB",
81
+ "grid.linestyle": "--",
82
+ "grid.alpha": 0.7,
83
+ "font.family": "sans-serif",
84
+ "font.size": 11,
85
+ "axes.titlesize": 13,
86
+ "axes.titleweight": "bold",
87
+ "figure.dpi": 150,
88
+ })
89
+ return plt, mpatches
90
+
91
+
92
+ def save(plt, name: str):
93
+ png = CHARTS_DIR / f"{name}.png"
94
+ svg = CHARTS_DIR / f"{name}.svg"
95
+ plt.tight_layout()
96
+ plt.savefig(png, bbox_inches="tight")
97
+ plt.savefig(svg, bbox_inches="tight")
98
+ plt.close()
99
+ print(f" [SAVED] {png}", flush=True)
100
+
101
+
102
+ # ── Chart 1: R1 scores comparison ────────────────────────────────────────────
103
+
104
+ def chart_r1_comparison(eval_data: dict):
105
+ plt, mpatches = _setup_matplotlib()
106
+
107
+ tasks = ["easy_sprint", "medium_sprint", "hard_sprint"]
108
+ labels = ["Easy Sprint", "Medium Sprint", "Hard Sprint"]
109
+ llama_base = [eval_data.get("r1_llama_baseline", LLAMA_BASELINE_R1).get(t, 0) for t in tasks]
110
+ rule_based = [eval_data.get("r1_rule_based", {}).get(t, {}).get("avg_score", 0) for t in tasks]
111
+ llm_scores = [eval_data.get("r1_llm", {}).get(t, {}).get("avg_score", 0) for t in tasks]
112
+
113
+ has_llm = any(v > 0 for v in llm_scores)
114
+ x = range(len(tasks))
115
+ width = 0.28 if has_llm else 0.38
116
+ fig, ax = plt.subplots(figsize=(9, 5))
117
+
118
+ b1 = ax.bar([i - width for i in x], llama_base, width, label=f"Llama-3.1-8B (zero-shot baseline)", color=C_LLAMA, zorder=3)
119
+ b2 = ax.bar([i for i in x], rule_based, width, label="Rule-based", color=C_RULE, zorder=3)
120
+ if has_llm:
121
+ b3 = ax.bar([i + width for i in x], llm_scores, width, label=f"{TRAINING_MODEL} (GRPO trained)", color=C_TRAINED, zorder=3)
122
+
123
+ def label_bars(bars):
124
+ for bar in bars:
125
+ h = bar.get_height()
126
+ if h > 0.01:
127
+ ax.text(bar.get_x() + bar.get_width()/2, h + 0.01,
128
+ f"{h:.2f}", ha="center", va="bottom", fontsize=9)
129
+
130
+ label_bars(b1); label_bars(b2)
131
+ if has_llm: label_bars(b3)
132
+
133
+ ax.set_xticks(list(x))
134
+ ax.set_xticklabels(labels)
135
+ ax.set_ylim(0, 1.15)
136
+ ax.set_ylabel("Score (0.01 – 0.99)")
137
+ ax.set_title("Round 1 — Score Comparison")
138
+ ax.legend(loc="upper right")
139
+ ax.yaxis.grid(True, zorder=0)
140
+ ax.set_axisbelow(True)
141
+
142
+ save(plt, "r1_scores_comparison")
143
+
144
+
145
+ # ── Chart 2: R2 scores comparison ────────────────────────────────────────────
146
+
147
+ def chart_r2_comparison(eval_data: dict):
148
+ plt, mpatches = _setup_matplotlib()
149
+
150
+ tasks = ["project_easy", "project_medium", "project_hard"]
151
+ labels = ["Easy (6 sprints)", "Medium (6 sprints)", "Hard (6 sprints)"]
152
+ llama_base = [eval_data.get("r2_llama_baseline", LLAMA_BASELINE_R2).get(t, 0) for t in tasks]
153
+ llm_scores = [eval_data.get("r2_llm", {}).get(t, {}).get("avg_score", 0) for t in tasks]
154
+
155
+ has_llm = any(v > 0 for v in llm_scores)
156
+ x = range(len(tasks))
157
+ width = 0.32 if has_llm else 0.5
158
+ fig, ax = plt.subplots(figsize=(9, 5))
159
+
160
+ b1 = ax.bar([i - width/2 for i in x], llama_base, width, label="Llama-3.1-8B (zero-shot)", color=C_LLAMA, zorder=3)
161
+ if has_llm:
162
+ b2 = ax.bar([i + width/2 for i in x], llm_scores, width, label=f"{TRAINING_MODEL} (GRPO)", color=C_TRAINED, zorder=3)
163
+
164
+ for bars in ([b1] + ([b2] if has_llm else [])):
165
+ for bar in bars:
166
+ h = bar.get_height()
167
+ if h > 0.01:
168
+ ax.text(bar.get_x() + bar.get_width()/2, h + 0.01,
169
+ f"{h:.2f}", ha="center", va="bottom", fontsize=9)
170
+
171
+ ax.set_xticks(list(x))
172
+ ax.set_xticklabels(labels)
173
+ ax.set_ylim(0, 1.15)
174
+ ax.set_ylabel("Project Score (delivery × instruction × health)")
175
+ ax.set_title("Round 2 — Multi-Sprint Project Score")
176
+ ax.legend(loc="upper right")
177
+ ax.yaxis.grid(True, zorder=0)
178
+ ax.set_axisbelow(True)
179
+
180
+ save(plt, "r2_scores_comparison")
181
+
182
+
183
+ # ── Chart 3: Sprint reward curves ─────────────────────────────────────────────
184
+
185
+ def chart_sprint_rewards(eval_data: dict):
186
+ """Per-sprint reward for each R2 scenario (Llama baseline vs trained)."""
187
+ plt, _ = _setup_matplotlib()
188
+ tasks = ["project_easy", "project_medium", "project_hard"]
189
+ colors = [C_EASY, C_MEDIUM, C_HARD]
190
+ labels = ["Easy", "Medium", "Hard"]
191
+
192
+ fig, axes = plt.subplots(1, 3, figsize=(13, 4), sharey=True)
193
+
194
+ for ax, task, color, label in zip(axes, tasks, colors, labels):
195
+ llm_eps = eval_data.get("r2_llm", {}).get(task, {}).get("episodes", [])
196
+
197
+ if llm_eps:
198
+ sr = llm_eps[0].get("sprint_rewards", [])
199
+ if sr:
200
+ ax.plot(range(1, len(sr)+1), sr, "o-",
201
+ color=color, label="Qwen GRPO", linewidth=2, markersize=6)
202
+
203
+ ax.set_xlabel("Sprint")
204
+ ax.set_title(f"{label} Project")
205
+ ax.set_xticks(range(1, 7))
206
+ ax.set_ylim(0, 2.2)
207
+ ax.yaxis.grid(True)
208
+ ax.set_axisbelow(True)
209
+ if ax == axes[0]:
210
+ ax.set_ylabel("Sprint Reward")
211
+ ax.legend(fontsize=8)
212
+
213
+ fig.suptitle("Sprint-by-Sprint Reward — Qwen GRPO Trained", y=1.02, fontsize=13, fontweight="bold")
214
+ save(plt, "sprint_rewards")
215
+
216
+
217
+ # ── Chart 4: Improvement summary (main presentation slide) ───────────────────
218
+
219
+ def chart_improvement_summary(eval_data: dict):
220
+ """Main before/after chart. Uses Llama zero-shot as the 'before' bar."""
221
+ plt, _ = _setup_matplotlib()
222
+
223
+ all_tasks = (
224
+ [f"R1: {t.replace('_sprint','').title()}" for t in ["easy_sprint","medium_sprint","hard_sprint"]] +
225
+ [f"R2: {t.replace('project_','').title()}" for t in ["project_easy","project_medium","project_hard"]]
226
+ )
227
+
228
+ llama_base, trained_scores = [], []
229
+ for t in ["easy_sprint","medium_sprint","hard_sprint"]:
230
+ llama_base.append(eval_data.get("r1_llama_baseline", LLAMA_BASELINE_R1).get(t, 0))
231
+ trained_scores.append(eval_data.get("r1_llm", {}).get(t, {}).get("avg_score", 0))
232
+ for t in ["project_easy","project_medium","project_hard"]:
233
+ llama_base.append(eval_data.get("r2_llama_baseline", LLAMA_BASELINE_R2).get(t, 0))
234
+ trained_scores.append(eval_data.get("r2_llm", {}).get(t, {}).get("avg_score", 0))
235
+
236
+ x = range(len(all_tasks))
237
+ width = 0.36
238
+ fig, ax = plt.subplots(figsize=(11, 5))
239
+
240
+ b1 = ax.bar([i - width/2 for i in x], llama_base, width,
241
+ label="Before: Llama-3.1-8B (zero-shot)", color=C_LLAMA, zorder=3)
242
+ b2 = ax.bar([i + width/2 for i in x], trained_scores, width,
243
+ label=f"After: {TRAINING_MODEL} (GRPO)", color=C_TRAINED, zorder=3)
244
+
245
+ # Delta arrows and labels
246
+ for i, (base, trained) in enumerate(zip(llama_base, trained_scores)):
247
+ if trained > base + 0.01:
248
+ ax.annotate("", xy=(i + width/2, trained + 0.02), xytext=(i - width/2, base + 0.02),
249
+ arrowprops=dict(arrowstyle="->", color="#059669", lw=1.5))
250
+ ax.text(i, max(base, trained) + 0.06, f"+{trained-base:.2f}",
251
+ ha="center", fontsize=8, color="#059669", fontweight="bold")
252
+
253
+ ax.set_xticks(list(x))
254
+ ax.set_xticklabels(all_tasks, rotation=15, ha="right")
255
+ ax.set_ylim(0, 1.25)
256
+ ax.set_ylabel("Score")
257
+ ax.set_title(f"Reward Improvement: Llama Zero-Shot → Qwen2.5-1.5B GRPO Trained")
258
+ ax.legend(loc="upper left")
259
+ ax.yaxis.grid(True, zorder=0)
260
+ ax.set_axisbelow(True)
261
+
262
+ # Divider between R1 and R2
263
+ ax.axvline(x=2.5, color="#9CA3AF", linestyle=":", linewidth=1.5)
264
+ ax.text(1.0, 1.20, "Round 1", ha="center", fontsize=10, color="#6B7280")
265
+ ax.text(4.0, 1.20, "Round 2", ha="center", fontsize=10, color="#6B7280")
266
+
267
+ save(plt, "improvement_summary")
268
+
269
+
270
+ # ── Chart 5: Training loss/reward curve ───────────────────────────────────────
271
+
272
+ def chart_training_curve(trainer_state_path: str):
273
+ plt, _ = _setup_matplotlib()
274
+
275
+ with open(trainer_state_path) as f:
276
+ state = json.load(f)
277
+
278
+ log_history = state.get("log_history", [])
279
+ if not log_history:
280
+ print(" [SKIP] No log_history in trainer_state.json", flush=True)
281
+ return
282
+
283
+ steps, losses, rewards = [], [], []
284
+ for entry in log_history:
285
+ if "loss" in entry:
286
+ steps.append(entry.get("step", 0))
287
+ losses.append(entry["loss"])
288
+ if "reward" in entry:
289
+ rewards.append((entry.get("step", 0), entry["reward"]))
290
+
291
+ fig, axes = plt.subplots(1, 2, figsize=(11, 4))
292
+
293
+ if steps and losses:
294
+ axes[0].plot(steps, losses, color=C_RULE, linewidth=2)
295
+ axes[0].set_xlabel("Training Step")
296
+ axes[0].set_ylabel("Loss")
297
+ axes[0].set_title("GRPO Training Loss (Qwen2.5-1.5B)")
298
+ axes[0].yaxis.grid(True)
299
+
300
+ if rewards:
301
+ rsteps, rvals = zip(*rewards)
302
+ axes[1].plot(rsteps, rvals, color=C_TRAINED, linewidth=2)
303
+ axes[1].set_xlabel("Training Step")
304
+ axes[1].set_ylabel("Mean Reward")
305
+ axes[1].set_title("GRPO Mean Reward per Step")
306
+ axes[1].yaxis.grid(True)
307
+
308
+ fig.suptitle(f"GRPO Training Curves — {TRAINING_MODEL}", fontsize=13, fontweight="bold")
309
+ save(plt, "training_curve")
310
+
311
+
312
+ # ── Demo mode (hardcoded real baselines, placeholder trained scores) ──────────
313
+
314
+ def demo_mode():
315
+ """
316
+ Generate charts using real measured baselines.
317
+ Trained scores are placeholders — replace with real evaluate_r2.py output after on-site training.
318
+ """
319
+ print("[INFO] Demo mode — real Llama baselines, placeholder trained scores", flush=True)
320
+ print(f"[INFO] Training model: {TRAINING_MODEL}", flush=True)
321
+
322
+ # Placeholder trained scores — update after on-site GRPO training
323
+ PLACEHOLDER_R1_TRAINED = {
324
+ "easy_sprint": 0.0, # update after training
325
+ "medium_sprint": 0.0, # update after training
326
+ "hard_sprint": 0.0, # update after training
327
+ }
328
+ PLACEHOLDER_R2_TRAINED = {
329
+ "project_easy": 0.0, # update after training
330
+ "project_medium": 0.0, # update after training
331
+ "project_hard": 0.0, # update after training
332
+ }
333
+
334
+ demo_data = {
335
+ "r1_llama_baseline": LLAMA_BASELINE_R1,
336
+ "r2_llama_baseline": LLAMA_BASELINE_R2,
337
+ "r1_rule_based": {
338
+ "easy_sprint": {"avg_score": 0.92},
339
+ "medium_sprint": {"avg_score": 0.35},
340
+ "hard_sprint": {"avg_score": 0.01},
341
+ },
342
+ "r1_llm": {
343
+ t: {"avg_score": v} for t, v in PLACEHOLDER_R1_TRAINED.items()
344
+ },
345
+ "r2_rule_based": {},
346
+ "r2_llm": {
347
+ t: {"avg_score": v, "episodes": []}
348
+ for t, v in PLACEHOLDER_R2_TRAINED.items()
349
+ },
350
+ "improvement": {
351
+ t: {
352
+ "llama_baseline": LLAMA_BASELINE_R2[t],
353
+ "trained_llm": PLACEHOLDER_R2_TRAINED[t],
354
+ "delta_vs_llama": round(PLACEHOLDER_R2_TRAINED[t] - LLAMA_BASELINE_R2[t], 4),
355
+ }
356
+ for t in ["project_easy", "project_medium", "project_hard"]
357
+ },
358
+ }
359
+ return demo_data
360
+
361
+
362
+ # ── CLI ───────────────────────────────────────────────────────────────────────
363
+
364
+ def main():
365
+ parser = argparse.ArgumentParser(description="Generate result charts for presentation")
366
+ parser.add_argument("--eval", type=str, default=None,
367
+ help="Path to r2_evaluation.json from evaluate_r2.py")
368
+ parser.add_argument("--trainer", type=str, default=None,
369
+ help="Path to trainer_state.json from train_llm.py output")
370
+ parser.add_argument("--demo", action="store_true",
371
+ help="Generate charts with real baselines + placeholder trained scores")
372
+ args = parser.parse_args()
373
+
374
+ try:
375
+ import matplotlib
376
+ except ImportError:
377
+ print("[ERROR] matplotlib not installed. Run: pip install matplotlib", flush=True)
378
+ import sys; sys.exit(1)
379
+
380
+ if args.demo:
381
+ eval_data = demo_mode()
382
+ elif args.eval:
383
+ with open(args.eval) as f:
384
+ eval_data = json.load(f)
385
+ # Back-fill baseline keys if running against old JSON format
386
+ if "r1_llama_baseline" not in eval_data:
387
+ eval_data["r1_llama_baseline"] = LLAMA_BASELINE_R1
388
+ if "r2_llama_baseline" not in eval_data:
389
+ eval_data["r2_llama_baseline"] = LLAMA_BASELINE_R2
390
+ else:
391
+ print("[INFO] No --eval file specified. Using --demo mode.", flush=True)
392
+ eval_data = demo_mode()
393
+
394
+ print(f"\nGenerating charts → {CHARTS_DIR}/", flush=True)
395
+ print(f" Baselines: R1 avg={LLAMA_BASELINE_R1['average']:.4f} R2 avg={LLAMA_BASELINE_R2['average']:.4f}", flush=True)
396
+ print(f" Training model: {TRAINING_MODEL}", flush=True)
397
+
398
+ print(" Chart 1: R1 scores comparison...", flush=True)
399
+ chart_r1_comparison(eval_data)
400
+
401
+ print(" Chart 2: R2 scores comparison...", flush=True)
402
+ chart_r2_comparison(eval_data)
403
+
404
+ print(" Chart 3: Sprint reward curves...", flush=True)
405
+ chart_sprint_rewards(eval_data)
406
+
407
+ print(" Chart 4: Improvement summary...", flush=True)
408
+ chart_improvement_summary(eval_data)
409
+
410
+ if args.trainer and Path(args.trainer).exists():
411
+ print(" Chart 5: Training curve...", flush=True)
412
+ chart_training_curve(args.trainer)
413
+ else:
414
+ print(" Chart 5: Training curve — skipped (no --trainer file provided)", flush=True)
415
+
416
+ print(f"\n✅ All charts saved to {CHARTS_DIR}/", flush=True)
417
+ print(f" Use improvement_summary.png in your HF blog post and pitch slides.", flush=True)
418
+
419
+
420
+ if __name__ == "__main__":
421
+ main()
project_client.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Project Manager — OpenEnv Client (Round 2)
3
+
4
+ Typed HTTP client for the multi-sprint /project/* endpoints.
5
+ Mirrors the R1 SprintEnvClient interface exactly, extended for R2.
6
+
7
+ Usage:
8
+ from project_client import ProjectEnvClient
9
+
10
+ client = ProjectEnvClient(base_url="https://sejal-k-ai-sprint-manager.hf.space")
11
+ obs = client.reset(task_name="project_easy", seed=42)
12
+
13
+ while not obs["done"]:
14
+ result = client.step({"action_type": "assign", "task_id": "T01", "dev_id": "dev1"})
15
+ obs = result.observation
16
+
17
+ client.close()
18
+
19
+ # Or as context manager:
20
+ with ProjectEnvClient() as client:
21
+ obs = client.reset("project_hard")
22
+ while not obs["done"]:
23
+ result = client.step({"action_type": "skip"})
24
+ obs = result.observation
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import requests
30
+ from typing import Any, Optional
31
+
32
+
33
+ # ── Typed step result ──────────────────────────────────────────────────────────
34
+
35
+ class ProjectStepResult:
36
+ """
37
+ Typed result from a project step() call.
38
+
39
+ Extends R1 StepResult with R2-specific fields surfaced as properties.
40
+ """
41
+
42
+ def __init__(self, payload: dict) -> None:
43
+ self.observation: dict = payload["observation"]
44
+ self.reward: float = payload["reward"]
45
+ self.done: bool = payload["done"]
46
+ self.info: dict = payload.get("info", {})
47
+
48
+ # ── R2 convenience accessors ──────────────────────────────────────────────
49
+
50
+ @property
51
+ def current_sprint(self) -> int:
52
+ return self.observation.get("current_sprint", 1)
53
+
54
+ @property
55
+ def current_day(self) -> int:
56
+ return self.observation.get("current_day", 1)
57
+
58
+ @property
59
+ def instruction_queue(self) -> list[dict]:
60
+ """All instructions released up to current_day."""
61
+ return self.observation.get("instruction_queue", [])
62
+
63
+ @property
64
+ def active_instructions(self) -> list[dict]:
65
+ """Released instructions not yet followed."""
66
+ return [i for i in self.instruction_queue if not i.get("followed", False)]
67
+
68
+ @property
69
+ def tech_debt(self) -> list[str]:
70
+ """Task IDs that became tech debt at sprint boundaries."""
71
+ return self.observation.get("tech_debt", [])
72
+
73
+ @property
74
+ def sprint_rewards(self) -> list[float]:
75
+ """Per-sprint reward history."""
76
+ return self.observation.get("sprint_rewards", [])
77
+
78
+ @property
79
+ def instruction_following_score(self) -> float:
80
+ return self.observation.get("instruction_following_score", 1.0)
81
+
82
+ @property
83
+ def tasks_completed(self) -> int:
84
+ return self.observation.get("tasks_completed", 0)
85
+
86
+ @property
87
+ def tasks_missed(self) -> int:
88
+ return self.observation.get("tasks_missed", 0)
89
+
90
+ @property
91
+ def cumulative_reward(self) -> float:
92
+ return self.observation.get("cumulative_reward", 0.0)
93
+
94
+ def __repr__(self) -> str:
95
+ return (
96
+ f"ProjectStepResult("
97
+ f"reward={self.reward:+.3f}, done={self.done}, "
98
+ f"day={self.current_day}/60, sprint={self.current_sprint}/6, "
99
+ f"completed={self.tasks_completed}, "
100
+ f"inst_score={self.instruction_following_score:.2f}, "
101
+ f"debt={len(self.tech_debt)})"
102
+ )
103
+
104
+
105
+ # ── Client ─────────────────────────────────────────────────────────────────────
106
+
107
+ class ProjectEnvClient:
108
+ """
109
+ HTTP client for the R2 multi-sprint Project Manager environment.
110
+
111
+ Wraps the /project/* REST API into a clean typed Python interface.
112
+ Use this in RL training loops (train_llm.py), evaluation scripts
113
+ (evaluate_r2.py), and notebooks.
114
+
115
+ All endpoints mirror the R1 SprintEnvClient interface so training
116
+ code can swap between R1 and R2 with minimal changes.
117
+ """
118
+
119
+ def __init__(
120
+ self,
121
+ base_url: str = "http://localhost:7860",
122
+ timeout: int = 60, # 60s — R2 steps can be slower than R1
123
+ ) -> None:
124
+ self.base_url = base_url.rstrip("/")
125
+ self.timeout = timeout
126
+ self._session = requests.Session()
127
+ self._prefix = "/project"
128
+
129
+ # ── Core API ──────────────────────────────────────────────────────────────
130
+
131
+ def reset(
132
+ self,
133
+ task_name: str = "project_easy",
134
+ seed: Optional[int] = None,
135
+ episode_id: Optional[str] = None,
136
+ ) -> dict:
137
+ """
138
+ Start a new 60-day multi-sprint episode.
139
+
140
+ Args:
141
+ task_name: "project_easy" | "project_medium" | "project_hard"
142
+ seed: Random seed for reproducibility
143
+ episode_id: Optional custom episode identifier
144
+
145
+ Returns:
146
+ Initial observation dict (includes current_sprint, instruction_queue,
147
+ tech_debt, sprint_rewards).
148
+ """
149
+ payload: dict[str, Any] = {"task_name": task_name}
150
+ if seed is not None:
151
+ payload["seed"] = seed
152
+ if episode_id is not None:
153
+ payload["episode_id"] = episode_id
154
+
155
+ resp = self._session.post(
156
+ f"{self.base_url}{self._prefix}/reset",
157
+ json=payload,
158
+ timeout=self.timeout,
159
+ )
160
+ resp.raise_for_status()
161
+ return resp.json()
162
+
163
+ def step(self, action: dict[str, Any]) -> ProjectStepResult:
164
+ """
165
+ Take one action and advance the project by one day.
166
+
167
+ Args:
168
+ action: dict with keys: action_type, task_id, dev_id,
169
+ new_priority, task_ids (for sprint_plan), notes.
170
+ Minimum required: {"action_type": "skip"}
171
+
172
+ Returns:
173
+ ProjectStepResult with observation, reward, done, info,
174
+ plus R2 convenience properties.
175
+ """
176
+ payload = {"action": action}
177
+ resp = self._session.post(
178
+ f"{self.base_url}{self._prefix}/step",
179
+ json=payload,
180
+ timeout=self.timeout,
181
+ )
182
+ resp.raise_for_status()
183
+ return ProjectStepResult(resp.json())
184
+
185
+ def state(self) -> dict:
186
+ """Return the full current internal state snapshot."""
187
+ resp = self._session.get(
188
+ f"{self.base_url}{self._prefix}/state",
189
+ timeout=self.timeout,
190
+ )
191
+ resp.raise_for_status()
192
+ return resp.json()
193
+
194
+ def health(self) -> dict:
195
+ """
196
+ Check R2 server health.
197
+ Returns round=2 and current episode summary.
198
+ """
199
+ resp = self._session.get(
200
+ f"{self.base_url}{self._prefix}/health",
201
+ timeout=self.timeout,
202
+ )
203
+ resp.raise_for_status()
204
+ return resp.json()
205
+
206
+ def list_tasks(self) -> list[dict]:
207
+ """List all available R2 multi-sprint scenarios."""
208
+ resp = self._session.get(
209
+ f"{self.base_url}{self._prefix}/tasks",
210
+ timeout=self.timeout,
211
+ )
212
+ resp.raise_for_status()
213
+ return resp.json()["tasks"]
214
+
215
+ # ── Helpers ───────────────────────────────────────────────────────────────
216
+
217
+ def run_episode(
218
+ self,
219
+ task_name: str,
220
+ policy_fn, # Callable[[dict], dict]
221
+ seed: Optional[int] = None,
222
+ max_steps: int = 60,
223
+ verbose: bool = False,
224
+ ) -> dict:
225
+ """
226
+ Convenience: run a full episode with a policy function.
227
+
228
+ Args:
229
+ task_name: Scenario name
230
+ policy_fn: fn(observation: dict) -> action: dict
231
+ seed: Optional random seed
232
+ max_steps: Safety cap (default 60 = full project)
233
+ verbose: Print step summaries to stdout
234
+
235
+ Returns:
236
+ dict with keys: steps, cumulative_reward, final_score,
237
+ tasks_completed, tasks_missed, instruction_following_score,
238
+ tech_debt, sprint_rewards, done
239
+ """
240
+ obs = self.reset(task_name=task_name, seed=seed)
241
+ total_reward = 0.0
242
+ steps = 0
243
+
244
+ for step_num in range(1, max_steps + 1):
245
+ if obs.get("done", False):
246
+ break
247
+ action = policy_fn(obs)
248
+ result = self.step(action)
249
+ obs = result.observation
250
+ total_reward += result.reward
251
+ steps += 1
252
+
253
+ if verbose:
254
+ print(
255
+ f" day={obs['current_day']-1:02d}/60 "
256
+ f"sprint={obs['current_sprint']}/6 "
257
+ f"done={obs['tasks_completed']} "
258
+ f"missed={obs['tasks_missed']} "
259
+ f"debt={len(obs['tech_debt'])} "
260
+ f"inst={obs['instruction_following_score']:.2f} "
261
+ f"reward={result.reward:+.3f}",
262
+ flush=True,
263
+ )
264
+
265
+ return {
266
+ "steps": steps,
267
+ "cumulative_reward": round(total_reward, 4),
268
+ "tasks_completed": obs.get("tasks_completed", 0),
269
+ "tasks_missed": obs.get("tasks_missed", 0),
270
+ "instruction_following_score": obs.get("instruction_following_score", 0.0),
271
+ "tech_debt": obs.get("tech_debt", []),
272
+ "sprint_rewards": obs.get("sprint_rewards", []),
273
+ "done": obs.get("done", False),
274
+ }
275
+
276
+ # ── Session lifecycle ─────────────────────────────────────────────────────
277
+
278
+ def close(self) -> None:
279
+ """Close the underlying HTTP session."""
280
+ self._session.close()
281
+
282
+ def __enter__(self) -> "ProjectEnvClient":
283
+ return self
284
+
285
+ def __exit__(self, *args: Any) -> None:
286
+ self.close()
287
+
288
+ def __repr__(self) -> str:
289
+ return f"ProjectEnvClient(base_url='{self.base_url}', prefix='{self._prefix}')"
pyproject.toml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "ai-sprint-manager"
3
+ version = "1.0.0"
4
+ description = "RL environment for agile sprint management - OpenEnv"
5
+ requires-python = ">=3.11,<3.13"
6
+ dependencies = [
7
+ "fastapi==0.115.5",
8
+ "uvicorn==0.32.1",
9
+ "pydantic==2.10.3",
10
+ "openai",
11
+ "requests",
12
+ "openenv-core",
13
+ ]
14
+
15
+ [project.scripts]
16
+ server = "server.app:main"
17
+
18
+ [build-system]
19
+ requires = ["setuptools>=68"]
20
+ build-backend = "setuptools.backends.legacy:build"
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["."]
24
+ include = ["sprint_env*", "server*"]
requirements.env ADDED
File without changes
requirements.txt ADDED
Binary file (438 Bytes). View file
 
results/phase1/README.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ base_model: unsloth/qwen2.5-1.5b-instruct-unsloth-bnb-4bit
3
+ library_name: peft
4
+ model_name: phase1
5
+ tags:
6
+ - base_model:adapter:unsloth/qwen2.5-1.5b-instruct-unsloth-bnb-4bit
7
+ - grpo
8
+ - lora
9
+ - transformers
10
+ - trl
11
+ - unsloth
12
+ licence: license
13
+ pipeline_tag: text-generation
14
+ ---
15
+
16
+ # Model Card for phase1
17
+
18
+ This model is a fine-tuned version of [unsloth/qwen2.5-1.5b-instruct-unsloth-bnb-4bit](https://huggingface.co/unsloth/qwen2.5-1.5b-instruct-unsloth-bnb-4bit).
19
+ It has been trained using [TRL](https://github.com/huggingface/trl).
20
+
21
+ ## Quick start
22
+
23
+ ```python
24
+ from transformers import pipeline
25
+
26
+ question = "If you had a time machine, but could only go to the past or the future once and never return, which would you choose and why?"
27
+ generator = pipeline("text-generation", model="None", device="cuda")
28
+ output = generator([{"role": "user", "content": question}], max_new_tokens=128, return_full_text=False)[0]
29
+ print(output["generated_text"])
30
+ ```
31
+
32
+ ## Training procedure
33
+
34
+
35
+
36
+
37
+ This model was trained with GRPO, a method introduced in [DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models](https://huggingface.co/papers/2402.03300).
38
+
39
+ ### Framework versions
40
+
41
+ - PEFT 0.18.1
42
+ - TRL: 0.24.0
43
+ - Transformers: 5.5.0
44
+ - Pytorch: 2.10.0+cu128
45
+ - Datasets: 4.3.0
46
+ - Tokenizers: 0.22.2
47
+
48
+ ## Citations
49
+
50
+ Cite GRPO as:
51
+
52
+ ```bibtex
53
+ @article{shao2024deepseekmath,
54
+ title = {{DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models}},
55
+ author = {Zhihong Shao and Peiyi Wang and Qihao Zhu and Runxin Xu and Junxiao Song and Mingchuan Zhang and Y. K. Li and Y. Wu and Daya Guo},
56
+ year = 2024,
57
+ eprint = {arXiv:2402.03300},
58
+ }
59
+
60
+ ```
61
+
62
+ Cite TRL as:
63
+
64
+ ```bibtex
65
+ @misc{vonwerra2022trl,
66
+ title = {{TRL: Transformer Reinforcement Learning}},
67
+ author = {Leandro von Werra and Younes Belkada and Lewis Tunstall and Edward Beeching and Tristan Thrush and Nathan Lambert and Shengyi Huang and Kashif Rasul and Quentin Gallou{\'e}dec},
68
+ year = 2020,
69
+ journal = {GitHub repository},
70
+ publisher = {GitHub},
71
+ howpublished = {\url{https://github.com/huggingface/trl}}
72
+ }
73
+ ```
results/phase1/adapter_config.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "alora_invocation_tokens": null,
3
+ "alpha_pattern": {},
4
+ "arrow_config": null,
5
+ "auto_mapping": {
6
+ "base_model_class": "Qwen2ForCausalLM",
7
+ "parent_library": "transformers.models.qwen2.modeling_qwen2",
8
+ "unsloth_fixed": true
9
+ },
10
+ "base_model_name_or_path": "unsloth/qwen2.5-1.5b-instruct-unsloth-bnb-4bit",
11
+ "bias": "none",
12
+ "corda_config": null,
13
+ "ensure_weight_tying": false,
14
+ "eva_config": null,
15
+ "exclude_modules": null,
16
+ "fan_in_fan_out": false,
17
+ "inference_mode": true,
18
+ "init_lora_weights": true,
19
+ "layer_replication": null,
20
+ "layers_pattern": null,
21
+ "layers_to_transform": null,
22
+ "loftq_config": {},
23
+ "lora_alpha": 32,
24
+ "lora_bias": false,
25
+ "lora_dropout": 0.05,
26
+ "megatron_config": null,
27
+ "megatron_core": "megatron.core",
28
+ "modules_to_save": null,
29
+ "peft_type": "LORA",
30
+ "peft_version": "0.18.1",
31
+ "qalora_group_size": 16,
32
+ "r": 16,
33
+ "rank_pattern": {},
34
+ "revision": null,
35
+ "target_modules": [
36
+ "k_proj",
37
+ "up_proj",
38
+ "down_proj",
39
+ "o_proj",
40
+ "v_proj",
41
+ "gate_proj",
42
+ "q_proj"
43
+ ],
44
+ "target_parameters": null,
45
+ "task_type": "CAUSAL_LM",
46
+ "trainable_token_indices": null,
47
+ "use_dora": false,
48
+ "use_qalora": false,
49
+ "use_rslora": false
50
+ }
results/phase1/adapter_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6d9ce380266a065d31b8d4962851f4d105daeda1ef56d4e9cd0835e5d5d0644a
3
+ size 73911112
results/phase1/chat_template.jinja ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- if tools %}
2
+ {{- '<|im_start|>system\n' }}
3
+ {%- if messages[0]['role'] == 'system' %}
4
+ {{- messages[0]['content'] }}
5
+ {%- else %}
6
+ {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}
7
+ {%- endif %}
8
+ {{- "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
9
+ {%- for tool in tools %}
10
+ {{- "\n" }}
11
+ {{- tool | tojson }}
12
+ {%- endfor %}
13
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
14
+ {%- else %}
15
+ {%- if messages[0]['role'] == 'system' %}
16
+ {{- '<|im_start|>system\n' + messages[0]['content'] + '<|im_end|>\n' }}
17
+ {%- else %}
18
+ {{- '<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\n' }}
19
+ {%- endif %}
20
+ {%- endif %}
21
+ {%- for message in messages %}
22
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) %}
23
+ {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
24
+ {%- elif message.role == "assistant" %}
25
+ {{- '<|im_start|>' + message.role }}
26
+ {%- if message.content %}
27
+ {{- '\n' + message.content }}
28
+ {%- endif %}
29
+ {%- for tool_call in message.tool_calls %}
30
+ {%- if tool_call.function is defined %}
31
+ {%- set tool_call = tool_call.function %}
32
+ {%- endif %}
33
+ {{- '\n<tool_call>\n{"name": "' }}
34
+ {{- tool_call.name }}
35
+ {{- '", "arguments": ' }}
36
+ {{- tool_call.arguments | tojson }}
37
+ {{- '}\n</tool_call>' }}
38
+ {%- endfor %}
39
+ {{- '<|im_end|>\n' }}
40
+ {%- elif message.role == "tool" %}
41
+ {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") %}
42
+ {{- '<|im_start|>user' }}
43
+ {%- endif %}
44
+ {{- '\n<tool_response>\n' }}
45
+ {{- message.content }}
46
+ {{- '\n</tool_response>' }}
47
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
48
+ {{- '<|im_end|>\n' }}
49
+ {%- endif %}
50
+ {%- endif %}
51
+ {%- endfor %}
52
+ {%- if add_generation_prompt %}
53
+ {{- '<|im_start|>assistant\n' }}
54
+ {%- endif %}
results/phase1/checkpoint-100/README.md ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ base_model: unsloth/qwen2.5-1.5b-instruct-unsloth-bnb-4bit
3
+ library_name: peft
4
+ pipeline_tag: text-generation
5
+ tags:
6
+ - base_model:adapter:unsloth/qwen2.5-1.5b-instruct-unsloth-bnb-4bit
7
+ - grpo
8
+ - lora
9
+ - transformers
10
+ - trl
11
+ - unsloth
12
+ ---
13
+
14
+ # Model Card for Model ID
15
+
16
+ <!-- Provide a quick summary of what the model is/does. -->
17
+
18
+
19
+
20
+ ## Model Details
21
+
22
+ ### Model Description
23
+
24
+ <!-- Provide a longer summary of what this model is. -->
25
+
26
+
27
+
28
+ - **Developed by:** [More Information Needed]
29
+ - **Funded by [optional]:** [More Information Needed]
30
+ - **Shared by [optional]:** [More Information Needed]
31
+ - **Model type:** [More Information Needed]
32
+ - **Language(s) (NLP):** [More Information Needed]
33
+ - **License:** [More Information Needed]
34
+ - **Finetuned from model [optional]:** [More Information Needed]
35
+
36
+ ### Model Sources [optional]
37
+
38
+ <!-- Provide the basic links for the model. -->
39
+
40
+ - **Repository:** [More Information Needed]
41
+ - **Paper [optional]:** [More Information Needed]
42
+ - **Demo [optional]:** [More Information Needed]
43
+
44
+ ## Uses
45
+
46
+ <!-- Address questions around how the model is intended to be used, including the foreseeable users of the model and those affected by the model. -->
47
+
48
+ ### Direct Use
49
+
50
+ <!-- This section is for the model use without fine-tuning or plugging into a larger ecosystem/app. -->
51
+
52
+ [More Information Needed]
53
+
54
+ ### Downstream Use [optional]
55
+
56
+ <!-- This section is for the model use when fine-tuned for a task, or when plugged into a larger ecosystem/app -->
57
+
58
+ [More Information Needed]
59
+
60
+ ### Out-of-Scope Use
61
+
62
+ <!-- This section addresses misuse, malicious use, and uses that the model will not work well for. -->
63
+
64
+ [More Information Needed]
65
+
66
+ ## Bias, Risks, and Limitations
67
+
68
+ <!-- This section is meant to convey both technical and sociotechnical limitations. -->
69
+
70
+ [More Information Needed]
71
+
72
+ ### Recommendations
73
+
74
+ <!-- This section is meant to convey recommendations with respect to the bias, risk, and technical limitations. -->
75
+
76
+ Users (both direct and downstream) should be made aware of the risks, biases and limitations of the model. More information needed for further recommendations.
77
+
78
+ ## How to Get Started with the Model
79
+
80
+ Use the code below to get started with the model.
81
+
82
+ [More Information Needed]
83
+
84
+ ## Training Details
85
+
86
+ ### Training Data
87
+
88
+ <!-- This should link to a Dataset Card, perhaps with a short stub of information on what the training data is all about as well as documentation related to data pre-processing or additional filtering. -->
89
+
90
+ [More Information Needed]
91
+
92
+ ### Training Procedure
93
+
94
+ <!-- This relates heavily to the Technical Specifications. Content here should link to that section when it is relevant to the training procedure. -->
95
+
96
+ #### Preprocessing [optional]
97
+
98
+ [More Information Needed]
99
+
100
+
101
+ #### Training Hyperparameters
102
+
103
+ - **Training regime:** [More Information Needed] <!--fp32, fp16 mixed precision, bf16 mixed precision, bf16 non-mixed precision, fp16 non-mixed precision, fp8 mixed precision -->
104
+
105
+ #### Speeds, Sizes, Times [optional]
106
+
107
+ <!-- This section provides information about throughput, start/end time, checkpoint size if relevant, etc. -->
108
+
109
+ [More Information Needed]
110
+
111
+ ## Evaluation
112
+
113
+ <!-- This section describes the evaluation protocols and provides the results. -->
114
+
115
+ ### Testing Data, Factors & Metrics
116
+
117
+ #### Testing Data
118
+
119
+ <!-- This should link to a Dataset Card if possible. -->
120
+
121
+ [More Information Needed]
122
+
123
+ #### Factors
124
+
125
+ <!-- These are the things the evaluation is disaggregating by, e.g., subpopulations or domains. -->
126
+
127
+ [More Information Needed]
128
+
129
+ #### Metrics
130
+
131
+ <!-- These are the evaluation metrics being used, ideally with a description of why. -->
132
+
133
+ [More Information Needed]
134
+
135
+ ### Results
136
+
137
+ [More Information Needed]
138
+
139
+ #### Summary
140
+
141
+
142
+
143
+ ## Model Examination [optional]
144
+
145
+ <!-- Relevant interpretability work for the model goes here -->
146
+
147
+ [More Information Needed]
148
+
149
+ ## Environmental Impact
150
+
151
+ <!-- Total emissions (in grams of CO2eq) and additional considerations, such as electricity usage, go here. Edit the suggested text below accordingly -->
152
+
153
+ Carbon emissions can be estimated using the [Machine Learning Impact calculator](https://mlco2.github.io/impact#compute) presented in [Lacoste et al. (2019)](https://arxiv.org/abs/1910.09700).
154
+
155
+ - **Hardware Type:** [More Information Needed]
156
+ - **Hours used:** [More Information Needed]
157
+ - **Cloud Provider:** [More Information Needed]
158
+ - **Compute Region:** [More Information Needed]
159
+ - **Carbon Emitted:** [More Information Needed]
160
+
161
+ ## Technical Specifications [optional]
162
+
163
+ ### Model Architecture and Objective
164
+
165
+ [More Information Needed]
166
+
167
+ ### Compute Infrastructure
168
+
169
+ [More Information Needed]
170
+
171
+ #### Hardware
172
+
173
+ [More Information Needed]
174
+
175
+ #### Software
176
+
177
+ [More Information Needed]
178
+
179
+ ## Citation [optional]
180
+
181
+ <!-- If there is a paper or blog post introducing the model, the APA and Bibtex information for that should go in this section. -->
182
+
183
+ **BibTeX:**
184
+
185
+ [More Information Needed]
186
+
187
+ **APA:**
188
+
189
+ [More Information Needed]
190
+
191
+ ## Glossary [optional]
192
+
193
+ <!-- If relevant, include terms and calculations in this section that can help readers understand the model or model card. -->
194
+
195
+ [More Information Needed]
196
+
197
+ ## More Information [optional]
198
+
199
+ [More Information Needed]
200
+
201
+ ## Model Card Authors [optional]
202
+
203
+ [More Information Needed]
204
+
205
+ ## Model Card Contact
206
+
207
+ [More Information Needed]
208
+ ### Framework versions
209
+
210
+ - PEFT 0.18.1
results/phase1/checkpoint-100/adapter_config.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "alora_invocation_tokens": null,
3
+ "alpha_pattern": {},
4
+ "arrow_config": null,
5
+ "auto_mapping": {
6
+ "base_model_class": "Qwen2ForCausalLM",
7
+ "parent_library": "transformers.models.qwen2.modeling_qwen2",
8
+ "unsloth_fixed": true
9
+ },
10
+ "base_model_name_or_path": "unsloth/qwen2.5-1.5b-instruct-unsloth-bnb-4bit",
11
+ "bias": "none",
12
+ "corda_config": null,
13
+ "ensure_weight_tying": false,
14
+ "eva_config": null,
15
+ "exclude_modules": null,
16
+ "fan_in_fan_out": false,
17
+ "inference_mode": true,
18
+ "init_lora_weights": true,
19
+ "layer_replication": null,
20
+ "layers_pattern": null,
21
+ "layers_to_transform": null,
22
+ "loftq_config": {},
23
+ "lora_alpha": 32,
24
+ "lora_bias": false,
25
+ "lora_dropout": 0.05,
26
+ "megatron_config": null,
27
+ "megatron_core": "megatron.core",
28
+ "modules_to_save": null,
29
+ "peft_type": "LORA",
30
+ "peft_version": "0.18.1",
31
+ "qalora_group_size": 16,
32
+ "r": 16,
33
+ "rank_pattern": {},
34
+ "revision": null,
35
+ "target_modules": [
36
+ "k_proj",
37
+ "up_proj",
38
+ "down_proj",
39
+ "o_proj",
40
+ "v_proj",
41
+ "gate_proj",
42
+ "q_proj"
43
+ ],
44
+ "target_parameters": null,
45
+ "task_type": "CAUSAL_LM",
46
+ "trainable_token_indices": null,
47
+ "use_dora": false,
48
+ "use_qalora": false,
49
+ "use_rslora": false
50
+ }
results/phase1/checkpoint-100/adapter_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d430aea910e6d8b7347568b5b75ef11de9ff5b23b6d2010fd456033e1f218b07
3
+ size 73911112
results/phase1/checkpoint-100/chat_template.jinja ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- if tools %}
2
+ {{- '<|im_start|>system\n' }}
3
+ {%- if messages[0]['role'] == 'system' %}
4
+ {{- messages[0]['content'] }}
5
+ {%- else %}
6
+ {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}
7
+ {%- endif %}
8
+ {{- "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
9
+ {%- for tool in tools %}
10
+ {{- "\n" }}
11
+ {{- tool | tojson }}
12
+ {%- endfor %}
13
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
14
+ {%- else %}
15
+ {%- if messages[0]['role'] == 'system' %}
16
+ {{- '<|im_start|>system\n' + messages[0]['content'] + '<|im_end|>\n' }}
17
+ {%- else %}
18
+ {{- '<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\n' }}
19
+ {%- endif %}
20
+ {%- endif %}
21
+ {%- for message in messages %}
22
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) %}
23
+ {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
24
+ {%- elif message.role == "assistant" %}
25
+ {{- '<|im_start|>' + message.role }}
26
+ {%- if message.content %}
27
+ {{- '\n' + message.content }}
28
+ {%- endif %}
29
+ {%- for tool_call in message.tool_calls %}
30
+ {%- if tool_call.function is defined %}
31
+ {%- set tool_call = tool_call.function %}
32
+ {%- endif %}
33
+ {{- '\n<tool_call>\n{"name": "' }}
34
+ {{- tool_call.name }}
35
+ {{- '", "arguments": ' }}
36
+ {{- tool_call.arguments | tojson }}
37
+ {{- '}\n</tool_call>' }}
38
+ {%- endfor %}
39
+ {{- '<|im_end|>\n' }}
40
+ {%- elif message.role == "tool" %}
41
+ {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") %}
42
+ {{- '<|im_start|>user' }}
43
+ {%- endif %}
44
+ {{- '\n<tool_response>\n' }}
45
+ {{- message.content }}
46
+ {{- '\n</tool_response>' }}
47
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
48
+ {{- '<|im_end|>\n' }}
49
+ {%- endif %}
50
+ {%- endif %}
51
+ {%- endfor %}
52
+ {%- if add_generation_prompt %}
53
+ {{- '<|im_start|>assistant\n' }}
54
+ {%- endif %}
results/phase1/checkpoint-100/optimizer.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:13de9f002823fbe63e71b9d1eb8ec6d2c4b8eca35a4ee367099322a459b3b1cb
3
+ size 37969669
results/phase1/checkpoint-100/rng_state.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c1449e60d52f06cceec8589785267b0457345be74a8ca37687099a6addff91e0
3
+ size 14645
results/phase1/checkpoint-100/scaler.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c4393a84a3109995aa1202073b039b12062e3189ed89aa0b94ef0510ba843009
3
+ size 1383
results/phase1/checkpoint-100/scheduler.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7254cd54e69ff9b68992c6e5838d1590c8e033e5a4605990d0b4034185923db6
3
+ size 1465
results/phase1/checkpoint-100/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:af5891a15588546db1ac7f2baf8fa94835a51a85c032c39793a55bb048b47446
3
+ size 11422523