| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import random |
| from pathlib import Path |
|
|
| from bdo_ai_env.baseline_agent import greedy_policy, stingy_policy |
| from bdo_ai_env.training import build_prompt, rollout_episode, save_episode_traces |
| from server.bdo_environment import BDOEnvironment |
|
|
|
|
| def scripted_training_policy(observation: dict) -> dict: |
| """ |
| Lightweight stand-in policy for local development. |
| |
| This gives us a deterministic training-like path before Unsloth/TRL is |
| installed. It tries to be more coherent than the baselines so we can test |
| reward plumbing and trace export. |
| """ |
|
|
| highest = max(observation["nodes"], key=lambda node: node["reported_demand"]) |
| weakest_hardware = min(observation["nodes"], key=lambda node: node["biometric_signal"]) |
| actions = [] |
|
|
| if weakest_hardware["biometric_signal"] < 0.55: |
| actions.append( |
| {"name": "dispatch_repair", "params": {"village": weakest_hardware["village"]}} |
| ) |
| elif highest["report_lag_days"] > 0 and highest["reported_demand"] > 2600: |
| actions.append( |
| {"name": "trigger_field_audit", "params": {"village": highest["village"]}} |
| ) |
|
|
| remaining_budget = observation["treasury"]["district_budget"] |
| spend = min(max(2000, int(highest["reported_demand"] * 0.8)), remaining_budget) |
| actions.append({"name": "allocate_funds", "params": {"village": highest["village"], "amount": spend}}) |
| actions.append({"name": "approve_batch", "params": {"village": highest["village"], "mode": "conservative"}}) |
|
|
| predicted_fraud = min( |
| 0.9, |
| max(0.1, 1 - sum(node["biometric_signal"] for node in observation["nodes"]) / len(observation["nodes"])), |
| ) |
| thought = ( |
| f"{highest['village']} shows the highest reported demand, while " |
| f"{weakest_hardware['village']} looks most fragile on biometrics. " |
| f"Use conservative controls and targeted intervention." |
| ) |
| return { |
| "thought_process": thought, |
| "predicted_fraud_level": round(predicted_fraud, 3), |
| "actions": actions, |
| } |
|
|
|
|
| def try_unsloth_training() -> str: |
| """ |
| Detect whether the actual training stack is available. |
| |
| We don't execute a full GRPO pipeline in this environment yet because the |
| dependencies are not installed here, but this hook makes the next step very |
| small once they are. |
| """ |
|
|
| try: |
| import unsloth |
| import trl |
| import transformers |
| except Exception: |
| return "unavailable" |
| return "available" |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser(description="Phase 3 training scaffold for BDO.ai") |
| parser.add_argument("--scenario", default="black_swan") |
| parser.add_argument("--episodes", type=int, default=12) |
| parser.add_argument("--output", default="artifacts/training_traces.json") |
| args = parser.parse_args() |
|
|
| stack_status = try_unsloth_training() |
| env = BDOEnvironment(scenario=args.scenario) |
|
|
| traces = [] |
| policies = [scripted_training_policy, greedy_policy, stingy_policy] |
| for episode in range(args.episodes): |
| policy_fn = policies[episode % len(policies)] |
| trace = rollout_episode(env, policy_fn=policy_fn, episode_index=episode) |
| traces.append(trace) |
|
|
| save_episode_traces(traces, args.output) |
|
|
| prompt_preview = build_prompt(env.reset().model_dump(mode="json", exclude_none=True)) |
| report = { |
| "scenario": args.scenario, |
| "episodes": args.episodes, |
| "output": args.output, |
| "training_stack": stack_status, |
| "prompt_preview": prompt_preview[:700], |
| "best_total_training_reward": max(trace.total_training_reward for trace in traces), |
| "best_avg_belief_accuracy": max(trace.avg_belief_accuracy for trace in traces), |
| } |
| Path("artifacts").mkdir(exist_ok=True) |
| Path("artifacts/train_report.json").write_text(json.dumps(report, indent=2), encoding="utf-8") |
| print(json.dumps(report, indent=2)) |
|
|
|
|
| if __name__ == "__main__": |
| random.seed(7) |
| main() |
|
|