1use crate::types::{Answer, Question};
3use rig_core::error::ProviderError;
4use rig_core::wasm_compat::{WasmCompatSend, WasmCompatSync};
5use serde::{Deserialize, Serialize, de::DeserializeOwned};
6use serde_json::Value;
7use std::{collections::BTreeMap, marker::PhantomData};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ChoiceAnswer<T: Ord> {
12 pub choice: T,
14 pub probabilities: BTreeMap<T, f64>,
16 pub confidence: f64,
18}
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct DynamicScoreAnswer {
22 pub score: f64,
24 pub probabilities: BTreeMap<usize, f64>,
26 pub legend: BTreeMap<usize, Value>,
28 pub confidence: f64,
30}
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct NoulAnswer {
34 pub noul: f64,
36}
37
38#[derive(Debug)]
40pub struct Choice<T> {
41 instructions: Value,
42 criteria: BTreeMap<String, Option<Value>>,
43 marker: PhantomData<fn() -> T>,
44}
45
46#[derive(Debug)]
48pub struct Score<L> {
49 instructions: Value,
50 criteria: Vec<Value>,
51 levels: Vec<L>,
52}
53
54#[derive(Debug, Clone)]
56pub struct DynamicScore {
57 instructions: Value,
58 criteria: Vec<Value>,
59}
60
61#[derive(Debug, Clone)]
63pub struct Noul {
64 instructions: Value,
65 criteria: Option<BTreeMap<String, Value>>,
66}
67
68impl<T> Clone for Choice<T> {
69 fn clone(&self) -> Self {
70 Self {
71 instructions: self.instructions.clone(),
72 criteria: self.criteria.clone(),
73 marker: PhantomData,
74 }
75 }
76}
77impl<L: Clone> Clone for Score<L> {
78 fn clone(&self) -> Self {
79 Self {
80 instructions: self.instructions.clone(),
81 criteria: self.criteria.clone(),
82 levels: self.levels.clone(),
83 }
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ScoreAnswer<L: Ord> {
90 pub score: f64,
92 pub probabilities: BTreeMap<L, f64>,
94 pub confidence: f64,
96}
97
98impl<L: Ord> Score<L> {
99 pub fn new<const N: usize, D: Serialize>(
107 instructions: impl Serialize,
108 levels: [(L, D); N],
109 ) -> Result<Self, ProviderError> {
110 const { assert!(N >= 2 && N <= 10, "score requires 2 to 10 levels") };
111 Self::try_from_iter(instructions, levels)
112 }
113
114 pub fn try_from_iter<I, D>(
117 instructions: impl Serialize,
118 levels: I,
119 ) -> Result<Self, ProviderError>
120 where
121 I: IntoIterator<Item = (L, D)>,
122 D: Serialize,
123 {
124 let mut variants = Vec::new();
125 let mut criteria = Vec::new();
126 for (level, description) in levels {
127 if variants.contains(&level) {
128 return Err(ProviderError::request("score levels must be unique"));
129 }
130 variants.push(level);
131 criteria.push(content(description)?);
132 }
133 if !(2..=10).contains(&criteria.len()) {
134 return Err(ProviderError::request("score requires 2 to 10 levels"));
135 }
136 Ok(Self {
137 instructions: content(instructions)?,
138 criteria,
139 levels: variants,
140 })
141 }
142}
143
144impl<L: Ord + Clone + WasmCompatSend + WasmCompatSync> Query for Score<L> {
145 type Response = Answer;
146 type Output = ScoreAnswer<L>;
147 fn decode(&self, response: Answer) -> Result<Self::Output, ProviderError> {
148 let answer: DynamicScoreAnswer = decode_question(&self.definition(), &response)?;
149 let probabilities = answer
150 .probabilities
151 .into_iter()
152 .map(|(index, probability)| {
153 let level = self.levels.get(index).ok_or_else(|| {
154 ProviderError::Response(format!("unknown score level: {index}"))
155 })?;
156 Ok((level.clone(), probability))
157 })
158 .collect::<Result<_, ProviderError>>()?;
159 Ok(ScoreAnswer {
160 score: answer.score,
161 confidence: answer.confidence,
162 probabilities,
163 })
164 }
165}
166impl<L> Score<L> {
167 fn definition(&self) -> Question {
168 Question::Score {
169 instructions: self.instructions.clone(),
170 criteria: self.criteria.clone(),
171 }
172 }
173}
174
175fn content(value: impl Serialize) -> Result<Value, ProviderError> {
176 let value = serde_json::to_value(value)?;
177 if !matches!(
178 value,
179 Value::String(_) | Value::Object(_) | Value::Array(_) | Value::Null
180 ) {
181 return Err(ProviderError::request(
182 "question content must be a string, object, array, or null",
183 ));
184 }
185 Ok(value)
186}
187fn question_id(id: impl Into<String>) -> Result<String, ProviderError> {
188 let id = id.into();
189 if id.is_empty() {
190 return Err(ProviderError::request("question ID cannot be empty"));
191 }
192 Ok(id)
193}
194impl<T: Serialize + DeserializeOwned + Ord> Choice<T> {
195 pub fn new<const N: usize, D: Serialize>(
204 instructions: impl Serialize,
205 alternatives: [(T, D); N],
206 ) -> Result<Self, ProviderError> {
207 const { assert!(N >= 2 && N <= 255, "choice requires 2 to 255 alternatives") };
208 Self::try_from_iter(instructions, alternatives)
209 }
210
211 pub fn try_from_iter<I, D>(
213 instructions: impl Serialize,
214 alternatives: I,
215 ) -> Result<Self, ProviderError>
216 where
217 I: IntoIterator<Item = (T, D)>,
218 D: Serialize,
219 {
220 let mut criteria = BTreeMap::new();
221 for (value, description) in alternatives {
222 let Value::String(label) = serde_json::to_value(&value)? else {
223 return Err(ProviderError::request(
224 "choice values must serialize to strings",
225 ));
226 };
227 let decoded: T =
228 serde_json::from_value(Value::String(label.clone())).map_err(|_| {
229 ProviderError::request(
230 "choice labels must deserialize to their original values",
231 )
232 })?;
233 if decoded != value {
234 return Err(ProviderError::request(
235 "choice labels must round-trip without changing values",
236 ));
237 }
238 let description = serde_json::to_value(description)?;
239 let description = match description {
240 Value::Null => None,
241 description => Some(content(description)?),
242 };
243 if label.is_empty() || criteria.insert(label, description).is_some() {
244 return Err(ProviderError::request(
245 "choice labels must be nonempty and unique",
246 ));
247 }
248 }
249 if !(2..=255).contains(&criteria.len()) {
250 return Err(ProviderError::request(
251 "choice requires 2 to 255 alternatives",
252 ));
253 }
254 Ok(Self {
255 instructions: content(instructions)?,
256 criteria,
257 marker: PhantomData,
258 })
259 }
260}
261impl DynamicScore {
262 pub fn new<const N: usize, D: Serialize>(
270 instructions: impl Serialize,
271 levels: [D; N],
272 ) -> Result<Self, ProviderError> {
273 const { assert!(N >= 2 && N <= 10, "score requires 2 to 10 levels") };
274 Self::try_from_iter(instructions, levels)
275 }
276
277 pub fn try_from_iter<I, D>(
279 instructions: impl Serialize,
280 levels: I,
281 ) -> Result<Self, ProviderError>
282 where
283 I: IntoIterator<Item = D>,
284 D: Serialize,
285 {
286 let criteria = levels
287 .into_iter()
288 .map(content)
289 .collect::<Result<Vec<_>, _>>()?;
290 if !(2..=10).contains(&criteria.len()) {
291 return Err(ProviderError::request("score requires 2 to 10 levels"));
292 }
293 Ok(Self {
294 instructions: content(instructions)?,
295 criteria,
296 })
297 }
298}
299impl Noul {
300 pub fn new(instructions: impl Serialize) -> Result<Self, ProviderError> {
302 Ok(Self {
303 instructions: content(instructions)?,
304 criteria: None,
305 })
306 }
307 pub fn criteria(
309 mut self,
310 yes: impl Serialize,
311 no: impl Serialize,
312 ) -> Result<Self, ProviderError> {
313 self.criteria = Some(BTreeMap::from([
314 ("true".into(), content(yes)?),
315 ("false".into(), content(no)?),
316 ]));
317 Ok(self)
318 }
319}
320
321pub trait Query: Serialize + WasmCompatSend + WasmCompatSync {
357 type Response: DeserializeOwned;
359 type Output;
361 fn decode(&self, response: Self::Response) -> Result<Self::Output, ProviderError>;
364
365 fn named(self, id: impl Into<String>) -> Result<NamedQuery<Self>, ProviderError>
367 where
368 Self: Sized,
369 {
370 Ok(NamedQuery {
371 id: question_id(id)?,
372 question: self,
373 })
374 }
375
376 fn join<Q: Query>(self, other: Q) -> JoinedQuery<Self, Q>
381 where
382 Self: Sized,
383 {
384 JoinedQuery {
385 first: self,
386 second: other,
387 }
388 }
389
390 fn map<A, F>(self, map: F) -> MappedQuery<Self, F>
392 where
393 Self: Sized,
394 F: Fn(Self::Output) -> A + WasmCompatSend + WasmCompatSync,
395 {
396 MappedQuery {
397 questions: self,
398 map,
399 }
400 }
401}
402
403#[derive(Debug, Clone)]
405pub struct NamedQuery<Q> {
406 id: String,
407 question: Q,
408}
409impl<Q: Serialize> Serialize for NamedQuery<Q> {
410 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
411 use serde::ser::SerializeMap;
412 let mut map = serializer.serialize_map(Some(1))?;
413 map.serialize_entry(&self.id, &self.question)?;
414 map.end()
415 }
416}
417impl<Q: Query> Query for NamedQuery<Q> {
418 type Response = BTreeMap<String, Q::Response>;
419 type Output = Q::Output;
420 fn decode(&self, mut response: Self::Response) -> Result<Self::Output, ProviderError> {
421 if response.len() != 1 {
422 return Err(ProviderError::Response(
423 "response question IDs differ from request".into(),
424 ));
425 }
426 let answer = response
427 .remove(&self.id)
428 .ok_or_else(|| ProviderError::Response(format!("missing answer: {}", self.id)))?;
429 self.question.decode(answer)
430 }
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct JoinedQuery<A, B> {
436 #[serde(flatten)]
437 first: A,
438 #[serde(flatten)]
439 second: B,
440}
441impl<A: Query, B: Query> Query for JoinedQuery<A, B> {
442 type Response = Value;
443 type Output = (A::Output, B::Output);
444 fn decode(&self, response: Self::Response) -> Result<Self::Output, ProviderError> {
445 Ok((
446 self.first
447 .decode(select_response(&self.first, &response)?)?,
448 self.second
449 .decode(select_response(&self.second, &response)?)?,
450 ))
451 }
452}
453
454fn select_response<Q: Query>(query: &Q, response: &Value) -> Result<Q::Response, ProviderError> {
455 let definitions = serde_json::to_value(query)?;
456 let definitions = definitions
457 .as_object()
458 .ok_or_else(|| ProviderError::request("joined queries must serialize as objects"))?;
459 let response = response
460 .as_object()
461 .ok_or_else(|| ProviderError::Response("answers must be an object".into()))?;
462 let selected = definitions
463 .keys()
464 .map(|id| {
465 response
466 .get(id)
467 .cloned()
468 .map(|value| (id.clone(), value))
469 .ok_or_else(|| ProviderError::Response(format!("missing answer: {id}")))
470 })
471 .collect::<Result<serde_json::Map<_, _>, _>>()?;
472 Ok(serde_json::from_value(Value::Object(selected))?)
473}
474
475#[derive(Debug, Clone, Serialize)]
477#[serde(transparent)]
478pub struct MappedQuery<Q, F> {
479 questions: Q,
480 #[serde(skip)]
481 map: F,
482}
483impl<Q: Query, F, A> Query for MappedQuery<Q, F>
484where
485 F: Fn(Q::Output) -> A + WasmCompatSend + WasmCompatSync,
486{
487 type Response = Q::Response;
488 type Output = A;
489 fn decode(&self, response: Self::Response) -> Result<A, ProviderError> {
490 Ok((self.map)(self.questions.decode(response)?))
491 }
492}
493impl<Q: Query + ?Sized> Query for &Q {
494 type Response = Q::Response;
495 type Output = Q::Output;
496 fn decode(&self, response: Self::Response) -> Result<Self::Output, ProviderError> {
497 (**self).decode(response)
498 }
499}
500impl<Q: Query> Query for BTreeMap<String, Q> {
501 type Response = BTreeMap<String, Q::Response>;
502 type Output = BTreeMap<String, Q::Output>;
503 fn decode(&self, response: Self::Response) -> Result<Self::Output, ProviderError> {
504 if !self.keys().eq(response.keys()) {
505 return Err(ProviderError::Response(
506 "response question IDs differ from request".into(),
507 ));
508 }
509 self.iter()
510 .zip(response)
511 .map(|((_, question), (id, answer))| Ok((id, question.decode(answer)?)))
512 .collect()
513 }
514}
515
516fn decode_question<A: crate::decode::DecodeAnswer>(
517 question: &Question,
518 answer: &Answer,
519) -> Result<A, ProviderError> {
520 validate(question, answer)?;
521 A::decode(answer)
522}
523macro_rules! primitive {
524 ([$($generics:tt)*] $question:ty => $answer:ty, $kind:ident) => {
525 impl $($generics)* $question {
526 fn definition(&self) -> Question {
527 Question::$kind { instructions: self.instructions.clone(), criteria: self.criteria.clone() }
528 }
529 }
530 impl $($generics)* Query for $question {
531 type Response = Answer;
532 type Output = $answer;
533 fn decode(&self, response: Answer) -> Result<Self::Output, ProviderError> {
534 decode_question(&self.definition(), &response)
535 }
536 }
537 };
538}
539primitive!([<T: DeserializeOwned + Ord>] Choice<T> => ChoiceAnswer<T>, Choice);
540primitive!([] DynamicScore => DynamicScoreAnswer, Score);
541primitive!([] Noul => NoulAnswer, Noul);
542
543impl<T> Serialize for Choice<T> {
544 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
545 Question::Choice {
546 instructions: self.instructions.clone(),
547 criteria: self.criteria.clone(),
548 }
549 .serialize(serializer)
550 }
551}
552impl<L> Serialize for Score<L> {
553 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
554 self.definition().serialize(serializer)
555 }
556}
557impl Serialize for DynamicScore {
558 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
559 self.definition().serialize(serializer)
560 }
561}
562impl Serialize for Noul {
563 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
564 self.definition().serialize(serializer)
565 }
566}
567
568pub(crate) fn validate_definition(id: &str, question: &Question) -> Result<(), ProviderError> {
569 question_id(id)?;
570 match question {
571 Question::Choice {
572 instructions,
573 criteria,
574 } => {
575 content(instructions)?;
576 if !(2..=255).contains(&criteria.len()) || criteria.keys().any(String::is_empty) {
577 return Err(ProviderError::request(
578 "choice requires 2 to 255 nonempty labels",
579 ));
580 }
581 for description in criteria.values().flatten() {
582 content(description)?;
583 }
584 }
585 Question::Score {
586 instructions,
587 criteria,
588 } => {
589 content(instructions)?;
590 if !(2..=10).contains(&criteria.len()) {
591 return Err(ProviderError::request("score requires 2 to 10 levels"));
592 }
593 for description in criteria {
594 content(description)?;
595 }
596 }
597 Question::Noul {
598 instructions,
599 criteria,
600 } => {
601 content(instructions)?;
602 if let Some(criteria) = criteria {
603 if criteria.len() != 2
604 || !criteria.contains_key("true")
605 || !criteria.contains_key("false")
606 {
607 return Err(ProviderError::request(
608 "Noul criteria require true and false descriptions",
609 ));
610 }
611 for description in criteria.values() {
612 content(description)?;
613 }
614 }
615 }
616 }
617 Ok(())
618}
619
620fn probability(value: f64) -> Result<(), ProviderError> {
621 if !value.is_finite() || !(0.0..=1.0).contains(&value) {
622 return Err(ProviderError::Response(
623 "probability or confidence is outside [0, 1]".into(),
624 ));
625 }
626 Ok(())
627}
628fn rounded_to_hundredths(probabilities: &BTreeMap<String, f64>) -> bool {
629 probabilities.values().all(|value| {
630 let cents = value * 100.0;
631 (cents - cents.round()).abs() <= 1e-9
632 })
633}
634pub(crate) fn distribution(probabilities: &BTreeMap<String, f64>) -> Result<(), ProviderError> {
635 for &weight in probabilities.values() {
636 probability(weight)?;
637 }
638 let rounded_to_cents = rounded_to_hundredths(probabilities);
642 let tolerance = if rounded_to_cents {
643 (probabilities.len() as f64 * 0.005).min(0.02) + 1e-12
644 } else {
645 1e-3
646 };
647 if (probabilities.values().sum::<f64>() - 1.0).abs() > tolerance {
648 return Err(ProviderError::Response(
649 "probabilities do not sum to one".into(),
650 ));
651 }
652 Ok(())
653}
654pub(crate) fn validate(question: &Question, answer: &Answer) -> Result<(), ProviderError> {
655 match (question, answer) {
656 (
657 Question::Choice { criteria, .. },
658 Answer::Choice {
659 choice,
660 probabilities,
661 confidence,
662 },
663 ) => {
664 if !criteria.contains_key(choice) || !criteria.keys().eq(probabilities.keys()) {
665 return Err(ProviderError::Response(
666 "choice labels differ from the requested alternatives".into(),
667 ));
668 }
669 distribution(probabilities)?;
670 probability(*confidence)?;
671 let selected = probabilities.get(choice).copied().ok_or_else(|| {
672 ProviderError::Response("selected choice has no probability".into())
673 })?;
674 if probabilities.values().any(|p| *p > selected + 1e-6) {
675 return Err(ProviderError::Response(
676 "selected choice is not a maximum".into(),
677 ));
678 }
679 }
680 (
681 Question::Score { criteria, .. },
682 Answer::Score {
683 score,
684 probabilities,
685 legend,
686 confidence,
687 },
688 ) => {
689 let expected = criteria
690 .iter()
691 .enumerate()
692 .map(|(i, v)| (i.to_string(), v.clone()))
693 .collect::<BTreeMap<_, _>>();
694 if &expected != legend || !expected.keys().eq(probabilities.keys()) {
695 return Err(ProviderError::Response(
696 "score levels differ from the requested rubric".into(),
697 ));
698 }
699 distribution(probabilities)?;
700 probability(*confidence)?;
701 if !score.is_finite() || *score < 0.0 || *score > (criteria.len() - 1) as f64 {
702 return Err(ProviderError::Response(
703 "score is outside the rubric".into(),
704 ));
705 }
706 let total = probabilities.values().sum::<f64>();
707 let mean = probabilities
710 .values()
711 .enumerate()
712 .map(|(index, weight)| index as f64 * weight)
713 .sum::<f64>()
714 / total;
715 let weight_error = if rounded_to_hundredths(probabilities) {
720 (0..criteria.len())
721 .map(|index| (index as f64 - mean).abs() * 0.005)
722 .sum::<f64>()
723 } else {
724 0.0
725 };
726 if (*score - mean).abs() > weight_error + 0.005 + 1e-12 {
727 return Err(ProviderError::Response(
728 "score differs from the probability-weighted mean".into(),
729 ));
730 }
731 }
732 (Question::Noul { .. }, Answer::Noul { noul }) => probability(*noul)?,
733 _ => {
734 return Err(ProviderError::Response(
735 "answer kind differs from question kind".into(),
736 ));
737 }
738 }
739 Ok(())
740}
741
742pub(crate) fn state(value: impl Serialize) -> Result<Value, ProviderError> {
743 let value = content(value)?;
744 if value.is_null() {
745 return Err(ProviderError::request("state cannot be null"));
746 }
747 Ok(value)
748}