The three question types, with the API's limits enforced when a question is built, so an over-limit request cannot be constructed.
4use std::collections::HashSet;
"You can have a maximum of 255 options per Choice" (api.md).
12pub const MAX_CHOICE_OPTIONS: usize = 255;
"A Score should have at least two levels; the API accepts up to 10."
14pub const SCORE_LEVELS: std::ops::RangeInclusive<usize> = 2..=10;
Yes or no; answered with the probability of yes.
What a yes (value near 1) means.
What a no (value near 0) means.
One label out of 2 to 255. The labels are sent to the model as the option keys, in the caller's order, which is part of the question (order bias is measured; contract SQL surface).
58impl Choice {
A single option is not a question: answer it locally with probability 1 rather than sending it.
61 pub fn new( 62 instructions: Json, 63 options: impl IntoIterator<Item = (String, Option<Json>)>, 64 ) -> Result<Self, ProtocolError> { 65 let options: Vec<_> = options.into_iter().collect(); 66 if options.len() < 2 || options.len() > MAX_CHOICE_OPTIONS { 67 return Err(ProtocolError::Invalid(format!( 68 "a Choice takes 2 to {MAX_CHOICE_OPTIONS} options, not {}", 69 options.len() 70 ))); 71 } 72 let mut seen = HashSet::new(); 73 for (label, _) in &options { 74 if label.is_empty() { 75 return Err(ProtocolError::Invalid("a Choice option label is empty".into())); 76 } 77 if !seen.insert(label.as_str()) { 78 return Err(ProtocolError::Invalid(format!("the Choice option {label:?} appears twice"))); 79 } 80 } 81 Ok(Choice { instructions, options }) 82 }
A position on 2 to 10 ordered levels, lowest first.
96impl Score { 97 pub fn new(instructions: Json, levels: impl IntoIterator<Item = Json>) -> Result<Self, ProtocolError> { 98 let levels: Vec<_> = levels.into_iter().collect(); 99 if !SCORE_LEVELS.contains(&levels.len()) { 100 return Err(ProtocolError::Invalid(format!( 101 "a Score takes {} to {} levels, not {}", 102 SCORE_LEVELS.start(), 103 SCORE_LEVELS.end(), 104 levels.len() 105 ))); 106 } 107 Ok(Score { instructions, levels }) 108 } 109 110 pub fn level_count(&self) -> usize { 111 self.levels.len() 112 } 113} 114 115impl Serialize for Question { 116 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { 117 let mut m = s.serialize_map(None)?; 118 match self { 119 Question::Noul(q) => { 120 m.serialize_entry("type", "noul")?; 121 m.serialize_entry("instructions", &q.instructions)?; 122 if q.yes.is_some() || q.no.is_some() { 123 m.serialize_entry("criteria", &NoulCriteria(q))?; 124 } 125 } 126 Question::Choice(q) => { 127 m.serialize_entry("type", "choice")?; 128 m.serialize_entry("instructions", &q.instructions)?; 129 m.serialize_entry("criteria", &ChoiceCriteria(&q.options))?; 130 } 131 Question::Score(q) => { 132 m.serialize_entry("type", "score")?; 133 m.serialize_entry("instructions", &q.instructions)?; 134 m.serialize_entry("criteria", &q.levels)?; 135 } 136 } 137 m.end() 138 } 139} 140 141struct NoulCriteria<'a>(&'a Noul); 142 143impl Serialize for NoulCriteria<'_> { 144 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { 145 let mut m = s.serialize_map(None)?; 146 if let Some(yes) = &self.0.yes { 147 m.serialize_entry("true", yes)?; 148 } 149 if let Some(no) = &self.0.no { 150 m.serialize_entry("false", no)?; 151 } 152 m.end() 153 } 154}
Written in the caller's order; never through a map type that sorts.
157struct ChoiceCriteria<'a>(&'a [(String, Option<Json>)]);
159impl Serialize for ChoiceCriteria<'_> { 160 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { 161 s.collect_map(self.0.iter().map(|(label, description)| (label, description))) 162 } 163} 164 165#[cfg(test)] 166mod tests { 167 use super::*; 168 169 fn opts(labels: &[&str]) -> Vec<(String, Option<Json>)> { 170 labels.iter().map(|l| (l.to_string(), None)).collect() 171 } 172 173 fn json(q: &Question) -> String { 174 serde_json::to_string(q).unwrap() 175 } 176 177 #[test] 178 fn choice_limits() { 179 let q = Json::text("q"); 180 assert!(Choice::new(q.clone(), opts(&["a"])).is_err()); 181 assert!(Choice::new(q.clone(), opts(&["a", "a"])).is_err()); 182 assert!(Choice::new(q.clone(), opts(&["a", ""])).is_err()); 183 let many: Vec<String> = (0..256).map(|i| i.to_string()).collect(); 184 let many: Vec<&str> = many.iter().map(String::as_str).collect(); 185 assert!(Choice::new(q.clone(), opts(&many[..255])).is_ok()); 186 assert!(Choice::new(q, opts(&many)).is_err()); 187 } 188 189 #[test] 190 fn score_limits() { 191 let level = |n: usize| (0..n).map(|i| Json::text(&i.to_string())).collect::<Vec<_>>(); 192 assert!(Score::new(Json::text("q"), level(1)).is_err()); 193 assert!(Score::new(Json::text("q"), level(2)).is_ok()); 194 assert!(Score::new(Json::text("q"), level(10)).is_ok()); 195 assert!(Score::new(Json::text("q"), level(11)).is_err()); 196 } 197 198 #[test] 199 fn choice_keeps_caller_order() { 200 let q = Choice::new( 201 Json::text("Team?"), 202 [("zeta".to_string(), Some(Json::text("z"))), ("alpha".to_string(), None)], 203 ) 204 .unwrap(); 205 assert_eq!( 206 json(&Question::Choice(q)), 207 r#"{"type":"choice","instructions":"Team?","criteria":{"zeta":"z","alpha":null}}"# 208 ); 209 } 210 211 #[test] 212 fn noul_and_score_shapes() { 213 assert_eq!(json(&Question::Noul(Noul::new(Json::text("U?")))), r#"{"type":"noul","instructions":"U?"}"#); 214 let n = Noul::new(Json::text("U?")).yes_means(Json::text("urgent")); 215 assert_eq!(json(&Question::Noul(n)), r#"{"type":"noul","instructions":"U?","criteria":{"true":"urgent"}}"#); 216 let s = Score::new(Json::text("How?"), [Json::text("calm"), Json::text("angry")]).unwrap(); 217 assert_eq!( 218 json(&Question::Score(s)), 219 r#"{"type":"score","instructions":"How?","criteria":["calm","angry"]}"# 220 ); 221 } 222}