Skip to content

Commit 8482fdf

Browse files
feat(libsy): support ordered cutoffs for N-way decision routing
Signed-off-by: nachiketb <nachiketb@nvidia.com>
1 parent 4cbb2d1 commit 8482fdf

6 files changed

Lines changed: 260 additions & 64 deletions

File tree

‎crates/libsy-llm-client/src/run.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,7 @@ mod tests {
13051305
LlmClassifierConfig::Capability {
13061306
config: TaskClassifierConfig {
13071307
judge: CapabilityJudgeConfig::Decision(DecisionJudgeConfig {
1308+
policy: None,
13081309
cutoff: 0.4,
13091310
instructions: None,
13101311
candidates: BTreeMap::from([

‎crates/libsy/src/algorithms/llm_class.rs‎

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use switchyard_protocol::{Category, ContentBlock, Message, Role};
1313

1414
mod decision;
1515
use decision::DecisionClassifier;
16-
pub use decision::DecisionJudgeConfig;
16+
pub use decision::{DecisionJudgeConfig, DecisionPolicy, DecisionRule};
1717

1818
use super::escalation;
1919
use super::fall_through::FallThrough;
@@ -1294,6 +1294,7 @@ mod tests {
12941294

12951295
let settings = DecisionJudgeConfig {
12961296
cutoff: 0.4,
1297+
policy: None,
12971298
instructions: None,
12981299
candidates: BTreeMap::from([
12991300
("a".into(), "capable".into()),
@@ -1306,6 +1307,21 @@ mod tests {
13061307
"summaries": [],
13071308
}),
13081309
};
1310+
let ordered_policy = DecisionPolicy {
1311+
rules: vec![
1312+
DecisionRule {
1313+
score: "a".into(),
1314+
target: "a".into(),
1315+
cutoff: 0.6,
1316+
},
1317+
DecisionRule {
1318+
score: "c".into(),
1319+
target: "c".into(),
1320+
cutoff: 0.2,
1321+
},
1322+
],
1323+
fallback: "b".into(),
1324+
};
13091325
let mut request = classify_session_request();
13101326
request.llm_request.messages.push(Message {
13111327
role: Role::Assistant,
@@ -1344,8 +1360,19 @@ mod tests {
13441360
("provider error", 0.0, false, None),
13451361
("dropped reply", 0.0, true, Some("capable")),
13461362
("missing candidate", 0.0, true, None),
1363+
("ordered first", 0.65, true, Some("capable")),
1364+
("ordered second", 0.6, false, Some("third")),
1365+
("ordered fallback", 0.6, false, Some("efficient")),
1366+
("ordered no distribution", 0.0, true, Some("efficient")),
1367+
("ordered provider error", 0.0, true, Some("efficient")),
1368+
("ordered provider error", 0.0, false, None),
1369+
("ordered invalid", f64::NAN, true, Some("efficient")),
13471370
] {
13481371
let mut config = settings.clone();
1372+
let ordered = name.starts_with("ordered");
1373+
if ordered {
1374+
config.policy = Some(ordered_policy.clone());
1375+
}
13491376
if name == "missing candidate" {
13501377
config.candidates.remove("a");
13511378
}
@@ -1378,15 +1405,15 @@ mod tests {
13781405
calls.fetch_add(1, Ordering::SeqCst);
13791406
assert_eq!(call.model, "judge");
13801407
assert_eq!(call.request.model, Some("judge".into()));
1381-
assert_eq!(
1382-
call.request.context,
1383-
json!({
1384-
"task": expected_task,
1385-
"candidates": ["a", "b", "c"],
1386-
"comparison": {"capable": "a", "efficient": "b"},
1387-
"evidence": settings.evidence,
1388-
})
1389-
);
1408+
let mut context = json!({
1409+
"task": expected_task,
1410+
"candidates": ["a", "b", "c"],
1411+
"evidence": settings.evidence,
1412+
});
1413+
if !ordered {
1414+
context["comparison"] = json!({"capable": "a", "efficient": "b"});
1415+
}
1416+
assert_eq!(call.request.context, context);
13901417
assert_eq!(call.request.questions.len(), 1);
13911418
let question = &call.request.questions["route"];
13921419
let switchyard_protocol::DecisionKind::Choice { options } = &question.kind
@@ -1398,16 +1425,20 @@ mod tests {
13981425
.iter()
13991426
.map(|option| option.id.as_str())
14001427
.collect::<Vec<_>>(),
1401-
["advantage", "no_advantage"]
1428+
if ordered {
1429+
vec!["a", "b", "c"]
1430+
} else {
1431+
vec!["advantage", "no_advantage"]
1432+
}
14021433
);
14031434
if let Some(instructions) = override_instructions {
14041435
assert_eq!(&question.instructions, instructions);
1405-
} else {
1436+
} else if !ordered {
14061437
for key in ["setting", "evidence", "comparison", "boundary", "policy"] {
14071438
assert!(question.instructions[key].as_str().is_some());
14081439
}
14091440
}
1410-
if name == "provider error" {
1441+
if name.ends_with("provider error") {
14111442
return call
14121443
.respond(std::future::ready(Err(LibsyError::client_call(
14131444
"judge",
@@ -1423,8 +1454,16 @@ mod tests {
14231454
DecisionValue::Boolean(BooleanEstimate::Value(true))
14241455
} else {
14251456
DecisionValue::Choice {
1426-
selected: "no_advantage".into(),
1427-
probabilities: (name != "no distribution").then(|| {
1457+
selected: if ordered { "b" } else { "no_advantage" }.into(),
1458+
probabilities: (!name.ends_with("no distribution")).then(|| {
1459+
if ordered {
1460+
let third = if name == "ordered fallback" { 0.2 } else { 0.3 };
1461+
return BTreeMap::from([
1462+
("a".into(), Probability(score)),
1463+
("b".into(), Probability(1.0 - score - third)),
1464+
("c".into(), Probability(third)),
1465+
]);
1466+
}
14281467
BTreeMap::from([
14291468
("advantage".into(), Probability(score)),
14301469
("no_advantage".into(), Probability(1.0 - score)),
@@ -1452,7 +1491,16 @@ mod tests {
14521491
.await
14531492
}
14541493
};
1455-
let models = Arc::new(RuntimeModels::new(runtime_models()));
1494+
let mut runtime = runtime_models();
1495+
if ordered {
1496+
runtime.remove(&Category::Capable);
1497+
runtime.remove(&Category::Efficient);
1498+
runtime
1499+
.get_mut(&Category::Any)
1500+
.unwrap()
1501+
.push("third".into());
1502+
}
1503+
let models = Arc::new(RuntimeModels::new(runtime));
14561504
let result = drive(router.clone(), request.clone(), models.clone(), &serve).await;
14571505
assert_eq!(
14581506
calls.load(Ordering::SeqCst),
@@ -1480,14 +1528,19 @@ mod tests {
14801528
.metadata
14811529
.and_then(|metadata| metadata.evidence)
14821530
.expect("routing evidence");
1483-
if matches!(name, "above" | "equal" | "below") {
1531+
if matches!(
1532+
name,
1533+
"ordered first" | "ordered second" | "ordered fallback"
1534+
) {
1535+
assert_eq!(evidence["verdict"], "ordered_cutoffs");
1536+
} else if matches!(name, "above" | "equal" | "below") {
14841537
assert_eq!(evidence["source"], "decision_classifier");
14851538
assert_eq!(evidence["verdict"], "relative_advantage");
14861539
assert_eq!(evidence["threshold"], settings.cutoff);
14871540
assert_eq!(evidence["score"], score);
14881541
} else {
14891542
let reason = match name {
1490-
"provider error" => "client_error",
1543+
name if name.ends_with("provider error") => "client_error",
14911544
"dropped reply" => "call_error",
14921545
_ => "invalid_verdict",
14931546
};
@@ -1504,9 +1557,18 @@ mod tests {
15041557
"affinity should skip the judge: {name}"
15051558
);
15061559
}
1507-
for cutoff in [-0.1, 1.1, f64::NAN] {
1560+
for (cutoff, ordered) in [-0.1, 1.1, f64::NAN]
1561+
.into_iter()
1562+
.flat_map(|cutoff| [(cutoff, false), (cutoff, true)])
1563+
{
15081564
let mut config = settings.clone();
1509-
config.cutoff = cutoff;
1565+
if ordered {
1566+
let mut policy = ordered_policy.clone();
1567+
policy.rules[0].cutoff = cutoff;
1568+
config.policy = Some(policy);
1569+
} else {
1570+
config.cutoff = cutoff;
1571+
}
15101572
assert!(
15111573
LlmTaskClassifier::new(LlmClassifierConfig::Capability {
15121574
config: TaskClassifierConfig {
@@ -1517,6 +1579,18 @@ mod tests {
15171579
.is_err()
15181580
);
15191581
}
1582+
for field in ["score", "target", "fallback", "rules"] {
1583+
let mut config = settings.clone();
1584+
let mut policy = ordered_policy.clone();
1585+
match field {
1586+
"score" => policy.rules[0].score = "missing".into(),
1587+
"target" => policy.rules[0].target = "missing".into(),
1588+
"fallback" => policy.fallback = "missing".into(),
1589+
_ => policy.rules.clear(),
1590+
}
1591+
config.policy = Some(policy);
1592+
assert!(config.validate().is_err());
1593+
}
15201594
let mut duplicate = settings.clone();
15211595
duplicate
15221596
.candidates

0 commit comments

Comments
 (0)