junaid0600 commited on
Commit
7f198e8
Β·
verified Β·
1 Parent(s): 79b5f71

Update training/train_agent.py

Browse files
Files changed (1) hide show
  1. training/train_agent.py +282 -211
training/train_agent.py CHANGED
@@ -1,301 +1,372 @@
1
  """
2
  training/train_agent.py β€” SQL Database Engineer Agent
3
- Unsloth + GRPO training script.
4
- Run on venue GPU (April 25-26) with compute credits.
5
- ENV_URL points to live HF Space environment.
 
6
  """
7
 
8
- import os
9
- import re
10
- import json
11
- import requests
12
- from datasets import Dataset
13
 
14
- # ── Try importing Unsloth (GPU only) ─────────────────────────
 
15
  try:
 
 
 
 
16
  from unsloth import FastLanguageModel
17
  from trl import GRPOTrainer, GRPOConfig
 
18
  UNSLOTH_AVAILABLE = True
19
- except ImportError:
20
- UNSLOTH_AVAILABLE = False
21
- print("⚠️ Unsloth not available. Run: pip install unsloth trl")
 
 
22
 
23
- # ─────────────────────────────────────────────
24
- # CONFIG
25
- # ─────────────────────────────────────────────
26
 
 
27
  ENV_URL = os.getenv("ENV_URL", "https://junaid0600-sql-db-engineer-agent.hf.space")
28
  HF_TOKEN = os.getenv("HF_TOKEN", "")
29
- MODEL_NAME = os.getenv("MODEL_NAME", "unsloth/Qwen2.5-7B-Instruct")
30
  OUTPUT_DIR = os.getenv("OUTPUT_DIR", "./sdea-trained")
 
31
 
32
- SYSTEM_PROMPT = """You are a senior database engineer.
33
- Given the current database state with slow queries, choose the BEST action to improve performance.
34
- Think step by step:
35
- 1. If you haven't inspected queries yet β†’ use inspect_query
36
- 2. If you haven't analyzed indexes β†’ use analyze_indexes
37
- 3. If you know which index is missing β†’ use create_index
38
- 4. If query can be rewritten better β†’ use rewrite_query
39
- 5. If table is huge (1M+ rows) β†’ use partition_table
40
- 6. When performance target is reached β†’ use submit_report
41
 
42
- Respond with JSON only β€” no explanation, no markdown:
43
- {"action_type": "...", "payload": {...}}"""
 
44
 
 
 
45
 
46
- # ─────────────────────────────────────────────
47
- # REWARD FUNCTION (calls live HF Space)
48
- # ─────────────────────────────────────────────
49
 
50
- def parse_action(text: str) -> dict:
51
- """Safely parse model output into an action dict.
52
-
53
- Supports markdown ```json blocks and returns a fallback action on failure.
54
- """
55
- if not isinstance(text, str):
56
- text = str(text)
57
 
58
- candidate = text.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- fenced = re.search(r"```(?:json)?\s*(\{.*\})\s*```", candidate, re.S | re.I)
61
- if fenced:
62
- candidate = fenced.group(1).strip()
63
- elif candidate.startswith("```") and candidate.endswith("```"):
64
- candidate = candidate[3:-3].strip()
65
 
 
 
 
66
  try:
67
- action = json.loads(candidate)
68
- if isinstance(action, dict):
69
- action["_parse_fallback"] = False
70
- return action
71
- except json.JSONDecodeError:
 
 
 
 
 
72
  pass
 
73
 
74
- start = candidate.find("{")
75
- end = candidate.rfind("}")
76
- if start != -1 and end != -1 and end > start:
77
- try:
78
- action = json.loads(candidate[start:end + 1])
79
- if isinstance(action, dict):
80
- action["_parse_fallback"] = False
81
- return action
82
- except json.JSONDecodeError:
83
- pass
84
-
85
- return {
86
- "action_type": "inspect_query",
87
- "payload": {"query_id": "q1"},
88
- "_parse_fallback": True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  }
 
90
 
 
 
91
 
