1use crate::{
4 Answer, DecisionRequest, DecisionResponse, Error, EvidenceSpan, Language, MediaDetail, MediaInput, Question,
5 QuestionType, ToolRanking, Usage,
6};
7use indexmap::IndexMap;
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10use std::{marker::PhantomData, sync::Arc};
11
12#[derive(Debug, Clone, Copy, Default)]
14pub enum OptionKeys {
15 #[default]
17 Show,
18 Hide,
20}
21#[derive(Debug, Clone, Copy, Default)]
23pub struct ChoiceOptions {
24 pub evidence: bool,
26 pub option_keys: OptionKeys,
28}
29#[derive(Debug, Clone, Copy, Default)]
31pub struct EvidenceOptions {
32 pub evidence: bool,
34}
35#[derive(Debug, Clone, Copy, Default)]
37pub struct MultiOptions<'a> {
38 pub threshold: Option<f64>,
40 pub min: Option<usize>,
42 pub max: Option<usize>,
44 pub objective: MultiObjective,
46 pub exclusive: &'a [&'a str],
48 pub fallback: Option<&'a str>,
50 pub fallback_threshold: Option<f64>,
52}
53#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "lowercase")]
56pub enum MultiObjective {
57 #[default]
59 Threshold,
60 Exact,
62 Iou,
64 F1,
66}
67impl MultiObjective {
68 fn as_str(self) -> &'static str {
69 match self {
70 Self::Threshold => "threshold",
71 Self::Exact => "exact",
72 Self::Iou => "iou",
73 Self::F1 => "f1",
74 }
75 }
76}
77#[derive(Debug, Clone, Copy, Serialize)]
79#[serde(rename_all = "snake_case")]
80pub enum ToolExtra {
81 AnswerDirectly,
83 AskUser,
85 None,
87}
88#[derive(Debug, Clone, Copy, Default)]
90pub struct ToolOptions<'a> {
91 pub extra: Option<&'a [ToolExtra]>,
94 pub rank: Option<usize>,
96 pub multi: bool,
98 pub max_error: Option<f64>,
100 pub selection: MultiOptions<'a>,
102}
103#[derive(Debug, Clone, Copy, Default)]
105pub struct ActOptions<'a> {
106 pub criteria: Option<&'a [(&'a str, &'a str)]>,
108 pub evidence: bool,
110 pub defer_action: Option<&'a str>,
112 pub max_error: Option<f64>,
114}
115pub type CostMatrix = IndexMap<String, IndexMap<String, f64>>;
117#[derive(Debug, Clone, Copy)]
119pub enum FactsMode {
120 Off,
122 Auto,
124}
125#[derive(Debug, Clone, Copy, Default)]
127pub struct DecisionOptions<'a> {
128 pub model: Option<&'a str>,
130 pub facts: Option<FactsMode>,
132 pub lang: Option<Language>,
134 pub media: Option<&'a [MediaInput]>,
136 pub detail: Option<MediaDetail>,
138}
139
140mod sealed {
141 pub trait Sealed {}
142}
143pub trait AnswerKind: sealed::Sealed {
145 type Output<'a>;
147 fn parse(answer: &Answer) -> Result<Self::Output<'_>, Error>;
149}
150
151#[derive(Debug, Clone, Copy)]
153pub struct ChoiceAnswer<'a> {
154 pub choice: &'a str,
156 pub confidence: f64,
158 pub probabilities: &'a IndexMap<String, f64>,
160 pub evidence: &'a [EvidenceSpan],
162 pub raw: &'a Answer,
164}
165#[derive(Debug, Clone, Copy)]
167pub struct NoulAnswer<'a> {
168 pub noul: f64,
170 pub confidence: f64,
172 pub probabilities: &'a IndexMap<String, f64>,
174 pub evidence: &'a [EvidenceSpan],
176 pub raw: &'a Answer,
178}
179#[derive(Debug, Clone, Copy)]
181pub struct ScoreAnswer<'a> {
182 pub score: f64,
184 pub confidence: f64,
186 pub legend: &'a IndexMap<String, String>,
188 pub probabilities: &'a IndexMap<String, f64>,
190 pub evidence: &'a [EvidenceSpan],
192 pub raw: &'a Answer,
194}
195#[derive(Debug, Clone, Copy)]
197pub struct MultiAnswer<'a> {
198 pub selected: &'a [String],
200 pub threshold: Option<f64>,
202 pub objective: MultiObjective,
204 pub expected_score: Option<f64>,
206 pub constraints_applied: Option<&'a Value>,
208 pub set_confidence: f64,
210 pub probabilities: &'a IndexMap<String, f64>,
212 pub raw: &'a Answer,
214}
215#[derive(Debug, Clone, Copy)]
217pub struct ToolAnswer<'a> {
218 pub tool: Option<&'a str>,
220 pub call: bool,
222 pub confidence: Option<f64>,
224 pub multi: bool,
226 pub tools: &'a [String],
228 pub selection: Option<MultiAnswer<'a>>,
230 pub ranking: &'a [ToolRanking],
232 pub probabilities: &'a IndexMap<String, f64>,
234 pub raw: &'a Answer,
236}
237#[derive(Debug, Clone, Copy)]
239pub struct ActAnswer<'a> {
240 pub action: &'a str,
242 pub answer: &'a str,
244 pub confidence: f64,
246 pub calibration: &'a str,
248 pub expected_costs: &'a IndexMap<String, f64>,
250 pub probabilities: &'a IndexMap<String, f64>,
252 pub evidence: &'a [EvidenceSpan],
254 pub raw: &'a Answer,
256}
257
258fn result_number(value: Option<f64>) -> Result<f64, Error> {
259 value.filter(|v| v.is_finite()).ok_or(Error::InvalidResponse("missing or non-finite answer field"))
260}
261macro_rules! kind {
262 ($name:ident, $view:ident, $variant:ident, $answer:ident, $body:block) => {
263 #[doc = concat!("Result marker for ", stringify!($variant), " questions.")]
264 #[derive(Debug)]
265 pub struct $name;
266 impl sealed::Sealed for $name {}
267 impl AnswerKind for $name {
268 type Output<'a> = $view<'a>;
269 fn parse($answer: &Answer) -> Result<Self::Output<'_>, Error> {
270 if !matches!($answer.kind, QuestionType::$variant) {
271 return Err(Error::InvalidResponse("mismatched answer type"));
272 }
273 $body
274 }
275 }
276 };
277}
278kind!(ChoiceKind, ChoiceAnswer, Choice, a, {
279 Ok(ChoiceAnswer {
280 choice: a.choice.as_deref().ok_or(Error::InvalidResponse("missing choice"))?,
281 confidence: result_number(a.confidence)?,
282 probabilities: &a.probabilities,
283 evidence: a.evidence.as_deref().unwrap_or_default(),
284 raw: a,
285 })
286});
287kind!(NoulKind, NoulAnswer, Noul, a, {
288 Ok(NoulAnswer {
289 noul: result_number(a.noul)?,
290 confidence: result_number(a.confidence)?,
291 probabilities: &a.probabilities,
292 evidence: a.evidence.as_deref().unwrap_or_default(),
293 raw: a,
294 })
295});
296kind!(ScoreKind, ScoreAnswer, Score, a, {
297 Ok(ScoreAnswer {
298 score: result_number(a.score)?,
299 confidence: result_number(a.confidence)?,
300 legend: a.legend.as_ref().ok_or(Error::InvalidResponse("missing legend"))?,
301 probabilities: &a.probabilities,
302 evidence: a.evidence.as_deref().unwrap_or_default(),
303 raw: a,
304 })
305});
306fn selection<'a>(a: &'a Answer, selected: &'a [String]) -> Result<MultiAnswer<'a>, Error> {
308 let objective = a.objective.unwrap_or_default();
309 Ok(MultiAnswer {
310 selected,
311 threshold: if objective == MultiObjective::Threshold { Some(result_number(a.threshold)?) } else { None },
312 objective,
313 expected_score: a.expected_score.map(|v| result_number(Some(v))).transpose()?,
314 constraints_applied: a.constraints_applied.as_ref(),
315 set_confidence: result_number(a.set_confidence)?,
316 probabilities: &a.probabilities,
317 raw: a,
318 })
319}
320kind!(MultiKind, MultiAnswer, Multi, a, {
321 selection(a, a.selected.as_deref().ok_or(Error::InvalidResponse("missing selected labels"))?)
322});
323kind!(ActKind, ActAnswer, Act, a, {
324 let costs = a.expected_costs.as_ref().ok_or(Error::InvalidResponse("missing expected costs"))?;
325 if costs.values().any(|v| !v.is_finite()) {
326 return Err(Error::InvalidResponse("non-finite expected cost"));
327 }
328 let calibration = a
329 .calibration
330 .as_deref()
331 .filter(|v| matches!(*v, "validated" | "unvalidated" | "text"))
332 .ok_or(Error::InvalidResponse("invalid calibration"))?;
333 Ok(ActAnswer {
334 action: a.action.as_deref().ok_or(Error::InvalidResponse("missing action"))?,
335 answer: a.answer.as_deref().ok_or(Error::InvalidResponse("missing outcome"))?,
336 confidence: result_number(a.confidence)?,
337 calibration,
338 expected_costs: costs,
339 probabilities: &a.probabilities,
340 evidence: a.evidence.as_deref().unwrap_or_default(),
341 raw: a,
342 })
343});
344
345kind!(ToolKind, ToolAnswer, Tool, a, {
346 let multi = a.multi.unwrap_or(false);
347 let selection = if multi {
348 let tools = a.tools.as_deref().ok_or(Error::InvalidResponse("missing selected tools"))?;
349 if tools.iter().any(|k| !a.probabilities.contains_key(k)) {
350 return Err(Error::InvalidResponse("unknown selected tool"));
351 }
352 Some(selection(a, tools)?)
353 } else {
354 result_number(a.confidence)?;
355 if a.tool.as_ref().is_some_and(|k| !a.probabilities.contains_key(k))
356 || (a.tool.is_none() && a.deferred != Some(true))
357 {
358 return Err(Error::InvalidResponse("missing or unknown tool"));
359 }
360 None
361 };
362 if a.ranking.as_deref().unwrap_or_default().iter().any(|r| {
363 !a.probabilities.contains_key(&r.tool) || !r.probability.is_finite() || !(0.0..=1.0).contains(&r.probability)
364 }) {
365 return Err(Error::InvalidResponse("invalid tool ranking"));
366 }
367 Ok(ToolAnswer {
368 tool: a.tool.as_deref(),
369 call: a.call.ok_or(Error::InvalidResponse("missing call flag"))?,
370 confidence: a.confidence,
371 multi,
372 tools: a.tools.as_deref().unwrap_or_default(),
373 selection,
374 ranking: a.ranking.as_deref().unwrap_or_default(),
375 probabilities: &a.probabilities,
376 raw: a,
377 })
378});
379
380#[derive(Debug)]
382pub struct QuestionSpec<A: AnswerKind> {
383 data: Arc<Question>,
384 marker: PhantomData<A>,
385}
386impl<A: AnswerKind> Clone for QuestionSpec<A> {
387 fn clone(&self) -> Self {
388 Self { data: self.data.clone(), marker: PhantomData }
389 }
390}
391impl<A: AnswerKind> QuestionSpec<A> {
392 fn new(question: Question) -> Self {
393 Self { data: Arc::new(question), marker: PhantomData }
394 }
395 pub fn to_wire(&self) -> &Question {
397 &self.data
398 }
399}
400pub type ChoiceQuestion = QuestionSpec<ChoiceKind>;
402pub type NoulQuestion = QuestionSpec<NoulKind>;
404pub type ScoreQuestion = QuestionSpec<ScoreKind>;
406pub type MultiQuestion = QuestionSpec<MultiKind>;
408pub type ActQuestion = QuestionSpec<ActKind>;
410pub type ToolQuestion = QuestionSpec<ToolKind>;
412
413fn text(value: &str) -> Result<(), Error> {
414 if value.trim().is_empty() {
415 Err(Error::InvalidRequest("expected nonempty text"))
416 } else {
417 Ok(())
418 }
419}
420fn finite(value: f64, probability: bool) -> Result<Value, Error> {
421 if !value.is_finite() || (probability && !(0.0..=1.0).contains(&value)) {
422 return Err(Error::InvalidRequest("invalid finite number/range"));
423 }
424 Ok(Value::from(value))
425}
426fn criteria(values: &[(&str, &str)], min: usize, max: usize) -> Result<Value, Error> {
427 if values.len() < min || values.len() > max {
428 return Err(Error::InvalidRequest("invalid number of criteria"));
429 }
430 let mut fields = Map::new();
431 for &(key, value) in values {
432 text(key)?;
433 text(value)?;
434 if fields.insert(key.into(), Value::String(value.into())).is_some() {
435 return Err(Error::InvalidRequest("duplicate criterion key"));
436 }
437 }
438 Ok(Value::Object(fields))
439}
440fn base(kind: QuestionType, instructions: &str, evidence: bool) -> Result<Question, Error> {
441 text(instructions)?;
442 let mut q = Question::new(kind, instructions);
443 if evidence {
444 q.extra.insert("evidence".into(), Value::Bool(true));
445 }
446 Ok(q)
447}
448pub fn choice_with_options(
450 instructions: &str,
451 values: &[(&str, &str)],
452 options: ChoiceOptions,
453) -> Result<ChoiceQuestion, Error> {
454 let mut q = base(QuestionType::Choice, instructions, options.evidence)?;
455 q.criteria = Some(criteria(values, 2, 255)?);
456 if matches!(options.option_keys, OptionKeys::Hide) {
457 q.extra.insert("option_keys".into(), Value::String("hide".into()));
458 }
459 Ok(QuestionSpec::new(q))
460}
461pub fn noul_with_options(instructions: &str, options: EvidenceOptions) -> Result<NoulQuestion, Error> {
463 Ok(QuestionSpec::new(base(QuestionType::Noul, instructions, options.evidence)?))
464}
465pub fn score_with_options(
467 instructions: &str,
468 levels: &[&str],
469 options: EvidenceOptions,
470) -> Result<ScoreQuestion, Error> {
471 if !(2..=255).contains(&levels.len()) {
472 return Err(Error::InvalidRequest("score requires 2–255 levels"));
473 }
474 let mut values = Vec::with_capacity(levels.len());
475 for &level in levels {
476 text(level)?;
477 values.push(Value::String(level.into()));
478 }
479 let mut q = base(QuestionType::Score, instructions, options.evidence)?;
480 q.criteria = Some(Value::Array(values));
481 Ok(QuestionSpec::new(q))
482}
483pub fn multi_with_options(
485 instructions: &str,
486 values: &[(&str, &str)],
487 options: MultiOptions<'_>,
488) -> Result<MultiQuestion, Error> {
489 let mut q = base(QuestionType::Multi, instructions, false)?;
490 q.criteria = Some(criteria(values, 1, usize::MAX)?);
491 q.extra.extend(selection_fields(options, Some(values))?);
492 Ok(QuestionSpec::new(q))
493}
494fn selection_fields(options: MultiOptions<'_>, values: Option<&[(&str, &str)]>) -> Result<Map<String, Value>, Error> {
495 let mut fields = Map::new();
496 if options.objective != MultiObjective::Threshold {
497 fields.insert("objective".into(), serde_json::to_value(options.objective).map_err(Error::Encode)?);
498 }
499 if let Some(t) = options.threshold {
500 if options.objective != MultiObjective::Threshold {
501 return Err(Error::InvalidRequest("threshold requires threshold objective"));
502 }
503 fields.insert("threshold".into(), finite(t, true)?);
504 }
505 for (key, limit) in [("min", options.min), ("max", options.max)] {
506 if let Some(n) = limit {
507 if values.is_some_and(|v| n > v.len()) {
508 return Err(Error::InvalidRequest("selection limit exceeds labels"));
509 }
510 fields.insert(key.into(), n.into());
511 }
512 }
513 if options.min.zip(options.max).is_some_and(|(a, b)| a > b) {
514 return Err(Error::InvalidRequest("min exceeds max"));
515 }
516 let valid_key = |key: &str| -> Result<(), Error> {
517 text(key)?;
518 if values.is_some_and(|vs| !vs.iter().any(|(k, _)| *k == key)) {
519 return Err(Error::InvalidRequest("unknown selection key"));
520 }
521 Ok(())
522 };
523 if !options.exclusive.is_empty() {
524 for key in options.exclusive {
525 valid_key(key)?;
526 }
527 fields.insert("exclusive".into(), serde_json::to_value(options.exclusive).map_err(Error::Encode)?);
528 }
529 if let Some(key) = options.fallback {
530 valid_key(key)?;
531 fields.insert("fallback".into(), key.into());
532 }
533 if let Some(t) = options.fallback_threshold {
534 if options.fallback.is_none() {
535 return Err(Error::InvalidRequest("fallback_threshold requires fallback"));
536 }
537 fields.insert("fallback_threshold".into(), finite(t, true)?);
538 }
539 Ok(fields)
540}
541pub fn tool_with_options(instructions: &str, tools: &[Value], options: ToolOptions<'_>) -> Result<ToolQuestion, Error> {
543 if tools.iter().any(|v| !v.is_object()) {
544 return Err(Error::InvalidRequest("tools require definition objects"));
545 }
546 let mut q = base(QuestionType::Tool, instructions, false)?;
547 q.extra.insert("tools".into(), Value::Array(tools.to_vec()));
548 if let Some(extra) = options.extra {
549 q.extra.insert("extra".into(), serde_json::to_value(extra).map_err(Error::Encode)?);
550 }
551 if let Some(rank) = options.rank {
552 if rank == 0 {
553 return Err(Error::InvalidRequest("rank must be positive"));
554 }
555 q.extra.insert("rank".into(), rank.into());
556 }
557 if let Some(v) = options.max_error {
558 q.extra.insert("max_error".into(), finite(v, true)?);
559 }
560 let fields = selection_fields(options.selection, None)?;
561 if !options.multi && !fields.is_empty() {
562 return Err(Error::InvalidRequest("selection rules require multi"));
563 }
564 if options.multi {
565 q.extra.insert("multi".into(), true.into());
566 q.extra.extend(fields);
567 }
568 Ok(QuestionSpec::new(q))
569}
570pub fn tool(instructions: &str, tools: &[Value]) -> Result<ToolQuestion, Error> {
572 tool_with_options(instructions, tools, ToolOptions::default())
573}
574
575pub fn act_with_options(instructions: &str, costs: &CostMatrix, options: ActOptions<'_>) -> Result<ActQuestion, Error> {
577 if costs.is_empty() {
578 return Err(Error::InvalidRequest("act requires costs"));
579 }
580 let mut q = base(QuestionType::Act, instructions, options.evidence)?;
581 if let Some(values) = options.criteria {
582 q.criteria = Some(criteria(values, 2, 255)?);
583 }
584 let outcomes: Vec<&str> =
585 options.criteria.map_or_else(|| vec!["true", "false"], |values| values.iter().map(|(key, _)| *key).collect());
586 let mut matrix = Map::new();
587 for (action, row) in costs {
588 text(action)?;
589 if row.len() != outcomes.len() || outcomes.iter().any(|key| !row.contains_key(*key)) {
590 return Err(Error::InvalidRequest("each cost row must cover every outcome exactly"));
591 }
592 let mut values = Map::new();
593 for (key, value) in row {
594 values.insert(key.clone(), finite(*value, false)?);
595 }
596 matrix.insert(action.clone(), Value::Object(values));
597 }
598 q.extra.insert("costs".into(), Value::Object(matrix));
599 if let Some(action) = options.defer_action {
600 if !costs.contains_key(action) {
601 return Err(Error::InvalidRequest("unknown defer action"));
602 }
603 q.extra.insert("defer_action".into(), Value::String(action.into()));
604 }
605 if let Some(error) = options.max_error {
606 q.extra.insert("max_error".into(), finite(error, true)?);
607 }
608 Ok(QuestionSpec::new(q))
609}
610
611type Validator = fn(&Answer) -> Result<(), Error>;
612#[derive(Debug, Clone)]
613struct Entry {
614 data: Arc<Question>,
615 validate: Validator,
616}
617#[derive(Debug, Clone, Default)]
619pub struct Questions {
620 entries: IndexMap<String, Entry>,
621}
622impl Questions {
623 pub fn new() -> Self {
625 Self::default()
626 }
627 pub fn with<A: AnswerKind>(mut self, id: &str, question: &QuestionSpec<A>) -> Result<Self, Error> {
629 text(id)?;
630 if self.entries.contains_key(id) {
631 return Err(Error::InvalidRequest("duplicate question ID"));
632 }
633 self.entries.insert(id.into(), Entry { data: question.data.clone(), validate: |a| A::parse(a).map(|_| ()) });
634 Ok(self)
635 }
636 pub(crate) fn request(&self, state: Value, options: DecisionOptions<'_>) -> Result<DecisionRequest, Error> {
637 if state.is_null() || self.entries.is_empty() {
638 return Err(Error::InvalidRequest("state and questions must not be empty/null"));
639 }
640 let mut request = DecisionRequest::new(state);
641 if let Some(model) = options.model {
642 text(model)?;
643 request.model = Some(model.into());
644 }
645 if let Some(facts) = options.facts {
646 request.extra.insert(
647 "facts".into(),
648 Value::String(
649 match facts {
650 FactsMode::Off => "off",
651 FactsMode::Auto => "auto",
652 }
653 .into(),
654 ),
655 );
656 }
657 request.lang = options.lang;
658 request.detail = options.detail;
659 if let Some(media) = options.media {
660 if !(1..=8).contains(&media.len()) {
661 return Err(Error::InvalidRequest("media requires 1 to 8 inputs"));
662 }
663 for input in media {
664 input.validate()?;
665 }
666 request.media = Some(media.to_vec());
667 }
668 for (id, entry) in &self.entries {
669 request.insert_question(id, &entry.data)?;
670 }
671 Ok(request)
672 }
673 pub(crate) fn parse<S: AnswerSchema>(
674 self,
675 raw: DecisionResponse,
676 schema: S,
677 ) -> Result<TypedDecisionResponse<S>, Error> {
678 for (id, entry) in &self.entries {
679 let answer = raw.answers.get(id).ok_or(Error::InvalidResponse("missing answer"))?;
680 (entry.validate)(answer)?;
681 if matches!(answer.kind, QuestionType::Tool)
682 && answer.multi.unwrap_or(false)
683 != entry.data.extra.get("multi").and_then(Value::as_bool).unwrap_or(false)
684 {
685 return Err(Error::InvalidResponse("mismatched tool selection mode"));
686 }
687 if matches!(answer.kind, QuestionType::Multi)
688 || (matches!(answer.kind, QuestionType::Tool) && answer.multi == Some(true))
689 {
690 let expected = entry.data.extra.get("objective").and_then(Value::as_str).unwrap_or("threshold");
691 if answer.objective.unwrap_or_default().as_str() != expected {
692 return Err(Error::InvalidResponse("mismatched selection objective"));
693 }
694 }
695 if answer.probabilities.values().any(|v| !v.is_finite() || !(0.0..=1.0).contains(v)) {
696 return Err(Error::InvalidResponse("invalid probabilities"));
697 }
698 if let Some(key) = &answer.choice {
699 if entry.data.criteria.as_ref().is_none_or(|v| v.get(key).is_none()) {
700 return Err(Error::InvalidResponse("unknown choice"));
701 }
702 }
703 if let Some(keys) = &answer.selected {
704 if keys.iter().any(|key| entry.data.criteria.as_ref().is_none_or(|v| v.get(key).is_none())) {
705 return Err(Error::InvalidResponse("unknown multi label"));
706 }
707 }
708 if let Some(action) = &answer.action {
709 if entry.data.extra.get("costs").is_none_or(|v| v.get(action).is_none()) {
710 return Err(Error::InvalidResponse("unknown action"));
711 }
712 }
713 }
714 Ok(TypedDecisionResponse { raw, questions: self, schema })
715 }
716}
717#[derive(Debug, Clone, Copy)]
719pub enum AnswerResult<'a> {
720 Choice(ChoiceAnswer<'a>),
722 Noul(NoulAnswer<'a>),
724 Score(ScoreAnswer<'a>),
726 Multi(MultiAnswer<'a>),
728 Act(ActAnswer<'a>),
730 Tool(ToolAnswer<'a>),
732}
733
734impl<'a> AnswerResult<'a> {
735 fn parse(answer: &'a Answer) -> Result<Self, Error> {
736 match answer.kind {
737 QuestionType::Choice => ChoiceKind::parse(answer).map(Self::Choice),
738 QuestionType::Noul => NoulKind::parse(answer).map(Self::Noul),
739 QuestionType::Score => ScoreKind::parse(answer).map(Self::Score),
740 QuestionType::Multi => MultiKind::parse(answer).map(Self::Multi),
741 QuestionType::Act => ActKind::parse(answer).map(Self::Act),
742 QuestionType::Tool => ToolKind::parse(answer).map(Self::Tool),
743 }
744 }
745}
746
747pub trait AnswerSchema: Clone {
752 type Answers<'a>;
754 fn parse<'a>(&self, raw: &'a DecisionResponse, questions: &'a Questions) -> Result<Self::Answers<'a>, Error>;
756}
757
758#[derive(Debug, Clone, Copy, Default)]
760pub struct DynamicSchema;
761
762impl AnswerSchema for DynamicSchema {
763 type Answers<'a> = IndexMap<&'a str, AnswerResult<'a>>;
764
765 fn parse<'a>(&self, raw: &'a DecisionResponse, questions: &'a Questions) -> Result<Self::Answers<'a>, Error> {
766 questions
767 .entries
768 .keys()
769 .map(|id| {
770 let answer = raw.answers.get(id).ok_or(Error::InvalidResponse("missing answer"))?;
771 Ok((id.as_str(), AnswerResult::parse(answer)?))
772 })
773 .collect()
774 }
775}
776
777pub trait QuestionSet: sealed::Sealed {
781 type Schema: AnswerSchema;
783 fn as_questions(&self) -> &Questions;
785 fn schema(&self) -> Self::Schema;
787}
788
789impl sealed::Sealed for Questions {}
790impl QuestionSet for Questions {
791 type Schema = DynamicSchema;
792 fn as_questions(&self) -> &Questions {
793 self
794 }
795 fn schema(&self) -> Self::Schema {
796 DynamicSchema
797 }
798}
799
800#[derive(Debug, Clone)]
803pub struct PreparedQuestions<S: AnswerSchema> {
804 questions: Questions,
805 schema: S,
806}
807
808impl<S: AnswerSchema> PreparedQuestions<S> {
809 pub fn new(questions: Questions, schema: S) -> Self {
811 Self { questions, schema }
812 }
813}
814
815impl<S: AnswerSchema> sealed::Sealed for PreparedQuestions<S> {}
816impl<S: AnswerSchema> QuestionSet for PreparedQuestions<S> {
817 type Schema = S;
818 fn as_questions(&self) -> &Questions {
819 &self.questions
820 }
821 fn schema(&self) -> Self::Schema {
822 self.schema.clone()
823 }
824}
825
826#[derive(Debug)]
828pub struct TypedDecisionResponse<S: AnswerSchema = DynamicSchema> {
829 raw: DecisionResponse,
830 questions: Questions,
831 schema: S,
832}
833impl<S: AnswerSchema> TypedDecisionResponse<S> {
834 pub fn raw(&self) -> &DecisionResponse {
836 &self.raw
837 }
838 pub fn model(&self) -> &str {
840 &self.raw.model
841 }
842 pub fn usage(&self) -> &Usage {
844 &self.raw.usage
845 }
846 pub fn answers(&self) -> Result<S::Answers<'_>, Error> {
849 self.schema.parse(&self.raw, &self.questions)
850 }
851 fn answer_raw(&self, id: &str) -> Result<&Answer, Error> {
852 if !self.questions.entries.contains_key(id) {
853 return Err(Error::InvalidRequest("question key was not part of this response"));
854 }
855 self.raw.answers.get(id).ok_or(Error::InvalidResponse("missing answer"))
856 }
857 pub fn answer(&self, id: &str) -> Result<AnswerResult<'_>, Error> {
859 AnswerResult::parse(self.answer_raw(id)?)
860 }
861 pub fn choice(&self, id: &str) -> Result<ChoiceAnswer<'_>, Error> {
863 ChoiceKind::parse(self.answer_raw(id)?)
864 }
865 pub fn noul(&self, id: &str) -> Result<NoulAnswer<'_>, Error> {
867 NoulKind::parse(self.answer_raw(id)?)
868 }
869 pub fn score(&self, id: &str) -> Result<ScoreAnswer<'_>, Error> {
871 ScoreKind::parse(self.answer_raw(id)?)
872 }
873 pub fn multi(&self, id: &str) -> Result<MultiAnswer<'_>, Error> {
875 MultiKind::parse(self.answer_raw(id)?)
876 }
877 pub fn tool(&self, id: &str) -> Result<ToolAnswer<'_>, Error> {
879 ToolKind::parse(self.answer_raw(id)?)
880 }
881 pub fn act(&self, id: &str) -> Result<ActAnswer<'_>, Error> {
883 ActKind::parse(self.answer_raw(id)?)
884 }
885}
886
887#[macro_export]
899macro_rules! questions {
900 ($($field:ident : $question:expr),+ $(,)?) => {{
901 #[allow(non_camel_case_types, reason = "macro type parameters reuse caller field names")]
903 mod __basal_question_shape {
904 pub struct Schema<$($field: $crate::AnswerKind),+> {
905 marker: ::std::marker::PhantomData<($($field,)+)>,
906 }
907
908 impl<$($field: $crate::AnswerKind),+> ::std::fmt::Debug for self::Schema<$($field),+> {
909 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
910 f.write_str("QuestionSchema")
911 }
912 }
913
914 impl<$($field: $crate::AnswerKind),+> ::std::clone::Clone for self::Schema<$($field),+> {
915 fn clone(&self) -> Self { Self { marker: ::std::marker::PhantomData } }
916 }
917
918 pub struct Answers<'a, $($field: $crate::AnswerKind),+> {
919 $(pub $field: <$field as $crate::AnswerKind>::Output<'a>,)+
920 }
921
922 impl<$($field: $crate::AnswerKind),+> $crate::AnswerSchema for self::Schema<$($field),+> {
923 type Answers<'a> = self::Answers<'a, $($field),+>;
924
925 fn parse<'a>(
926 &self,
927 raw: &'a $crate::DecisionResponse,
928 _: &'a $crate::Questions,
929 ) -> ::std::result::Result<Self::Answers<'a>, $crate::Error> {
930 ::std::result::Result::Ok(self::Answers {
931 $($field: {
932 let id = ::std::stringify!($field);
933 let id = id.strip_prefix("r#").unwrap_or(id);
934 let answer = raw.answers.get(id).ok_or($crate::Error::InvalidResponse("missing answer"))?;
935 <$field as $crate::AnswerKind>::parse(answer)?
936 },)+
937 })
938 }
939 }
940
941 pub fn build<$($field: $crate::AnswerKind),+>(
942 $($field: &$crate::QuestionSpec<$field>),+
943 ) -> ::std::result::Result<$crate::PreparedQuestions<self::Schema<$($field),+>>, $crate::Error> {
944 let questions = $crate::Questions::new();
945 $(let id = ::std::stringify!($field);
946 let id = id.strip_prefix("r#").unwrap_or(id);
947 let questions = questions.with(id, $field)?;)+
948 ::std::result::Result::Ok($crate::PreparedQuestions::new(questions, self::Schema { marker: ::std::marker::PhantomData }))
949 }
950 }
951 let ($($field,)+) = ($(&$question,)+);
953 __basal_question_shape::build($($field),+)
954 }};
955}
956
957pub fn choice(instructions: &str, values: &[(&str, &str)]) -> Result<ChoiceQuestion, Error> {
959 choice_with_options(instructions, values, ChoiceOptions::default())
960}
961pub fn noul(instructions: &str) -> Result<NoulQuestion, Error> {
963 noul_with_options(instructions, EvidenceOptions::default())
964}
965pub fn score(instructions: &str, levels: &[&str]) -> Result<ScoreQuestion, Error> {
967 score_with_options(instructions, levels, EvidenceOptions::default())
968}
969pub fn multi(instructions: &str, values: &[(&str, &str)]) -> Result<MultiQuestion, Error> {
971 multi_with_options(instructions, values, MultiOptions::default())
972}
973pub fn act(instructions: &str, costs: &CostMatrix) -> Result<ActQuestion, Error> {
975 act_with_options(instructions, costs, ActOptions::default())
976}