junaid0600 commited on
Commit
86abfc1
Β·
verified Β·
1 Parent(s): 987f2db

Update training/train_agent.py

Browse files
Files changed (1) hide show
  1. training/train_agent.py +227 -259
training/train_agent.py CHANGED
@@ -1,92 +1,98 @@
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
 
6
- FREE T4 (Colab): MODEL_NAME=unsloth/Qwen2.5-1.5B-Instruct (default)
7
- VENUE A100: set ENV_VAR MODEL_NAME=unsloth/Qwen2.5-7B-Instruct
8
  """
9
 
10
- import os
11
- import json
12
- import requests
13
- import sys
14
  from pathlib import Path
15
 
16
- # ── Try importing Unsloth (GPU only) ─────────────────────────
 
17
  try:
 
 
 
 
18
  from unsloth import FastLanguageModel
19
  from trl import GRPOTrainer, GRPOConfig
20
- import torch
21
  UNSLOTH_AVAILABLE = True
22
- print("Unsloth + TRL loaded successfully")
23
- except ImportError:
24
- UNSLOTH_AVAILABLE = False
25
- print("Unsloth not available. Run: pip install unsloth trl")
26
-
27
- # ─────────────────────────────────────────────
28
- # CONFIG β€” change MODEL_NAME via env var at venue
29
- # ─────────────────────────────────────────────
30
-
31
  ENV_URL = os.getenv("ENV_URL", "https://junaid0600-sql-db-engineer-agent.hf.space")
32
  HF_TOKEN = os.getenv("HF_TOKEN", "")
33
- MODEL_NAME = os.getenv("MODEL_NAME", "unsloth/Qwen2.5-1.5B-Instruct") # 1.5B for free T4
34
  OUTPUT_DIR = os.getenv("OUTPUT_DIR", "./sdea-trained")
35
- MAX_STEPS = int(os.getenv("MAX_STEPS", "100")) # increase to 300+ at venue
36
-
37
- print(f"[CONFIG] Model: {MODEL_NAME}")
38
- print(f"[CONFIG] Output: {OUTPUT_DIR}")
39
- print(f"[CONFIG] Max steps: {MAX_STEPS}")
40
- print(f"[CONFIG] ENV URL: {ENV_URL}")
41
 
42
- # ─────────────────────────────────────────────
43
- # SYSTEM PROMPT
44
- # ─────────────────────────────────────────────
 
45
 
 
46
  SYSTEM_PROMPT = """You are a senior database engineer.
47
- Given the current database state with slow queries, choose the BEST action to improve performance.
48
- Think step by step:
49
- 1. If you have not inspected queries yet -> use inspect_query
50
- 2. If you have not analyzed indexes -> use analyze_indexes
51
- 3. If you know which index is missing -> use create_index
52
- 4. If query can be rewritten better -> use rewrite_query
53
- 5. If table is huge (1M+ rows) -> use partition_table
54
- 6. When performance target is reached -> use submit_report
55
-
56
- Respond with JSON only β€” no explanation, no markdown:
57
- {"action_type": "...", "payload": {...}}"""
58
-
59
-
60
- # ─────────────────────────────────────────────
61
- # REWARD FUNCTION (calls live HF Space)
62
- # ─────────────────────────────────────────────
63
-
 
 
 
64
  def parse_action(text: str) -> dict:
65
- """Parse LLM output into action dict. Returns safe fallback on failure."""
66
  try:
67
  text = text.strip()
68
- if "```" in text:
69
- text = text.split("```")[1]
70
- if text.startswith("json"):
71
- text = text[4:]
 
72
  text = text.strip()
73
  data = json.loads(text)
74
- if "action_type" in data:
75
  return data
76
  except Exception:
77
  pass
78
- # Safe fallback β€” never crashes
79
  return {"action_type": "inspect_query", "payload": {"query_id": "q1"}}
80
 
81
 
 
82
  def reward_fn(prompts, completions, **kwargs):
83
  """