92
- def compute_reward(action_dict: dict, obs: dict, env_response: dict) -> float:
93
- """Apply shaping rules on top of environment reward.
 
 
 
 
 
 
94
 
95
- This strengthens the signal, penalizes repeated inspection, empty payloads,
96
- and bounds final reward between 0.0 and 1.0.
97
- """
98
- base_score = env_response.get("score", 0.0)
99
- try:
100
- base_score = float(base_score)
101
- except (TypeError, ValueError):
102
- base_score = 0.0
103
-
104
- if action_dict.get("_parse_fallback", False):
105
- return 0.0
106
-
107
- action_type = action_dict.get("action_type", "")
108
- payload = action_dict.get("payload") or {}
109
-
110
- if action_type == "inspect_query":
111
- query_id = payload.get("query_id")
112
- inspected_queries = []
113
- if isinstance(obs, dict):
114
- inspected_queries = obs.get("inspected_queries") or obs.get("inspected_query_ids") or []
115
-
116
- if isinstance(inspected_queries, list) and query_id in inspected_queries:
117
- return max(0.0, base_score - 0.2)
118
-
119
- if not payload or (isinstance(payload, dict) and len(payload) == 0):
120
- return max(0.0, base_score - 0.3)
121
-
122
- valid_actions = {
123
- "inspect_query",
124
- "analyze_indexes",
125
- "create_index",
126
- "fix_query",
127
- "optimize_query",
128
- }
129
- if action_type not in valid_actions:
130
- return 0.0
131
 
132
- if base_score > 0.9:
133
- return min(1.0, base_score + 0.1)
134
 
135
- return base_score
136
 
137
 
 
138
  def reward_fn(prompts, completions, **kwargs):
139
  """
140
- GRPO reward function β€” calls /step on live environment.
141
- Returns list of float rewards, one per completion.
 
 
 
 
 
 
 
142
  """
143
  rewards = []
144
- task_ids = kwargs.get("task_ids", ["easy_s001"] * len(prompts))
145
 
146
  for i, (prompt, completion) in enumerate(zip(prompts, completions)):
147
  try:
148
- text = completion[0]["content"] if isinstance(completion, list) else str(completion)
 
 
 
 
 
 
 
 
 
149
  action = parse_action(text)
150
 
151
- task_id = task_ids[i] if i < len(task_ids) else "easy_s001"
152
- requests.post(
153
- f"{ENV_URL}/reset",
154
- json={"task_id": task_id},
155
- timeout=15,
156
- )
157
-
158
- resp = requests.post(
159
- f"{ENV_URL}/step",
160
- json=action,
161
- timeout=15,
162
- )
163
- env_response = resp.json()
164
- score = compute_reward(
165
- action_dict=action,
166
- obs=env_response.get("observation", {}),
167
- env_response=env_response,
168
- )
169
- score = max(0.0, min(1.0, float(score)))
170
  rewards.append(score)
171
 
 
 
 
 
 
 
172
  except Exception as e:
173
- print(f"Reward fn error: {e}")
174
- rewards.append(0.0)
175
 
176
  return rewards
177
 
178
 
179
- # ─────────────────────────────────────────────
180
- # BUILD TRAINING DATASET
181
- # ─────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
- def build_dataset():
184
- """Build training examples from all 15 Round 2 scenarios."""
185
- scenarios = []
186
 
187
- # Load all scenario files
188
- for fname in ["dataset/easy_scenarios.json",
189
- "dataset/medium_scenarios.json",
190
- "dataset/hard_scenarios.json"]:
191
- try:
192
- with open(fname) as f:
193
- scenarios.extend(json.load(f))
194
- except FileNotFoundError:
195
- print(f"{fname} not found, skipping")
196
 
197
- if not scenarios:
198
- # Fallback: fetch from live environment
199
- resp = requests.get(f"{ENV_URL}/tasks", timeout=15)
200
- tasks = resp.json().get("tasks", [])
201
- scenarios = [{"id": t["id"], "description": t["description"]} for t in tasks]
202
 
