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) {
422 return Err(Error::InvalidRequest("duplicate question ID"));
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<S: AnswerSchema>(
454 self,
455 raw: DecisionResponse,
456 schema: S,
457 ) -> Result<TypedDecisionResponse<S>, Error> {
458 for (id, entry) in &self.entries {
459 let answer = raw.answers.get(id).ok_or(Error::InvalidResponse("missing answer"))?;
460 (entry.validate)(answer)?;
461 if answer.probabilities.values().any(|v| !v.is_finite() || !(0.0..=1.0).contains(v)) {
462 return Err(Error::InvalidResponse("invalid probabilities"));
463 }
464 if let Some(key) = &answer.choice {
465 if entry.data.criteria.as_ref().is_none_or(|v| v.get(key).is_none()) {
466 return Err(Error::InvalidResponse("unknown choice"));
467 }
468 }
469 if let Some(keys) = &answer.selected {
470 if keys.iter().any(|key| entry.data.criteria.as_ref().is_none_or(|v| v.get(key).is_none())) {
471 return Err(Error::InvalidResponse("unknown multi label"));
472 }
473 }
474 if let Some(action) = &answer.action {
475 if entry.data.extra.get("costs").is_none_or(|v| v.get(action).is_none()) {
476 return Err(Error::InvalidResponse("unknown action"));
477 }
478 }
479 }
480 Ok(TypedDecisionResponse { raw, questions: self, schema })
481 }
482}
483#[derive(Debug, Clone, Copy)]
485pub enum AnswerResult<'a> {
486 Choice(ChoiceAnswer<'a>),
488 Noul(NoulAnswer<'a>),
490 Score(ScoreAnswer<'a>),
492 Multi(MultiAnswer<'a>),
494 Act(ActAnswer<'a>),
496}
497
498impl<'a> AnswerResult<'a> {
499 fn parse(answer: &'a Answer) -> Result<Self, Error> {
500 match answer.kind {
501 QuestionType::Choice => ChoiceKind::parse(answer).map(Self::Choice),
502 QuestionType::Noul => NoulKind::parse(answer).map(Self::Noul),
503 QuestionType::Score => ScoreKind::parse(answer).map(Self::Score),
504 QuestionType::Multi => MultiKind::parse(answer).map(Self::Multi),
505 QuestionType::Act => ActKind::parse(answer).map(Self::Act),
506 }
507 }
508}
509
510pub trait AnswerSchema: Clone {
515 type Answers<'a>;
517 fn parse<'a>(&self, raw: &'a DecisionResponse, questions: &'a Questions) -> Result<Self::Answers<'a>, Error>;
519}
520
521#[derive(Debug, Clone, Copy, Default)]
523pub struct DynamicSchema;
524
525impl AnswerSchema for DynamicSchema {
526 type Answers<'a> = IndexMap<&'a str, AnswerResult<'a>>;
527
528 fn parse<'a>(&self, raw: &'a DecisionResponse, questions: &'a Questions) -> Result<Self::Answers<'a>, Error> {
529 questions
530 .entries
531 .keys()
532 .map(|id| {
533 let answer = raw.answers.get(id).ok_or(Error::InvalidResponse("missing answer"))?;
534 Ok((id.as_str(), AnswerResult::parse(answer)?))
535 })
536 .collect()
537 }
538}
539
540pub trait QuestionSet: sealed::Sealed {
544 type Schema: AnswerSchema;
546 fn as_questions(&self) -> &Questions;
548 fn schema(&self) -> Self::Schema;
550}
551
552impl sealed::Sealed for Questions {}
553impl QuestionSet for Questions {
554 type Schema = DynamicSchema;
555 fn as_questions(&self) -> &Questions {
556 self
557 }
558 fn schema(&self) -> Self::Schema {
559 DynamicSchema
560 }
561}
562
563#[derive(Debug, Clone)]
566pub struct PreparedQuestions<S: AnswerSchema> {
567 questions: Questions,
568 schema: S,
569}
570
571impl<S: AnswerSchema> PreparedQuestions<S> {
572 pub fn new(questions: Questions, schema: S) -> Self {
574 Self { questions, schema }
575 }
576}
577
578impl<S: AnswerSchema> sealed::Sealed for PreparedQuestions<S> {}
579impl<S: AnswerSchema> QuestionSet for PreparedQuestions<S> {
580 type Schema = S;
581 fn as_questions(&self) -> &Questions {
582 &self.questions
583 }
584 fn schema(&self) -> Self::Schema {
585 self.schema.clone()
586 }
587}
588
589#[derive(Debug)]
591pub struct TypedDecisionResponse<S: AnswerSchema = DynamicSchema> {
592 raw: DecisionResponse,
593 questions: Questions,
594 schema: S,
595}
596impl<S: AnswerSchema> TypedDecisionResponse<S> {
597 pub fn raw(&self) -> &DecisionResponse {
599 &self.raw
600 }
601 pub fn model(&self) -> &str {
603 &self.raw.model
604 }
605 pub fn usage(&self) -> &Usage {
607 &self.raw.usage
608 }
609 pub fn answers(&self) -> Result<S::Answers<'_>, Error> {
612 self.schema.parse(&self.raw, &self.questions)
613 }
614 fn answer_raw(&self, id: &str) -> Result<&Answer, Error> {
615 if !self.questions.entries.contains_key(id) {
616 return Err(Error::InvalidRequest("question key was not part of this response"));
617 }
618 self.raw.answers.get(id).ok_or(Error::InvalidResponse("missing answer"))
619 }
620 pub fn answer(&self, id: &str) -> Result<AnswerResult<'_>, Error> {
622 AnswerResult::parse(self.answer_raw(id)?)
623 }
624 pub fn choice(&self, id: &str) -> Result<ChoiceAnswer<'_>, Error> {
626 ChoiceKind::parse(self.answer_raw(id)?)
627 }
628 pub fn noul(&self, id: &str) -> Result<NoulAnswer<'_>, Error> {
630 NoulKind::parse(self.answer_raw(id)?)
631 }
632 pub fn score(&self, id: &str) -> Result<ScoreAnswer<'_>, Error> {
634 ScoreKind::parse(self.answer_raw(id)?)
635 }
636 pub fn multi(&self, id: &str) -> Result<MultiAnswer<'_>, Error> {
638 MultiKind::parse(self.answer_raw(id)?)
639 }
640 pub fn act(&self, id: &str) -> Result<ActAnswer<'_>, Error> {
642 ActKind::parse(self.answer_raw(id)?)
643 }
644}
645
646#[macro_export]
658macro_rules! questions {
659 ($($field:ident : $question:expr),+ $(,)?) => {{
660 #[allow(non_camel_case_types, reason = "macro type parameters reuse caller field names")]
662 mod __basal_question_shape {
663 pub struct Schema<$($field: $crate::AnswerKind),+> {
664 marker: ::std::marker::PhantomData<($($field,)+)>,
665 }
666
667 impl<$($field: $crate::AnswerKind),+> ::std::fmt::Debug for self::Schema<$($field),+> {
668 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
669 f.write_str("QuestionSchema")
670 }
671 }
672
673 impl<$($field: $crate::AnswerKind),+> ::std::clone::Clone for self::Schema<$($field),+> {
674 fn clone(&self) -> Self { Self { marker: ::std::marker::PhantomData } }
675 }
676
677 pub struct Answers<'a, $($field: $crate::AnswerKind),+> {
678 $(pub $field: <$field as $crate::AnswerKind>::Output<'a>,)+
679 }
680
681 impl<$($field: $crate::AnswerKind),+> $crate::AnswerSchema for self::Schema<$($field),+> {
682 type Answers<'a> = self::Answers<'a, $($field),+>;
683
684 fn parse<'a>(
685 &self,
686 raw: &'a $crate::DecisionResponse,
687 _: &'a $crate::Questions,
688 ) -> ::std::result::Result<Self::Answers<'a>, $crate::Error> {
689 ::std::result::Result::Ok(self::Answers {
690 $($field: {
691 let id = ::std::stringify!($field);
692 let id = id.strip_prefix("r#").unwrap_or(id);
693 let answer = raw.answers.get(id).ok_or($crate::Error::InvalidResponse("missing answer"))?;
694 <$field as $crate::AnswerKind>::parse(answer)?
695 },)+
696 })
697 }
698 }
699
700 pub fn build<$($field: $crate::AnswerKind),+>(
701 $($field: &$crate::QuestionSpec<$field>),+
702 ) -> ::std::result::Result<$crate::PreparedQuestions<self::Schema<$($field),+>>, $crate::Error> {
703 let questions = $crate::Questions::new();
704 $(let id = ::std::stringify!($field);
705 let id = id.strip_prefix("r#").unwrap_or(id);
706 let questions = questions.with(id, $field)?;)+
707 ::std::result::Result::Ok($crate::PreparedQuestions::new(questions, self::Schema { marker: ::std::marker::PhantomData }))
708 }
709 }
710 let ($($field,)+) = ($(&$question,)+);
712 __basal_question_shape::build($($field),+)
713 }};
714}
715
716pub fn choice(instructions: &str, values: &[(&str, &str)]) -> Result<ChoiceQuestion, Error> {
718 choice_with_options(instructions, values, ChoiceOptions::default())
719}
720pub fn noul(instructions: &str) -> Result<NoulQuestion, Error> {
722 noul_with_options(instructions, EvidenceOptions::default())
723}
724pub fn score(instructions: &str, levels: &[&str]) -> Result<ScoreQuestion, Error> {
726 score_with_options(instructions, levels, EvidenceOptions::default())
727}
728pub fn multi(instructions: &str, values: &[(&str, &str)]) -> Result<MultiQuestion, Error> {
730 multi_with_options(instructions, values, MultiOptions::default())
731}
732pub fn act(instructions: &str, costs: &CostMatrix) -> Result<ActQuestion, Error> {
734 act_with_options(instructions, costs, ActOptions::default())
735}