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 description_chars: Option<usize>,
98 pub multi: bool,
100 pub max_error: Option<f64>,
102 pub selection: MultiOptions<'a>,
104}
105#[derive(Debug, Clone, Copy, Default)]
107pub struct ActOptions<'a> {
108 pub criteria: Option<&'a [(&'a str, &'a str)]>,
110 pub evidence: bool,
112 pub defer_action: Option<&'a str>,
114 pub max_error: Option<f64>,
116}
117pub type CostMatrix = IndexMap<String, IndexMap<String, f64>>;
119#[derive(Debug, Clone, Copy)]
121pub enum FactsMode {
122 Off,
124 Auto,
126}
127#[derive(Debug, Clone, Copy, Default)]
129pub struct DecisionOptions<'a> {
130 pub model: Option<&'a str>,
132 pub facts: Option<FactsMode>,
134 pub lang: Option<Language>,
136 pub media: Option<&'a [MediaInput]>,
138 pub detail: Option<MediaDetail>,
140}
141
142pub(crate) mod sealed {
143 pub trait Sealed {}
144}
145pub trait AnswerKind: sealed::Sealed {
147 type Output<'a>;
149 fn parse(answer: &Answer) -> Result<Self::Output<'_>, Error>;
151}
152
153#[derive(Debug, Clone, Copy)]
155pub struct ChoiceAnswer<'a> {
156 pub choice: &'a str,
158 pub confidence: f64,
160 pub probabilities: &'a IndexMap<String, f64>,
162 pub evidence: &'a [EvidenceSpan],
164 pub raw: &'a Answer,
166}
167#[derive(Debug, Clone, Copy)]
169pub struct NoulAnswer<'a> {
170 pub noul: f64,
172 pub confidence: f64,
174 pub probabilities: &'a IndexMap<String, f64>,
176 pub evidence: &'a [EvidenceSpan],
178 pub raw: &'a Answer,
180}
181#[derive(Debug, Clone, Copy)]
183pub struct ScoreAnswer<'a> {
184 pub score: f64,
186 pub confidence: f64,
188 pub legend: &'a IndexMap<String, String>,
190 pub probabilities: &'a IndexMap<String, f64>,
192 pub evidence: &'a [EvidenceSpan],
194 pub raw: &'a Answer,
196}
197#[derive(Debug, Clone, Copy)]
199pub struct MultiAnswer<'a> {
200 pub selected: &'a [String],
202 pub threshold: Option<f64>,
204 pub objective: MultiObjective,
206 pub expected_score: Option<f64>,
208 pub constraints_applied: Option<&'a Value>,
210 pub set_confidence: f64,
212 pub probabilities: &'a IndexMap<String, f64>,
214 pub raw: &'a Answer,
216}
217#[derive(Debug, Clone, Copy)]
219pub struct ToolAnswer<'a> {
220 pub tool: Option<&'a str>,
222 pub call: bool,
224 pub confidence: Option<f64>,
226 pub multi: bool,
228 pub tools: &'a [String],
230 pub selection: Option<MultiAnswer<'a>>,
232 pub ranking: &'a [ToolRanking],
234 pub probabilities: &'a IndexMap<String, f64>,
236 pub raw: &'a Answer,
238}
239#[derive(Debug, Clone, Copy)]
241pub struct ActAnswer<'a> {
242 pub action: &'a str,
244 pub answer: &'a str,
246 pub confidence: f64,
248 pub calibration: &'a str,
250 pub expected_costs: &'a IndexMap<String, f64>,
252 pub probabilities: &'a IndexMap<String, f64>,
254 pub evidence: &'a [EvidenceSpan],
256 pub raw: &'a Answer,
258}
259
260fn result_number(value: Option<f64>) -> Result<f64, Error> {
261 value.filter(|v| v.is_finite()).ok_or(Error::InvalidResponse("missing or non-finite answer field"))
262}
263macro_rules! kind {
264 ($name:ident, $view:ident, $variant:ident, $answer:ident, $body:block) => {
265 #[doc = concat!("Result marker for ", stringify!($variant), " questions.")]
266 #[derive(Debug)]
267 pub struct $name;
268 impl sealed::Sealed for $name {}
269 impl AnswerKind for $name {
270 type Output<'a> = $view<'a>;
271 fn parse($answer: &Answer) -> Result<Self::Output<'_>, Error> {
272 if !matches!($answer.kind, QuestionType::$variant) || $answer.probabilities.is_empty() {
273 return Err(Error::InvalidResponse("mismatched answer type"));
274 }
275 $body
276 }
277 }
278 };
279}
280kind!(ChoiceKind, ChoiceAnswer, Choice, a, {
281 Ok(ChoiceAnswer {
282 choice: a.choice.as_deref().ok_or(Error::InvalidResponse("missing choice"))?,
283 confidence: result_number(a.confidence)?,
284 probabilities: &a.probabilities,
285 evidence: a.evidence.as_deref().unwrap_or_default(),
286 raw: a,
287 })
288});
289kind!(NoulKind, NoulAnswer, Noul, a, {
290 Ok(NoulAnswer {
291 noul: result_number(a.noul)?,
292 confidence: result_number(a.confidence)?,
293 probabilities: &a.probabilities,
294 evidence: a.evidence.as_deref().unwrap_or_default(),
295 raw: a,
296 })
297});
298kind!(ScoreKind, ScoreAnswer, Score, a, {
299 Ok(ScoreAnswer {
300 score: result_number(a.score)?,
301 confidence: result_number(a.confidence)?,
302 legend: a.legend.as_ref().ok_or(Error::InvalidResponse("missing legend"))?,
303 probabilities: &a.probabilities,
304 evidence: a.evidence.as_deref().unwrap_or_default(),
305 raw: a,
306 })
307});
308fn selection<'a>(a: &'a Answer, selected: &'a [String]) -> Result<MultiAnswer<'a>, Error> {
310 let objective = a.objective.unwrap_or_default();
311 Ok(MultiAnswer {
312 selected,
313 threshold: if objective == MultiObjective::Threshold { Some(result_number(a.threshold)?) } else { None },
314 objective,
315 expected_score: a.expected_score.map(|v| result_number(Some(v))).transpose()?,
316 constraints_applied: a.constraints_applied.as_ref(),
317 set_confidence: result_number(a.set_confidence)?,
318 probabilities: &a.probabilities,
319 raw: a,
320 })
321}
322kind!(MultiKind, MultiAnswer, Multi, a, {
323 selection(a, a.selected.as_deref().ok_or(Error::InvalidResponse("missing selected labels"))?)
324});
325kind!(ActKind, ActAnswer, Act, a, {
326 let costs = a.expected_costs.as_ref().ok_or(Error::InvalidResponse("missing expected costs"))?;
327 if costs.values().any(|v| !v.is_finite()) {
328 return Err(Error::InvalidResponse("non-finite expected cost"));
329 }
330 let calibration = a
331 .calibration
332 .as_deref()
333 .filter(|v| matches!(*v, "validated" | "unvalidated" | "text"))
334 .ok_or(Error::InvalidResponse("invalid calibration"))?;
335 Ok(ActAnswer {
336 action: a.action.as_deref().ok_or(Error::InvalidResponse("missing action"))?,
337 answer: a.answer.as_deref().ok_or(Error::InvalidResponse("missing outcome"))?,
338 confidence: result_number(a.confidence)?,
339 calibration,
340 expected_costs: costs,
341 probabilities: &a.probabilities,
342 evidence: a.evidence.as_deref().unwrap_or_default(),
343 raw: a,
344 })
345});
346
347kind!(ToolKind, ToolAnswer, Tool, a, {
348 let multi = a.multi.unwrap_or(false);
349 let selection = if multi {
350 let tools = a.tools.as_deref().ok_or(Error::InvalidResponse("missing selected tools"))?;
351 if tools.iter().any(|k| !a.probabilities.contains_key(k)) {
352 return Err(Error::InvalidResponse("unknown selected tool"));
353 }
354 Some(selection(a, tools)?)
355 } else {
356 result_number(a.confidence)?;
357 if a.tool.as_ref().is_some_and(|k| !a.probabilities.contains_key(k))
358 || (a.tool.is_none() && a.deferred != Some(true))
359 {
360 return Err(Error::InvalidResponse("missing or unknown tool"));
361 }
362 None
363 };
364 if a.ranking.as_deref().unwrap_or_default().iter().any(|r| {
365 !a.probabilities.contains_key(&r.tool) || !r.probability.is_finite() || !(0.0..=1.0).contains(&r.probability)
366 }) {
367 return Err(Error::InvalidResponse("invalid tool ranking"));
368 }
369 Ok(ToolAnswer {
370 tool: a.tool.as_deref(),
371 call: a.call.ok_or(Error::InvalidResponse("missing call flag"))?,
372 confidence: a.confidence,
373 multi,
374 tools: a.tools.as_deref().unwrap_or_default(),
375 selection,
376 ranking: a.ranking.as_deref().unwrap_or_default(),
377 probabilities: &a.probabilities,
378 raw: a,
379 })
380});
381
382#[derive(Debug)]
384pub struct QuestionSpec<A: AnswerKind> {
385 data: Arc<Question>,
386 marker: PhantomData<A>,
387}
388impl<A: AnswerKind> Clone for QuestionSpec<A> {
389 fn clone(&self) -> Self {
390 Self { data: self.data.clone(), marker: PhantomData }
391 }
392}
393impl<A: AnswerKind> QuestionSpec<A> {
394 pub(crate) fn new(question: Question) -> Self {
395 Self { data: Arc::new(question), marker: PhantomData }
396 }
397 pub fn to_wire(&self) -> &Question {
399 &self.data
400 }
401}
402pub type ChoiceQuestion = QuestionSpec<ChoiceKind>;
404pub type NoulQuestion = QuestionSpec<NoulKind>;
406pub type ScoreQuestion = QuestionSpec<ScoreKind>;
408pub type MultiQuestion = QuestionSpec<MultiKind>;
410pub type ActQuestion = QuestionSpec<ActKind>;
412pub type ToolQuestion = QuestionSpec<ToolKind>;
414
415fn text(value: &str) -> Result<(), Error> {
416 if value.trim().is_empty() {
417 Err(Error::InvalidRequest("expected nonempty text"))
418 } else {
419 Ok(())
420 }
421}
422fn finite(value: f64, probability: bool) -> Result<Value, Error> {
423 if !value.is_finite() || (probability && !(0.0..=1.0).contains(&value)) {
424 return Err(Error::InvalidRequest("invalid finite number/range"));
425 }
426 Ok(Value::from(value))
427}
428fn criteria(values: &[(&str, &str)], min: usize, max: usize) -> Result<Value, Error> {
429 if values.len() < min || values.len() > max {
430 return Err(Error::InvalidRequest("invalid number of criteria"));
431 }
432 let mut fields = Map::new();
433 for &(key, value) in values {
434 text(key)?;
435 text(value)?;
436 if fields.insert(key.into(), Value::String(value.into())).is_some() {
437 return Err(Error::InvalidRequest("duplicate criterion key"));
438 }
439 }
440 Ok(Value::Object(fields))
441}
442fn base(kind: QuestionType, instructions: &str, evidence: bool) -> Result<Question, Error> {
443 text(instructions)?;
444 let mut q = Question::new(kind, instructions);
445 if evidence {
446 q.extra.insert("evidence".into(), Value::Bool(true));
447 }
448 Ok(q)
449}
450pub fn choice_with_options(
452 instructions: &str,
453 values: &[(&str, &str)],
454 options: ChoiceOptions,
455) -> Result<ChoiceQuestion, Error> {
456 let mut q = base(QuestionType::Choice, instructions, options.evidence)?;
457 q.criteria = Some(criteria(values, 2, 255)?);
458 if matches!(options.option_keys, OptionKeys::Hide) {
459 q.extra.insert("option_keys".into(), Value::String("hide".into()));
460 }
461 Ok(QuestionSpec::new(q))
462}
463pub fn noul_with_options(instructions: &str, options: EvidenceOptions) -> Result<NoulQuestion, Error> {
465 Ok(QuestionSpec::new(base(QuestionType::Noul, instructions, options.evidence)?))
466}
467pub fn score_with_options(
469 instructions: &str,
470 levels: &[&str],
471 options: EvidenceOptions,
472) -> Result<ScoreQuestion, Error> {
473 if !(2..=255).contains(&levels.len()) {
474 return Err(Error::InvalidRequest("score requires 2–255 levels"));
475 }
476 let mut values = Vec::with_capacity(levels.len());
477 for &level in levels {
478 text(level)?;
479 values.push(Value::String(level.into()));
480 }
481 let mut q = base(QuestionType::Score, instructions, options.evidence)?;
482 q.criteria = Some(Value::Array(values));
483 Ok(QuestionSpec::new(q))
484}
485pub fn multi_with_options(
487 instructions: &str,
488 values: &[(&str, &str)],
489 options: MultiOptions<'_>,
490) -> Result<MultiQuestion, Error> {
491 let mut q = base(QuestionType::Multi, instructions, false)?;
492 q.criteria = Some(criteria(values, 1, usize::MAX)?);
493 q.extra.extend(selection_fields(options, Some(values))?);
494 Ok(QuestionSpec::new(q))
495}
496pub(crate) fn selection_fields(
497 options: MultiOptions<'_>,
498 values: Option<&[(&str, &str)]>,
499) -> Result<Map<String, Value>, Error> {
500 let mut fields = Map::new();
501 if options.objective != MultiObjective::Threshold {
502 fields.insert("objective".into(), serde_json::to_value(options.objective).map_err(Error::Encode)?);
503 }
504 if let Some(t) = options.threshold {
505 if options.objective != MultiObjective::Threshold {
506 return Err(Error::InvalidRequest("threshold requires threshold objective"));
507 }
508 fields.insert("threshold".into(), finite(t, true)?);
509 }
510 for (key, limit) in [("min", options.min), ("max", options.max)] {
511 if let Some(n) = limit {
512 if values.is_some_and(|v| n > v.len()) {
513 return Err(Error::InvalidRequest("selection limit exceeds labels"));
514 }
515 fields.insert(key.into(), n.into());
516 }
517 }
518 if options.min.zip(options.max).is_some_and(|(a, b)| a > b) {
519 return Err(Error::InvalidRequest("min exceeds max"));
520 }
521 let valid_key = |key: &str| -> Result<(), Error> {
522 text(key)?;
523 if values.is_some_and(|vs| !vs.iter().any(|(k, _)| *k == key)) {
524 return Err(Error::InvalidRequest("unknown selection key"));
525 }
526 Ok(())
527 };
528 if !options.exclusive.is_empty() {
529 for key in options.exclusive {
530 valid_key(key)?;
531 }
532 fields.insert("exclusive".into(), serde_json::to_value(options.exclusive).map_err(Error::Encode)?);
533 }
534 if let Some(key) = options.fallback {
535 valid_key(key)?;
536 fields.insert("fallback".into(), key.into());
537 }
538 if let Some(t) = options.fallback_threshold {
539 if options.fallback.is_none() {
540 return Err(Error::InvalidRequest("fallback_threshold requires fallback"));
541 }
542 fields.insert("fallback_threshold".into(), finite(t, true)?);
543 }
544 Ok(fields)
545}
546pub fn tool_with_options(instructions: &str, tools: &[Value], options: ToolOptions<'_>) -> Result<ToolQuestion, Error> {
548 if tools.iter().any(|v| !v.is_object()) {
549 return Err(Error::InvalidRequest("tools require definition objects"));
550 }
551 let mut q = base(QuestionType::Tool, instructions, false)?;
552 q.extra.insert("tools".into(), Value::Array(tools.to_vec()));
553 if let Some(cap) = options.description_chars {
554 q.extra.insert("description_chars".into(), cap.into());
555 }
556 if let Some(extra) = options.extra {
557 q.extra.insert("extra".into(), serde_json::to_value(extra).map_err(Error::Encode)?);
558 }
559 if let Some(rank) = options.rank {
560 if rank == 0 {
561 return Err(Error::InvalidRequest("rank must be positive"));
562 }
563 q.extra.insert("rank".into(), rank.into());
564 }
565 if let Some(v) = options.max_error {
566 q.extra.insert("max_error".into(), finite(v, true)?);
567 }
568 let fields = selection_fields(options.selection, None)?;
569 if !options.multi && !fields.is_empty() {
570 return Err(Error::InvalidRequest("selection rules require multi"));
571 }
572 if options.multi {
573 q.extra.insert("multi".into(), true.into());
574 q.extra.extend(fields);
575 }
576 Ok(QuestionSpec::new(q))
577}
578pub fn tool(instructions: &str, tools: &[Value]) -> Result<ToolQuestion, Error> {
580 tool_with_options(instructions, tools, ToolOptions::default())
581}
582
583pub fn act_with_options(instructions: &str, costs: &CostMatrix, options: ActOptions<'_>) -> Result<ActQuestion, Error> {
585 if costs.is_empty() {
586 return Err(Error::InvalidRequest("act requires costs"));
587 }
588 let mut q = base(QuestionType::Act, instructions, options.evidence)?;
589 if let Some(values) = options.criteria {
590 q.criteria = Some(criteria(values, 2, 255)?);
591 }
592 let outcomes: Vec<&str> =
593 options.criteria.map_or_else(|| vec!["true", "false"], |values| values.iter().map(|(key, _)| *key).collect());
594 let mut matrix = Map::new();
595 for (action, row) in costs {
596 text(action)?;
597 if row.len() != outcomes.len() || outcomes.iter().any(|key| !row.contains_key(*key)) {
598 return Err(Error::InvalidRequest("each cost row must cover every outcome exactly"));
599 }
600 let mut values = Map::new();
601 for (key, value) in row {
602 values.insert(key.clone(), finite(*value, false)?);
603 }
604 matrix.insert(action.clone(), Value::Object(values));
605 }
606 q.extra.insert("costs".into(), Value::Object(matrix));
607 if let Some(action) = options.defer_action {
608 if !costs.contains_key(action) {
609 return Err(Error::InvalidRequest("unknown defer action"));
610 }
611 q.extra.insert("defer_action".into(), Value::String(action.into()));
612 }
613 if let Some(error) = options.max_error {
614 q.extra.insert("max_error".into(), finite(error, true)?);
615 }
616 Ok(QuestionSpec::new(q))
617}
618
619type Validator = fn(&Answer) -> Result<(), Error>;
620#[derive(Debug, Clone)]
621struct Entry {
622 data: Arc<Question>,
623 validate: Validator,
624}
625#[derive(Debug, Clone, Default)]
627pub struct Questions {
628 entries: IndexMap<String, Entry>,
629}
630impl Questions {
631 pub fn new() -> Self {
633 Self::default()
634 }
635 pub fn with<A: AnswerKind>(mut self, id: &str, question: &QuestionSpec<A>) -> Result<Self, Error> {
637 text(id)?;
638 if self.entries.contains_key(id) {
639 return Err(Error::InvalidRequest("duplicate question ID"));
640 }
641 self.entries.insert(id.into(), Entry { data: question.data.clone(), validate: |a| A::parse(a).map(|_| ()) });
642 Ok(self)
643 }
644 pub(crate) fn request(&self, state: Value, options: DecisionOptions<'_>) -> Result<DecisionRequest, Error> {
645 if state.is_null() || self.entries.is_empty() {
646 return Err(Error::InvalidRequest("state and questions must not be empty/null"));
647 }
648 let mut request = DecisionRequest::new(state);
649 if let Some(model) = options.model {
650 text(model)?;
651 request.model = Some(model.into());
652 }
653 if let Some(facts) = options.facts {
654 request.extra.insert(
655 "facts".into(),
656 Value::String(
657 match facts {
658 FactsMode::Off => "off",
659 FactsMode::Auto => "auto",
660 }
661 .into(),
662 ),
663 );
664 }
665 request.lang = options.lang;
666 request.detail = options.detail;
667 if let Some(media) = options.media {
668 if !(1..=8).contains(&media.len()) {
669 return Err(Error::InvalidRequest("media requires 1 to 8 inputs"));
670 }
671 for input in media {
672 input.validate()?;
673 }
674 request.media = Some(media.to_vec());
675 }
676 for (id, entry) in &self.entries {
677 request.insert_question(id, &entry.data)?;
678 }
679 Ok(request)
680 }
681 pub(crate) fn parse<S: AnswerSchema>(
682 self,
683 raw: DecisionResponse,
684 schema: S,
685 ) -> Result<TypedDecisionResponse<S>, Error> {
686 for (id, entry) in &self.entries {
687 let answer = raw.answers.get(id).ok_or(Error::InvalidResponse("missing answer"))?;
688 (entry.validate)(answer)?;
689 if matches!(answer.kind, QuestionType::Tool)
690 && answer.multi.unwrap_or(false)
691 != entry.data.extra.get("multi").and_then(Value::as_bool).unwrap_or(false)
692 {
693 return Err(Error::InvalidResponse("mismatched tool selection mode"));
694 }
695 if matches!(answer.kind, QuestionType::Multi)
696 || (matches!(answer.kind, QuestionType::Tool) && answer.multi == Some(true))
697 {
698 let expected = entry.data.extra.get("objective").and_then(Value::as_str).unwrap_or("threshold");
699 if answer.objective.unwrap_or_default().as_str() != expected {
700 return Err(Error::InvalidResponse("mismatched selection objective"));
701 }
702 }
703 if answer.probabilities.values().any(|v| !v.is_finite() || !(0.0..=1.0).contains(v)) {
704 return Err(Error::InvalidResponse("invalid probabilities"));
705 }
706 if let Some(key) = &answer.choice {
707 if entry.data.criteria.as_ref().is_none_or(|v| v.get(key).is_none()) {
708 return Err(Error::InvalidResponse("unknown choice"));
709 }
710 }
711 if let Some(keys) = &answer.selected {
712 if keys.iter().any(|key| entry.data.criteria.as_ref().is_none_or(|v| v.get(key).is_none())) {
713 return Err(Error::InvalidResponse("unknown multi label"));
714 }
715 }
716 if let Some(action) = &answer.action {
717 if entry.data.extra.get("costs").is_none_or(|v| v.get(action).is_none()) {
718 return Err(Error::InvalidResponse("unknown action"));
719 }
720 }
721 }
722 Ok(TypedDecisionResponse { raw, questions: self, schema })
723 }
724}
725#[derive(Debug, Clone, Copy)]
727pub enum AnswerResult<'a> {
728 Choice(ChoiceAnswer<'a>),
730 Noul(NoulAnswer<'a>),
732 Score(ScoreAnswer<'a>),
734 Multi(MultiAnswer<'a>),
736 Act(ActAnswer<'a>),
738 Tool(ToolAnswer<'a>),
740 Classify(crate::ClassifyAnswer<'a>),
742 Verify(crate::VerifyAnswer<'a>),
744 Compress(crate::CompressAnswer<'a>),
746 Extract(crate::ExtractAnswer<'a>),
748}
749
750impl<'a> AnswerResult<'a> {
751 fn parse(answer: &'a Answer) -> Result<Self, Error> {
752 match answer.kind {
753 QuestionType::Choice => ChoiceKind::parse(answer).map(Self::Choice),
754 QuestionType::Noul => NoulKind::parse(answer).map(Self::Noul),
755 QuestionType::Score => ScoreKind::parse(answer).map(Self::Score),
756 QuestionType::Multi => MultiKind::parse(answer).map(Self::Multi),
757 QuestionType::Act => ActKind::parse(answer).map(Self::Act),
758 QuestionType::Tool => ToolKind::parse(answer).map(Self::Tool),
759 QuestionType::Classify => crate::ClassifyKind::parse(answer).map(Self::Classify),
760 QuestionType::Verify => crate::VerifyKind::parse(answer).map(Self::Verify),
761 QuestionType::Compress => crate::CompressKind::parse(answer).map(Self::Compress),
762 QuestionType::Extract => crate::ExtractKind::parse(answer).map(Self::Extract),
763 }
764 }
765}
766
767pub trait AnswerSchema: Clone {
772 type Answers<'a>;
774 fn parse<'a>(&self, raw: &'a DecisionResponse, questions: &'a Questions) -> Result<Self::Answers<'a>, Error>;
776}
777
778#[derive(Debug, Clone, Copy, Default)]
780pub struct DynamicSchema;
781
782impl AnswerSchema for DynamicSchema {
783 type Answers<'a> = IndexMap<&'a str, AnswerResult<'a>>;
784
785 fn parse<'a>(&self, raw: &'a DecisionResponse, questions: &'a Questions) -> Result<Self::Answers<'a>, Error> {
786 questions
787 .entries
788 .keys()
789 .map(|id| {
790 let answer = raw.answers.get(id).ok_or(Error::InvalidResponse("missing answer"))?;
791 Ok((id.as_str(), AnswerResult::parse(answer)?))
792 })
793 .collect()
794 }
795}
796
797pub trait QuestionSet: sealed::Sealed {
801 type Schema: AnswerSchema;
803 fn as_questions(&self) -> &Questions;
805 fn schema(&self) -> Self::Schema;
807}
808
809impl sealed::Sealed for Questions {}
810impl QuestionSet for Questions {
811 type Schema = DynamicSchema;
812 fn as_questions(&self) -> &Questions {
813 self
814 }
815 fn schema(&self) -> Self::Schema {
816 DynamicSchema
817 }
818}
819
820#[derive(Debug, Clone)]
823pub struct PreparedQuestions<S: AnswerSchema> {
824 questions: Questions,
825 schema: S,
826}
827
828impl<S: AnswerSchema> PreparedQuestions<S> {
829 pub fn new(questions: Questions, schema: S) -> Self {
831 Self { questions, schema }
832 }
833}
834
835impl<S: AnswerSchema> sealed::Sealed for PreparedQuestions<S> {}
836impl<S: AnswerSchema> QuestionSet for PreparedQuestions<S> {
837 type Schema = S;
838 fn as_questions(&self) -> &Questions {
839 &self.questions
840 }
841 fn schema(&self) -> Self::Schema {
842 self.schema.clone()
843 }
844}
845
846#[derive(Debug)]
848pub struct TypedDecisionResponse<S: AnswerSchema = DynamicSchema> {
849 raw: DecisionResponse,
850 questions: Questions,
851 schema: S,
852}
853impl<S: AnswerSchema> TypedDecisionResponse<S> {
854 pub fn raw(&self) -> &DecisionResponse {
856 &self.raw
857 }
858 pub fn model(&self) -> &str {
860 &self.raw.model
861 }
862 pub fn usage(&self) -> &Usage {
864 &self.raw.usage
865 }
866 pub fn answers(&self) -> Result<S::Answers<'_>, Error> {
869 self.schema.parse(&self.raw, &self.questions)
870 }
871 fn answer_raw(&self, id: &str) -> Result<&Answer, Error> {
872 if !self.questions.entries.contains_key(id) {
873 return Err(Error::InvalidRequest("question key was not part of this response"));
874 }
875 self.raw.answers.get(id).ok_or(Error::InvalidResponse("missing answer"))
876 }
877 pub fn answer(&self, id: &str) -> Result<AnswerResult<'_>, Error> {
879 AnswerResult::parse(self.answer_raw(id)?)
880 }
881 pub fn choice(&self, id: &str) -> Result<ChoiceAnswer<'_>, Error> {
883 ChoiceKind::parse(self.answer_raw(id)?)
884 }
885 pub fn noul(&self, id: &str) -> Result<NoulAnswer<'_>, Error> {
887 NoulKind::parse(self.answer_raw(id)?)
888 }
889 pub fn score(&self, id: &str) -> Result<ScoreAnswer<'_>, Error> {
891 ScoreKind::parse(self.answer_raw(id)?)
892 }
893 pub fn multi(&self, id: &str) -> Result<MultiAnswer<'_>, Error> {
895 MultiKind::parse(self.answer_raw(id)?)
896 }
897 pub fn tool(&self, id: &str) -> Result<ToolAnswer<'_>, Error> {
899 ToolKind::parse(self.answer_raw(id)?)
900 }
901 pub fn act(&self, id: &str) -> Result<ActAnswer<'_>, Error> {
903 ActKind::parse(self.answer_raw(id)?)
904 }
905}
906
907#[macro_export]
919macro_rules! questions {
920 ($($field:ident : $question:expr),+ $(,)?) => {{
921 #[allow(non_camel_case_types, reason = "macro type parameters reuse caller field names")]
923 mod __basal_question_shape {
924 pub struct Schema<$($field: $crate::AnswerKind),+> {
925 marker: ::std::marker::PhantomData<($($field,)+)>,
926 }
927
928 impl<$($field: $crate::AnswerKind),+> ::std::fmt::Debug for self::Schema<$($field),+> {
929 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
930 f.write_str("QuestionSchema")
931 }
932 }
933
934 impl<$($field: $crate::AnswerKind),+> ::std::clone::Clone for self::Schema<$($field),+> {
935 fn clone(&self) -> Self { Self { marker: ::std::marker::PhantomData } }
936 }
937
938 pub struct Answers<'a, $($field: $crate::AnswerKind),+> {
939 $(pub $field: <$field as $crate::AnswerKind>::Output<'a>,)+
940 }
941
942 impl<$($field: $crate::AnswerKind),+> $crate::AnswerSchema for self::Schema<$($field),+> {
943 type Answers<'a> = self::Answers<'a, $($field),+>;
944
945 fn parse<'a>(
946 &self,
947 raw: &'a $crate::DecisionResponse,
948 _: &'a $crate::Questions,
949 ) -> ::std::result::Result<Self::Answers<'a>, $crate::Error> {
950 ::std::result::Result::Ok(self::Answers {
951 $($field: {
952 let id = ::std::stringify!($field);
953 let id = id.strip_prefix("r#").unwrap_or(id);
954 let answer = raw.answers.get(id).ok_or($crate::Error::InvalidResponse("missing answer"))?;
955 <$field as $crate::AnswerKind>::parse(answer)?
956 },)+
957 })
958 }
959 }
960
961 pub fn build<$($field: $crate::AnswerKind),+>(
962 $($field: &$crate::QuestionSpec<$field>),+
963 ) -> ::std::result::Result<$crate::PreparedQuestions<self::Schema<$($field),+>>, $crate::Error> {
964 let questions = $crate::Questions::new();
965 $(let id = ::std::stringify!($field);
966 let id = id.strip_prefix("r#").unwrap_or(id);
967 let questions = questions.with(id, $field)?;)+
968 ::std::result::Result::Ok($crate::PreparedQuestions::new(questions, self::Schema { marker: ::std::marker::PhantomData }))
969 }
970 }
971 let ($($field,)+) = ($(&$question,)+);
973 __basal_question_shape::build($($field),+)
974 }};
975}
976
977pub fn choice(instructions: &str, values: &[(&str, &str)]) -> Result<ChoiceQuestion, Error> {
979 choice_with_options(instructions, values, ChoiceOptions::default())
980}
981pub fn noul(instructions: &str) -> Result<NoulQuestion, Error> {
983 noul_with_options(instructions, EvidenceOptions::default())
984}
985pub fn score(instructions: &str, levels: &[&str]) -> Result<ScoreQuestion, Error> {
987 score_with_options(instructions, levels, EvidenceOptions::default())
988}
989pub fn multi(instructions: &str, values: &[(&str, &str)]) -> Result<MultiQuestion, Error> {
991 multi_with_options(instructions, values, MultiOptions::default())
992}
993pub fn act(instructions: &str, costs: &CostMatrix) -> Result<ActQuestion, Error> {
995 act_with_options(instructions, costs, ActOptions::default())
996}