Skip to main content

typesafe_jev/
question.rs

1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3
4use crate::Content;
5
6/// One typed question: a [`Noul`], a [`Choice`] or a [`Score`], told apart on the wire by its
7/// `type` field.
8///
9/// Each of the three converts with `into()`, which [`Questions::with`] and
10/// [`Questions::insert`] do for you. The enum is `#[non_exhaustive]`: System One may gain
11/// question types.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13#[serde(tag = "type", rename_all = "lowercase")]
14#[non_exhaustive]
15pub enum Question {
16    /// A yes/no question, answered by a [`NoulAnswer`](crate::NoulAnswer).
17    Noul(Noul),
18    /// One option out of a set, answered by a [`ChoiceAnswer`](crate::ChoiceAnswer).
19    Choice(Choice),
20    /// A rating on an ordered scale, answered by a [`ScoreAnswer`](crate::ScoreAnswer).
21    Score(Score),
22}
23
24impl From<Noul> for Question {
25    fn from(question: Noul) -> Self {
26        Question::Noul(question)
27    }
28}
29
30impl From<Choice> for Question {
31    fn from(question: Choice) -> Self {
32        Question::Choice(question)
33    }
34}
35
36impl From<Score> for Question {
37    fn from(question: Score) -> Self {
38        Question::Score(question)
39    }
40}
41
42/// A yes/no question. The answer is the probability that the answer is yes.
43///
44/// ```
45/// use typesafe_jev::{Noul, Question};
46///
47/// let question = Noul::new("Does this convey urgency?")
48///     .yes("Explicitly time-sensitive")
49///     .no("No urgency expressed");
50/// assert_eq!(
51///     serde_json::to_value(Question::from(question))?,
52///     serde_json::json!({
53///         "type": "noul",
54///         "instructions": "Does this convey urgency?",
55///         "criteria": {"true": "Explicitly time-sensitive", "false": "No urgency expressed"},
56///     }),
57/// );
58/// # Ok::<(), serde_json::Error>(())
59/// ```
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61#[non_exhaustive]
62pub struct Noul {
63    /// The yes/no question to evaluate.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub instructions: Option<Content>,
66    /// What a yes and a no mean, when the question alone does not say.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub criteria: Option<NoulCriteria>,
69}
70
71impl Noul {
72    /// A yes/no question with no description of either outcome.
73    pub fn new(instructions: impl Into<Content>) -> Self {
74        Noul { instructions: Some(instructions.into()), criteria: None }
75    }
76
77    /// Describes what a yes (an answer near 1) means.
78    #[must_use]
79    pub fn yes(mut self, meaning: impl Into<Content>) -> Self {
80        self.criteria.get_or_insert_with(NoulCriteria::default).yes = Some(meaning.into());
81        self
82    }
83
84    /// Describes what a no (an answer near 0) means.
85    #[must_use]
86    pub fn no(mut self, meaning: impl Into<Content>) -> Self {
87        self.criteria.get_or_insert_with(NoulCriteria::default).no = Some(meaning.into());
88        self
89    }
90}
91
92/// What the two outcomes of a [`Noul`] mean. On the wire the fields are `true` and `false`.
93#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
94#[non_exhaustive]
95pub struct NoulCriteria {
96    /// What a yes (an answer near 1) means.
97    #[serde(rename = "true", default, skip_serializing_if = "Option::is_none")]
98    pub yes: Option<Content>,
99    /// What a no (an answer near 0) means.
100    #[serde(rename = "false", default, skip_serializing_if = "Option::is_none")]
101    pub no: Option<Content>,
102}
103
104/// Picks one option from a set you define. The answer names the most probable option and gives
105/// the probability of each.
106///
107/// The options keep the order they are given in. The API accepts up to 255 of them.
108///
109/// ```
110/// use typesafe_jev::Choice;
111///
112/// let department = Choice::new("Which team should handle this?", [
113///     ("billing", "Payments, invoicing, refunds"),
114///     ("technical", "Bugs, outages, integrations"),
115/// ])
116/// .option("sales", "Pricing, upgrades, new accounts");
117/// assert_eq!(department.criteria.keys().collect::<Vec<_>>(), ["billing", "technical", "sales"]);
118///
119/// // Options whose names say it all need no description.
120/// let tone = Choice::labels("What is the tone?", ["calm", "frustrated", "angry"]);
121/// assert_eq!(tone.criteria["calm"], None);
122/// ```
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124#[non_exhaustive]
125pub struct Choice {
126    /// What the model should decide.
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub instructions: Option<Content>,
129    /// Each option and its description, in order. `None` (JSON `null`) is an option that needs
130    /// no more detail than its name.
131    pub criteria: IndexMap<String, Option<Content>>,
132}
133
134impl Choice {
135    /// A choice between described options.
136    pub fn new<N, D>(instructions: impl Into<Content>, options: impl IntoIterator<Item = (N, D)>) -> Self
137    where
138        N: Into<String>,
139        D: Into<Content>,
140    {
141        let criteria = options.into_iter().map(|(name, description)| (name.into(), Some(description.into()))).collect();
142        Choice { instructions: Some(instructions.into()), criteria }
143    }
144
145    /// A choice between options that their names describe well enough.
146    pub fn labels<N: Into<String>>(instructions: impl Into<Content>, names: impl IntoIterator<Item = N>) -> Self {
147        let criteria = names.into_iter().map(|name| (name.into(), None)).collect();
148        Choice { instructions: Some(instructions.into()), criteria }
149    }
150
151    /// Adds a described option, or replaces the description of one that is there.
152    #[must_use]
153    pub fn option(mut self, name: impl Into<String>, description: impl Into<Content>) -> Self {
154        self.criteria.insert(name.into(), Some(description.into()));
155        self
156    }
157
158    /// Adds an option with no description.
159    #[must_use]
160    pub fn label(mut self, name: impl Into<String>) -> Self {
161        self.criteria.insert(name.into(), None);
162        self
163    }
164}
165
166/// Rates the state against ordered, described levels. The answer is a probability-weighted
167/// position on that scale, which can fall between two levels.
168///
169/// A level's number is its position in `criteria`, from 0, from the low end of the scale to the
170/// high end. A score should have at least two levels; the API accepts up to 10.
171///
172/// ```
173/// use typesafe_jev::Score;
174///
175/// let frustration = Score::new("How frustrated is the customer?", ["Calm", "Frustrated", "Very angry"]);
176/// assert_eq!(frustration.criteria.len(), 3);
177/// ```
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179#[non_exhaustive]
180pub struct Score {
181    /// What the model should rate.
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub instructions: Option<Content>,
184    /// The description of each level, from level 0 up.
185    pub criteria: Vec<Content>,
186}
187
188impl Score {
189    /// A rating against `levels`, given from the low end of the scale to the high end.
190    pub fn new<L: Into<Content>>(instructions: impl Into<Content>, levels: impl IntoIterator<Item = L>) -> Self {
191        Score { instructions: Some(instructions.into()), criteria: levels.into_iter().map(Into::into).collect() }
192    }
193}
194
195/// The questions of one request, each under an id you choose. The answer comes back under the
196/// same id; the id itself is not shown to the model.
197///
198/// Questions keep the order they are added in, and every question of a request is evaluated
199/// against the same state independently: one answer is never context for another.
200///
201/// ```
202/// use typesafe_jev::{Choice, Noul, Questions, Score};
203///
204/// let questions = Questions::new()
205///     .with("department", Choice::labels("Which team should handle this?", ["billing", "technical", "sales"]))
206///     .with("frustration", Score::new("How frustrated is the customer?", ["Calm", "Frustrated", "Very angry"]))
207///     .with("is_urgent", Noul::new("Does this convey urgency?"));
208/// assert_eq!(questions.len(), 3);
209/// assert_eq!(questions.ids().collect::<Vec<_>>(), ["department", "frustration", "is_urgent"]);
210/// ```
211#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
212#[serde(transparent)]
213pub struct Questions(IndexMap<String, Question>);
214
215impl Questions {
216    /// No questions yet.
217    pub fn new() -> Self {
218        Questions::default()
219    }
220
221    /// Adds a question and returns the set, for chaining.
222    #[must_use]
223    pub fn with(mut self, id: impl Into<String>, question: impl Into<Question>) -> Self {
224        self.insert(id, question);
225        self
226    }
227
228    /// Adds a question. A question already under `id` is replaced, in place, and returned.
229    pub fn insert(&mut self, id: impl Into<String>, question: impl Into<Question>) -> Option<Question> {
230        self.0.insert(id.into(), question.into())
231    }
232
233    /// The question under `id`.
234    pub fn get(&self, id: &str) -> Option<&Question> {
235        self.0.get(id)
236    }
237
238    /// How many questions there are.
239    pub fn len(&self) -> usize {
240        self.0.len()
241    }
242
243    /// Whether there are no questions.
244    pub fn is_empty(&self) -> bool {
245        self.0.is_empty()
246    }
247
248    /// The ids, in order.
249    pub fn ids(&self) -> impl Iterator<Item = &str> {
250        self.0.keys().map(String::as_str)
251    }
252
253    /// The ids and their questions, in order.
254    pub fn iter(&self) -> indexmap::map::Iter<'_, String, Question> {
255        self.0.iter()
256    }
257
258    /// The questions as the map they are.
259    pub fn as_map(&self) -> &IndexMap<String, Question> {
260        &self.0
261    }
262}
263
264impl From<IndexMap<String, Question>> for Questions {
265    fn from(questions: IndexMap<String, Question>) -> Self {
266        Questions(questions)
267    }
268}
269
270impl From<Questions> for IndexMap<String, Question> {
271    fn from(questions: Questions) -> Self {
272        questions.0
273    }
274}
275
276impl<I: Into<String>, Q: Into<Question>> FromIterator<(I, Q)> for Questions {
277    fn from_iter<T: IntoIterator<Item = (I, Q)>>(questions: T) -> Self {
278        Questions(questions.into_iter().map(|(id, question)| (id.into(), question.into())).collect())
279    }
280}
281
282impl<I: Into<String>, Q: Into<Question>> Extend<(I, Q)> for Questions {
283    fn extend<T: IntoIterator<Item = (I, Q)>>(&mut self, questions: T) {
284        self.0.extend(questions.into_iter().map(|(id, question)| (id.into(), question.into())));
285    }
286}
287
288impl IntoIterator for Questions {
289    type Item = (String, Question);
290    type IntoIter = indexmap::map::IntoIter<String, Question>;
291
292    fn into_iter(self) -> Self::IntoIter {
293        self.0.into_iter()
294    }
295}
296
297impl<'a> IntoIterator for &'a Questions {
298    type Item = (&'a String, &'a Question);
299    type IntoIter = indexmap::map::Iter<'a, String, Question>;
300
301    fn into_iter(self) -> Self::IntoIter {
302        self.0.iter()
303    }
304}