203
- examples = []
204
- for s in scenarios:
205
- prompt = f"""{SYSTEM_PROMPT}
 
206
 
207
- Current Database State:
208
- - Scenario: {s.get('id', 'unknown')}
209
- - Description: {s.get('description', '')}
210
- - Tables: {json.dumps(s.get('tables', []))}
211
- - Slow Queries: {json.dumps(s.get('slow_queries', []))}
212
- - Performance Score: {s.get('performance_score_baseline', 0)} / 100
213
- - Target Score: {s.get('target_score', 85)}
214
 
215
- What is your next action?"""
 
 
216
 
217
- examples.append({
218
- "prompt": prompt,
219
- "task_id": s.get("id", "easy_s001"),
220
- })
 
221
 
222
- print(f"Built {len(examples)} training examples")
223
- return Dataset.from_list(examples)
 
 
 
 
 
 
 
224
 
 
 
 
 
225
 
226
- # ─────────────────────────────────────────────
227
- # MAIN TRAINING
228
- # ─────────────────────────────────────────────
229
 
 
230
  def train():
231
- if not UNSLOTH_AVAILABLE:
232
- print("Cannot train β€” Unsloth not installed")
233
- print("Run: pip install unsloth trl transformers datasets accelerate")
234
- return
235
-
236
- print(f"πŸš€ Loading model: {MODEL_NAME}")
237
- print(f"🌐 Environment: {ENV_URL}")
238
 
239
- # Load model with Unsloth 4-bit quantization
240
  model, tokenizer = FastLanguageModel.from_pretrained(
241
  model_name = MODEL_NAME,
242
- max_seq_length = 4096,
243
  load_in_4bit = True,
244
  token = HF_TOKEN or None,
245
  )
246
-
247
- # Add LoRA adapters
248
  model = FastLanguageModel.get_peft_model(
249
  model,
250
- r = 16,
251
- lora_alpha = 16,
252
- target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
253
- "gate_proj", "up_proj", "down_proj"],
254
- lora_dropout = 0,
255
- bias = "none",
256
- use_gradient_checkpointing = "unsloth",
257
  )
 
258
 
259
- # Build dataset
260
  dataset = build_dataset()
261
 
262
- # GRPO config
263
  config = GRPOConfig(
264
  output_dir = OUTPUT_DIR,
265
- num_train_epochs = 3,
266
- per_device_train_batch_size = 2,
267
- gradient_accumulation_steps = 8,
268
- learning_rate = 5e-5,
269
- max_completion_length = 256,
270
- num_generations = 4,
271
- logging_steps = 10,
272
- save_steps = 50,
 
 
273
  warmup_ratio = 0.1,
274
  report_to = "none",
 
275
  )
276
 
277
- # Reward function wrapper
278
- def reward_wrapper(prompts, completions, **kwargs):
279
- task_ids = [ex.get("task_id", "easy_s001") for ex in kwargs.get("batch", [])]
280
- return reward_fn(prompts, completions, task_ids=task_ids)
281
-
282
- # Train
283
  trainer = GRPOTrainer(
284
- model = model,
285
- tokenizer = tokenizer,
286
- reward_funcs = reward_wrapper,
287
- args = config,
288
  train_dataset = dataset,
289
  )
290
 
291
- print("πŸ‹οΈ Starting GRPO training...")
 
292
  trainer.train()
 
293
 
294
- # Save
295
  model.save_pretrained(f"{OUTPUT_DIR}/final")
296
  tokenizer.save_pretrained(f"{OUTPUT_DIR}/final")
297
- print(f"Training complete. Model saved to {OUTPUT_DIR}/final")
 
 
 
 
 
 
 
298
 
299
 
300
  if __name__ == "__main__":
301
- train()
 
1
  """
2
  training/train_agent.py β€” SQL Database Engineer Agent
3
+ FIXED: Uses local DatabaseSimulator for rewards (no HF Space calls)
4
+ - No shared singleton state
5
+ - Real delta rewards (0.0 for wrong actions, 40-75pts for correct)
6
+ - Clear reward difference teaches model to prefer create_index over inspect_query
7
  """
