use serde_json::{Map, Value, json};
use super::{Answer, Judgment, Question, QuestionType, Resolved};
use crate::attachment::{Attachment, AttachmentType};
use crate::error::{Error, ErrorResponse, Result};
use crate::message::{Message, RawResponse, Role};
use crate::model::Model;
use crate::protocols::anthropic::unsupported;
use crate::providers::Provider;
pub(super) const JUDGMENT_URL: &str = "decisions";
fn wire(kind: QuestionType) -> &'static str {
match kind {
QuestionType::Probability => "predicate",
QuestionType::Choice => "choice",
QuestionType::Score => "score",
}
}
fn describe(value: &Value) -> Option<String> {
match value {
Value::Null => None,
Value::String(s) => Some(s.clone()),
other => Some(other.to_string()),
}
}
pub(super) fn render_payload(
input: &Value,
questions: &[Resolved],
model: &str,
with: &[Attachment],
provider_options: &Value,
) -> Result<Value> {
let options = provider_options.as_object().cloned().unwrap_or_default();
let reserved: Vec<&str> = options
.keys()
.map(String::as_str)
.filter(|k| ["model", "input", "questions"].contains(k))
.collect();
if !reserved.is_empty() {
return Err(Error::Argument(format!(
"Use the judgment arguments instead of provider_options for {}",
reserved.join(", ")
)));
}
let mut payload = json!({
"model": model,
"input": render_input(input, with)?,
"questions": questions.iter().map(render_question).collect::<Vec<_>>(),
});
for (k, v) in options {
payload[k] = v;
}
Ok(payload)
}
fn render_input(input: &Value, with: &[Attachment]) -> Result<Value> {
let text = describe(input);
if with.is_empty() {
return Ok(text.map(Value::String).unwrap_or(Value::Null));
}
let images = with.iter().map(render_image).collect::<Result<Vec<_>>>()?;
let mut content: Vec<Value> = text
.filter(|t| !t.is_empty())
.map(|t| json!({ "type": "input_text", "text": t }))
.into_iter()
.collect();
content.extend(images);
Ok(json!([{ "type": "message", "role": "user", "content": content }]))
}
fn render_image(image: &Attachment) -> Result<Value> {
if image.kind() != AttachmentType::Image {
return Err(Error::UnsupportedAttachment(unsupported(&image.mime_type)));
}
if let Some(id) = image.provider_file_id() {
return Ok(json!({ "type": "input_image", "file_id": id }));
}
let mut part = json!({ "type": "input_image", "image_url": image.for_llm()? });
if let Some(resolution) = image.resolution {
part["detail"] = resolution
.image_detail(Provider::OpenAI.is_original_image_detail())
.into();
}
Ok(part)
}
fn render_question(q: &Resolved) -> Value {
let mut out = Map::new();
out.insert("type".into(), wire(q.kind).into());
out.insert("name".into(), q.name.clone().into());
if let Some(instructions) = render_instructions(q) {
out.insert("instructions".into(), instructions.into());
}
match q.kind {
QuestionType::Choice => {
let choices: Vec<Value> = q
.criteria
.as_object()
.into_iter()
.flatten()
.map(|(value, description)| {
let mut choice = json!({ "value": value });
if let Some(d) = describe(description) {
choice["description"] = d.into();
}
choice
})
.collect();
out.insert("choices".into(), choices.into());
}
QuestionType::Score => {
let levels: Vec<Value> = q
.criteria
.as_array()
.into_iter()
.flatten()
.enumerate()
.map(|(index, d)| json!({ "label": index.to_string(), "description": describe(d) }))
.collect();
out.insert("levels".into(), levels.into());
}
QuestionType::Probability => {}
}
Value::Object(out)
}
fn render_instructions(q: &Resolved) -> Option<String> {
let mut lines: Vec<String> = describe(&q.instructions).into_iter().collect();
if q.kind == QuestionType::Probability
&& let Some(criteria) = q.criteria.as_object()
{
for (outcome, description) in criteria {
if let Some(d) = describe(description) {
let label = if outcome == "yes" || outcome == "true" {
"Yes"
} else {
"No"
};
lines.push(format!("{label}: {d}"));
}
}
}
let instructions = lines.join("\n");
(!instructions.is_empty()).then_some(instructions)
}
pub(super) fn parse_response(
raw: RawResponse,
questions: &[Resolved],
model: &Model,
) -> Result<Judgment> {
let parsed = parse_body(&raw.body, questions);
let (answers, reported_model, tokens) = parsed.map_err(|message| {
Error::Api(
format!("OpenAI Decisions returned an invalid judgment: {message}"),
Some(ErrorResponse {
status: raw.status,
body: raw.body.to_string(),
..Default::default()
}),
)
})?;
Ok(Judgment {
answers,
model: reported_model,
raw: Some(raw),
usage_entries: Vec::new(),
tokens,
model_info: Some(model.clone()),
})
}
type Parsed = (Vec<(String, Answer)>, String, crate::tokens::Tokens);
fn fetch<'a>(value: &'a Value, key: &str) -> std::result::Result<&'a Value, String> {
let Some(map) = value.as_object() else {
return Err(format!("undefined method 'fetch' for {value}"));
};
map.get(key)
.ok_or_else(|| format!("key not found: {key:?}"))
}
fn number(value: &Value, key: &str) -> std::result::Result<f64, String> {
let v = fetch(value, key)?;
v.as_f64()
.ok_or_else(|| format!("{key} is not a number: {v}"))
}
fn parse_body(body: &Value, questions: &[Resolved]) -> std::result::Result<Parsed, String> {
let answers = fetch(body, "answers")?;
let answers = answers
.as_array()
.ok_or_else(|| format!("answers is not an Array: {answers}"))?;
let mut parsed = Vec::with_capacity(questions.len());
for (index, q) in questions.iter().enumerate() {
let answer = answers.get(index).unwrap_or(&Value::Null);
parsed.push((q.name.clone(), parse_answer(answer, q)?));
}
let model = fetch(body, "model")?
.as_str()
.unwrap_or_default()
.to_string();
Ok((parsed, model, parse_tokens(body.get("usage"))))
}
fn parse_answer(answer: &Value, q: &Resolved) -> std::result::Result<Answer, String> {
match q.kind {
QuestionType::Probability => Ok(Answer::Probability {
probability: number(answer, "probability")?,
}),
QuestionType::Choice => {
let options: Vec<&String> = q
.criteria
.as_object()
.map(|m| m.keys().collect())
.unwrap_or_default();
let option = |value: &Value| {
value
.as_str()
.filter(|v| options.iter().any(|o| o == v))
.map(str::to_string)
.ok_or_else(|| format!("key not found: {value}"))
};
Ok(Answer::Choice {
choice: option(fetch(answer, "choice")?)?,
probabilities: distribution(answer, "value", option)?,
confidence: number(answer, "confidence")?,
})
}
QuestionType::Score => {
let levels = q.criteria.as_array().cloned().unwrap_or_default();
let labels: Vec<String> = (0..levels.len()).map(|i| i.to_string()).collect();
let index = |label: &Value| {
labels
.iter()
.position(|l| label.as_str() == Some(l))
.ok_or_else(|| format!("key not found: {label}"))
};
Ok(Answer::Score {
score: number(answer, "score")?,
probabilities: distribution(answer, "label", index)?,
levels,
confidence: number(answer, "confidence")?,
})
}
}
}
fn distribution<K>(
answer: &Value,
key: &str,
name: impl Fn(&Value) -> std::result::Result<K, String>,
) -> std::result::Result<Vec<(K, f64)>, String> {
let entries = fetch(answer, "probabilities")?;
entries
.as_array()
.ok_or_else(|| format!("probabilities is not an Array: {entries}"))?
.iter()
.map(|entry| Ok((name(fetch(entry, key)?)?, number(entry, "probability")?)))
.collect()
}
fn parse_tokens(usage: Option<&Value>) -> crate::tokens::Tokens {
let mut message = Message::new(Role::Assistant, None::<String>);
let empty = json!({});
crate::protocols::responses::parse_usage(
Provider::OpenAI,
&mut message,
usage.filter(|u| u.is_object()).unwrap_or(&empty),
);
message.tokens
}
fn resolve_all(questions: &[Question]) -> Result<Vec<Resolved>> {
questions.iter().map(|q| q.resolve(&Map::new())).collect()
}
#[doc(hidden)]
pub async fn render_judgment_payload(
input: Value,
questions: &[Question],
model: &str,
mut with: Vec<Attachment>,
provider_options: Value,
) -> Result<Value> {
let resolved = resolve_all(questions)?;
let client = crate::transport::basic(&crate::config())?;
super::load_images(&mut with, &client).await?;
render_payload(&input, &resolved, model, &with, &provider_options)
}
#[doc(hidden)]
pub fn parse_judgment_response(
body: Value,
questions: &[Question],
model: &Model,
) -> Result<Judgment> {
let resolved = resolve_all(questions)?;
let raw = RawResponse {
status: 200,
headers: Vec::new(),
body,
request_body: "".into(),
};
parse_response(raw, &resolved, model)
}