84
- GRPO reward function β€” calls /grader on live environment.
85
- Returns list of float rewards, one per completion.
86
- Score always between 0.001 and 0.999.
 
 
 
 
87
  """
88
  rewards = []
89
- task_ids = kwargs.get("task_ids", ["easy_s001"] * len(prompts))
90
 
91
  for i, (prompt, completion) in enumerate(zip(prompts, completions)):
92
  try:
@@ -96,28 +102,62 @@ def reward_fn(prompts, completions, **kwargs):
96
  else:
97
  text = str(completion)
98
 
99
- # Parse into action
 
 
 
100
  action = parse_action(text)
101
- task_id = task_ids[i] if i < len(task_ids) else "easy_s001"
102
-
103
- # Call grader endpoint
104
- resp = requests.post(
105
- f"{ENV_URL}/grader",
106
- json={"task_id": task_id, "action": action},
107
- timeout=15,
108
- headers={"Content-Type": "application/json"}
109
- )
110
- if resp.status_code == 200:
111
- score = resp.json().get("score", 0.001)
112
- score = max(0.001, min(0.999, float(score)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  else:
114
- score = 0.001
115
 
116
- rewards.append(score)
117
- print(f" [REWARD] task={task_id} | action={action.get('action_type')} | score={score:.3f}")
 
 
 
 
 
 
 
118
 
119
- except json.JSONDecodeError:
120
- rewards.append(0.001)
121
  except Exception as e:
122
  print(f" [REWARD] Error: {e}")
123
  rewards.append(0.001)
@@ -125,198 +165,149 @@ def reward_fn(prompts, completions, **kwargs):
125
  return rewards
126
 
127
 
128
- # ─────────────────────────────────────────────
129
- # BUILD TRAINING DATASET
130
- # ─────────────────────────────────────────────
131
-
132
  def build_dataset():
133
- """Build training examples from all 15 Round 2 scenarios."""
134
  scenarios = []
135
-
136
- for fname in [
137
- "dataset/easy_scenarios.json",
138
- "dataset/medium_scenarios.json",
139
- "dataset/hard_scenarios.json"
140
- ]:
141
  try:
142
  with open(fname) as f:
143
  data = json.load(f)
144
  scenarios.extend(data)
145
- print(f" Loaded {len(data)} scenarios from {fname}")
146
  except FileNotFoundError:
147
- print(f"{fname} not found, skipping")
148
 
149
  if not scenarios:
150
- print("No local scenarios found. Fetching from live environment...")
151
- try:
152
- resp = requests.get(f"{ENV_URL}/tasks", timeout=15)
153
- tasks = resp.json().get("tasks", [])
154
- scenarios = [{"id": t["id"], "description": t["description"]} for t in tasks]
155
- print(f" Fetched {len(scenarios)} tasks from HF Space")
156
- except Exception as e:
157
- print(f"Could not fetch tasks: {e}")
158
- sys.exit(1)
159
 
160
  examples = []
161
- for s in scenarios:
162
- prompt = f"""{SYSTEM_PROMPT}
163
-
164
- Current Database State:
165
- - Scenario: {s.get('id', 'unknown')}
166
- - Description: {s.get('description', '')}
167
- - Tables: {json.dumps(s.get('tables', []))}
168
- - Slow Queries: {json.dumps(s.get('slow_queries', []))}
169
- - Performance Score: {s.get('performance_score_baseline', 0)} / 100
170
- - Target Score: {s.get('target_score', 85)}
171
-
172
- What is your next action?"""
173
-
 
174
  examples.append({
175
- "prompt": prompt,
176
- "task_id": s.get("id", "easy_s001"),
 
177
  })
178
 
179
- print(f" Built {len(examples)} training examples total")
180
- from datasets import Dataset
181
  return Dataset.from_list(examples)
182
 
183
 
184
- # ─────────────────────────────────────────────
185
- # INFERENCE TEST β€” run immediately after save
186
- # ─────────────────────────────────────────────
187
-
188
- def test_inference(model, tokenizer):
189
- """
190
- REQUIRED: Test inference immediately after saving.
191
- If this fails, the model was not saved correctly.
192
- """
193
- print("\n[INFERENCE TEST] Testing saved model...")
194
- try:
195
- FastLanguageModel.for_inference(model)
196
-
197
- test_prompt = f"""{SYSTEM_PROMPT}
198
-
199
- Current Database State:
200
- - Scenario: easy_s001
201
- - Description: User lookup query taking 2s on 10K users table
202
- - Tables: [{{"name": "users", "rows": 10000, "indexes": ["PRIMARY"]}}]
203
- - Slow Queries: [{{"id": "q1", "sql": "SELECT * FROM users WHERE email=?", "avg_ms": 2000}}]
204
- - Performance Score: 8.0 / 100
205
- - Target Score: 80.0
206
-
207
- What is your next action?"""
208
-
209
- inputs = tokenizer(
210
- test_prompt,
211
- return_tensors="pt",
212
- truncation=True,
213
- max_length=1024
214
- ).to(model.device)
215
-
216
- with torch.no_grad():
217
- outputs = model.generate(
218
- **inputs,
219
- max_new_tokens = 100,
220
- temperature = 0.3,
221
- do_sample = True,
222
- pad_token_id = tokenizer.eos_token_id,
223
- )
224
-
225
- response = tokenizer.decode(
226
- outputs[0][inputs["input_ids"].shape[1]:],
227
- skip_special_tokens=True
228
- ).strip()
229
-
230
- print(f"[INFERENCE TEST] Model output:\n {response}")
231
-
232
- # Validate output
233
- action = parse_action(response)
234
- print(f"[INFERENCE TEST] Parsed action: {action}")
235
- print("[INFERENCE TEST] PASSED β€” model saved correctly!")
236
- return True
237
-
238
- except Exception as e:
239
- print(f"[INFERENCE TEST] FAILED: {e}")
240
- print("[INFERENCE TEST] Check model save path. Do NOT proceed without fixing this.")
241
- return False
242
-
243
-
244
- # ─────────────────────────────────────────────
245
- # MAIN TRAINING
246
- # ─────────────────────────────────────────────
247
 
248
- def train():
249
- if not UNSLOTH_AVAILABLE:
250
- print(" Cannot train β€” Unsloth not installed or no GPU found")
251
- print("Run: pip install unsloth trl transformers datasets accelerate")
252
  return
253
 
254
- print(f"\n Loading model: {MODEL_NAME}")
255
- print(f" Environment: {ENV_URL}\n")
256
-
257
- # Verify environment is reachable
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  try:
259
  r = requests.get(f"{ENV_URL}/health", timeout=10)
260
- version = r.json().get("version", "?")
261
- print(f" Environment reachable β€” version {version}")
262
  except Exception as e:
263
- print(f" Cannot reach environment at {ENV_URL}: {e}")
264
- print("Check ENV_URL and make sure HF Space is running.")
265
  sys.exit(1)
266
 
267
- # ── Load model ───────────────────────────────────────────
 
268
  model, tokenizer = FastLanguageModel.from_pretrained(
269
  model_name = MODEL_NAME,
270
  max_seq_length = 2048,
271
- load_in_4bit = True, # QLoRA β€” required for T4
272
- dtype = None, # Auto detect
273
  token = HF_TOKEN or None,
274
  )
275
- print(" Model loaded")
276
-
277
- # ── Apply LoRA adapters ──────────────────────────────────
278
  model = FastLanguageModel.get_peft_model(
279
  model,
280
  r = 16,
281
  lora_alpha = 16,
282
- target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
283
- "gate_proj", "up_proj", "down_proj"],
284
  lora_dropout = 0,
285
  bias = "none",
286
  use_gradient_checkpointing = "unsloth",
287
  random_state = 42,
288
  )
289
- print(" LoRA adapters applied")
290
 
291
- # ── Build dataset ────────────────────────────────────────
292
- print("\n[DATASET] Building training dataset...")
293
  dataset = build_dataset()
294
- print(f" Dataset ready: {len(dataset)} examples")
295
 
296
- # ── Reward wrapper ───────────────────────────────────────
297
  def reward_wrapper(prompts, completions, **kwargs):
298
- batch = kwargs.get("batch", [])
299
- if batch and hasattr(batch[0], "get"):
300
- task_ids = [b.get("task_id", "easy_s001") for b in batch]
301
- else:
302
- task_ids = ["easy_s001"] * len(prompts)
303
- return reward_fn(prompts, completions, task_ids=task_ids)
304
-
305
- # ── GRPO config ──────────────────────────────────────────
306
- # NOTE: batch_size=1, num_generations=2 for free T4
307
- # At venue A100: increase to batch_size=2, num_generations=4
308
  config = GRPOConfig(
309
  output_dir = OUTPUT_DIR,
310
  max_steps = MAX_STEPS,
311
- per_device_train_batch_size = 1, # 1 for T4, 2 for A100
312
- gradient_accumulation_steps = 8,
313
  learning_rate = 5e-6,
314
- max_completion_length = 256,
315
- num_generations = 2, # 2 for T4, 4 for A100
316
- temperature = 0.8,
317
- logging_steps = 5,
318
- save_steps = 50,
319
- save_total_limit = 2,
320
  warmup_ratio = 0.1,
321
  report_to = "none",
322
  remove_unused_columns = False,
@@ -330,50 +321,27 @@ def train():
330
  train_dataset = dataset,
331
  )
332
 
333
- # ── Train ────────────────────────────────────────────────
334
- print(f"\nπŸ‹οΈ Starting GRPO training β€” {MAX_STEPS} steps...")
335
- print("Watch the 'reward' column β€” it should increase over time.\n")
336
  trainer.train()
337
- print("\n Training complete!")
338
 
339
- # ── Save β€” ADAPTER ONLY (correct way for QLoRA) ──────────
340
- # DO NOT call merge_and_unload() on 4-bit model
341
- # DO NOT upcast to 16-bit and merge naively
342
- # CORRECT: save adapter weights only, load with from_pretrained later
343
- print(f"\n[SAVE] Saving adapter to {OUTPUT_DIR}/final ...")
344
  Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
345
  model.save_pretrained(f"{OUTPUT_DIR}/final")
346
  tokenizer.save_pretrained(f"{OUTPUT_DIR}/final")
347
-
348
- # Save config for reference
349
- with open(f"{OUTPUT_DIR}/final/training_config.json", "w") as f:
350
- json.dump({
351
- "model_name": MODEL_NAME,
352
- "max_steps": MAX_STEPS,
353
- "save_method": "adapter_only_qlora",
354
- "lora_r": 16,
355
- "lora_alpha": 16,
356
- }, f, indent=2)
357
- print(f" Adapter saved to {OUTPUT_DIR}/final")
358
-
359
- # ── IMMEDIATE inference test (required) ──────────────────
360
- passed = test_inference(model, tokenizer)
361
-
362
- # ── Summary ──────────────────────────────────────────────
363
- print("\n" + "="*60)
364
- print("TRAINING COMPLETE")
365
- print("="*60)
366
- print(f" Model: {MODEL_NAME}")
367
- print(f" Steps: {MAX_STEPS}")
368
- print(f" Saved to: {OUTPUT_DIR}/final")
369
- print(f" Save method: Adapter only (QLoRA safe)")
370
- print(f" Inference test: {' PASSED' if passed else ' FAILED'}")
371
- print("="*60)
372
- print("\nNext steps:")
373
- print(" 1. python training/evaluate_agent.py")
374
- print(" 2. Open reward_curve.png β€” show to judges")
375
- print(" 3. git add reward_curve.png && git commit && git push")
376
- print("="*60)
377
 
378
 
379
  if __name__ == "__main__":
 
1
  """
