use std::collections::BTreeMap;
use serde::Deserialize;
use tracing::warn;
use super::jev::{ABSTAIN_CODES, JEV_MODEL, QUESTION};
use crate::classify::tiers::llm_prompt::{LlmCall, LlmUsage};
use crate::classify::tiers::ClassificationResult;
use crate::core::models::ClassificationMethod;
#[derive(Deserialize)]
struct JevResponse {
answers: BTreeMap<String, JevAnswer>,
}
#[derive(Deserialize)]
struct JevAnswer {
#[serde(rename = "type")]
kind: String,
choice: String,
probabilities: BTreeMap<String, f64>,
}
#[derive(Deserialize)]
struct JevUsage {
input_tokens: u64,
output_tokens: u64,
}
fn parse_usage(reply: &serde_json::Value) -> Option<LlmUsage> {
let u: JevUsage = serde_json::from_value(reply.get("usage")?.clone()).ok()?;
Some(LlmUsage {
input_tokens: u.input_tokens,
output_tokens: u.output_tokens,
})
}
pub(super) fn interpret(reply: serde_json::Value, codes: &BTreeMap<String, String>) -> LlmCall {
let named = reply
.get("model")
.and_then(|m| m.as_str())
.filter(|m| !m.trim().is_empty())
.map(str::to_string);
let Some(usage) = parse_usage(&reply) else {
warn!("Jev reply has no valid usage block");
let call = LlmCall::failed(None);
return match named {
Some(m) => call.with_model(m),
None => call,
};
};
let usage = Some(usage);
let Some(model) = named else {
warn!("Jev reply names no model");
return LlmCall::failed(usage);
};
if model != JEV_MODEL {
warn!(served = %model, pinned = JEV_MODEL, "Jev reply came from an unpinned model");
return LlmCall::failed(usage).with_model(model);
}
let call = |c: LlmCall| c.with_model(model.clone());
let Ok(mut parsed) = serde_json::from_value::<JevResponse>(reply) else {
warn!("Jev reply does not match the decision envelope");
return call(LlmCall::failed(usage));
};
let Some(answer) = parsed.answers.remove(QUESTION) else {
warn!("Jev reply has no answer to the category question");
return call(LlmCall::failed(usage));
};
let probs = &answer.probabilities;
let valid = |p: f64| p.is_finite() && (0.0..=1.0).contains(&p);
let mass: f64 = probs.values().sum();
let max = probs.values().copied().fold(0.0_f64, f64::max);
if answer.kind != "choice"
|| probs.is_empty()
|| !probs.values().all(|&p| valid(p))
|| (mass - 1.0).abs() > 0.005 * probs.len() as f64 + 0.001
{
warn!("Jev reply carries an invalid probability distribution");
return call(LlmCall::failed(usage));
}
if ABSTAIN_CODES.contains(&answer.choice.as_str()) {
return call(LlmCall::abstained(usage));
}
let Some(category) = codes.get(&answer.choice) else {
warn!("Jev chose a code outside the configured set; abstaining");
return call(LlmCall::out_of_set(usage));
};
match probs.get(&answer.choice) {
Some(&p) if p + 0.011 >= max => call(LlmCall::answered(
ClassificationResult {
category: category.clone(),
subcategory: None,
top_level: None,
confidence: p,
method: ClassificationMethod::LlmFallback,
ticket_id: None,
complexity: None,
},
usage,
)),
_ => {
warn!("Jev choice disagrees with its probabilities");
call(LlmCall::failed(usage))
}
}
}