#!/usr/bin/env python3 """ prepare_data.py — one-time data extractor for the Atlassian Cloud RL "trajectory playground" (a representational HuggingFace Static Space). It reads REAL recorded artifacts from the sibling `atlassian_env/` project and emits a single self-contained `data/playground.json` that the static page fetches at runtime. `trajectories/` is gitignored in the env, so committing this JSON is what makes the Space self-contained. Run it with the repo's uv project Python (recommended — it also lets the extractor drive the real environment to regenerate proper get -> act -> get gold reference trajectories): cd ../atlassian_env && uv run python ../atlassian_space/prepare_data.py It also runs without the env deps (pure standard library), in which case the recorded gold trajectories are used as-is: python3 prepare_data.py Sources (all under ../atlassian_env, read-only): - tasks/registry.json + tasks/.json -> task metadata + the user instruction - tasks/gold/.json -> gold reference call sequence - rubrics/.py (module docstring) -> human-readable rubric description - rollout/prompts.py -> the exact agent system prompt - tasks/gold/.json (calls) -> driven through the real env to make the gold reference trajectories (get->act->get) - trajectories/reward-spread-sweep/ -> the 18x7 model sweep over the hardened (+ sweep_summary.json) tasks/rubrics, with recorded reasoning Nothing here mutates the environment source. The disposable work DB is the env's own scratch file. Output is deterministic given the same inputs. """ from __future__ import annotations import ast import json import os import sys import tempfile from collections import Counter, defaultdict from pathlib import Path # -------------------------------------------------------------------------------------- # Paths # -------------------------------------------------------------------------------------- HERE = Path(__file__).resolve().parent # .../atlassian_space REPO_ROOT = HERE.parent # .../atlassian_env_rleaas ENV_ROOT = Path(os.environ.get("ENV_ROOT", REPO_ROOT / "atlassian_env")).resolve() TASKS_DIR = ENV_ROOT / "tasks" GOLD_DIR = TASKS_DIR / "gold" RUBRICS_DIR = ENV_ROOT / "rubrics" TRAJ_ROOT = ENV_ROOT / "trajectories" TRAJ_SWEEP = TRAJ_ROOT / "reward-spread-sweep" SWEEP_SUMMARY = TRAJ_SWEEP / "sweep_summary.json" OUT_PATH = HERE / "data" / "playground.json" if str(ENV_ROOT) not in sys.path: sys.path.insert(0, str(ENV_ROOT)) REASONING_CAP = 1600 # cap recorded reasoning/message length (one run loops to ~80k chars) # -------------------------------------------------------------------------------------- # Constants sourced from the env # -------------------------------------------------------------------------------------- TOOL_COUNT = 102 SHAPING_SIGNALS = [ {"name": "R_PROGRESS", "value": 0.1, "detail": "a successful mutating call that changed semantic state"}, {"name": "R_NEUTRAL", "value": 0.0, "detail": "a successful read, or a mutating call that changed nothing"}, {"name": "R_ERROR", "value": -0.2, "detail": "tool returned a 4xx/5xx (malformed / contract-violating)"}, {"name": "R_REDUNDANT", "value": -0.25, "detail": "identical call, no intervening state change (farming)"}, {"name": "R_UNAVAILABLE", "value": -0.5, "detail": "called a tool not in the task's available_tools"}, ] REWARD_MODEL = {"partial_cap": 0.9, "gate_cap": 0.3, "partial_credit_cap_config": 0.99, "shaping_signals": SHAPING_SIGNALS} SYSTEM_PROMPT_FALLBACK = ( "You are an assistant operating an Atlassian Cloud workspace (Jira and Confluence) " "through a set of tools. Accomplish the user's task using ONLY the tools provided to " "you; do not assume any capability a tool does not expose.\n" "\n" "Guidelines:\n" "- Inspect state with read tools before mutating, and verify the result afterward when " "it matters.\n" "- Call one or more tools per turn. Pass arguments exactly as each tool's schema " "requires.\n" "- If a tool returns an error (a 4xx/5xx status), read the message and adjust your next " "call instead of repeating the same one.\n" "- When the task is fully accomplished (or you have determined it cannot be done), call " "`finish` with a short `final_response` summarizing the outcome. If the task asks " "you to report a specific value, include that value verbatim in `final_response`.\n" ) MODEL_META = { "gold": {"label": "Gold (reference)", "vendor": "reference", "color": "#6366f1"}, "claude-sonnet-4.6": {"label": "claude-sonnet-4.6", "vendor": "anthropic", "color": "#d97757"}, "gpt-5.5": {"label": "gpt-5.5", "vendor": "openai", "color": "#10a37f"}, "gemini-3.5-flash": {"label": "gemini-3.5-flash", "vendor": "google", "color": "#4285f4"}, "deepseek-v4-pro": {"label": "deepseek-v4-pro", "vendor": "deepseek", "color": "#6d4dfc"}, "kimi-k2.6": {"label": "kimi-k2.6", "vendor": "moonshotai", "color": "#e0457b"}, "qwen3.7-plus": {"label": "qwen3.7-plus", "vendor": "qwen", "color": "#a855f7"}, "glm-5.2": {"label": "glm-5.2", "vendor": "z-ai", "color": "#f59e0b"}, } RUN_ORDER = {"gold": 0, "claude-sonnet-4.6": 1, "gpt-5.5": 2, "gemini-3.5-flash": 3, "deepseek-v4-pro": 4, "kimi-k2.6": 5, "qwen3.7-plus": 6, "glm-5.2": 7} LEADERBOARD_MODELS = ["claude-sonnet-4.6", "gpt-5.5", "gemini-3.5-flash", "deepseek-v4-pro", "kimi-k2.6", "qwen3.7-plus", "glm-5.2"] # -------------------------------------------------------------------------------------- # Helpers # -------------------------------------------------------------------------------------- def load_json(path: Path): with path.open(encoding="utf-8") as fh: return json.load(fh) def read_jsonl(path: Path): out = [] with path.open(encoding="utf-8") as fh: for line in fh: line = line.strip() if line: out.append(json.loads(line)) return out def cap(s): if not s: return s return s if len(s) <= REASONING_CAP else s[:REASONING_CAP].rstrip() + " … (truncated)" def scenario_name(task_id: str) -> str: stem = task_id.split("-", 1)[1] if "-" in task_id else task_id return stem.replace("-", " ").title() def rubric_description(task_id: str) -> str: path = RUBRICS_DIR / f"{task_id}.py" if not path.exists(): return "" try: return (ast.get_docstring(ast.parse(path.read_text(encoding="utf-8"))) or "").strip() except SyntaxError: return "" def rubric_scheme(task_id: str) -> str: path = RUBRICS_DIR / f"{task_id}.py" if path.exists() and 'scheme="multiplicative"' in path.read_text(encoding="utf-8"): return "multiplicative" return "weighted_sum" def resolve_system_prompt() -> str: try: from rollout.prompts import SYSTEM_PROMPT # type: ignore return SYSTEM_PROMPT except Exception: return SYSTEM_PROMPT_FALLBACK def model_id_from_header(model_raw: str, kind: str) -> str: if model_raw == "scripted-gold" or kind == "gold": return "gold" return model_raw.rsplit("/", 1)[-1] def parse_trajectory(path: Path, kind: str, end_reason: str | None = None) -> dict: records = read_jsonl(path) header = next((r for r in records if r.get("record") == "header"), {}) terminal = next((r for r in records if r.get("record") == "terminal"), {}) step_records = [r for r in records if r.get("record") == "step"] assistant_records = [r for r in records if r.get("record") == "assistant"] # Map each tool_call_id -> the assistant turn that issued it (for reasoning/content). turns = {} tcid_to_turn = {} for a in assistant_records: ti = a.get("turn_index") turns[ti] = {"reasoning": cap(a.get("reasoning")), "content": cap(a.get("content"))} for tcid in (a.get("tool_call_ids") or []): tcid_to_turn[tcid] = ti steps = [] for s in step_records: out = s.get("output") ti = tcid_to_turn.get(s.get("tool_call_id")) turn = turns.get(ti, {}) steps.append({ "step_index": s.get("step_index"), "tool_name": s.get("tool_name"), "tool_call_id": s.get("tool_call_id"), "args": s.get("args"), "output": out, "status": out.get("status") if isinstance(out, dict) else None, "reward": s.get("reward"), "turn_index": ti, "reasoning": turn.get("reasoning"), "content": turn.get("content"), }) # The model's closing message: the last assistant turn that issued no tool calls. final_msg = None for a in assistant_records: if not (a.get("tool_call_ids") or []) and a.get("content"): final_msg = a.get("content") final_response = terminal.get("final_response") or cap(final_msg) model_raw = header.get("model", "") model_id = model_id_from_header(model_raw, kind) meta = MODEL_META.get(model_id, {"label": model_id, "vendor": "model", "color": "#64748b"}) return { "run_id": path.stem.rsplit("__", 1)[-1], "file": path.name, "kind": kind, "model_raw": model_raw, "model_id": model_id, "model_label": meta["label"], "vendor": meta["vendor"], "color": meta["color"], "order": RUN_ORDER.get(model_id, 9), "seed": header.get("seed"), "start_ts": header.get("start_ts"), "config_hash": header.get("config_hash"), "final_reward": terminal.get("final_reward"), "passed": terminal.get("passed"), "n_steps": terminal.get("n_steps", len(steps)), "terminated_reason": end_reason or terminal.get("terminated_reason"), "final_response": final_response, "components": terminal.get("components", []), "steps": steps, "note": "", } def rebuild_gold(task_id: str, gold_calls, final_response=None) -> dict | None: """Drive the task's gold solution calls through the REAL environment to produce a genuine gold reference trajectory (the gold solutions are already proper get -> act -> get sequences). Returns None only if the env can't be driven.""" try: from orchestration.env import RLEnv except Exception: return None tmp = Path(tempfile.mkdtemp(prefix="goldgen_")) try: env = RLEnv(task_id, env_root=ENV_ROOT, model="scripted-gold", trajectories_dir=tmp) env.reset(seed=42) for c in gold_calls: env.step({"name": c["name"], "args": c.get("args", {})}) env.finish(final_response=final_response) path = env.trajectory_path env.close() return parse_trajectory(Path(path), "gold") except Exception as exc: print(f" ! gold rebuild failed for {task_id}: {exc!r}") return None # -------------------------------------------------------------------------------------- # Build # -------------------------------------------------------------------------------------- def build(): if not ENV_ROOT.exists(): sys.exit(f"ENV_ROOT not found: {ENV_ROOT}\nSet ENV_ROOT=/path/to/atlassian_env and re-run.") registry = load_json(TASKS_DIR / "registry.json")["tasks"] runs_by_task: dict[str, list[dict]] = defaultdict(list) # gold reference trajectories: drive each task's gold solution through the real env gold_ok, gold_bad = 0, [] for entry in registry: tid = entry["task_id"] gp = GOLD_DIR / f"{tid}.json" if not gp.exists(): continue g = load_json(gp) run = rebuild_gold(tid, g.get("calls", []), g.get("final_response")) if run is None: # env not drivable -> fall back to any recorded scripted gold rec = next((p for p in TRAJ_ROOT.glob(f"*__{tid}__*.jsonl")), None) run = parse_trajectory(rec, "gold") if rec else None if run is None: continue runs_by_task[tid].append(run) if run["final_reward"] == 1.0 and run["passed"]: gold_ok += 1 else: gold_bad.append((tid, run["final_reward"])) # model sweep: one run per (task, model). sweep_summary.json is the authority for the # canonical reward + end reason; pick the trajectory file that matches it (a few cells # have a second attempt), skipping any incomplete file with no terminal record. summary = {} if SWEEP_SUMMARY.exists(): for e in load_json(SWEEP_SUMMARY): summary[(e["task"], e["model"])] = e groups: dict[tuple, list[dict]] = defaultdict(list) if TRAJ_SWEEP.exists(): for path in sorted(TRAJ_SWEEP.glob("*.jsonl")): tid = path.stem.split("__")[1] if "__" in path.stem else None if tid is None: continue run = parse_trajectory(path, "sweep") if run["final_reward"] is None: # incomplete attempt (no terminal record) continue groups[(tid, run["model_id"])].append(run) sweep_n = 0 for (tid, mid), cand in groups.items(): s = summary.get((tid, mid)) if s is not None: # prefer the file matching the canonical reward matches = [r for r in cand if abs((r["final_reward"] or 0) - s["reward"]) < 1e-9] cand = matches or cand run = max(cand, key=lambda r: r["start_ts"] or "") if s is not None: run["terminated_reason"] = s.get("end") runs_by_task[tid].append(run) sweep_n += 1 # ---- per-task records ---------------------------------------------------------- tasks = [] for entry in registry: task_id = entry["task_id"] task = load_json(TASKS_DIR / entry["file"]) gold = None gp = GOLD_DIR / f"{task_id}.json" if gp.exists(): g = load_json(gp) gold = {"intended_state": g.get("intended_state", ""), "calls": g.get("calls", []), "n_calls": len(g.get("calls", []))} runs = sorted(runs_by_task.get(task_id, []), key=lambda r: (r["order"], r["start_ts"] or "")) gold_run = next((r for r in runs if r["kind"] == "gold"), None) comp_source = (gold_run["components"] if gold_run and gold_run["components"] else (runs[0]["components"] if runs else [])) rubric_components = [{"name": c.get("name"), "weight": c.get("weight"), "gate": bool(c.get("gate")), "detail": c.get("detail", "")} for c in comp_source] model_runs = [r for r in runs if r["model_id"] != "gold"] best_reward = max((r["final_reward"] for r in model_runs if r["final_reward"] is not None), default=None) models_run = sorted({r["model_id"] for r in model_runs}, key=lambda m: RUN_ORDER.get(m, 9)) tasks.append({ "task_id": task_id, "name": scenario_name(task_id), "title": task.get("title", entry.get("title", task_id)), "product": task.get("product", entry.get("product")), "difficulty": task.get("difficulty"), "instruction": task.get("instruction", ""), "available_tools": task.get("available_tools", []), "initial_state_notes": task.get("initial_state_notes", ""), "max_steps": task.get("max_steps"), "tags": task.get("tags", []), "rubric_path": task.get("rubric", f"rubrics/{task_id}.py"), "rubric": {"description": rubric_description(task_id), "aggregation": rubric_scheme(task_id), "components": rubric_components}, "gold": gold, "runs": runs, "has_live": bool(model_runs), "best_reward": best_reward, "models_run": models_run, "gold_steps": gold_run["n_steps"] if gold_run else (gold["n_calls"] if gold else None), }) # ---- leaderboard over all swept tasks ----------------------------------------- swept_task_ids = [t["task_id"] for t in tasks if t["has_live"]] def reward_for(tid, model_id): for r in runs_by_task.get(tid, []): if r["model_id"] == model_id and r["final_reward"] is not None: return r["final_reward"] return None leaderboard_rows = [] for mid in LEADERBOARD_MODELS: per_task = {t: reward_for(t, mid) for t in swept_task_ids} present = [v for v in per_task.values() if v is not None] leaderboard_rows.append({ "model_id": mid, "label": MODEL_META[mid]["label"], "color": MODEL_META[mid]["color"], "kind": "sweep", "order": RUN_ORDER.get(mid, 9), "per_task": per_task, "mean_reward": round(sum(present) / len(present), 4) if present else None, "pass_count": sum(1 for v in present if v >= 1.0), "task_count": len(present), }) # ---- meta ---------------------------------------------------------------------- diff_counts = Counter(t["difficulty"] for t in tasks) prod_counts = Counter(t["product"] for t in tasks) meta = { "env_id": "atlassian-cloud-rl", "title": "Atlassian Cloud RL Environment", "subtitle": "Tool-use RL environment cloning Atlassian Cloud (Jira v3 + Agile + Confluence v2/v1) over a SQLite store.", "counts": {"tasks": len(tasks), "tools": TOOL_COUNT, # the swept model trajectories (gold reference replays are counted separately) "trajectories": sum(1 for t in tasks for r in t["runs"] if r["model_id"] != "gold"), "products": ["jira", "confluence"]}, "difficulty_counts": dict(diff_counts), "product_counts": dict(prod_counts), "system_prompt": resolve_system_prompt(), "reward_model": REWARD_MODEL, "models": [{"id": mid, **MODEL_META[mid]} for mid in ["gold"] + LEADERBOARD_MODELS], "swept_task_ids": swept_task_ids, "source_note": "All values are read verbatim from the recorded JSONL trajectories and task/rubric files in atlassian_env/; gold reference runs are produced by driving the real environment.", } payload = {"meta": meta, "leaderboard": {"swept_task_ids": swept_task_ids, "rows": leaderboard_rows}, "tasks": tasks} OUT_PATH.parent.mkdir(parents=True, exist_ok=True) with OUT_PATH.open("w", encoding="utf-8") as fh: json.dump(payload, fh, ensure_ascii=False, separators=(",", ":")) # ---- console summary ----------------------------------------------------------- print(f"Wrote {OUT_PATH.relative_to(REPO_ROOT)} ({OUT_PATH.stat().st_size/1024:.0f} KB)") print(f" tasks: {len(tasks)} ({dict(prod_counts)}, {dict(diff_counts)}) | gold passing: {gold_ok}/{len(tasks)}") if gold_bad: print(f" ! gold not at 1.0: {gold_bad}") print(f" sweep runs: {sweep_n} | total runs: {meta['counts']['trajectories']} | swept tasks: {len(swept_task_ids)} | models: {len(LEADERBOARD_MODELS)}") gmin = min((t["gold_steps"] for t in tasks if t["gold_steps"] is not None), default=None) print(f" min gold steps across tasks: {gmin}") for r in leaderboard_rows: mr = "n/a" if r["mean_reward"] is None else f"{r['mean_reward']:.3f}" print(f" {r['label']:20s} mean={mr} pass={r['pass_count']}/{r['task_count']}") if __name__ == "__main__": build()