from __future__ import annotations import json import re import sys from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "src")) from supplymind_env.environment import V3SupplyMindEnv from supplymind_env.models import V3Action from supplymind_env.policies import baseline_policy from supplymind_env.seed_catalog import TRAIN_SEEDS, TASK_IDS SYSTEM_PROMPT = """You are the central orchestrator for SupplyMind. Return JSON only: {"central_replenishments":[],"inventory_transfers":[],"offer_matches":[],"priority_policy":[],"defer_orders":[],"coalition_deals":[]} Warehouses publish local offers and requests as market_signals, but hidden incentives must be inferred from public behavior. You do not see individual customer orders; local warehouse agents handle local fulfillment. Use central_replenishments for limited depot-to-warehouse restock, offer_matches for compatible stock trades, and inventory_transfers for direct stock sharing with compensation.""" def build_training_rows(limit_per_task: int = 8) -> list[dict[str, Any]]: rows: list[dict[str, Any]] = [] for task_id in TASK_IDS: for seed in TRAIN_SEEDS[task_id][:limit_per_task]: env = V3SupplyMindEnv(default_task_id=task_id) observation = env.reset_internal(task_id=task_id, internal_seed=seed, public_seed=seed) while not env.done: rows.append( { "prompt": [ {"role": "system", "content": SYSTEM_PROMPT}, { "role": "user", "content": json.dumps(observation.model_dump(mode="json"), separators=(",", ":")), }, ], "task_id": task_id, "seed": seed, "round_index": observation.round_index, } ) result = env.step(V3Action()) observation = result.observation return rows def extract_json(text: str) -> dict[str, Any] | None: match = re.search(r"\{.*\}", text, flags=re.DOTALL) if not match: return None try: return json.loads(match.group(0)) except json.JSONDecodeError: return None def reward_completions(prompts: list[Any], completions: list[str], task_id: list[str], seed: list[int], round_index: list[int], **_: Any) -> list[float]: rewards: list[float] = [] for completion, current_task, current_seed, current_round in zip(completions, task_id, seed, round_index, strict=True): payload = extract_json(completion) if payload is None: rewards.append(-8.0) continue try: action = V3Action.model_validate(payload) except Exception: rewards.append(-8.0) continue env = V3SupplyMindEnv(default_task_id=current_task) observation = env.reset_internal(task_id=current_task, internal_seed=current_seed, public_seed=current_seed) while observation.round_index < current_round and not env.done: result = env.step(baseline_policy(observation), grade_terminal=False) observation = result.observation result = env.step(action, grade_terminal=False) rewards.append(float(result.reward.step_reward)) return rewards def main() -> None: try: from datasets import Dataset from transformers import AutoTokenizer from trl import GRPOConfig, GRPOTrainer from unsloth import FastLanguageModel except ImportError as exc: raise SystemExit( "Install finale training deps in Colab first, for example: " "pip install unsloth trl datasets transformers accelerate" ) from exc model_name = "Qwen/Qwen2.5-1.5B-Instruct" model, tokenizer = FastLanguageModel.from_pretrained( model_name=model_name, max_seq_length=4096, load_in_4bit=True, ) model = FastLanguageModel.get_peft_model( model, r=16, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], ) tokenizer = AutoTokenizer.from_pretrained(model_name) dataset = Dataset.from_list(build_training_rows()) config = GRPOConfig( output_dir=str(ROOT / "outputs" / "supplymind-grpo"), num_train_epochs=1, per_device_train_batch_size=2, gradient_accumulation_steps=2, num_generations=2, max_prompt_length=2048, max_completion_length=256, logging_steps=1, report_to="none", ) trainer = GRPOTrainer( model=model, processing_class=tokenizer, reward_funcs=reward_completions, args=config, train_dataset=dataset, ) trainer.train() trainer.save_model(str(ROOT / "outputs" / "supplymind-grpo-final")) if __name__ == "__main__": main()