8
 
9
+ import os, json, sys, time
10
+ from pathlib import Path
 
 
 
11
 
12
+ # ── GPU check ─────────────────────────────────────────────────
13
+ UNSLOTH_AVAILABLE = False
14
  try:
15
+ import torch
16
+ if not torch.cuda.is_available():
17
+ print("❌ No GPU. Unsloth requires CUDA GPU.")
18
+ sys.exit(1)
19
  from unsloth import FastLanguageModel
20
  from trl import GRPOTrainer, GRPOConfig
21
+ from datasets import Dataset
22
  UNSLOTH_AVAILABLE = True
23
+ print(f"βœ… GPU: {torch.cuda.get_device_name(0)}")
24
+ print(f"βœ… VRAM: {torch.cuda.get_device_properties(0).total_memory/1e9:.1f}GB")
25
+ except ImportError as e:
26
+ print(f"❌ {e}\nRun: pip install unsloth trl transformers datasets accelerate")
27
+ sys.exit(1)
28
 
29
+ # Add project root so we can import DatabaseSimulator
30
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
31
+ from env.db_simulator import DatabaseSimulator
32
 
33
+ # ── Config ────────────────────────────────────────────────────
34
  ENV_URL = os.getenv("ENV_URL", "https://junaid0600-sql-db-engineer-agent.hf.space")
35
  HF_TOKEN = os.getenv("HF_TOKEN", "")
36
+ MODEL_NAME = os.getenv("MODEL_NAME", "unsloth/Qwen2.5-1.5B-Instruct")
37
  OUTPUT_DIR = os.getenv("OUTPUT_DIR", "./sdea-trained")
38
+ MAX_STEPS = int(os.getenv("MAX_STEPS", "100"))
39
 
40
+ print(f"\n[CONFIG] Model: {MODEL_NAME}")
41
+ print(f"[CONFIG] Max steps: {MAX_STEPS}")
42
+ print(f"[CONFIG] Output: {OUTPUT_DIR}\n")
 
 
 
 
 
 
43
 
44
+ # ── System prompt ─────────────────────────────────────────────
45
+ SYSTEM_PROMPT = """You are a senior database engineer fixing slow database queries.
46
+ You will see slow queries and table structures. Choose the BEST action.
47
 
48
+ Key insight: create_index on the RIGHT columns fixes slow queries.
49
+ Wrong columns = no improvement. Right columns = massive improvement.
50
 
51
+ Respond with ONLY valid JSON:
52
+ {"action_type": "create_index", "payload": {"table": "TABLE_NAME", "columns": ["COL1", "COL2"]}}
 
53
 
54
+ Available actions: inspect_query, analyze_indexes, create_index, rewrite_query, analyze_statistics, submit_report"""
 
 
 
 
 
 
55
 
56
+ # ── Load all 15 scenarios ─────────────────────────────────────
57
+ def load_all_scenarios() -> list:
58
+ scenarios = []
59
+ base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
60
+ for fname in ["easy_scenarios.json", "medium_scenarios.json", "hard_scenarios.json"]:
61
+ path = os.path.join(base, "dataset", fname)
62
+ try:
63
+ with open(path) as f:
64
+ data = json.load(f)
65
+ scenarios.extend(data)
66
+ print(f" βœ… Loaded {len(data)} from {fname}")
67
+ except FileNotFoundError:
68
+ print(f" ⚠️ {fname} not found")
69
+ print(f" Total: {len(scenarios)} scenarios\n")
70
+ return scenarios
71
 
72
+ ALL_SCENARIOS = load_all_scenarios()
 
 
 
 
73
 
74
+ # ── Parse LLM output ─────────────────────────────────────────
75
+ def parse_action(text: str) -> dict:
76
+ """Parse LLM output into action dict."""
77
  try:
78
+ text = text.strip()
79
+ for marker in ["```json", "```"]:
80
+ if marker in text:
81
+ parts = text.split(marker)
82
+ text = parts[1] if len(parts) > 1 else parts[0]
83
+ text = text.strip()
84
+ data = json.loads(text)
85
+ if "action_type" in data and "payload" in data:
86
+ return data
87
+ except Exception:
88
  pass
