use super::types::{Answer, Provider, Question, Request, Response, SystemOneError};
use serde_json::{Map, Value, json};
pub fn laya_keys(name: &str) -> (&'static str, &'static str) {
if name.bytes().map(u32::from).sum::<u32>() % 2 == 0 {
("A", "B")
} else {
("B", "A")
}
}
pub fn encode(provider: Provider, model: &str, request: &Request) -> Value {
match provider {
Provider::OpenAi => encode_decisions(model, request),
Provider::Laya | Provider::Jev => encode_systemone(provider, model, request),
}
}
fn encode_systemone(provider: Provider, model: &str, request: &Request) -> Value {
let mut questions = Map::new();
for (name, question) in &request.questions {
let body = match question {
Question::YesNo { instructions } if provider == Provider::Laya => {
let (yes, no) = laya_keys(name);
let mut criteria = Map::new();
criteria.insert(yes.to_string(), json!("yes"));
criteria.insert(no.to_string(), json!("no"));
json!({"type": "choice", "instructions": instructions, "criteria": criteria})
}
Question::YesNo { instructions } => {
json!({"type": "noul", "instructions": instructions})
}
Question::Pick {
instructions,
options,
} => {
let criteria: Map<String, Value> = options
.iter()
.map(|(value, description)| (value.clone(), json!(description)))
.collect();
json!({"type": "choice", "instructions": instructions, "criteria": criteria})
}
};
questions.insert(name.clone(), body);
}
json!({"model": model, "state": request.state, "questions": questions})
}
fn encode_decisions(model: &str, request: &Request) -> Value {
let questions: Vec<Value> = request
.questions
.iter()
.map(|(name, question)| match question {
Question::YesNo { instructions } => {
json!({"type": "predicate", "name": name, "instructions": instructions})
}
Question::Pick {
instructions,
options,
} => {
let choices: Vec<Value> = options
.iter()
.map(|(value, description)| json!({"value": value, "description": description}))
.collect();
json!({"type": "choice", "name": name, "instructions": instructions, "choices": choices})
}
})
.collect();
let input = match &request.state {
Value::String(text) => text.clone(),
other => other.to_string(),
};
json!({"model": model, "input": input, "questions": questions})
}
pub fn decode(
provider: Provider,
request: &Request,
body: &Value,
) -> Result<Response, SystemOneError> {
let mut response = match provider {
Provider::OpenAi => decode_decisions(request, body)?,
Provider::Laya | Provider::Jev => decode_systemone(provider, request, body)?,
};
response.input_tokens = body
.get("usage")
.and_then(|usage| usage.get("input_tokens"))
.and_then(Value::as_u64);
Ok(response)
}
fn probability(value: &Value) -> Option<f32> {
value
.as_f64()
.filter(|p| p.is_finite() && (0.0..=1.0).contains(p))
.map(|p| p as f32)
}
fn pick_answer(
options: &[(String, String)],
choice: Option<&str>,
probabilities: Vec<(String, f32)>,
confidence: Option<f32>,
) -> Option<Answer> {
let choice = choice?;
if !options.iter().any(|(value, _)| value == choice) {
return None;
}
let top = probabilities
.iter()
.map(|(_, p)| *p)
.fold(0.0_f32, f32::max);
Some(Answer::Pick {
choice: choice.to_string(),
probabilities,
confidence: confidence.unwrap_or(top),
})
}
fn decode_systemone(
provider: Provider,
request: &Request,
body: &Value,
) -> Result<Response, SystemOneError> {
let answers = body
.get("answers")
.and_then(Value::as_object)
.ok_or_else(|| SystemOneError::Decode("missing `answers` object".into()))?;
let mut out = Response::default();
for (name, question) in &request.questions {
let Some(answer) = answers.get(name) else {
continue;
};
let decoded = match question {
Question::YesNo { .. } if provider == Provider::Laya => {
let (yes, _) = laya_keys(name);
answer
.get("probabilities")
.and_then(|p| p.get(yes))
.and_then(probability)
}
Question::YesNo { .. } => answer.get("noul").and_then(probability),
Question::Pick { .. } => None,
}
.map(|p_yes| Answer::YesNo { p_yes });
let decoded = match question {
Question::YesNo { .. } => decoded,
Question::Pick { options, .. } => {
let probabilities = answer
.get("probabilities")
.and_then(Value::as_object)
.map(|map| {
map.iter()
.filter_map(|(k, v)| probability(v).map(|p| (k.clone(), p)))
.collect()
})
.unwrap_or_default();
pick_answer(
options,
answer.get("choice").and_then(Value::as_str),
probabilities,
answer.get("confidence").and_then(probability),
)
}
};
if let Some(decoded) = decoded {
out.answers.insert(name.clone(), decoded);
}
}
Ok(out)
}
fn decode_decisions(request: &Request, body: &Value) -> Result<Response, SystemOneError> {
let answers = body
.get("answers")
.and_then(Value::as_array)
.ok_or_else(|| SystemOneError::Decode("missing `answers` array".into()))?;
let mut out = Response::default();
for (name, question) in &request.questions {
let Some(answer) = answers
.iter()
.find(|a| a.get("name").and_then(Value::as_str) == Some(name.as_str()))
else {
continue;
};
let kind = answer.get("type").and_then(Value::as_str);
let decoded = match (question, kind) {
(Question::YesNo { .. }, Some("predicate")) => answer
.get("probability")
.and_then(probability)
.map(|p_yes| Answer::YesNo { p_yes }),
(Question::Pick { options, .. }, Some("choice")) => {
let probabilities = answer
.get("probabilities")
.and_then(Value::as_array)
.map(|rows| {
rows.iter()
.filter_map(|row| {
let value = row.get("value").and_then(Value::as_str)?;
let p = row.get("probability").and_then(probability)?;
Some((value.to_string(), p))
})
.collect()
})
.unwrap_or_default();
pick_answer(
options,
answer.get("choice").and_then(Value::as_str),
probabilities,
answer.get("confidence").and_then(probability),
)
}
_ => None,
};
if let Some(decoded) = decoded {
out.answers.insert(name.clone(), decoded);
}
}
Ok(out)
}
#[cfg(test)]
mod test {
use super::*;
fn sample() -> Request {
Request {
state: json!({"query": "q"}),
questions: vec![
(
"ok".into(),
Question::YesNo {
instructions: "Is it ok?".into(),
},
),
(
"dept".into(),
Question::Pick {
instructions: "Which?".into(),
options: vec![
("billing".into(), "payments".into()),
("other".into(), "else".into()),
],
},
),
],
}
}
#[test]
fn test_jev_encodes_noul_and_choice_criteria_map() {
let body = encode(Provider::Jev, "jev-latest", &sample());
assert_eq!(body["model"], "jev-latest");
assert_eq!(body["questions"]["ok"]["type"], "noul");
assert_eq!(body["questions"]["dept"]["type"], "choice");
assert_eq!(body["questions"]["dept"]["criteria"]["billing"], "payments");
}
#[test]
fn test_laya_encodes_yes_no_as_neutral_two_option_choice() {
let body = encode(Provider::Laya, "laya", &sample());
let q = &body["questions"]["ok"];
assert_eq!(q["type"], "choice");
let (yes, no) = laya_keys("ok");
assert_eq!(q["criteria"][yes], "yes");
assert_eq!(q["criteria"][no], "no");
}
#[test]
fn test_laya_yes_key_varies_with_question_name() {
let keys: std::collections::HashSet<_> = ["a", "b", "ab", "wrong:search_mode"]
.iter()
.map(|n| laya_keys(n).0)
.collect();
assert_eq!(keys.len(), 2, "yes must not always be the same letter");
}
#[test]
fn test_openai_encodes_predicate_array_with_names() {
let body = encode(Provider::OpenAi, "gpt-6-luna", &sample());
assert_eq!(body["model"], "gpt-6-luna");
assert!(body["input"].is_string());
assert_eq!(body["questions"][0]["type"], "predicate");
assert_eq!(body["questions"][0]["name"], "ok");
assert_eq!(body["questions"][1]["choices"][0]["value"], "billing");
}
#[test]
fn test_jev_decodes_documented_response() {
let body = json!({
"model": "jev-1.13.0",
"answers": {
"ok": {"type": "noul", "noul": 0.91},
"dept": {"type": "choice", "choice": "billing",
"probabilities": {"billing": 0.85, "other": 0.15}, "confidence": 0.78}
},
"usage": {"input_tokens": 412, "output_tokens": 67}
});
let r = decode(Provider::Jev, &sample(), &body).unwrap();
assert_eq!(r.p_yes("ok"), Some(0.91));
assert_eq!(r.pick("dept"), Some(("billing", 0.78)));
assert_eq!(r.input_tokens, Some(412));
}
#[test]
fn test_laya_decodes_yes_from_the_yes_key() {
let (yes, no) = laya_keys("ok");
let mut probs = Map::new();
probs.insert(yes.into(), json!(0.8));
probs.insert(no.into(), json!(0.2));
let body =
json!({"answers": {"ok": {"type": "choice", "choice": yes, "probabilities": probs}}});
let r = decode(Provider::Laya, &sample(), &body).unwrap();
assert_eq!(r.p_yes("ok"), Some(0.8));
}
#[test]
fn test_openai_decodes_documented_response_and_skips_refusal() {
let body = json!({"answers": [
{"type": "refusal", "name": "ok"},
{"type": "choice", "name": "dept", "choice": "billing",
"probabilities": [{"value": "billing", "probability": 0.95},
{"value": "other", "probability": 0.05}],
"confidence": 0.93}
]});
let r = decode(Provider::OpenAi, &sample(), &body).unwrap();
assert_eq!(r.p_yes("ok"), None, "refusal must yield no answer");
assert_eq!(r.pick("dept"), Some(("billing", 0.93)));
}
#[test]
fn test_rejects_choice_outside_offered_options_and_bad_probabilities() {
let body = json!({"answers": {
"ok": {"type": "noul", "noul": 1.7},
"dept": {"type": "choice", "choice": "invented", "probabilities": {}}
}});
let r = decode(Provider::Jev, &sample(), &body).unwrap();
assert!(r.answers.is_empty());
}
#[test]
fn test_structurally_wrong_body_is_an_error() {
assert!(decode(Provider::Jev, &sample(), &json!({"nope": 1})).is_err());
assert!(decode(Provider::OpenAi, &sample(), &json!({"answers": {}})).is_err());
}
}