2
  training/train_agent.py β€” SQL Database Engineer Agent
3
+ Fixed version: reward_fn runs FULL EPISODES via /reset + /step
4
+ This gives real delta rewards, milestones, and meaningful learning signal.
5
 
6
+ FREE T4 (Colab/Kaggle): MODEL_NAME=unsloth/Qwen2.5-1.5B-Instruct
7
+ VENUE A100: MODEL_NAME=unsloth/Qwen2.5-7B-Instruct
8
  """
9
 
10
+ import os, json, requests, sys, time
 
 
 
11
  from pathlib import Path
12
 
13
+ # ── GPU check + imports ───────────────────────────────────────
14
+ UNSLOTH_AVAILABLE = False
15
  try:
16
+ import torch
17
+ if not torch.cuda.is_available():
18
+ print("❌ No GPU found. Unsloth requires GPU.")
19
+ sys.exit(1)
20
  from unsloth import FastLanguageModel
21
  from trl import GRPOTrainer, GRPOConfig
22
+ from datasets import Dataset
23
  UNSLOTH_AVAILABLE = True
24
+ print(f"βœ… GPU: {torch.cuda.get_device_name(0)}")
25
+ print(f"βœ… VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
26
+ except ImportError as e:
27
+ print(f"❌ Import error: {e}")
28
+ print("Run: pip install unsloth trl transformers datasets accelerate")
29
+ sys.exit(1)
30
+
31
+ # ── Config ────────────────────────────────────────────────────
 
32
  ENV_URL = os.getenv("ENV_URL", "https://junaid0600-sql-db-engineer-agent.hf.space")
33
  HF_TOKEN = os.getenv("HF_TOKEN", "")
34
+ MODEL_NAME = os.getenv("MODEL_NAME", "unsloth/Qwen2.5-1.5B-Instruct")
35
  OUTPUT_DIR = os.getenv("OUTPUT_DIR", "./sdea-trained")
36
+ MAX_STEPS = int(os.getenv("MAX_STEPS", "100"))
 
 
 
 
 
37
 
38
+ print(f"\n[CONFIG] Model: {MODEL_NAME}")
39
+ print(f"[CONFIG] ENV URL: {ENV_URL}")
40
+ print(f"[CONFIG] Max steps: {MAX_STEPS}")
41
+ print(f"[CONFIG] Output: {OUTPUT_DIR}\n")
42
 
43
+ # ── System prompt ─────────────────────────────────────────────
44
  SYSTEM_PROMPT = """You are a senior database engineer.
