CoolFace
Apppublic

Pandago/graphstrike-model-training

sourceHugging Facemitupdated 5mo agoView on Hugging Face
0likes
_round2_runner.py521 linesDownload Raw Back to eval-models
1"""2Round 2 shared evaluation runner.3 4Deterministic loop + two LLM decision points per suspicious account.5 6Framework logic (deterministic, no LLM):7  - GET_POLICY as step 08  - Loop over visible accounts sorted by risk_score desc9  - INSPECT if not yet inspected10  - INVESTIGATE_NETWORK on very high-risk suspects (risk >= 0.80, once)11  - SUBMIT at end12 13Per-account LLM decision points:14  - DP1 (tool selection): "reverse_image_search" | "analyze_bio" | "check_ip" | "done"15  - DP2 (flag decision):  "flag" | "skip"16 17Entry point:18    run_evaluation(model_name, call_llm, platform, base_url, tasks, seeds, out_path)19"""20 21from __future__ import annotations22 23import json24import os25import sys26import time27from dataclasses import dataclass, asdict, field28from pathlib import Path29from typing import Callable, Dict, List, Optional30 31# Make parent (fake_gang_env/) importable so the local client + models resolve32# AHEAD of any same-named modules in site-packages. Always insert at index 033# (and drop any pre-cached aliases) — a no-op `if not in sys.path` check is34# unsafe because the path may already be present at lower priority.35_PARENT = Path(__file__).resolve().parent.parent36sys.path.insert(0, str(_PARENT))37for _stale in ("models", "client"):38    sys.modules.pop(_stale, None)39 40from client import FakeGangEnvClient, StepResult  # noqa: E40241from models import ActionType, FakeGangAction  # noqa: E40242 43 44# ---------------------------------------------------------------------------45# Prompts for the two decision points46# ---------------------------------------------------------------------------47 48DP1_PROMPT = """You are evaluating a suspicious account. Pick the next tool to call, or 'done' if evidence is sufficient.49 50PLATFORM: {platform} | primary signal: {primary_signal} | threshold θ*: {threshold:.3f}51ACCOUNT: {account_id} | risk_score: {risk:.3f} | hub_legitimacy: {hub:.2f}52 53Revealed signals (None = not yet revealed):54  photo_reuse_score:   {photo}55  bio_template_score:  {bio}56  ip_cluster_signal:   {ip}57  shared_ip_count:     {shared_ip}58 59Budget: steps_remaining={steps_left}  |  tool costs: reverse_image_search=1, analyze_bio=1, check_ip=260 61Reply with EXACTLY one token (no punctuation, no explanation):62  reverse_image_search   (reveal photo_reuse_score)63  analyze_bio            (reveal bio_template_score)64  check_ip               (reveal ip_cluster_signal, costs 2 steps)65  done                   (evidence is sufficient, move to flag decision)66"""67 68DP2_PROMPT = """You must decide whether to FLAG this account as a coordinated fake.69 70PLATFORM: {platform} | threshold θ*: {threshold:.3f} | fp_penalty: {fp_weight}71ACCOUNT: {account_id} | risk_score: {risk:.3f} | hub_legitimacy: {hub:.2f}72 73Revealed signals:74  photo_reuse_score:   {photo}75  bio_template_score:  {bio}76  ip_cluster_signal:   {ip}77  shared_ip_count:     {shared_ip}78 79Running totals: flagged={n_flagged}/10  |  steps_remaining={steps_left}80 81Guidance:82  - Flag ONLY if risk >= threshold and hub_legitimacy < 0.70 (celebrities are hubs).83  - High photo_reuse + bio_template or shared_ip_count >= 5 is strong evidence.84  - Skip on ambiguous cases when fp_penalty is high.85 86Reply with EXACTLY one token:87  flag   (mark this account as a coordinated fake)88  skip   (leave it alone, move on)89"""90 91 92# ---------------------------------------------------------------------------93# Episode log record94# ---------------------------------------------------------------------------95 96@dataclass97class EpisodeLog:98    model: str99    platform: str100    task: str101    seed: int102    episode_id: str = ""103    threshold: float = 0.0104    primary_signal: str = ""105    steps_taken: int = 0106    inspected: int = 0107    tool_calls: Dict[str, int] = field(default_factory=lambda: {108        "reverse_image_search": 0, "analyze_bio": 0, "check_ip": 0,109        "get_policy": 0, "investigate_network": 0,110    })111    flagged: int = 0112    dp1_calls: int = 0113    dp2_calls: int = 0114    dp1_invalid: int = 0115    dp2_invalid: int = 0116    reward: Optional[float] = None117    grader_score: Optional[float] = None118    final_message: str = ""119    wall_seconds: float = 0.0120 121 122# ---------------------------------------------------------------------------123# Helpers124# ---------------------------------------------------------------------------125 126def _find_account(obs, account_id: str):127    for a in obs.visible_accounts:128        if a.account_id == account_id:129            return a130    return None131 132 133def _render_signal(val) -> str:134    if val is None:135        return "None"136    if isinstance(val, float):137        return f"{val:.3f}" if val > 0 else "0.000"138    return str(val)139 140 141def _parse_dp1(text: str) -> Optional[str]:142    t = text.strip().lower().split("\n")[0].strip().strip("'\"`.,")143    valid = {"reverse_image_search", "analyze_bio", "check_ip", "done"}144    for tok in valid:145        if tok in t:146            return tok147    return None148 149 150def _parse_dp2(text: str) -> Optional[str]:151    t = text.strip().lower().split("\n")[0].strip().strip("'\"`.,")152    if "flag" in t and "unflag" not in t:153        return "flag"154    if "skip" in t or "no" == t or "keep" in t:155        return "skip"156    return None157 158 159def _seeds_for_platform(seeds: List[int], platform: Optional[str]) -> List[int]:160    """The env assigns platform by seed%2 (even=Instagram, odd=Snapchat).161    Offset seeds so the requested platform is actually used."""162    if platform is None:163        return seeds164    p = platform.lower()165    if p == "instagram":166        return [s if s % 2 == 0 else s + 1 for s in seeds]167    if p == "snapchat":168        return [s if s % 2 == 1 else s + 1 for s in seeds]169    return seeds  # unknown platform → env will fall back to its own mapping170 171 172def _policy_from_message(msg: str) -> Dict[str, float]:173    """Parse the message returned by GET_POLICY into a dict.174    Message format: 'Policy compiled: Platform: X | Threshold: 0.081 | Primary Signal: photo_reuse | FP Penalty: 0.5x | ...'175    """176    out = {"threshold": 0.0, "primary_signal": "", "platform": "", "fp_weight": "?"}177    try:178        body = msg.split("Policy compiled:", 1)[-1]179        for part in body.split("|"):180            k, _, v = part.strip().partition(":")181            k = k.strip().lower()182            v = v.strip()183            if k == "platform":184                out["platform"] = v185            elif k == "threshold":186                out["threshold"] = float(v)187            elif k == "primary signal":188                out["primary_signal"] = v189            elif k == "fp penalty":190                out["fp_weight"] = v191    except Exception:192        pass193    return out194 195 196# ---------------------------------------------------------------------------197# Per-account loop (DP1 + DP2)198# ---------------------------------------------------------------------------199 200def _gather_and_flag(201    client: FakeGangEnvClient,202    obs,203    account_id: str,204    policy: Dict,205    call_llm: Callable[[str], str],206    log: EpisodeLog,207    max_dp1_iters: int = 4,208    tuples: Optional[List[Dict]] = None,209) -> StepResult:210    """Run DP1 tool-gathering loop then DP2 flag decision for one account.211    Returns the latest StepResult (observation reflects any actions taken).212 213    If `tuples` is provided, each LLM decision appends a dict with keys:214      prompt, completion, step_reward, decision_type, platform, threshold,215      fp_penalty, step_index216    """217    last = StepResult(observation=obs, done=False, reward=None, message="")218    last.observation = obs219    plat = policy.get("platform") or log.platform220    fp_w = policy.get("fp_weight", "?")221    thr = float(policy.get("threshold", 0.0) or 0.0)222 223    # DP1 loop224    for _ in range(max_dp1_iters):225        acc = _find_account(last.observation, account_id)226        if acc is None:227            return last228 229        photo = acc.photo_reuse_score if acc.photo_reuse_score > 0 else None230        bio = acc.bio_template_score if acc.bio_template_score > 0 else None231        ip_signal = None  # ip_cluster_signal is returned only in the step message; treat as unknown until check_ip called232 233        prompt = DP1_PROMPT.format(234            platform=policy.get("platform") or log.platform,235            primary_signal=policy.get("primary_signal", "?"),236            threshold=policy.get("threshold", 0.0),237            account_id=account_id,238            risk=acc.fake_risk_score,239            hub=acc.hub_legitimacy_score,240            photo=_render_signal(photo),241            bio=_render_signal(bio),242            ip=_render_signal(ip_signal),243            shared_ip=acc.shared_ip_count,244            steps_left=last.observation.steps_remaining,245        )246        log.dp1_calls += 1247        resp = call_llm(prompt)248        choice = _parse_dp1(resp)249        if choice is None:250            log.dp1_invalid += 1251            if tuples is not None:252                tuples.append({253                    "prompt": prompt, "completion": resp, "step_reward": 0.0,254                    "decision_type": "dp1", "platform": plat, "threshold": thr,255                    "fp_penalty": fp_w, "step_index": log.steps_taken,256                })257            break258        if choice == "done":259            if tuples is not None:260                tuples.append({261                    "prompt": prompt, "completion": resp, "step_reward": 0.0,262                    "decision_type": "dp1", "platform": plat, "threshold": thr,263                    "fp_penalty": fp_w, "step_index": log.steps_taken,264                })265            break266 267        atype = {268            "reverse_image_search": ActionType.REVERSE_IMAGE_SEARCH,269            "analyze_bio": ActionType.ANALYZE_BIO,270            "check_ip": ActionType.CHECK_IP,271        }[choice]272        last = client.step(FakeGangAction(action_type=atype, account_id=account_id))273        log.tool_calls[choice] = log.tool_calls.get(choice, 0) + 1274        log.steps_taken += 1275        if tuples is not None:276            tuples.append({277                "prompt": prompt, "completion": resp,278                "step_reward": float(last.reward) if last.reward is not None else 0.0,279                "decision_type": "dp1", "platform": plat, "threshold": thr,280                "fp_penalty": fp_w, "step_index": log.steps_taken,281            })282        if last.done or last.observation.steps_remaining <= 1:283            return last284 285        # Stop early if all cheap signals revealed286        acc2 = _find_account(last.observation, account_id)287        if acc2 and acc2.photo_reuse_score > 0 and acc2.bio_template_score > 0:288            break289 290    # DP2291    acc = _find_account(last.observation, account_id)292    if acc is None:293        return last294    prompt = DP2_PROMPT.format(295        platform=policy.get("platform") or log.platform,296        threshold=policy.get("threshold", 0.0),297        fp_weight=policy.get("fp_weight", "?"),298        account_id=account_id,299        risk=acc.fake_risk_score,300        hub=acc.hub_legitimacy_score,301        photo=_render_signal(acc.photo_reuse_score if acc.photo_reuse_score > 0 else None),302        bio=_render_signal(acc.bio_template_score if acc.bio_template_score > 0 else None),303        ip=_render_signal(None),304        shared_ip=acc.shared_ip_count,305        n_flagged=len(last.observation.flagged_ids),306        steps_left=last.observation.steps_remaining,307    )308    log.dp2_calls += 1309    resp = call_llm(prompt)310    choice = _parse_dp2(resp)311    step_reward = 0.0312    if choice is None:313        log.dp2_invalid += 1314    elif choice == "flag":315        last = client.step(FakeGangAction(action_type=ActionType.FLAG, account_id=account_id))316        log.flagged = len(last.observation.flagged_ids)317        step_reward = float(last.reward) if last.reward is not None else 0.0318    if tuples is not None:319        tuples.append({320            "prompt": prompt, "completion": resp, "step_reward": step_reward,321            "decision_type": "dp2", "platform": plat, "threshold": thr,322            "fp_penalty": fp_w, "step_index": log.steps_taken,323        })324    return last325 326 327# ---------------------------------------------------------------------------328# Single episode329# ---------------------------------------------------------------------------330 331def _run_episode(332    client: FakeGangEnvClient,333    model: str,334    platform: str,335    task: str,336    seed: int,337    call_llm: Callable[[str], str],338    max_accounts_per_episode: int = 15,339    collect_tuples: bool = False,340):341    """Run one episode. Returns EpisodeLog by default, or342    (EpisodeLog, list[tuple_dict]) when collect_tuples=True."""343    log = EpisodeLog(model=model, platform=platform, task=task, seed=seed)344    tuples: List[Dict] = [] if collect_tuples else None  # type: ignore[assignment]345    t0 = time.time()346 347    res = client.reset(task=task, seed=seed)348    obs = res.observation349 350    # Deterministic step 0: GET_POLICY351    res = client.step(FakeGangAction(action_type=ActionType.GET_POLICY))352    log.tool_calls["get_policy"] += 1353    policy = _policy_from_message(res.message or res.observation.message)354    log.threshold = float(policy.get("threshold", 0.0) or 0.0)355    log.primary_signal = str(policy.get("primary_signal", "") or "")356    obs = res.observation357 358    investigated: set[str] = set()359    processed: set[str] = set()360 361    while not res.done and obs.steps_remaining > 1 and len(processed) < max_accounts_per_episode:362        # Candidate pool: suspects first, then visible not-yet-processed, ranked by risk.363        candidates = [a for a in obs.visible_accounts if a.account_id not in processed]364        suspects = set(obs.suspect_ids)365        candidates.sort(366            key=lambda a: (a.account_id in suspects, a.fake_risk_score),367            reverse=True,368        )369        if not candidates:370            break371        acc = candidates[0]372        aid = acc.account_id373 374        # Ensure inspected375        if aid not in obs.inspected_ids:376            res = client.step(FakeGangAction(action_type=ActionType.INSPECT, account_id=aid))377            log.steps_taken += 1378            log.inspected += 1379            obs = res.observation380            if res.done or obs.steps_remaining <= 1:381                break382 383        # Expand network once for very risky hubs384        acc_now = _find_account(obs, aid)385        if (386            acc_now387            and acc_now.fake_risk_score >= 0.80388            and aid not in investigated389            and obs.steps_remaining >= 5390        ):391            res = client.step(FakeGangAction(action_type=ActionType.INVESTIGATE_NETWORK, account_id=aid))392            log.tool_calls["investigate_network"] += 1393            log.steps_taken += 2394            investigated.add(aid)395            obs = res.observation396            if res.done or obs.steps_remaining <= 1:397                break398 399        # DP1 + DP2 per account400        res = _gather_and_flag(client, obs, aid, policy, call_llm, log, tuples=tuples)401        obs = res.observation402        processed.add(aid)403        if res.done:404            break405 406    # Final submit (if not already done)407    if not res.done:408        res = client.step(FakeGangAction(action_type=ActionType.SUBMIT))409 410    log.flagged = len(res.observation.flagged_ids)411    log.reward = res.reward412    log.final_message = (res.message or "")[:400]413    log.episode_id = getattr(res.observation, "episode_id", "") or f"{task}_{seed:03d}_{platform}"414    log.wall_seconds = round(time.time() - t0, 2)415 416    # Grader endpoint (optional)417    try:418        import requests419        g = requests.get(f"{client.base_url}/grader", timeout=30).json()420        log.grader_score = g.get("score")421    except Exception:422        pass423 424    if collect_tuples:425        # Attach episode-level identifiers to each decision tuple.426        for t in tuples:427            t["episode_id"] = log.episode_id428            t["grader_score"] = log.grader_score429        return log, tuples430    return log431 432 433# ---------------------------------------------------------------------------434# Public entry point435# ---------------------------------------------------------------------------436 437def run_evaluation(438    model_name: str,439    call_llm: Callable[[str], str],440    platform: str,441    base_url: str = "http://localhost:8000",442    tasks: Optional[List[str]] = None,443    seeds: Optional[List[int]] = None,444    out_path: Optional[str] = None,445) -> List[EpisodeLog]:446    tasks = tasks or ["easy", "medium", "hard"]447    seeds = seeds or [0, 1, 2]448    seeds = _seeds_for_platform(seeds, platform)449 450    out_path = out_path or str(451        _PARENT / "eval-models" / "results" /452        f"{model_name.replace('/', '_')}_{platform.lower()}_results.jsonl"453    )454    Path(out_path).parent.mkdir(parents=True, exist_ok=True)455 456    print(f"\n{'='*70}")457    print(f"Round 2 evaluation | model={model_name} | platform={platform}")458    print(f"Target: {base_url} | tasks={tasks} | seeds={seeds}")459    print(f"Log:    {out_path}")460    print(f"{'='*70}")461 462    logs: List[EpisodeLog] = []463    client = FakeGangEnvClient(base_url=base_url)464 465    with open(out_path, "w") as f:466        for task in tasks:467            for seed in seeds:468                print(f"\n--- episode: task={task} seed={seed} ---")469                try:470                    log = _run_episode(client, model_name, platform, task, seed, call_llm)471                except Exception as e:472                    print(f"  ✗ episode failed: {e}")473                    log = EpisodeLog(474                        model=model_name, platform=platform, task=task, seed=seed,475                        final_message=f"EXCEPTION: {e}",476                    )477                logs.append(log)478                f.write(json.dumps(asdict(log)) + "\n")479                f.flush()480                print(481                    f"  → steps={log.steps_taken} inspected={log.inspected} "482                    f"flagged={log.flagged} dp1={log.dp1_calls}(bad={log.dp1_invalid}) "483                    f"dp2={log.dp2_calls}(bad={log.dp2_invalid}) "484                    f"reward={log.reward} grader={log.grader_score} ({log.wall_seconds}s)"485                )486 487    # Summary488    print(f"\n{'='*70}")489    print("SUMMARY")490    print(f"{'='*70}")491    rewards = [l.reward for l in logs if l.reward is not None]492    graders = [l.grader_score for l in logs if l.grader_score is not None]493    if rewards:494        print(f"  mean reward:  {sum(rewards)/len(rewards):.4f}  (n={len(rewards)})")495    if graders:496        print(f"  mean grader:  {sum(graders)/len(graders):.4f}  (n={len(graders)})")497    total_dp1 = sum(l.dp1_calls for l in logs)498    total_dp2 = sum(l.dp2_calls for l in logs)499    bad_dp1 = sum(l.dp1_invalid for l in logs)500    bad_dp2 = sum(l.dp2_invalid for l in logs)501    print(f"  DP1 calls:    {total_dp1}  (invalid: {bad_dp1})")502    print(f"  DP2 calls:    {total_dp2}  (invalid: {bad_dp2})")503    print(f"  Logged to:    {out_path}")504    return logs505 506 507# ---------------------------------------------------------------------------508# Standard CLI argument parser (reused by every model shim)509# ---------------------------------------------------------------------------510 511def build_cli():512    import argparse513    parser = argparse.ArgumentParser(description="Round 2 eval runner")514    parser.add_argument("--url", default=os.getenv("API_BASE_URL_ENV", "http://localhost:8000"),515                        help="Environment server URL")516    parser.add_argument("--platform", default="Instagram", help="Platform name (Instagram/Snapchat/...)")517    parser.add_argument("--tasks", nargs="+", default=["easy", "medium", "hard"])518    parser.add_argument("--seeds", nargs="+", type=int, default=[0, 1, 2])519    parser.add_argument("--out", default=None, help="Output JSONL path")520    return parser521