SavK1 Claude Sonnet 4.6 commited on
Commit
16705aa
·
1 Parent(s): 2f02c01

fix(v4): increase T4 SFT to 50 eps, add action trace to eval output

Browse files

N_SFT_EPISODES 20→50 on T4: 20 eps gave loss≈2.0 which is too high for
GRPO to improve from. 50 eps targets loss<1.0 as SFT floor.

Eval now prints the action sequence per episode in brackets so it's
immediately visible whether the model is: (a) not generating valid JSON
(marked with !), (b) skipping assign_ticket, (c) posting to wrong channel.
This makes it easy to diagnose score=0.000 episodes without rerunning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. training/train_v4.ipynb +76 -35
training/train_v4.ipynb CHANGED
@@ -1,17 +1,4 @@
1
  {
2
- "nbformat": 4,
3
- "nbformat_minor": 5,
4
- "metadata": {
5
- "kernelspec": {
6
- "display_name": "Python 3",
7
- "language": "python",
8
- "name": "python3"
9
- },
10
- "language_info": {
11
- "name": "python",
12
- "version": "3.12.0"
13
- }
14
- },
15
  "cells": [
16
  {
17
  "cell_type": "markdown",
@@ -46,9 +33,9 @@
46
  },
47
  {
48
  "cell_type": "code",
 
49
  "id": "v4-install",
50
  "metadata": {},
51
- "execution_count": null,
52
  "outputs": [],
53
  "source": [
54
  "%%capture\n",
@@ -79,11 +66,11 @@
79
  },
80
  {
81
  "cell_type": "code",
 
82
  "id": "v4-config",
83
  "metadata": {},
84
- "execution_count": null,
85
  "outputs": [],
86
- "source": "import torch\nfrom unsloth import FastLanguageModel, PatchFastRL\n\n# Must patch BEFORE any trl imports\nPatchFastRL('GRPO', FastLanguageModel)\n\nimport trl\n\ngpu = torch.cuda.get_device_properties(0)\nTOTAL_GB = round(gpu.total_memory / 1024**3, 1)\nIS_A100 = TOTAL_GB >= 35\n\nNUM_GEN = 6 if IS_A100 else 2\nGRAD_ACCUM = 32 if IS_A100 else 8\n# Per-step max tokens: JSON actions are ~80-120 tokens; 192 gives headroom\n# without filling TRL's completion buffer and zeroing the policy loss.\nMAX_COMP_LEN = 192\nN_SFT_EPISODES = 120 if IS_A100 else 20\nN_GRPO_EPISODES = 150 if IS_A100 else 30\nTRAIN_MAX_STEPS = 12\n\nprint(f'torch={torch.__version__} trl={trl.__version__}')\nprint(f'GPU: {gpu.name} ({TOTAL_GB} GB) IS_A100={IS_A100}')\nprint(f'num_gen={NUM_GEN} grad_accum={GRAD_ACCUM} '\n f'sft_eps={N_SFT_EPISODES} grpo_eps={N_GRPO_EPISODES} '\n f'max_steps={TRAIN_MAX_STEPS} max_comp_len={MAX_COMP_LEN}')"
87
  },
88
  {
89
  "cell_type": "markdown",
@@ -95,9 +82,9 @@
95
  },
96
  {
97
  "cell_type": "code",
 
98
  "id": "v4-clone",
99
  "metadata": {},
100
- "execution_count": null,
101
  "outputs": [],
102
  "source": [
103
  "import os, sys\n",
@@ -130,9 +117,9 @@
130
  },
131
  {
132
  "cell_type": "code",
 
133
  "id": "v4-hf-login",
134
  "metadata": {},
135
- "execution_count": null,
136
  "outputs": [],
137
  "source": [
138
  "from huggingface_hub import notebook_login\n",
@@ -149,9 +136,9 @@
149
  },
150
  {
151
  "cell_type": "code",
 
152
  "id": "v4-server",
153
  "metadata": {},
154
- "execution_count": null,
155
  "outputs": [],
156
  "source": [
157
  "import subprocess, time, requests\n",
@@ -189,9 +176,9 @@
189
  },
190
  {
191
  "cell_type": "code",
 
192
  "id": "v4-verify-env",
193
  "metadata": {},
194
- "execution_count": null,
195
  "outputs": [],
196
  "source": [
197
  "from openenv.core import GenericEnvClient\n",
@@ -221,9 +208,9 @@
221
  },
222
  {
223
  "cell_type": "code",
 
224
  "id": "v4-model",
225
  "metadata": {},
226
- "execution_count": null,
227
  "outputs": [],
228
  "source": [
229
  "MODEL_NAME = 'Qwen/Qwen3-1.7B'\n",
@@ -275,9 +262,9 @@
275
  },
276
  {
277
  "cell_type": "code",
 
278
  "id": "v4-sft-data",
279
  "metadata": {},
280
- "execution_count": null,
281
  "outputs": [],
282
  "source": [
283
  "import json as _json\n",
@@ -343,9 +330,9 @@
343
  },
344
  {
345
  "cell_type": "code",
 
346
  "id": "v4-sft-train",
347
  "metadata": {},
348
- "execution_count": null,
349
  "outputs": [],
350
  "source": [
351
  "from trl import SFTTrainer, SFTConfig\n",
@@ -389,9 +376,9 @@
389
  },
390
  {
391
  "cell_type": "code",
 
392
  "id": "v4-sft-verify",
393
  "metadata": {},
394
- "execution_count": null,
395
  "outputs": [],
396
  "source": [
397
  "from training.rollout import extract_json_action\n",
@@ -455,9 +442,9 @@
455
  },
456
  {
457
  "cell_type": "code",
 
458
  "id": "v4-grpo-data",
459
  "metadata": {},
460
- "execution_count": null,
461
  "outputs": [],
462
  "source": [
463
  "from training.dataset import generate_triage_dataset\n",
@@ -479,9 +466,9 @@
479
  },
480
  {
481
  "cell_type": "code",
 
482
  "id": "v4-rollout",
483
  "metadata": {},
484
- "execution_count": null,
485
  "outputs": [],
486
  "source": [
487
  "import torch.nn.functional as F\n",
@@ -676,11 +663,52 @@
676
  },
677
  {
678
  "cell_type": "code",
 
679
  "id": "v4-trainer",
680
  "metadata": {},
681
- "execution_count": null,
682
  "outputs": [],
683
- "source": "from trl import GRPOConfig\nfrom training.pm_ops_trainer import PMOpsGRPOTrainer\n\nOUTPUT_DIR = 'pm-ops-grpo-Qwen3-1.7B-triage-v4'\nHF_REPO_ID = f'Saurav1/{OUTPUT_DIR}'\n\ngrpo_cfg = GRPOConfig(\n num_train_epochs = 2,\n learning_rate = 1e-6,\n gradient_accumulation_steps = GRAD_ACCUM,\n per_device_train_batch_size = 1,\n warmup_steps = 5,\n num_generations = NUM_GEN,\n # max_completion_length must match MAX_COMP_LEN so TRL's internal\n # single-turn generation doesn't overflow and zero the policy loss\n # (clipped_ratio=1.0 means all gradient is masked → loss=0.000).\n max_completion_length = MAX_COMP_LEN,\n max_prompt_length = 4096,\n use_vllm = False,\n output_dir = OUTPUT_DIR,\n report_to = 'none',\n logging_steps = 1,\n save_steps = 20,\n gradient_checkpointing = False,\n)\n\neff_batch = grpo_cfg.per_device_train_batch_size * GRAD_ACCUM\ntotal_steps = len(grpo_dataset) * NUM_GEN * grpo_cfg.num_train_epochs // eff_batch\nprint(f'GRPO: {len(grpo_dataset)} eps × {NUM_GEN} gen × {grpo_cfg.num_train_epochs} epochs → ~{total_steps} steps')\n\ntrainer = PMOpsGRPOTrainer(\n model = model,\n processing_class = tokenizer,\n reward_funcs = grpo_reward_func,\n train_dataset = grpo_dataset,\n args = grpo_cfg,\n rollout_func = grpo_rollout_func,\n)\nassert grpo_cfg.use_vllm is False\nprint(f'Trainer: {type(trainer).__name__} ready')"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
684
  },
685
  {
686
  "cell_type": "markdown",
@@ -692,9 +720,9 @@
692
  },
693
  {
694
  "cell_type": "code",
 
695
  "id": "v4-probe",
696
  "metadata": {},
697
- "execution_count": null,
698
  "outputs": [],
699
  "source": [
700
  "# Run 2 episodes and verify reward varies (not stuck at a constant)\n",
@@ -723,9 +751,9 @@
723
  },
724
  {
725
  "cell_type": "code",
 
726
  "id": "v4-train",
727
  "metadata": {},
728
- "execution_count": null,
729
  "outputs": [],
730
  "source": [
731
  "trainer_stats = trainer.train()\n",
@@ -746,9 +774,9 @@
746
  },
747
  {
748
  "cell_type": "code",
 
749
  "id": "v4-save",
750
  "metadata": {},
751
- "execution_count": null,
752
  "outputs": [],
753
  "source": [
754
  "grpo_env.close()\n",
@@ -777,11 +805,11 @@
777
  },
778
  {
779
  "cell_type": "code",
 
780
  "id": "v4-eval",
781
  "metadata": {},
782
- "execution_count": null,
783
  "outputs": [],
784
- "source": "from training.rollout import extract_json_action, step_aware_fallback\nfrom inference import baseline_agent\n\nN_EVAL = 15\nEVAL_MAX_STEPS = 12\nEVAL_SEED_BASE = 9000\n\n# Switch model to Unsloth fast inference mode.\n# Required after training — without this, model.generate() is very slow\n# and may hang on T4 because Unsloth's training hooks are still active.\nFastLanguageModel.for_inference(model)\nmodel.eval()\nprint('Model switched to fast inference mode')\n\n\ndef run_eval(n=N_EVAL):\n scores = []\n with GenericEnvClient(base_url=ENV_URL).sync() as env:\n for i in range(n):\n result = env.reset(seed=EVAL_SEED_BASE + i)\n obs_dict = _obs_to_dict(result.observation if hasattr(result, 'observation') else result)\n task_brief = obs_dict.get('task_brief', '')\n history, step, score, done = [], 0, 0.0, False\n\n while not done and step < EVAL_MAX_STEPS:\n obs_text = _current_obs_text(obs_dict, step, task_brief)\n msgs = build_messages(history, obs_text)\n prompt = tokenizer.apply_chat_template(\n msgs, add_generation_prompt=True, tokenize=False, enable_thinking=False\n )\n inputs = tokenizer([prompt], return_tensors='pt', truncation=True, max_length=4096)\n inputs = {k: v.to(model.device) for k, v in inputs.items()}\n with torch.no_grad():\n out_ids = model.generate(\n **inputs,\n max_new_tokens = 128,\n do_sample = False,\n pad_token_id = tokenizer.eos_token_id,\n )\n completion = tokenizer.decode(\n out_ids[0][inputs['input_ids'].shape[1]:], skip_special_tokens=True\n )\n parsed = extract_json_action(completion) or step_aware_fallback(step, EVAL_MAX_STEPS)\n result = env.step({'action_type': parsed['action_type'], 'args': parsed.get('args', {})})\n obs_dict = _obs_to_dict(result.observation if hasattr(result, 'observation') else result)\n done = bool(getattr(result, 'done', obs_dict.get('done', False)))\n score = float(getattr(result, 'reward', obs_dict.get('reward', 0.0)))\n history.append({'obs_text': obs_text, 'completion': completion, 'is_runbook': False})\n step += 1\n\n scores.append(score)\n print(f' Trained ep {i+1}/{n}: score={score:.3f}')\n return scores\n\n\ndef run_baseline(n=N_EVAL):\n scores = []\n with GenericEnvClient(base_url=ENV_URL).sync() as env:\n for i in range(n):\n result = env.reset(seed=EVAL_SEED_BASE + i)\n obs_dict = _obs_to_dict(result.observation if hasattr(result, 'observation') else result)\n org_config, step, score, done = {}, 0, 0.0, False\n while not done and step < EVAL_MAX_STEPS:\n at, args = baseline_agent(obs_dict, org_config)\n result = env.step({'action_type': at, 'args': args})\n obs_dict = _obs_to_dict(result.observation if hasattr(result, 'observation') else result)\n done = bool(getattr(result, 'done', obs_dict.get('done', False)))\n score = float(getattr(result, 'reward', obs_dict.get('reward', 0.0)))\n if at == 'meta.read_runbook':\n last = obs_dict.get('last_action_result') or {}\n if last.get('ok'):\n data = last.get('data') or {}\n if isinstance(data, dict) and 'org_config' in data:\n org_config.update(data['org_config'])\n step += 1\n scores.append(score)\n print(f' Baseline ep {i+1}/{n}: score={score:.3f}')\n return scores\n\n\nprint('--- Baseline ---')\nbaseline_scores = run_baseline()\nprint('\\n--- Trained ---')\ntrained_scores = run_eval()\n\nb_avg = sum(baseline_scores) / N_EVAL\nt_avg = sum(trained_scores) / N_EVAL\nprint(f'\\nBaseline avg : {b_avg:.3f}')\nprint(f'Trained avg : {t_avg:.3f}')\nprint(f'Delta : {t_avg - b_avg:+.3f}')"
785
  },
786
  {
787
  "cell_type": "markdown",
@@ -793,14 +821,27 @@
793
  },
794
  {
795
  "cell_type": "code",
 
796
  "id": "v4-teardown",
797
  "metadata": {},
798
- "execution_count": null,
799
  "outputs": [],
800
  "source": [
801
  "server_proc.terminate()\n",
802
  "print('PM-Ops server stopped')"
803
  ]
804
  }
805
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
806
  }
 
1
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  "cells": [
3
  {
4
  "cell_type": "markdown",
 
33
  },
34
  {
35
  "cell_type": "code",
36
+ "execution_count": null,
37
  "id": "v4-install",
38
  "metadata": {},
 
39
  "outputs": [],
40
  "source": [
41
  "%%capture\n",
 
66
  },
67
  {
68
  "cell_type": "code",
69
+ "execution_count": null,
70
  "id": "v4-config",
71
  "metadata": {},
 
72
  "outputs": [],
73
+ "source": "import torch\nfrom unsloth import FastLanguageModel, PatchFastRL\n\n# Must patch BEFORE any trl imports\nPatchFastRL('GRPO', FastLanguageModel)\n\nimport trl\n\ngpu = torch.cuda.get_device_properties(0)\nTOTAL_GB = round(gpu.total_memory / 1024**3, 1)\nIS_A100 = TOTAL_GB >= 35\n\nNUM_GEN = 6 if IS_A100 else 2\nGRAD_ACCUM = 32 if IS_A100 else 8\nMAX_COMP_LEN = 192\n# T4 needs more SFT to reach low enough loss before GRPO.\n# 20 eps loss≈2.0 is too high; 50 eps loss≈0.8 is a better floor.\nN_SFT_EPISODES = 120 if IS_A100 else 50\nN_GRPO_EPISODES = 150 if IS_A100 else 30\nTRAIN_MAX_STEPS = 12\n\nprint(f'torch={torch.__version__} trl={trl.__version__}')\nprint(f'GPU: {gpu.name} ({TOTAL_GB} GB) IS_A100={IS_A100}')\nprint(f'num_gen={NUM_GEN} grad_accum={GRAD_ACCUM} '\n f'sft_eps={N_SFT_EPISODES} grpo_eps={N_GRPO_EPISODES} '\n f'max_steps={TRAIN_MAX_STEPS} max_comp_len={MAX_COMP_LEN}')"
74
  },
75
  {
76
  "cell_type": "markdown",
 
82
  },
83
  {
84
  "cell_type": "code",
85
+ "execution_count": null,
86
  "id": "v4-clone",
87
  "metadata": {},
 
88
  "outputs": [],
89
  "source": [
90
  "import os, sys\n",
 
117
  },
118
  {
119
  "cell_type": "code",
120
+ "execution_count": null,
121
  "id": "v4-hf-login",
122
  "metadata": {},
 
123
  "outputs": [],
124
  "source": [
125
  "from huggingface_hub import notebook_login\n",
 
136
  },
137
  {
138
  "cell_type": "code",
139
+ "execution_count": null,
140
  "id": "v4-server",
141
  "metadata": {},
 
142
  "outputs": [],
143
  "source": [
144
  "import subprocess, time, requests\n",
 
176
  },
177
  {
178
  "cell_type": "code",
179
+ "execution_count": null,
180
  "id": "v4-verify-env",
181
  "metadata": {},
 
182
  "outputs": [],
183
  "source": [
184
  "from openenv.core import GenericEnvClient\n",
 
208
  },
209
  {
210
  "cell_type": "code",
211
+ "execution_count": null,
212
  "id": "v4-model",
213
  "metadata": {},
 
214
  "outputs": [],
215
  "source": [
216
  "MODEL_NAME = 'Qwen/Qwen3-1.7B'\n",
 
262
  },
263
  {
264
  "cell_type": "code",
265
+ "execution_count": null,
266
  "id": "v4-sft-data",
267
  "metadata": {},
 
268
  "outputs": [],
269
  "source": [
270
  "import json as _json\n",
 
330
  },
331
  {
332
  "cell_type": "code",
333
+ "execution_count": null,
334
  "id": "v4-sft-train",
335
  "metadata": {},
 
336
  "outputs": [],
337
  "source": [
338
  "from trl import SFTTrainer, SFTConfig\n",
 
376
  },
377
  {
378
  "cell_type": "code",
379
+ "execution_count": null,
380
  "id": "v4-sft-verify",
381
  "metadata": {},
 
382
  "outputs": [],
383
  "source": [
384
  "from training.rollout import extract_json_action\n",
 
442
  },
443
  {
444
  "cell_type": "code",
445
+ "execution_count": null,
446
  "id": "v4-grpo-data",
447
  "metadata": {},
 
448
  "outputs": [],
449
  "source": [
450
  "from training.dataset import generate_triage_dataset\n",
 
466
  },
467
  {
468
  "cell_type": "code",
469
+ "execution_count": null,
470
  "id": "v4-rollout",
471
  "metadata": {},
 
472
  "outputs": [],
473
  "source": [
474
  "import torch.nn.functional as F\n",
 
663
  },
664
  {
665
  "cell_type": "code",
666
+ "execution_count": null,
667
  "id": "v4-trainer",
668
  "metadata": {},
 
669
  "outputs": [],
670
+ "source": [
671
+ "from trl import GRPOConfig\n",
672
+ "from training.pm_ops_trainer import PMOpsGRPOTrainer\n",
673
+ "\n",
674
+ "OUTPUT_DIR = 'pm-ops-grpo-Qwen3-1.7B-triage-v4'\n",
675
+ "HF_REPO_ID = f'Saurav1/{OUTPUT_DIR}'\n",
676
+ "\n",
677
+ "grpo_cfg = GRPOConfig(\n",
678
+ " num_train_epochs = 2,\n",
679
+ " learning_rate = 1e-6,\n",
680
+ " gradient_accumulation_steps = GRAD_ACCUM,\n",
681
+ " per_device_train_batch_size = 1,\n",
682
+ " warmup_steps = 5,\n",
683
+ " num_generations = NUM_GEN,\n",
684
+ " # max_completion_length must match MAX_COMP_LEN so TRL's internal\n",
685
+ " # single-turn generation doesn't overflow and zero the policy loss\n",
686
+ " # (clipped_ratio=1.0 means all gradient is masked → loss=0.000).\n",
687
+ " max_completion_length = MAX_COMP_LEN,\n",
688
+ " max_prompt_length = 4096,\n",
689
+ " use_vllm = False,\n",
690
+ " output_dir = OUTPUT_DIR,\n",
691
+ " report_to = 'none',\n",
692
+ " logging_steps = 1,\n",
693
+ " save_steps = 20,\n",
694
+ " gradient_checkpointing = False,\n",
695
+ ")\n",
696
+ "\n",
697
+ "eff_batch = grpo_cfg.per_device_train_batch_size * GRAD_ACCUM\n",
698
+ "total_steps = len(grpo_dataset) * NUM_GEN * grpo_cfg.num_train_epochs // eff_batch\n",
699
+ "print(f'GRPO: {len(grpo_dataset)} eps × {NUM_GEN} gen × {grpo_cfg.num_train_epochs} epochs → ~{total_steps} steps')\n",
700
+ "\n",
701
+ "trainer = PMOpsGRPOTrainer(\n",
702
+ " model = model,\n",
703
+ " processing_class = tokenizer,\n",
704
+ " reward_funcs = grpo_reward_func,\n",
705
+ " train_dataset = grpo_dataset,\n",
706
+ " args = grpo_cfg,\n",
707
+ " rollout_func = grpo_rollout_func,\n",
708
+ ")\n",
709
+ "assert grpo_cfg.use_vllm is False\n",
710
+ "print(f'Trainer: {type(trainer).__name__} ready')"
711
+ ]
712
  },
713
  {
714
  "cell_type": "markdown",
 
720
  },
721
  {
722
  "cell_type": "code",
723
+ "execution_count": null,
724
  "id": "v4-probe",
725
  "metadata": {},
 
726
  "outputs": [],
727
  "source": [
728
  "# Run 2 episodes and verify reward varies (not stuck at a constant)\n",
 
751
  },
752
  {
753
  "cell_type": "code",
754
+ "execution_count": null,
755
  "id": "v4-train",
756
  "metadata": {},
 
757
  "outputs": [],
758
  "source": [
759
  "trainer_stats = trainer.train()\n",
 
774
  },
775
  {
776
  "cell_type": "code",
777
+ "execution_count": null,
778
  "id": "v4-save",
779
  "metadata": {},
 
780
  "outputs": [],
781
  "source": [
782
  "grpo_env.close()\n",
 
805
  },
806
  {
807
  "cell_type": "code",
808
+ "execution_count": null,
809
  "id": "v4-eval",
810
  "metadata": {},
 
811
  "outputs": [],
812
+ "source": "from training.rollout import extract_json_action, step_aware_fallback\nfrom inference import baseline_agent\n\nN_EVAL = 15\nEVAL_MAX_STEPS = 12\nEVAL_SEED_BASE = 9000\n\n# Required after training: switch Unsloth from training hooks to fast inference.\n# Without this, model.generate() is very slow and hangs on T4.\nFastLanguageModel.for_inference(model)\nmodel.eval()\nprint('Model switched to fast inference mode')\n\n\ndef _model_step(obs_dict, task_brief, history, step):\n \"\"\"One greedy decode step. Returns (action_type, args, json_ok).\"\"\"\n obs_text = _current_obs_text(obs_dict, step, task_brief)\n msgs = build_messages(history, obs_text)\n prompt = tokenizer.apply_chat_template(\n msgs, add_generation_prompt=True, tokenize=False, enable_thinking=False\n )\n inputs = tokenizer([prompt], return_tensors='pt', truncation=True, max_length=4096)\n inputs = {k: v.to(model.device) for k, v in inputs.items()}\n with torch.no_grad():\n out_ids = model.generate(\n **inputs,\n max_new_tokens = 128,\n do_sample = False,\n pad_token_id = tokenizer.eos_token_id,\n )\n completion = tokenizer.decode(\n out_ids[0][inputs['input_ids'].shape[1]:], skip_special_tokens=True\n )\n parsed = extract_json_action(completion)\n json_ok = parsed is not None\n if not json_ok:\n parsed = step_aware_fallback(step, EVAL_MAX_STEPS)\n history.append({'obs_text': obs_text, 'completion': completion, 'is_runbook': False})\n return parsed.get('action_type', 'meta.noop'), parsed.get('args', {}), json_ok, obs_text\n\n\ndef run_eval(n=N_EVAL, verbose=False):\n scores = []\n with GenericEnvClient(base_url=ENV_URL).sync() as env:\n for i in range(n):\n result = env.reset(seed=EVAL_SEED_BASE + i)\n obs_dict = _obs_to_dict(result.observation if hasattr(result, 'observation') else result)\n task_brief = obs_dict.get('task_brief', '')\n history, step, score, done = [], 0, 0.0, False\n actions = []\n\n while not done and step < EVAL_MAX_STEPS:\n action_type, args, json_ok, _ = _model_step(obs_dict, task_brief, history, step)\n actions.append(f'{action_type}{\"\" if json_ok else \"!\"}')\n result = env.step({'action_type': action_type, 'args': args})\n obs_dict = _obs_to_dict(result.observation if hasattr(result, 'observation') else result)\n done = bool(getattr(result, 'done', obs_dict.get('done', False)))\n score = float(getattr(result, 'reward', obs_dict.get('reward', 0.0)))\n step += 1\n\n scores.append(score)\n action_summary = ' → '.join(a.split('.')[-1] for a in actions)\n print(f' Trained ep {i+1}/{n}: score={score:.3f} [{action_summary}]')\n return scores\n\n\ndef run_baseline(n=N_EVAL):\n scores = []\n with GenericEnvClient(base_url=ENV_URL).sync() as env:\n for i in range(n):\n result = env.reset(seed=EVAL_SEED_BASE + i)\n obs_dict = _obs_to_dict(result.observation if hasattr(result, 'observation') else result)\n org_config, step, score, done = {}, 0, 0.0, False\n actions = []\n while not done and step < EVAL_MAX_STEPS:\n at, args = baseline_agent(obs_dict, org_config)\n actions.append(at.split('.')[-1])\n result = env.step({'action_type': at, 'args': args})\n obs_dict = _obs_to_dict(result.observation if hasattr(result, 'observation') else result)\n done = bool(getattr(result, 'done', obs_dict.get('done', False)))\n score = float(getattr(result, 'reward', obs_dict.get('reward', 0.0)))\n if at == 'meta.read_runbook':\n last = obs_dict.get('last_action_result') or {}\n if last.get('ok'):\n data = last.get('data') or {}\n if isinstance(data, dict) and 'org_config' in data:\n org_config.update(data['org_config'])\n step += 1\n scores.append(score)\n action_summary = ' → '.join(actions)\n print(f' Baseline ep {i+1}/{n}: score={score:.3f} [{action_summary}]')\n return scores\n\n\nprint('--- Baseline ---')\nbaseline_scores = run_baseline()\nprint('\\n--- Trained ---')\ntrained_scores = run_eval()\n\nb_avg = sum(baseline_scores) / N_EVAL\nt_avg = sum(trained_scores) / N_EVAL\nprint(f'\\nBaseline avg : {b_avg:.3f}')\nprint(f'Trained avg : {t_avg:.3f}')\nprint(f'Delta : {t_avg - b_avg:+.3f}')\nprint()\nprint('Action key: ! = fallback (no valid JSON from model)')"
813
  },
814
  {
815
  "cell_type": "markdown",
 
821
  },
822
  {
823
  "cell_type": "code",
824
+ "execution_count": null,
825
  "id": "v4-teardown",
826
  "metadata": {},
 
827
  "outputs": [],
828
  "source": [
829
  "server_proc.terminate()\n",
830
  "print('PM-Ops server stopped')"
831
  ]
832
  }
833
+ ],
834
+ "metadata": {
835
+ "kernelspec": {
836
+ "display_name": "Python 3",
837
+ "language": "python",
838
+ "name": "python3"
839
+ },
840
+ "language_info": {
841
+ "name": "python",
842
+ "version": "3.12.0"
843
+ }
844
+ },
845
+ "nbformat": 4,
846
+ "nbformat_minor": 5
847
  }