use crate::{Answer, AnswerKind, Error, MultiObjective, Question, QuestionSpec, QuestionType};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub type Automation = IndexMap<String, Option<bool>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Verdict {
Supported,
Contradicted,
NotStated,
NotSupported,
}
impl Verdict {
fn key(self) -> &'static str {
match self {
Self::Supported => "supported",
Self::Contradicted => "contradicted",
Self::NotStated => "not_stated",
Self::NotSupported => "not_supported",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaimResult {
pub verdict: Verdict,
pub probabilities: IndexMap<String, f64>,
pub confidence: f64,
pub automated: Automation,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CompressionUnitKind {
#[default]
Sentence,
Line,
Paragraph,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompressionUnit {
pub start: usize,
pub end: usize,
pub probability: f64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExtractSource {
Choice,
Span,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ExtractCalibration {
#[serde(rename = "validated")]
Validated,
#[serde(rename = "per decision")]
PerDecision,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractField {
pub value: Value,
pub confidence: f64,
pub source: ExtractSource,
pub calibration: ExtractCalibration,
pub probabilities: Option<IndexMap<String, f64>>,
pub present_probability: Option<f64>,
pub text: Option<String>,
pub start: Option<usize>,
pub end: Option<usize>,
pub line_probability: Option<f64>,
pub span_probability: Option<f64>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct ClassifyAnswer<'a> {
pub scores: &'a IndexMap<String, f64>,
pub label: Option<&'a str>,
pub top_label: Option<&'a str>,
pub confidence: Option<f64>,
pub automated: Option<&'a Automation>,
pub labels: Option<&'a [String]>,
pub set_confidence: Option<f64>,
}
#[derive(Debug, Clone, Copy)]
pub struct VerifyAnswer<'a> {
pub claims: &'a IndexMap<String, ClaimResult>,
pub all_supported: bool,
pub not_supported: &'a [String],
}
#[derive(Debug, Clone, Copy)]
pub struct CompressAnswer<'a> {
pub compressed: &'a str,
pub kept: &'a [usize],
pub units: &'a [CompressionUnit],
pub unit: CompressionUnitKind,
pub original_chars: usize,
pub compressed_chars: usize,
pub ratio: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct ExtractAnswer<'a> {
pub data: &'a IndexMap<String, Value>,
pub fields: &'a IndexMap<String, ExtractField>,
}
fn required<T>(v: Option<T>) -> Result<T, Error> {
v.ok_or(Error::InvalidResponse("missing task answer field"))
}
fn probability(v: f64) -> Result<(), Error> {
if v.is_finite() && (0.0..=1.0).contains(&v) {
Ok(())
} else {
Err(Error::InvalidResponse("invalid task probability"))
}
}
fn probabilities(v: &IndexMap<String, f64>) -> Result<(), Error> {
v.values().try_for_each(|n| probability(*n))
}
macro_rules! task_kind {
($marker:ident,$view:ident,$variant:ident,$a:ident,$body:block) => {
#[doc=concat!("Result marker for ",stringify!($variant)," tasks.")]
#[derive(Debug)]
pub struct $marker;
impl crate::questions::sealed::Sealed for $marker {}
impl AnswerKind for $marker {
type Output<'a> = $view<'a>;
fn parse($a: &Answer) -> Result<Self::Output<'_>, Error> {
if !matches!($a.kind, QuestionType::$variant) || $a.experimental != Some(true) {
return Err(Error::InvalidResponse("mismatched experimental task"));
}
$body
}
}
};
}
task_kind!(ClassifyKind, ClassifyAnswer, Classify, a, {
let scores = required(a.scores.as_ref())?;
probabilities(scores)?;
if let Some(labels) = &a.labels {
if labels.iter().any(|k| !scores.contains_key(k)) {
return Err(Error::InvalidResponse("unknown classification label"));
}
probability(required(a.set_confidence)?)?;
} else {
let label = required(a.label.as_deref())?;
if !scores.contains_key(label) || a.top_label.as_deref() != Some(label) {
return Err(Error::InvalidResponse("invalid classification label"));
}
probability(required(a.confidence)?)?;
required(a.automated.as_ref())?;
}
Ok(ClassifyAnswer {
scores,
label: a.label.as_deref(),
top_label: a.top_label.as_deref(),
confidence: a.confidence,
automated: a.automated.as_ref(),
labels: a.labels.as_deref(),
set_confidence: a.set_confidence,
})
});
task_kind!(VerifyKind, VerifyAnswer, Verify, a, {
let claims = required(a.claims.as_ref())?;
for c in claims.values() {
probabilities(&c.probabilities)?;
probability(c.confidence)?;
if !c.probabilities.contains_key(c.verdict.key()) {
return Err(Error::InvalidResponse("invalid claim verdict"));
}
}
let flagged = required(a.not_supported.as_deref())?;
if flagged.iter().any(|k| !claims.contains_key(k)) {
return Err(Error::InvalidResponse("invalid flagged claim"));
}
Ok(VerifyAnswer { claims, all_supported: required(a.all_supported)?, not_supported: flagged })
});
task_kind!(CompressKind, CompressAnswer, Compress, a, {
let units = required(a.units.as_deref())?;
for u in units {
probability(u.probability)?;
if u.end < u.start {
return Err(Error::InvalidResponse("reversed compression span"));
}
}
let kept = required(a.kept.as_deref())?;
if kept.iter().any(|i| *i >= units.len()) {
return Err(Error::InvalidResponse("invalid kept unit"));
}
let ratio = required(a.ratio)?;
if !ratio.is_finite() || ratio < 0.0 {
return Err(Error::InvalidResponse("invalid compression ratio"));
}
Ok(CompressAnswer {
compressed: required(a.compressed.as_deref())?,
kept,
units,
unit: required(a.unit)?,
original_chars: required(a.original_chars)?,
compressed_chars: required(a.compressed_chars)?,
ratio,
})
});
task_kind!(ExtractKind, ExtractAnswer, Extract, a, {
let fields = required(a.fields.as_ref())?;
for f in fields.values() {
probability(f.confidence)?;
if let Some(p) = &f.probabilities {
probabilities(p)?;
}
for p in [f.present_probability, f.line_probability, f.span_probability].into_iter().flatten() {
probability(p)?;
}
if f.text.is_some() && required(f.end)? < required(f.start)? {
return Err(Error::InvalidResponse("reversed extraction span"));
}
}
Ok(ExtractAnswer { data: required(a.data.as_ref())?, fields })
});
pub type ClassifyQuestion = QuestionSpec<ClassifyKind>;
pub type VerifyQuestion = QuestionSpec<VerifyKind>;
pub type CompressQuestion = QuestionSpec<CompressKind>;
pub type ExtractQuestion = QuestionSpec<ExtractKind>;
#[derive(Debug, Clone, Copy, Default)]
pub struct ClassifyOptions<'a> {
pub multi_label: bool,
pub none: bool,
pub max_error: Option<&'a [f64]>,
pub hide_keys: bool,
pub selection: crate::MultiOptions<'a>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct VerifyOptions<'a> {
pub binary: bool,
pub max_error: Option<&'a [f64]>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CompressOptions {
pub unit: CompressionUnitKind,
pub keep_ratio: Option<f64>,
pub threshold: Option<f64>,
pub min_keep: Option<usize>,
}
fn task(kind: QuestionType, instructions: &str) -> Question {
let mut q = Question::new(kind, instructions);
if instructions.is_empty() {
q.instructions = None;
}
q
}
fn risks(q: &mut Question, values: Option<&[f64]>) -> Result<(), Error> {
if let Some(values) = values {
if values.len() > 64 || values.iter().any(|v| !v.is_finite() || *v <= 0.0 || *v >= 1.0) {
return Err(Error::InvalidRequest("invalid task max_error"));
}
q.extra.insert("max_error".into(), serde_json::json!(values));
}
Ok(())
}
pub fn classify(instructions: &str, labels: Value, options: ClassifyOptions<'_>) -> Result<ClassifyQuestion, Error> {
let count = match &labels {
Value::Object(m) => m.len(),
Value::Array(a) => a.len(),
_ => 0,
};
if !(1..=256).contains(&count) {
return Err(Error::InvalidRequest("classify requires 1..256 labels"));
}
let mut q = task(QuestionType::Classify, instructions);
q.extra.insert("labels".into(), labels);
q.extra.insert("multi_label".into(), options.multi_label.into());
q.extra.insert("none".into(), options.none.into());
risks(&mut q, options.max_error)?;
if options.hide_keys {
q.extra.insert("option_keys".into(), "hide".into());
}
if options.multi_label {
if !options.selection.exclusive.is_empty()
|| options.selection.fallback.is_some()
|| options.selection.fallback_threshold.is_some()
{
return Err(Error::InvalidRequest("classify does not support exclusive/fallback"));
}
q.extra.extend(crate::questions::selection_fields(options.selection, None)?);
} else if options.selection.threshold.is_some()
|| options.selection.min.is_some()
|| options.selection.max.is_some()
|| !matches!(options.selection.objective, MultiObjective::Threshold)
|| !options.selection.exclusive.is_empty()
|| options.selection.fallback.is_some()
|| options.selection.fallback_threshold.is_some()
{
return Err(Error::InvalidRequest("selection options require multi_label"));
}
Ok(QuestionSpec::new(q))
}
pub fn verify(instructions: &str, claims: Value, options: VerifyOptions<'_>) -> Result<VerifyQuestion, Error> {
if match &claims {
Value::Object(m) => m.is_empty(),
Value::Array(a) => a.is_empty(),
_ => true,
} {
return Err(Error::InvalidRequest("verify requires claims"));
}
let mut q = task(QuestionType::Verify, instructions);
q.extra.insert("claims".into(), claims);
q.extra.insert("binary".into(), options.binary.into());
risks(&mut q, options.max_error)?;
Ok(QuestionSpec::new(q))
}
pub fn compress(instructions: &str, options: CompressOptions) -> Result<CompressQuestion, Error> {
let mut q = task(QuestionType::Compress, instructions);
q.extra.insert("unit".into(), serde_json::to_value(options.unit).map_err(Error::Encode)?);
for (k, v) in [("keep_ratio", options.keep_ratio), ("threshold", options.threshold)] {
if let Some(v) = v {
if !v.is_finite() || !(0.0..=1.0).contains(&v) || (k == "keep_ratio" && v == 0.0) {
return Err(Error::InvalidRequest("invalid compression probability"));
}
q.extra.insert(k.into(), v.into());
}
}
if let Some(n) = options.min_keep {
q.extra.insert("min_keep".into(), n.into());
}
Ok(QuestionSpec::new(q))
}
pub fn extract(instructions: &str, schema: Value) -> Result<ExtractQuestion, Error> {
if !schema.is_object() {
return Err(Error::InvalidRequest("schema must be an object"));
}
let mut q = task(QuestionType::Extract, instructions);
q.extra.insert("schema".into(), schema);
Ok(QuestionSpec::new(q))
}