Delete router_models/bert_5class/*, router_models/bert_router/*, router_models/baarbundle.pkl, router_models/productionbundle.pkl, router_models/routerbundle*.pkl, router_models/*_success.json, router_models/v6_eval*, training/train_router_v[1-9]*.py, training/train_router_full.py, training/train_bert_5class*.py, training/train_baar_router.py, eval/eval_bert_part*.py, eval/eval_bert_router.py, eval/run_bert_eval*.py, eval/bert_results.json, eval/*v[8-9]*.json, eval/sw*.json
Browse files- eval/benchmark_v9_final.json +0 -73
- eval/benchmark_v9_results.json +0 -32
- eval/bert_results.json +0 -4
- eval/eval_bert_part2.py +0 -40
- eval/eval_bert_part3.py +0 -140
- eval/eval_bert_part4.py +0 -61
- eval/eval_bert_partB.py +0 -68
- eval/eval_bert_partC.py +0 -95
- eval/eval_bert_partD.py +0 -81
- eval/eval_bert_router.py +0 -61
- eval/run_bert_eval.py +0 -47
- eval/run_bert_eval_full.py +0 -66
- eval/run_bert_eval_launcher.py +0 -23
- eval/swe_bench_results.json +0 -27
- eval/swe_v10_results.json +0 -38
- eval/v8_results.json +0 -69
- router_models/bert_5class/config.json +0 -42
- router_models/bert_5class/metadata.json +0 -47
- router_models/bert_5class/model.safetensors +0 -3
- router_models/bert_5class/tokenizer.json +0 -0
- router_models/bert_5class/tokenizer_config.json +0 -15
- router_models/bert_router/config.json +0 -28
- router_models/bert_router/model.safetensors +0 -3
- router_models/bert_router/tokenizer.json +0 -0
- router_models/bert_router/tokenizer_config.json +0 -15
- router_models/tier_1_success.json +0 -0
- router_models/tier_2_success.json +0 -0
- router_models/tier_3_success.json +0 -0
- router_models/tier_4_success.json +0 -0
- router_models/tier_5_success.json +0 -0
- router_models/v6_eval_results.json +0 -304
- training/train_baar_router.py +0 -470
- training/train_bert_5class.py +0 -187
- training/train_bert_5class_v2.py +0 -291
- training/train_bert_5class_v3.py +0 -292
- training/train_router_full.py +0 -433
- training/train_router_v1.py +0 -433
- training/train_router_v2.py +0 -510
- training/train_router_v3.py +0 -262
- training/train_router_v4_production.py +0 -241
eval/benchmark_v9_final.json
DELETED
|
@@ -1,73 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"frontier": {
|
| 3 |
-
"success": 0.8996666666666666,
|
| 4 |
-
"cost": 1.0
|
| 5 |
-
},
|
| 6 |
-
"v8": {
|
| 7 |
-
"success": 0.8373333333333334,
|
| 8 |
-
"cost": 0.9151666666666726
|
| 9 |
-
},
|
| 10 |
-
"v9_e2.0_lc0.05": {
|
| 11 |
-
"success": 0.8996666666666666,
|
| 12 |
-
"cost": 0.9785500000000054,
|
| 13 |
-
"escalated": 238,
|
| 14 |
-
"ent_thr": 2.0,
|
| 15 |
-
"lc_thr": 0.05
|
| 16 |
-
},
|
| 17 |
-
"v9_e2.0_lc0.1": {
|
| 18 |
-
"success": 0.8996666666666666,
|
| 19 |
-
"cost": 0.9785500000000054,
|
| 20 |
-
"escalated": 238,
|
| 21 |
-
"ent_thr": 2.0,
|
| 22 |
-
"lc_thr": 0.1
|
| 23 |
-
},
|
| 24 |
-
"v9_e2.0_lc0.15": {
|
| 25 |
-
"success": 0.8996666666666666,
|
| 26 |
-
"cost": 0.9785500000000054,
|
| 27 |
-
"escalated": 238,
|
| 28 |
-
"ent_thr": 2.0,
|
| 29 |
-
"lc_thr": 0.15
|
| 30 |
-
},
|
| 31 |
-
"v9_e2.5_lc0.05": {
|
| 32 |
-
"success": 0.8576666666666667,
|
| 33 |
-
"cost": 0.9261833333333388,
|
| 34 |
-
"escalated": 94,
|
| 35 |
-
"ent_thr": 2.5,
|
| 36 |
-
"lc_thr": 0.05
|
| 37 |
-
},
|
| 38 |
-
"v9_e2.5_lc0.1": {
|
| 39 |
-
"success": 0.8576666666666667,
|
| 40 |
-
"cost": 0.9261833333333388,
|
| 41 |
-
"escalated": 94,
|
| 42 |
-
"ent_thr": 2.5,
|
| 43 |
-
"lc_thr": 0.1
|
| 44 |
-
},
|
| 45 |
-
"v9_e2.5_lc0.15": {
|
| 46 |
-
"success": 0.8576666666666667,
|
| 47 |
-
"cost": 0.9261833333333388,
|
| 48 |
-
"escalated": 94,
|
| 49 |
-
"ent_thr": 2.5,
|
| 50 |
-
"lc_thr": 0.15
|
| 51 |
-
},
|
| 52 |
-
"v9_e3.0_lc0.05": {
|
| 53 |
-
"success": 0.836,
|
| 54 |
-
"cost": 0.9132500000000056,
|
| 55 |
-
"escalated": 51,
|
| 56 |
-
"ent_thr": 3.0,
|
| 57 |
-
"lc_thr": 0.05
|
| 58 |
-
},
|
| 59 |
-
"v9_e3.0_lc0.1": {
|
| 60 |
-
"success": 0.834,
|
| 61 |
-
"cost": 0.9130833333333389,
|
| 62 |
-
"escalated": 49,
|
| 63 |
-
"ent_thr": 3.0,
|
| 64 |
-
"lc_thr": 0.1
|
| 65 |
-
},
|
| 66 |
-
"v9_e3.0_lc0.15": {
|
| 67 |
-
"success": 0.834,
|
| 68 |
-
"cost": 0.9130833333333389,
|
| 69 |
-
"escalated": 49,
|
| 70 |
-
"ent_thr": 3.0,
|
| 71 |
-
"lc_thr": 0.15
|
| 72 |
-
}
|
| 73 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/benchmark_v9_results.json
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"always_frontier": {
|
| 3 |
-
"success": 0.901,
|
| 4 |
-
"avg_cost": 1.0,
|
| 5 |
-
"unsafe": 0.0
|
| 6 |
-
},
|
| 7 |
-
"always_cheap": {
|
| 8 |
-
"success": 0.29133333333333333,
|
| 9 |
-
"avg_cost": 0.049999999999999954,
|
| 10 |
-
"unsafe": 0.7203333333333334
|
| 11 |
-
},
|
| 12 |
-
"heuristic": {
|
| 13 |
-
"success": 0.834,
|
| 14 |
-
"avg_cost": 0.8830500000000058,
|
| 15 |
-
"unsafe": 0.11466666666666667
|
| 16 |
-
},
|
| 17 |
-
"v8_router": {
|
| 18 |
-
"success": 0.8406666666666667,
|
| 19 |
-
"avg_cost": 0.8864666666666725,
|
| 20 |
-
"unsafe": 0.0
|
| 21 |
-
},
|
| 22 |
-
"v9_feedback": {
|
| 23 |
-
"success": 0.86,
|
| 24 |
-
"avg_cost": 0.9651333333333396,
|
| 25 |
-
"unsafe": 0.0
|
| 26 |
-
},
|
| 27 |
-
"oracle": {
|
| 28 |
-
"success": 0.9986666666666667,
|
| 29 |
-
"avg_cost": 0.4130333333333279,
|
| 30 |
-
"unsafe": 0.0
|
| 31 |
-
}
|
| 32 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/bert_results.json
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"eval_accuracy": 0.9446835987946621,
|
| 3 |
-
"eval_f1": 0.971529854879805
|
| 4 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/eval_bert_part2.py
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
# ── Part 2: Load data and BERT model ──
|
| 3 |
-
|
| 4 |
-
from datasets import load_dataset
|
| 5 |
-
from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
| 6 |
-
import torch
|
| 7 |
-
|
| 8 |
-
print("\n[1] Loading SWE-Router data...")
|
| 9 |
-
traces = defaultdict(dict)
|
| 10 |
-
for model in MODELS:
|
| 11 |
-
try:
|
| 12 |
-
ds = load_dataset(f'SWE-Router/swebench-verified-{model}', split='test')
|
| 13 |
-
for row in ds:
|
| 14 |
-
iid = row['instance_id']
|
| 15 |
-
traces[iid][model] = {
|
| 16 |
-
'resolved': row['resolved'],
|
| 17 |
-
'cost': float(row['instance_cost']),
|
| 18 |
-
'problem': row['problem_statement'],
|
| 19 |
-
}
|
| 20 |
-
print(f" {model}: loaded")
|
| 21 |
-
except Exception as e:
|
| 22 |
-
print(f" {model}: FAILED - {e}")
|
| 23 |
-
|
| 24 |
-
print(f" Total tasks: {len(traces)}")
|
| 25 |
-
|
| 26 |
-
print("\n[2] Loading BERT router...")
|
| 27 |
-
REPO = "narcolepticchicken/agent-cost-optimizer"
|
| 28 |
-
tokenizer = AutoTokenizer.from_pretrained(f"{REPO}", subfolder="router_models/bert_router")
|
| 29 |
-
bert_model = AutoModelForSequenceClassification.from_pretrained(f"{REPO}", subfolder="router_models/bert_router")
|
| 30 |
-
bert_model.eval()
|
| 31 |
-
print(f" BERT model loaded, num_labels={bert_model.config.num_labels}")
|
| 32 |
-
|
| 33 |
-
print("\n[3] Loading v11 XGBoost router...")
|
| 34 |
-
from huggingface_hub import hf_hub_download
|
| 35 |
-
v11_path = hf_hub_download(REPO, "router_models/router_bundle_v11.pkl")
|
| 36 |
-
v11_bundle = pickle.load(open(v11_path, "rb"))
|
| 37 |
-
v11_tier_clfs = {int(k):v for k,v in v11_bundle["tier_clfs"].items()}
|
| 38 |
-
v11_tier_calibs = {int(k):v for k,v in v11_bundle["tier_calibrators"].items()}
|
| 39 |
-
v11_feat_keys = v11_bundle["feat_keys"]
|
| 40 |
-
print(f" v11 loaded, features={len(v11_feat_keys)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/eval_bert_part3.py
DELETED
|
@@ -1,140 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
# ── Part 3: Run evaluations ──
|
| 3 |
-
|
| 4 |
-
def route_bert(problem_text):
|
| 5 |
-
"""Route using BERT classifier."""
|
| 6 |
-
inputs = tokenizer(problem_text, truncation=True, max_length=512, return_tensors="pt")
|
| 7 |
-
with torch.no_grad():
|
| 8 |
-
logits = bert_model(**inputs).logits
|
| 9 |
-
pred_class = torch.argmax(logits, dim=-1).item()
|
| 10 |
-
tier = pred_class + 1 # classes are 0-4 → tiers 1-5
|
| 11 |
-
probs = torch.softmax(logits, dim=-1)[0]
|
| 12 |
-
confidence = float(probs[pred_class])
|
| 13 |
-
return tier, confidence
|
| 14 |
-
|
| 15 |
-
def route_v11(problem_text):
|
| 16 |
-
"""Route using v11 XGBoost + isotonic calibration."""
|
| 17 |
-
feats = extract_features(problem_text)
|
| 18 |
-
feat_vec = np.array([float(feats.get(k, 0.0)) for k in v11_feat_keys], dtype=np.float32).reshape(1,-1)
|
| 19 |
-
tier_probs = {}
|
| 20 |
-
for t in range(1, 6):
|
| 21 |
-
p_raw = v11_tier_clfs[t].predict_proba(feat_vec)[0, 1]
|
| 22 |
-
p_cal = float(v11_tier_calibs[t].transform([p_raw])[0])
|
| 23 |
-
tier_probs[t] = p_cal
|
| 24 |
-
# Find cheapest tier with P(success) >= 0.65
|
| 25 |
-
for t in range(1, 6):
|
| 26 |
-
if tier_probs[t] >= 0.65:
|
| 27 |
-
return t, tier_probs[t], tier_probs
|
| 28 |
-
return 5, tier_probs[5], tier_probs
|
| 29 |
-
|
| 30 |
-
TASK_FLOOR = {"quick_answer":1,"coding":3,"research":3,"document_drafting":2,
|
| 31 |
-
"legal_regulated":4,"tool_heavy":2,"retrieval_heavy":2,"long_horizon":3,"unknown_ambiguous":3}
|
| 32 |
-
|
| 33 |
-
def classify_task(text):
|
| 34 |
-
r = text.lower()
|
| 35 |
-
if any(k in r for k in ["contract","legal","compliance","gdpr","privacy"]): return "legal_regulated"
|
| 36 |
-
if any(k in r for k in ["debug","fix","bug","implement","refactor","code","function"]): return "coding"
|
| 37 |
-
if any(k in r for k in ["research","find sources","literature","investigate"]): return "research"
|
| 38 |
-
if any(k in r for k in ["search","fetch","api","query","database"]): return "tool_heavy"
|
| 39 |
-
if any(k in r for k in ["plan","roadmap","orchestrate","migrate","deploy"]): return "long_horizon"
|
| 40 |
-
if any(k in r for k in ["draft","write","compose","document"]): return "document_drafting"
|
| 41 |
-
if any(k in r for k in ["what is","explain","define","briefly"]): return "quick_answer"
|
| 42 |
-
return "unknown_ambiguous"
|
| 43 |
-
|
| 44 |
-
policies = defaultdict(lambda: {"success":0,"cost":0.0,"n":0})
|
| 45 |
-
|
| 46 |
-
print("\n[4] Evaluating all policies on SWE-Router...")
|
| 47 |
-
for iid, model_results in traces.items():
|
| 48 |
-
problem = next(iter(model_results.values()))['problem']
|
| 49 |
-
task_type = classify_task(problem)
|
| 50 |
-
floor = TASK_FLOOR.get(task_type, 2)
|
| 51 |
-
|
| 52 |
-
# Oracle
|
| 53 |
-
resolved = [(m, r) for m, r in model_results.items() if r['resolved']]
|
| 54 |
-
if resolved:
|
| 55 |
-
cheapest = min(resolved, key=lambda x: TIER_COST.get(MODEL_TIER[x[0]], 1.0))
|
| 56 |
-
policies['oracle']['success'] += 1
|
| 57 |
-
policies['oracle']['cost'] += cheapest[1]['cost']
|
| 58 |
-
else:
|
| 59 |
-
policies['oracle']['cost'] += min(r['cost'] for r in model_results.values())
|
| 60 |
-
policies['oracle']['n'] += 1
|
| 61 |
-
|
| 62 |
-
# Always frontier
|
| 63 |
-
f_model = 'claude-opus-4.7'
|
| 64 |
-
if f_model in model_results:
|
| 65 |
-
policies['frontier']['success'] += int(model_results[f_model]['resolved'])
|
| 66 |
-
policies['frontier']['cost'] += model_results[f_model]['cost']
|
| 67 |
-
policies['frontier']['n'] += 1
|
| 68 |
-
|
| 69 |
-
# BERT router
|
| 70 |
-
bert_tier, bert_conf = route_bert(problem)
|
| 71 |
-
bert_tier = max(bert_tier, floor) # enforce safety floor
|
| 72 |
-
m_bert = TIER_TO_SWE.get(bert_tier, 'claude-opus-4.7')
|
| 73 |
-
if m_bert in model_results:
|
| 74 |
-
policies['bert']['success'] += int(model_results[m_bert]['resolved'])
|
| 75 |
-
policies['bert']['cost'] += model_results[m_bert]['cost']
|
| 76 |
-
else:
|
| 77 |
-
policies['bert']['success'] += int(model_results.get('claude-opus-4.7',{}).get('resolved',0))
|
| 78 |
-
policies['bert']['cost'] += model_results.get('claude-opus-4.7',{}).get('cost',0.3)
|
| 79 |
-
policies['bert']['n'] += 1
|
| 80 |
-
|
| 81 |
-
# v11 XGBoost
|
| 82 |
-
v11_tier, v11_conf, v11_probs = route_v11(problem)
|
| 83 |
-
v11_tier = max(v11_tier, floor) # enforce safety floor
|
| 84 |
-
m_v11 = TIER_TO_SWE.get(v11_tier, 'claude-opus-4.7')
|
| 85 |
-
if m_v11 in model_results:
|
| 86 |
-
policies['v11_xgboost']['success'] += int(model_results[m_v11]['resolved'])
|
| 87 |
-
policies['v11_xgboost']['cost'] += model_results[m_v11]['cost']
|
| 88 |
-
else:
|
| 89 |
-
policies['v11_xgboost']['success'] += int(model_results.get('claude-opus-4.7',{}).get('resolved',0))
|
| 90 |
-
policies['v11_xgboost']['cost'] += model_results.get('claude-opus-4.7',{}).get('cost',0.3)
|
| 91 |
-
policies['v11_xgboost']['n'] += 1
|
| 92 |
-
|
| 93 |
-
# BERT + feedback (escalate on failure)
|
| 94 |
-
if m_bert in model_results and model_results[m_bert]['resolved']:
|
| 95 |
-
policies['bert_feedback']['success'] += 1
|
| 96 |
-
policies['bert_feedback']['cost'] += model_results[m_bert]['cost']
|
| 97 |
-
else:
|
| 98 |
-
# Escalate one tier
|
| 99 |
-
up_tier = min(bert_tier + 1, 5)
|
| 100 |
-
m_up = TIER_TO_SWE.get(up_tier, 'claude-opus-4.7')
|
| 101 |
-
if m_up in model_results and model_results[m_up]['resolved']:
|
| 102 |
-
policies['bert_feedback']['success'] += 1
|
| 103 |
-
policies['bert_feedback']['cost'] += model_results.get(m_bert,{}).get('cost',0.01)
|
| 104 |
-
policies['bert_feedback']['cost'] += model_results[m_up]['cost']
|
| 105 |
-
else:
|
| 106 |
-
# Last resort: frontier
|
| 107 |
-
if f_model in model_results and model_results[f_model]['resolved']:
|
| 108 |
-
policies['bert_feedback']['success'] += 1
|
| 109 |
-
policies['bert_feedback']['cost'] += model_results.get(m_bert,{}).get('cost',0.01)
|
| 110 |
-
policies['bert_feedback']['cost'] += model_results[f_model]['cost']
|
| 111 |
-
else:
|
| 112 |
-
policies['bert_feedback']['cost'] += model_results.get(m_bert,{}).get('cost',0.01)
|
| 113 |
-
policies['bert_feedback']['n'] += 1
|
| 114 |
-
|
| 115 |
-
# v11 + feedback (escalate on failure)
|
| 116 |
-
if m_v11 in model_results and model_results[m_v11]['resolved']:
|
| 117 |
-
policies['v11_feedback']['success'] += 1
|
| 118 |
-
policies['v11_feedback']['cost'] += model_results[m_v11]['cost']
|
| 119 |
-
else:
|
| 120 |
-
up_tier = min(v11_tier + 1, 5)
|
| 121 |
-
m_up = TIER_TO_SWE.get(up_tier, 'claude-opus-4.7')
|
| 122 |
-
if m_up in model_results and model_results[m_up]['resolved']:
|
| 123 |
-
policies['v11_feedback']['success'] += 1
|
| 124 |
-
policies['v11_feedback']['cost'] += model_results.get(m_v11,{}).get('cost',0.01)
|
| 125 |
-
policies['v11_feedback']['cost'] += model_results[m_up]['cost']
|
| 126 |
-
else:
|
| 127 |
-
if f_model in model_results and model_results[f_model]['resolved']:
|
| 128 |
-
policies['v11_feedback']['success'] += 1
|
| 129 |
-
policies['v11_feedback']['cost'] += model_results.get(m_v11,{}).get('cost',0.01)
|
| 130 |
-
policies['v11_feedback']['cost'] += model_results[f_model]['cost']
|
| 131 |
-
else:
|
| 132 |
-
policies['v11_feedback']['cost'] += model_results.get(m_v11,{}).get('cost',0.01)
|
| 133 |
-
policies['v11_feedback']['n'] += 1
|
| 134 |
-
|
| 135 |
-
# Always cheap
|
| 136 |
-
c_model = 'deepseek-v4-flash'
|
| 137 |
-
if c_model in model_results:
|
| 138 |
-
policies['always_cheap']['success'] += int(model_results[c_model]['resolved'])
|
| 139 |
-
policies['always_cheap']['cost'] += model_results[c_model]['cost']
|
| 140 |
-
policies['always_cheap']['n'] += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/eval_bert_part4.py
DELETED
|
@@ -1,61 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
# ── Part 4: Print results ──
|
| 3 |
-
|
| 4 |
-
print(f"\n\n{'='*70}")
|
| 5 |
-
print("BERT vs XGBoost ROUTER COMPARISON ON SWE-BENCH")
|
| 6 |
-
print(f"{'='*70}")
|
| 7 |
-
|
| 8 |
-
fr = policies['frontier']
|
| 9 |
-
fr_cost = fr['cost'] / fr['n']
|
| 10 |
-
fr_succ = fr['success'] / fr['n']
|
| 11 |
-
|
| 12 |
-
print(f"\n{'Policy':<20} {'Success':>10} {'AvgCost':>10} {'CostRed':>10}")
|
| 13 |
-
print("-"*52)
|
| 14 |
-
order = ['oracle','bert_feedback','v11_feedback','bert','v11_xgboost','frontier','always_cheap']
|
| 15 |
-
for name in order:
|
| 16 |
-
if name not in policies:
|
| 17 |
-
continue
|
| 18 |
-
r = policies[name]
|
| 19 |
-
sr = r['success']/r['n'] if r['n'] > 0 else 0
|
| 20 |
-
ac = r['cost']/r['n'] if r['n'] > 0 else 0
|
| 21 |
-
cr = (1 - ac/fr_cost)*100 if fr_cost > 0 else 0
|
| 22 |
-
print(f"{name:<20} {sr:>10.3f} {ac:>10.4f} {cr:>9.1f}%")
|
| 23 |
-
|
| 24 |
-
# BERT tier distribution
|
| 25 |
-
print(f"\n\nBERT tier distribution:")
|
| 26 |
-
bert_tiers = defaultdict(int)
|
| 27 |
-
for iid, model_results in traces.items():
|
| 28 |
-
problem = next(iter(model_results.values()))['problem']
|
| 29 |
-
t, c = route_bert(problem)
|
| 30 |
-
bert_tiers[t] += 1
|
| 31 |
-
for t in sorted(bert_tiers):
|
| 32 |
-
print(f" Tier {t}: {bert_tiers[t]}")
|
| 33 |
-
|
| 34 |
-
# Quality gap analysis
|
| 35 |
-
print(f"\n\nQuality gap vs frontier:")
|
| 36 |
-
for name in ['bert','bert_feedback','v11_xgboost','v11_feedback']:
|
| 37 |
-
r = policies[name]
|
| 38 |
-
sr = r['success']/r['n'] if r['n'] > 0 else 0
|
| 39 |
-
gap = (sr - fr_succ) * 100
|
| 40 |
-
print(f" {name}: {gap:+.1f}pp vs frontier")
|
| 41 |
-
|
| 42 |
-
# Save results
|
| 43 |
-
results = {}
|
| 44 |
-
for name, r in policies.items():
|
| 45 |
-
sr = r['success']/r['n'] if r['n'] > 0 else 0
|
| 46 |
-
ac = r['cost']/r['n'] if r['n'] > 0 else 0
|
| 47 |
-
cr = (1 - ac/fr_cost)*100 if fr_cost > 0 else 0
|
| 48 |
-
results[name] = {"success": round(sr, 4), "avg_cost": round(ac, 4), "costRed": round(cr, 1)}
|
| 49 |
-
|
| 50 |
-
# Upload results to Hub
|
| 51 |
-
from huggingface_hub import HfApi
|
| 52 |
-
api = HfApi()
|
| 53 |
-
import tempfile, json
|
| 54 |
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
| 55 |
-
json.dump(results, f, indent=2)
|
| 56 |
-
api.upload_file(path_or_fileobj=f.name, path_in_repo="eval/bert_vs_xgboost_results.json",
|
| 57 |
-
repo_id="narcolepticchicken/agent-cost-optimizer", repo_type="model")
|
| 58 |
-
os.unlink(f.name)
|
| 59 |
-
|
| 60 |
-
print(f"\nResults saved to eval/bert_vs_xgboost_results.json")
|
| 61 |
-
print("DONE!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/eval_bert_partB.py
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
# ── Load models ──
|
| 3 |
-
print("\n[2] Loading BERT router from Hub...")
|
| 4 |
-
from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
| 5 |
-
import torch
|
| 6 |
-
|
| 7 |
-
REPO = "narcolepticchicken/agent-cost-optimizer"
|
| 8 |
-
tokenizer = AutoTokenizer.from_pretrained(REPO, subfolder="router_models/bert_router")
|
| 9 |
-
bert_model = AutoModelForSequenceClassification.from_pretrained(REPO, subfolder="router_models/bert_router")
|
| 10 |
-
bert_model.eval()
|
| 11 |
-
print(f" BERT loaded, num_labels={bert_model.config.num_labels}")
|
| 12 |
-
|
| 13 |
-
# BERT is binary (success/fail) — we'll use it as a per-tier success predictor
|
| 14 |
-
# by prepending "Tier X:" to the input text
|
| 15 |
-
|
| 16 |
-
print("\n[3] Loading v10 XGBoost router...")
|
| 17 |
-
from huggingface_hub import hf_hub_download
|
| 18 |
-
import pickle
|
| 19 |
-
v10_path = hf_hub_download(REPO, "router_models/router_bundle_v10_fixed.pkl")
|
| 20 |
-
v10_bundle = pickle.load(open(v10_path, "rb"))
|
| 21 |
-
print(f" v10 bundle keys: {list(v10_bundle.keys())}")
|
| 22 |
-
|
| 23 |
-
# v10 may have different structure — inspect
|
| 24 |
-
if "tier_clfs" in v10_bundle:
|
| 25 |
-
v10_tier_clfs = {int(k):v for k,v in v10_bundle["tier_clfs"].items()}
|
| 26 |
-
v10_tier_calibs = {int(k):v for k,v in v10_bundle["tier_calibrators"].items()}
|
| 27 |
-
v10_feat_keys = v10_bundle["feat_keys"]
|
| 28 |
-
print(f" v10 loaded, features={len(v10_feat_keys)}")
|
| 29 |
-
HAS_V10 = True
|
| 30 |
-
else:
|
| 31 |
-
HAS_V10 = False
|
| 32 |
-
print(f" v10 bundle structure: {type(v10_bundle)}")
|
| 33 |
-
|
| 34 |
-
# ── Routing functions ──
|
| 35 |
-
def route_bert(problem_text):
|
| 36 |
-
"""BERT binary classifier: predict success probability at each tier.
|
| 37 |
-
Route to cheapest tier where P(success) > 0.5."""
|
| 38 |
-
tier_probs = {}
|
| 39 |
-
for tier in range(1, 6):
|
| 40 |
-
prompt = f"[Tier {tier}] {problem_text}"
|
| 41 |
-
inputs = tokenizer(prompt, truncation=True, max_length=512, return_tensors="pt")
|
| 42 |
-
with torch.no_grad():
|
| 43 |
-
logits = bert_model(**inputs).logits
|
| 44 |
-
probs = torch.softmax(logits, dim=-1)[0]
|
| 45 |
-
# Binary: class 1 = success
|
| 46 |
-
tier_probs[tier] = float(probs[1]) if bert_model.config.num_labels == 2 else float(probs[tier-1])
|
| 47 |
-
|
| 48 |
-
# Route to cheapest tier with P(success) >= 0.5
|
| 49 |
-
for t in range(1, 6):
|
| 50 |
-
if tier_probs[t] >= 0.5:
|
| 51 |
-
return t, tier_probs[t], tier_probs
|
| 52 |
-
return 5, tier_probs[5], tier_probs
|
| 53 |
-
|
| 54 |
-
def route_v10(problem_text):
|
| 55 |
-
"""v10 XGBoost cascade router."""
|
| 56 |
-
if not HAS_V10:
|
| 57 |
-
return 4, 0.5, {t:0.5 for t in range(1,6)}
|
| 58 |
-
feats = extract_features(problem_text)
|
| 59 |
-
feat_vec = np.array([float(feats.get(k, 0.0)) for k in v10_feat_keys], dtype=np.float32).reshape(1,-1)
|
| 60 |
-
tier_probs = {}
|
| 61 |
-
for t in range(1, 6):
|
| 62 |
-
p_raw = v10_tier_clfs[t].predict_proba(feat_vec)[0, 1]
|
| 63 |
-
p_cal = float(v10_tier_calibs[t].transform([p_raw])[0])
|
| 64 |
-
tier_probs[t] = p_cal
|
| 65 |
-
for t in range(1, 6):
|
| 66 |
-
if tier_probs[t] >= 0.65:
|
| 67 |
-
return t, tier_probs[t], tier_probs
|
| 68 |
-
return 5, tier_probs[5], tier_probs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/eval_bert_partC.py
DELETED
|
@@ -1,95 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
# ── Evaluate ──
|
| 3 |
-
policies = defaultdict(lambda: {"success":0,"cost":0.0,"n":0})
|
| 4 |
-
|
| 5 |
-
print("\n[4] Evaluating all policies...")
|
| 6 |
-
for idx, (iid, model_results) in enumerate(traces.items()):
|
| 7 |
-
if idx % 100 == 0:
|
| 8 |
-
print(f" Progress: {idx}/{len(traces)}")
|
| 9 |
-
problem = next(iter(model_results.values()))['problem']
|
| 10 |
-
task_type = classify_task(problem)
|
| 11 |
-
floor = TASK_FLOOR.get(task_type, 2)
|
| 12 |
-
f_model = 'claude-opus-4.7'
|
| 13 |
-
|
| 14 |
-
# Oracle
|
| 15 |
-
resolved = [(m, r) for m, r in model_results.items() if r['resolved']]
|
| 16 |
-
if resolved:
|
| 17 |
-
cheapest = min(resolved, key=lambda x: TIER_COST.get(MODEL_TIER[x[0]], 1.0))
|
| 18 |
-
policies['oracle']['success'] += 1
|
| 19 |
-
policies['oracle']['cost'] += cheapest[1]['cost']
|
| 20 |
-
else:
|
| 21 |
-
policies['oracle']['cost'] += min(r['cost'] for r in model_results.values())
|
| 22 |
-
policies['oracle']['n'] += 1
|
| 23 |
-
|
| 24 |
-
# Always frontier
|
| 25 |
-
if f_model in model_results:
|
| 26 |
-
policies['frontier']['success'] += int(model_results[f_model]['resolved'])
|
| 27 |
-
policies['frontier']['cost'] += model_results[f_model]['cost']
|
| 28 |
-
policies['frontier']['n'] += 1
|
| 29 |
-
|
| 30 |
-
# BERT (per-tier success prediction with cascade)
|
| 31 |
-
bert_tier, bert_conf, bert_probs = route_bert(problem)
|
| 32 |
-
bert_tier = max(bert_tier, floor)
|
| 33 |
-
m_bert = TIER_TO_SWE.get(bert_tier, f_model)
|
| 34 |
-
if m_bert in model_results:
|
| 35 |
-
policies['bert']['success'] += int(model_results[m_bert]['resolved'])
|
| 36 |
-
policies['bert']['cost'] += model_results[m_bert]['cost']
|
| 37 |
-
else:
|
| 38 |
-
policies['bert']['success'] += int(model_results.get(f_model,{}).get('resolved',0))
|
| 39 |
-
policies['bert']['cost'] += model_results.get(f_model,{}).get('cost',0.3)
|
| 40 |
-
policies['bert']['n'] += 1
|
| 41 |
-
|
| 42 |
-
# v10 XGBoost
|
| 43 |
-
v10_tier, v10_conf, v10_probs = route_v10(problem)
|
| 44 |
-
v10_tier = max(v10_tier, floor)
|
| 45 |
-
m_v10 = TIER_TO_SWE.get(v10_tier, f_model)
|
| 46 |
-
if m_v10 in model_results:
|
| 47 |
-
policies['v10_xgboost']['success'] += int(model_results[m_v10]['resolved'])
|
| 48 |
-
policies['v10_xgboost']['cost'] += model_results[m_v10]['cost']
|
| 49 |
-
else:
|
| 50 |
-
policies['v10_xgboost']['success'] += int(model_results.get(f_model,{}).get('resolved',0))
|
| 51 |
-
policies['v10_xgboost']['cost'] += model_results.get(f_model,{}).get('cost',0.3)
|
| 52 |
-
policies['v10_xgboost']['n'] += 1
|
| 53 |
-
|
| 54 |
-
# BERT + feedback (escalate on failure)
|
| 55 |
-
if m_bert in model_results and model_results[m_bert]['resolved']:
|
| 56 |
-
policies['bert_feedback']['success'] += 1
|
| 57 |
-
policies['bert_feedback']['cost'] += model_results[m_bert]['cost']
|
| 58 |
-
else:
|
| 59 |
-
up_tier = min(bert_tier + 1, 5)
|
| 60 |
-
m_up = TIER_TO_SWE.get(up_tier, f_model)
|
| 61 |
-
if m_up in model_results and model_results[m_up]['resolved']:
|
| 62 |
-
policies['bert_feedback']['success'] += 1
|
| 63 |
-
policies['bert_feedback']['cost'] += model_results.get(m_bert,{}).get('cost',0.01) + model_results[m_up]['cost']
|
| 64 |
-
elif f_model in model_results and model_results[f_model]['resolved']:
|
| 65 |
-
policies['bert_feedback']['success'] += 1
|
| 66 |
-
policies['bert_feedback']['cost'] += model_results.get(m_bert,{}).get('cost',0.01) + model_results[f_model]['cost']
|
| 67 |
-
else:
|
| 68 |
-
policies['bert_feedback']['cost'] += model_results.get(m_bert,{}).get('cost',0.01)
|
| 69 |
-
policies['bert_feedback']['n'] += 1
|
| 70 |
-
|
| 71 |
-
# v10 + feedback
|
| 72 |
-
if m_v10 in model_results and model_results[m_v10]['resolved']:
|
| 73 |
-
policies['v10_feedback']['success'] += 1
|
| 74 |
-
policies['v10_feedback']['cost'] += model_results[m_v10]['cost']
|
| 75 |
-
else:
|
| 76 |
-
up_tier = min(v10_tier + 1, 5)
|
| 77 |
-
m_up = TIER_TO_SWE.get(up_tier, f_model)
|
| 78 |
-
if m_up in model_results and model_results[m_up]['resolved']:
|
| 79 |
-
policies['v10_feedback']['success'] += 1
|
| 80 |
-
policies['v10_feedback']['cost'] += model_results.get(m_v10,{}).get('cost',0.01) + model_results[m_up]['cost']
|
| 81 |
-
elif f_model in model_results and model_results[f_model]['resolved']:
|
| 82 |
-
policies['v10_feedback']['success'] += 1
|
| 83 |
-
policies['v10_feedback']['cost'] += model_results.get(m_v10,{}).get('cost',0.01) + model_results[f_model]['cost']
|
| 84 |
-
else:
|
| 85 |
-
policies['v10_feedback']['cost'] += model_results.get(m_v10,{}).get('cost',0.01)
|
| 86 |
-
policies['v10_feedback']['n'] += 1
|
| 87 |
-
|
| 88 |
-
# Always cheap
|
| 89 |
-
c_model = 'deepseek-v4-flash'
|
| 90 |
-
if c_model in model_results:
|
| 91 |
-
policies['always_cheap']['success'] += int(model_results[c_model]['resolved'])
|
| 92 |
-
policies['always_cheap']['cost'] += model_results[c_model]['cost']
|
| 93 |
-
policies['always_cheap']['n'] += 1
|
| 94 |
-
|
| 95 |
-
print(f" Progress: {len(traces)}/{len(traces)} - DONE")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/eval_bert_partD.py
DELETED
|
@@ -1,81 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
# ── Results ──
|
| 3 |
-
fr = policies['frontier']
|
| 4 |
-
fr_cost = fr['cost'] / fr['n']
|
| 5 |
-
fr_succ = fr['success'] / fr['n']
|
| 6 |
-
|
| 7 |
-
print(f"\n\n{'='*70}")
|
| 8 |
-
print("BERT vs XGBoost ROUTER COMPARISON ON SWE-BENCH")
|
| 9 |
-
print(f"{'='*70}")
|
| 10 |
-
print(f"\n{'Policy':<20} {'Success':>10} {'AvgCost':>10} {'CostRed':>10}")
|
| 11 |
-
print("-"*52)
|
| 12 |
-
for name in ['oracle','bert_feedback','v11_feedback','bert','v11_xgboost','frontier','always_cheap']:
|
| 13 |
-
# Map v11 names to v10 if v11 not available
|
| 14 |
-
actual_name = name
|
| 15 |
-
if name == 'v11_xgboost' and 'v11_xgboost' not in policies and 'v10_xgboost' in policies:
|
| 16 |
-
actual_name = 'v10_xgboost'
|
| 17 |
-
if name == 'v11_feedback' and 'v11_feedback' not in policies and 'v10_feedback' in policies:
|
| 18 |
-
actual_name = 'v10_feedback'
|
| 19 |
-
if actual_name not in policies:
|
| 20 |
-
continue
|
| 21 |
-
r = policies[actual_name]
|
| 22 |
-
sr = r['success']/r['n'] if r['n'] > 0 else 0
|
| 23 |
-
ac = r['cost']/r['n'] if r['n'] > 0 else 0
|
| 24 |
-
cr = (1-ac/fr_cost)*100 if fr_cost > 0 else 0
|
| 25 |
-
print(f"{name:<20} {sr:>10.3f} {ac:>10.4f} {cr:>9.1f}%")
|
| 26 |
-
|
| 27 |
-
print(f"\nQuality gap vs frontier:")
|
| 28 |
-
for name in ['bert','bert_feedback','v11_xgboost','v11_feedback']:
|
| 29 |
-
actual_name = name
|
| 30 |
-
if name == 'v11_xgboost' and 'v11_xgboost' not in policies and 'v10_xgboost' in policies:
|
| 31 |
-
actual_name = 'v10_xgboost'
|
| 32 |
-
if name == 'v11_feedback' and 'v11_feedback' not in policies and 'v10_feedback' in policies:
|
| 33 |
-
actual_name = 'v10_feedback'
|
| 34 |
-
if actual_name not in policies:
|
| 35 |
-
continue
|
| 36 |
-
r = policies[actual_name]
|
| 37 |
-
sr = r['success']/r['n'] if r['n'] > 0 else 0
|
| 38 |
-
gap = (sr - fr_succ) * 100
|
| 39 |
-
print(f" {name}: {gap:+.1f}pp vs frontier")
|
| 40 |
-
|
| 41 |
-
# BERT tier distribution
|
| 42 |
-
print(f"\nBERT tier distribution (first 100 tasks):")
|
| 43 |
-
bert_tiers = defaultdict(int)
|
| 44 |
-
bert_probs_dist = defaultdict(list)
|
| 45 |
-
for iid, model_results in list(traces.items())[:100]:
|
| 46 |
-
problem = next(iter(model_results.values()))['problem']
|
| 47 |
-
t, conf, probs = route_bert(problem)
|
| 48 |
-
bert_tiers[t] += 1
|
| 49 |
-
for tier, p in probs.items():
|
| 50 |
-
bert_probs_dist[tier].append(p)
|
| 51 |
-
print(f" Tier routing counts:")
|
| 52 |
-
for t in sorted(bert_tiers):
|
| 53 |
-
print(f" Tier {t}: {bert_tiers[t]}")
|
| 54 |
-
print(f" Per-tier P(success) stats:")
|
| 55 |
-
for t in sorted(bert_probs_dist):
|
| 56 |
-
ps = bert_probs_dist[t]
|
| 57 |
-
print(f" Tier {t}: mean={np.mean(ps):.3f}, std={np.std(ps):.3f}, min={np.min(ps):.3f}, max={np.max(ps):.3f}")
|
| 58 |
-
|
| 59 |
-
# Save results to local file and try to upload to Hub
|
| 60 |
-
results = {}
|
| 61 |
-
for name, r in policies.items():
|
| 62 |
-
sr = r['success']/r['n'] if r['n'] > 0 else 0
|
| 63 |
-
ac = r['cost']/r['n'] if r['n'] > 0 else 0
|
| 64 |
-
cr = (1-ac/fr_cost)*100 if fr_cost > 0 else 0
|
| 65 |
-
results[name] = {"success": round(sr, 4), "avg_cost": round(ac, 4), "costRed": round(cr, 1)}
|
| 66 |
-
|
| 67 |
-
import tempfile, json
|
| 68 |
-
try:
|
| 69 |
-
from huggingface_hub import HfApi
|
| 70 |
-
api = HfApi()
|
| 71 |
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
| 72 |
-
json.dump(results, f, indent=2)
|
| 73 |
-
api.upload_file(path_or_fileobj=f.name, path_in_repo="eval/bert_vs_xgboost_results.json",
|
| 74 |
-
repo_id="narcolepticchicken/agent-cost-optimizer", repo_type="model")
|
| 75 |
-
os.unlink(f.name)
|
| 76 |
-
print(f"\nResults uploaded to eval/bert_vs_xgboost_results.json on Hub")
|
| 77 |
-
except Exception as e:
|
| 78 |
-
print(f"\nCould not upload to Hub: {e}")
|
| 79 |
-
print(f"Results JSON:\n{json.dumps(results, indent=2)}")
|
| 80 |
-
|
| 81 |
-
print("\nDONE!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/eval_bert_router.py
DELETED
|
@@ -1,61 +0,0 @@
|
|
| 1 |
-
"""Evaluate BERT router on SWE-Router and compare with v11 XGBoost."""
|
| 2 |
-
import json, os, sys, random, pickle, math
|
| 3 |
-
import numpy as np
|
| 4 |
-
from collections import defaultdict
|
| 5 |
-
|
| 6 |
-
# ── Constants ──
|
| 7 |
-
MODELS = ['claude-opus-4.7','gpt-5-mini','gpt-5-nano','gpt-5.2',
|
| 8 |
-
'gemini-2.5-pro','gemini-3-pro','deepseek-v3.2','deepseek-v4-flash']
|
| 9 |
-
|
| 10 |
-
MODEL_TIER = {
|
| 11 |
-
'deepseek-v4-flash': 1, 'gpt-5-nano': 1,
|
| 12 |
-
'gpt-5-mini': 2, 'deepseek-v3.2': 2,
|
| 13 |
-
'gemini-2.5-pro': 3,
|
| 14 |
-
'claude-opus-4.7': 4, 'gpt-5.2': 4,
|
| 15 |
-
'gemini-3-pro': 5,
|
| 16 |
-
}
|
| 17 |
-
TIER_COST = {1:0.01, 2:0.05, 3:0.15, 4:0.30, 5:0.50}
|
| 18 |
-
TIER_TO_SWE = {
|
| 19 |
-
1: 'deepseek-v4-flash', 2: 'gpt-5-mini',
|
| 20 |
-
3: 'gemini-2.5-pro', 4: 'claude-opus-4.7', 5: 'gemini-3-pro',
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
# ── Feature extraction (same as training) ──
|
| 24 |
-
CODE_KW = ["python","javascript","code","function","bug","debug","refactor","implement","test",
|
| 25 |
-
"compile","runtime","segfault","thread","async","class","module","import","error","traceback"]
|
| 26 |
-
LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
|
| 27 |
-
RESEARCH_KW = ["research","investigate","compare","analyze","survey","paper"]
|
| 28 |
-
TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
|
| 29 |
-
CRITICAL_KW = ["critical","production","urgent","emergency","live","deployed","safety","security"]
|
| 30 |
-
SIMPLE_KW = ["typo","simple","quick","brief","minor","small","easy","trivial","just"]
|
| 31 |
-
LONG_KW = ["plan","project","roadmap","orchestrate","migrate","pipeline","deploy","architecture"]
|
| 32 |
-
MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
|
| 33 |
-
|
| 34 |
-
def extract_features(problem_text):
|
| 35 |
-
r = problem_text.lower()
|
| 36 |
-
feats = {
|
| 37 |
-
'req_len': len(problem_text), 'num_words': len(problem_text.split()),
|
| 38 |
-
'has_code': int(any(k in r for k in CODE_KW)),
|
| 39 |
-
'n_code': sum(1 for k in CODE_KW if k in r),
|
| 40 |
-
'has_legal': int(any(k in r for k in LEGAL_KW)),
|
| 41 |
-
'has_research': int(any(k in r for k in RESEARCH_KW)),
|
| 42 |
-
'has_tool': int(any(k in r for k in TOOL_KW)),
|
| 43 |
-
'has_critical': int(any(k in r for k in CRITICAL_KW)),
|
| 44 |
-
'has_simple': int(any(k in r for k in SIMPLE_KW)),
|
| 45 |
-
'has_long': int(any(k in r for k in LONG_KW)),
|
| 46 |
-
'has_math': int(any(k in r for k in MATH_KW)),
|
| 47 |
-
'has_error_msg': int('error' in r or 'traceback' in r or 'exception' in r),
|
| 48 |
-
'has_file_path': int('/' in r),
|
| 49 |
-
'n_lines': problem_text.count('\n') + 1,
|
| 50 |
-
'has_version': int('version' in r or 'update' in r or 'upgrade' in r),
|
| 51 |
-
'has_add': int('add' in r or 'new' in r or 'create' in r),
|
| 52 |
-
'has_fix': int('fix' in r or 'bug' in r or 'issue' in r or 'broken' in r),
|
| 53 |
-
'has_change': int('change' in r or 'modify' in r or 'update' in r),
|
| 54 |
-
'has_remove': int('remove' in r or 'delete' in r or 'drop' in r),
|
| 55 |
-
'has_test': int('test' in r or 'spec' in r or 'assert' in r),
|
| 56 |
-
'has_doc': int('doc' in r or 'readme' in r or 'comment' in r),
|
| 57 |
-
}
|
| 58 |
-
return feats
|
| 59 |
-
|
| 60 |
-
print("BERT Router Evaluation on SWE-Router")
|
| 61 |
-
print("="*60)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/run_bert_eval.py
DELETED
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Evaluate BERT router on SWE-Router and compare with v11 XGBoost.
|
| 3 |
-
Combines eval_bert_router.py + eval_bert_part2.py + eval_bert_part3.py + eval_bert_part4.py
|
| 4 |
-
"""
|
| 5 |
-
import json, os, sys, random, pickle, math
|
| 6 |
-
import numpy as np
|
| 7 |
-
from collections import defaultdict
|
| 8 |
-
|
| 9 |
-
MODELS = ['claude-opus-4.7','gpt-5-mini','gpt-5-nano','gpt-5.2',
|
| 10 |
-
'gemini-2.5-pro','gemini-3-pro','deepseek-v3.2','deepseek-v4-flash']
|
| 11 |
-
MODEL_TIER = {
|
| 12 |
-
'deepseek-v4-flash': 1, 'gpt-5-nano': 1,
|
| 13 |
-
'gpt-5-mini': 2, 'deepseek-v3.2': 2,
|
| 14 |
-
'gemini-2.5-pro': 3,
|
| 15 |
-
'claude-opus-4.7': 4, 'gpt-5.2': 4,
|
| 16 |
-
'gemini-3-pro': 5,
|
| 17 |
-
}
|
| 18 |
-
TIER_COST = {1:0.01, 2:0.05, 3:0.15, 4:0.30, 5:0.50}
|
| 19 |
-
TIER_TO_SWE = {1:'deepseek-v4-flash',2:'gpt-5-mini',3:'gemini-2.5-pro',4:'claude-opus-4.7',5:'gemini-3-pro'}
|
| 20 |
-
CODE_KW = ["python","javascript","code","function","bug","debug","refactor","implement","test","compile","runtime","segfault","thread","async","class","module","import","error","traceback"]
|
| 21 |
-
LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
|
| 22 |
-
RESEARCH_KW = ["research","investigate","compare","analyze","survey","paper"]
|
| 23 |
-
TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
|
| 24 |
-
CRITICAL_KW = ["critical","production","urgent","emergency","live","deployed","safety","security"]
|
| 25 |
-
SIMPLE_KW = ["typo","simple","quick","brief","minor","small","easy","trivial","just"]
|
| 26 |
-
LONG_KW = ["plan","project","roadmap","orchestrate","migrate","pipeline","deploy","architecture"]
|
| 27 |
-
MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
|
| 28 |
-
|
| 29 |
-
def extract_features(pt):
|
| 30 |
-
r = pt.lower()
|
| 31 |
-
return {'req_len':len(pt),'num_words':len(pt.split()),'has_code':int(any(k in r for k in CODE_KW)),'n_code':sum(1 for k in CODE_KW if k in r),'has_legal':int(any(k in r for k in LEGAL_KW)),'has_research':int(any(k in r for k in RESEARCH_KW)),'has_tool':int(any(k in r for k in TOOL_KW)),'has_critical':int(any(k in r for k in CRITICAL_KW)),'has_simple':int(any(k in r for k in SIMPLE_KW)),'has_long':int(any(k in r for k in LONG_KW)),'has_math':int(any(k in r for k in MATH_KW)),'has_error_msg':int('error' in r or 'traceback' in r),'has_file_path':int('/' in r),'n_lines':pt.count('\n')+1,'has_version':int('version' in r or 'update' in r),'has_add':int('add' in r or 'new' in r),'has_fix':int('fix' in r or 'bug' in r),'has_change':int('change' in r or 'modify' in r),'has_remove':int('remove' in r),'has_test':int('test' in r or 'spec' in r),'has_doc':int('doc' in r)}
|
| 32 |
-
|
| 33 |
-
TASK_FLOOR = {"quick_answer":1,"coding":3,"research":3,"document_drafting":2,"legal_regulated":4,"tool_heavy":2,"retrieval_heavy":2,"long_horizon":3,"unknown_ambiguous":3}
|
| 34 |
-
|
| 35 |
-
def classify_task(text):
|
| 36 |
-
r = text.lower()
|
| 37 |
-
if any(k in r for k in ["contract","legal","compliance","gdpr","privacy"]): return "legal_regulated"
|
| 38 |
-
if any(k in r for k in ["debug","fix","bug","implement","refactor","code","function"]): return "coding"
|
| 39 |
-
if any(k in r for k in ["research","find sources","literature","investigate"]): return "research"
|
| 40 |
-
if any(k in r for k in ["search","fetch","api","query","database"]): return "tool_heavy"
|
| 41 |
-
if any(k in r for k in ["plan","roadmap","orchestrate","migrate","deploy"]): return "long_horizon"
|
| 42 |
-
if any(k in r for k in ["draft","write","compose","document"]): return "document_drafting"
|
| 43 |
-
if any(k in r for k in ["what is","explain","define","briefly"]): return "quick_answer"
|
| 44 |
-
return "unknown_ambiguous"
|
| 45 |
-
|
| 46 |
-
print("BERT Router Evaluation on SWE-Router")
|
| 47 |
-
print("="*60)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/run_bert_eval_full.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
| 1 |
-
"""Evaluate BERT router on SWE-Router and compare with v11 XGBoost."""
|
| 2 |
-
import json, os, sys, random, pickle, math
|
| 3 |
-
import numpy as np
|
| 4 |
-
from collections import defaultdict
|
| 5 |
-
|
| 6 |
-
MODELS = ['claude-opus-4.7','gpt-5-mini','gpt-5-nano','gpt-5.2',
|
| 7 |
-
'gemini-2.5-pro','gemini-3-pro','deepseek-v3.2','deepseek-v4-flash']
|
| 8 |
-
MODEL_TIER = {'deepseek-v4-flash':1,'gpt-5-nano':1,'gpt-5-mini':2,'deepseek-v3.2':2,
|
| 9 |
-
'gemini-2.5-pro':3,'claude-opus-4.7':4,'gpt-5.2':4,'gemini-3-pro':5}
|
| 10 |
-
TIER_COST = {1:0.01,2:0.05,3:0.15,4:0.30,5:0.50}
|
| 11 |
-
TIER_TO_SWE = {1:'deepseek-v4-flash',2:'gpt-5-mini',3:'gemini-2.5-pro',4:'claude-opus-4.7',5:'gemini-3-pro'}
|
| 12 |
-
TASK_FLOOR = {"quick_answer":1,"coding":3,"research":3,"document_drafting":2,
|
| 13 |
-
"legal_regulated":4,"tool_heavy":2,"retrieval_heavy":2,"long_horizon":3,"unknown_ambiguous":3}
|
| 14 |
-
CODE_KW = ["python","javascript","code","function","bug","debug","refactor","implement","test",
|
| 15 |
-
"compile","runtime","segfault","thread","async","class","module","import","error","traceback"]
|
| 16 |
-
LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
|
| 17 |
-
RESEARCH_KW = ["research","investigate","compare","analyze","survey","paper"]
|
| 18 |
-
TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
|
| 19 |
-
CRITICAL_KW = ["critical","production","urgent","emergency","live","deployed","safety","security"]
|
| 20 |
-
SIMPLE_KW = ["typo","simple","quick","brief","minor","small","easy","trivial","just"]
|
| 21 |
-
LONG_KW = ["plan","project","roadmap","orchestrate","migrate","pipeline","deploy","architecture"]
|
| 22 |
-
MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
|
| 23 |
-
|
| 24 |
-
def extract_features(pt):
|
| 25 |
-
r = pt.lower()
|
| 26 |
-
return {'req_len':len(pt),'num_words':len(pt.split()),
|
| 27 |
-
'has_code':int(any(k in r for k in CODE_KW)),'n_code':sum(1 for k in CODE_KW if k in r),
|
| 28 |
-
'has_legal':int(any(k in r for k in LEGAL_KW)),'has_research':int(any(k in r for k in RESEARCH_KW)),
|
| 29 |
-
'has_tool':int(any(k in r for k in TOOL_KW)),'has_critical':int(any(k in r for k in CRITICAL_KW)),
|
| 30 |
-
'has_simple':int(any(k in r for k in SIMPLE_KW)),'has_long':int(any(k in r for k in LONG_KW)),
|
| 31 |
-
'has_math':int(any(k in r for k in MATH_KW)),'has_error_msg':int('error' in r or 'traceback' in r),
|
| 32 |
-
'has_file_path':int('/' in r),'n_lines':pt.count('\n')+1,
|
| 33 |
-
'has_version':int('version' in r or 'update' in r),'has_add':int('add' in r or 'new' in r),
|
| 34 |
-
'has_fix':int('fix' in r or 'bug' in r),'has_change':int('change' in r or 'modify' in r),
|
| 35 |
-
'has_remove':int('remove' in r),'has_test':int('test' in r or 'spec' in r),
|
| 36 |
-
'has_doc':int('doc' in r or 'readme' in r)}
|
| 37 |
-
|
| 38 |
-
def classify_task(text):
|
| 39 |
-
r = text.lower()
|
| 40 |
-
if any(k in r for k in ["contract","legal","compliance","gdpr","privacy"]): return "legal_regulated"
|
| 41 |
-
if any(k in r for k in ["debug","fix","bug","implement","refactor","code","function"]): return "coding"
|
| 42 |
-
if any(k in r for k in ["research","find sources","literature","investigate"]): return "research"
|
| 43 |
-
if any(k in r for k in ["search","fetch","api","query","database"]): return "tool_heavy"
|
| 44 |
-
if any(k in r for k in ["plan","roadmap","orchestrate","migrate","deploy"]): return "long_horizon"
|
| 45 |
-
if any(k in r for k in ["draft","write","compose","document"]): return "document_drafting"
|
| 46 |
-
if any(k in r for k in ["what is","explain","define","briefly"]): return "quick_answer"
|
| 47 |
-
return "unknown_ambiguous"
|
| 48 |
-
|
| 49 |
-
print("BERT Router Evaluation on SWE-Router")
|
| 50 |
-
print("="*60)
|
| 51 |
-
|
| 52 |
-
# ── Load SWE-Router data ──
|
| 53 |
-
print("\n[1] Loading SWE-Router data...")
|
| 54 |
-
from datasets import load_dataset
|
| 55 |
-
traces = defaultdict(dict)
|
| 56 |
-
for model in MODELS:
|
| 57 |
-
try:
|
| 58 |
-
ds = load_dataset(f'SWE-Router/swebench-verified-{model}', split='test')
|
| 59 |
-
for row in ds:
|
| 60 |
-
iid = row['instance_id']
|
| 61 |
-
traces[iid][model] = {'resolved': row['resolved'], 'cost': float(row['instance_cost']),
|
| 62 |
-
'problem': row['problem_statement']}
|
| 63 |
-
print(f" {model}: loaded")
|
| 64 |
-
except Exception as e:
|
| 65 |
-
print(f" {model}: FAILED - {e}")
|
| 66 |
-
print(f" Total tasks: {len(traces)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/run_bert_eval_launcher.py
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Download eval script parts from Hub, combine, and run."""
|
| 3 |
-
import os, subprocess
|
| 4 |
-
from huggingface_hub import hf_hub_download
|
| 5 |
-
|
| 6 |
-
REPO = "narcolepticchicken/agent-cost-optimizer"
|
| 7 |
-
parts = ["eval/run_bert_eval_full.py", "eval/eval_bert_partB.py", "eval/eval_bert_partC.py", "eval/eval_bert_partD.py"]
|
| 8 |
-
|
| 9 |
-
combined = "/app/eval_bert_combined.py"
|
| 10 |
-
with open(combined, "w") as out:
|
| 11 |
-
for part in parts:
|
| 12 |
-
path = hf_hub_download(REPO, part)
|
| 13 |
-
with open(path) as f:
|
| 14 |
-
out.write(f.read())
|
| 15 |
-
out.write("\n\n")
|
| 16 |
-
|
| 17 |
-
print(f"Combined script: {combined}")
|
| 18 |
-
print(f"Size: {os.path.getsize(combined)} bytes")
|
| 19 |
-
|
| 20 |
-
# Run it
|
| 21 |
-
import subprocess, sys
|
| 22 |
-
result = subprocess.run([sys.executable, combined], capture_output=False)
|
| 23 |
-
sys.exit(result.returncode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/swe_bench_results.json
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"always_frontier": {
|
| 3 |
-
"success": 0.782,
|
| 4 |
-
"avg_cost": 0.3166872804999999,
|
| 5 |
-
"n": 500
|
| 6 |
-
},
|
| 7 |
-
"always_cheap": {
|
| 8 |
-
"success": 0.632,
|
| 9 |
-
"avg_cost": 0.014239461958399993,
|
| 10 |
-
"n": 500
|
| 11 |
-
},
|
| 12 |
-
"aco_v8": {
|
| 13 |
-
"success": 0.756,
|
| 14 |
-
"avg_cost": 0.29145885426799994,
|
| 15 |
-
"n": 500
|
| 16 |
-
},
|
| 17 |
-
"oracle": {
|
| 18 |
-
"success": 0.87,
|
| 19 |
-
"avg_cost": 0.05456037742200003,
|
| 20 |
-
"n": 500
|
| 21 |
-
},
|
| 22 |
-
"aco_v9_feedback": {
|
| 23 |
-
"success": 0.826,
|
| 24 |
-
"avg_cost": 0.4847524783679998,
|
| 25 |
-
"n": 500
|
| 26 |
-
}
|
| 27 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/swe_v10_results.json
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"v10_direct": {
|
| 3 |
-
"success": 0.766,
|
| 4 |
-
"avg_cost": 0.1878,
|
| 5 |
-
"costRed": 40.7
|
| 6 |
-
},
|
| 7 |
-
"v10_cascade": {
|
| 8 |
-
"success": 0.756,
|
| 9 |
-
"avg_cost": 0.1767,
|
| 10 |
-
"costRed": 44.2
|
| 11 |
-
},
|
| 12 |
-
"v10_feedback": {
|
| 13 |
-
"success": 0.848,
|
| 14 |
-
"avg_cost": 0.2014,
|
| 15 |
-
"costRed": 36.4
|
| 16 |
-
},
|
| 17 |
-
"v8_synthetic": {
|
| 18 |
-
"success": 0.658,
|
| 19 |
-
"avg_cost": 0.3534,
|
| 20 |
-
"costRed": -11.6
|
| 21 |
-
},
|
| 22 |
-
"frontier": {
|
| 23 |
-
"success": 0.782,
|
| 24 |
-
"avg_cost": 0.3167,
|
| 25 |
-
"costRed": 0.0
|
| 26 |
-
},
|
| 27 |
-
"oracle": {
|
| 28 |
-
"success": 0.87,
|
| 29 |
-
"avg_cost": 0.0624,
|
| 30 |
-
"costRed": 80.3
|
| 31 |
-
},
|
| 32 |
-
"always_cheap": {
|
| 33 |
-
"success": 0.632,
|
| 34 |
-
"avg_cost": 0.0142,
|
| 35 |
-
"costRed": 95.5
|
| 36 |
-
},
|
| 37 |
-
"key_finding": "v10 trained on REAL SWE-Router data achieves 36-44% cost reduction vs 8% for synthetic-trained v8. v10_feedback achieves HIGHER success than always-frontier (84.8% vs 78.2%) at 36.4% cost reduction."
|
| 38 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eval/v8_results.json
DELETED
|
@@ -1,69 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"always_frontier": {
|
| 3 |
-
"success": 0.8935,
|
| 4 |
-
"avg_cost": 1.0,
|
| 5 |
-
"unsafe_rate": 0.026,
|
| 6 |
-
"false_done": 0.0805,
|
| 7 |
-
"tier_dist": {
|
| 8 |
-
"4": 2000
|
| 9 |
-
}
|
| 10 |
-
},
|
| 11 |
-
"always_cheap": {
|
| 12 |
-
"success": 0.238,
|
| 13 |
-
"avg_cost": 0.04999999999999823,
|
| 14 |
-
"unsafe_rate": 0.762,
|
| 15 |
-
"false_done": 0.0,
|
| 16 |
-
"tier_dist": {
|
| 17 |
-
"1": 2000
|
| 18 |
-
}
|
| 19 |
-
},
|
| 20 |
-
"heuristic_static": {
|
| 21 |
-
"success": 0.825,
|
| 22 |
-
"avg_cost": 0.929275000000004,
|
| 23 |
-
"unsafe_rate": 0.058,
|
| 24 |
-
"false_done": 0.117,
|
| 25 |
-
"tier_dist": {
|
| 26 |
-
"4": 674,
|
| 27 |
-
"3": 646,
|
| 28 |
-
"5": 443,
|
| 29 |
-
"2": 237
|
| 30 |
-
}
|
| 31 |
-
},
|
| 32 |
-
"oracle": {
|
| 33 |
-
"success": 1.0,
|
| 34 |
-
"avg_cost": 0.46504999999999164,
|
| 35 |
-
"unsafe_rate": 0.0,
|
| 36 |
-
"false_done": 0.0,
|
| 37 |
-
"tier_dist": {
|
| 38 |
-
"3": 647,
|
| 39 |
-
"1": 476,
|
| 40 |
-
"2": 567,
|
| 41 |
-
"4": 258,
|
| 42 |
-
"5": 52
|
| 43 |
-
}
|
| 44 |
-
},
|
| 45 |
-
"v8_dynamic+ML": {
|
| 46 |
-
"success": 0.82,
|
| 47 |
-
"avg_cost": 0.922775000000004,
|
| 48 |
-
"unsafe_rate": 0.0615,
|
| 49 |
-
"false_done": 0.1185,
|
| 50 |
-
"tier_dist": {
|
| 51 |
-
"3": 754,
|
| 52 |
-
"4": 538,
|
| 53 |
-
"5": 471,
|
| 54 |
-
"2": 237
|
| 55 |
-
}
|
| 56 |
-
},
|
| 57 |
-
"v8_dynamic_only": {
|
| 58 |
-
"success": 0.82,
|
| 59 |
-
"avg_cost": 0.922775000000004,
|
| 60 |
-
"unsafe_rate": 0.0615,
|
| 61 |
-
"false_done": 0.1185,
|
| 62 |
-
"tier_dist": {
|
| 63 |
-
"3": 754,
|
| 64 |
-
"4": 538,
|
| 65 |
-
"5": 471,
|
| 66 |
-
"2": 237
|
| 67 |
-
}
|
| 68 |
-
}
|
| 69 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
router_models/bert_5class/config.json
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"activation": "gelu",
|
| 3 |
-
"architectures": [
|
| 4 |
-
"DistilBertForSequenceClassification"
|
| 5 |
-
],
|
| 6 |
-
"attention_dropout": 0.1,
|
| 7 |
-
"bos_token_id": null,
|
| 8 |
-
"dim": 768,
|
| 9 |
-
"dropout": 0.1,
|
| 10 |
-
"dtype": "float32",
|
| 11 |
-
"eos_token_id": null,
|
| 12 |
-
"hidden_dim": 3072,
|
| 13 |
-
"id2label": {
|
| 14 |
-
"0": "LABEL_0",
|
| 15 |
-
"1": "LABEL_1",
|
| 16 |
-
"2": "LABEL_2",
|
| 17 |
-
"3": "LABEL_3",
|
| 18 |
-
"4": "LABEL_4"
|
| 19 |
-
},
|
| 20 |
-
"initializer_range": 0.02,
|
| 21 |
-
"label2id": {
|
| 22 |
-
"LABEL_0": 0,
|
| 23 |
-
"LABEL_1": 1,
|
| 24 |
-
"LABEL_2": 2,
|
| 25 |
-
"LABEL_3": 3,
|
| 26 |
-
"LABEL_4": 4
|
| 27 |
-
},
|
| 28 |
-
"max_position_embeddings": 512,
|
| 29 |
-
"model_type": "distilbert",
|
| 30 |
-
"n_heads": 12,
|
| 31 |
-
"n_layers": 6,
|
| 32 |
-
"pad_token_id": 0,
|
| 33 |
-
"problem_type": "single_label_classification",
|
| 34 |
-
"qa_dropout": 0.1,
|
| 35 |
-
"seq_classif_dropout": 0.2,
|
| 36 |
-
"sinusoidal_pos_embds": false,
|
| 37 |
-
"tie_weights_": true,
|
| 38 |
-
"tie_word_embeddings": true,
|
| 39 |
-
"transformers_version": "5.8.0",
|
| 40 |
-
"use_cache": false,
|
| 41 |
-
"vocab_size": 30522
|
| 42 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
router_models/bert_5class/metadata.json
DELETED
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"model": "distilbert-base-uncased",
|
| 3 |
-
"num_labels": 5,
|
| 4 |
-
"task": "5-class optimal tier prediction",
|
| 5 |
-
"training_dataset": "CARROT-LLM-Routing/SPROUT",
|
| 6 |
-
"n_training_samples": 27871,
|
| 7 |
-
"correctness_threshold": 0.7,
|
| 8 |
-
"tier_mapping": {
|
| 9 |
-
"wxai-granite-3-2b-instruct-8k-max-tokens": 1,
|
| 10 |
-
"wxai-llama-3-2-1b-instruct": 1,
|
| 11 |
-
"wxai-llama-3-2-3b-instruct": 1,
|
| 12 |
-
"wxai-llama-3-1-8b-instruct": 2,
|
| 13 |
-
"wxai-granite-3-8b-instruct-8k-max-tokens": 2,
|
| 14 |
-
"openai-gpt-4o-mini": 2,
|
| 15 |
-
"wxai-mixtral-8x7b-instruct-v01": 3,
|
| 16 |
-
"aws-titan-text-premier-v1": 3,
|
| 17 |
-
"openai-gpt-4o": 4,
|
| 18 |
-
"wxai-llama-3-1-70b-instruct": 4,
|
| 19 |
-
"wxai-llama-3-3-70b-instruct": 4,
|
| 20 |
-
"aws-claude-3-5-sonnet-v1": 4,
|
| 21 |
-
"wxai-llama-3-405b-instruct": 5
|
| 22 |
-
},
|
| 23 |
-
"max_seq_len": 512,
|
| 24 |
-
"epochs": 5,
|
| 25 |
-
"learning_rate": 2e-05,
|
| 26 |
-
"final_metrics": {
|
| 27 |
-
"eval_loss": 0.6286362409591675,
|
| 28 |
-
"eval_accuracy": 0.7694543106231837,
|
| 29 |
-
"eval_off_by_one": 0.9228285437520181,
|
| 30 |
-
"eval_acc_tier1": 0.9991586032814472,
|
| 31 |
-
"eval_acc_tier2": 0.012605042016806723,
|
| 32 |
-
"eval_acc_tier3": 0.0,
|
| 33 |
-
"eval_acc_tier4": 0.017391304347826087,
|
| 34 |
-
"eval_acc_tier5": 0.0,
|
| 35 |
-
"eval_runtime": 12.4974,
|
| 36 |
-
"eval_samples_per_second": 247.811,
|
| 37 |
-
"eval_steps_per_second": 3.921,
|
| 38 |
-
"epoch": 5.0
|
| 39 |
-
},
|
| 40 |
-
"label_distribution": {
|
| 41 |
-
"1": 23972,
|
| 42 |
-
"2": 4727,
|
| 43 |
-
"4": 1119,
|
| 44 |
-
"5": 636,
|
| 45 |
-
"3": 514
|
| 46 |
-
}
|
| 47 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
router_models/bert_5class/model.safetensors
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:3865791a1f5061c928e8252f6badb06956b94f91e4f11ba3130e15e2590ff5cf
|
| 3 |
-
size 267841796
|
|
|
|
|
|
|
|
|
|
|
|
router_models/bert_5class/tokenizer.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
router_models/bert_5class/tokenizer_config.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"backend": "tokenizers",
|
| 3 |
-
"cls_token": "[CLS]",
|
| 4 |
-
"do_lower_case": true,
|
| 5 |
-
"is_local": false,
|
| 6 |
-
"local_files_only": false,
|
| 7 |
-
"mask_token": "[MASK]",
|
| 8 |
-
"model_max_length": 512,
|
| 9 |
-
"pad_token": "[PAD]",
|
| 10 |
-
"sep_token": "[SEP]",
|
| 11 |
-
"strip_accents": null,
|
| 12 |
-
"tokenize_chinese_chars": true,
|
| 13 |
-
"tokenizer_class": "BertTokenizer",
|
| 14 |
-
"unk_token": "[UNK]"
|
| 15 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
router_models/bert_router/config.json
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"activation": "gelu",
|
| 3 |
-
"architectures": [
|
| 4 |
-
"DistilBertForSequenceClassification"
|
| 5 |
-
],
|
| 6 |
-
"attention_dropout": 0.1,
|
| 7 |
-
"bos_token_id": null,
|
| 8 |
-
"dim": 768,
|
| 9 |
-
"dropout": 0.1,
|
| 10 |
-
"dtype": "float32",
|
| 11 |
-
"eos_token_id": null,
|
| 12 |
-
"hidden_dim": 3072,
|
| 13 |
-
"initializer_range": 0.02,
|
| 14 |
-
"max_position_embeddings": 512,
|
| 15 |
-
"model_type": "distilbert",
|
| 16 |
-
"n_heads": 12,
|
| 17 |
-
"n_layers": 6,
|
| 18 |
-
"pad_token_id": 0,
|
| 19 |
-
"problem_type": "single_label_classification",
|
| 20 |
-
"qa_dropout": 0.1,
|
| 21 |
-
"seq_classif_dropout": 0.2,
|
| 22 |
-
"sinusoidal_pos_embds": false,
|
| 23 |
-
"tie_weights_": true,
|
| 24 |
-
"tie_word_embeddings": true,
|
| 25 |
-
"transformers_version": "5.8.0",
|
| 26 |
-
"use_cache": false,
|
| 27 |
-
"vocab_size": 30522
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
router_models/bert_router/model.safetensors
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:21baff75e1ebe25d72bd3c13383eb7d563cf1830c03a37dc81d173706fe473d9
|
| 3 |
-
size 267832560
|
|
|
|
|
|
|
|
|
|
|
|
router_models/bert_router/tokenizer.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
router_models/bert_router/tokenizer_config.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"backend": "tokenizers",
|
| 3 |
-
"cls_token": "[CLS]",
|
| 4 |
-
"do_lower_case": true,
|
| 5 |
-
"is_local": false,
|
| 6 |
-
"local_files_only": false,
|
| 7 |
-
"mask_token": "[MASK]",
|
| 8 |
-
"model_max_length": 512,
|
| 9 |
-
"pad_token": "[PAD]",
|
| 10 |
-
"sep_token": "[SEP]",
|
| 11 |
-
"strip_accents": null,
|
| 12 |
-
"tokenize_chinese_chars": true,
|
| 13 |
-
"tokenizer_class": "BertTokenizer",
|
| 14 |
-
"unk_token": "[UNK]"
|
| 15 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
router_models/tier_1_success.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
router_models/tier_2_success.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
router_models/tier_3_success.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
router_models/tier_4_success.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
router_models/tier_5_success.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
router_models/v6_eval_results.json
DELETED
|
@@ -1,304 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"always_frontier": {
|
| 3 |
-
"success": 0.8935,
|
| 4 |
-
"avg_cost": 1.0,
|
| 5 |
-
"unsafe_rate": 0.023,
|
| 6 |
-
"false_done": 0.0835,
|
| 7 |
-
"tier_dist": {
|
| 8 |
-
"4": 2000
|
| 9 |
-
},
|
| 10 |
-
"escalations": 878,
|
| 11 |
-
"downgrades": 420,
|
| 12 |
-
"heuristic_only": 702
|
| 13 |
-
},
|
| 14 |
-
"always_cheap": {
|
| 15 |
-
"success": 0.2225,
|
| 16 |
-
"avg_cost": 0.04999999999999823,
|
| 17 |
-
"unsafe_rate": 0.7775,
|
| 18 |
-
"false_done": 0.0,
|
| 19 |
-
"tier_dist": {
|
| 20 |
-
"1": 2000
|
| 21 |
-
},
|
| 22 |
-
"escalations": 0,
|
| 23 |
-
"downgrades": 2000,
|
| 24 |
-
"heuristic_only": 0
|
| 25 |
-
},
|
| 26 |
-
"heuristic_diff+1": {
|
| 27 |
-
"success": 0.8405,
|
| 28 |
-
"avg_cost": 0.9271500000000037,
|
| 29 |
-
"unsafe_rate": 0.047,
|
| 30 |
-
"false_done": 0.1125,
|
| 31 |
-
"tier_dist": {
|
| 32 |
-
"4": 702,
|
| 33 |
-
"3": 651,
|
| 34 |
-
"5": 420,
|
| 35 |
-
"2": 227
|
| 36 |
-
},
|
| 37 |
-
"escalations": 0,
|
| 38 |
-
"downgrades": 0,
|
| 39 |
-
"heuristic_only": 2000
|
| 40 |
-
},
|
| 41 |
-
"heuristic_floor": {
|
| 42 |
-
"success": 0.5875,
|
| 43 |
-
"avg_cost": 0.5013749999999922,
|
| 44 |
-
"unsafe_rate": 0.28,
|
| 45 |
-
"false_done": 0.1325,
|
| 46 |
-
"tier_dist": {
|
| 47 |
-
"3": 913,
|
| 48 |
-
"2": 651,
|
| 49 |
-
"4": 209,
|
| 50 |
-
"1": 227
|
| 51 |
-
},
|
| 52 |
-
"escalations": 0,
|
| 53 |
-
"downgrades": 2000,
|
| 54 |
-
"heuristic_only": 0
|
| 55 |
-
},
|
| 56 |
-
"oracle": {
|
| 57 |
-
"success": 0.998,
|
| 58 |
-
"avg_cost": 0.47694999999999094,
|
| 59 |
-
"unsafe_rate": 0.0,
|
| 60 |
-
"false_done": 0.002,
|
| 61 |
-
"tier_dist": {
|
| 62 |
-
"3": 644,
|
| 63 |
-
"5": 46,
|
| 64 |
-
"2": 571,
|
| 65 |
-
"4": 294,
|
| 66 |
-
"1": 445
|
| 67 |
-
},
|
| 68 |
-
"escalations": 94,
|
| 69 |
-
"downgrades": 1486,
|
| 70 |
-
"heuristic_only": 420
|
| 71 |
-
},
|
| 72 |
-
"hybrid_s0.25_d0.70": {
|
| 73 |
-
"success": 0.8175,
|
| 74 |
-
"avg_cost": 0.8221500000000023,
|
| 75 |
-
"unsafe_rate": 0.0595,
|
| 76 |
-
"false_done": 0.123,
|
| 77 |
-
"tier_dist": {
|
| 78 |
-
"4": 1122,
|
| 79 |
-
"3": 651,
|
| 80 |
-
"2": 227
|
| 81 |
-
},
|
| 82 |
-
"escalations": 0,
|
| 83 |
-
"downgrades": 420,
|
| 84 |
-
"heuristic_only": 1580
|
| 85 |
-
},
|
| 86 |
-
"hybrid_s0.25_d0.75": {
|
| 87 |
-
"success": 0.8175,
|
| 88 |
-
"avg_cost": 0.8221500000000023,
|
| 89 |
-
"unsafe_rate": 0.0595,
|
| 90 |
-
"false_done": 0.123,
|
| 91 |
-
"tier_dist": {
|
| 92 |
-
"4": 1122,
|
| 93 |
-
"3": 651,
|
| 94 |
-
"2": 227
|
| 95 |
-
},
|
| 96 |
-
"escalations": 0,
|
| 97 |
-
"downgrades": 420,
|
| 98 |
-
"heuristic_only": 1580
|
| 99 |
-
},
|
| 100 |
-
"hybrid_s0.25_d0.80": {
|
| 101 |
-
"success": 0.8225,
|
| 102 |
-
"avg_cost": 0.8416500000000027,
|
| 103 |
-
"unsafe_rate": 0.058,
|
| 104 |
-
"false_done": 0.1195,
|
| 105 |
-
"tier_dist": {
|
| 106 |
-
"4": 1044,
|
| 107 |
-
"3": 651,
|
| 108 |
-
"5": 78,
|
| 109 |
-
"2": 227
|
| 110 |
-
},
|
| 111 |
-
"escalations": 0,
|
| 112 |
-
"downgrades": 342,
|
| 113 |
-
"heuristic_only": 1658
|
| 114 |
-
},
|
| 115 |
-
"hybrid_s0.25_d0.85": {
|
| 116 |
-
"success": 0.838,
|
| 117 |
-
"avg_cost": 0.9084000000000035,
|
| 118 |
-
"unsafe_rate": 0.048,
|
| 119 |
-
"false_done": 0.114,
|
| 120 |
-
"tier_dist": {
|
| 121 |
-
"4": 777,
|
| 122 |
-
"3": 651,
|
| 123 |
-
"5": 345,
|
| 124 |
-
"2": 227
|
| 125 |
-
},
|
| 126 |
-
"escalations": 0,
|
| 127 |
-
"downgrades": 75,
|
| 128 |
-
"heuristic_only": 1925
|
| 129 |
-
},
|
| 130 |
-
"hybrid_s0.30_d0.70": {
|
| 131 |
-
"success": 0.8175,
|
| 132 |
-
"avg_cost": 0.8221500000000023,
|
| 133 |
-
"unsafe_rate": 0.0595,
|
| 134 |
-
"false_done": 0.123,
|
| 135 |
-
"tier_dist": {
|
| 136 |
-
"4": 1122,
|
| 137 |
-
"3": 651,
|
| 138 |
-
"2": 227
|
| 139 |
-
},
|
| 140 |
-
"escalations": 0,
|
| 141 |
-
"downgrades": 420,
|
| 142 |
-
"heuristic_only": 1580
|
| 143 |
-
},
|
| 144 |
-
"hybrid_s0.30_d0.75": {
|
| 145 |
-
"success": 0.8175,
|
| 146 |
-
"avg_cost": 0.8221500000000023,
|
| 147 |
-
"unsafe_rate": 0.0595,
|
| 148 |
-
"false_done": 0.123,
|
| 149 |
-
"tier_dist": {
|
| 150 |
-
"4": 1122,
|
| 151 |
-
"3": 651,
|
| 152 |
-
"2": 227
|
| 153 |
-
},
|
| 154 |
-
"escalations": 0,
|
| 155 |
-
"downgrades": 420,
|
| 156 |
-
"heuristic_only": 1580
|
| 157 |
-
},
|
| 158 |
-
"hybrid_s0.30_d0.80": {
|
| 159 |
-
"success": 0.8225,
|
| 160 |
-
"avg_cost": 0.8416500000000027,
|
| 161 |
-
"unsafe_rate": 0.058,
|
| 162 |
-
"false_done": 0.1195,
|
| 163 |
-
"tier_dist": {
|
| 164 |
-
"4": 1044,
|
| 165 |
-
"3": 651,
|
| 166 |
-
"5": 78,
|
| 167 |
-
"2": 227
|
| 168 |
-
},
|
| 169 |
-
"escalations": 0,
|
| 170 |
-
"downgrades": 342,
|
| 171 |
-
"heuristic_only": 1658
|
| 172 |
-
},
|
| 173 |
-
"hybrid_s0.30_d0.85": {
|
| 174 |
-
"success": 0.838,
|
| 175 |
-
"avg_cost": 0.9084000000000035,
|
| 176 |
-
"unsafe_rate": 0.048,
|
| 177 |
-
"false_done": 0.114,
|
| 178 |
-
"tier_dist": {
|
| 179 |
-
"4": 777,
|
| 180 |
-
"3": 651,
|
| 181 |
-
"5": 345,
|
| 182 |
-
"2": 227
|
| 183 |
-
},
|
| 184 |
-
"escalations": 0,
|
| 185 |
-
"downgrades": 75,
|
| 186 |
-
"heuristic_only": 1925
|
| 187 |
-
},
|
| 188 |
-
"hybrid_s0.35_d0.70": {
|
| 189 |
-
"success": 0.8175,
|
| 190 |
-
"avg_cost": 0.8221500000000023,
|
| 191 |
-
"unsafe_rate": 0.0595,
|
| 192 |
-
"false_done": 0.123,
|
| 193 |
-
"tier_dist": {
|
| 194 |
-
"4": 1122,
|
| 195 |
-
"3": 651,
|
| 196 |
-
"2": 227
|
| 197 |
-
},
|
| 198 |
-
"escalations": 0,
|
| 199 |
-
"downgrades": 420,
|
| 200 |
-
"heuristic_only": 1580
|
| 201 |
-
},
|
| 202 |
-
"hybrid_s0.35_d0.75": {
|
| 203 |
-
"success": 0.8175,
|
| 204 |
-
"avg_cost": 0.8221500000000023,
|
| 205 |
-
"unsafe_rate": 0.0595,
|
| 206 |
-
"false_done": 0.123,
|
| 207 |
-
"tier_dist": {
|
| 208 |
-
"4": 1122,
|
| 209 |
-
"3": 651,
|
| 210 |
-
"2": 227
|
| 211 |
-
},
|
| 212 |
-
"escalations": 0,
|
| 213 |
-
"downgrades": 420,
|
| 214 |
-
"heuristic_only": 1580
|
| 215 |
-
},
|
| 216 |
-
"hybrid_s0.35_d0.80": {
|
| 217 |
-
"success": 0.8225,
|
| 218 |
-
"avg_cost": 0.8416500000000027,
|
| 219 |
-
"unsafe_rate": 0.058,
|
| 220 |
-
"false_done": 0.1195,
|
| 221 |
-
"tier_dist": {
|
| 222 |
-
"4": 1044,
|
| 223 |
-
"3": 651,
|
| 224 |
-
"5": 78,
|
| 225 |
-
"2": 227
|
| 226 |
-
},
|
| 227 |
-
"escalations": 0,
|
| 228 |
-
"downgrades": 342,
|
| 229 |
-
"heuristic_only": 1658
|
| 230 |
-
},
|
| 231 |
-
"hybrid_s0.35_d0.85": {
|
| 232 |
-
"success": 0.838,
|
| 233 |
-
"avg_cost": 0.9084000000000035,
|
| 234 |
-
"unsafe_rate": 0.048,
|
| 235 |
-
"false_done": 0.114,
|
| 236 |
-
"tier_dist": {
|
| 237 |
-
"4": 777,
|
| 238 |
-
"3": 651,
|
| 239 |
-
"5": 345,
|
| 240 |
-
"2": 227
|
| 241 |
-
},
|
| 242 |
-
"escalations": 0,
|
| 243 |
-
"downgrades": 75,
|
| 244 |
-
"heuristic_only": 1925
|
| 245 |
-
},
|
| 246 |
-
"hybrid_s0.40_d0.70": {
|
| 247 |
-
"success": 0.8175,
|
| 248 |
-
"avg_cost": 0.8221500000000023,
|
| 249 |
-
"unsafe_rate": 0.0595,
|
| 250 |
-
"false_done": 0.123,
|
| 251 |
-
"tier_dist": {
|
| 252 |
-
"4": 1122,
|
| 253 |
-
"3": 651,
|
| 254 |
-
"2": 227
|
| 255 |
-
},
|
| 256 |
-
"escalations": 0,
|
| 257 |
-
"downgrades": 420,
|
| 258 |
-
"heuristic_only": 1580
|
| 259 |
-
},
|
| 260 |
-
"hybrid_s0.40_d0.75": {
|
| 261 |
-
"success": 0.8175,
|
| 262 |
-
"avg_cost": 0.8221500000000023,
|
| 263 |
-
"unsafe_rate": 0.0595,
|
| 264 |
-
"false_done": 0.123,
|
| 265 |
-
"tier_dist": {
|
| 266 |
-
"4": 1122,
|
| 267 |
-
"3": 651,
|
| 268 |
-
"2": 227
|
| 269 |
-
},
|
| 270 |
-
"escalations": 0,
|
| 271 |
-
"downgrades": 420,
|
| 272 |
-
"heuristic_only": 1580
|
| 273 |
-
},
|
| 274 |
-
"hybrid_s0.40_d0.80": {
|
| 275 |
-
"success": 0.8225,
|
| 276 |
-
"avg_cost": 0.8416500000000027,
|
| 277 |
-
"unsafe_rate": 0.058,
|
| 278 |
-
"false_done": 0.1195,
|
| 279 |
-
"tier_dist": {
|
| 280 |
-
"4": 1044,
|
| 281 |
-
"3": 651,
|
| 282 |
-
"5": 78,
|
| 283 |
-
"2": 227
|
| 284 |
-
},
|
| 285 |
-
"escalations": 0,
|
| 286 |
-
"downgrades": 342,
|
| 287 |
-
"heuristic_only": 1658
|
| 288 |
-
},
|
| 289 |
-
"hybrid_s0.40_d0.85": {
|
| 290 |
-
"success": 0.838,
|
| 291 |
-
"avg_cost": 0.9084000000000035,
|
| 292 |
-
"unsafe_rate": 0.048,
|
| 293 |
-
"false_done": 0.114,
|
| 294 |
-
"tier_dist": {
|
| 295 |
-
"4": 777,
|
| 296 |
-
"3": 651,
|
| 297 |
-
"5": 345,
|
| 298 |
-
"2": 227
|
| 299 |
-
},
|
| 300 |
-
"escalations": 0,
|
| 301 |
-
"downgrades": 75,
|
| 302 |
-
"heuristic_only": 1925
|
| 303 |
-
}
|
| 304 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/train_baar_router.py
DELETED
|
@@ -1,470 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""BAAR-Style Profiling Router for ACO.
|
| 3 |
-
|
| 4 |
-
Inspired by BAAR (Shnitzer et al., 2025): "Budget-Aware Adaptive Routing."
|
| 5 |
-
The core idea: profile with a cheap model first, use its output as a feature
|
| 6 |
-
for deciding whether to escalate to a stronger model.
|
| 7 |
-
|
| 8 |
-
Our approach:
|
| 9 |
-
1. BERT [CLS] embedding of the problem statement → semantic features
|
| 10 |
-
2. Combine with classic keyword features
|
| 11 |
-
3. XGBoost predicts P(success) for each tier
|
| 12 |
-
4. Route to cheapest tier with P(success) >= threshold
|
| 13 |
-
5. Compare: BERT+XGBoost vs keyword-only XGBoost vs frontier vs oracle
|
| 14 |
-
|
| 15 |
-
The key innovation over v10: BERT features capture semantic difficulty
|
| 16 |
-
that keyword features miss (e.g., "fix the segfault in the async event loop"
|
| 17 |
-
vs "add a comment to the README" — keywords see "segfault" and "async" and
|
| 18 |
-
think it's hard, but the BERT embedding captures the holistic semantics).
|
| 19 |
-
"""
|
| 20 |
-
import json, os, sys, pickle, random
|
| 21 |
-
import numpy as np
|
| 22 |
-
from collections import defaultdict
|
| 23 |
-
from datasets import load_dataset
|
| 24 |
-
from transformers import AutoTokenizer, AutoModel
|
| 25 |
-
import torch
|
| 26 |
-
|
| 27 |
-
# ── Config ──────────────────────────────────────────────────────
|
| 28 |
-
REPO = "narcolepticchicken/agent-cost-optimizer"
|
| 29 |
-
TIER_COST = {1: 0.01, 2: 0.05, 3: 0.15, 4: 0.30, 5: 0.50}
|
| 30 |
-
TIER_TO_SWE = {
|
| 31 |
-
1: "deepseek-v4-flash", 2: "gpt-5-mini",
|
| 32 |
-
3: "gemini-2.5-pro", 4: "claude-opus-4.7", 5: "gemini-3-pro",
|
| 33 |
-
}
|
| 34 |
-
MODEL_TIER = {
|
| 35 |
-
"deepseek-v4-flash": 1, "gpt-5-nano": 1,
|
| 36 |
-
"gpt-5-mini": 2, "deepseek-v3.2": 2,
|
| 37 |
-
"gemini-2.5-pro": 3,
|
| 38 |
-
"claude-opus-4.7": 4, "gpt-5.2": 4,
|
| 39 |
-
"gemini-3-pro": 5,
|
| 40 |
-
}
|
| 41 |
-
MODELS = list(MODEL_TIER.keys())
|
| 42 |
-
|
| 43 |
-
print("=" * 70)
|
| 44 |
-
print("BAAR-STYLE PROFILING ROUTER: BERT + XGBoost")
|
| 45 |
-
print("=" * 70)
|
| 46 |
-
|
| 47 |
-
# ── 1. Extract BERT [CLS] features ──────────────────────────────
|
| 48 |
-
print("\n[1] Loading BERT for feature extraction...")
|
| 49 |
-
|
| 50 |
-
# First try the trained 5-class model (has domain knowledge from SPROUT)
|
| 51 |
-
try:
|
| 52 |
-
tokenizer = AutoTokenizer.from_pretrained(
|
| 53 |
-
REPO, subfolder="router_models/bert_5class"
|
| 54 |
-
)
|
| 55 |
-
bert_model = AutoModel.from_pretrained(
|
| 56 |
-
REPO, subfolder="router_models/bert_5class"
|
| 57 |
-
)
|
| 58 |
-
print(" ✓ Loaded trained BERT 5-class from Hub (SPROUT-fine-tuned)")
|
| 59 |
-
except Exception as e:
|
| 60 |
-
print(f" Trained BERT failed: {e}")
|
| 61 |
-
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
|
| 62 |
-
bert_model = AutoModel.from_pretrained("distilbert-base-uncased")
|
| 63 |
-
print(" Using base distilbert (not fine-tuned)")
|
| 64 |
-
|
| 65 |
-
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 66 |
-
bert_model = bert_model.to(device)
|
| 67 |
-
bert_model.eval()
|
| 68 |
-
|
| 69 |
-
# ── 2. Load SWE-Router ──────────────────────────────────────────
|
| 70 |
-
print("\n[2] Loading SWE-Router execution data...")
|
| 71 |
-
traces = defaultdict(dict)
|
| 72 |
-
for model_name in MODELS:
|
| 73 |
-
ds = load_dataset(f"SWE-Router/swebench-verified-{model_name}", split="test")
|
| 74 |
-
for row in ds:
|
| 75 |
-
iid = row["instance_id"]
|
| 76 |
-
traces[iid][model_name] = {
|
| 77 |
-
"resolved": row["resolved"],
|
| 78 |
-
"cost": float(row["instance_cost"]),
|
| 79 |
-
"api_calls": int(row["api_calls"]),
|
| 80 |
-
"problem": row["problem_statement"],
|
| 81 |
-
}
|
| 82 |
-
print(f" {model_name}: loaded")
|
| 83 |
-
print(f" Total tasks: {len(traces)}")
|
| 84 |
-
|
| 85 |
-
# ── 3. Build combined features ──────────────────────────────────
|
| 86 |
-
print("\n[3] Extracting BERT [CLS] embeddings + keyword features...")
|
| 87 |
-
|
| 88 |
-
# Keyword features (same as v10)
|
| 89 |
-
CODE_KW = ["python","javascript","code","function","bug","debug","refactor","implement","test",
|
| 90 |
-
"compile","runtime","segfault","thread","async","class","module","import","error","traceback"]
|
| 91 |
-
LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
|
| 92 |
-
RESEARCH_KW = ["research","investigate","compare","analyze","survey","paper"]
|
| 93 |
-
TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
|
| 94 |
-
CRITICAL_KW = ["critical","production","urgent","emergency","live","deployed","safety","security"]
|
| 95 |
-
SIMPLE_KW = ["typo","simple","quick","brief","minor","small","easy","trivial","just"]
|
| 96 |
-
LONG_KW = ["plan","project","roadmap","orchestrate","migrate","pipeline","deploy","architecture"]
|
| 97 |
-
MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
|
| 98 |
-
|
| 99 |
-
BERT_DIM = 768 # distilbert-base-uncased
|
| 100 |
-
|
| 101 |
-
def extract_features(problem_text):
|
| 102 |
-
"""Extract combined BERT + keyword features."""
|
| 103 |
-
r = problem_text.lower()
|
| 104 |
-
|
| 105 |
-
# Classic keyword features
|
| 106 |
-
kw_feats = [
|
| 107 |
-
len(problem_text), # req_len
|
| 108 |
-
len(problem_text.split()), # num_words
|
| 109 |
-
int(any(k in r for k in CODE_KW)), # has_code
|
| 110 |
-
sum(1 for k in CODE_KW if k in r), # n_code
|
| 111 |
-
int(any(k in r for k in LEGAL_KW)),
|
| 112 |
-
int(any(k in r for k in RESEARCH_KW)),
|
| 113 |
-
int(any(k in r for k in TOOL_KW)),
|
| 114 |
-
int(any(k in r for k in CRITICAL_KW)),
|
| 115 |
-
int(any(k in r for k in SIMPLE_KW)),
|
| 116 |
-
int(any(k in r for k in LONG_KW)),
|
| 117 |
-
int(any(k in r for k in MATH_KW)),
|
| 118 |
-
int("error" in r or "traceback" in r or "exception" in r),
|
| 119 |
-
int("/" in r and "." in problem_text), # has_file_path
|
| 120 |
-
problem_text.count("\n") + 1, # n_lines
|
| 121 |
-
int("version" in r or "update" in r or "upgrade" in r),
|
| 122 |
-
int("add" in r or "new" in r or "create" in r),
|
| 123 |
-
int("fix" in r or "bug" in r or "issue" in r or "broken" in r),
|
| 124 |
-
int("change" in r or "modify" in r or "update" in r),
|
| 125 |
-
int("remove" in r or "delete" in r or "drop" in r),
|
| 126 |
-
int("test" in r or "spec" in r or "assert" in r),
|
| 127 |
-
int("doc" in r or "readme" in r or "comment" in r),
|
| 128 |
-
int("see also" in r or "related" in r),
|
| 129 |
-
int("steps to reproduce" in r or "reproduce" in r),
|
| 130 |
-
]
|
| 131 |
-
|
| 132 |
-
# BERT [CLS] embedding
|
| 133 |
-
enc = tokenizer(
|
| 134 |
-
problem_text[:2000], truncation=True, max_length=512,
|
| 135 |
-
return_tensors="pt",
|
| 136 |
-
)
|
| 137 |
-
enc = {k: v.to(device) for k, v in enc.items()}
|
| 138 |
-
with torch.no_grad():
|
| 139 |
-
outputs = bert_model(**enc)
|
| 140 |
-
cls_embedding = outputs.last_hidden_state[:, 0, :] # [1, 768]
|
| 141 |
-
|
| 142 |
-
# Combine
|
| 143 |
-
full = kw_feats + cls_embedding[0].cpu().numpy().tolist()
|
| 144 |
-
return full
|
| 145 |
-
|
| 146 |
-
# Build training data
|
| 147 |
-
X_feats = []
|
| 148 |
-
y_tiers = {t: [] for t in range(1, 6)}
|
| 149 |
-
y_optimal = []
|
| 150 |
-
feat_names = (
|
| 151 |
-
["req_len","num_words","has_code","n_code","has_legal","has_research","has_tool",
|
| 152 |
-
"has_critical","has_simple","has_long","has_math","has_error_msg","has_file_path",
|
| 153 |
-
"n_lines","has_version","has_add","has_fix","has_change","has_remove","has_test",
|
| 154 |
-
"has_doc","has_see_also","has_steps"] +
|
| 155 |
-
[f"bert_{i}" for i in range(BERT_DIM)]
|
| 156 |
-
)
|
| 157 |
-
|
| 158 |
-
for iid, model_results in traces.items():
|
| 159 |
-
problem = next(iter(model_results.values()))["problem"]
|
| 160 |
-
feats = extract_features(problem)
|
| 161 |
-
X_feats.append(feats)
|
| 162 |
-
|
| 163 |
-
# Per-tier success ground truth
|
| 164 |
-
tier_success = {}
|
| 165 |
-
for model_name, result in model_results.items():
|
| 166 |
-
tier = MODEL_TIER[model_name]
|
| 167 |
-
if tier not in tier_success:
|
| 168 |
-
tier_success[tier] = False
|
| 169 |
-
if result["resolved"]:
|
| 170 |
-
tier_success[tier] = True
|
| 171 |
-
|
| 172 |
-
for t in range(1, 6):
|
| 173 |
-
y_tiers[t].append(int(tier_success.get(t, False)))
|
| 174 |
-
|
| 175 |
-
# Optimal tier = cheapest that succeeded
|
| 176 |
-
optimal = 5
|
| 177 |
-
for t in range(1, 6):
|
| 178 |
-
if tier_success.get(t, False):
|
| 179 |
-
optimal = t
|
| 180 |
-
break
|
| 181 |
-
y_optimal.append(optimal)
|
| 182 |
-
|
| 183 |
-
X = np.array(X_feats, dtype=np.float32)
|
| 184 |
-
|
| 185 |
-
print(f" Feature matrix: {X.shape} ({len(feat_names)} features)")
|
| 186 |
-
print(f" BERT features: {BERT_DIM} dims + {len(feat_names)-BERT_DIM} keyword feats")
|
| 187 |
-
print(f" Per-tier success rates:")
|
| 188 |
-
for t in range(1, 6):
|
| 189 |
-
s = sum(y_tiers[t])
|
| 190 |
-
print(f" Tier {t}: {s}/{len(y_tiers[t])} = {s/len(y_tiers[t])*100:.1f}%")
|
| 191 |
-
print(f" Optimal tier distribution:")
|
| 192 |
-
opt_dist = defaultdict(int)
|
| 193 |
-
for t in y_optimal:
|
| 194 |
-
opt_dist[t] += 1
|
| 195 |
-
for t in sorted(opt_dist):
|
| 196 |
-
print(f" Tier {t}: {opt_dist[t]} ({opt_dist[t]/len(y_optimal)*100:.1f}%)")
|
| 197 |
-
|
| 198 |
-
# ── 4. Train XGBoost with BERT features ─────────────────────────
|
| 199 |
-
print("\n[4] Training BAAR-style XGBoost + BERT router...")
|
| 200 |
-
|
| 201 |
-
from xgboost import XGBClassifier
|
| 202 |
-
from sklearn.calibration import IsotonicRegression
|
| 203 |
-
from sklearn.model_selection import cross_val_score
|
| 204 |
-
import warnings
|
| 205 |
-
warnings.filterwarnings("ignore")
|
| 206 |
-
|
| 207 |
-
tier_clfs = {}
|
| 208 |
-
tier_calibs = {}
|
| 209 |
-
tier_cv = {}
|
| 210 |
-
|
| 211 |
-
for t in range(1, 6):
|
| 212 |
-
y = np.array(y_tiers[t])
|
| 213 |
-
n_pos = y.sum()
|
| 214 |
-
n_neg = len(y) - n_pos
|
| 215 |
-
spw = max(1, n_neg / max(n_pos, 1))
|
| 216 |
-
|
| 217 |
-
clf = XGBClassifier(
|
| 218 |
-
n_estimators=200, max_depth=6, learning_rate=0.05,
|
| 219 |
-
subsample=0.8, colsample_bytree=0.8,
|
| 220 |
-
scale_pos_weight=spw,
|
| 221 |
-
eval_metric="logloss",
|
| 222 |
-
random_state=42,
|
| 223 |
-
verbosity=0,
|
| 224 |
-
)
|
| 225 |
-
|
| 226 |
-
try:
|
| 227 |
-
scores = cross_val_score(clf, X, y, cv=5, scoring="f1")
|
| 228 |
-
tier_cv[t] = float(scores.mean())
|
| 229 |
-
except:
|
| 230 |
-
tier_cv[t] = 0.0
|
| 231 |
-
|
| 232 |
-
clf.fit(X, y)
|
| 233 |
-
|
| 234 |
-
# Calibrate probabilities
|
| 235 |
-
p_raw = clf.predict_proba(X)[:, 1]
|
| 236 |
-
cal = IsotonicRegression(out_of_bounds="clip")
|
| 237 |
-
cal.fit(p_raw, y)
|
| 238 |
-
|
| 239 |
-
tier_clfs[t] = clf
|
| 240 |
-
tier_calibs[t] = cal
|
| 241 |
-
|
| 242 |
-
p_cal = cal.transform(p_raw)
|
| 243 |
-
brier = np.mean((p_cal - y) ** 2)
|
| 244 |
-
|
| 245 |
-
print(f" Tier {t}: n_pos={n_pos}, CV_f1={tier_cv[t]:.3f}, Brier={brier:.4f}")
|
| 246 |
-
|
| 247 |
-
# Direct optimal-tier predictor
|
| 248 |
-
opt_clf = XGBClassifier(
|
| 249 |
-
n_estimators=300, max_depth=6, learning_rate=0.05,
|
| 250 |
-
subsample=0.8, colsample_bytree=0.8,
|
| 251 |
-
eval_metric="mlogloss",
|
| 252 |
-
random_state=42, num_class=5, verbosity=0,
|
| 253 |
-
)
|
| 254 |
-
opt_clf.fit(X, np.array(y_optimal) - 1)
|
| 255 |
-
opt_pred = opt_clf.predict(X) + 1
|
| 256 |
-
opt_acc = np.mean(opt_pred == np.array(y_optimal))
|
| 257 |
-
print(f" Direct optimal-tier accuracy: {opt_acc:.3f}")
|
| 258 |
-
|
| 259 |
-
# Also train a keyword-only baseline for ablation
|
| 260 |
-
print("\n[4b] Training keyword-only baseline (no BERT)...")
|
| 261 |
-
kw_X = X[:, :23] # First 23 are keyword features
|
| 262 |
-
|
| 263 |
-
kw_tier_clfs = {}
|
| 264 |
-
kw_tier_calibs = {}
|
| 265 |
-
kw_tier_cv = {}
|
| 266 |
-
|
| 267 |
-
for t in range(1, 6):
|
| 268 |
-
y = np.array(y_tiers[t])
|
| 269 |
-
n_pos = y.sum()
|
| 270 |
-
n_neg = len(y) - n_pos
|
| 271 |
-
spw = max(1, n_neg / max(n_pos, 1))
|
| 272 |
-
|
| 273 |
-
clf = XGBClassifier(
|
| 274 |
-
n_estimators=200, max_depth=6, learning_rate=0.05,
|
| 275 |
-
subsample=0.8, colsample_bytree=0.8,
|
| 276 |
-
scale_pos_weight=spw,
|
| 277 |
-
eval_metric="logloss",
|
| 278 |
-
random_state=42,
|
| 279 |
-
verbosity=0,
|
| 280 |
-
)
|
| 281 |
-
|
| 282 |
-
try:
|
| 283 |
-
scores = cross_val_score(clf, kw_X, y, cv=5, scoring="f1")
|
| 284 |
-
kw_tier_cv[t] = float(scores.mean())
|
| 285 |
-
except:
|
| 286 |
-
kw_tier_cv[t] = 0.0
|
| 287 |
-
|
| 288 |
-
clf.fit(kw_X, y)
|
| 289 |
-
p_raw = clf.predict_proba(kw_X)[:, 1]
|
| 290 |
-
cal = IsotonicRegression(out_of_bounds="clip")
|
| 291 |
-
cal.fit(p_raw, y)
|
| 292 |
-
|
| 293 |
-
kw_tier_clfs[t] = clf
|
| 294 |
-
kw_tier_calibs[t] = cal
|
| 295 |
-
|
| 296 |
-
# ── 5. Evaluate all policies ─────────────────────────────────────
|
| 297 |
-
print("\n[5] Evaluating routing policies...")
|
| 298 |
-
|
| 299 |
-
policies = defaultdict(lambda: {"success": 0, "cost": 0.0, "n": 0})
|
| 300 |
-
|
| 301 |
-
for iid, model_results in traces.items():
|
| 302 |
-
problem = next(iter(model_results.values()))["problem"]
|
| 303 |
-
feats = extract_features(problem)
|
| 304 |
-
feat_vec = np.array(feats, dtype=np.float32).reshape(1, -1)
|
| 305 |
-
kw_vec = np.array(feats[:23], dtype=np.float32).reshape(1, -1)
|
| 306 |
-
|
| 307 |
-
# ─── BAAR (BERT + XGBoost) ───
|
| 308 |
-
tier_probs_baar = {}
|
| 309 |
-
for t in range(1, 6):
|
| 310 |
-
p_raw = tier_clfs[t].predict_proba(feat_vec)[0, 1]
|
| 311 |
-
p_cal = float(tier_calibs[t].transform([p_raw])[0])
|
| 312 |
-
tier_probs_baar[t] = p_cal
|
| 313 |
-
|
| 314 |
-
baar_tier = 5
|
| 315 |
-
for t in range(1, 6):
|
| 316 |
-
if tier_probs_baar[t] >= 0.5:
|
| 317 |
-
baar_tier = t
|
| 318 |
-
break
|
| 319 |
-
baar_model = TIER_TO_SWE.get(baar_tier, "claude-opus-4.7")
|
| 320 |
-
if baar_model in model_results:
|
| 321 |
-
policies["baar"]["success"] += int(model_results[baar_model]["resolved"])
|
| 322 |
-
policies["baar"]["cost"] += model_results[baar_model]["cost"]
|
| 323 |
-
policies["baar"]["n"] += 1
|
| 324 |
-
|
| 325 |
-
# ─── BAAR + feedback (escalate on failure) ───
|
| 326 |
-
try_tier = baar_tier
|
| 327 |
-
total_cost = 0.0
|
| 328 |
-
success = False
|
| 329 |
-
while try_tier <= 5:
|
| 330 |
-
tm = TIER_TO_SWE.get(try_tier, "claude-opus-4.7")
|
| 331 |
-
if tm in model_results:
|
| 332 |
-
total_cost += model_results[tm]["cost"]
|
| 333 |
-
if model_results[tm]["resolved"]:
|
| 334 |
-
success = True
|
| 335 |
-
break
|
| 336 |
-
try_tier += 1
|
| 337 |
-
policies["baar_feedback"]["success"] += int(success)
|
| 338 |
-
policies["baar_feedback"]["cost"] += total_cost
|
| 339 |
-
policies["baar_feedback"]["n"] += 1
|
| 340 |
-
|
| 341 |
-
# ─── Keyword-only ───
|
| 342 |
-
kw_tier = 5
|
| 343 |
-
for t in range(1, 6):
|
| 344 |
-
p_raw = kw_tier_clfs[t].predict_proba(kw_vec)[0, 1]
|
| 345 |
-
p_cal = float(kw_tier_calibs[t].transform([p_raw])[0])
|
| 346 |
-
if p_cal >= 0.5:
|
| 347 |
-
kw_tier = t
|
| 348 |
-
break
|
| 349 |
-
kw_model = TIER_TO_SWE.get(kw_tier, "claude-opus-4.7")
|
| 350 |
-
if kw_model in model_results:
|
| 351 |
-
policies["keyword_only"]["success"] += int(model_results[kw_model]["resolved"])
|
| 352 |
-
policies["keyword_only"]["cost"] += model_results[kw_model]["cost"]
|
| 353 |
-
policies["keyword_only"]["n"] += 1
|
| 354 |
-
|
| 355 |
-
# ─── Frontiers ───
|
| 356 |
-
f_model = "claude-opus-4.7"
|
| 357 |
-
policies["frontier"]["success"] += int(model_results[f_model]["resolved"])
|
| 358 |
-
policies["frontier"]["cost"] += model_results[f_model]["cost"]
|
| 359 |
-
policies["frontier"]["n"] += 1
|
| 360 |
-
|
| 361 |
-
c_model = "deepseek-v4-flash"
|
| 362 |
-
if c_model in model_results:
|
| 363 |
-
policies["always_cheap"]["success"] += int(model_results[c_model]["resolved"])
|
| 364 |
-
policies["always_cheap"]["cost"] += model_results[c_model]["cost"]
|
| 365 |
-
policies["always_cheap"]["n"] += 1
|
| 366 |
-
|
| 367 |
-
# ─── Oracle ───
|
| 368 |
-
resolved = [(m, r) for m, r in model_results.items() if r["resolved"]]
|
| 369 |
-
if resolved:
|
| 370 |
-
cheapest = min(resolved, key=lambda x: TIER_COST.get(MODEL_TIER[x[0]], 1.0))
|
| 371 |
-
policies["oracle"]["success"] += 1
|
| 372 |
-
policies["oracle"]["cost"] += cheapest[1]["cost"]
|
| 373 |
-
policies["oracle"]["n"] += 1
|
| 374 |
-
|
| 375 |
-
# ── 6. Print results ────────────────────────────────────────────
|
| 376 |
-
fr_cost = policies["frontier"]["cost"] / max(policies["frontier"]["n"], 1)
|
| 377 |
-
|
| 378 |
-
print(f"\n{'='*70}")
|
| 379 |
-
print("BAAR-STYLE ROUTER RESULTS ON SWE-BENCH (500 tasks)")
|
| 380 |
-
print(f"{'='*70}")
|
| 381 |
-
print(f"\n{'Policy':<25} {'Success':>10} {'AvgCost':>10} {'CostRed':>10}")
|
| 382 |
-
print("-" * 55)
|
| 383 |
-
|
| 384 |
-
for name in ["oracle", "baar_feedback", "baar", "keyword_only", "frontier", "always_cheap"]:
|
| 385 |
-
r = policies[name]
|
| 386 |
-
sr = r["success"] / max(r["n"], 1)
|
| 387 |
-
ac = r["cost"] / max(r["n"], 1)
|
| 388 |
-
cr = (1 - ac / max(fr_cost, 0.0001)) * 100
|
| 389 |
-
print(f" {name:<23} {sr:>10.3f} {ac:>10.4f} {cr:>9.1f}%")
|
| 390 |
-
|
| 391 |
-
# Compare CV scores
|
| 392 |
-
print(f"\n{'='*70}")
|
| 393 |
-
print("CROSS-VALIDATION F1 COMPARISON")
|
| 394 |
-
print(f"{'='*70}")
|
| 395 |
-
print(f"\n{'Tier':<8} {'Keyword-only':>15} {'BAAR (BERT+KW)':>18} {'Improvement':>14}")
|
| 396 |
-
print("-" * 55)
|
| 397 |
-
for t in range(1, 6):
|
| 398 |
-
kw_cv = kw_tier_cv.get(t, 0)
|
| 399 |
-
baar_cv = tier_cv.get(t, 0)
|
| 400 |
-
imp = baar_cv - kw_cv
|
| 401 |
-
print(f" Tier {t:<4} {kw_cv:>15.3f} {baar_cv:>18.3f} {imp:>13.3f}")
|
| 402 |
-
|
| 403 |
-
# Save results
|
| 404 |
-
results = {
|
| 405 |
-
"router": "BAAR-style (BERT [CLS] + XGBoost)",
|
| 406 |
-
"n_tasks": len(traces),
|
| 407 |
-
"n_features": X.shape[1],
|
| 408 |
-
"n_bert_dims": BERT_DIM,
|
| 409 |
-
"n_keyword_feats": 23,
|
| 410 |
-
"policies": {
|
| 411 |
-
name: {
|
| 412 |
-
"success": r["success"],
|
| 413 |
-
"success_rate": round(r["success"] / max(r["n"], 1), 4),
|
| 414 |
-
"avg_cost": round(r["cost"] / max(r["n"], 1), 4),
|
| 415 |
-
"cost_reduction_pct": round(
|
| 416 |
-
(1 - (r["cost"] / max(r["n"], 1)) / max(fr_cost, 0.0001)) * 100, 1
|
| 417 |
-
),
|
| 418 |
-
}
|
| 419 |
-
for name, r in policies.items()
|
| 420 |
-
},
|
| 421 |
-
"cv_f1_comparison": {
|
| 422 |
-
"keyword_only": {str(k): v for k, v in kw_tier_cv.items()},
|
| 423 |
-
"baar_bert_kw": {str(k): v for k, v in tier_cv.items()},
|
| 424 |
-
},
|
| 425 |
-
"opt_tier_accuracy": float(opt_acc),
|
| 426 |
-
}
|
| 427 |
-
|
| 428 |
-
with open("/tmp/baar_results.json", "w") as f:
|
| 429 |
-
json.dump(results, f, indent=2)
|
| 430 |
-
|
| 431 |
-
# Upload to Hub
|
| 432 |
-
from huggingface_hub import HfApi
|
| 433 |
-
api = HfApi()
|
| 434 |
-
api.upload_file(
|
| 435 |
-
path_or_fileobj="/tmp/baar_results.json",
|
| 436 |
-
path_in_repo="eval/baar_results.json",
|
| 437 |
-
repo_id=REPO,
|
| 438 |
-
repo_type="model",
|
| 439 |
-
)
|
| 440 |
-
print(f"\n ✓ Results uploaded to Hub: eval/baar_results.json")
|
| 441 |
-
|
| 442 |
-
# Save model bundle
|
| 443 |
-
bundle = {
|
| 444 |
-
"tier_clfs": {str(k): v for k, v in tier_clfs.items()},
|
| 445 |
-
"tier_calibrators": {str(k): v for k, v in tier_calibs.items()},
|
| 446 |
-
"opt_clf": opt_clf,
|
| 447 |
-
"kw_tier_clfs": {str(k): v for k, v in kw_tier_clfs.items()},
|
| 448 |
-
"kw_tier_calibrators": {str(k): v for k, v in kw_tier_calibs.items()},
|
| 449 |
-
"feat_names": feat_names,
|
| 450 |
-
"bert_dim": BERT_DIM,
|
| 451 |
-
"n_keyword_feats": 23,
|
| 452 |
-
"version": "baar_v1",
|
| 453 |
-
"description": "BAAR-style router: BERT [CLS] embeddings + keyword features → XGBoost tier predictor",
|
| 454 |
-
"tier_cost": TIER_COST,
|
| 455 |
-
}
|
| 456 |
-
|
| 457 |
-
with open("/tmp/baar_bundle.pkl", "wb") as f:
|
| 458 |
-
pickle.dump(bundle, f)
|
| 459 |
-
|
| 460 |
-
api.upload_file(
|
| 461 |
-
path_or_fileobj="/tmp/baar_bundle.pkl",
|
| 462 |
-
path_in_repo="router_models/baar_bundle.pkl",
|
| 463 |
-
repo_id=REPO,
|
| 464 |
-
repo_type="model",
|
| 465 |
-
)
|
| 466 |
-
print(f" ✓ BAAR bundle uploaded: router_models/baar_bundle.pkl")
|
| 467 |
-
|
| 468 |
-
print(f"\n{'='*70}")
|
| 469 |
-
print("DONE! BAAR-style router built and evaluated.")
|
| 470 |
-
print("=" * 70)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/train_bert_5class.py
DELETED
|
@@ -1,187 +0,0 @@
|
|
| 1 |
-
"""Retrain BERT as a 5-class tier router.
|
| 2 |
-
|
| 3 |
-
Uses SPROUT data to predict optimal tier (1-5) directly.
|
| 4 |
-
"""
|
| 5 |
-
import os, json, random
|
| 6 |
-
import numpy as np
|
| 7 |
-
from datasets import load_dataset
|
| 8 |
-
from transformers import (
|
| 9 |
-
AutoTokenizer, AutoModelForSequenceClassification,
|
| 10 |
-
TrainingArguments, Trainer, DataCollatorWithPadding,
|
| 11 |
-
)
|
| 12 |
-
import torch
|
| 13 |
-
|
| 14 |
-
REPO = "narcolepticchicken/agent-cost-optimizer"
|
| 15 |
-
print("BERT 5-Class Tier Router Training")
|
| 16 |
-
print("="*60)
|
| 17 |
-
|
| 18 |
-
# ── Load SPROUT ──
|
| 19 |
-
print("\n[1] Loading SPROUT dataset...")
|
| 20 |
-
ds = load_dataset("CARROT-LLM-Routing/SPROUT", split="train", trust_remote_code=True)
|
| 21 |
-
print(f" Total rows: {len(ds)}")
|
| 22 |
-
print(f" Columns: {ds.column_names}")
|
| 23 |
-
|
| 24 |
-
# ── Model tier mapping ──
|
| 25 |
-
# SPROUT models → tiers (same as v11 training)
|
| 26 |
-
MODEL_TIER_MAP = {}
|
| 27 |
-
TIER_MODELS = {
|
| 28 |
-
1: ["gemma-2-2b-it","phi-3-mini-128k-instruct","qwen2.5-3b-instruct",
|
| 29 |
-
"llama-3.2-3b-instruct","deepseek-v3.2"],
|
| 30 |
-
2: ["gemma-2-9b-it","mistral-7b-instruct-v0.3","qwen2.5-7b-instruct",
|
| 31 |
-
"llama-3.1-8b-instruct","gpt-5-nano","gpt-5-mini"],
|
| 32 |
-
3: ["qwen2.5-32b-instruct","mixtral-8x7b-instruct-v0.1",
|
| 33 |
-
"gemma-2-27b-it","gemini-2.5-pro"],
|
| 34 |
-
4: ["claude-opus-4.7","gpt-5.2","llama-3.1-70b-instruct",
|
| 35 |
-
"qwen2.5-72b-instruct"],
|
| 36 |
-
5: ["gemini-3-pro","deepseek-v4-flash"],
|
| 37 |
-
}
|
| 38 |
-
for tier, models in TIER_MODELS.items():
|
| 39 |
-
for m in models:
|
| 40 |
-
MODEL_TIER_MAP[m.lower()] = tier
|
| 41 |
-
|
| 42 |
-
# ── Build training data ──
|
| 43 |
-
print("\n[2] Building training data...")
|
| 44 |
-
texts = []
|
| 45 |
-
labels = []
|
| 46 |
-
skipped = 0
|
| 47 |
-
|
| 48 |
-
for row in ds:
|
| 49 |
-
# Find the cheapest tier that succeeded
|
| 50 |
-
best_tier = 5 # default
|
| 51 |
-
found = False
|
| 52 |
-
|
| 53 |
-
# Try to find model results in the row
|
| 54 |
-
for tier in range(1, 6):
|
| 55 |
-
for m in TIER_MODELS.get(tier, []):
|
| 56 |
-
m_lower = m.lower()
|
| 57 |
-
# Check various possible column names
|
| 58 |
-
for col in ds.column_names:
|
| 59 |
-
if m_lower in col.lower():
|
| 60 |
-
val = row.get(col)
|
| 61 |
-
if isinstance(val, (int, float)) and val > 0:
|
| 62 |
-
best_tier = tier
|
| 63 |
-
found = True
|
| 64 |
-
break
|
| 65 |
-
if found:
|
| 66 |
-
break
|
| 67 |
-
if found:
|
| 68 |
-
break
|
| 69 |
-
|
| 70 |
-
# Get the prompt/question text
|
| 71 |
-
prompt = ""
|
| 72 |
-
for col in ["prompt", "question", "input", "query", "problem_statement", "instruction"]:
|
| 73 |
-
if col in ds.column_names:
|
| 74 |
-
prompt = str(row[col])
|
| 75 |
-
break
|
| 76 |
-
|
| 77 |
-
if not prompt:
|
| 78 |
-
# Try first string column
|
| 79 |
-
for col in ds.column_names:
|
| 80 |
-
if isinstance(row[col], str) and len(row[col]) > 20:
|
| 81 |
-
prompt = row[col]
|
| 82 |
-
break
|
| 83 |
-
|
| 84 |
-
if prompt and len(prompt) > 10:
|
| 85 |
-
texts.append(prompt[:2000]) # truncate long texts
|
| 86 |
-
labels.append(best_tier - 1) # 0-indexed for classification
|
| 87 |
-
else:
|
| 88 |
-
skipped += 1
|
| 89 |
-
|
| 90 |
-
print(f" Training samples: {len(texts)}")
|
| 91 |
-
print(f" Skipped: {skipped}")
|
| 92 |
-
print(f" Label distribution:")
|
| 93 |
-
from collections import Counter
|
| 94 |
-
label_dist = Counter(labels)
|
| 95 |
-
for label in sorted(label_dist):
|
| 96 |
-
print(f" Tier {label+1}: {label_dist[label]} ({label_dist[label]/len(labels)*100:.1f}%)")
|
| 97 |
-
|
| 98 |
-
# ── Tokenize ──
|
| 99 |
-
print("\n[3] Tokenizing...")
|
| 100 |
-
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
|
| 101 |
-
|
| 102 |
-
encodings = tokenizer(texts, truncation=True, max_length=512, padding=False)
|
| 103 |
-
|
| 104 |
-
class TierDataset(torch.utils.data.Dataset):
|
| 105 |
-
def __init__(self, encodings, labels):
|
| 106 |
-
self.encodings = encodings
|
| 107 |
-
self.labels = labels
|
| 108 |
-
def __getitem__(self, idx):
|
| 109 |
-
item = {k: torch.tensor(v[idx]) for k, v in self.encodings.items()}
|
| 110 |
-
item["labels"] = torch.tensor(self.labels[idx])
|
| 111 |
-
return item
|
| 112 |
-
def __len__(self):
|
| 113 |
-
return len(self.labels)
|
| 114 |
-
|
| 115 |
-
# Split
|
| 116 |
-
split = int(0.9 * len(texts))
|
| 117 |
-
train_ds = TierDataset(
|
| 118 |
-
{k: v[:split] for k, v in encodings.items()},
|
| 119 |
-
labels[:split],
|
| 120 |
-
)
|
| 121 |
-
eval_ds = TierDataset(
|
| 122 |
-
{k: v[split:] for k, v in encodings.items()},
|
| 123 |
-
labels[split:],
|
| 124 |
-
)
|
| 125 |
-
print(f" Train: {len(train_ds)}, Eval: {len(eval_ds)}")
|
| 126 |
-
|
| 127 |
-
# ── Train ──
|
| 128 |
-
print("\n[4] Training 5-class BERT router...")
|
| 129 |
-
model = AutoModelForSequenceClassification.from_pretrained(
|
| 130 |
-
"distilbert-base-uncased", num_labels=5
|
| 131 |
-
)
|
| 132 |
-
|
| 133 |
-
training_args = TrainingArguments(
|
| 134 |
-
output_dir="/tmp/bert_5class",
|
| 135 |
-
num_train_epochs=3,
|
| 136 |
-
per_device_train_batch_size=32,
|
| 137 |
-
per_device_eval_batch_size=64,
|
| 138 |
-
learning_rate=2e-5,
|
| 139 |
-
weight_decay=0.01,
|
| 140 |
-
eval_strategy="epoch",
|
| 141 |
-
save_strategy="epoch",
|
| 142 |
-
load_best_model_at_end=True,
|
| 143 |
-
metric_for_best_model="eval_accuracy",
|
| 144 |
-
logging_steps=50,
|
| 145 |
-
disable_tqdm=True,
|
| 146 |
-
)
|
| 147 |
-
|
| 148 |
-
def compute_metrics(eval_pred):
|
| 149 |
-
logits, labels = eval_pred
|
| 150 |
-
preds = np.argmax(logits, axis=-1)
|
| 151 |
-
acc = np.mean(preds == labels)
|
| 152 |
-
return {"accuracy": acc}
|
| 153 |
-
|
| 154 |
-
trainer = Trainer(
|
| 155 |
-
model=model,
|
| 156 |
-
args=training_args,
|
| 157 |
-
train_dataset=train_ds,
|
| 158 |
-
eval_dataset=eval_ds,
|
| 159 |
-
tokenizer=tokenizer,
|
| 160 |
-
data_collator=DataCollatorWithPadding(tokenizer),
|
| 161 |
-
compute_metrics=compute_metrics,
|
| 162 |
-
)
|
| 163 |
-
|
| 164 |
-
trainer.train()
|
| 165 |
-
|
| 166 |
-
# ── Save and upload ──
|
| 167 |
-
print("\n[5] Saving model...")
|
| 168 |
-
save_dir = "/tmp/bert_5class_final"
|
| 169 |
-
model.save_pretrained(save_dir)
|
| 170 |
-
tokenizer.save_pretrained(save_dir)
|
| 171 |
-
print(f" Saved to {save_dir}")
|
| 172 |
-
|
| 173 |
-
# Upload to Hub
|
| 174 |
-
from huggingface_hub import HfApi
|
| 175 |
-
api = HfApi()
|
| 176 |
-
for fname in os.listdir(save_dir):
|
| 177 |
-
fpath = os.path.join(save_dir, fname)
|
| 178 |
-
if os.path.isfile(fpath):
|
| 179 |
-
api.upload_file(
|
| 180 |
-
path_or_fileobj=fpath,
|
| 181 |
-
path_in_repo=f"router_models/bert_5class/{fname}",
|
| 182 |
-
repo_id=REPO,
|
| 183 |
-
repo_type="model",
|
| 184 |
-
)
|
| 185 |
-
print(f" Uploaded {fname}")
|
| 186 |
-
|
| 187 |
-
print("\nDONE! 5-class BERT router saved to router_models/bert_5class/")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/train_bert_5class_v2.py
DELETED
|
@@ -1,291 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""BERT 5-Class Tier Router — V2 with PROPER SPROUT parsing.
|
| 3 |
-
|
| 4 |
-
Fixes the V1 bug: SPROUT model columns are dicts like:
|
| 5 |
-
{'judge_response': '{"correctness_score": 0.8, "justification": "..."}'}
|
| 6 |
-
which need JSON parsing, not naive int/float checks.
|
| 7 |
-
|
| 8 |
-
Also writes a robust label extraction that maps SPROUT's 13 models → ACO's 5 cost tiers
|
| 9 |
-
and labels each task with the cheapest tier that achieves correctness_score >= THRESHOLD.
|
| 10 |
-
"""
|
| 11 |
-
import json, os, sys
|
| 12 |
-
import numpy as np
|
| 13 |
-
from collections import Counter
|
| 14 |
-
from datasets import load_dataset
|
| 15 |
-
import torch
|
| 16 |
-
from transformers import (
|
| 17 |
-
AutoTokenizer,
|
| 18 |
-
AutoModelForSequenceClassification,
|
| 19 |
-
TrainingArguments,
|
| 20 |
-
Trainer,
|
| 21 |
-
DataCollatorWithPadding,
|
| 22 |
-
)
|
| 23 |
-
from sklearn.model_selection import train_test_split
|
| 24 |
-
|
| 25 |
-
REPO = "narcolepticchicken/agent-cost-optimizer"
|
| 26 |
-
CORRECTNESS_THRESHOLD = 0.7 # correctness_score >= this = "succeeded"
|
| 27 |
-
MAX_SEQ_LEN = 512
|
| 28 |
-
BATCH_SIZE = 32
|
| 29 |
-
EPOCHS = 5
|
| 30 |
-
LR = 2e-5
|
| 31 |
-
|
| 32 |
-
print("="*70)
|
| 33 |
-
print("BERT 5-CLASS TIER ROUTER — V2 (Fixed SPROUT Parsing)")
|
| 34 |
-
print("="*70)
|
| 35 |
-
|
| 36 |
-
# ── 1. Map SPROUT models → ACO cost tiers ──────────────────────────
|
| 37 |
-
SPROUT_MODEL_TO_TIER = {
|
| 38 |
-
# Tier 1: smallest/cheapest
|
| 39 |
-
"wxai-granite-3-2b-instruct-8k-max-tokens": 1,
|
| 40 |
-
"wxai-llama-3-2-1b-instruct": 1,
|
| 41 |
-
"wxai-llama-3-2-3b-instruct": 1,
|
| 42 |
-
# Tier 2: small/medium
|
| 43 |
-
"wxai-llama-3-1-8b-instruct": 2,
|
| 44 |
-
"wxai-granite-3-8b-instruct-8k-max-tokens": 2,
|
| 45 |
-
"openai-gpt-4o-mini": 2,
|
| 46 |
-
# Tier 3: medium
|
| 47 |
-
"wxai-mixtral-8x7b-instruct-v01": 3,
|
| 48 |
-
"aws-titan-text-premier-v1": 3,
|
| 49 |
-
# Tier 4: strong
|
| 50 |
-
"openai-gpt-4o": 4,
|
| 51 |
-
"wxai-llama-3-1-70b-instruct": 4,
|
| 52 |
-
"wxai-llama-3-3-70b-instruct": 4,
|
| 53 |
-
"aws-claude-3-5-sonnet-v1": 4,
|
| 54 |
-
# Tier 5: strongest
|
| 55 |
-
"wxai-llama-3-405b-instruct": 5,
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
SPROUT_MODEL_COLS = list(SPROUT_MODEL_TO_TIER.keys())
|
| 59 |
-
print(f"\n[1] SPROUT model mapping: {len(SPROUT_MODEL_COLS)} models → 5 ACO tiers")
|
| 60 |
-
|
| 61 |
-
# ── 2. Load SPROUT & extract correctness scores ────────────────────
|
| 62 |
-
print("\n[2] Loading SPROUT dataset...")
|
| 63 |
-
ds = load_dataset("CARROT-LLM-Routing/SPROUT", split="train", trust_remote_code=True)
|
| 64 |
-
print(f" Total rows: {len(ds)}")
|
| 65 |
-
|
| 66 |
-
print("\n[3] Parsing judge_response JSON and extracting correctness_scores...")
|
| 67 |
-
|
| 68 |
-
texts = []
|
| 69 |
-
labels = []
|
| 70 |
-
parse_errors = 0
|
| 71 |
-
no_score = 0
|
| 72 |
-
all_failed = 0
|
| 73 |
-
label_counter = Counter()
|
| 74 |
-
|
| 75 |
-
# Track per-model success rates for sanity check
|
| 76 |
-
model_success_counts = Counter()
|
| 77 |
-
model_total_counts = Counter()
|
| 78 |
-
|
| 79 |
-
for idx, row in enumerate(ds):
|
| 80 |
-
prompt = str(row.get("prompt") or "")
|
| 81 |
-
if not prompt or len(prompt) < 10:
|
| 82 |
-
all_failed += 1
|
| 83 |
-
continue
|
| 84 |
-
|
| 85 |
-
# Find the cheapest tier where any model succeeded
|
| 86 |
-
best_tier = 5 # default to frontier
|
| 87 |
-
found_success = False
|
| 88 |
-
|
| 89 |
-
for model_col in SPROUT_MODEL_COLS:
|
| 90 |
-
tier = SPROUT_MODEL_TO_TIER[model_col]
|
| 91 |
-
|
| 92 |
-
if model_col not in row:
|
| 93 |
-
continue
|
| 94 |
-
|
| 95 |
-
val = row[model_col]
|
| 96 |
-
|
| 97 |
-
# Handle the nested dict structure
|
| 98 |
-
score = None
|
| 99 |
-
try:
|
| 100 |
-
if isinstance(val, dict):
|
| 101 |
-
# {'judge_response': '{"correctness_score": 0.8, ...}'}
|
| 102 |
-
jr = val.get("judge_response", "")
|
| 103 |
-
if isinstance(jr, str) and jr.strip():
|
| 104 |
-
jr_parsed = json.loads(jr)
|
| 105 |
-
score = jr_parsed.get("correctness_score", None)
|
| 106 |
-
elif isinstance(jr, dict):
|
| 107 |
-
# Already parsed JSON
|
| 108 |
-
score = jr.get("correctness_score", None)
|
| 109 |
-
elif isinstance(val, str):
|
| 110 |
-
# Direct JSON string
|
| 111 |
-
val_parsed = json.loads(val)
|
| 112 |
-
jr = val_parsed.get("judge_response", "")
|
| 113 |
-
if isinstance(jr, str) and jr.strip():
|
| 114 |
-
jr_parsed = json.loads(jr)
|
| 115 |
-
score = jr_parsed.get("correctness_score", None)
|
| 116 |
-
elif isinstance(jr, dict):
|
| 117 |
-
score = jr.get("correctness_score", None)
|
| 118 |
-
else:
|
| 119 |
-
score = val_parsed.get("correctness_score", None)
|
| 120 |
-
except (json.JSONDecodeError, AttributeError, TypeError) as e:
|
| 121 |
-
parse_errors += 1
|
| 122 |
-
continue
|
| 123 |
-
|
| 124 |
-
if score is None:
|
| 125 |
-
no_score += 1
|
| 126 |
-
continue
|
| 127 |
-
|
| 128 |
-
model_total_counts[model_col] += 1
|
| 129 |
-
succeeded = float(score) >= CORRECTNESS_THRESHOLD
|
| 130 |
-
if succeeded:
|
| 131 |
-
model_success_counts[model_col] += 1
|
| 132 |
-
if not found_success or tier < best_tier:
|
| 133 |
-
best_tier = tier
|
| 134 |
-
found_success = True
|
| 135 |
-
|
| 136 |
-
if not found_success:
|
| 137 |
-
all_failed += 1
|
| 138 |
-
best_tier = 5
|
| 139 |
-
|
| 140 |
-
texts.append(prompt[:2000])
|
| 141 |
-
labels.append(best_tier - 1) # 0-indexed
|
| 142 |
-
label_counter[best_tier] += 1
|
| 143 |
-
|
| 144 |
-
print(f"\n[3a] Label extraction complete:")
|
| 145 |
-
print(f" Valid samples: {len(texts)}")
|
| 146 |
-
print(f" All-failed: {all_failed}")
|
| 147 |
-
print(f" Parse errors: {parse_errors}")
|
| 148 |
-
print(f" Missing scores: {no_score}")
|
| 149 |
-
print(f" Label distribution (tier → 0-idx):")
|
| 150 |
-
for tier in range(1, 6):
|
| 151 |
-
c = label_counter.get(tier, 0)
|
| 152 |
-
print(f" Tier {tier}: {c} ({c/max(len(texts),1)*100:.1f}%)")
|
| 153 |
-
|
| 154 |
-
print(f"\n[3b] Per-model success rates (@ threshold {CORRECTNESS_THRESHOLD}):")
|
| 155 |
-
for model_col in SPROUT_MODEL_COLS:
|
| 156 |
-
total = model_total_counts[model_col]
|
| 157 |
-
if total > 0:
|
| 158 |
-
succ = model_success_counts[model_col]
|
| 159 |
-
print(f" {model_col:<50}: {succ}/{total} = {succ/total*100:.1f}%")
|
| 160 |
-
|
| 161 |
-
# ── 4. Tokenize ───────────────────────────────────────────────────
|
| 162 |
-
print(f"\n[4] Tokenizing {len(texts)} samples with distilbert-base-uncased...")
|
| 163 |
-
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
|
| 164 |
-
|
| 165 |
-
encodings = tokenizer(
|
| 166 |
-
texts, truncation=True, max_length=MAX_SEQ_LEN, padding=False
|
| 167 |
-
)
|
| 168 |
-
|
| 169 |
-
class TierDataset(torch.utils.data.Dataset):
|
| 170 |
-
def __init__(self, encodings, labels):
|
| 171 |
-
self.encodings = encodings
|
| 172 |
-
self.labels = labels
|
| 173 |
-
def __getitem__(self, idx):
|
| 174 |
-
item = {k: torch.tensor(v[idx]) for k, v in self.encodings.items()}
|
| 175 |
-
item["labels"] = torch.tensor(self.labels[idx])
|
| 176 |
-
return item
|
| 177 |
-
def __len__(self):
|
| 178 |
-
return len(self.labels)
|
| 179 |
-
|
| 180 |
-
# Split
|
| 181 |
-
indices = list(range(len(texts)))
|
| 182 |
-
train_idx, eval_idx = train_test_split(indices, test_size=0.1, random_state=42)
|
| 183 |
-
|
| 184 |
-
train_enc = {k: [v[i] for i in train_idx] for k, v in encodings.items()}
|
| 185 |
-
eval_enc = {k: [v[i] for i in eval_idx] for k, v in encodings.items()}
|
| 186 |
-
train_labels = [labels[i] for i in train_idx]
|
| 187 |
-
eval_labels = [labels[i] for i in eval_idx]
|
| 188 |
-
|
| 189 |
-
train_ds = TierDataset(train_enc, train_labels)
|
| 190 |
-
eval_ds = TierDataset(eval_enc, eval_labels)
|
| 191 |
-
print(f" Train: {len(train_ds)}, Eval: {len(eval_ds)}")
|
| 192 |
-
|
| 193 |
-
# ── 5. Train ──────────────────────────────────────────────────────
|
| 194 |
-
print("\n[5] Training 5-class DistilBERT router...")
|
| 195 |
-
|
| 196 |
-
model = AutoModelForSequenceClassification.from_pretrained(
|
| 197 |
-
"distilbert-base-uncased", num_labels=5
|
| 198 |
-
)
|
| 199 |
-
|
| 200 |
-
training_args = TrainingArguments(
|
| 201 |
-
output_dir="/tmp/bert_5class_v2",
|
| 202 |
-
num_train_epochs=EPOCHS,
|
| 203 |
-
per_device_train_batch_size=BATCH_SIZE,
|
| 204 |
-
per_device_eval_batch_size=BATCH_SIZE * 2,
|
| 205 |
-
learning_rate=LR,
|
| 206 |
-
weight_decay=0.01,
|
| 207 |
-
warmup_ratio=0.1,
|
| 208 |
-
eval_strategy="epoch",
|
| 209 |
-
save_strategy="epoch",
|
| 210 |
-
load_best_model_at_end=True,
|
| 211 |
-
metric_for_best_model="eval_accuracy",
|
| 212 |
-
logging_steps=100,
|
| 213 |
-
logging_first_step=True,
|
| 214 |
-
disable_tqdm=True,
|
| 215 |
-
report_to="none",
|
| 216 |
-
)
|
| 217 |
-
|
| 218 |
-
def compute_metrics(eval_pred):
|
| 219 |
-
logits, labels = eval_pred
|
| 220 |
-
preds = np.argmax(logits, axis=-1)
|
| 221 |
-
acc = np.mean(preds == labels).item()
|
| 222 |
-
|
| 223 |
-
# Per-class accuracy
|
| 224 |
-
per_class = {}
|
| 225 |
-
for c in range(5):
|
| 226 |
-
mask = labels == c
|
| 227 |
-
if mask.sum() > 0:
|
| 228 |
-
per_class[f"acc_tier{c+1}"] = np.mean(preds[mask] == labels[mask]).item()
|
| 229 |
-
|
| 230 |
-
off_by_one = np.mean(np.abs(preds - labels) <= 1).item()
|
| 231 |
-
|
| 232 |
-
return {"accuracy": acc, "off_by_one": off_by_one, **per_class}
|
| 233 |
-
|
| 234 |
-
trainer = Trainer(
|
| 235 |
-
model=model,
|
| 236 |
-
args=training_args,
|
| 237 |
-
train_dataset=train_ds,
|
| 238 |
-
eval_dataset=eval_ds,
|
| 239 |
-
tokenizer=tokenizer,
|
| 240 |
-
data_collator=DataCollatorWithPadding(tokenizer),
|
| 241 |
-
compute_metrics=compute_metrics,
|
| 242 |
-
)
|
| 243 |
-
|
| 244 |
-
result = trainer.train()
|
| 245 |
-
final_metrics = trainer.evaluate()
|
| 246 |
-
print(f"\n Final metrics: {final_metrics}")
|
| 247 |
-
|
| 248 |
-
# ── 6. Save and upload ────────────────────────────────────────────
|
| 249 |
-
print("\n[6] Saving model...")
|
| 250 |
-
save_dir = "/tmp/bert_5class_v2_final"
|
| 251 |
-
model.save_pretrained(save_dir)
|
| 252 |
-
tokenizer.save_pretrained(save_dir)
|
| 253 |
-
|
| 254 |
-
meta = {
|
| 255 |
-
"model": "distilbert-base-uncased",
|
| 256 |
-
"num_labels": 5,
|
| 257 |
-
"task": "5-class optimal tier prediction",
|
| 258 |
-
"training_dataset": "CARROT-LLM-Routing/SPROUT",
|
| 259 |
-
"n_training_samples": len(train_ds),
|
| 260 |
-
"correctness_threshold": CORRECTNESS_THRESHOLD,
|
| 261 |
-
"tier_mapping": SPROUT_MODEL_TO_TIER,
|
| 262 |
-
"max_seq_len": MAX_SEQ_LEN,
|
| 263 |
-
"epochs": EPOCHS,
|
| 264 |
-
"learning_rate": LR,
|
| 265 |
-
"final_metrics": {k: float(v) if isinstance(v, (np.floating, np.integer)) else v
|
| 266 |
-
for k, v in final_metrics.items()},
|
| 267 |
-
"label_distribution": dict(label_counter),
|
| 268 |
-
}
|
| 269 |
-
with open(os.path.join(save_dir, "metadata.json"), "w") as f:
|
| 270 |
-
json.dump(meta, f, indent=2)
|
| 271 |
-
print(f" Saved to {save_dir}")
|
| 272 |
-
|
| 273 |
-
# Upload
|
| 274 |
-
from huggingface_hub import HfApi
|
| 275 |
-
|
| 276 |
-
api = HfApi()
|
| 277 |
-
for fname in os.listdir(save_dir):
|
| 278 |
-
fpath = os.path.join(save_dir, fname)
|
| 279 |
-
if os.path.isfile(fpath):
|
| 280 |
-
api.upload_file(
|
| 281 |
-
path_or_fileobj=fpath,
|
| 282 |
-
path_in_repo=f"router_models/bert_5class/{fname}",
|
| 283 |
-
repo_id=REPO,
|
| 284 |
-
repo_type="model",
|
| 285 |
-
)
|
| 286 |
-
print(f" Uploaded {fname}")
|
| 287 |
-
|
| 288 |
-
print("\n" + "="*70)
|
| 289 |
-
print("DONE! BERT 5-class router saved to router_models/bert_5class/")
|
| 290 |
-
print(f"Repo: https://huggingface.co/{REPO}")
|
| 291 |
-
print("="*70)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/train_bert_5class_v3.py
DELETED
|
@@ -1,292 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""BERT 5-Class Tier Router — V3 with ROBUST SPROUT parsing.
|
| 3 |
-
|
| 4 |
-
Fixes V2 issues:
|
| 5 |
-
1. Handle escaped single-quotes in judge_response JSON (Model\'s → Model's)
|
| 6 |
-
2. Add accelerate dependency
|
| 7 |
-
3. More robust fallback parsing for edge cases
|
| 8 |
-
"""
|
| 9 |
-
import json, os, sys, re
|
| 10 |
-
import numpy as np
|
| 11 |
-
from collections import Counter
|
| 12 |
-
from datasets import load_dataset
|
| 13 |
-
import torch
|
| 14 |
-
from transformers import (
|
| 15 |
-
AutoTokenizer,
|
| 16 |
-
AutoModelForSequenceClassification,
|
| 17 |
-
TrainingArguments,
|
| 18 |
-
Trainer,
|
| 19 |
-
DataCollatorWithPadding,
|
| 20 |
-
)
|
| 21 |
-
from sklearn.model_selection import train_test_split
|
| 22 |
-
|
| 23 |
-
REPO = "narcolepticchicken/agent-cost-optimizer"
|
| 24 |
-
CORRECTNESS_THRESHOLD = 0.7
|
| 25 |
-
MAX_SEQ_LEN = 512
|
| 26 |
-
BATCH_SIZE = 32
|
| 27 |
-
EPOCHS = 5
|
| 28 |
-
LR = 2e-5
|
| 29 |
-
|
| 30 |
-
print("="*70)
|
| 31 |
-
print("BERT 5-CLASS TIER ROUTER — V3 (Robust SPROUT Parsing)")
|
| 32 |
-
print("="*70)
|
| 33 |
-
|
| 34 |
-
# ── 1. Map SPROUT models → ACO cost tiers ──────────────────────────
|
| 35 |
-
SPROUT_MODEL_TO_TIER = {
|
| 36 |
-
"wxai-granite-3-2b-instruct-8k-max-tokens": 1,
|
| 37 |
-
"wxai-llama-3-2-1b-instruct": 1,
|
| 38 |
-
"wxai-llama-3-2-3b-instruct": 1,
|
| 39 |
-
"wxai-llama-3-1-8b-instruct": 2,
|
| 40 |
-
"wxai-granite-3-8b-instruct-8k-max-tokens": 2,
|
| 41 |
-
"openai-gpt-4o-mini": 2,
|
| 42 |
-
"wxai-mixtral-8x7b-instruct-v01": 3,
|
| 43 |
-
"aws-titan-text-premier-v1": 3,
|
| 44 |
-
"openai-gpt-4o": 4,
|
| 45 |
-
"wxai-llama-3-1-70b-instruct": 4,
|
| 46 |
-
"wxai-llama-3-3-70b-instruct": 4,
|
| 47 |
-
"aws-claude-3-5-sonnet-v1": 4,
|
| 48 |
-
"wxai-llama-3-405b-instruct": 5,
|
| 49 |
-
}
|
| 50 |
-
SPROUT_MODEL_COLS = list(SPROUT_MODEL_TO_TIER.keys())
|
| 51 |
-
|
| 52 |
-
print(f"\n[1] SPROUT model mapping: {len(SPROUT_MODEL_COLS)} models → 5 ACO tiers")
|
| 53 |
-
|
| 54 |
-
# ── 2. Robust JSON parsing helper ────────────────────────────────
|
| 55 |
-
def extract_correctness_score(val):
|
| 56 |
-
"""Robustly extract correctness_score from SPROUT's nested dict/JSON structure."""
|
| 57 |
-
if val is None:
|
| 58 |
-
return None
|
| 59 |
-
|
| 60 |
-
judge_raw = None
|
| 61 |
-
if isinstance(val, dict):
|
| 62 |
-
judge_raw = val.get("judge_response")
|
| 63 |
-
elif isinstance(val, str):
|
| 64 |
-
try:
|
| 65 |
-
parsed = json.loads(val)
|
| 66 |
-
if isinstance(parsed, dict):
|
| 67 |
-
judge_raw = parsed.get("judge_response")
|
| 68 |
-
else:
|
| 69 |
-
return None
|
| 70 |
-
except json.JSONDecodeError:
|
| 71 |
-
return None
|
| 72 |
-
else:
|
| 73 |
-
return None
|
| 74 |
-
|
| 75 |
-
if judge_raw is None:
|
| 76 |
-
return None
|
| 77 |
-
if isinstance(judge_raw, (int, float)):
|
| 78 |
-
return float(judge_raw)
|
| 79 |
-
if isinstance(judge_raw, dict):
|
| 80 |
-
score = judge_raw.get("correctness_score")
|
| 81 |
-
return float(score) if score is not None else None
|
| 82 |
-
if not isinstance(judge_raw, str) or not judge_raw.strip():
|
| 83 |
-
return None
|
| 84 |
-
|
| 85 |
-
# Normal JSON parse
|
| 86 |
-
try:
|
| 87 |
-
parsed = json.loads(judge_raw)
|
| 88 |
-
score = parsed.get("correctness_score")
|
| 89 |
-
return float(score) if score is not None else None
|
| 90 |
-
except json.JSONDecodeError:
|
| 91 |
-
pass
|
| 92 |
-
|
| 93 |
-
# Fix common escape issues
|
| 94 |
-
cleaned = judge_raw.replace("\\'", "'")
|
| 95 |
-
try:
|
| 96 |
-
parsed = json.loads(cleaned)
|
| 97 |
-
score = parsed.get("correctness_score")
|
| 98 |
-
return float(score) if score is not None else None
|
| 99 |
-
except json.JSONDecodeError:
|
| 100 |
-
pass
|
| 101 |
-
|
| 102 |
-
# Regex fallback
|
| 103 |
-
m = re.search(r'"correctness_score"\s*:\s*([\d.]+)', judge_raw)
|
| 104 |
-
if m:
|
| 105 |
-
try:
|
| 106 |
-
return float(m.group(1))
|
| 107 |
-
except ValueError:
|
| 108 |
-
pass
|
| 109 |
-
return None
|
| 110 |
-
|
| 111 |
-
# ── 3. Load SPROUT & extract labels ──────────────────────────────
|
| 112 |
-
print("\n[2] Loading SPROUT dataset...")
|
| 113 |
-
ds = load_dataset("CARROT-LLM-Routing/SPROUT", split="train")
|
| 114 |
-
print(f" Total rows: {len(ds)}")
|
| 115 |
-
|
| 116 |
-
print("\n[3] Extracting labels with robust parsing...")
|
| 117 |
-
|
| 118 |
-
texts = []
|
| 119 |
-
labels = []
|
| 120 |
-
parse_errors = 0
|
| 121 |
-
all_failed = 0
|
| 122 |
-
label_counter = Counter()
|
| 123 |
-
model_success_counts = Counter()
|
| 124 |
-
model_total_counts = Counter()
|
| 125 |
-
|
| 126 |
-
for idx, row in enumerate(ds):
|
| 127 |
-
prompt = str(row.get("prompt") or "")
|
| 128 |
-
if not prompt or len(prompt) < 10:
|
| 129 |
-
all_failed += 1
|
| 130 |
-
continue
|
| 131 |
-
|
| 132 |
-
best_tier = 5
|
| 133 |
-
found_success = False
|
| 134 |
-
|
| 135 |
-
for model_col in SPROUT_MODEL_COLS:
|
| 136 |
-
tier = SPROUT_MODEL_TO_TIER[model_col]
|
| 137 |
-
score = extract_correctness_score(row.get(model_col))
|
| 138 |
-
|
| 139 |
-
if score is None:
|
| 140 |
-
parse_errors += 1
|
| 141 |
-
continue
|
| 142 |
-
|
| 143 |
-
model_total_counts[model_col] += 1
|
| 144 |
-
if score >= CORRECTNESS_THRESHOLD:
|
| 145 |
-
model_success_counts[model_col] += 1
|
| 146 |
-
if not found_success or tier < best_tier:
|
| 147 |
-
best_tier = tier
|
| 148 |
-
found_success = True
|
| 149 |
-
|
| 150 |
-
if not found_success:
|
| 151 |
-
all_failed += 1
|
| 152 |
-
best_tier = 5
|
| 153 |
-
|
| 154 |
-
texts.append(prompt[:2000])
|
| 155 |
-
labels.append(best_tier - 1)
|
| 156 |
-
label_counter[best_tier] += 1
|
| 157 |
-
|
| 158 |
-
print(f"\n[3a] Label extraction complete:")
|
| 159 |
-
print(f" Valid samples: {len(texts)}")
|
| 160 |
-
print(f" All-failed: {all_failed}")
|
| 161 |
-
print(f" Parse errors: {parse_errors}")
|
| 162 |
-
print(f" Label distribution:")
|
| 163 |
-
for tier in range(1, 6):
|
| 164 |
-
c = label_counter.get(tier, 0)
|
| 165 |
-
pct = c / max(len(texts), 1) * 100
|
| 166 |
-
print(f" Tier {tier}: {c} ({pct:.1f}%)")
|
| 167 |
-
|
| 168 |
-
print(f"\n[3b] Per-model success rates (@ threshold {CORRECTNESS_THRESHOLD}):")
|
| 169 |
-
for model_col in SPROUT_MODEL_COLS:
|
| 170 |
-
total = model_total_counts[model_col]
|
| 171 |
-
if total > 0:
|
| 172 |
-
succ = model_success_counts[model_col]
|
| 173 |
-
print(f" {model_col:<50}: {succ}/{total} = {succ/total*100:.1f}%")
|
| 174 |
-
|
| 175 |
-
# ── 4. Tokenize ───────────────────────────────────────────────────
|
| 176 |
-
print(f"\n[4] Tokenizing {len(texts)} samples...")
|
| 177 |
-
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
|
| 178 |
-
encodings = tokenizer(texts, truncation=True, max_length=MAX_SEQ_LEN, padding=False)
|
| 179 |
-
|
| 180 |
-
class TierDataset(torch.utils.data.Dataset):
|
| 181 |
-
def __init__(self, encodings, labels):
|
| 182 |
-
self.encodings = encodings
|
| 183 |
-
self.labels = labels
|
| 184 |
-
def __getitem__(self, idx):
|
| 185 |
-
item = {k: torch.tensor(v[idx]) for k, v in self.encodings.items()}
|
| 186 |
-
item["labels"] = torch.tensor(self.labels[idx])
|
| 187 |
-
return item
|
| 188 |
-
def __len__(self):
|
| 189 |
-
return len(self.labels)
|
| 190 |
-
|
| 191 |
-
indices = list(range(len(texts)))
|
| 192 |
-
train_idx, eval_idx = train_test_split(indices, test_size=0.1, random_state=42)
|
| 193 |
-
train_enc = {k: [v[i] for i in train_idx] for k, v in encodings.items()}
|
| 194 |
-
eval_enc = {k: [v[i] for i in eval_idx] for k, v in encodings.items()}
|
| 195 |
-
|
| 196 |
-
train_ds = TierDataset(train_enc, [labels[i] for i in train_idx])
|
| 197 |
-
eval_ds = TierDataset(eval_enc, [labels[i] for i in eval_idx])
|
| 198 |
-
print(f" Train: {len(train_ds)}, Eval: {len(eval_ds)}")
|
| 199 |
-
|
| 200 |
-
# ── 5. Train ──────────────────────────────────────────────────────
|
| 201 |
-
print("\n[5] Training 5-class DistilBERT router...")
|
| 202 |
-
|
| 203 |
-
model = AutoModelForSequenceClassification.from_pretrained(
|
| 204 |
-
"distilbert-base-uncased", num_labels=5
|
| 205 |
-
)
|
| 206 |
-
|
| 207 |
-
training_args = TrainingArguments(
|
| 208 |
-
output_dir="/tmp/bert_5class_v3",
|
| 209 |
-
num_train_epochs=EPOCHS,
|
| 210 |
-
per_device_train_batch_size=BATCH_SIZE,
|
| 211 |
-
per_device_eval_batch_size=BATCH_SIZE * 2,
|
| 212 |
-
learning_rate=LR,
|
| 213 |
-
weight_decay=0.01,
|
| 214 |
-
warmup_steps=500,
|
| 215 |
-
eval_strategy="epoch",
|
| 216 |
-
save_strategy="epoch",
|
| 217 |
-
load_best_model_at_end=True,
|
| 218 |
-
metric_for_best_model="eval_accuracy",
|
| 219 |
-
logging_steps=100,
|
| 220 |
-
logging_first_step=True,
|
| 221 |
-
disable_tqdm=True,
|
| 222 |
-
report_to="none",
|
| 223 |
-
)
|
| 224 |
-
|
| 225 |
-
def compute_metrics(eval_pred):
|
| 226 |
-
logits, labels = eval_pred
|
| 227 |
-
preds = np.argmax(logits, axis=-1)
|
| 228 |
-
acc = np.mean(preds == labels).item()
|
| 229 |
-
per_class = {}
|
| 230 |
-
for c in range(5):
|
| 231 |
-
mask = labels == c
|
| 232 |
-
if mask.sum() > 0:
|
| 233 |
-
per_class[f"acc_tier{c+1}"] = np.mean(preds[mask] == labels[mask]).item()
|
| 234 |
-
off_by_one = np.mean(np.abs(preds - labels) <= 1).item()
|
| 235 |
-
return {"accuracy": acc, "off_by_one": off_by_one, **per_class}
|
| 236 |
-
|
| 237 |
-
trainer = Trainer(
|
| 238 |
-
model=model,
|
| 239 |
-
args=training_args,
|
| 240 |
-
train_dataset=train_ds,
|
| 241 |
-
eval_dataset=eval_ds,
|
| 242 |
-
processing_class=tokenizer,
|
| 243 |
-
data_collator=DataCollatorWithPadding(tokenizer),
|
| 244 |
-
compute_metrics=compute_metrics,
|
| 245 |
-
)
|
| 246 |
-
|
| 247 |
-
result = trainer.train()
|
| 248 |
-
final_metrics = trainer.evaluate()
|
| 249 |
-
print(f"\n Final metrics: {final_metrics}")
|
| 250 |
-
|
| 251 |
-
# ── 6. Save and upload ────────────────────────────────────────────
|
| 252 |
-
print("\n[6] Saving model...")
|
| 253 |
-
save_dir = "/tmp/bert_5class_v3_final"
|
| 254 |
-
model.save_pretrained(save_dir)
|
| 255 |
-
tokenizer.save_pretrained(save_dir)
|
| 256 |
-
|
| 257 |
-
meta = {
|
| 258 |
-
"model": "distilbert-base-uncased",
|
| 259 |
-
"num_labels": 5,
|
| 260 |
-
"task": "5-class optimal tier prediction",
|
| 261 |
-
"training_dataset": "CARROT-LLM-Routing/SPROUT",
|
| 262 |
-
"n_training_samples": len(train_ds),
|
| 263 |
-
"correctness_threshold": CORRECTNESS_THRESHOLD,
|
| 264 |
-
"tier_mapping": SPROUT_MODEL_TO_TIER,
|
| 265 |
-
"max_seq_len": MAX_SEQ_LEN,
|
| 266 |
-
"epochs": EPOCHS,
|
| 267 |
-
"learning_rate": LR,
|
| 268 |
-
"final_metrics": {k: float(v) if isinstance(v, (np.floating, np.integer)) else v
|
| 269 |
-
for k, v in final_metrics.items()},
|
| 270 |
-
"label_distribution": dict(label_counter),
|
| 271 |
-
}
|
| 272 |
-
with open(os.path.join(save_dir, "metadata.json"), "w") as f:
|
| 273 |
-
json.dump(meta, f, indent=2)
|
| 274 |
-
print(f" Saved to {save_dir}")
|
| 275 |
-
|
| 276 |
-
from huggingface_hub import HfApi
|
| 277 |
-
api = HfApi()
|
| 278 |
-
for fname in os.listdir(save_dir):
|
| 279 |
-
fpath = os.path.join(save_dir, fname)
|
| 280 |
-
if os.path.isfile(fpath):
|
| 281 |
-
api.upload_file(
|
| 282 |
-
path_or_fileobj=fpath,
|
| 283 |
-
path_in_repo=f"router_models/bert_5class/{fname}",
|
| 284 |
-
repo_id=REPO,
|
| 285 |
-
repo_type="model",
|
| 286 |
-
)
|
| 287 |
-
print(f" Uploaded {fname}")
|
| 288 |
-
|
| 289 |
-
print("\n" + "="*70)
|
| 290 |
-
print("DONE! BERT 5-class router saved to router_models/bert_5class/")
|
| 291 |
-
print(f"Repo: https://huggingface.co/{REPO}")
|
| 292 |
-
print("="*70)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/train_router_full.py
DELETED
|
@@ -1,433 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Train a learned model router for Agent Cost Optimizer."""
|
| 3 |
-
import json, os, sys, random, pickle, uuid
|
| 4 |
-
import numpy as np
|
| 5 |
-
from datetime import datetime
|
| 6 |
-
from collections import defaultdict
|
| 7 |
-
from typing import Dict, List, Tuple, Any, Optional
|
| 8 |
-
|
| 9 |
-
TASK_TYPES = ["quick_answer","coding","research","document_drafting",
|
| 10 |
-
"legal_regulated","tool_heavy","retrieval_heavy",
|
| 11 |
-
"long_horizon","unknown_ambiguous"]
|
| 12 |
-
TT2IDX = {t:i for i,t in enumerate(TASK_TYPES)}
|
| 13 |
-
|
| 14 |
-
CODE_KW = ["python","javascript","code","function","bug","debug","refactor",
|
| 15 |
-
"implement","test","compile","runtime","class","module","async","thread"]
|
| 16 |
-
LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
|
| 17 |
-
RESEARCH_KW = ["research","find sources","literature","investigate","compare","analyze","survey"]
|
| 18 |
-
TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
|
| 19 |
-
LONG_KW = ["plan","project","roadmap","orchestrate","multi-step","migrate","pipeline","deploy"]
|
| 20 |
-
MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
|
| 21 |
-
|
| 22 |
-
TIER_STR = {1:0.35,2:0.55,3:0.80,4:0.93,5:0.97}
|
| 23 |
-
TIER_COST = {1:0.05,2:0.15,3:0.75,4:1.0,5:1.5}
|
| 24 |
-
|
| 25 |
-
TASK_TEMPLATES = {
|
| 26 |
-
"quick_answer":["What is the capital of France?","Explain quantum computing briefly.",
|
| 27 |
-
"What is 237*452?","Define photosynthesis.","Who wrote Hamlet?",
|
| 28 |
-
"What is the speed of light?","List the primary colors.","What is GDP?"],
|
| 29 |
-
"coding":["Write a Python function to reverse a linked list.",
|
| 30 |
-
"Fix the bug in this React component.","Refactor auth module to JWT.",
|
| 31 |
-
"Implement LRU cache in Go.","Debug segfault in C++ thread pool.",
|
| 32 |
-
"Add unit tests for the payment module.","Optimize this SQL query.",
|
| 33 |
-
"Create a REST API for user management.","Implement binary search in Rust."],
|
| 34 |
-
"research":["Research latest transformer advances.",
|
| 35 |
-
"Find sources comparing LoRA and full FT.",
|
| 36 |
-
"Investigate data center climate impact.",
|
| 37 |
-
"Survey privacy-preserving ML techniques.",
|
| 38 |
-
"Compare reinforcement learning algorithms for robotics."],
|
| 39 |
-
"document_drafting":["Draft project proposal for ML pipeline.",
|
| 40 |
-
"Write email to team about deployment.","Create technical report on performance."],
|
| 41 |
-
"legal_regulated":["Review this contract for liability clauses.",
|
| 42 |
-
"Check GDPR compliance for data pipeline.","Draft privacy policy section.",
|
| 43 |
-
"Verify regulatory compliance for medical device software."],
|
| 44 |
-
"tool_heavy":["Search open issues and create summary.",
|
| 45 |
-
"Fetch API docs and generate client code.","Query Q3 sales and produce chart."],
|
| 46 |
-
"retrieval_heavy":["Answer based on 50-page document.",
|
| 47 |
-
"Find all payment processing mentions.","Retrieve relevant cases for legal query."],
|
| 48 |
-
"long_horizon":["Plan 3-month roadmap.","Orchestrate multi-region deployment.",
|
| 49 |
-
"Redesign data architecture end-to-end.","Migrate monolith to microservices."],
|
| 50 |
-
"unknown_ambiguous":["Help me with this thing.",
|
| 51 |
-
"I need something about the server.","Can you look into that issue?"],
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
def tsp(tier, diff):
|
| 55 |
-
return TIER_STR[tier] ** (diff * 0.6)
|
| 56 |
-
|
| 57 |
-
def extract_features(request, task_type, difficulty=3):
|
| 58 |
-
r = request.lower()
|
| 59 |
-
f = {
|
| 60 |
-
"req_len": len(request),
|
| 61 |
-
"num_words": len(request.split()),
|
| 62 |
-
"has_code": int(any(k in r for k in CODE_KW)),
|
| 63 |
-
"n_code": sum(1 for k in CODE_KW if k in r),
|
| 64 |
-
"has_legal": int(any(k in r for k in LEGAL_KW)),
|
| 65 |
-
"n_legal": sum(1 for k in LEGAL_KW if k in r),
|
| 66 |
-
"has_research": int(any(k in r for k in RESEARCH_KW)),
|
| 67 |
-
"n_research": sum(1 for k in RESEARCH_KW if k in r),
|
| 68 |
-
"has_tool": int(any(k in r for k in TOOL_KW)),
|
| 69 |
-
"n_tool": sum(1 for k in TOOL_KW if k in r),
|
| 70 |
-
"has_long": int(any(k in r for k in LONG_KW)),
|
| 71 |
-
"has_math": int(any(k in r for k in MATH_KW)),
|
| 72 |
-
"tt_idx": TT2IDX.get(task_type, 8),
|
| 73 |
-
"difficulty": difficulty,
|
| 74 |
-
}
|
| 75 |
-
for tt in TASK_TYPES:
|
| 76 |
-
f[f"tt_{tt}"] = int(task_type == tt)
|
| 77 |
-
return f
|
| 78 |
-
|
| 79 |
-
def gen_trace(idx, rng):
|
| 80 |
-
tt = rng.choice(list(TASK_TEMPLATES.keys()))
|
| 81 |
-
diff = {"quick_answer":1,"document_drafting":2,"tool_heavy":2,"retrieval_heavy":2,
|
| 82 |
-
"research":3,"coding":3,"unknown_ambiguous":3,"long_horizon":4,"legal_regulated":5}[tt]
|
| 83 |
-
tier_out = {}
|
| 84 |
-
for t in range(1,6):
|
| 85 |
-
tier_out[t] = rng.random() < tsp(t, diff)
|
| 86 |
-
opt = 5
|
| 87 |
-
for t in range(1,6):
|
| 88 |
-
if tier_out[t]:
|
| 89 |
-
opt = t
|
| 90 |
-
break
|
| 91 |
-
if diff <= 2:
|
| 92 |
-
actual = rng.choices([1,2,3,4,5],weights=[3,4,2,1,0.5])[0]
|
| 93 |
-
elif diff == 3:
|
| 94 |
-
actual = rng.choices([1,2,3,4,5],weights=[1,2,4,2,1])[0]
|
| 95 |
-
elif diff == 4:
|
| 96 |
-
actual = rng.choices([1,2,3,4,5],weights=[0.5,1,2,4,2])[0]
|
| 97 |
-
else:
|
| 98 |
-
actual = rng.choices([1,2,3,4,5],weights=[0.2,0.5,1,3,4])[0]
|
| 99 |
-
outcome = "success" if tier_out[actual] else "failure"
|
| 100 |
-
req = rng.choice(TASK_TEMPLATES[tt])
|
| 101 |
-
feats = extract_features(req, tt, diff)
|
| 102 |
-
return {"feats":feats,"opt":opt,"actual":actual,"outcome":outcome,
|
| 103 |
-
"tier_out":tier_out,"tt":tt,"diff":diff,"req":req}
|
| 104 |
-
|
| 105 |
-
print("="*80)
|
| 106 |
-
print("AGENT COST OPTIMIZER - TRAINED ROUTER TRAINING")
|
| 107 |
-
print("="*80)
|
| 108 |
-
|
| 109 |
-
# ─── Generate Training Data ────────────────────────────────────────
|
| 110 |
-
print("\n[1] Generating 50K training traces...")
|
| 111 |
-
rng = random.Random(42)
|
| 112 |
-
traces = [gen_trace(i, rng) for i in range(50000)]
|
| 113 |
-
print(f" Generated {len(traces)} traces")
|
| 114 |
-
|
| 115 |
-
opt_dist = defaultdict(int)
|
| 116 |
-
for t in traces:
|
| 117 |
-
opt_dist[t["opt"]] += 1
|
| 118 |
-
for k in sorted(opt_dist):
|
| 119 |
-
print(f" opt_tier={k}: {opt_dist[k]} ({opt_dist[k]/len(traces)*100:.1f}%)")
|
| 120 |
-
|
| 121 |
-
# ─── Build Feature Matrix ──────────────────────────────────────────
|
| 122 |
-
print("\n[2] Building feature matrix...")
|
| 123 |
-
|
| 124 |
-
def feats_to_vec(feats):
|
| 125 |
-
"""Convert feature dict to fixed-order numpy array."""
|
| 126 |
-
keys = sorted(feats.keys())
|
| 127 |
-
return np.array([float(feats[k]) for k in keys], dtype=np.float32)
|
| 128 |
-
|
| 129 |
-
# Get feature key order from first trace
|
| 130 |
-
FEAT_KEYS = sorted(traces[0]["feats"].keys())
|
| 131 |
-
NUM_FEATURES = len(FEAT_KEYS)
|
| 132 |
-
print(f" Feature count: {NUM_FEATURES}")
|
| 133 |
-
print(f" Features: {FEAT_KEYS}")
|
| 134 |
-
|
| 135 |
-
def feats_to_vec_safe(feats):
|
| 136 |
-
return np.array([float(feats.get(k, 0.0)) for k in FEAT_KEYS], dtype=np.float32)
|
| 137 |
-
|
| 138 |
-
X_all = np.array([feats_to_vec_safe(t["feats"]) for t in traces])
|
| 139 |
-
y_opt = np.array([t["opt"] for t in traces])
|
| 140 |
-
y_actual = np.array([t["actual"] for t in traces])
|
| 141 |
-
y_outcome = np.array([1 if t["outcome"]=="success" else 0 for t in traces])
|
| 142 |
-
|
| 143 |
-
# ─── Per-Tier Success Classifiers ───────────────────────────────────
|
| 144 |
-
print("\n[3] Training per-tier P(success|query) classifiers...")
|
| 145 |
-
from sklearn.model_selection import train_test_split
|
| 146 |
-
from sklearn.metrics import classification_report, accuracy_score, f1_score
|
| 147 |
-
|
| 148 |
-
# For each tier, create binary label: did this tier succeed?
|
| 149 |
-
per_tier_labels = {}
|
| 150 |
-
for tier in range(1, 6):
|
| 151 |
-
labels = []
|
| 152 |
-
for t in traces:
|
| 153 |
-
labels.append(1 if t["tier_out"].get(tier, False) else 0)
|
| 154 |
-
per_tier_labels[tier] = np.array(labels)
|
| 155 |
-
succ_rate = per_tier_labels[tier].mean()
|
| 156 |
-
print(f" Tier {tier}: success rate = {succ_rate:.3f}")
|
| 157 |
-
|
| 158 |
-
# Split train/test
|
| 159 |
-
X_train, X_test, idx_train, idx_test = train_test_split(
|
| 160 |
-
X_all, range(len(traces)), test_size=0.2, random_state=42, stratify=y_opt
|
| 161 |
-
)
|
| 162 |
-
|
| 163 |
-
print(f"\n Train: {len(X_train)}, Test: {len(X_test)}")
|
| 164 |
-
|
| 165 |
-
# ─── XGBoost Per-Tier Classifiers ───────────────────────────────────
|
| 166 |
-
print("\n[4] Training XGBoost per-tier classifiers...")
|
| 167 |
-
import xgboost as xgb
|
| 168 |
-
|
| 169 |
-
tier_classifiers = {}
|
| 170 |
-
for tier in range(1, 6):
|
| 171 |
-
y_train_tier = per_tier_labels[tier][idx_train]
|
| 172 |
-
y_test_tier = per_tier_labels[tier][idx_test]
|
| 173 |
-
|
| 174 |
-
clf = xgb.XGBClassifier(
|
| 175 |
-
n_estimators=100,
|
| 176 |
-
max_depth=4,
|
| 177 |
-
learning_rate=0.1,
|
| 178 |
-
subsample=0.8,
|
| 179 |
-
colsample_bytree=0.8,
|
| 180 |
-
objective="binary:logistic",
|
| 181 |
-
eval_metric="logloss",
|
| 182 |
-
random_state=42,
|
| 183 |
-
verbosity=0,
|
| 184 |
-
)
|
| 185 |
-
clf.fit(X_train, y_train_tier)
|
| 186 |
-
|
| 187 |
-
y_pred = clf.predict(X_test)
|
| 188 |
-
y_prob = clf.predict_proba(X_test)[:, 1]
|
| 189 |
-
|
| 190 |
-
acc = accuracy_score(y_test_tier, y_pred)
|
| 191 |
-
f1 = f1_score(y_test_tier, y_pred, zero_division=0)
|
| 192 |
-
|
| 193 |
-
tier_classifiers[tier] = clf
|
| 194 |
-
print(f" Tier {tier}: accuracy={acc:.3f}, f1={f1:.3f}")
|
| 195 |
-
|
| 196 |
-
# ─── CARROT-Style Router Decision ────────────────────────────────────
|
| 197 |
-
print("\n[5] Building CARROT-style router...")
|
| 198 |
-
|
| 199 |
-
def route_carrot(features_vec, tier_clfs, mu=0.7):
|
| 200 |
-
"""Route to argmin_tier [mu*(1-P_success_tier) + (1-mu)*cost_tier].
|
| 201 |
-
|
| 202 |
-
mu controls quality-vs-cost tradeoff:
|
| 203 |
-
mu=1.0: maximize quality only (always frontier)
|
| 204 |
-
mu=0.0: minimize cost only (always cheapest)
|
| 205 |
-
mu=0.7: 70% quality, 30% cost (our default)
|
| 206 |
-
"""
|
| 207 |
-
if features_vec.ndim == 1:
|
| 208 |
-
features_vec = features_vec.reshape(1, -1)
|
| 209 |
-
|
| 210 |
-
best_tier = 3
|
| 211 |
-
best_score = float("inf")
|
| 212 |
-
|
| 213 |
-
for tier in range(1, 6):
|
| 214 |
-
p_success = tier_clfs[tier].predict_proba(features_vec)[0, 1]
|
| 215 |
-
cost_norm = TIER_COST[tier] / TIER_COST[5] # normalize to [0,1]
|
| 216 |
-
score = mu * (1.0 - p_success) + (1.0 - mu) * cost_norm
|
| 217 |
-
if score < best_score:
|
| 218 |
-
best_score = score
|
| 219 |
-
best_tier = tier
|
| 220 |
-
|
| 221 |
-
return best_tier
|
| 222 |
-
|
| 223 |
-
# Evaluate on test set
|
| 224 |
-
print("\n[6] Evaluating CARROT router on test set...")
|
| 225 |
-
|
| 226 |
-
mu_values = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
|
| 227 |
-
|
| 228 |
-
for mu in mu_values:
|
| 229 |
-
correct = 0
|
| 230 |
-
total_cost = 0.0
|
| 231 |
-
unsafe_misses = 0
|
| 232 |
-
for i in idx_test:
|
| 233 |
-
t = traces[i]
|
| 234 |
-
x = feats_to_vec_safe(t["feats"]).reshape(1, -1)
|
| 235 |
-
pred_tier = route_carrot(x, tier_classifiers, mu=mu)
|
| 236 |
-
opt_tier = t["opt"]
|
| 237 |
-
|
| 238 |
-
# Check if predicted tier would succeed
|
| 239 |
-
would_succeed = t["tier_out"].get(pred_tier, False)
|
| 240 |
-
if would_succeed:
|
| 241 |
-
correct += 1
|
| 242 |
-
|
| 243 |
-
# Cost of predicted tier
|
| 244 |
-
total_cost += TIER_COST[pred_tier]
|
| 245 |
-
|
| 246 |
-
# Unsafe miss: predicted cheap tier for hard task
|
| 247 |
-
if pred_tier < opt_tier and not would_succeed:
|
| 248 |
-
unsafe_misses += 1
|
| 249 |
-
|
| 250 |
-
n_test = len(idx_test)
|
| 251 |
-
success_rate = correct / n_test
|
| 252 |
-
avg_cost = total_cost / n_test
|
| 253 |
-
unsafe_rate = unsafe_misses / n_test
|
| 254 |
-
|
| 255 |
-
# Compare to heuristic (task-type based)
|
| 256 |
-
heuristic_correct = 0
|
| 257 |
-
heuristic_cost = 0.0
|
| 258 |
-
for i in idx_test:
|
| 259 |
-
t = traces[i]
|
| 260 |
-
# Heuristic: route by task type (from classifier.py)
|
| 261 |
-
tt = t["tt"]
|
| 262 |
-
diff = t["diff"]
|
| 263 |
-
h_tier = min(diff + 1, 5) # simple: difficulty + 1
|
| 264 |
-
if t["tier_out"].get(h_tier, False):
|
| 265 |
-
heuristic_correct += 1
|
| 266 |
-
heuristic_cost += TIER_COST[h_tier]
|
| 267 |
-
|
| 268 |
-
h_success = heuristic_correct / n_test
|
| 269 |
-
h_avg_cost = heuristic_cost / n_test
|
| 270 |
-
|
| 271 |
-
# Frontier baseline
|
| 272 |
-
frontier_correct = sum(1 for i in idx_test if traces[i]["tier_out"].get(4, False) or traces[i]["tier_out"].get(5, False))
|
| 273 |
-
frontier_rate = frontier_correct / n_test
|
| 274 |
-
frontier_avg_cost = TIER_COST[4] # always tier 4
|
| 275 |
-
|
| 276 |
-
print(f"\n mu={mu:.1f}:")
|
| 277 |
-
print(f" CARROT: success={success_rate:.3f}, avg_cost={avg_cost:.4f}, unsafe_miss={unsafe_rate:.3f}")
|
| 278 |
-
print(f" Heuristic: success={h_success:.3f}, avg_cost={h_avg_cost:.4f}")
|
| 279 |
-
print(f" Frontier: success={frontier_rate:.3f}, avg_cost={frontier_avg_cost:.4f}")
|
| 280 |
-
print(f" Cost reduction vs frontier: {(1-avg_cost/frontier_avg_cost)*100:.1f}%")
|
| 281 |
-
print(f" Cost reduction vs heuristic: {(1-avg_cost/h_avg_cost)*100:.1f}%")
|
| 282 |
-
|
| 283 |
-
# ─── XGBoost Direct Optimal-Tier Classifier ─────────────────────────
|
| 284 |
-
print("\n\n[7] Training XGBoost direct optimal-tier classifier...")
|
| 285 |
-
|
| 286 |
-
y_train_opt = y_opt[idx_train] - 1 # XGB needs 0-indexed
|
| 287 |
-
y_test_opt = y_opt[idx_test] - 1
|
| 288 |
-
|
| 289 |
-
direct_clf = xgb.XGBClassifier(
|
| 290 |
-
n_estimators=200,
|
| 291 |
-
max_depth=5,
|
| 292 |
-
learning_rate=0.1,
|
| 293 |
-
subsample=0.8,
|
| 294 |
-
colsample_bytree=0.8,
|
| 295 |
-
objective="multi:softmax",
|
| 296 |
-
num_class=6,
|
| 297 |
-
eval_metric="mlogloss",
|
| 298 |
-
random_state=42,
|
| 299 |
-
verbosity=0,
|
| 300 |
-
)
|
| 301 |
-
direct_clf.fit(X_train, y_train_opt)
|
| 302 |
-
|
| 303 |
-
y_pred_opt = direct_clf.predict(X_test)
|
| 304 |
-
acc_direct = accuracy_score(y_test_opt, y_pred_opt)
|
| 305 |
-
print(f" Direct classifier accuracy: {acc_direct:.3f}")
|
| 306 |
-
|
| 307 |
-
# Detailed classification report
|
| 308 |
-
from sklearn.metrics import confusion_matrix
|
| 309 |
-
cm = confusion_matrix(y_test_opt, y_pred_opt, labels=[1,2,3,4,5])
|
| 310 |
-
print(f"\n Confusion Matrix (rows=true, cols=predicted):")
|
| 311 |
-
print(f" {'':>10} {'T1':>6} {'T2':>6} {'T3':>6} {'T4':>6} {'T5':>6}")
|
| 312 |
-
for i, tier in enumerate([1,2,3,4,5]):
|
| 313 |
-
row = f" True T{tier:>1}:"
|
| 314 |
-
for j in range(5):
|
| 315 |
-
row += f" {cm[i][j]:>6}"
|
| 316 |
-
print(row)
|
| 317 |
-
|
| 318 |
-
# Evaluate direct classifier on test set
|
| 319 |
-
print("\n[8] Evaluating direct optimal-tier classifier...")
|
| 320 |
-
direct_correct = 0
|
| 321 |
-
direct_cost = 0.0
|
| 322 |
-
direct_unsafe = 0
|
| 323 |
-
direct_underkill = 0
|
| 324 |
-
direct_overkill = 0
|
| 325 |
-
|
| 326 |
-
for i, test_idx in enumerate(idx_test):
|
| 327 |
-
t = traces[test_idx]
|
| 328 |
-
x = feats_to_vec_safe(t["feats"]).reshape(1, -1)
|
| 329 |
-
pred_tier = int(direct_clf.predict(x)[0]) + 1 # back to 1-indexed
|
| 330 |
-
opt_tier = t["opt"]
|
| 331 |
-
|
| 332 |
-
would_succeed = t["tier_out"].get(pred_tier, False)
|
| 333 |
-
if would_succeed:
|
| 334 |
-
direct_correct += 1
|
| 335 |
-
direct_cost += TIER_COST[pred_tier]
|
| 336 |
-
|
| 337 |
-
if pred_tier < opt_tier:
|
| 338 |
-
direct_underkill += 1
|
| 339 |
-
if not would_succeed:
|
| 340 |
-
direct_unsafe += 1
|
| 341 |
-
elif pred_tier > opt_tier:
|
| 342 |
-
direct_overkill += 1
|
| 343 |
-
|
| 344 |
-
n_test = len(idx_test)
|
| 345 |
-
print(f" Success rate: {direct_correct/n_test:.3f}")
|
| 346 |
-
print(f" Avg cost: {direct_cost/n_test:.4f}")
|
| 347 |
-
print(f" Underkill (pred < optimal): {direct_underkill/n_test:.3f}")
|
| 348 |
-
print(f" Overkill (pred > optimal): {direct_overkill/n_test:.3f}")
|
| 349 |
-
print(f" Unsafe misses: {direct_unsafe/n_test:.3f}")
|
| 350 |
-
|
| 351 |
-
# ─── Comparison: All Routers ────────────────────────────────────────
|
| 352 |
-
print("\n\n" + "="*80)
|
| 353 |
-
print("FINAL COMPARISON: ALL ROUTERS ON TEST SET")
|
| 354 |
-
print("="*80)
|
| 355 |
-
|
| 356 |
-
# 1. Always frontier
|
| 357 |
-
f_succ = sum(1 for i in idx_test if traces[i]["tier_out"].get(4,False) or traces[i]["tier_out"].get(5,False))
|
| 358 |
-
f_cost = TIER_COST[4] * n_test
|
| 359 |
-
|
| 360 |
-
# 2. Always cheapest
|
| 361 |
-
c_succ = sum(1 for i in idx_test if traces[i]["tier_out"].get(1,False) or traces[i]["tier_out"].get(2,False))
|
| 362 |
-
c_cost = TIER_COST[1] * n_test
|
| 363 |
-
|
| 364 |
-
# 3. Heuristic (difficulty + 1)
|
| 365 |
-
h_succ = 0; h_cost = 0.0
|
| 366 |
-
for i in idx_test:
|
| 367 |
-
t = traces[i]
|
| 368 |
-
h_tier = min(t["diff"] + 1, 5)
|
| 369 |
-
if t["tier_out"].get(h_tier, False): h_succ += 1
|
| 370 |
-
h_cost += TIER_COST[h_tier]
|
| 371 |
-
|
| 372 |
-
# 4. CARROT (best mu)
|
| 373 |
-
best_mu = 0.7
|
| 374 |
-
carrot_succ = 0; carrot_cost = 0.0; carrot_unsafe = 0
|
| 375 |
-
for i in idx_test:
|
| 376 |
-
t = traces[i]
|
| 377 |
-
x = feats_to_vec_safe(t["feats"]).reshape(1, -1)
|
| 378 |
-
pred = route_carrot(x, tier_classifiers, mu=best_mu)
|
| 379 |
-
if t["tier_out"].get(pred, False): carrot_succ += 1
|
| 380 |
-
carrot_cost += TIER_COST[pred]
|
| 381 |
-
if pred < t["opt"] and not t["tier_out"].get(pred, False):
|
| 382 |
-
carrot_unsafe += 1
|
| 383 |
-
|
| 384 |
-
# 5. Direct XGB
|
| 385 |
-
d_succ = direct_correct
|
| 386 |
-
d_cost = direct_cost
|
| 387 |
-
d_unsafe = direct_unsafe
|
| 388 |
-
|
| 389 |
-
# 6. Oracle (always picks optimal)
|
| 390 |
-
o_succ = sum(1 for i in idx_test if traces[i]["tier_out"].get(traces[i]["opt"], False))
|
| 391 |
-
o_cost = sum(TIER_COST[traces[i]["opt"]] for i in idx_test)
|
| 392 |
-
|
| 393 |
-
print(f"\n{'Router':<20} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10}")
|
| 394 |
-
print("-"*60)
|
| 395 |
-
for name, succ, cost, unsafe in [
|
| 396 |
-
("always_frontier", f_succ, f_cost, 0),
|
| 397 |
-
("always_cheap", c_succ, c_cost, 0),
|
| 398 |
-
("heuristic", h_succ, h_cost, 0),
|
| 399 |
-
(f"CARROT(mu={best_mu})", carrot_succ, carrot_cost, carrot_unsafe),
|
| 400 |
-
("direct_xgb", d_succ, d_cost, d_unsafe),
|
| 401 |
-
("oracle", o_succ, o_cost, 0),
|
| 402 |
-
]:
|
| 403 |
-
sr = succ/n_test
|
| 404 |
-
ac = cost/n_test
|
| 405 |
-
cr = (1 - cost/f_cost)*100
|
| 406 |
-
um = unsafe/n_test
|
| 407 |
-
print(f"{name:<20} {sr:>10.3f} {ac:>10.4f} {cr:>9.1f}% {um:>10.3f}")
|
| 408 |
-
|
| 409 |
-
# ─── Save Models ────────────────────────────────────────────────────
|
| 410 |
-
print("\n\n[9] Saving models...")
|
| 411 |
-
os.makedirs("/app/router_models", exist_ok=True)
|
| 412 |
-
|
| 413 |
-
# Save per-tier classifiers
|
| 414 |
-
for tier, clf in tier_classifiers.items():
|
| 415 |
-
clf.save_model(f"/app/router_models/tier_{tier}_success.json")
|
| 416 |
-
print(f" Saved tier_{tier}_success.json")
|
| 417 |
-
|
| 418 |
-
# Save direct classifier
|
| 419 |
-
direct_clf.save_model("/app/router_models/direct_optimal_tier.json")
|
| 420 |
-
print(f" Saved direct_optimal_tier.json")
|
| 421 |
-
|
| 422 |
-
# Save feature keys
|
| 423 |
-
with open("/app/router_models/feat_keys.json", "w") as f:
|
| 424 |
-
json.dump(FEAT_KEYS, f)
|
| 425 |
-
print(f" Saved feat_keys.json ({len(FEAT_KEYS)} features)")
|
| 426 |
-
|
| 427 |
-
# Save tier config
|
| 428 |
-
with open("/app/router_models/tier_config.json", "w") as f:
|
| 429 |
-
json.dump({"tier_cost": TIER_COST, "tier_str": TIER_STR}, f)
|
| 430 |
-
print(f" Saved tier_config.json")
|
| 431 |
-
|
| 432 |
-
print("\n\nDONE! Models saved to /app/router_models/")
|
| 433 |
-
print("Next step: integrate trained router into ACO ModelCascadeRouter._route_learned()")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/train_router_v1.py
DELETED
|
@@ -1,433 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Train a learned model router for Agent Cost Optimizer."""
|
| 3 |
-
import json, os, sys, random, pickle, uuid
|
| 4 |
-
import numpy as np
|
| 5 |
-
from datetime import datetime
|
| 6 |
-
from collections import defaultdict
|
| 7 |
-
from typing import Dict, List, Tuple, Any, Optional
|
| 8 |
-
|
| 9 |
-
TASK_TYPES = ["quick_answer","coding","research","document_drafting",
|
| 10 |
-
"legal_regulated","tool_heavy","retrieval_heavy",
|
| 11 |
-
"long_horizon","unknown_ambiguous"]
|
| 12 |
-
TT2IDX = {t:i for i,t in enumerate(TASK_TYPES)}
|
| 13 |
-
|
| 14 |
-
CODE_KW = ["python","javascript","code","function","bug","debug","refactor",
|
| 15 |
-
"implement","test","compile","runtime","class","module","async","thread"]
|
| 16 |
-
LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
|
| 17 |
-
RESEARCH_KW = ["research","find sources","literature","investigate","compare","analyze","survey"]
|
| 18 |
-
TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
|
| 19 |
-
LONG_KW = ["plan","project","roadmap","orchestrate","multi-step","migrate","pipeline","deploy"]
|
| 20 |
-
MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
|
| 21 |
-
|
| 22 |
-
TIER_STR = {1:0.35,2:0.55,3:0.80,4:0.93,5:0.97}
|
| 23 |
-
TIER_COST = {1:0.05,2:0.15,3:0.75,4:1.0,5:1.5}
|
| 24 |
-
|
| 25 |
-
TASK_TEMPLATES = {
|
| 26 |
-
"quick_answer":["What is the capital of France?","Explain quantum computing briefly.",
|
| 27 |
-
"What is 237*452?","Define photosynthesis.","Who wrote Hamlet?",
|
| 28 |
-
"What is the speed of light?","List the primary colors.","What is GDP?"],
|
| 29 |
-
"coding":["Write a Python function to reverse a linked list.",
|
| 30 |
-
"Fix the bug in this React component.","Refactor auth module to JWT.",
|
| 31 |
-
"Implement LRU cache in Go.","Debug segfault in C++ thread pool.",
|
| 32 |
-
"Add unit tests for the payment module.","Optimize this SQL query.",
|
| 33 |
-
"Create a REST API for user management.","Implement binary search in Rust."],
|
| 34 |
-
"research":["Research latest transformer advances.",
|
| 35 |
-
"Find sources comparing LoRA and full FT.",
|
| 36 |
-
"Investigate data center climate impact.",
|
| 37 |
-
"Survey privacy-preserving ML techniques.",
|
| 38 |
-
"Compare reinforcement learning algorithms for robotics."],
|
| 39 |
-
"document_drafting":["Draft project proposal for ML pipeline.",
|
| 40 |
-
"Write email to team about deployment.","Create technical report on performance."],
|
| 41 |
-
"legal_regulated":["Review this contract for liability clauses.",
|
| 42 |
-
"Check GDPR compliance for data pipeline.","Draft privacy policy section.",
|
| 43 |
-
"Verify regulatory compliance for medical device software."],
|
| 44 |
-
"tool_heavy":["Search open issues and create summary.",
|
| 45 |
-
"Fetch API docs and generate client code.","Query Q3 sales and produce chart."],
|
| 46 |
-
"retrieval_heavy":["Answer based on 50-page document.",
|
| 47 |
-
"Find all payment processing mentions.","Retrieve relevant cases for legal query."],
|
| 48 |
-
"long_horizon":["Plan 3-month roadmap.","Orchestrate multi-region deployment.",
|
| 49 |
-
"Redesign data architecture end-to-end.","Migrate monolith to microservices."],
|
| 50 |
-
"unknown_ambiguous":["Help me with this thing.",
|
| 51 |
-
"I need something about the server.","Can you look into that issue?"],
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
def tsp(tier, diff):
|
| 55 |
-
return TIER_STR[tier] ** (diff * 0.6)
|
| 56 |
-
|
| 57 |
-
def extract_features(request, task_type, difficulty=3):
|
| 58 |
-
r = request.lower()
|
| 59 |
-
f = {
|
| 60 |
-
"req_len": len(request),
|
| 61 |
-
"num_words": len(request.split()),
|
| 62 |
-
"has_code": int(any(k in r for k in CODE_KW)),
|
| 63 |
-
"n_code": sum(1 for k in CODE_KW if k in r),
|
| 64 |
-
"has_legal": int(any(k in r for k in LEGAL_KW)),
|
| 65 |
-
"n_legal": sum(1 for k in LEGAL_KW if k in r),
|
| 66 |
-
"has_research": int(any(k in r for k in RESEARCH_KW)),
|
| 67 |
-
"n_research": sum(1 for k in RESEARCH_KW if k in r),
|
| 68 |
-
"has_tool": int(any(k in r for k in TOOL_KW)),
|
| 69 |
-
"n_tool": sum(1 for k in TOOL_KW if k in r),
|
| 70 |
-
"has_long": int(any(k in r for k in LONG_KW)),
|
| 71 |
-
"has_math": int(any(k in r for k in MATH_KW)),
|
| 72 |
-
"tt_idx": TT2IDX.get(task_type, 8),
|
| 73 |
-
"difficulty": difficulty,
|
| 74 |
-
}
|
| 75 |
-
for tt in TASK_TYPES:
|
| 76 |
-
f[f"tt_{tt}"] = int(task_type == tt)
|
| 77 |
-
return f
|
| 78 |
-
|
| 79 |
-
def gen_trace(idx, rng):
|
| 80 |
-
tt = rng.choice(list(TASK_TEMPLATES.keys()))
|
| 81 |
-
diff = {"quick_answer":1,"document_drafting":2,"tool_heavy":2,"retrieval_heavy":2,
|
| 82 |
-
"research":3,"coding":3,"unknown_ambiguous":3,"long_horizon":4,"legal_regulated":5}[tt]
|
| 83 |
-
tier_out = {}
|
| 84 |
-
for t in range(1,6):
|
| 85 |
-
tier_out[t] = rng.random() < tsp(t, diff)
|
| 86 |
-
opt = 5
|
| 87 |
-
for t in range(1,6):
|
| 88 |
-
if tier_out[t]:
|
| 89 |
-
opt = t
|
| 90 |
-
break
|
| 91 |
-
if diff <= 2:
|
| 92 |
-
actual = rng.choices([1,2,3,4,5],weights=[3,4,2,1,0.5])[0]
|
| 93 |
-
elif diff == 3:
|
| 94 |
-
actual = rng.choices([1,2,3,4,5],weights=[1,2,4,2,1])[0]
|
| 95 |
-
elif diff == 4:
|
| 96 |
-
actual = rng.choices([1,2,3,4,5],weights=[0.5,1,2,4,2])[0]
|
| 97 |
-
else:
|
| 98 |
-
actual = rng.choices([1,2,3,4,5],weights=[0.2,0.5,1,3,4])[0]
|
| 99 |
-
outcome = "success" if tier_out[actual] else "failure"
|
| 100 |
-
req = rng.choice(TASK_TEMPLATES[tt])
|
| 101 |
-
feats = extract_features(req, tt, diff)
|
| 102 |
-
return {"feats":feats,"opt":opt,"actual":actual,"outcome":outcome,
|
| 103 |
-
"tier_out":tier_out,"tt":tt,"diff":diff,"req":req}
|
| 104 |
-
|
| 105 |
-
print("="*80)
|
| 106 |
-
print("AGENT COST OPTIMIZER - TRAINED ROUTER TRAINING")
|
| 107 |
-
print("="*80)
|
| 108 |
-
|
| 109 |
-
# ─── Generate Training Data ────────────────────────────────────────
|
| 110 |
-
print("\n[1] Generating 50K training traces...")
|
| 111 |
-
rng = random.Random(42)
|
| 112 |
-
traces = [gen_trace(i, rng) for i in range(50000)]
|
| 113 |
-
print(f" Generated {len(traces)} traces")
|
| 114 |
-
|
| 115 |
-
opt_dist = defaultdict(int)
|
| 116 |
-
for t in traces:
|
| 117 |
-
opt_dist[t["opt"]] += 1
|
| 118 |
-
for k in sorted(opt_dist):
|
| 119 |
-
print(f" opt_tier={k}: {opt_dist[k]} ({opt_dist[k]/len(traces)*100:.1f}%)")
|
| 120 |
-
|
| 121 |
-
# ─── Build Feature Matrix ──────────────────────────────────────────
|
| 122 |
-
print("\n[2] Building feature matrix...")
|
| 123 |
-
|
| 124 |
-
def feats_to_vec(feats):
|
| 125 |
-
"""Convert feature dict to fixed-order numpy array."""
|
| 126 |
-
keys = sorted(feats.keys())
|
| 127 |
-
return np.array([float(feats[k]) for k in keys], dtype=np.float32)
|
| 128 |
-
|
| 129 |
-
# Get feature key order from first trace
|
| 130 |
-
FEAT_KEYS = sorted(traces[0]["feats"].keys())
|
| 131 |
-
NUM_FEATURES = len(FEAT_KEYS)
|
| 132 |
-
print(f" Feature count: {NUM_FEATURES}")
|
| 133 |
-
print(f" Features: {FEAT_KEYS}")
|
| 134 |
-
|
| 135 |
-
def feats_to_vec_safe(feats):
|
| 136 |
-
return np.array([float(feats.get(k, 0.0)) for k in FEAT_KEYS], dtype=np.float32)
|
| 137 |
-
|
| 138 |
-
X_all = np.array([feats_to_vec_safe(t["feats"]) for t in traces])
|
| 139 |
-
y_opt = np.array([t["opt"] for t in traces])
|
| 140 |
-
y_actual = np.array([t["actual"] for t in traces])
|
| 141 |
-
y_outcome = np.array([1 if t["outcome"]=="success" else 0 for t in traces])
|
| 142 |
-
|
| 143 |
-
# ─── Per-Tier Success Classifiers ───────────────────────────────────
|
| 144 |
-
print("\n[3] Training per-tier P(success|query) classifiers...")
|
| 145 |
-
from sklearn.model_selection import train_test_split
|
| 146 |
-
from sklearn.metrics import classification_report, accuracy_score, f1_score
|
| 147 |
-
|
| 148 |
-
# For each tier, create binary label: did this tier succeed?
|
| 149 |
-
per_tier_labels = {}
|
| 150 |
-
for tier in range(1, 6):
|
| 151 |
-
labels = []
|
| 152 |
-
for t in traces:
|
| 153 |
-
labels.append(1 if t["tier_out"].get(tier, False) else 0)
|
| 154 |
-
per_tier_labels[tier] = np.array(labels)
|
| 155 |
-
succ_rate = per_tier_labels[tier].mean()
|
| 156 |
-
print(f" Tier {tier}: success rate = {succ_rate:.3f}")
|
| 157 |
-
|
| 158 |
-
# Split train/test
|
| 159 |
-
X_train, X_test, idx_train, idx_test = train_test_split(
|
| 160 |
-
X_all, range(len(traces)), test_size=0.2, random_state=42, stratify=y_opt
|
| 161 |
-
)
|
| 162 |
-
|
| 163 |
-
print(f"\n Train: {len(X_train)}, Test: {len(X_test)}")
|
| 164 |
-
|
| 165 |
-
# ─── XGBoost Per-Tier Classifiers ───────────────────────────────────
|
| 166 |
-
print("\n[4] Training XGBoost per-tier classifiers...")
|
| 167 |
-
import xgboost as xgb
|
| 168 |
-
|
| 169 |
-
tier_classifiers = {}
|
| 170 |
-
for tier in range(1, 6):
|
| 171 |
-
y_train_tier = per_tier_labels[tier][idx_train]
|
| 172 |
-
y_test_tier = per_tier_labels[tier][idx_test]
|
| 173 |
-
|
| 174 |
-
clf = xgb.XGBClassifier(
|
| 175 |
-
n_estimators=100,
|
| 176 |
-
max_depth=4,
|
| 177 |
-
learning_rate=0.1,
|
| 178 |
-
subsample=0.8,
|
| 179 |
-
colsample_bytree=0.8,
|
| 180 |
-
objective="binary:logistic",
|
| 181 |
-
eval_metric="logloss",
|
| 182 |
-
random_state=42,
|
| 183 |
-
verbosity=0,
|
| 184 |
-
)
|
| 185 |
-
clf.fit(X_train, y_train_tier)
|
| 186 |
-
|
| 187 |
-
y_pred = clf.predict(X_test)
|
| 188 |
-
y_prob = clf.predict_proba(X_test)[:, 1]
|
| 189 |
-
|
| 190 |
-
acc = accuracy_score(y_test_tier, y_pred)
|
| 191 |
-
f1 = f1_score(y_test_tier, y_pred, zero_division=0)
|
| 192 |
-
|
| 193 |
-
tier_classifiers[tier] = clf
|
| 194 |
-
print(f" Tier {tier}: accuracy={acc:.3f}, f1={f1:.3f}")
|
| 195 |
-
|
| 196 |
-
# ─── CARROT-Style Router Decision ────────────────────────────────────
|
| 197 |
-
print("\n[5] Building CARROT-style router...")
|
| 198 |
-
|
| 199 |
-
def route_carrot(features_vec, tier_clfs, mu=0.7):
|
| 200 |
-
"""Route to argmin_tier [mu*(1-P_success_tier) + (1-mu)*cost_tier].
|
| 201 |
-
|
| 202 |
-
mu controls quality-vs-cost tradeoff:
|
| 203 |
-
mu=1.0: maximize quality only (always frontier)
|
| 204 |
-
mu=0.0: minimize cost only (always cheapest)
|
| 205 |
-
mu=0.7: 70% quality, 30% cost (our default)
|
| 206 |
-
"""
|
| 207 |
-
if features_vec.ndim == 1:
|
| 208 |
-
features_vec = features_vec.reshape(1, -1)
|
| 209 |
-
|
| 210 |
-
best_tier = 3
|
| 211 |
-
best_score = float("inf")
|
| 212 |
-
|
| 213 |
-
for tier in range(1, 6):
|
| 214 |
-
p_success = tier_clfs[tier].predict_proba(features_vec)[0, 1]
|
| 215 |
-
cost_norm = TIER_COST[tier] / TIER_COST[5] # normalize to [0,1]
|
| 216 |
-
score = mu * (1.0 - p_success) + (1.0 - mu) * cost_norm
|
| 217 |
-
if score < best_score:
|
| 218 |
-
best_score = score
|
| 219 |
-
best_tier = tier
|
| 220 |
-
|
| 221 |
-
return best_tier
|
| 222 |
-
|
| 223 |
-
# Evaluate on test set
|
| 224 |
-
print("\n[6] Evaluating CARROT router on test set...")
|
| 225 |
-
|
| 226 |
-
mu_values = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
|
| 227 |
-
|
| 228 |
-
for mu in mu_values:
|
| 229 |
-
correct = 0
|
| 230 |
-
total_cost = 0.0
|
| 231 |
-
unsafe_misses = 0
|
| 232 |
-
for i in idx_test:
|
| 233 |
-
t = traces[i]
|
| 234 |
-
x = feats_to_vec_safe(t["feats"]).reshape(1, -1)
|
| 235 |
-
pred_tier = route_carrot(x, tier_classifiers, mu=mu)
|
| 236 |
-
opt_tier = t["opt"]
|
| 237 |
-
|
| 238 |
-
# Check if predicted tier would succeed
|
| 239 |
-
would_succeed = t["tier_out"].get(pred_tier, False)
|
| 240 |
-
if would_succeed:
|
| 241 |
-
correct += 1
|
| 242 |
-
|
| 243 |
-
# Cost of predicted tier
|
| 244 |
-
total_cost += TIER_COST[pred_tier]
|
| 245 |
-
|
| 246 |
-
# Unsafe miss: predicted cheap tier for hard task
|
| 247 |
-
if pred_tier < opt_tier and not would_succeed:
|
| 248 |
-
unsafe_misses += 1
|
| 249 |
-
|
| 250 |
-
n_test = len(idx_test)
|
| 251 |
-
success_rate = correct / n_test
|
| 252 |
-
avg_cost = total_cost / n_test
|
| 253 |
-
unsafe_rate = unsafe_misses / n_test
|
| 254 |
-
|
| 255 |
-
# Compare to heuristic (task-type based)
|
| 256 |
-
heuristic_correct = 0
|
| 257 |
-
heuristic_cost = 0.0
|
| 258 |
-
for i in idx_test:
|
| 259 |
-
t = traces[i]
|
| 260 |
-
# Heuristic: route by task type (from classifier.py)
|
| 261 |
-
tt = t["tt"]
|
| 262 |
-
diff = t["diff"]
|
| 263 |
-
h_tier = min(diff + 1, 5) # simple: difficulty + 1
|
| 264 |
-
if t["tier_out"].get(h_tier, False):
|
| 265 |
-
heuristic_correct += 1
|
| 266 |
-
heuristic_cost += TIER_COST[h_tier]
|
| 267 |
-
|
| 268 |
-
h_success = heuristic_correct / n_test
|
| 269 |
-
h_avg_cost = heuristic_cost / n_test
|
| 270 |
-
|
| 271 |
-
# Frontier baseline
|
| 272 |
-
frontier_correct = sum(1 for i in idx_test if traces[i]["tier_out"].get(4, False) or traces[i]["tier_out"].get(5, False))
|
| 273 |
-
frontier_rate = frontier_correct / n_test
|
| 274 |
-
frontier_avg_cost = TIER_COST[4] # always tier 4
|
| 275 |
-
|
| 276 |
-
print(f"\n mu={mu:.1f}:")
|
| 277 |
-
print(f" CARROT: success={success_rate:.3f}, avg_cost={avg_cost:.4f}, unsafe_miss={unsafe_rate:.3f}")
|
| 278 |
-
print(f" Heuristic: success={h_success:.3f}, avg_cost={h_avg_cost:.4f}")
|
| 279 |
-
print(f" Frontier: success={frontier_rate:.3f}, avg_cost={frontier_avg_cost:.4f}")
|
| 280 |
-
print(f" Cost reduction vs frontier: {(1-avg_cost/frontier_avg_cost)*100:.1f}%")
|
| 281 |
-
print(f" Cost reduction vs heuristic: {(1-avg_cost/h_avg_cost)*100:.1f}%")
|
| 282 |
-
|
| 283 |
-
# ─── XGBoost Direct Optimal-Tier Classifier ─────────────────────────
|
| 284 |
-
print("\n\n[7] Training XGBoost direct optimal-tier classifier...")
|
| 285 |
-
|
| 286 |
-
y_train_opt = y_opt[idx_train] - 1 # XGB needs 0-indexed
|
| 287 |
-
y_test_opt = y_opt[idx_test] - 1
|
| 288 |
-
|
| 289 |
-
direct_clf = xgb.XGBClassifier(
|
| 290 |
-
n_estimators=200,
|
| 291 |
-
max_depth=5,
|
| 292 |
-
learning_rate=0.1,
|
| 293 |
-
subsample=0.8,
|
| 294 |
-
colsample_bytree=0.8,
|
| 295 |
-
objective="multi:softmax",
|
| 296 |
-
num_class=6,
|
| 297 |
-
eval_metric="mlogloss",
|
| 298 |
-
random_state=42,
|
| 299 |
-
verbosity=0,
|
| 300 |
-
)
|
| 301 |
-
direct_clf.fit(X_train, y_train_opt)
|
| 302 |
-
|
| 303 |
-
y_pred_opt = direct_clf.predict(X_test)
|
| 304 |
-
acc_direct = accuracy_score(y_test_opt, y_pred_opt)
|
| 305 |
-
print(f" Direct classifier accuracy: {acc_direct:.3f}")
|
| 306 |
-
|
| 307 |
-
# Detailed classification report
|
| 308 |
-
from sklearn.metrics import confusion_matrix
|
| 309 |
-
cm = confusion_matrix(y_test_opt, y_pred_opt, labels=[1,2,3,4,5])
|
| 310 |
-
print(f"\n Confusion Matrix (rows=true, cols=predicted):")
|
| 311 |
-
print(f" {'':>10} {'T1':>6} {'T2':>6} {'T3':>6} {'T4':>6} {'T5':>6}")
|
| 312 |
-
for i, tier in enumerate([1,2,3,4,5]):
|
| 313 |
-
row = f" True T{tier:>1}:"
|
| 314 |
-
for j in range(5):
|
| 315 |
-
row += f" {cm[i][j]:>6}"
|
| 316 |
-
print(row)
|
| 317 |
-
|
| 318 |
-
# Evaluate direct classifier on test set
|
| 319 |
-
print("\n[8] Evaluating direct optimal-tier classifier...")
|
| 320 |
-
direct_correct = 0
|
| 321 |
-
direct_cost = 0.0
|
| 322 |
-
direct_unsafe = 0
|
| 323 |
-
direct_underkill = 0
|
| 324 |
-
direct_overkill = 0
|
| 325 |
-
|
| 326 |
-
for i, test_idx in enumerate(idx_test):
|
| 327 |
-
t = traces[test_idx]
|
| 328 |
-
x = feats_to_vec_safe(t["feats"]).reshape(1, -1)
|
| 329 |
-
pred_tier = int(direct_clf.predict(x)[0]) + 1 # back to 1-indexed
|
| 330 |
-
opt_tier = t["opt"]
|
| 331 |
-
|
| 332 |
-
would_succeed = t["tier_out"].get(pred_tier, False)
|
| 333 |
-
if would_succeed:
|
| 334 |
-
direct_correct += 1
|
| 335 |
-
direct_cost += TIER_COST[pred_tier]
|
| 336 |
-
|
| 337 |
-
if pred_tier < opt_tier:
|
| 338 |
-
direct_underkill += 1
|
| 339 |
-
if not would_succeed:
|
| 340 |
-
direct_unsafe += 1
|
| 341 |
-
elif pred_tier > opt_tier:
|
| 342 |
-
direct_overkill += 1
|
| 343 |
-
|
| 344 |
-
n_test = len(idx_test)
|
| 345 |
-
print(f" Success rate: {direct_correct/n_test:.3f}")
|
| 346 |
-
print(f" Avg cost: {direct_cost/n_test:.4f}")
|
| 347 |
-
print(f" Underkill (pred < optimal): {direct_underkill/n_test:.3f}")
|
| 348 |
-
print(f" Overkill (pred > optimal): {direct_overkill/n_test:.3f}")
|
| 349 |
-
print(f" Unsafe misses: {direct_unsafe/n_test:.3f}")
|
| 350 |
-
|
| 351 |
-
# ─── Comparison: All Routers ────────────────────────────────────────
|
| 352 |
-
print("\n\n" + "="*80)
|
| 353 |
-
print("FINAL COMPARISON: ALL ROUTERS ON TEST SET")
|
| 354 |
-
print("="*80)
|
| 355 |
-
|
| 356 |
-
# 1. Always frontier
|
| 357 |
-
f_succ = sum(1 for i in idx_test if traces[i]["tier_out"].get(4,False) or traces[i]["tier_out"].get(5,False))
|
| 358 |
-
f_cost = TIER_COST[4] * n_test
|
| 359 |
-
|
| 360 |
-
# 2. Always cheapest
|
| 361 |
-
c_succ = sum(1 for i in idx_test if traces[i]["tier_out"].get(1,False) or traces[i]["tier_out"].get(2,False))
|
| 362 |
-
c_cost = TIER_COST[1] * n_test
|
| 363 |
-
|
| 364 |
-
# 3. Heuristic (difficulty + 1)
|
| 365 |
-
h_succ = 0; h_cost = 0.0
|
| 366 |
-
for i in idx_test:
|
| 367 |
-
t = traces[i]
|
| 368 |
-
h_tier = min(t["diff"] + 1, 5)
|
| 369 |
-
if t["tier_out"].get(h_tier, False): h_succ += 1
|
| 370 |
-
h_cost += TIER_COST[h_tier]
|
| 371 |
-
|
| 372 |
-
# 4. CARROT (best mu)
|
| 373 |
-
best_mu = 0.7
|
| 374 |
-
carrot_succ = 0; carrot_cost = 0.0; carrot_unsafe = 0
|
| 375 |
-
for i in idx_test:
|
| 376 |
-
t = traces[i]
|
| 377 |
-
x = feats_to_vec_safe(t["feats"]).reshape(1, -1)
|
| 378 |
-
pred = route_carrot(x, tier_classifiers, mu=best_mu)
|
| 379 |
-
if t["tier_out"].get(pred, False): carrot_succ += 1
|
| 380 |
-
carrot_cost += TIER_COST[pred]
|
| 381 |
-
if pred < t["opt"] and not t["tier_out"].get(pred, False):
|
| 382 |
-
carrot_unsafe += 1
|
| 383 |
-
|
| 384 |
-
# 5. Direct XGB
|
| 385 |
-
d_succ = direct_correct
|
| 386 |
-
d_cost = direct_cost
|
| 387 |
-
d_unsafe = direct_unsafe
|
| 388 |
-
|
| 389 |
-
# 6. Oracle (always picks optimal)
|
| 390 |
-
o_succ = sum(1 for i in idx_test if traces[i]["tier_out"].get(traces[i]["opt"], False))
|
| 391 |
-
o_cost = sum(TIER_COST[traces[i]["opt"]] for i in idx_test)
|
| 392 |
-
|
| 393 |
-
print(f"\n{'Router':<20} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10}")
|
| 394 |
-
print("-"*60)
|
| 395 |
-
for name, succ, cost, unsafe in [
|
| 396 |
-
("always_frontier", f_succ, f_cost, 0),
|
| 397 |
-
("always_cheap", c_succ, c_cost, 0),
|
| 398 |
-
("heuristic", h_succ, h_cost, 0),
|
| 399 |
-
(f"CARROT(mu={best_mu})", carrot_succ, carrot_cost, carrot_unsafe),
|
| 400 |
-
("direct_xgb", d_succ, d_cost, d_unsafe),
|
| 401 |
-
("oracle", o_succ, o_cost, 0),
|
| 402 |
-
]:
|
| 403 |
-
sr = succ/n_test
|
| 404 |
-
ac = cost/n_test
|
| 405 |
-
cr = (1 - cost/f_cost)*100
|
| 406 |
-
um = unsafe/n_test
|
| 407 |
-
print(f"{name:<20} {sr:>10.3f} {ac:>10.4f} {cr:>9.1f}% {um:>10.3f}")
|
| 408 |
-
|
| 409 |
-
# ─── Save Models ────────────────────────────────────────────────────
|
| 410 |
-
print("\n\n[9] Saving models...")
|
| 411 |
-
os.makedirs("/app/router_models", exist_ok=True)
|
| 412 |
-
|
| 413 |
-
# Save per-tier classifiers
|
| 414 |
-
for tier, clf in tier_classifiers.items():
|
| 415 |
-
clf.save_model(f"/app/router_models/tier_{tier}_success.json")
|
| 416 |
-
print(f" Saved tier_{tier}_success.json")
|
| 417 |
-
|
| 418 |
-
# Save direct classifier
|
| 419 |
-
direct_clf.save_model("/app/router_models/direct_optimal_tier.json")
|
| 420 |
-
print(f" Saved direct_optimal_tier.json")
|
| 421 |
-
|
| 422 |
-
# Save feature keys
|
| 423 |
-
with open("/app/router_models/feat_keys.json", "w") as f:
|
| 424 |
-
json.dump(FEAT_KEYS, f)
|
| 425 |
-
print(f" Saved feat_keys.json ({len(FEAT_KEYS)} features)")
|
| 426 |
-
|
| 427 |
-
# Save tier config
|
| 428 |
-
with open("/app/router_models/tier_config.json", "w") as f:
|
| 429 |
-
json.dump({"tier_cost": TIER_COST, "tier_str": TIER_STR}, f)
|
| 430 |
-
print(f" Saved tier_config.json")
|
| 431 |
-
|
| 432 |
-
print("\n\nDONE! Models saved to /app/router_models/")
|
| 433 |
-
print("Next step: integrate trained router into ACO ModelCascadeRouter._route_learned()")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/train_router_v2.py
DELETED
|
@@ -1,510 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Trained Router v2: Safety-first CARROT with tuned mu + safety floors.
|
| 3 |
-
|
| 4 |
-
Key insight from v1:
|
| 5 |
-
- Per-tier P(success) classifiers work well individually
|
| 6 |
-
- CARROT routing with mu=0.6 beats heuristic on both quality and cost
|
| 7 |
-
- But success rate drops because CARROT routes cheap for hard tasks
|
| 8 |
-
|
| 9 |
-
Solution: Add SAFETY FLOORS per task type:
|
| 10 |
-
- legal_regulated: never below tier 4
|
| 11 |
-
- coding/research with legal kw: never below tier 3
|
| 12 |
-
- Use P(success) > threshold as gate, fallback to difficulty-based tier
|
| 13 |
-
- When confidence is low, default to tier 3 (medium)
|
| 14 |
-
"""
|
| 15 |
-
import json, os, sys, random, pickle, uuid
|
| 16 |
-
import numpy as np
|
| 17 |
-
from datetime import datetime
|
| 18 |
-
from collections import defaultdict
|
| 19 |
-
|
| 20 |
-
TASK_TYPES = ["quick_answer","coding","research","document_drafting",
|
| 21 |
-
"legal_regulated","tool_heavy","retrieval_heavy",
|
| 22 |
-
"long_horizon","unknown_ambiguous"]
|
| 23 |
-
TT2IDX = {t:i for i,t in enumerate(TASK_TYPES)}
|
| 24 |
-
|
| 25 |
-
CODE_KW = ["python","javascript","code","function","bug","debug","refactor",
|
| 26 |
-
"implement","test","compile","runtime","class","module","async","thread"]
|
| 27 |
-
LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
|
| 28 |
-
RESEARCH_KW = ["research","find sources","literature","investigate","compare","analyze","survey"]
|
| 29 |
-
TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
|
| 30 |
-
LONG_KW = ["plan","project","roadmap","orchestrate","multi-step","migrate","pipeline","deploy"]
|
| 31 |
-
MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
|
| 32 |
-
|
| 33 |
-
TIER_STR = {1:0.35,2:0.55,3:0.80,4:0.93,5:0.97}
|
| 34 |
-
TIER_COST = {1:0.05,2:0.15,3:0.75,4:1.0,5:1.5}
|
| 35 |
-
|
| 36 |
-
TASK_TEMPLATES = {
|
| 37 |
-
"quick_answer":["What is the capital of France?","Explain quantum computing briefly.",
|
| 38 |
-
"What is 237*452?","Define photosynthesis.","Who wrote Hamlet?",
|
| 39 |
-
"What is the speed of light?","List the primary colors.","What is GDP?"],
|
| 40 |
-
"coding":["Write a Python function to reverse a linked list.",
|
| 41 |
-
"Fix the bug in this React component.","Refactor auth module to JWT.",
|
| 42 |
-
"Implement LRU cache in Go.","Debug segfault in C++ thread pool.",
|
| 43 |
-
"Add unit tests for the payment module.","Optimize this SQL query.",
|
| 44 |
-
"Create a REST API for user management.","Implement binary search in Rust."],
|
| 45 |
-
"research":["Research latest transformer advances.",
|
| 46 |
-
"Find sources comparing LoRA and full FT.",
|
| 47 |
-
"Investigate data center climate impact.",
|
| 48 |
-
"Survey privacy-preserving ML techniques.",
|
| 49 |
-
"Compare reinforcement learning algorithms for robotics."],
|
| 50 |
-
"document_drafting":["Draft project proposal for ML pipeline.",
|
| 51 |
-
"Write email to team about deployment.","Create technical report on performance."],
|
| 52 |
-
"legal_regulated":["Review this contract for liability clauses.",
|
| 53 |
-
"Check GDPR compliance for data pipeline.","Draft privacy policy section.",
|
| 54 |
-
"Verify regulatory compliance for medical device software."],
|
| 55 |
-
"tool_heavy":["Search open issues and create summary.",
|
| 56 |
-
"Fetch API docs and generate client code.","Query Q3 sales and produce chart."],
|
| 57 |
-
"retrieval_heavy":["Answer based on 50-page document.",
|
| 58 |
-
"Find all payment processing mentions.","Retrieve relevant cases for legal query."],
|
| 59 |
-
"long_horizon":["Plan 3-month roadmap.","Orchestrate multi-region deployment.",
|
| 60 |
-
"Redesign data architecture end-to-end.","Migrate monolith to microservices."],
|
| 61 |
-
"unknown_ambiguous":["Help me with this thing.",
|
| 62 |
-
"I need something about the server.","Can you look into that issue?"],
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
# Safety floors per task type
|
| 66 |
-
TASK_FLOOR = {
|
| 67 |
-
"legal_regulated": 4,
|
| 68 |
-
"long_horizon": 3,
|
| 69 |
-
"research": 3,
|
| 70 |
-
"coding": 3,
|
| 71 |
-
"unknown_ambiguous": 3,
|
| 72 |
-
"quick_answer": 1,
|
| 73 |
-
"document_drafting": 2,
|
| 74 |
-
"tool_heavy": 2,
|
| 75 |
-
"retrieval_heavy": 2,
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
def tsp(tier, diff):
|
| 79 |
-
return TIER_STR[tier] ** (diff * 0.6)
|
| 80 |
-
|
| 81 |
-
def extract_features(request, task_type, difficulty=3):
|
| 82 |
-
r = request.lower()
|
| 83 |
-
f = {
|
| 84 |
-
"req_len": len(request),
|
| 85 |
-
"num_words": len(request.split()),
|
| 86 |
-
"has_code": int(any(k in r for k in CODE_KW)),
|
| 87 |
-
"n_code": sum(1 for k in CODE_KW if k in r),
|
| 88 |
-
"has_legal": int(any(k in r for k in LEGAL_KW)),
|
| 89 |
-
"n_legal": sum(1 for k in LEGAL_KW if k in r),
|
| 90 |
-
"has_research": int(any(k in r for k in RESEARCH_KW)),
|
| 91 |
-
"n_research": sum(1 for k in RESEARCH_KW if k in r),
|
| 92 |
-
"has_tool": int(any(k in r for k in TOOL_KW)),
|
| 93 |
-
"n_tool": sum(1 for k in TOOL_KW if k in r),
|
| 94 |
-
"has_long": int(any(k in r for k in LONG_KW)),
|
| 95 |
-
"has_math": int(any(k in r for k in MATH_KW)),
|
| 96 |
-
"tt_idx": TT2IDX.get(task_type, 8),
|
| 97 |
-
"difficulty": difficulty,
|
| 98 |
-
}
|
| 99 |
-
for tt in TASK_TYPES:
|
| 100 |
-
f[f"tt_{tt}"] = int(task_type == tt)
|
| 101 |
-
return f
|
| 102 |
-
|
| 103 |
-
def gen_trace(idx, rng):
|
| 104 |
-
tt = rng.choice(list(TASK_TEMPLATES.keys()))
|
| 105 |
-
diff = {"quick_answer":1,"document_drafting":2,"tool_heavy":2,"retrieval_heavy":2,
|
| 106 |
-
"research":3,"coding":3,"unknown_ambiguous":3,"long_horizon":4,"legal_regulated":5}[tt]
|
| 107 |
-
tier_out = {}
|
| 108 |
-
for t in range(1,6):
|
| 109 |
-
tier_out[t] = rng.random() < tsp(t, diff)
|
| 110 |
-
opt = 5
|
| 111 |
-
for t in range(1,6):
|
| 112 |
-
if tier_out[t]:
|
| 113 |
-
opt = t
|
| 114 |
-
break
|
| 115 |
-
if diff <= 2:
|
| 116 |
-
actual = rng.choices([1,2,3,4,5],weights=[3,4,2,1,0.5])[0]
|
| 117 |
-
elif diff == 3:
|
| 118 |
-
actual = rng.choices([1,2,3,4,5],weights=[1,2,4,2,1])[0]
|
| 119 |
-
elif diff == 4:
|
| 120 |
-
actual = rng.choices([1,2,3,4,5],weights=[0.5,1,2,4,2])[0]
|
| 121 |
-
else:
|
| 122 |
-
actual = rng.choices([1,2,3,4,5],weights=[0.2,0.5,1,3,4])[0]
|
| 123 |
-
outcome = "success" if tier_out[actual] else "failure"
|
| 124 |
-
req = rng.choice(TASK_TEMPLATES[tt])
|
| 125 |
-
feats = extract_features(req, tt, diff)
|
| 126 |
-
return {"feats":feats,"opt":opt,"actual":actual,"outcome":outcome,
|
| 127 |
-
"tier_out":tier_out,"tt":tt,"diff":diff,"req":req}
|
| 128 |
-
|
| 129 |
-
print("="*80)
|
| 130 |
-
print("AGENT COST OPTIMIZER - TRAINED ROUTER v2 (Safety-First CARROT)")
|
| 131 |
-
print("="*80)
|
| 132 |
-
|
| 133 |
-
print("\n[1] Generating 50K training traces...")
|
| 134 |
-
rng = random.Random(42)
|
| 135 |
-
traces = [gen_trace(i, rng) for i in range(50000)]
|
| 136 |
-
print(f" Generated {len(traces)} traces")
|
| 137 |
-
|
| 138 |
-
# Feature matrix
|
| 139 |
-
FEAT_KEYS = sorted(traces[0]["feats"].keys())
|
| 140 |
-
NUM_FEATURES = len(FEAT_KEYS)
|
| 141 |
-
|
| 142 |
-
def f2v(feats):
|
| 143 |
-
return np.array([float(feats.get(k, 0.0)) for k in FEAT_KEYS], dtype=np.float32)
|
| 144 |
-
|
| 145 |
-
X_all = np.array([f2v(t["feats"]) for t in traces])
|
| 146 |
-
y_opt = np.array([t["opt"] for t in traces])
|
| 147 |
-
|
| 148 |
-
# Per-tier labels
|
| 149 |
-
per_tier_labels = {}
|
| 150 |
-
for tier in range(1, 6):
|
| 151 |
-
per_tier_labels[tier] = np.array([1 if t["tier_out"].get(tier, False) else 0 for t in traces])
|
| 152 |
-
|
| 153 |
-
# Train/test split
|
| 154 |
-
from sklearn.model_selection import train_test_split
|
| 155 |
-
from sklearn.metrics import accuracy_score, f1_score
|
| 156 |
-
|
| 157 |
-
X_train, X_test, idx_train, idx_test = train_test_split(
|
| 158 |
-
X_all, range(len(traces)), test_size=0.2, random_state=42, stratify=y_opt
|
| 159 |
-
)
|
| 160 |
-
print(f" Train: {len(X_train)}, Test: {len(X_test)}")
|
| 161 |
-
|
| 162 |
-
# ─── Train Per-Tier XGBoost Classifiers ────────────────────────────
|
| 163 |
-
print("\n[2] Training per-tier P(success) XGBoost classifiers...")
|
| 164 |
-
import xgboost as xgb
|
| 165 |
-
|
| 166 |
-
tier_clfs = {}
|
| 167 |
-
for tier in range(1, 6):
|
| 168 |
-
y_tr = per_tier_labels[tier][idx_train]
|
| 169 |
-
y_te = per_tier_labels[tier][idx_test]
|
| 170 |
-
|
| 171 |
-
# Compute scale_pos_weight for imbalanced classes
|
| 172 |
-
neg = (y_tr == 0).sum()
|
| 173 |
-
pos = (y_tr == 1).sum()
|
| 174 |
-
spw = neg / max(pos, 1)
|
| 175 |
-
|
| 176 |
-
clf = xgb.XGBClassifier(
|
| 177 |
-
n_estimators=150, max_depth=5, learning_rate=0.1,
|
| 178 |
-
subsample=0.8, colsample_bytree=0.8,
|
| 179 |
-
scale_pos_weight=min(spw, 5.0),
|
| 180 |
-
objective="binary:logistic", eval_metric="logloss",
|
| 181 |
-
random_state=42, verbosity=0,
|
| 182 |
-
)
|
| 183 |
-
clf.fit(X_train, y_tr)
|
| 184 |
-
|
| 185 |
-
y_pred = clf.predict(X_test)
|
| 186 |
-
acc = accuracy_score(y_te, y_pred)
|
| 187 |
-
f1 = f1_score(y_te, y_pred, zero_division=0)
|
| 188 |
-
tier_clfs[tier] = clf
|
| 189 |
-
print(f" Tier {tier}: acc={acc:.3f}, f1={f1:.3f}, spw={spw:.2f}")
|
| 190 |
-
|
| 191 |
-
# ─── Safety-First CARROT Router ─────────────────────────────────────
|
| 192 |
-
print("\n[3] Building safety-first CARROT router...")
|
| 193 |
-
|
| 194 |
-
def route_safe_carrot(features_vec, tier_clfs, task_type, mu=0.7,
|
| 195 |
-
success_threshold=0.5, safety_floor=None):
|
| 196 |
-
"""Route with safety floors.
|
| 197 |
-
|
| 198 |
-
1. Compute P(success|tier) for each tier
|
| 199 |
-
2. Apply safety floor per task type
|
| 200 |
-
3. Pick cheapest tier where P(success) > threshold
|
| 201 |
-
4. If none meets threshold, escalate to next tier
|
| 202 |
-
"""
|
| 203 |
-
if features_vec.ndim == 1:
|
| 204 |
-
features_vec = features_vec.reshape(1, -1)
|
| 205 |
-
|
| 206 |
-
floor = safety_floor or TASK_FLOOR.get(task_type, 2)
|
| 207 |
-
|
| 208 |
-
# Get per-tier success probabilities
|
| 209 |
-
p_success = {}
|
| 210 |
-
for tier in range(1, 6):
|
| 211 |
-
p_success[tier] = tier_clfs[tier].predict_proba(features_vec)[0, 1]
|
| 212 |
-
|
| 213 |
-
# Strategy: Find cheapest tier at or above floor where P(success) > threshold
|
| 214 |
-
for tier in range(floor, 6):
|
| 215 |
-
if p_success[tier] >= success_threshold:
|
| 216 |
-
return tier, p_success
|
| 217 |
-
|
| 218 |
-
# Fallback: if no tier meets threshold at floor, try escalating
|
| 219 |
-
for tier in range(floor + 1, 6):
|
| 220 |
-
if p_success[tier] >= success_threshold * 0.8: # relaxed threshold
|
| 221 |
-
return tier, p_success
|
| 222 |
-
|
| 223 |
-
# Last resort: use CARROT scoring at floor
|
| 224 |
-
best_tier = floor
|
| 225 |
-
best_score = float("inf")
|
| 226 |
-
for tier in range(floor, 6):
|
| 227 |
-
cost_norm = TIER_COST[tier] / TIER_COST[5]
|
| 228 |
-
score = mu * (1.0 - p_success[tier]) + (1.0 - mu) * cost_norm
|
| 229 |
-
if score < best_score:
|
| 230 |
-
best_score = score
|
| 231 |
-
best_tier = tier
|
| 232 |
-
|
| 233 |
-
return best_tier, p_success
|
| 234 |
-
|
| 235 |
-
# ─── Evaluate ────────────────────────────────────────────────────────
|
| 236 |
-
print("\n[4] Evaluating all routers on test set...")
|
| 237 |
-
|
| 238 |
-
n_test = len(idx_test)
|
| 239 |
-
results = {}
|
| 240 |
-
|
| 241 |
-
# Helper: evaluate a router function
|
| 242 |
-
def eval_router(name, route_fn):
|
| 243 |
-
succ = 0; cost = 0.0; unsafe = 0; false_done = 0
|
| 244 |
-
tier_dist = defaultdict(int)
|
| 245 |
-
for i in idx_test:
|
| 246 |
-
t = traces[i]
|
| 247 |
-
x = f2v(t["feats"]).reshape(1, -1)
|
| 248 |
-
pred, _ = route_fn(x, t)
|
| 249 |
-
tier_dist[pred] += 1
|
| 250 |
-
if t["tier_out"].get(pred, False):
|
| 251 |
-
succ += 1
|
| 252 |
-
else:
|
| 253 |
-
if pred < t["opt"]:
|
| 254 |
-
unsafe += 1
|
| 255 |
-
if pred >= t["opt"] and not t["tier_out"].get(pred, False):
|
| 256 |
-
false_done += 1
|
| 257 |
-
cost += TIER_COST[pred]
|
| 258 |
-
results[name] = {
|
| 259 |
-
"success": succ/n_test, "avg_cost": cost/n_test,
|
| 260 |
-
"unsafe_rate": unsafe/n_test, "false_done": false_done/n_test,
|
| 261 |
-
"tier_dist": dict(tier_dist),
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
# 1. Always frontier
|
| 265 |
-
eval_router("always_frontier", lambda x, t: (4, {}))
|
| 266 |
-
|
| 267 |
-
# 2. Always cheapest
|
| 268 |
-
eval_router("always_cheap", lambda x, t: (1, {}))
|
| 269 |
-
|
| 270 |
-
# 3. Heuristic (difficulty + 1)
|
| 271 |
-
eval_router("heuristic_diff+1", lambda x, t: (min(t["diff"]+1, 5), {}))
|
| 272 |
-
|
| 273 |
-
# 4. Heuristic (task floor only)
|
| 274 |
-
eval_router("heuristic_floor", lambda x, t: (TASK_FLOOR.get(t["tt"], 3), {}))
|
| 275 |
-
|
| 276 |
-
# 5. CARROT v1 (no safety floors, mu=0.6)
|
| 277 |
-
def carrot_v1(x, t):
|
| 278 |
-
ps = {tier: tier_clfs[tier].predict_proba(x)[0,1] for tier in range(1,6)}
|
| 279 |
-
best = 3; best_s = float("inf")
|
| 280 |
-
for tier in range(1,6):
|
| 281 |
-
s = 0.6*(1-ps[tier]) + 0.4*(TIER_COST[tier]/TIER_COST[5])
|
| 282 |
-
if s < best_s: best_s = s; best = tier
|
| 283 |
-
return best, ps
|
| 284 |
-
eval_router("CARROT_v1_mu0.6", carrot_v1)
|
| 285 |
-
|
| 286 |
-
# 6. Safety-first CARROT (mu=0.7, threshold=0.5)
|
| 287 |
-
def safe_carrot_050(x, t):
|
| 288 |
-
return route_safe_carrot(x, tier_clfs, t["tt"], mu=0.7, success_threshold=0.5)
|
| 289 |
-
eval_router("safe_CARROT_t0.50", safe_carrot_050)
|
| 290 |
-
|
| 291 |
-
# 7. Safety-first CARROT (mu=0.7, threshold=0.6)
|
| 292 |
-
def safe_carrot_060(x, t):
|
| 293 |
-
return route_safe_carrot(x, tier_clfs, t["tt"], mu=0.7, success_threshold=0.6)
|
| 294 |
-
eval_router("safe_CARROT_t0.60", safe_carrot_060)
|
| 295 |
-
|
| 296 |
-
# 8. Safety-first CARROT (mu=0.7, threshold=0.65)
|
| 297 |
-
def safe_carrot_065(x, t):
|
| 298 |
-
return route_safe_carrot(x, tier_clfs, t["tt"], mu=0.7, success_threshold=0.65)
|
| 299 |
-
eval_router("safe_CARROT_t0.65", safe_carrot_065)
|
| 300 |
-
|
| 301 |
-
# 9. Oracle
|
| 302 |
-
eval_router("oracle", lambda x, t: (t["opt"], {}))
|
| 303 |
-
|
| 304 |
-
# Print comparison
|
| 305 |
-
print(f"\n{'Router':<25} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10} {'F-DONE':>10}")
|
| 306 |
-
print("-"*75)
|
| 307 |
-
frontier_cost = results["always_frontier"]["avg_cost"]
|
| 308 |
-
for name, r in sorted(results.items(), key=lambda x: -x[1]["success"]):
|
| 309 |
-
cr = (1 - r["avg_cost"]/frontier_cost)*100
|
| 310 |
-
print(f"{name:<25} {r['success']:>10.3f} {r['avg_cost']:>10.4f} {cr:>9.1f}% {r['unsafe_rate']:>10.3f} {r['false_done']:>10.3f}")
|
| 311 |
-
|
| 312 |
-
# ─── Train Improved Direct Classifier ───────────────────────────────
|
| 313 |
-
print("\n\n[5] Training improved direct classifier (0-indexed)...")
|
| 314 |
-
|
| 315 |
-
y_train_direct = y_opt[idx_train] - 1
|
| 316 |
-
y_test_direct = y_opt[idx_test] - 1
|
| 317 |
-
|
| 318 |
-
# Use sample weights: penalize underprediction more
|
| 319 |
-
from sklearn.utils.class_weight import compute_sample_weight
|
| 320 |
-
|
| 321 |
-
# Custom weight: underkill is 3x worse than overkill
|
| 322 |
-
sample_weights = []
|
| 323 |
-
for i in idx_train:
|
| 324 |
-
t = traces[i]
|
| 325 |
-
opt = t["opt"]
|
| 326 |
-
# Weight by inverse frequency + safety penalty
|
| 327 |
-
sample_weights.append(1.0)
|
| 328 |
-
sample_weights = np.array(sample_weights)
|
| 329 |
-
|
| 330 |
-
direct_clf = xgb.XGBClassifier(
|
| 331 |
-
n_estimators=300, max_depth=6, learning_rate=0.05,
|
| 332 |
-
subsample=0.8, colsample_bytree=0.8,
|
| 333 |
-
objective="multi:softmax", num_class=5,
|
| 334 |
-
eval_metric="mlogloss", random_state=42, verbosity=0,
|
| 335 |
-
)
|
| 336 |
-
direct_clf.fit(X_train, y_train_direct, sample_weight=sample_weights)
|
| 337 |
-
|
| 338 |
-
y_pred_direct = direct_clf.predict(X_test) + 1 # back to 1-indexed
|
| 339 |
-
acc = accuracy_score(y_opt[idx_test], y_pred_direct)
|
| 340 |
-
print(f" Direct classifier accuracy: {acc:.3f}")
|
| 341 |
-
|
| 342 |
-
# Evaluate direct classifier with safety floors
|
| 343 |
-
def direct_safe(x, t):
|
| 344 |
-
pred = int(direct_clf.predict(x)[0]) + 1
|
| 345 |
-
floor = TASK_FLOOR.get(t["tt"], 2)
|
| 346 |
-
return max(pred, floor), {}
|
| 347 |
-
|
| 348 |
-
eval_router("direct_safe_xgb", direct_safe)
|
| 349 |
-
|
| 350 |
-
# ─── Feature Importance ─────────────────────────────────────────────
|
| 351 |
-
print("\n\n[6] Feature importance (from direct classifier)...")
|
| 352 |
-
imp = direct_clf.feature_importances_
|
| 353 |
-
for feat, score in sorted(zip(FEAT_KEYS, imp), key=lambda x: -x[1])[:10]:
|
| 354 |
-
print(f" {feat:<25}: {score:.4f}")
|
| 355 |
-
|
| 356 |
-
# ─── Save Models ────────────────────────────────────────────────────
|
| 357 |
-
print("\n\n[7] Saving models...")
|
| 358 |
-
os.makedirs("/app/router_models", exist_ok=True)
|
| 359 |
-
for tier, clf in tier_clfs.items():
|
| 360 |
-
clf.save_model(f"/app/router_models/tier_{tier}_success.json")
|
| 361 |
-
direct_clf.save_model("/app/router_models/direct_optimal_tier.json")
|
| 362 |
-
with open("/app/router_models/feat_keys.json", "w") as f:
|
| 363 |
-
json.dump(FEAT_KEYS, f)
|
| 364 |
-
with open("/app/router_models/tier_config.json", "w") as f:
|
| 365 |
-
json.dump({"tier_cost": TIER_COST, "tier_str": TIER_STR, "task_floor": TASK_FLOOR}, f, indent=2)
|
| 366 |
-
|
| 367 |
-
# Final print
|
| 368 |
-
print(f"\n\n{'='*80}")
|
| 369 |
-
print("FINAL COMPARISON (ALL ROUTERS)")
|
| 370 |
-
print(f"{'='*80}")
|
| 371 |
-
print(f"\n{'Router':<25} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10} {'F-DONE':>10}")
|
| 372 |
-
print("-"*75)
|
| 373 |
-
frontier_cost = results["always_frontier"]["avg_cost"]
|
| 374 |
-
for name, r in sorted(results.items(), key=lambda x: (-x[1]["success"], x[1]["avg_cost"])):
|
| 375 |
-
cr = (1 - r["avg_cost"]/frontier_cost)*100
|
| 376 |
-
print(f"{name:<25} {r['success']:>10.3f} {r['avg_cost']:>10.4f} {cr:>9.1f}% {r['unsafe_rate']:>10.3f} {r['false_done']:>10.3f}")
|
| 377 |
-
|
| 378 |
-
print(f"\n\nDONE! Models saved to /app/router_models/")
|
| 379 |
-
|
| 380 |
-
# ─── RouteLLM-Style Binary Router ────────────────────────────────────
|
| 381 |
-
print("\n\n[8] Training RouteLLM-style binary classifiers...")
|
| 382 |
-
print(" (For each tier pair, train: should we route to cheaper or more expensive tier?)")
|
| 383 |
-
|
| 384 |
-
# For each tier boundary, train a binary classifier
|
| 385 |
-
# tier_boundary[t] = P(should use tier >= t | query)
|
| 386 |
-
# Route to the first tier where the boundary classifier says "yes, this is enough"
|
| 387 |
-
|
| 388 |
-
boundary_clfs = {}
|
| 389 |
-
for boundary in range(2, 6):
|
| 390 |
-
# Label: 1 if optimal_tier < boundary (cheaper tier is sufficient)
|
| 391 |
-
# 0 if optimal_tier >= boundary (need this tier or higher)
|
| 392 |
-
y_boundary = np.array([1 if traces[i]["opt"] < boundary else 0 for i in range(len(traces))])
|
| 393 |
-
|
| 394 |
-
y_tr = y_boundary[idx_train]
|
| 395 |
-
y_te = y_boundary[idx_test]
|
| 396 |
-
|
| 397 |
-
neg = (y_tr == 0).sum()
|
| 398 |
-
pos = (y_tr == 1).sum()
|
| 399 |
-
spw = neg / max(pos, 1)
|
| 400 |
-
|
| 401 |
-
clf = xgb.XGBClassifier(
|
| 402 |
-
n_estimators=150, max_depth=5, learning_rate=0.1,
|
| 403 |
-
subsample=0.8, colsample_bytree=0.8,
|
| 404 |
-
scale_pos_weight=min(spw, 3.0),
|
| 405 |
-
objective="binary:logistic", eval_metric="logloss",
|
| 406 |
-
random_state=42, verbosity=0,
|
| 407 |
-
)
|
| 408 |
-
clf.fit(X_train, y_tr)
|
| 409 |
-
|
| 410 |
-
y_pred = clf.predict(X_test)
|
| 411 |
-
acc = accuracy_score(y_te, y_pred)
|
| 412 |
-
f1 = f1_score(y_te, y_pred, zero_division=0)
|
| 413 |
-
|
| 414 |
-
boundary_clfs[boundary] = clf
|
| 415 |
-
rate = (y_tr == 0).mean() # fraction that needs this tier
|
| 416 |
-
print(f" Boundary {boundary}: acc={acc:.3f}, f1={f1:.3f}, needs_tier={rate:.3f}")
|
| 417 |
-
|
| 418 |
-
def route_cascade_binary(x, t):
|
| 419 |
-
"""RouteLLM-style cascade: check each boundary, route to first that passes."""
|
| 420 |
-
if x.ndim == 1:
|
| 421 |
-
x = x.reshape(1, -1)
|
| 422 |
-
floor = TASK_FLOOR.get(t["tt"], 2)
|
| 423 |
-
|
| 424 |
-
# Start at floor, check if we need higher
|
| 425 |
-
current_tier = floor
|
| 426 |
-
|
| 427 |
-
for boundary in range(floor + 1, 6):
|
| 428 |
-
# boundary_clfs[boundary] predicts P(optimal < boundary)
|
| 429 |
-
# If P(optimal < boundary) > threshold, we can stay below boundary
|
| 430 |
-
# i.e., if P(need tier >= boundary) > threshold, escalate
|
| 431 |
-
p_need_higher = boundary_clfs[boundary].predict_proba(x)[0, 0] # P(optimal >= boundary)
|
| 432 |
-
if p_need_higher > 0.4: # confidence threshold
|
| 433 |
-
current_tier = boundary
|
| 434 |
-
else:
|
| 435 |
-
break
|
| 436 |
-
|
| 437 |
-
return current_tier, {}
|
| 438 |
-
|
| 439 |
-
eval_router("cascade_binary_t0.4", route_cascade_binary)
|
| 440 |
-
|
| 441 |
-
def route_cascade_binary_t050(x, t):
|
| 442 |
-
if x.ndim == 1: x = x.reshape(1, -1)
|
| 443 |
-
floor = TASK_FLOOR.get(t["tt"], 2)
|
| 444 |
-
current_tier = floor
|
| 445 |
-
for boundary in range(floor + 1, 6):
|
| 446 |
-
p_need = boundary_clfs[boundary].predict_proba(x)[0, 0]
|
| 447 |
-
if p_need > 0.5:
|
| 448 |
-
current_tier = boundary
|
| 449 |
-
else:
|
| 450 |
-
break
|
| 451 |
-
return current_tier, {}
|
| 452 |
-
|
| 453 |
-
eval_router("cascade_binary_t0.5", route_cascade_binary_t050)
|
| 454 |
-
|
| 455 |
-
def route_cascade_binary_t030(x, t):
|
| 456 |
-
if x.ndim == 1: x = x.reshape(1, -1)
|
| 457 |
-
floor = TASK_FLOOR.get(t["tt"], 2)
|
| 458 |
-
current_tier = floor
|
| 459 |
-
for boundary in range(floor + 1, 6):
|
| 460 |
-
p_need = boundary_clfs[boundary].predict_proba(x)[0, 0]
|
| 461 |
-
if p_need > 0.3:
|
| 462 |
-
current_tier = boundary
|
| 463 |
-
else:
|
| 464 |
-
break
|
| 465 |
-
return current_tier, {}
|
| 466 |
-
|
| 467 |
-
eval_router("cascade_binary_t0.3", route_cascade_binary_t030)
|
| 468 |
-
|
| 469 |
-
# Save boundary classifiers
|
| 470 |
-
for boundary, clf in boundary_clfs.items():
|
| 471 |
-
clf.save_model(f"/app/router_models/boundary_{boundary}.json")
|
| 472 |
-
print(f" Saved boundary_{boundary}.json")
|
| 473 |
-
|
| 474 |
-
# ─── Final Final Comparison ───────────────────────────────────────────
|
| 475 |
-
print(f"\n\n{'='*80}")
|
| 476 |
-
print("FINAL COMPARISON v2 (WITH BINARY CASCADE ROUTER)")
|
| 477 |
-
print(f"{'='*80}")
|
| 478 |
-
print(f"\n{'Router':<25} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10} {'F-DONE':>10}")
|
| 479 |
-
print("-"*75)
|
| 480 |
-
frontier_cost = results["always_frontier"]["avg_cost"]
|
| 481 |
-
for name, r in sorted(results.items(), key=lambda x: (-x[1]["success"], x[1]["avg_cost"])):
|
| 482 |
-
cr = (1 - r["avg_cost"]/frontier_cost)*100
|
| 483 |
-
# Only show key results
|
| 484 |
-
if name in ("oracle","always_frontier","heuristic_diff+1","safe_CARROT_t0.60",
|
| 485 |
-
"cascade_binary_t0.4","cascade_binary_t0.5","cascade_binary_t0.3",
|
| 486 |
-
"always_cheap"):
|
| 487 |
-
print(f"{name:<25} {r['success']:>10.3f} {r['avg_cost']:>10.4f} {cr:>9.1f}% {r['unsafe_rate']:>10.3f} {r['false_done']:>10.3f}")
|
| 488 |
-
|
| 489 |
-
# Find best Pareto
|
| 490 |
-
print("\n\nPARETO FRONTIER:")
|
| 491 |
-
pareto = []
|
| 492 |
-
for name, r in results.items():
|
| 493 |
-
if name in ("always_cheap",):
|
| 494 |
-
continue # skip dominated
|
| 495 |
-
dominated = False
|
| 496 |
-
for name2, r2 in results.items():
|
| 497 |
-
if name == name2: continue
|
| 498 |
-
if r2["success"] >= r["success"] and r2["avg_cost"] <= r["avg_cost"]:
|
| 499 |
-
if r2["success"] > r["success"] or r2["avg_cost"] < r["avg_cost"]:
|
| 500 |
-
dominated = True; break
|
| 501 |
-
if not dominated:
|
| 502 |
-
pareto.append((name, r))
|
| 503 |
-
cr = (1 - r["avg_cost"]/frontier_cost)*100
|
| 504 |
-
print(f" {name:<25} success={r['success']:.3f} cost={r['avg_cost']:.4f} costRed={cr:.1f}%")
|
| 505 |
-
|
| 506 |
-
# Save all results
|
| 507 |
-
with open("/app/router_models/eval_results.json", "w") as f:
|
| 508 |
-
json.dump(results, f, indent=2, default=str)
|
| 509 |
-
print(f"\n Saved eval_results.json")
|
| 510 |
-
print(f"\nDONE!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/train_router_v3.py
DELETED
|
@@ -1,262 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Trained Router v3: Combined best approach.
|
| 3 |
-
Uses per-tier P(success) classifiers + safety floors +
|
| 4 |
-
cost-aware routing with ASYMMETRIC penalties (underkill penalized 5x harder than overkill).
|
| 5 |
-
"""
|
| 6 |
-
import json, os, sys, random, pickle, uuid
|
| 7 |
-
import numpy as np
|
| 8 |
-
from datetime import datetime
|
| 9 |
-
from collections import defaultdict
|
| 10 |
-
from typing import Dict, List, Tuple, Any, Optional
|
| 11 |
-
|
| 12 |
-
TASK_TYPES = ["quick_answer","coding","research","document_drafting",
|
| 13 |
-
"legal_regulated","tool_heavy","retrieval_heavy",
|
| 14 |
-
"long_horizon","unknown_ambiguous"]
|
| 15 |
-
TT2IDX = {t:i for i,t in enumerate(TASK_TYPES)}
|
| 16 |
-
|
| 17 |
-
CODE_KW = ["python","javascript","code","function","bug","debug","refactor",
|
| 18 |
-
"implement","test","compile","runtime","class","module","async","thread"]
|
| 19 |
-
LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
|
| 20 |
-
RESEARCH_KW = ["research","find sources","literature","investigate","compare","analyze","survey"]
|
| 21 |
-
TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
|
| 22 |
-
LONG_KW = ["plan","project","roadmap","orchestrate","multi-step","migrate","pipeline","deploy"]
|
| 23 |
-
MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
|
| 24 |
-
|
| 25 |
-
TIER_STR = {1:0.35,2:0.55,3:0.80,4:0.93,5:0.97}
|
| 26 |
-
TIER_COST = {1:0.05,2:0.15,3:0.75,4:1.0,5:1.5}
|
| 27 |
-
TASK_FLOOR = {"legal_regulated":4,"long_horizon":3,"research":3,"coding":3,
|
| 28 |
-
"unknown_ambiguous":3,"quick_answer":1,"document_drafting":2,
|
| 29 |
-
"tool_heavy":2,"retrieval_heavy":2}
|
| 30 |
-
|
| 31 |
-
TASK_TEMPLATES = {
|
| 32 |
-
"quick_answer":["What is the capital of France?","Explain quantum computing briefly.",
|
| 33 |
-
"What is 237*452?","Define photosynthesis.","Who wrote Hamlet?",
|
| 34 |
-
"What is the speed of light?","List the primary colors.","What is GDP?"],
|
| 35 |
-
"coding":["Write a Python function to reverse a linked list.",
|
| 36 |
-
"Fix the bug in this React component.","Refactor auth module to JWT.",
|
| 37 |
-
"Implement LRU cache in Go.","Debug segfault in C++ thread pool.",
|
| 38 |
-
"Add unit tests for the payment module.","Optimize this SQL query.",
|
| 39 |
-
"Create a REST API for user management.","Implement binary search in Rust."],
|
| 40 |
-
"research":["Research latest transformer advances.",
|
| 41 |
-
"Find sources comparing LoRA and full FT.",
|
| 42 |
-
"Investigate data center climate impact.",
|
| 43 |
-
"Survey privacy-preserving ML techniques.",
|
| 44 |
-
"Compare reinforcement learning algorithms for robotics."],
|
| 45 |
-
"document_drafting":["Draft project proposal for ML pipeline.",
|
| 46 |
-
"Write email to team about deployment.","Create technical report on performance."],
|
| 47 |
-
"legal_regulated":["Review this contract for liability clauses.",
|
| 48 |
-
"Check GDPR compliance for data pipeline.","Draft privacy policy section.",
|
| 49 |
-
"Verify regulatory compliance for medical device software."],
|
| 50 |
-
"tool_heavy":["Search open issues and create summary.",
|
| 51 |
-
"Fetch API docs and generate client code.","Query Q3 sales and produce chart."],
|
| 52 |
-
"retrieval_heavy":["Answer based on 50-page document.",
|
| 53 |
-
"Find all payment processing mentions.","Retrieve relevant cases for legal query."],
|
| 54 |
-
"long_horizon":["Plan 3-month roadmap.","Orchestrate multi-region deployment.",
|
| 55 |
-
"Redesign data architecture end-to-end.","Migrate monolith to microservices."],
|
| 56 |
-
"unknown_ambiguous":["Help me with this thing.",
|
| 57 |
-
"I need something about the server.","Can you look into that issue?"],
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
def tsp(tier, diff):
|
| 61 |
-
return TIER_STR[tier] ** (diff * 0.6)
|
| 62 |
-
|
| 63 |
-
def extract_features(request, task_type, difficulty=3):
|
| 64 |
-
r = request.lower()
|
| 65 |
-
f = {
|
| 66 |
-
"req_len": len(request), "num_words": len(request.split()),
|
| 67 |
-
"has_code": int(any(k in r for k in CODE_KW)),
|
| 68 |
-
"n_code": sum(1 for k in CODE_KW if k in r),
|
| 69 |
-
"has_legal": int(any(k in r for k in LEGAL_KW)),
|
| 70 |
-
"n_legal": sum(1 for k in LEGAL_KW if k in r),
|
| 71 |
-
"has_research": int(any(k in r for k in RESEARCH_KW)),
|
| 72 |
-
"n_research": sum(1 for k in RESEARCH_KW if k in r),
|
| 73 |
-
"has_tool": int(any(k in r for k in TOOL_KW)),
|
| 74 |
-
"n_tool": sum(1 for k in TOOL_KW if k in r),
|
| 75 |
-
"has_long": int(any(k in r for k in LONG_KW)),
|
| 76 |
-
"has_math": int(any(k in r for k in MATH_KW)),
|
| 77 |
-
"tt_idx": TT2IDX.get(task_type, 8), "difficulty": difficulty,
|
| 78 |
-
}
|
| 79 |
-
for tt in TASK_TYPES:
|
| 80 |
-
f[f"tt_{tt}"] = int(task_type == tt)
|
| 81 |
-
return f
|
| 82 |
-
|
| 83 |
-
def gen_trace(idx, rng):
|
| 84 |
-
tt = rng.choice(list(TASK_TEMPLATES.keys()))
|
| 85 |
-
diff = {"quick_answer":1,"document_drafting":2,"tool_heavy":2,"retrieval_heavy":2,
|
| 86 |
-
"research":3,"coding":3,"unknown_ambiguous":3,"long_horizon":4,"legal_regulated":5}[tt]
|
| 87 |
-
tier_out = {}
|
| 88 |
-
for t in range(1,6):
|
| 89 |
-
tier_out[t] = rng.random() < tsp(t, diff)
|
| 90 |
-
opt = 5
|
| 91 |
-
for t in range(1,6):
|
| 92 |
-
if tier_out[t]: opt = t; break
|
| 93 |
-
if diff <= 2: actual = rng.choices([1,2,3,4,5],weights=[3,4,2,1,0.5])[0]
|
| 94 |
-
elif diff == 3: actual = rng.choices([1,2,3,4,5],weights=[1,2,4,2,1])[0]
|
| 95 |
-
elif diff == 4: actual = rng.choices([1,2,3,4,5],weights=[0.5,1,2,4,2])[0]
|
| 96 |
-
else: actual = rng.choices([1,2,3,4,5],weights=[0.2,0.5,1,3,4])[0]
|
| 97 |
-
outcome = "success" if tier_out[actual] else "failure"
|
| 98 |
-
req = rng.choice(TASK_TEMPLATES[tt])
|
| 99 |
-
feats = extract_features(req, tt, diff)
|
| 100 |
-
return {"feats":feats,"opt":opt,"actual":actual,"outcome":outcome,
|
| 101 |
-
"tier_out":tier_out,"tt":tt,"diff":diff,"req":req}
|
| 102 |
-
|
| 103 |
-
print("="*80)
|
| 104 |
-
print("AGENT COST OPTIMIZER - TRAINED ROUTER v3")
|
| 105 |
-
print("Asymmetric cost scoring: underkill 5x penalty")
|
| 106 |
-
print("="*80)
|
| 107 |
-
|
| 108 |
-
# ─── Generate ────────────────────────────────────────────────────────
|
| 109 |
-
print("\n[1] Generating 50K training traces...")
|
| 110 |
-
rng = random.Random(42)
|
| 111 |
-
traces = [gen_trace(i, rng) for i in range(50000)]
|
| 112 |
-
FEAT_KEYS = sorted(traces[0]["feats"].keys())
|
| 113 |
-
def f2v(feats):
|
| 114 |
-
return np.array([float(feats.get(k, 0.0)) for k in FEAT_KEYS], dtype=np.float32)
|
| 115 |
-
|
| 116 |
-
X_all = np.array([f2v(t["feats"]) for t in traces])
|
| 117 |
-
y_opt = np.array([t["opt"] for t in traces])
|
| 118 |
-
|
| 119 |
-
per_tier_labels = {}
|
| 120 |
-
for tier in range(1,6):
|
| 121 |
-
per_tier_labels[tier] = np.array([1 if t["tier_out"].get(tier,False) else 0 for t in traces])
|
| 122 |
-
|
| 123 |
-
from sklearn.model_selection import train_test_split
|
| 124 |
-
from sklearn.metrics import accuracy_score, f1_score
|
| 125 |
-
import xgboost as xgb
|
| 126 |
-
|
| 127 |
-
X_train, X_test, idx_train, idx_test = train_test_split(X_all, range(len(traces)), test_size=0.2, random_state=42, stratify=y_opt)
|
| 128 |
-
print(f" Train: {len(X_train)}, Test: {len(X_test)}")
|
| 129 |
-
|
| 130 |
-
# ─── Train Per-Tier Classifiers ─────────────────────────────────────
|
| 131 |
-
print("\n[2] Training per-tier P(success) classifiers...")
|
| 132 |
-
tier_clfs = {}
|
| 133 |
-
for tier in range(1,6):
|
| 134 |
-
y_tr = per_tier_labels[tier][idx_train]
|
| 135 |
-
neg = (y_tr==0).sum(); pos = (y_tr==1).sum()
|
| 136 |
-
spw = neg / max(pos,1)
|
| 137 |
-
clf = xgb.XGBClassifier(n_estimators=150, max_depth=5, learning_rate=0.1,
|
| 138 |
-
subsample=0.8, colsample_bytree=0.8, scale_pos_weight=min(spw,5.0),
|
| 139 |
-
objective="binary:logistic", eval_metric="logloss", random_state=42, verbosity=0)
|
| 140 |
-
clf.fit(X_train, y_tr)
|
| 141 |
-
y_pred = clf.predict(X_test)
|
| 142 |
-
acc = accuracy_score(per_tier_labels[tier][idx_test], y_pred)
|
| 143 |
-
f1 = f1_score(per_tier_labels[tier][idx_test], y_pred, zero_division=0)
|
| 144 |
-
tier_clfs[tier] = clf
|
| 145 |
-
print(f" Tier {tier}: acc={acc:.3f}, f1={f1:.3f}")
|
| 146 |
-
|
| 147 |
-
# ─── Asymmetric Cost Router ──────────────────────────────────────────
|
| 148 |
-
print("\n[3] Building asymmetric cost router...")
|
| 149 |
-
|
| 150 |
-
def route_asymmetric(x, task_type, tier_clfs, underkill_penalty=5.0, overkill_penalty=1.0):
|
| 151 |
-
"""Score each tier with asymmetric penalties.
|
| 152 |
-
|
| 153 |
-
score(tier) = P(failure@tier) * underkill_penalty * cost_of_failure
|
| 154 |
-
+ cost(tier) * overkill_penalty
|
| 155 |
-
|
| 156 |
-
Underkill (routing too low) is penalized 5x more than overkill.
|
| 157 |
-
"""
|
| 158 |
-
if x.ndim == 1:
|
| 159 |
-
x = x.reshape(1, -1)
|
| 160 |
-
floor = TASK_FLOOR.get(task_type, 2)
|
| 161 |
-
|
| 162 |
-
best_tier = floor
|
| 163 |
-
best_score = float("inf")
|
| 164 |
-
|
| 165 |
-
for tier in range(floor, 6):
|
| 166 |
-
p_fail = 1.0 - tier_clfs[tier].predict_proba(x)[0, 1]
|
| 167 |
-
cost_norm = TIER_COST[tier] / TIER_COST[5] # [0.03, 1.0]
|
| 168 |
-
|
| 169 |
-
# Expected cost of failure (cheap model on hard task)
|
| 170 |
-
failure_cost = p_fail * underkill_penalty
|
| 171 |
-
|
| 172 |
-
# Cost of using this tier (overkill penalty)
|
| 173 |
-
tier_cost = cost_norm * overkill_penalty
|
| 174 |
-
|
| 175 |
-
score = failure_cost + tier_cost
|
| 176 |
-
|
| 177 |
-
if score < best_score:
|
| 178 |
-
best_score = score
|
| 179 |
-
best_tier = tier
|
| 180 |
-
|
| 181 |
-
return best_tier
|
| 182 |
-
|
| 183 |
-
# ─── Evaluate ─────────────────────────────────────────────────────────
|
| 184 |
-
print("\n[4] Evaluating all routers...")
|
| 185 |
-
n_test = len(idx_test)
|
| 186 |
-
results = {}
|
| 187 |
-
|
| 188 |
-
def eval_router(name, route_fn):
|
| 189 |
-
succ = 0; cost = 0.0; unsafe = 0; fd = 0
|
| 190 |
-
td = defaultdict(int)
|
| 191 |
-
for i in idx_test:
|
| 192 |
-
t = traces[i]
|
| 193 |
-
x = f2v(t["feats"]).reshape(1,-1)
|
| 194 |
-
pred = route_fn(x, t)
|
| 195 |
-
td[pred] += 1
|
| 196 |
-
if t["tier_out"].get(pred, False): succ += 1
|
| 197 |
-
elif pred < t["opt"]: unsafe += 1
|
| 198 |
-
elif pred >= t["opt"]: fd += 1
|
| 199 |
-
cost += TIER_COST[pred]
|
| 200 |
-
results[name] = {"success":succ/n_test, "avg_cost":cost/n_test,
|
| 201 |
-
"unsafe_rate":unsafe/n_test, "false_done":fd/n_test,
|
| 202 |
-
"tier_dist":dict(td)}
|
| 203 |
-
|
| 204 |
-
eval_router("always_frontier", lambda x,t: 4)
|
| 205 |
-
eval_router("always_cheap", lambda x,t: 1)
|
| 206 |
-
eval_router("heuristic_diff+1", lambda x,t: min(t["diff"]+1,5))
|
| 207 |
-
eval_router("heuristic_floor", lambda x,t: TASK_FLOOR.get(t["tt"],2))
|
| 208 |
-
|
| 209 |
-
for ukp in [3.0, 5.0, 8.0, 10.0, 15.0]:
|
| 210 |
-
eval_router(f"asymmetric_uk{ukp:.0f}", lambda x,t,uk=ukp: route_asymmetric(x, t["tt"], tier_clfs, underkill_penalty=uk))
|
| 211 |
-
|
| 212 |
-
eval_router("oracle", lambda x,t: t["opt"])
|
| 213 |
-
|
| 214 |
-
# Print comparison
|
| 215 |
-
print(f"\n{'Router':<25} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10} {'F-DONE':>10}")
|
| 216 |
-
print("-"*75)
|
| 217 |
-
fc = results["always_frontier"]["avg_cost"]
|
| 218 |
-
for name, r in sorted(results.items(), key=lambda x: (-x[1]["success"], x[1]["avg_cost"])):
|
| 219 |
-
cr = (1 - r["avg_cost"]/fc)*100
|
| 220 |
-
print(f"{name:<25} {r['success']:>10.3f} {r['avg_cost']:>10.4f} {cr:>9.1f}% {r['unsafe_rate']:>10.3f} {r['false_done']:>10.3f}")
|
| 221 |
-
|
| 222 |
-
# Pareto frontier
|
| 223 |
-
print("\nPARETO FRONTIER:")
|
| 224 |
-
pareto = []
|
| 225 |
-
for name, r in results.items():
|
| 226 |
-
if name == "always_cheap": continue
|
| 227 |
-
dominated = False
|
| 228 |
-
for name2, r2 in results.items():
|
| 229 |
-
if name == name2: continue
|
| 230 |
-
if r2["success"] >= r["success"] and r2["avg_cost"] <= r["avg_cost"]:
|
| 231 |
-
if r2["success"] > r["success"] or r2["avg_cost"] < r["avg_cost"]:
|
| 232 |
-
dominated = True; break
|
| 233 |
-
if not dominated:
|
| 234 |
-
pareto.append((name, r))
|
| 235 |
-
cr = (1 - r["avg_cost"]/fc)*100
|
| 236 |
-
print(f" {name:<25} success={r['success']:.3f} cost={r['avg_cost']:.4f} costRed={cr:.1f}% unsafe={r['unsafe_rate']:.3f}")
|
| 237 |
-
|
| 238 |
-
# ─── Save Best Model ─────────────────────────────────────────────────
|
| 239 |
-
print("\n[5] Saving models...")
|
| 240 |
-
os.makedirs("/app/router_models", exist_ok=True)
|
| 241 |
-
for tier, clf in tier_clfs.items():
|
| 242 |
-
clf.save_model(f"/app/router_models/tier_{tier}_success.json")
|
| 243 |
-
with open("/app/router_models/feat_keys.json","w") as f:
|
| 244 |
-
json.dump(FEAT_KEYS, f)
|
| 245 |
-
with open("/app/router_models/tier_config.json","w") as f:
|
| 246 |
-
json.dump({"tier_cost":TIER_COST,"tier_str":TIER_STR,"task_floor":TASK_FLOOR}, f, indent=2)
|
| 247 |
-
with open("/app/router_models/eval_results_v3.json","w") as f:
|
| 248 |
-
json.dump(results, f, indent=2, default=str)
|
| 249 |
-
print(f" Saved all models to /app/router_models/")
|
| 250 |
-
|
| 251 |
-
# Find best config
|
| 252 |
-
best_name = ""
|
| 253 |
-
best_score = -float("inf")
|
| 254 |
-
for name, r in results.items():
|
| 255 |
-
if name in ("oracle","always_cheap"): continue
|
| 256 |
-
# Composite: success*20 - cost*50 - unsafe*100
|
| 257 |
-
score = r["success"]*20 - r["avg_cost"]*50 - r["unsafe_rate"]*100
|
| 258 |
-
if score > best_score:
|
| 259 |
-
best_score = score
|
| 260 |
-
best_name = name
|
| 261 |
-
print(f"\n BEST CONFIG: {best_name} (composite score: {best_score:.2f})")
|
| 262 |
-
print(f"\nDONE!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/train_router_v4_production.py
DELETED
|
@@ -1,241 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Production Trained Router v4: Difficulty-first + ML refinement.
|
| 3 |
-
|
| 4 |
-
Architecture:
|
| 5 |
-
1. Predict difficulty (task_type -> difficulty mapping + ML difficulty classifier)
|
| 6 |
-
2. Convert difficulty to base_tier (difficulty + 1, capped at 5)
|
| 7 |
-
3. Apply safety floor per task_type
|
| 8 |
-
4. Use per-tier P(success) classifiers to CONFIRM or ESCALATE
|
| 9 |
-
5. If P(success@base_tier) < 0.65, escalate to next tier
|
| 10 |
-
|
| 11 |
-
This combines the best of both worlds:
|
| 12 |
-
- Difficulty mapping (heuristic) is reliable and interpretable
|
| 13 |
-
- ML classifiers add a safety net: they catch cases where difficulty is underestimated
|
| 14 |
-
- Safety floors prevent dangerous under-routing on legal/critical tasks
|
| 15 |
-
"""
|
| 16 |
-
import json, os, sys, random, uuid
|
| 17 |
-
import numpy as np
|
| 18 |
-
from datetime import datetime
|
| 19 |
-
from collections import defaultdict
|
| 20 |
-
from typing import Dict, List, Optional, Any
|
| 21 |
-
|
| 22 |
-
import xgboost as xgb
|
| 23 |
-
|
| 24 |
-
# ─── Load Models ──────────────────────────────────────────────────────
|
| 25 |
-
MODEL_DIR = "/app/router_models"
|
| 26 |
-
feat_keys = json.load(open(f"{MODEL_DIR}/feat_keys.json"))
|
| 27 |
-
tier_config = json.load(open(f"{MODEL_DIR}/tier_config.json"))
|
| 28 |
-
TIER_COST = {int(k):v for k,v in tier_config["tier_cost"].items()}
|
| 29 |
-
TIER_STR = {int(k):v for k,v in tier_config["tier_str"].items()}
|
| 30 |
-
TASK_FLOOR = tier_config["task_floor"]
|
| 31 |
-
|
| 32 |
-
tier_clfs = {}
|
| 33 |
-
for tier in range(1, 6):
|
| 34 |
-
clf = xgb.XGBClassifier()
|
| 35 |
-
clf.load_model(f"{MODEL_DIR}/tier_{tier}_success.json")
|
| 36 |
-
tier_clfs[tier] = clf
|
| 37 |
-
|
| 38 |
-
# ─── Feature Extraction ────────────────────────────────────────────────
|
| 39 |
-
TASK_TYPES = ["quick_answer","coding","research","document_drafting",
|
| 40 |
-
"legal_regulated","tool_heavy","retrieval_heavy",
|
| 41 |
-
"long_horizon","unknown_ambiguous"]
|
| 42 |
-
TT2IDX = {t:i for i,t in enumerate(TASK_TYPES)}
|
| 43 |
-
|
| 44 |
-
CODE_KW = ["python","javascript","code","function","bug","debug","refactor",
|
| 45 |
-
"implement","test","compile","runtime","class","module","async","thread"]
|
| 46 |
-
LEGAL_KW = ["contract","legal","compliance","gdpr","privacy","policy","regulatory","liability"]
|
| 47 |
-
RESEARCH_KW = ["research","find sources","literature","investigate","compare","analyze","survey"]
|
| 48 |
-
TOOL_KW = ["search","fetch","retrieve","query","api","database","scrape","aggregate"]
|
| 49 |
-
LONG_KW = ["plan","project","roadmap","orchestrate","multi-step","migrate","pipeline","deploy"]
|
| 50 |
-
MATH_KW = ["calculate","compute","solve","equation","formula","optimize","probability"]
|
| 51 |
-
|
| 52 |
-
def extract_features(request, task_type, difficulty=3):
|
| 53 |
-
r = request.lower()
|
| 54 |
-
f = {"req_len":len(request),"num_words":len(request.split()),
|
| 55 |
-
"has_code":int(any(k in r for k in CODE_KW)),
|
| 56 |
-
"n_code":sum(1 for k in CODE_KW if k in r),
|
| 57 |
-
"has_legal":int(any(k in r for k in LEGAL_KW)),
|
| 58 |
-
"n_legal":sum(1 for k in LEGAL_KW if k in r),
|
| 59 |
-
"has_research":int(any(k in r for k in RESEARCH_KW)),
|
| 60 |
-
"n_research":sum(1 for k in RESEARCH_KW if k in r),
|
| 61 |
-
"has_tool":int(any(k in r for k in TOOL_KW)),
|
| 62 |
-
"n_tool":sum(1 for k in TOOL_KW if k in r),
|
| 63 |
-
"has_long":int(any(k in r for k in LONG_KW)),
|
| 64 |
-
"has_math":int(any(k in r for k in MATH_KW)),
|
| 65 |
-
"tt_idx":TT2IDX.get(task_type,8),"difficulty":difficulty}
|
| 66 |
-
for tt in TASK_TYPES:
|
| 67 |
-
f[f"tt_{tt}"] = int(task_type == tt)
|
| 68 |
-
return f
|
| 69 |
-
|
| 70 |
-
def f2v(feats):
|
| 71 |
-
return np.array([float(feats.get(k,0.0)) for k in feat_keys], dtype=np.float32)
|
| 72 |
-
|
| 73 |
-
# ─── Production Router ─────────────────────────────────────────────────
|
| 74 |
-
def route_production(request, task_type, difficulty, escalation_threshold=0.65):
|
| 75 |
-
"""Production router: difficulty-first + ML confirmation.
|
| 76 |
-
|
| 77 |
-
Step 1: difficulty -> base_tier (difficulty + 1, capped at 5)
|
| 78 |
-
Step 2: Apply safety floor (task_type -> minimum tier)
|
| 79 |
-
Step 3: base_tier = max(base_tier, safety_floor)
|
| 80 |
-
Step 4: If P(success@base_tier) < escalation_threshold, escalate one tier
|
| 81 |
-
Step 5: Never exceed tier 5
|
| 82 |
-
"""
|
| 83 |
-
base_tier = min(difficulty + 1, 5)
|
| 84 |
-
floor = TASK_FLOOR.get(task_type, 2)
|
| 85 |
-
base_tier = max(base_tier, floor)
|
| 86 |
-
|
| 87 |
-
# ML confirmation: check if base_tier is likely to succeed
|
| 88 |
-
feats = extract_features(request, task_type, difficulty)
|
| 89 |
-
x = f2v(feats).reshape(1, -1)
|
| 90 |
-
|
| 91 |
-
p_success = tier_clfs[base_tier].predict_proba(x)[0, 1]
|
| 92 |
-
|
| 93 |
-
# If P(success) at base_tier is too low, escalate
|
| 94 |
-
while p_success < escalation_threshold and base_tier < 5:
|
| 95 |
-
base_tier += 1
|
| 96 |
-
p_success = tier_clfs[base_tier].predict_proba(x)[0, 1]
|
| 97 |
-
|
| 98 |
-
return base_tier
|
| 99 |
-
|
| 100 |
-
# ─── Generate Evaluation ────────────────────────────────────────────────
|
| 101 |
-
TASK_TEMPLATES_EVAL = {
|
| 102 |
-
"quick_answer":["What is the capital of France?","Explain quantum computing briefly.",
|
| 103 |
-
"What is 237*452?","Define photosynthesis.","Who wrote Hamlet?"],
|
| 104 |
-
"coding":["Write a Python function to reverse a linked list.",
|
| 105 |
-
"Fix the bug in this React component.","Refactor auth module to JWT.",
|
| 106 |
-
"Implement LRU cache in Go.","Debug segfault in C++ thread pool."],
|
| 107 |
-
"research":["Research latest transformer advances.",
|
| 108 |
-
"Find sources comparing LoRA and full FT.",
|
| 109 |
-
"Investigate data center climate impact."],
|
| 110 |
-
"document_drafting":["Draft project proposal for ML pipeline.",
|
| 111 |
-
"Write email to team about deployment.","Create technical report on performance."],
|
| 112 |
-
"legal_regulated":["Review this contract for liability clauses.",
|
| 113 |
-
"Check GDPR compliance for data pipeline.","Draft privacy policy section."],
|
| 114 |
-
"tool_heavy":["Search open issues and create summary.",
|
| 115 |
-
"Fetch API docs and generate client code.","Query Q3 sales and produce chart."],
|
| 116 |
-
"retrieval_heavy":["Answer based on 50-page document.",
|
| 117 |
-
"Find all payment processing mentions.","Retrieve relevant cases for legal query."],
|
| 118 |
-
"long_horizon":["Plan 3-month roadmap.","Orchestrate multi-region deployment.",
|
| 119 |
-
"Redesign data architecture end-to-end."],
|
| 120 |
-
"unknown_ambiguous":["Help me with this thing.",
|
| 121 |
-
"I need something about the server.","Can you look into that issue?"],
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
def tsp(tier, diff):
|
| 125 |
-
s = {1:0.35,2:0.55,3:0.80,4:0.93,5:0.97}[tier]
|
| 126 |
-
return s ** (diff * 0.6)
|
| 127 |
-
|
| 128 |
-
print("="*80)
|
| 129 |
-
print("PRODUCTION TRAINED ROUTER v4 BENCHMARK")
|
| 130 |
-
print("="*80)
|
| 131 |
-
print("\nArchitecture: difficulty-first + ML confirmation + safety floors")
|
| 132 |
-
|
| 133 |
-
print("\n[1] Generating 2K eval traces (seed=999)...")
|
| 134 |
-
eval_rng = random.Random(999)
|
| 135 |
-
eval_traces = []
|
| 136 |
-
for i in range(2000):
|
| 137 |
-
tt = eval_rng.choice(list(TASK_TEMPLATES_EVAL.keys()))
|
| 138 |
-
diff = {"quick_answer":1,"document_drafting":2,"tool_heavy":2,"retrieval_heavy":2,
|
| 139 |
-
"research":3,"coding":3,"unknown_ambiguous":3,"long_horizon":4,"legal_regulated":5}[tt]
|
| 140 |
-
tier_out = {t: eval_rng.random() < tsp(t, diff) for t in range(1,6)}
|
| 141 |
-
opt = 5
|
| 142 |
-
for t in range(1,6):
|
| 143 |
-
if tier_out[t]: opt = t; break
|
| 144 |
-
req = eval_rng.choice(TASK_TEMPLATES_EVAL[tt])
|
| 145 |
-
eval_traces.append({"tt":tt,"diff":diff,"opt":opt,"tier_out":tier_out,"req":req})
|
| 146 |
-
print(f" Generated {len(eval_traces)} traces")
|
| 147 |
-
|
| 148 |
-
# ─── Evaluate ──────────────────────────────────────────────────────────
|
| 149 |
-
print("\n[2] Evaluating all routers...")
|
| 150 |
-
|
| 151 |
-
def eval_router(name, route_fn):
|
| 152 |
-
succ=0; cost=0.0; unsafe=0; fd=0; td=defaultdict(int)
|
| 153 |
-
for t in eval_traces:
|
| 154 |
-
pred = route_fn(t)
|
| 155 |
-
td[pred] += 1
|
| 156 |
-
if t["tier_out"].get(pred, False): succ += 1
|
| 157 |
-
elif pred < t["opt"]: unsafe += 1
|
| 158 |
-
else: fd += 1
|
| 159 |
-
cost += TIER_COST[pred]
|
| 160 |
-
n = len(eval_traces)
|
| 161 |
-
return {"success":succ/n, "avg_cost":cost/n, "unsafe_rate":unsafe/n,
|
| 162 |
-
"false_done":fd/n, "tier_dist":dict(td)}
|
| 163 |
-
|
| 164 |
-
# Baselines
|
| 165 |
-
results = {}
|
| 166 |
-
results["always_frontier"] = eval_router("always_frontier", lambda t: 4)
|
| 167 |
-
results["always_cheap"] = eval_router("always_cheap", lambda t: 1)
|
| 168 |
-
results["heuristic_diff+1"] = eval_router("heuristic_diff+1", lambda t: min(t["diff"]+1, 5))
|
| 169 |
-
results["heuristic_floor"] = eval_router("heuristic_floor", lambda t: TASK_FLOOR.get(t["tt"], 2))
|
| 170 |
-
results["oracle"] = eval_router("oracle", lambda t: t["opt"])
|
| 171 |
-
|
| 172 |
-
# Production router at different escalation thresholds
|
| 173 |
-
for threshold in [0.50, 0.55, 0.60, 0.65, 0.70, 0.75]:
|
| 174 |
-
name = f"prod_t{threshold:.2f}"
|
| 175 |
-
results[name] = eval_router(name,
|
| 176 |
-
lambda t, th=threshold: route_production(t["req"], t["tt"], t["diff"], escalation_threshold=th))
|
| 177 |
-
|
| 178 |
-
# Print comparison
|
| 179 |
-
print(f"\n{'Router':<25} {'Success':>10} {'AvgCost':>10} {'CostRed':>10} {'Unsafe':>10} {'F-DONE':>10}")
|
| 180 |
-
print("-"*75)
|
| 181 |
-
fc = results["always_frontier"]["avg_cost"]
|
| 182 |
-
for name, r in sorted(results.items(), key=lambda x: (-x[1]["success"], x[1]["avg_cost"])):
|
| 183 |
-
cr = (1 - r["avg_cost"]/fc)*100
|
| 184 |
-
print(f"{name:<25} {r['success']:>10.3f} {r['avg_cost']:>10.4f} {cr:>9.1f}% {r['unsafe_rate']:>10.3f} {r['false_done']:>10.3f}")
|
| 185 |
-
|
| 186 |
-
# Find best production config
|
| 187 |
-
print("\n\n[3] Finding best production config...")
|
| 188 |
-
best_name = ""
|
| 189 |
-
best_score = -float("inf")
|
| 190 |
-
for name, r in results.items():
|
| 191 |
-
if name in ("oracle","always_cheap"): continue
|
| 192 |
-
# Composite: maximize success, minimize cost and unsafe
|
| 193 |
-
score = r["success"]*20 - r["avg_cost"]*30 - r["unsafe_rate"]*100
|
| 194 |
-
if score > best_score:
|
| 195 |
-
best_score = score
|
| 196 |
-
best_name = name
|
| 197 |
-
|
| 198 |
-
print(f" Best: {best_name} (composite: {best_score:.2f})")
|
| 199 |
-
print(f" Success: {results[best_name]['success']:.3f}")
|
| 200 |
-
print(f" Avg cost: {results[best_name]['avg_cost']:.4f}")
|
| 201 |
-
fc_val = results["always_frontier"]["avg_cost"]
|
| 202 |
-
cr = (1 - results[best_name]["avg_cost"]/fc_val)*100
|
| 203 |
-
print(f" Cost reduction vs frontier: {cr:.1f}%")
|
| 204 |
-
print(f" Unsafe rate: {results[best_name]['unsafe_rate']:.3f}")
|
| 205 |
-
print(f" False-DONE rate: {results[best_name]['false_done']:.3f}")
|
| 206 |
-
|
| 207 |
-
# Per-task breakdown for best
|
| 208 |
-
print(f"\n\n[4] Per-task breakdown for {best_name}...")
|
| 209 |
-
for tt in sorted(set(t["tt"] for t in eval_traces)):
|
| 210 |
-
tt_traces = [t for t in eval_traces if t["tt"] == tt]
|
| 211 |
-
n_tt = len(tt_traces)
|
| 212 |
-
if n_tt == 0: continue
|
| 213 |
-
|
| 214 |
-
for rname, rfn in [("frontier", lambda t:4),
|
| 215 |
-
("heuristic", lambda t:min(t["diff"]+1,5)),
|
| 216 |
-
("trained", lambda t:route_production(t["req"],t["tt"],t["diff"],
|
| 217 |
-
escalation_threshold=float(best_name.split("t")[1]))),
|
| 218 |
-
("oracle", lambda t:t["opt"])]:
|
| 219 |
-
succ = sum(1 for t in tt_traces if t["tier_out"].get(rfn(t), False))
|
| 220 |
-
cost = sum(TIER_COST[rfn(t)] for t in tt_traces)
|
| 221 |
-
sr = succ/n_tt; ac = cost/n_tt
|
| 222 |
-
if rname == "frontier":
|
| 223 |
-
print(f"\n {tt} (n={n_tt}):")
|
| 224 |
-
cr = (1 - ac/fc_val)*100
|
| 225 |
-
print(f" {rname:<12} success={sr:.3f} cost={ac:.4f} costRed={cr:.1f}%")
|
| 226 |
-
|
| 227 |
-
# Pareto
|
| 228 |
-
print("\n\nPARETO FRONTIER:")
|
| 229 |
-
for name, r in results.items():
|
| 230 |
-
if name == "always_cheap": continue
|
| 231 |
-
dominated = False
|
| 232 |
-
for name2, r2 in results.items():
|
| 233 |
-
if name == name2: continue
|
| 234 |
-
if r2["success"] >= r["success"] and r2["avg_cost"] <= r["avg_cost"]:
|
| 235 |
-
if r2["success"] > r["success"] or r2["avg_cost"] < r["avg_cost"]:
|
| 236 |
-
dominated = True; break
|
| 237 |
-
if not dominated:
|
| 238 |
-
cr = (1 - r["avg_cost"]/fc_val)*100
|
| 239 |
-
print(f" {name:<25} success={r['success']:.3f} cost={r['avg_cost']:.4f} costRed={cr:.1f}% unsafe={r['unsafe_rate']:.3f}")
|
| 240 |
-
|
| 241 |
-
print(f"\nDONE!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|