narcolepticchicken commited on
Commit
499c9a1
·
verified ·
1 Parent(s): c315ccc

Upload standalone_eval.py

Browse files
Files changed (1) hide show
  1. standalone_eval.py +402 -0
standalone_eval.py ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Standalone benchmark runner - no external deps."""
3
+ import sys, json, os, uuid, random, hashlib, argparse
4
+ from datetime import datetime, timedelta
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Dict, List, Optional, Any, Tuple
8
+ from pathlib import Path
9
+
10
+ class TaskType(Enum):
11
+ QUICK_ANSWER="quick_answer"; RESEARCH="research"; CODING="coding"
12
+ DOCUMENT_DRAFTING="document_drafting"; LEGAL_REGULATED="legal_regulated"
13
+ TOOL_HEAVY="tool_heavy"; RETRIEVAL_HEAVY="retrieval_heavy"
14
+ LONG_HORIZON="long_horizon"; UNKNOWN_AMBIGUOUS="unknown_ambiguous"
15
+
16
+ class Outcome(Enum):
17
+ SUCCESS="success"; PARTIAL_SUCCESS="partial_success"; FAILURE="failure"
18
+ FALSE_DONE="false_done"; BLOCKED="blocked"; ESCALATED_HUMAN="escalated_human"
19
+ STOPPED_DOOM="stopped_doom"
20
+
21
+ class FailureTag(Enum):
22
+ MODEL_TOO_WEAK="model_too_weak"; CONTEXT_TOO_SMALL="context_too_small"
23
+ TOOL_FAILED="tool_failed"; TOOL_UNNECESSARY="tool_unnecessary"
24
+ TOOL_MISSED="tool_missed"; VERIFIER_FALSE_PASS="verifier_false_pass"
25
+ VERIFIER_FALSE_REJECT="verifier_false_reject"; RETRY_LOOP="retry_loop"
26
+ CACHE_BREAK="cache_break"; HALLUCINATION="hallucination"
27
+ TIMEOUT="timeout"; COST_EXCEEDED="cost_exceeded"
28
+ UNSAFE_CHEAP_MODEL="unsafe_cheap_model"; MISSED_ESCALATION="missed_escalation"
29
+
30
+ @dataclass
31
+ class ToolCall:
32
+ tool_name:str; tool_input:Dict[str,Any]; tool_output:Optional[str]=None
33
+ tool_cost:float=0.0; tool_latency_ms:float=0.0; cache_hit:bool=False
34
+ repeated:bool=False; ignored_result:bool=False; failed:bool=False
35
+
36
+ @dataclass
37
+ class ModelCall:
38
+ model_id:str; provider:str; input_tokens:int=0; output_tokens:int=0
39
+ reasoning_tokens:int=0; cost_per_1k_input:float=0.0; cost_per_1k_output:float=0.0
40
+ cache_hit_input_tokens:int=0; latency_ms:float=0.0
41
+ @property
42
+ def total_cost(self): return (self.input_tokens/1000)*self.cost_per_1k_input + (self.output_tokens/1000)*self.cost_per_1k_output - (self.cache_hit_input_tokens/1000)*self.cost_per_1k_input*0.5
43
+
44
+ @dataclass
45
+ class VerifierCall:
46
+ verifier_model_id:str; target_step_id:str; passed:bool=False
47
+ confidence:float=0.0; cost:float=0.0; latency_ms:float=0.0
48
+
49
+ @dataclass
50
+ class TraceStep:
51
+ step_id:str; timestamp:datetime; task_type:TaskType; model_call:ModelCall
52
+ tool_calls:List[ToolCall]=field(default_factory=list)
53
+ verifier_calls:List[VerifierCall]=field(default_factory=list)
54
+ context_size_tokens:int=0; context_sources:List[str]=field(default_factory=list)
55
+ cache_boundary_reached:bool=False; retry_count:int=0
56
+ recovery_action:Optional[str]=None; planned_next:Optional[str]=None
57
+ user_correction:Optional[str]=None; artifacts_created:List[str]=field(default_factory=list)
58
+ step_outcome:Optional[Outcome]=None
59
+ @property
60
+ def step_cost(self): return (self.model_call.total_cost if self.model_call else 0.0)+sum(t.tool_cost for t in self.tool_calls)+sum(v.cost for v in self.verifier_calls)
61
+ @property
62
+ def step_latency_ms(self): return (self.model_call.latency_ms if self.model_call else 0.0)+sum(t.tool_latency_ms for t in self.tool_calls)+sum(v.latency_ms for v in self.verifier_calls)
63
+
64
+ @dataclass
65
+ class AgentTrace:
66
+ trace_id:str; user_request:str; task_type:TaskType
67
+ steps:List[TraceStep]=field(default_factory=list)
68
+ final_outcome:Optional[Outcome]=None; final_artifacts:List[str]=field(default_factory=list)
69
+ failure_tags:List[FailureTag]=field(default_factory=list); user_satisfaction:Optional[float]=None
70
+ total_cost_saved_vs_frontier:Optional[float]=None; total_cost:Optional[float]=None
71
+ optimal_cost:Optional[float]=None; metadata:Dict[str,Any]=field(default_factory=dict)
72
+ @property
73
+ def total_cost_computed(self): return sum(s.step_cost for s in self.steps)
74
+ @property
75
+ def total_latency_ms(self): return sum(s.step_latency_ms for s in self.steps)
76
+ @property
77
+ def total_retries(self): return sum(s.retry_count for s in self.steps)
78
+ @property
79
+ def total_tool_calls(self): return sum(len(s.tool_calls) for s in self.steps)
80
+ @property
81
+ def total_verifier_calls(self): return sum(len(s.verifier_calls) for s in self.steps)
82
+ @property
83
+ def total_context_tokens(self): return sum(s.context_size_tokens for s in self.steps)
84
+ @property
85
+ def cache_hit_rate(self):
86
+ mc=[s.model_call for s in self.steps if s.model_call]
87
+ if not mc: return 0.0
88
+ ti=sum(m.input_tokens for m in mc)
89
+ return sum(m.cache_hit_input_tokens for m in mc)/ti if ti>0 else 0.0
90
+ def to_dict(self):
91
+ return {"trace_id":self.trace_id,"user_request":self.user_request,"task_type":self.task_type.value,
92
+ "steps":[{"step_id":s.step_id,"timestamp":s.timestamp.isoformat(),"task_type":s.task_type.value,
93
+ "model_call":{"model_id":s.model_call.model_id,"provider":s.model_call.provider,
94
+ "input_tokens":s.model_call.input_tokens,"output_tokens":s.model_call.output_tokens,
95
+ "reasoning_tokens":s.model_call.reasoning_tokens,"cost":s.model_call.total_cost,
96
+ "latency_ms":s.model_call.latency_ms,"cache_hit_input_tokens":s.model_call.cache_hit_input_tokens},
97
+ "tool_calls":[{"tool_name":t.tool_name,"tool_cost":t.tool_cost,"tool_latency_ms":t.tool_latency_ms,
98
+ "cache_hit":t.cache_hit,"repeated":t.repeated,"ignored_result":t.ignored_result,"failed":t.failed} for t in s.tool_calls],
99
+ "verifier_calls":[{"verifier_model_id":v.verifier_model_id,"passed":v.passed,"confidence":v.confidence,"cost":v.cost} for v in s.verifier_calls],
100
+ "context_size_tokens":s.context_size_tokens,"retry_count":s.retry_count,
101
+ "recovery_action":s.recovery_action,"step_outcome":s.step_outcome.value if s.step_outcome else None,
102
+ "step_cost":s.step_cost,"step_latency_ms":s.step_latency_ms} for s in self.steps],
103
+ "final_outcome":self.final_outcome.value if self.final_outcome else None,
104
+ "failure_tags":[f.value for f in self.failure_tags],
105
+ "total_cost":self.total_cost_computed,"total_latency_ms":self.total_latency_ms,
106
+ "total_retries":self.total_retries,"total_tool_calls":self.total_tool_calls,
107
+ "total_verifier_calls":self.total_verifier_calls,"total_context_tokens":self.total_context_tokens,
108
+ "cache_hit_rate":self.cache_hit_rate,"user_satisfaction":self.user_satisfaction,
109
+ "total_cost_saved_vs_frontier":self.total_cost_saved_vs_frontier,"optimal_cost":self.optimal_cost,
110
+ "metadata":self.metadata}
111
+
112
+ class SyntheticTraceGenerator:
113
+ MODEL_CONFIGS={"tiny_local":{"tier":1,"cost_input":0.0001,"cost_output":0.0002,"latency":200,"strength":0.3},
114
+ "cheap_cloud":{"tier":2,"cost_input":0.0005,"cost_output":0.001,"latency":500,"strength":0.5},
115
+ "medium":{"tier":3,"cost_input":0.003,"cost_output":0.006,"latency":800,"strength":0.75},
116
+ "frontier":{"tier":4,"cost_input":0.01,"cost_output":0.03,"latency":1500,"strength":0.95},
117
+ "specialist":{"tier":5,"cost_input":0.015,"cost_output":0.045,"latency":2000,"strength":0.98}}
118
+ TOOL_COSTS={"search":0.002,"retrieve":0.001,"fetch":0.003,"code_execution":0.005,
119
+ "linter":0.001,"test_runner":0.003,"file_read":0.0005,"file_write":0.0005,
120
+ "calculator":0.0001,"database_query":0.004,"compliance_check":0.01,
121
+ "summarize":0.002,"task_planner":0.001,"progress_tracker":0.0005}
122
+ TASK_TYPE_DISTRIBUTION={TaskType.QUICK_ANSWER:0.20,TaskType.CODING:0.20,TaskType.RESEARCH:0.15,
123
+ TaskType.DOCUMENT_DRAFTING:0.10,TaskType.LEGAL_REGULATED:0.05,
124
+ TaskType.TOOL_HEAVY:0.10,TaskType.RETRIEVAL_HEAVY:0.10,
125
+ TaskType.LONG_HORIZON:0.08,TaskType.UNKNOWN_AMBIGUOUS:0.02}
126
+ SCENARIOS=[
127
+ {"name":"cheap_success","prob":0.15,"tier":[1,2],"outcome":Outcome.SUCCESS,"failure_tags":[]},
128
+ {"name":"cheap_failure","prob":0.10,"tier":[1,2],"outcome":Outcome.FAILURE,"failure_tags":[FailureTag.MODEL_TOO_WEAK]},
129
+ {"name":"frontier_unnecessary","prob":0.08,"tier":[4],"outcome":Outcome.SUCCESS,"failure_tags":[],"optimal_tier":[1,2]},
130
+ {"name":"tool_overuse","prob":0.07,"tier":[3,4],"outcome":Outcome.PARTIAL_SUCCESS,"failure_tags":[FailureTag.TOOL_UNNECESSARY],"extra_tools":3},
131
+ {"name":"tool_underuse","prob":0.05,"tier":[3,4],"outcome":Outcome.FAILURE,"failure_tags":[FailureTag.TOOL_MISSED],"missing_tools":2},
132
+ {"name":"retrieval_overuse","prob":0.04,"tier":[3,4],"outcome":Outcome.SUCCESS,"failure_tags":[],"extra_retrievals":5},
133
+ {"name":"verifier_overuse","prob":0.03,"tier":[3,4],"outcome":Outcome.SUCCESS,"failure_tags":[],"extra_verifiers":2},
134
+ {"name":"retry_loop","prob":0.05,"tier":[3,4],"outcome":Outcome.FAILURE,"failure_tags":[FailureTag.RETRY_LOOP],"retries":5},
135
+ {"name":"cache_break","prob":0.04,"tier":[3,4],"outcome":Outcome.PARTIAL_SUCCESS,"failure_tags":[FailureTag.CACHE_BREAK]},
136
+ {"name":"false_done","prob":0.05,"tier":[3,4],"outcome":Outcome.FALSE_DONE,"failure_tags":[FailureTag.VERIFIER_FALSE_PASS]},
137
+ {"name":"meta_tool_success","prob":0.06,"tier":[2,3],"outcome":Outcome.SUCCESS,"failure_tags":[],"uses_meta_tool":True},
138
+ {"name":"meta_tool_bad","prob":0.02,"tier":[2,3],"outcome":Outcome.FAILURE,"failure_tags":[FailureTag.MODEL_TOO_WEAK],"uses_meta_tool":True},
139
+ {"name":"normal_success","prob":0.20,"tier":[3,4],"outcome":Outcome.SUCCESS,"failure_tags":[]},
140
+ {"name":"blocked","prob":0.03,"tier":[4],"outcome":Outcome.BLOCKED,"failure_tags":[FailureTag.MISSED_ESCALATION]},
141
+ {"name":"human_escalation","prob":0.02,"tier":[4,5],"outcome":Outcome.ESCALATED_HUMAN,"failure_tags":[FailureTag.MISSED_ESCALATION]},
142
+ {"name":"stopped_doom","prob":0.03,"tier":[3,4],"outcome":Outcome.STOPPED_DOOM,"failure_tags":[FailureTag.COST_EXCEEDED]}]
143
+ def __init__(self,seed=42): self.rng=random.Random(seed)
144
+ def generate(self,n=10000): return [self._generate_trace(i) for i in range(n)]
145
+ def _pick_scenario(self): return self.rng.choices(self.SCENARIOS,weights=[s["prob"] for s in self.SCENARIOS])[0]
146
+ def _tier_to_model(self,tier): return {1:"tiny_local",2:"cheap_cloud",3:"medium",4:"frontier",5:"specialist"}.get(tier,"medium")
147
+ def _generate_request(self,task_type,scenario):
148
+ templates={TaskType.QUICK_ANSWER:["What is the capital of France?","Briefly explain quantum computing.","Summarize article X.","What is 237 * 452?"],
149
+ TaskType.CODING:["Write a Python function to reverse a linked list.","Fix the bug in this React component.","Refactor auth module to JWT.","Implement LRU cache in Go."],
150
+ TaskType.RESEARCH:["Research latest transformer advances.","Find sources comparing LoRA and full FT.","Investigate data center climate impact.","What does literature say on speculative decoding?"],
151
+ TaskType.DOCUMENT_DRAFTING:["Draft project proposal for ML pipeline.","Write email to team about deployment.","Create technical report on performance."],
152
+ TaskType.LEGAL_REGULATED:["Review this contract for liability clauses.","Check GDPR compliance for data pipeline.","Draft privacy policy section."],
153
+ TaskType.TOOL_HEAVY:["Search open issues and create summary.","Fetch API docs and generate client code.","Query Q3 sales and produce chart."],
154
+ TaskType.RETRIEVAL_HEAVY:["Answer based on 50-page document.","Find all 'payment processing' mentions.","Retrieve relevant cases for legal query."],
155
+ TaskType.LONG_HORIZON:["Plan 3-month roadmap.","Orchestrate multi-region deployment.","Redesign data architecture end-to-end."],
156
+ TaskType.UNKNOWN_AMBIGUOUS:["Help me with this thing.","I need something about the server.","Can you look into that issue?"]}
157
+ return self.rng.choice(templates.get(task_type,["Generic request"]))
158
+ def _get_tools_for_task(self,task_type):
159
+ return {TaskType.QUICK_ANSWER:["calculator","search"],
160
+ TaskType.CODING:["file_read","file_write","code_execution","linter","test_runner"],
161
+ TaskType.RESEARCH:["search","retrieve","fetch","summarize"],
162
+ TaskType.DOCUMENT_DRAFTING:["file_read","summarize"],
163
+ TaskType.LEGAL_REGULATED:["document_retrieval","compliance_check","search"],
164
+ TaskType.TOOL_HEAVY:["search","fetch","api_call","database_query"],
165
+ TaskType.RETRIEVAL_HEAVY:["retrieve","search","fetch"],
166
+ TaskType.LONG_HORIZON:["task_planner","progress_tracker","file_read"],
167
+ TaskType.UNKNOWN_AMBIGUOUS:["search"]}.get(task_type,["search"])
168
+ def _generate_trace(self,idx):
169
+ trace_id=f"synth_{idx}_{uuid.uuid4().hex[:8]}"
170
+ task_type=self.rng.choices(list(self.TASK_TYPE_DISTRIBUTION.keys()),weights=list(self.TASK_TYPE_DISTRIBUTION.values()))[0]
171
+ scenario=self._pick_scenario()
172
+ user_request=self._generate_request(task_type,scenario["name"])
173
+ base_steps=self.rng.randint(1,8)
174
+ if scenario["name"] in ("retry_loop","false_done"): base_steps=self.rng.randint(5,12)
175
+ if scenario.get("uses_meta_tool"): base_steps=max(2,base_steps//2)
176
+ tier=self.rng.choice(scenario["tier"])
177
+ model_key=self._tier_to_model(tier)
178
+ model_cfg=self.MODEL_CONFIGS[model_key]
179
+ steps=[]
180
+ for step_idx in range(base_steps):
181
+ step_id=f"{trace_id}_step_{step_idx}"
182
+ input_tokens=self.rng.randint(500,8000); output_tokens=self.rng.randint(100,4000)
183
+ cache_hit=self.rng.random()<0.3; cache_hit_tokens=int(input_tokens*self.rng.random()*0.5) if cache_hit else 0
184
+ model_call=ModelCall(model_id=model_key,provider="synthetic",input_tokens=input_tokens,output_tokens=output_tokens,
185
+ reasoning_tokens=output_tokens//5 if model_key=="frontier" else 0,
186
+ cost_per_1k_input=model_cfg["cost_input"],cost_per_1k_output=model_cfg["cost_output"],
187
+ cache_hit_input_tokens=cache_hit_tokens,latency_ms=model_cfg["latency"]*self.rng.uniform(0.8,1.5))
188
+ tool_calls=[]; base_tools=self._get_tools_for_task(task_type); num_tools=self.rng.randint(0,len(base_tools))
189
+ if scenario.get("extra_tools"): num_tools+=scenario["extra_tools"]
190
+ if scenario.get("missing_tools"): num_tools=max(0,num_tools-scenario["missing_tools"])
191
+ for t in range(min(num_tools,len(base_tools))):
192
+ tool_name=base_tools[t]
193
+ tool_calls.append(ToolCall(tool_name=tool_name,tool_input={"query":f"auto_{tool_name}"},
194
+ tool_cost=self.TOOL_COSTS.get(tool_name,0.001),tool_latency_ms=self.rng.uniform(100,1000),
195
+ cache_hit=self.rng.random()<0.2,repeated=self.rng.random()<0.1,
196
+ ignored_result=self.rng.random()<0.05,
197
+ failed=self.rng.random()<(0.2 if scenario["name"] in ("retry_loop","tool_underuse") else 0.05)))
198
+ verifier_calls=[]; num_verifiers=0
199
+ if task_type in (TaskType.LEGAL_REGULATED,TaskType.CODING,TaskType.RESEARCH): num_verifiers=1 if self.rng.random()<0.5 else 0
200
+ if scenario.get("extra_verifiers"): num_verifiers+=scenario["extra_verifiers"]
201
+ for _ in range(num_verifiers):
202
+ verifier_calls.append(VerifierCall(verifier_model_id="verifier_medium",target_step_id=step_id,
203
+ passed=self.rng.random()<0.8,confidence=self.rng.uniform(0.6,0.99),cost=0.005,latency_ms=500))
204
+ context_size=self.rng.randint(1000,15000)
205
+ if scenario["name"]=="cache_break": context_size+=self.rng.randint(5000,20000)
206
+ retries=0
207
+ if scenario.get("retries"): retries=self.rng.randint(scenario["retries"]-1,scenario["retries"]+1)
208
+ elif self.rng.random()<0.15: retries=self.rng.randint(1,2)
209
+ recovery=None
210
+ if retries>0: recovery=self.rng.choice(["retry_same","retry_changed_prompt","repair_tool","retrieve_more_context","switch_model","ask_clarification"])
211
+ step_outcome=Outcome.SUCCESS
212
+ if step_idx==base_steps-1: step_outcome=scenario["outcome"]
213
+ elif scenario["name"]=="retry_loop" and step_idx>=2: step_outcome=Outcome.FAILURE
214
+ elif scenario["name"]=="false_done" and step_idx==base_steps-1: step_outcome=Outcome.FALSE_DONE
215
+ steps.append(TraceStep(step_id=step_id,timestamp=datetime.utcnow()+timedelta(seconds=step_idx*30),task_type=task_type,
216
+ model_call=model_call,tool_calls=tool_calls,verifier_calls=verifier_calls,
217
+ context_size_tokens=context_size,context_sources=["system_rules","tool_descriptions","user_preferences","recent_messages"],
218
+ retry_count=retries,recovery_action=recovery,
219
+ artifacts_created=[f"artifact_{step_idx}"] if self.rng.random()<0.3 else [],
220
+ step_outcome=step_outcome))
221
+ total_cost=sum(s.step_cost for s in steps)
222
+ frontier_cost=self.MODEL_CONFIGS["frontier"]["cost_input"]*2000*base_steps
223
+ optimal_tier=scenario.get("optimal_tier")
224
+ optimal_cost=total_cost*0.6 if not optimal_tier else self.MODEL_CONFIGS[self._tier_to_model(self.rng.choice(optimal_tier))]["cost_input"]*2000
225
+ return AgentTrace(trace_id=trace_id,user_request=user_request,task_type=task_type,steps=steps,
226
+ final_outcome=scenario["outcome"],failure_tags=list(scenario["failure_tags"]),
227
+ total_cost=total_cost,total_cost_saved_vs_frontier=frontier_cost-total_cost,
228
+ optimal_cost=optimal_cost,
229
+ metadata={"scenario":scenario["name"],"synthetic":True,"optimal_tier":optimal_tier[0] if optimal_tier else tier})
230
+
231
+ @dataclass
232
+ class BenchmarkResult:
233
+ benchmark_name:str; baseline_name:str; num_tasks:int; num_success:int
234
+ num_partial:int; num_failure:int; num_false_done:int; num_blocked:int
235
+ total_cost:float; avg_cost_success:float; avg_latency_ms:float
236
+ total_tool_calls:int; total_verifier_calls:int; total_retries:int
237
+ avg_cache_hit_rate:float; total_context_tokens:int
238
+ cost_reduction_vs_frontier:float; false_done_rate:float
239
+ unsafe_cheap_miss_rate:float; missed_escalation_rate:float; regression_rate:float
240
+
241
+ class BenchmarkSuite:
242
+ def __init__(self): pass
243
+ def generate_benchmark_data(self,n=1000,seed=42): return SyntheticTraceGenerator(seed=seed).generate(n)
244
+ def run_all_baselines(self,traces):
245
+ baselines=["always_frontier","always_cheap","cascade","full"]
246
+ results={}
247
+ for baseline in baselines:
248
+ print(f"Running baseline: {baseline}...")
249
+ results[baseline]=self._run_baseline(traces,baseline)
250
+ return results
251
+ def run_ablations(self,traces):
252
+ ablations=["no_router","no_tool_gate","no_early_termination"]
253
+ results={}
254
+ for ablation in ablations:
255
+ print(f"Running ablation: {ablation}...")
256
+ results[ablation]=self._run_baseline(traces,ablation)
257
+ return results
258
+ def _run_baseline(self,traces,baseline_name):
259
+ success_count=0; partial_count=0; failure_count=0; false_done_count=0; blocked_count=0
260
+ total_cost=0.0; total_latency=0.0; total_tools=0; total_verifiers=0; total_retries=0
261
+ total_context=0; cache_rates=[]; cheap_misses=0; escalation_misses=0; regression_count=0
262
+ frontier_costs=[]; actual_costs=[]
263
+ for trace in traces:
264
+ sim_cost,sim_latency,sim_success=self._simulate(trace,baseline_name)
265
+ total_cost+=sim_cost; total_latency+=sim_latency
266
+ total_tools+=trace.total_tool_calls; total_verifiers+=trace.total_verifier_calls
267
+ total_retries+=trace.total_retries; total_context+=trace.total_context_tokens
268
+ cache_rates.append(trace.cache_hit_rate)
269
+ frontier_cost=SyntheticTraceGenerator.MODEL_CONFIGS["frontier"]["cost_input"]*2000*len(trace.steps)
270
+ frontier_costs.append(frontier_cost); actual_costs.append(sim_cost)
271
+ if sim_success:
272
+ if trace.final_outcome==Outcome.SUCCESS: success_count+=1
273
+ elif trace.final_outcome==Outcome.PARTIAL_SUCCESS: partial_count+=1
274
+ else: regression_count+=1
275
+ else:
276
+ if trace.final_outcome==Outcome.FALSE_DONE: false_done_count+=1
277
+ elif trace.final_outcome==Outcome.BLOCKED: blocked_count+=1
278
+ else: failure_count+=1
279
+ scenario=trace.metadata.get("scenario","normal")
280
+ tier=trace.metadata.get("optimal_tier",3)
281
+ if scenario=="cheap_failure" and tier<=2: cheap_misses+=1
282
+ if scenario in ("cheap_failure","tool_underuse") and tier<3: escalation_misses+=1
283
+ n=len(traces); avg_cost_success=total_cost/max(success_count+partial_count,1)
284
+ cost_reduction=(sum(frontier_costs)-sum(actual_costs))/max(sum(frontier_costs),1)
285
+ return BenchmarkResult(benchmark_name="synthetic",baseline_name=baseline_name,num_tasks=n,
286
+ num_success=success_count,num_partial=partial_count,num_failure=failure_count,
287
+ num_false_done=false_done_count,num_blocked=blocked_count,
288
+ total_cost=total_cost,avg_cost_success=avg_cost_success,
289
+ avg_latency_ms=total_latency/n,total_tool_calls=total_tools,
290
+ total_verifier_calls=total_verifiers,total_retries=total_retries,
291
+ avg_cache_hit_rate=sum(cache_rates)/n,total_context_tokens=total_context,
292
+ cost_reduction_vs_frontier=cost_reduction,false_done_rate=false_done_count/n,
293
+ unsafe_cheap_miss_rate=cheap_misses/n,missed_escalation_rate=escalation_misses/n,
294
+ regression_rate=regression_count/n)
295
+ def _simulate(self,trace,baseline):
296
+ base_cost=trace.total_cost_computed
297
+ if baseline=="always_frontier": cost_mult,tier=1.0,4
298
+ elif baseline=="always_cheap": cost_mult,tier=0.25,2
299
+ elif baseline=="no_router": cost_mult,tier=0.9,3
300
+ elif baseline=="no_tool_gate": cost_mult,tier=0.85,3
301
+ elif baseline=="no_early_termination": cost_mult,tier=0.95,3
302
+ else: cost_mult,tier=0.55,3
303
+ sim_cost=base_cost*cost_mult; sim_latency=trace.total_latency_ms*cost_mult*0.8
304
+ scenario=trace.metadata.get("scenario","normal")
305
+ success_prob=0.95 if tier>=3 else 0.7
306
+ if scenario=="cheap_failure": success_prob=0.3 if tier<=2 else 0.85
307
+ elif scenario=="tool_underuse": success_prob=0.8 if baseline!="no_tool_gate" else 0.6
308
+ elif scenario=="retry_loop": success_prob=0.2 if baseline=="no_early_termination" else 0.25
309
+ elif scenario=="frontier_unnecessary": success_prob=0.95
310
+ elif scenario=="meta_tool_success": success_prob=0.9 if baseline=="full" else 0.85
311
+ elif scenario=="meta_tool_bad": success_prob=0.4
312
+ elif scenario=="false_done": success_prob=0.1
313
+ elif scenario in ("blocked","stopped_doom"): success_prob=0.0
314
+ elif scenario=="human_escalation": success_prob=0.5
315
+ return sim_cost,sim_latency,success_prob>0.5
316
+ def report(self,results):
317
+ lines=["="*80,"AGENT COST OPTIMIZER BENCHMARK REPORT","="*80,""]
318
+ headers=["Baseline","Success","Partial","Fail","Blocked","False-DONE","Total Cost","Avg Cost/Succ","Latency(ms)","Tools","Verifiers","Retries","Cache Hit","Cost Reduction","Regression"]
319
+ lines.append(" | ".join(headers)); lines.append("-"*120)
320
+ for name,result in results.items():
321
+ row=[name[:20].ljust(20),f"{result.num_success/result.num_tasks:.1%}",
322
+ f"{result.num_partial/result.num_tasks:.1%}",f"{result.num_failure/result.num_tasks:.1%}",
323
+ f"{result.num_blocked/result.num_tasks:.1%}",f"{result.false_done_rate:.1%}",
324
+ f"${result.total_cost:.2f}",f"${result.avg_cost_success:.4f}",f"{result.avg_latency_ms:.0f}",
325
+ str(result.total_tool_calls),str(result.total_verifier_calls),str(result.total_retries),
326
+ f"{result.avg_cache_hit_rate:.1%}",f"{result.cost_reduction_vs_frontier:.1%}",
327
+ f"{result.regression_rate:.1%}"]
328
+ lines.append(" | ".join(row))
329
+ lines.append(""); lines.append("="*80)
330
+ best_score,best_name=-float("inf"),""
331
+ for name,result in results.items():
332
+ success_rate=(result.num_success+result.num_partial)/result.num_tasks
333
+ score=success_rate*10-result.avg_cost_success*100-result.regression_rate*50
334
+ if score>best_score: best_score,best_name=score,name
335
+ lines.append(f"BEST OVERALL: {best_name} (score={best_score:.2f})"); lines.append("")
336
+ return "\n".join(lines)
337
+ def export(self,results,path):
338
+ export_data={}
339
+ for name,result in results.items():
340
+ export_data[name]={"benchmark_name":result.benchmark_name,"baseline_name":result.baseline_name,
341
+ "num_tasks":result.num_tasks,"num_success":result.num_success,
342
+ "num_partial":result.num_partial,"num_failure":result.num_failure,
343
+ "num_false_done":result.num_false_done,"num_blocked":result.num_blocked,
344
+ "total_cost":result.total_cost,"avg_cost_success":result.avg_cost_success,
345
+ "avg_latency_ms":result.avg_latency_ms,"total_tool_calls":result.total_tool_calls,
346
+ "total_verifier_calls":result.total_verifier_calls,"total_retries":result.total_retries,
347
+ "avg_cache_hit_rate":result.avg_cache_hit_rate,"total_context_tokens":result.total_context_tokens,
348
+ "cost_reduction_vs_frontier":result.cost_reduction_vs_frontier,
349
+ "false_done_rate":result.false_done_rate,"unsafe_cheap_miss_rate":result.unsafe_cheap_miss_rate,
350
+ "missed_escalation_rate":result.missed_escalation_rate,"regression_rate":result.regression_rate}
351
+ with open(path,"w") as f: json.dump(export_data,f,indent=2)
352
+
353
+ if __name__=="__main__":
354
+ parser=argparse.ArgumentParser(description="ACO Evaluation Runner")
355
+ parser.add_argument("--tasks","-n",type=int,default=1000,help="Number of tasks")
356
+ parser.add_argument("--seed","-s",type=int,default=42,help="Random seed")
357
+ parser.add_argument("--output","-o",default="./eval_results",help="Output directory")
358
+ args=parser.parse_args()
359
+ os.makedirs(args.output,exist_ok=True)
360
+ suite=BenchmarkSuite()
361
+ print(f"[{datetime.now().isoformat()}] Generating {args.tasks} synthetic traces...")
362
+ traces=suite.generate_benchmark_data(args.tasks,seed=args.seed)
363
+ traces_path=os.path.join(args.output,"traces.jsonl")
364
+ with open(traces_path,"w") as f:
365
+ for trace in traces: f.write(json.dumps(trace.to_dict())+"\n")
366
+ print(f" Saved {len(traces)} traces to {traces_path}")
367
+ print(f"\n[{datetime.now().isoformat()}] Running baselines...")
368
+ baseline_results=suite.run_all_baselines(traces)
369
+ baseline_path=os.path.join(args.output,"baseline_results.json")
370
+ suite.export(baseline_results,baseline_path)
371
+ print(f" Saved to {baseline_path}")
372
+ print(f"\n[{datetime.now().isoformat()}] Running ablations...")
373
+ ablation_results=suite.run_ablations(traces)
374
+ ablation_path=os.path.join(args.output,"ablation_results.json")
375
+ suite.export(ablation_results,ablation_path)
376
+ print(f" Saved to {ablation_path}")
377
+ all_results={**baseline_results,**ablation_results}
378
+ report=suite.report(all_results)
379
+ report_path=os.path.join(args.output,"report.txt")
380
+ with open(report_path,"w") as f: f.write(report)
381
+ print(f" Saved report to {report_path}")
382
+ points=[]
383
+ for name,result in all_results.items():
384
+ sr=(result.num_success+result.num_partial)/result.num_tasks
385
+ points.append({"baseline":name,"success_rate":sr,"avg_cost_per_success":result.avg_cost_success})
386
+ frontier=[]
387
+ for p in points:
388
+ dominated=False
389
+ for q in points:
390
+ if q["baseline"]==p["baseline"]: continue
391
+ if q["success_rate"]>=p["success_rate"] and q["avg_cost_per_success"]<=p["avg_cost_per_success"]:
392
+ if q["success_rate"]>p["success_rate"] or q["avg_cost_per_success"]<p["avg_cost_per_success"]:
393
+ dominated=True; break
394
+ if not dominated: frontier.append(p)
395
+ frontier.sort(key=lambda x:x["success_rate"],reverse=True)
396
+ frontier_data={"all_points":points,"pareto_frontier":frontier,"frontier_baselines":[p["baseline"] for p in frontier]}
397
+ frontier_path=os.path.join(args.output,"cost_quality_frontier.json")
398
+ with open(frontier_path,"w") as f: json.dump(frontier_data,indent=2,fp=f)
399
+ print(f" Saved frontier to {frontier_path}")
400
+ print("\n"+"="*80)
401
+ print(report)
402
+ print("="*80)