| """Reliability harness: measure how cleanly a served model speaks the action schema. |
| |
| Runs the REAL agent prompts (ttw.agents.build_messages over a seeded world for a |
| few turns) against a backend, parses each raw response through the same layer the |
| game uses, and reports the metrics that quantify the Well-Tuned win: |
| |
| valid_json_pct fraction of responses that yield a JSON object |
| valid_offer_pct fraction of raw offers that coerce to a real Offer |
| self_buy_rate fraction of BUY offers that buy the creature's own product |
| avg_offers_per_turn mean valid offers per creature-turn |
| |
| Workflow for the badge: deploy the base model, eval; deploy the fine-tuned model |
| (TTW_MODEL=...), eval; compare the two tables in Field Notes. |
| |
| Backends: |
| --backend modal call the currently deployed ttw-serve endpoint (default) |
| --backend local run a local transformers pipeline on --model (needs a GPU/CPU + deps) |
| --backend dummy no network: drive the deterministic dummy policy (verifies harness logic) |
| |
| Usage: |
| python scripts/finetune/eval_model.py # eval the live endpoint |
| python scripts/finetune/eval_model.py --backend dummy # self-test, no GPU |
| python scripts/finetune/eval_model.py --backend local --model AdmiralTaco/ttw-trader-0.5b |
| """ |
|
|
| import argparse |
| import json |
| import sys |
| from pathlib import Path |
|
|
| sys.path.insert(0, str(Path(__file__).resolve().parents[2])) |
|
|
| from ttw.actions import _coerce_offer, _extract_json |
| from ttw.agents import build_messages |
| from ttw.world import seed_world |
|
|
|
|
| def collect_modal(turns: int, max_tokens: int, temperature: float) -> list[tuple[str, str]]: |
| """Drive the deployed ttw-serve endpoint. Returns [(creature, raw_text), ...].""" |
| from ttw.llm import ModalLLM |
| from ttw.sim import step |
|
|
| client = ModalLLM() |
| samples: list[tuple[str, str]] = [] |
| world = seed_world() |
|
|
| |
| |
| from ttw.agents import make_llm_policy |
|
|
| policy = make_llm_policy(client, temperature=temperature, max_tokens=max_tokens) |
| for _ in range(turns): |
| step(world, policy) |
| for name, raw in policy.state["raw"].items(): |
| samples.append((name, raw)) |
| return samples |
|
|
|
|
| def collect_local(model_id: str, turns: int, max_tokens: int, temperature: float) -> list[tuple[str, str]]: |
| """Run a local transformers chat pipeline over the real prompts.""" |
| import torch |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
|
|
| tok = AutoTokenizer.from_pretrained(model_id) |
| model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.bfloat16) |
|
|
| samples: list[tuple[str, str]] = [] |
| world = seed_world() |
|
|
| def local_chat(messages: list[dict]) -> str: |
| prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) |
| inputs = tok(prompt, return_tensors="pt").to(model.device) |
| out = model.generate( |
| **inputs, |
| max_new_tokens=max_tokens, |
| temperature=temperature, |
| do_sample=temperature > 0, |
| pad_token_id=tok.eos_token_id, |
| ) |
| return tok.decode(out[0][inputs["input_ids"].shape[1] :], skip_special_tokens=True) |
|
|
| from ttw.actions import parse_actions |
| from ttw.sim import step |
|
|
| def policy(w, name): |
| msgs = build_messages(w, w.creatures[name]) |
| raw = local_chat(msgs) |
| samples.append((name, raw)) |
| return parse_actions(name, raw) |
|
|
| for _ in range(turns): |
| step(world, policy) |
| return samples |
|
|
|
|
| def collect_dummy(turns: int) -> list[tuple[str, str]]: |
| """No-network path: synthesize raw JSON from the dummy policy's offers. |
| |
| Lets the scoring logic be exercised end-to-end without any model. The dummy |
| never self-buys, so self_buy_rate should read 0.0, a useful sanity check. |
| """ |
| from ttw.dummy import make_random_policy |
| from ttw.sim import step |
|
|
| base_policy = make_random_policy(seed=7) |
| samples: list[tuple[str, str]] = [] |
|
|
| def policy(w, name): |
| offers, gossip = base_policy(w, name) |
| raw = json.dumps( |
| { |
| "thought": f"{name} works the market", |
| "offers": [ |
| {"side": o.side, "good": o.good, "price": o.price, "qty": o.qty} |
| for o in offers |
| ], |
| "gossip": gossip[0].message if gossip else "", |
| } |
| ) |
| samples.append((name, raw)) |
| return offers, gossip |
|
|
| world = seed_world() |
| for _ in range(turns): |
| step(world, policy) |
| return samples |
|
|
|
|
| def score(samples: list[tuple[str, str]]) -> dict: |
| """Compute reliability metrics from (creature, raw_text) samples.""" |
| produces = {name: c.produces for name, c in seed_world().creatures.items()} |
|
|
| n_turns = len(samples) |
| valid_json = 0 |
| raw_offer_count = 0 |
| valid_offer_count = 0 |
| buy_count = 0 |
| self_buy_count = 0 |
|
|
| for creature, raw in samples: |
| payload = _extract_json(raw) |
| if payload is None: |
| continue |
| valid_json += 1 |
| raw_offers = payload.get("offers", []) |
| if isinstance(raw_offers, dict): |
| raw_offers = [raw_offers] |
| if not isinstance(raw_offers, list): |
| raw_offers = [] |
| for raw_offer in raw_offers: |
| if not isinstance(raw_offer, dict): |
| continue |
| raw_offer_count += 1 |
| offer = _coerce_offer(creature, raw_offer) |
| if offer is None: |
| continue |
| valid_offer_count += 1 |
| if offer.side == "buy": |
| buy_count += 1 |
| if offer.good == produces.get(creature): |
| self_buy_count += 1 |
|
|
| return { |
| "samples": n_turns, |
| "valid_json_pct": valid_json / n_turns if n_turns else 0.0, |
| "valid_offer_pct": valid_offer_count / raw_offer_count if raw_offer_count else 0.0, |
| "self_buy_rate": self_buy_count / buy_count if buy_count else 0.0, |
| "avg_offers_per_turn": valid_offer_count / n_turns if n_turns else 0.0, |
| } |
|
|
|
|
| def print_table(label: str, m: dict) -> None: |
| """Print one clean metrics block.""" |
| print(f"\n=== {label} ({m['samples']} creature-turns) ===") |
| print(f" valid_json_pct {m['valid_json_pct']:.1%}") |
| print(f" valid_offer_pct {m['valid_offer_pct']:.1%}") |
| print(f" self_buy_rate {m['self_buy_rate']:.1%}") |
| print(f" avg_offers_per_turn {m['avg_offers_per_turn']:.2f}") |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) |
| ap.add_argument("--backend", choices=["modal", "local", "dummy"], default="modal") |
| ap.add_argument("--model", default=None, help="model id for --backend local") |
| ap.add_argument("--turns", type=int, default=6) |
| ap.add_argument("--max-tokens", type=int, default=256) |
| ap.add_argument("--temperature", type=float, default=0.7) |
| args = ap.parse_args() |
|
|
| if args.backend == "modal": |
| samples = collect_modal(args.turns, args.max_tokens, args.temperature) |
| label = "modal:ttw-serve" |
| elif args.backend == "local": |
| if not args.model: |
| ap.error("--backend local requires --model") |
| samples = collect_local(args.model, args.turns, args.max_tokens, args.temperature) |
| label = f"local:{args.model}" |
| else: |
| samples = collect_dummy(args.turns) |
| label = "dummy" |
|
|
| print_table(label, score(samples)) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|