45
+ Given the current database state, choose the BEST next action.
46
+
47
+ Rules:
48
+ 1. First action MUST be inspect_query to see what's slow
49
+ 2. Then analyze_indexes to see what's missing
50
+ 3. Then create_index with correct table and columns
51
+ 4. Then analyze_statistics to update planner
52
+ 5. Finally submit_report when performance target is reached
53
+
54
+ Respond with ONLY valid JSON β€” no markdown, no explanation:
55
+ {"action_type": "create_index", "payload": {"table": "users", "columns": ["email"]}}"""
56
+
57
+ # ── All 15 Round 2 scenario IDs ──────────────���────────────────
58
+ ALL_SCENARIOS = [
59
+ "easy_s001", "easy_s002", "easy_s003", "easy_s004", "easy_s005",
60
+ "medium_s001", "medium_s002", "medium_s003", "medium_s004", "medium_s005",
61
+ "hard_s001", "hard_s002", "hard_s003", "hard_s004", "hard_s005",
62
+ ]
63
+
64
+ # ── Parse LLM output β†’ action dict ───────────────────────────
65
  def parse_action(text: str) -> dict:
 
66
  try:
67
  text = text.strip()
68
+ # Strip markdown
69
+ for marker in ["```json", "```"]:
70
+ if marker in text:
71
+ parts = text.split(marker)
72
+ text = parts[1] if len(parts) > 1 else parts[0]
73
  text = text.strip()
74
  data = json.loads(text)
75
+ if "action_type" in data and "payload" in data:
76
  return data
77
  except Exception:
78
  pass
79
+ # Safe fallback
80
  return {"action_type": "inspect_query", "payload": {"query_id": "q1"}}
81
 
82
 
83
+ # ── REWARD FUNCTION β€” runs FULL EPISODE ───────────────────────
84
  def reward_fn(prompts, completions, **kwargs):
85
  """