89
+ return None # None = invalid JSON = penalized
90
 
91
+
92
+ # ── LOCAL reward function using DatabaseSimulator ─────────────
93
+ def compute_local_reward(action: dict, scenario: dict) -> tuple:
94
+ """
95
+ Compute reward LOCALLY using DatabaseSimulator.
96
+ No HF Space calls. No shared state. Clean every time.
97
+
98
+ Returns (reward_score, db_delta, milestone_bonus)
99
+ """
100
+ sim = DatabaseSimulator(scenario)
101
+ baseline = sim.get_performance_score()
102
+ hints = scenario.get("missing_index_hints", [])
103
+
104
+ action_type = action.get("action_type", "")
105
+ payload = action.get("payload", {})
106
+
107
+ # Apply action to simulator
108
+ if action_type == "create_index":
109
+ result = sim.apply_action("create_index", payload)
110
+ delta = result.get("delta", 0.0)
111
+
112
+ elif action_type == "inspect_query":
113
+ # Investigation β€” small reward, no DB change
114
+ delta = 0.0
115
+
116
+ elif action_type == "analyze_indexes":
117
+ delta = 0.0
118
+
119
+ elif action_type == "rewrite_query":
120
+ result = sim.apply_action("rewrite_query", payload)
121
+ delta = result.get("delta", 0.0)
122
+
123
+ elif action_type == "analyze_statistics":
124
+ result = sim.apply_action("analyze_statistics", payload)
125
+ delta = result.get("delta", 0.0)
126
+
127
+ elif action_type == "partition_table":
128
+ result = sim.apply_action("partition_table", payload)
129
+ delta = result.get("delta", 0.0)
130
+
131
+ elif action_type == "submit_report":
132
+ # Terminal: score based on how much DB improved so far
133
+ final = sim.get_performance_score()
134
+ improvement = max(0, final - baseline)
135
+ delta = improvement
136
+
137
+ else:
138
+ delta = -5.0 # Unknown action = penalty
139
+
140
+ final_score = sim.get_performance_score()
141
+ improvement = max(0.0, final_score - baseline)
142
+ max_possible = max(1.0, 100.0 - baseline)
143
+
144
+ # ── Reward components ─────────────────────────────────────
145
+ # 1. Step reward β€” different per action type
146
+ step_rewards = {
147
+ "inspect_query": 0.10,
148
+ "analyze_indexes": 0.10,
149
+ "create_index": 0.15,
150
+ "rewrite_query": 0.20,
151
+ "analyze_statistics":0.08,
152
+ "partition_table": 0.15,
153
+ "submit_report": 0.05,
154
  }
155
+ step_r = step_rewards.get(action_type, 0.001)
156
 
157
+ # 2. Delta reward β€” proportional to actual improvement
158
+ delta_r = min(0.70, (improvement / max_possible) * 0.70)
159
 
160
+ # 3. Milestone bonus β€” one-time for big improvements
161
+ milestone_r = 0.0
162
+ if improvement / max_possible >= 0.75:
163
+ milestone_r = 0.40
164
+ elif improvement / max_possible >= 0.50:
165
+ milestone_r = 0.25
166
+ elif improvement / max_possible >= 0.25:
167
+ milestone_r = 0.15
168
 
169
+ # 4. Penalty for wrong index (delta=0 on create_index)
170
+ wrong_index_pen = 0.0
171
+ if action_type == "create_index" and delta <= 0.0:
172
+ wrong_index_pen = -0.15 # created useless index
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
+ total = step_r + delta_r + milestone_r + wrong_index_pen
175
+ total = max(0.001, min(0.999, total))
176
 
177
+ return total, improvement, milestone_r
178
 
179
 
180
+ # ── GRPO reward function ──────────────────────────────────────
181
  def reward_fn(prompts, completions, **kwargs):
