1use crate::{Answer, DecisionRequest, DecisionResponse, Error, EvidenceSpan, Question, QuestionType, Usage};
4use indexmap::IndexMap;
5use serde_json::{Map, Value};
6use std::{marker::PhantomData, sync::Arc};
7
8#[derive(Debug, Clone, Copy, Default)]
10pub enum OptionKeys {
11 #[default]
13 Show,
14 Hide,
16}
17#[derive(Debug, Clone, Copy, Default)]
19pub struct ChoiceOptions {
20 pub evidence: bool,
22 pub option_keys: OptionKeys,
24}
25#[derive(Debug, Clone, Copy, Default)]
27pub struct EvidenceOptions {
28 pub evidence: bool,
30}
31#[derive(Debug, Clone, Copy, Default)]
33pub struct MultiOptions {
34 pub threshold: Option<f64>,
36 pub min: Option<usize>,
38 pub max: Option<usize>,
40}
41#[derive(Debug, Clone, Copy, Default)]
43pub struct ActOptions<'a> {
44 pub criteria: Option<&'a [(&'a str, &'a str)]>,
46 pub evidence: bool,
48 pub defer_action: Option<&'a str>,
50 pub max_error: Option<f64>,
52}
53pub type CostMatrix = IndexMap<String, IndexMap<String, f64>>;
55#[derive(Debug, Clone, Copy)]
57pub enum FactsMode {
58 Off,
60 Auto,
62}
63#[derive(Debug, Clone, Copy, Default)]
65pub struct DecisionOptions<'a> {
66 pub model: Option<&'a str>,
68 pub facts: Option<FactsMode>,
70}
71
72mod sealed {
73 pub trait Sealed {}
74}
75pub trait AnswerKind: sealed::Sealed {
77 type Output<'a>;
79 fn parse(answer: &Answer) -> Result<Self::Output<'_>, Error>;
81}
82
83#[derive(Debug, Clone, Copy)]
85pub struct ChoiceAnswer<'a> {
86 pub choice: &'a str,
88 pub confidence: f64,
90 pub probabilities: &'a IndexMap<String, f64>,
92 pub evidence: &'a [EvidenceSpan],
94 pub raw: &'a Answer,
96}
97#[derive(Debug, Clone, Copy)]
99pub struct NoulAnswer<'a> {
100 pub noul: f64,
102 pub confidence: f64,
104 pub probabilities: &'a IndexMap<String, f64>,
106 pub evidence: &'a [EvidenceSpan],
108 pub raw: &'a Answer,
110}
111#[derive(Debug, Clone, Copy)]
113pub struct ScoreAnswer<'a> {
114 pub score: f64,
116 pub confidence: f64,
118 pub legend: &'a IndexMap<String, String>,
120 pub probabilities: &'a IndexMap<String, f64>,
122 pub evidence: &'a [EvidenceSpan],
124 pub raw: &'a Answer,
126}
127#[derive(Debug, Clone, Copy)]
129pub struct MultiAnswer<'a> {
130 pub selected: &'a [String],
132 pub threshold: f64,
134 pub set_confidence: f64,
136 pub probabilities: &'a IndexMap<String, f64>,
138 pub raw: &'a Answer,
140}
141#[derive(Debug, Clone, Copy)]
143pub struct ActAnswer<'a> {
144 pub action: &'a str,
146 pub answer: &'a str,
148 pub confidence: f64,
150 pub calibration: &'a str,
152 pub expected_costs: &'a IndexMap<String, f64>,
154 pub probabilities: &'a IndexMap<String, f64>,
156 pub evidence: &'a [EvidenceSpan],
158 pub raw: &'a Answer,
160}
161
162fn result_number(value: Option<f64>) -> Result<f64, Error> {
163 value.filter(|v| v.is_finite()).ok_or(Error::InvalidResponse("missing or non-finite answer field"))
164}
165macro_rules! kind {
166 ($name:ident, $view:ident, $variant:ident, $answer:ident, $body:block) => {
167 #[doc = concat!("Result marker for ", stringify!($variant), " questions.")]
168 #[derive(Debug)]
169 pub struct $name;
170 impl sealed::Sealed for $name {}
171 impl AnswerKind for $name {
172 type Output<'a> = $view<'a>;
173 fn parse($answer: &Answer) -> Result<Self::Output<'_>, Error> {
174 if !matches!($answer.kind, QuestionType::$variant) {
175 return Err(Error::InvalidResponse("mismatched answer type"));
176 }
177 $body
178 }
179 }
180 };
181}
182kind!(ChoiceKind, ChoiceAnswer, Choice, a, {
183 Ok(ChoiceAnswer {
184 choice: a.choice.as_deref().ok_or(Error::InvalidResponse("missing choice"))?,
185 confidence: result_number(a.confidence)?,
186 probabilities: &a.probabilities,
187 evidence: a.evidence.as_deref().unwrap_or_default(),
188 raw: a,
189 })
190});
191kind!(NoulKind, NoulAnswer, Noul, a, {
192 Ok(NoulAnswer {
193 noul: result_number(a.noul)?,
194 confidence: result_number(a.confidence)?,
195 probabilities: &a.probabilities,
196 evidence: a.evidence.as_deref().unwrap_or_default(),
197 raw: a,
198 })
199});
200kind!(ScoreKind, ScoreAnswer, Score, a, {
201 Ok(ScoreAnswer {
202 score: result_number(a.score)?,
203 confidence: result_number(a.confidence)?,
204 legend: a.legend.as_ref().ok_or(Error::InvalidResponse("missing legend"))?,
205 probabilities: &a.probabilities,
206 evidence: a.evidence.as_deref().unwrap_or_default(),
207 raw: a,
208 })
209});
210kind!(MultiKind, MultiAnswer, Multi, a, {
211 Ok(MultiAnswer {
212 selected: a.selected.as_deref().ok_or(Error::InvalidResponse("missing selected labels"))?,
213 threshold: result_number(a.threshold)?,
214 set_confidence: result_number(a.set_confidence)?,
215 probabilities: &a.probabilities,
216 raw: a,
217 })
218});
219kind!(ActKind, ActAnswer, Act, a, {
220 let costs = a.expected_costs.as_ref().ok_or(Error::InvalidResponse("missing expected costs"))?;
221 if costs.values().any(|v| !v.is_finite()) {
222 return Err(Error::InvalidResponse("non-finite expected cost"));
223 }
224 let calibration = a
225 .calibration
226 .as_deref()
227 .filter(|v| matches!(*v, "validated" | "unvalidated"))
228 .ok_or(Error::InvalidResponse("invalid calibration"))?;
229 Ok(ActAnswer {
230 action: a.action.as_deref().ok_or(Error::InvalidResponse("missing action"))?,
231 answer: a.answer.as_deref().ok_or(Error::InvalidResponse("missing outcome"))?,
232 confidence: result_number(a.confidence)?,
233 calibration,
234 expected_costs: costs,
235 probabilities: &a.probabilities,
236 evidence: a.evidence.as_deref().unwrap_or_default(),
237 raw: a,
238 })
239});
240
241#[derive(Debug)]
243pub struct QuestionSpec<A: AnswerKind> {
244 data: Arc<Question>,
245 marker: PhantomData<A>,
246}
247impl<A: AnswerKind> Clone for QuestionSpec<A> {
248 fn clone(&self) -> Self {
249 Self { data: self.data.clone(), marker: PhantomData }
250 }
251}
252impl<A: AnswerKind> QuestionSpec<A> {
253 fn new(question: Question) -> Self {
254 Self { data: Arc::new(question), marker: PhantomData }
255 }
256 pub fn to_wire(&self) -> &Question {
258 &self.data
259 }
260}
261pub type ChoiceQuestion = QuestionSpec<ChoiceKind>;
263pub type NoulQuestion = QuestionSpec<NoulKind>;
265pub type ScoreQuestion = QuestionSpec<ScoreKind>;
267pub type MultiQuestion = QuestionSpec<MultiKind>;
269pub type ActQuestion = QuestionSpec<ActKind>;
271
272fn text(value: &str) -> Result<(), Error> {
273 if value.trim().is_empty() {
274 Err(Error::InvalidRequest("expected nonempty text"))
275 } else {
276 Ok(())
277 }
278}
279fn finite(value: f64, probability: bool) -> Result<Value, Error> {
280 if !value.is_finite() || (probability && !(0.0..=1.0).contains(&value)) {
281 return Err(Error::InvalidRequest("invalid finite number/range"));
282 }
283 Ok(Value::from(value))
284}
285fn criteria(values: &[(&str, &str)], min: usize, max: usize) -> Result<Value, Error> {
286 if values.len() < min || values.len() > max {
287 return Err(Error::InvalidRequest("invalid number of criteria"));
288 }
289 let mut fields = Map::new();
290 for &(key, value) in values {
291 text(key)?;
292 text(value)?;
293 if fields.insert(key.into(), Value::String(value.into())).is_some() {
294 return Err(Error::InvalidRequest("duplicate criterion key"));
295 }
296 }
297 Ok(Value::Object(fields))
298}
299fn base(kind: QuestionType, instructions: &str, evidence: bool) -> Result<Question, Error> {
300 text(instructions)?;
301 let mut q = Question::new(kind, instructions);
302 if evidence {
303 q.extra.insert("evidence".into(), Value::Bool(true));
304 }
305 Ok(q)
306}
307pub fn choice_with_options(
309 instructions: &str,
310 values: &[(&str, &str)],
311 options: ChoiceOptions,
312) -> Result<ChoiceQuestion, Error> {
313 let mut q = base(QuestionType::Choice, instructions, options.evidence)?;
314 q.criteria = Some(criteria(values, 2, 10)?);
315 if matches!(options.option_keys, OptionKeys::Hide) {
316 q.extra.insert("option_keys".into(), Value::String("hide".into()));
317 }
318 Ok(QuestionSpec::new(q))
319}
320pub fn noul_with_options(instructions: &str, options: EvidenceOptions) -> Result<NoulQuestion, Error> {
322 Ok(QuestionSpec::new(base(QuestionType::Noul, instructions, options.evidence)?))
323}
324pub fn score_with_options(
326 instructions: &str,
327 levels: &[&str],
328 options: EvidenceOptions,
329) -> Result<ScoreQuestion, Error> {
330 if !(2..=10).contains(&levels.len()) {
331 return Err(Error::InvalidRequest("score requires 2–10 levels"));
332 }
333 let mut values = Vec::with_capacity(levels.len());
334 for &level in levels {
335 text(level)?;
336 values.push(Value::String(level.into()));
337 }
338 let mut q = base(QuestionType::Score, instructions, options.evidence)?;
339 q.criteria = Some(Value::Array(values));
340 Ok(QuestionSpec::new(q))
341}
342pub fn multi_with_options(
344 instructions: &str,
345 values: &[(&str, &str)],
346 options: MultiOptions,
347) -> Result<MultiQuestion, Error> {
348 let mut q = base(QuestionType::Multi, instructions, false)?;
349 q.criteria = Some(criteria(values, 1, usize::MAX)?);
350 if let Some(t) = options.threshold {
351 q.extra.insert("threshold".into(), finite(t, true)?);
352 }
353 for (name, limit) in [("min", options.min), ("max", options.max)] {
354 if let Some(limit) = limit {
355 if limit > values.len() {
356 return Err(Error::InvalidRequest("selection limit exceeds labels"));
357 }
358 q.extra.insert(name.into(), Value::from(limit));
359 }
360 }
361 if options.min.zip(options.max).is_some_and(|(min, max)| min > max) {
362 return Err(Error::InvalidRequest("min exceeds max"));
363 }
364 Ok(QuestionSpec::new(q))
365}
366pub fn act_with_options(instructions: &str, costs: &CostMatrix, options: ActOptions<'_>) -> Result<ActQuestion, Error> {
368 if costs.is_empty() {
369 return Err(Error::InvalidRequest("act requires costs"));
370 }
371 let mut q = base(QuestionType::Act, instructions, options.evidence)?;
372 if let Some(values) = options.criteria {
373 q.criteria = Some(criteria(values, 2, 10)?);
374 }
375 let outcomes: Vec<&str> =
376 options.criteria.map_or_else(|| vec!["true", "false"], |values| values.iter().map(|(key, _)| *key).collect());
377 let mut matrix = Map::new();
378 for (action, row) in costs {
379 text(action)?;
380 if row.len() != outcomes.len() || outcomes.iter().any(|key| !row.contains_key(*key)) {
381 return Err(Error::InvalidRequest("each cost row must cover every outcome exactly"));
382 }
383 let mut values = Map::new();
384 for (key, value) in row {
385 values.insert(key.clone(), finite(*value, false)?);
386 }
387 matrix.insert(action.clone(), Value::Object(values));
388 }
389 q.extra.insert("costs".into(), Value::Object(matrix));
390 if let Some(action) = options.defer_action {
391 if !costs.contains_key(action) {
392 return Err(Error::InvalidRequest("unknown defer action"));
393 }
394 q.extra.insert("defer_action".into(), Value::String(action.into()));
395 }
396 if let Some(error) = options.max_error {
397 q.extra.insert("max_error".into(), finite(error, true)?);
398 }
399 Ok(QuestionSpec::new(q))
400}
401
402type Validator = fn(&Answer) -> Result<(), Error>;
403#[derive(Debug, Clone)]
404struct Entry {
405 data: Arc<Question>,
406 validate: Validator,
407}
408#[derive(Debug, Clone, Default)]
410pub struct Questions {
411 entries: IndexMap<String, Entry>,
412}
413impl Questions {
414 pub fn new() -> Self {
416 Self::default()
417 }
418 pub fn with<A: AnswerKind>(mut self, id: &str, question: &QuestionSpec<A>) -> Result<Self, Error> {
420 text(id)?;
421 if self.entries.contains_key(id) || self.entries.values().any(|e| Arc::ptr_eq(&e.data, &question.data)) {
422 return Err(Error::InvalidRequest("duplicate ID or question object"));
423 }
424 self.entries.insert(id.into(), Entry { data: question.data.clone(), validate: |a| A::parse(a).map(|_| ()) });
425 Ok(self)
426 }
427 pub(crate) fn request(&self, state: Value, options: DecisionOptions<'_>) -> Result<DecisionRequest, Error> {
428 if state.is_null() || self.entries.is_empty() {
429 return Err(Error::InvalidRequest("state and questions must not be empty/null"));
430 }
431 let mut request = DecisionRequest::new(state);
432 if let Some(model) = options.model {
433 text(model)?;
434 request.model = Some(model.into());
435 }
436 if let Some(facts) = options.facts {
437 request.extra.insert(
438 "facts".into(),
439 Value::String(
440 match facts {
441 FactsMode::Off => "off",
442 FactsMode::Auto => "auto",
443 }
444 .into(),
445 ),
446 );
447 }
448 for (id, entry) in &self.entries {
449 request.insert_question(id, &entry.data)?;
450 }
451 Ok(request)
452 }
453 pub(crate) fn parse(self, raw: DecisionResponse) -> Result<TypedDecisionResponse, Error> {
454 for (id, entry) in &self.entries {
455 let answer = raw.answers.get(id).ok_or(Error::InvalidResponse("missing answer"))?;
456 (entry.validate)(answer)?;
457 if answer.probabilities.values().any(|v| !v.is_finite() || !(0.0..=1.0).contains(v)) {
458 return Err(Error::InvalidResponse("invalid probabilities"));
459 }
460 if let Some(key) = &answer.choice {
461 if entry.data.criteria.as_ref().is_none_or(|v| v.get(key).is_none()) {
462 return Err(Error::InvalidResponse("unknown choice"));
463 }
464 }
465 if let Some(keys) = &answer.selected {
466 if keys.iter().any(|key| entry.data.criteria.as_ref().is_none_or(|v| v.get(key).is_none())) {
467 return Err(Error::InvalidResponse("unknown multi label"));
468 }
469 }
470 if let Some(action) = &answer.action {
471 if entry.data.extra.get("costs").is_none_or(|v| v.get(action).is_none()) {
472 return Err(Error::InvalidResponse("unknown action"));
473 }
474 }
475 }
476 Ok(TypedDecisionResponse { raw, questions: self })
477 }
478}
479#[derive(Debug)]
481pub struct TypedDecisionResponse {
482 raw: DecisionResponse,
483 questions: Questions,
484}
485impl TypedDecisionResponse {
486 pub fn raw(&self) -> &DecisionResponse {
488 &self.raw
489 }
490 pub fn model(&self) -> &str {
492 &self.raw.model
493 }
494 pub fn usage(&self) -> &Usage {
496 &self.raw.usage
497 }
498 pub fn answer<A: AnswerKind>(&self, question: &QuestionSpec<A>) -> Result<A::Output<'_>, Error> {
501 let (id, _) = self
502 .questions
503 .entries
504 .iter()
505 .find(|(_, e)| Arc::ptr_eq(&e.data, &question.data))
506 .ok_or(Error::InvalidRequest("question was not part of this response"))?;
507 A::parse(self.raw.answers.get(id).ok_or(Error::InvalidResponse("missing answer"))?)
508 }
509}
510
511pub fn choice(instructions: &str, values: &[(&str, &str)]) -> Result<ChoiceQuestion, Error> {
513 choice_with_options(instructions, values, ChoiceOptions::default())
514}
515pub fn noul(instructions: &str) -> Result<NoulQuestion, Error> {
517 noul_with_options(instructions, EvidenceOptions::default())
518}
519pub fn score(instructions: &str, levels: &[&str]) -> Result<ScoreQuestion, Error> {
521 score_with_options(instructions, levels, EvidenceOptions::default())
522}
523pub fn multi(instructions: &str, values: &[(&str, &str)]) -> Result<MultiQuestion, Error> {
525 multi_with_options(instructions, values, MultiOptions::default())
526}
527pub fn act(instructions: &str, costs: &CostMatrix) -> Result<ActQuestion, Error> {
529 act_with_options(instructions, costs, ActOptions::default())
530}