AlexWortega commited on
Commit
f2e1333
·
verified ·
1 Parent(s): 3909a82

code: eval_agentic.py

Browse files
Files changed (1) hide show
  1. code/eval_agentic.py +243 -0
code/eval_agentic.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Agentic evaluation for jev cross-encoders: BFCL v4 and held-out Mind2Web websites. Nothing here is trained on.
3
+
4
+ python eval_agentic.py --models ckpt/qwen3.5-0.8b-nli-v2s-agent --out results/v2s/agentic.json
5
+
6
+ Tasks (score = P(entailment)):
7
+ bfcl_relevance BFCL v4 irrelevance + live_relevance: premise = request + the available functions, hypothesis =
8
+ "One of the available functions can serve this request." Gold: relevance yes / irrelevance no.
9
+ AUROC + balanced accuracy at 0.5.
10
+ bfcl_call BFCL v4 multiple / live_multiple: the gold call against the other functions in the same item,
11
+ phrased as "The correct call is f(args)". Rank-1 accuracy over the candidate calls.
12
+ taubench tau2-bench simulation results (sierra-research/tau2-bench, data/tau2/results/final): premise =
13
+ domain policy + conversation. Two labels come with each simulation — the 0/1 task reward, and
14
+ each `nl_assertion` ("Agent does not cancel insurance or offer a refund.") with met / not met.
15
+ telecom is the headline: tau-bench v1 (the training source) has no telecom domain, so the policy,
16
+ the tools and the tasks are all unseen. retail / airline are reported too, but their v1 policies
17
+ were in training, so only the traces are new there.
18
+ mind2web the websites data_mix.py `agentic2` held out (mind2web_heldout_websites.json): pick the next
19
+ action among the step's own actions. Rank-1 accuracy, plus "is the task finished" accuracy.
20
+ """
21
+ import argparse
22
+ import json
23
+ import os
24
+ import random
25
+ import urllib.request
26
+ from collections import defaultdict
27
+
28
+ import numpy as np
29
+
30
+ from eval import ENT, NLIScorer
31
+ from eval_extra import Window, auroc, bacc, fetch
32
+
33
+ BFCL = ("https://raw.githubusercontent.com/ShishirPatil/gorilla/main/berkeley-function-call-leaderboard/"
34
+ "bfcl_eval/data/")
35
+ RELEVANT = "One of the available functions can serve this request."
36
+
37
+
38
+ def load_bfcl(name, cache, answers=False):
39
+ url = BFCL + (f"possible_answer/{name}" if answers else name)
40
+ path = fetch(url, cache)
41
+ return [json.loads(line) for line in open(path) if line.strip()]
42
+
43
+
44
+ def user_text(item):
45
+ msgs = item["question"][0] if item["question"] and isinstance(item["question"][0], list) else item["question"]
46
+ return "\n".join(m.get("content", "") for m in msgs if m.get("role") == "user")
47
+
48
+
49
+ def fn_text(fn, limit=400):
50
+ params = (fn.get("parameters") or {}).get("properties") or {}
51
+ args = ", ".join(f"{k}: {(v or {}).get('type', '?')}" for k, v in list(params.items())[:8])
52
+ return f"{fn.get('name')}({args}) — {(fn.get('description') or '')[:limit]}"
53
+
54
+
55
+ def eval_bfcl_relevance(w, args):
56
+ rows = []
57
+ for name, gold in (("BFCL_v4_irrelevance.json", 0), ("BFCL_v4_live_relevance.json", 1)):
58
+ for it in load_bfcl(name, args.cache):
59
+ funcs = "\n".join(fn_text(f) for f in (it.get("function") or [])[:20])
60
+ rows.append((f"User request: {user_text(it)}\n\nAvailable functions:\n{funcs}", gold))
61
+ if args.limit:
62
+ rows = random.Random(0).sample(rows, min(args.limit * 4, len(rows)))
63
+ p = w.probs([(prem, RELEVANT) for prem, _ in rows])[:, ENT]
64
+ y = [g for _, g in rows]
65
+ return {"n": len(y), "pos_rate": float(np.mean(y)), "auroc": auroc(y, p), "bacc@0.5": bacc(y, p)}
66
+
67
+
68
+ def call_text(name, params):
69
+ args = ", ".join(f"{k}={json.dumps(v[0] if isinstance(v, list) and v else v, ensure_ascii=False)}"
70
+ for k, v in (params or {}).items())
71
+ return f"The correct call is {name}({args})."
72
+
73
+
74
+ def eval_bfcl_call(w, args):
75
+ res = {}
76
+ for name in ("BFCL_v4_multiple.json", "BFCL_v4_live_multiple.json"):
77
+ items = {it["id"]: it for it in load_bfcl(name, args.cache)}
78
+ golds = {g["id"]: g for g in load_bfcl(name, args.cache, answers=True)}
79
+ pairs, owner, gold_idx = [], [], {}
80
+ ids = list(items)
81
+ if args.limit:
82
+ ids = random.Random(0).sample(ids, min(args.limit, len(ids)))
83
+ for i, tid in enumerate(ids):
84
+ it, g = items[tid], golds.get(tid)
85
+ if not g or not g.get("ground_truth"):
86
+ continue
87
+ truth = g["ground_truth"][0]
88
+ gold_name = next(iter(truth))
89
+ funcs = it.get("function") or []
90
+ names = [f.get("name") for f in funcs]
91
+ if gold_name not in names or len(names) < 2:
92
+ continue
93
+ prem = f"User request: {user_text(it)}\n\nAvailable functions:\n" + "\n".join(fn_text(f) for f in funcs[:20])
94
+ gold_idx[i] = len(pairs)
95
+ pairs.append((prem, call_text(gold_name, truth[gold_name]))); owner.append(i)
96
+ for other in [n for n in names if n != gold_name][:4]:
97
+ pairs.append((prem, call_text(other, truth[gold_name]))); owner.append(i)
98
+ if not pairs:
99
+ continue
100
+ p = w.probs(pairs)[:, ENT]
101
+ by = defaultdict(list)
102
+ for j, i in enumerate(owner):
103
+ by[i].append((j, p[j]))
104
+ hits = [max(v, key=lambda x: x[1])[0] == gold_idx[i] for i, v in by.items() if i in gold_idx]
105
+ res[name.replace("BFCL_v4_", "").replace(".json", "")] = {"n": len(hits), "rank1_acc": float(np.mean(hits))}
106
+ return res
107
+
108
+
109
+ def eval_mind2web(w, args):
110
+ from datasets import load_dataset
111
+ held = set(json.load(open(args.m2w_heldout))) if os.path.exists(args.m2w_heldout) else set()
112
+ ds = load_dataset("osunlp/Mind2Web", split="train")
113
+ steps, done_rows = [], []
114
+ for ex in ds:
115
+ if held and ex["website"] not in held:
116
+ continue
117
+ reprs = ex["action_reprs"]
118
+ for i, act in enumerate(reprs):
119
+ history = " -> ".join(reprs[max(0, i - 3):i]) or "(nothing yet)"
120
+ prem = (f"Goal: {ex['confirmed_task']}\nWebsite: {ex['website']}\nActions so far: {history}")
121
+ others = [a for a in reprs if a != act]
122
+ if len(others) < 3:
123
+ continue
124
+ cands = [act] + random.Random(i).sample(others, 3)
125
+ steps.append((prem, cands))
126
+ done_rows.append((prem, int(i + 1 == len(reprs))))
127
+ if args.limit:
128
+ keep = random.Random(0).sample(range(len(steps)), min(args.limit * 3, len(steps)))
129
+ steps = [steps[i] for i in keep]; done_rows = [done_rows[i] for i in keep]
130
+ pairs, owner, gold_at = [], [], {}
131
+ for i, (prem, cands) in enumerate(steps):
132
+ gold_at[i] = len(pairs) # cands[0] is the gold action
133
+ for c in cands:
134
+ pairs.append((prem, f"The next action is: {c}")); owner.append(i)
135
+ p = w.probs(pairs)[:, ENT]
136
+ best = defaultdict(lambda: (-1.0, -1))
137
+ for j, i in enumerate(owner):
138
+ if p[j] > best[i][0]:
139
+ best[i] = (float(p[j]), j)
140
+ hits = [best[i][1] == gold_at[i] for i in range(len(steps))]
141
+ pd = w.probs([(prem, "This is the last action needed to complete the task.") for prem, _ in done_rows])[:, ENT]
142
+ y = [g for _, g in done_rows]
143
+ return {"websites_held_out": sorted(held), "n_steps": len(steps), "next_action_rank1": float(np.mean(hits)),
144
+ "task_finished_auroc": auroc(y, pd), "task_finished_bacc@0.5": bacc(y, pd)}
145
+
146
+
147
+ TAU2 = ("https://raw.githubusercontent.com/sierra-research/tau2-bench/main/data/tau2/results/final/"
148
+ "{model}_{domain}_default_gpt-4.1-2025-04-14_4trials.json")
149
+ TAU2_RUNS = [("gpt-4.1-2025-04-14", d) for d in ("telecom", "airline", "retail")]
150
+ TAU_DONE = "The agent followed the domain policy and resolved the user's request."
151
+
152
+
153
+ def tau2_text(messages, policy, policy_chars=6000, trace_chars=6000):
154
+ lines = []
155
+ for m in messages:
156
+ role = m.get("role")
157
+ for c in (m.get("tool_calls") or []):
158
+ fn = c.get("function") or c
159
+ lines.append(f"{role} calls {fn.get('name')}({str(fn.get('arguments'))[:200]})")
160
+ text = (m.get("content") or "").strip()
161
+ if text:
162
+ lines.append(f"{role}: {text[:400]}")
163
+ return f"Domain policy:\n{policy[:policy_chars]}\n\nConversation:\n" + "\n".join(lines)[-trace_chars:]
164
+
165
+
166
+ def eval_taubench(w, args):
167
+ res = {}
168
+ for model, domain in TAU2_RUNS:
169
+ try:
170
+ path = fetch(TAU2.format(model=model, domain=domain), args.cache)
171
+ data = json.load(open(path))
172
+ except Exception as e: # noqa: BLE001
173
+ res[domain] = {"error": f"{type(e).__name__}: {str(e)[:120]}"}
174
+ continue
175
+ # airline/retail ship policy.md; telecom splits its policy into main_policy.md + the tech-support manual
176
+ policy = ""
177
+ for fn in ("policy.md", "main_policy.md", "tech_support_workflow.md"):
178
+ try:
179
+ policy += open(fetch(f"https://raw.githubusercontent.com/sierra-research/tau2-bench/main/"
180
+ f"data/tau2/domains/{domain}/{fn}", args.cache)).read() + "\n"
181
+ except Exception: # noqa: BLE001 - a domain has one layout or the other, never both
182
+ continue
183
+ sims = data.get("simulations") or []
184
+ if args.limit:
185
+ sims = random.Random(0).sample(sims, min(args.limit, len(sims)))
186
+ traj_pairs, traj_y, as_pairs, as_y = [], [], [], []
187
+ for s in sims:
188
+ info = s.get("reward_info") or {}
189
+ prem = tau2_text(s.get("messages") or [], policy)
190
+ traj_pairs.append((prem, TAU_DONE)); traj_y.append(int(float(info.get("reward", 0)) >= 1.0))
191
+ for a in (info.get("nl_assertions") or []):
192
+ text = (a.get("nl_assertion") or "").strip()
193
+ if text:
194
+ as_pairs.append((prem, text)); as_y.append(int(bool(a.get("met"))))
195
+ out = {"n_sims": len(sims)}
196
+ if traj_pairs:
197
+ p = w.probs(traj_pairs)[:, ENT]
198
+ out["task_success"] = {"n": len(traj_y), "pos_rate": float(np.mean(traj_y)),
199
+ "auroc": auroc(traj_y, p), "bacc@0.5": bacc(traj_y, p)}
200
+ if as_pairs:
201
+ p = w.probs(as_pairs)[:, ENT]
202
+ out["nl_assertions"] = {"n": len(as_y), "pos_rate": float(np.mean(as_y)),
203
+ "auroc": auroc(as_y, p), "bacc@0.5": bacc(as_y, p)}
204
+ res[domain] = out
205
+ return res
206
+
207
+
208
+ TASKS = {"taubench": eval_taubench, "bfcl_relevance": eval_bfcl_relevance, "bfcl_call": eval_bfcl_call, "mind2web": eval_mind2web}
209
+
210
+
211
+ def main():
212
+ ap = argparse.ArgumentParser()
213
+ ap.add_argument("--models", nargs="+", required=True)
214
+ ap.add_argument("--out", required=True)
215
+ ap.add_argument("--tasks", nargs="+", default=list(TASKS))
216
+ ap.add_argument("--bs", type=int, default=16)
217
+ ap.add_argument("--max-len", type=int, default=4096)
218
+ ap.add_argument("--limit", type=int, default=0)
219
+ ap.add_argument("--cache", default="data/extra_cache/bfcl")
220
+ ap.add_argument("--m2w-heldout", default=os.path.expanduser("~/qwen_nli/nli_stage4/mind2web_heldout_websites.json"))
221
+ args = ap.parse_args()
222
+ res = json.load(open(args.out)) if os.path.exists(args.out) else {}
223
+ for m in args.models:
224
+ w = Window(NLIScorer(m, bs=args.bs, max_len=args.max_len), args.max_len)
225
+ res.setdefault(m, {})
226
+ for t in args.tasks:
227
+ print(f"== {m} :: {t}", flush=True)
228
+ try:
229
+ res[m][t] = TASKS[t](w, args)
230
+ except Exception as e: # noqa: BLE001
231
+ import traceback
232
+ traceback.print_exc()
233
+ res[m][t] = {"error": f"{type(e).__name__}: {str(e)[:200]}"}
234
+ print(json.dumps(res[m][t])[:400], flush=True)
235
+ os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
236
+ json.dump(res, open(args.out, "w"), indent=2)
237
+ del w
238
+ import torch
239
+ torch.cuda.empty_cache()
240
+
241
+
242
+ if __name__ == "__main__":
243
+ main()