182
  """
183
+ LOCAL reward β€” no HTTP calls, no shared state.
184
+ Each completion gets its own fresh DatabaseSimulator.
185
+
186
+ Reward differences:
187
+ inspect_query (always): 0.10 + 0.0 = 0.10
188
+ create_index (wrong col): 0.15 - 0.15 = 0.001
189
+ create_index (right col): 0.15 + 0.60 = 0.75+
190
+
191
+ GRPO will learn: right create_index >> inspect_query >> wrong create_index
192
  """
193
  rewards = []
 
194
 
195
  for i, (prompt, completion) in enumerate(zip(prompts, completions)):
196
  try:
197
+ # Get text
198
+ if isinstance(completion, list):
199
+ text = completion[0].get("content", "") if completion else ""
200
+ else:
201
+ text = str(completion)
202
+
203
+ # Pick scenario (rotate through all)
204
+ scenario = ALL_SCENARIOS[i % len(ALL_SCENARIOS)]
205
+
206
+ # Parse action
207
  action = parse_action(text)
208
 
209
+ if action is None:
210
+ # Invalid JSON output β€” penalize
211
+ rewards.append(0.001)
212
+ print(f" [REWARD] scenario={scenario['id']} | "
213
+ f"INVALID JSON | score=0.001")
214
+ continue
215
+
216
+ # Compute reward locally
217
+ score, improvement, milestone = compute_local_reward(action, scenario)
 
 
 
 
 
 
 
 
 
 
218
  rewards.append(score)
219
 
220
+ print(f" [REWARD] scenario={scenario['id']} | "
221
+ f"action={action.get('action_type')} | "
222
+ f"improvement=+{improvement:.1f}pts | "
223
+ f"milestone=+{milestone:.2f} | "
224
+ f"score={score:.3f}")
225
+
226
  except Exception as e:
227
+ print(f" [REWARD] Error: {e}")
228
+ rewards.append(0.001)
229
 
230
  return rewards
231
 
232
 
233
+ # ── Build dataset ─────────────────────────────────────────────
234
+ def build_dataset() -> Dataset:
235
+ examples = []
236
+ for i, s in enumerate(ALL_SCENARIOS):
237
+ tables_str = json.dumps(s.get("tables", []))
238
+ queries_str = json.dumps(s.get("slow_queries", []))
239
+ hints_str = json.dumps(s.get("missing_index_hints", []))
240
+
241
+ prompt = (
242
+ f"{SYSTEM_PROMPT}\n\n"
243
+ f"=== DATABASE STATE ===\n"
244
+ f"Scenario: {s['id']}\n"
245
+ f"Description: {s.get('description','')}\n"
246
+ f"Tables: {tables_str}\n"
247
+ f"Slow Queries: {queries_str}\n"
248
+ f"Missing Index Hints: {hints_str}\n"
249
+ f"Performance: {s.get('performance_score_baseline',0)}/100 "
250
+ f"β†’ Target: {s.get('target_score',85)}/100\n\n"
251
+ f"What action should you take? Output JSON only:"
252
+ )
253
+ examples.append({
254
+ "prompt": prompt,
255
+ "scenario_id": s["id"],
256
+ })
257
 
258
+ print(f" βœ… Dataset: {len(examples)} examples")
259
+ return Dataset.from_list(examples)
 
260
 
 
 
 
 
 
 
 
 
 
261
 
262
+ # ── Generate plots ────────────────────────────────────────────
263
+ def generate_plots(trainer):
264
+ import matplotlib
265
+ matplotlib.use("Agg")
266
+ import matplotlib.pyplot as plt
267
 
268
+ logs = [l for l in trainer.state.log_history if "loss" in l]
269
+ if not logs:
270
+ print("⚠️ No logs for plotting")
271
+ return
272
 
273
+ steps = [l.get("step", i) for i,l in enumerate(logs)]
274
+ losses = [l.get("loss", 0) for l in logs]
 
 
 
 
 
275
 
276
+ fig, ax = plt.subplots(1, 1, figsize=(8, 4))
277
+ fig.suptitle("GRPO Training β€” SQL Database Engineer Agent",
278
+ fontsize=13, fontweight="bold")
279
 