86
+ KEY FIX: Runs a full episode per completion.
87
+ 1. /reset with scenario
88
+ 2. Parse LLM output as action
89
+ 3. /step with that action
90
+ 4. Get REAL reward including delta + milestones
91
+ 5. /step with submit_report to get terminal score
92
+ Returns real rewards β€” not constant 0.5
93
  """
94
  rewards = []
95
+ batch = kwargs.get("batch", [])
96
 
97
  for i, (prompt, completion) in enumerate(zip(prompts, completions)):
98
  try:
 
102
  else:
103
  text = str(completion)
104
 
105
+ # Pick scenario β€” rotate through all 15
106
+ scenario_id = ALL_SCENARIOS[i % len(ALL_SCENARIOS)]
107
+
108
+ # Parse LLM output
109
  action = parse_action(text)
110
+
111
+ # Step 1: Reset environment for this scenario
112
+ r = requests.post(f"{ENV_URL}/reset",
113
+ json={"task_id": scenario_id}, timeout=15)
114
+ if r.status_code != 200:
115
+ rewards.append(0.001)
116
+ continue
117
+
118
+ obs = r.json()
119
+ baseline = obs.get("current_context", {}).get("performance_score", 0)
120
+
121
+ # Step 2: Submit the LLM's action
122
+ r2 = requests.post(f"{ENV_URL}/step",
123
+ json=action, timeout=15)
124
+ if r2.status_code != 200:
125
+ rewards.append(0.001)
126
+ continue
127
+
128
+ data = r2.json()
129
+ step_score = data.get("reward", {}).get("score", 0.001)
130
+ db_delta = data.get("info", {}).get("db_delta", 0)
131
+ perf_score = data.get("info", {}).get("performance_score", baseline)
132
+ milestones = data.get("info", {}).get("milestones", [])
133
+ done = data.get("done", False)
134
+
135
+ # Step 3: If not done, submit report to get terminal score
136
+ if not done:
137
+ r3 = requests.post(f"{ENV_URL}/step",
138
+ json={"action_type": "submit_report",
139
+ "payload": {"summary": "Training episode complete."}},
140
+ timeout=15)
141
+ if r3.status_code == 200:
142
+ final_data = r3.json()
143
+ terminal = final_data.get("reward", {}).get("score", step_score)
144
+ # Combine step reward + terminal
145
+ final_score = (step_score * 0.4) + (terminal * 0.6)
146
+ else:
147
+ final_score = step_score
148
  else:
149
+ final_score = step_score
150
 
151
+ # Clamp
152
+ final_score = max(0.001, min(0.999, final_score))
153
+ rewards.append(final_score)
154
+
155
+ print(f" [REWARD] scenario={scenario_id} | "
156
+ f"action={action.get('action_type')} | "
157
+ f"db_delta=+{db_delta:.1f} | "
158
+ f"milestones={milestones} | "
159
+ f"score={final_score:.3f}")
160
 
 
 
161
  except Exception as e:
162
  print(f" [REWARD] Error: {e}")
163
  rewards.append(0.001)
 
165
  return rewards
166
 
167
 
168
+ # ── Build dataset with all 15 scenarios ───────────────────────
 
 
 
169
  def build_dataset():
 
170
  scenarios = []
171
+ for fname in ["dataset/easy_scenarios.json",
172
+ "dataset/medium_scenarios.json",
173
+ "dataset/hard_scenarios.json"]:
 
 
 
174
  try:
175
  with open(fname) as f:
176
  data = json.load(f)
177
  scenarios.extend(data)
178
+ print(f" Loaded {len(data)} from {fname}")
179
  except FileNotFoundError:
180
+ print(f" ⚠️ {fname} not found")
181
 
182
  if not scenarios:
183
+ print(" Fetching from live environment...")
184
+ resp = requests.get(f"{ENV_URL}/tasks", timeout=15)
185
+ tasks = resp.json().get("tasks", [])
186
+ scenarios = [t for t in tasks if t["id"].startswith(("easy_s","medium_s","hard_s"))]
 
 
 
 
 
187
 
188
  examples = []
189
+ for i, s in enumerate(scenarios):
190
+ # Build rich prompt with full DB state
191
+ tables_str = json.dumps(s.get("tables", []))
192
+ queries_str = json.dumps(s.get("slow_queries", []))
193
+ prompt = (
194
+ f"{SYSTEM_PROMPT}\n\n"
195
+ f"Scenario: {s.get('id')}\n"
196
+ f"Description: {s.get('description','')}\n"
197
+ f"Tables: {tables_str}\n"
198
+ f"Slow Queries: {queries_str}\n"
199
+ f"Performance: {s.get('performance_score_baseline',0)}/100 "
200
+ f"(target: {s.get('target_score',85)})\n\n"
201
+ f"What is your FIRST action?"
202
+ )
203
  examples.append({
204
+ "prompt": prompt,
205
+ "task_id": s.get("id", ALL_SCENARIOS[i % len(ALL_SCENARIOS)]),
206
+ "scenario_id": s.get("id", ALL_SCENARIOS[i % len(ALL_SCENARIOS)]),
207
  })
208
 
209
+ print(f" βœ… Dataset: {len(examples)} examples")
 
210
  return Dataset.from_list(examples)
211
 
212
 
213
+ # ── Generate loss + reward curve from training logs ───────────
214
+ def generate_training_plots(trainer):
215
+ import matplotlib
216
+ matplotlib.use("Agg")
217
+ import matplotlib.pyplot as plt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
+ logs = [l for l in trainer.state.log_history if "loss" in l]
220
+ if not logs:
221
+ print("⚠️ No training logs found for plotting")
 
222
  return
223
 
224
+ steps = [l.get("step", i) for i, l in enumerate(logs)]
225
+ losses = [l.get("loss", 0) for l in logs]
226
+ rewards = [l.get("reward", l.get("train_loss", 0)) for l in logs]
227
+
228
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
229
+ fig.suptitle("GRPO Training β€” SQL Database Engineer Agent",
230
+ fontsize=13, fontweight="bold")
231
+
232
+ ax1.plot(steps, losses, "b-o", lw=2, ms=4, label="Loss")
233
+ ax1.set_xlabel("Training Step")
234
+ ax1.set_ylabel("Loss")
235
+ ax1.set_title("Training Loss (↓ = learning)")
236
+ ax1.grid(True, alpha=0.3)
237
+ ax1.legend()
238
+
239
+ ax2.plot(steps, rewards, "g-o", lw=2, ms=4, label="Reward")
240
+ ax2.set_xlabel("Training Step")
241
+ ax2.set_ylabel("Reward")
242
+ ax2.set_title("Reward During Training (↑ = improving)")
243
+ ax2.grid(True, alpha=0.3)
244
+ ax2.legend()
245
+
246
+ plt.tight_layout()
247
+ plt.savefig("loss_curve.png", dpi=150, bbox_inches="tight")
248
+ print("βœ… loss_curve.png saved")
249
+
250
+ # Print summary
251
+ if losses:
252
+ print(f" Loss: {losses[0]:.4f} β†’ {losses[-1]:.4f}")
253
+ if rewards:
254
+ valid_r = [r for r in rewards if r > 0]
255
+ if valid_r:
256
+ print(f" Reward: {valid_r[0]:.4f} β†’ {valid_r[-1]:.4f}")
257
+
258
+
259
+ # ── Main training ─────────────────────────────────────────────
260
+ def train():
261
+ # Verify environment
262
  try:
263
  r = requests.get(f"{ENV_URL}/health", timeout=10)
264
+ print(f"βœ… Environment live: v{r.json().get('version','?')}\n")
 
265
  except Exception as e:
266
+ print(f"❌ Cannot reach {ENV_URL}: {e}")
 
267
  sys.exit(1)
268
 
269
+ # Load model
270
+ print(f"⏳ Loading {MODEL_NAME}...")
271
  model, tokenizer = FastLanguageModel.from_pretrained(
272
  model_name = MODEL_NAME,
273
  max_seq_length = 2048,
274
+ load_in_4bit = True,
 
275
  token = HF_TOKEN or None,
276
  )
 
 
 
277
  model = FastLanguageModel.get_peft_model(
278
  model,
279
  r = 16,
280
  lora_alpha = 16,
281
+ target_modules = ["q_proj","k_proj","v_proj","o_proj",
282
+ "gate_proj","up_proj","down_proj"],
283
  lora_dropout = 0,
284
  bias = "none",
285
  use_gradient_checkpointing = "unsloth",
286
  random_state = 42,
287
  )
288
+ print("βœ… Model + LoRA ready\n")
289
 
290
+ # Dataset
291
+ print("⏳ Building dataset...")
292
  dataset = build_dataset()
 
293
 
294
+ # Reward wrapper
295
  def reward_wrapper(prompts, completions, **kwargs):
296
+ return reward_fn(prompts, completions, **kwargs)
297
+
298
+ # GRPO config
 
 
 
 
 
 
 
299
  config = GRPOConfig(
300
  output_dir = OUTPUT_DIR,
301
  max_steps = MAX_STEPS,
302
+ per_device_train_batch_size = 1,
303
+ gradient_accumulation_steps = 4,
304
  learning_rate = 5e-6,
305
+ max_completion_length = 200,
306
+ num_generations = 2,
307
+ temperature = 0.9,
308
+ logging_steps = 1,
309
+ save_steps = 25,
310
+ save_total_limit = 3,
311
  warmup_ratio = 0.1,
312
  report_to = "none",
313
  remove_unused_columns = False,
 
321
  train_dataset = dataset,
322
  )
323
 
324
+ print(f"πŸ‹οΈ Starting GRPO β€” {MAX_STEPS} steps")
325
+ print("Watch for: db_delta > 0 and milestones in logs\n")
 
326
  trainer.train()
327
+ print("\nβœ… Training complete!")
328
 
329
+ # Save
 
 
 
 
330
  Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
331
  model.save_pretrained(f"{OUTPUT_DIR}/final")
332
  tokenizer.save_pretrained(f"{OUTPUT_DIR}/final")
333
+ print(f"βœ… Saved to {OUTPUT_DIR}/final")
334
+
335
+ # Generate plots
336
+ generate_training_plots(trainer)
337
+
338
+ print("\n" + "="*55)
339
+ print("NEXT STEPS:")
340
+ print(" python training/evaluate_agent.py")
341
+ print(" git add loss_curve.png reward_curve.png")
342
+ print(" git commit -m 'Real training evidence'")
343
+ print(" git push origin main")
344
+ print("="*55)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
 
347
  if __name__ == "__main__":