280
+ ax.plot(steps, losses, "b-o", lw=2, ms=4)
281
+ ax.set_xlabel("Training Step")
282
+ ax.set_ylabel("Loss")
283
+ ax.set_title("Training Loss (↓ = model learning DBA pattern)")
284
+ ax.grid(True, alpha=0.3)
285
 
286
+ if losses:
287
+ ax.annotate(f"Start: {losses[0]:.4f}",
288
+ xy=(steps[0], losses[0]),
289
+ xytext=(steps[0]+1, losses[0]*1.1),
290
+ fontsize=9, color="red")
291
+ ax.annotate(f"End: {losses[-1]:.4f}",
292
+ xy=(steps[-1], losses[-1]),
293
+ xytext=(steps[-1]-5, losses[-1]*1.1),
294
+ fontsize=9, color="green")
295
 
296
+ plt.tight_layout()
297
+ plt.savefig("loss_curve.png", dpi=150, bbox_inches="tight")
298
+ print("βœ… loss_curve.png saved")
299
+ print(f" Loss: {losses[0]:.4f} β†’ {losses[-1]:.4f}")
300
 
 
 
 
301
 
302
+ # ── Main ──────────────────────────────────────────────────────
303
  def train():
304
+ if not ALL_SCENARIOS:
305
+ print("❌ No scenarios found. Check dataset/ folder.")
306
+ sys.exit(1)
 
 
 
 
307
 
308
+ print(f"⏳ Loading {MODEL_NAME}...")
309
  model, tokenizer = FastLanguageModel.from_pretrained(
310
  model_name = MODEL_NAME,
311
+ max_seq_length = 2048,
312
  load_in_4bit = True,
313
  token = HF_TOKEN or None,
314
  )
 
 
315
  model = FastLanguageModel.get_peft_model(
316
  model,
317
+ r=16, lora_alpha=16,
318
+ target_modules=["q_proj","k_proj","v_proj","o_proj",
319
+ "gate_proj","up_proj","down_proj"],
320
+ lora_dropout=0, bias="none",
321
+ use_gradient_checkpointing="unsloth",
322
+ random_state=42,
 
323
  )
324
+ print("βœ… Model ready\n")
325
 
 
326
  dataset = build_dataset()
327
 
 
328
  config = GRPOConfig(
329
  output_dir = OUTPUT_DIR,
330
+ max_steps = MAX_STEPS,
331
+ per_device_train_batch_size = 1,
332
+ gradient_accumulation_steps = 4,
333
+ learning_rate = 5e-6,
334
+ max_completion_length = 150,
335
+ num_generations = 4, # compare 4 actions per step
336
+ temperature = 1.0,
337
+ logging_steps = 1,
338
+ save_steps = 25,
339
+ save_total_limit = 3,
340
  warmup_ratio = 0.1,
341
  report_to = "none",
342
+ remove_unused_columns = False,
343
  )
344
 
 
 
 
 
 
 
345
  trainer = GRPOTrainer(
346
+ model = model,
347
+ tokenizer = tokenizer,
348
+ reward_funcs = reward_fn,
349
+ args = config,
350
  train_dataset = dataset,
351
  )
352
 
353
+ print(f"πŸ‹οΈ GRPO training β€” {MAX_STEPS} steps")
354
+ print("Watch for: improvement > 0 and score > 0.5 on create_index\n")
355
  trainer.train()
356
+ print("\nβœ… Training complete!")
357
 
358
+ Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
359
  model.save_pretrained(f"{OUTPUT_DIR}/final")
360
  tokenizer.save_pretrained(f"{OUTPUT_DIR}/final")
361
+ print(f"βœ… Saved to {OUTPUT_DIR}/final")
362
+
363
+ generate_plots(trainer)
364
+
365
+ print("\n" + "="*50)
366
+ print("NEXT: python training/evaluate_agent.py")
367
+ print("THEN: git add loss_curve.png reward_curve.png")
368
+ print("="*50)
369
 
370
 
371
  if __name__ == "__main__":
372
+ train()