1use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10#[cfg(feature = "schema")]
11use utoipa::ToSchema;
12
13pub const MAX_OPTIONS: usize = 20;
19
20pub const MAX_QUESTIONS: usize = 32;
25
26pub const MIN_LEVELS: u8 = 2;
28
29pub const MAX_LEVELS: u8 = 10;
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(deny_unknown_fields)]
42#[cfg_attr(feature = "schema", derive(ToSchema))]
43pub struct SystemOneRequest {
44 pub state: String,
46
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub model: Option<String>,
50
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub calibration: Option<Calibration>,
54
55 pub questions: Vec<Question>,
57}
58
59#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
64#[serde(deny_unknown_fields)]
65#[cfg_attr(feature = "schema", derive(ToSchema))]
66pub struct Calibration {
67 pub temperature: f64,
68}
69
70impl Default for Calibration {
71 fn default() -> Self {
72 Self { temperature: 1.0 }
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(try_from = "RawQuestion")]
83#[cfg_attr(feature = "schema", derive(ToSchema))]
84pub struct Question {
85 pub id: String,
86 #[serde(flatten)]
87 pub kind: QuestionKind,
88}
89
90#[derive(Deserialize)]
92#[serde(deny_unknown_fields)]
93struct RawQuestion {
94 id: String,
95 #[serde(default)]
96 noul: Option<String>,
97 #[serde(default)]
98 choice: Option<ChoiceSpec>,
99 #[serde(default)]
100 score: Option<ScoreSpec>,
101}
102
103impl TryFrom<RawQuestion> for Question {
104 type Error = String;
105
106 fn try_from(raw: RawQuestion) -> Result<Self, Self::Error> {
107 let kind = match (raw.noul, raw.choice, raw.score) {
108 (Some(noul), None, None) => QuestionKind::Noul(noul),
109 (None, Some(choice), None) => QuestionKind::Choice(choice),
110 (None, None, Some(score)) => QuestionKind::Score(score),
111 _ => {
112 return Err(format!(
113 "question {:?} must have exactly one of `noul`, `choice` or `score`",
114 raw.id
115 ));
116 }
117 };
118 Ok(Self { id: raw.id, kind })
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125#[cfg_attr(feature = "schema", derive(ToSchema))]
126pub enum QuestionKind {
127 Noul(String),
129 Choice(ChoiceSpec),
131 Score(ScoreSpec),
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136#[serde(deny_unknown_fields)]
137#[cfg_attr(feature = "schema", derive(ToSchema))]
138pub struct ChoiceSpec {
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub question: Option<String>,
142 pub options: Vec<String>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(deny_unknown_fields)]
147#[cfg_attr(feature = "schema", derive(ToSchema))]
148pub struct ScoreSpec {
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub question: Option<String>,
151 pub levels: LevelSpec,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156#[serde(untagged)]
157#[cfg_attr(feature = "schema", derive(ToSchema))]
158pub enum LevelSpec {
159 Count(u32),
165 Labels(Vec<String>),
167}
168
169impl LevelSpec {
170 pub fn legend(&self) -> Vec<String> {
172 match self {
173 Self::Count(n) => (1..=*n).map(|i| i.to_string()).collect(),
174 Self::Labels(labels) => labels.clone(),
175 }
176 }
177
178 pub fn count(&self) -> usize {
179 match self {
180 Self::Count(n) => *n as usize,
181 Self::Labels(labels) => labels.len(),
182 }
183 }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
191#[cfg_attr(feature = "schema", derive(ToSchema))]
192pub struct SystemOneResponse {
193 pub answers: BTreeMap<String, Answer>,
195 pub model: String,
197 pub usage: Usage,
198 pub timing_ms: Timing,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
217#[serde(tag = "type", rename_all = "snake_case")]
218#[cfg_attr(feature = "schema", derive(ToSchema))]
219pub enum Answer {
220 Noul {
221 noul: f64,
223 raw_logprobs: BTreeMap<String, f64>,
224 truncated: bool,
225 truncated_labels: Vec<String>,
226 label_mass: f64,
227 },
228 Choice {
229 choice: String,
231 index: usize,
233 confidence: f64,
234 probabilities: Vec<OptionProbability>,
236 raw_logprobs: BTreeMap<String, f64>,
237 truncated: bool,
238 truncated_labels: Vec<String>,
239 label_mass: f64,
240 },
241 Score {
242 score: u8,
244 expected_score: f64,
246 legend: String,
248 confidence: f64,
249 probabilities: Vec<LevelProbability>,
250 raw_logprobs: BTreeMap<String, f64>,
251 truncated: bool,
252 truncated_labels: Vec<String>,
253 label_mass: f64,
254 },
255}
256
257impl Answer {
258 pub fn truncated(&self) -> bool {
261 match self {
262 Self::Noul { truncated, .. }
263 | Self::Choice { truncated, .. }
264 | Self::Score { truncated, .. } => *truncated,
265 }
266 }
267
268 pub fn truncated_labels(&self) -> &[String] {
271 match self {
272 Self::Noul {
273 truncated_labels, ..
274 }
275 | Self::Choice {
276 truncated_labels, ..
277 }
278 | Self::Score {
279 truncated_labels, ..
280 } => truncated_labels,
281 }
282 }
283
284 pub fn label_mass(&self) -> f64 {
287 match self {
288 Self::Noul { label_mass, .. }
289 | Self::Choice { label_mass, .. }
290 | Self::Score { label_mass, .. } => *label_mass,
291 }
292 }
293
294 pub fn confidence(&self) -> Option<f64> {
297 match self {
298 Self::Noul { .. } => None,
299 Self::Choice { confidence, .. } | Self::Score { confidence, .. } => Some(*confidence),
300 }
301 }
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
305#[cfg_attr(feature = "schema", derive(ToSchema))]
306pub struct OptionProbability {
307 pub option: String,
308 pub probability: f64,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
312#[cfg_attr(feature = "schema", derive(ToSchema))]
313pub struct LevelProbability {
314 pub level: u8,
315 pub legend: String,
316 pub probability: f64,
317}
318
319#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
320#[cfg_attr(feature = "schema", derive(ToSchema))]
321pub struct Usage {
322 pub input_tokens: u32,
324 pub questions: usize,
325}
326
327#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
328#[cfg_attr(feature = "schema", derive(ToSchema))]
329pub struct Timing {
330 pub total: u64,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
338#[cfg_attr(feature = "schema", derive(ToSchema))]
339pub struct ModelsResponse {
340 pub models: Vec<ModelInfo>,
341 pub default: String,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
346#[cfg_attr(feature = "schema", derive(ToSchema))]
347pub struct ModelInfo {
348 pub alias: String,
350 pub model: String,
352 pub calibration: Calibration,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
360#[cfg_attr(feature = "schema", derive(ToSchema))]
361pub struct ErrorResponse {
362 pub code: ErrorCode,
363 pub message: String,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub question_id: Option<String>,
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
371#[serde(rename_all = "snake_case")]
372#[cfg_attr(feature = "schema", derive(ToSchema))]
373pub enum ErrorCode {
374 TooManyOptions,
376 TooManyQuestions,
378 InvalidRequest,
381 TooFewOptions,
383 InvalidLevels,
385 EmptyState,
386 EmptyQuestion,
387 DuplicateQuestionId,
388 EmptyQuestionId,
390 DuplicateOption,
393 NoQuestions,
394 UnknownModel,
395 InvalidCalibration,
396 NoLabelMatched,
398 HostUnavailable,
399 HostTimeout,
400 Internal,
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use serde_json::json;
408
409 fn question(value: serde_json::Value) -> Result<Question, serde_json::Error> {
410 serde_json::from_value(value)
411 }
412
413 #[test]
414 fn each_primitive_reads_back_from_its_wire_form() {
415 assert!(matches!(
416 question(json!({"id": "u", "noul": "Urgent?"}))
417 .unwrap()
418 .kind,
419 QuestionKind::Noul(_)
420 ));
421 assert!(matches!(
422 question(json!({"id": "t", "choice": {"options": ["a", "b"]}}))
423 .unwrap()
424 .kind,
425 QuestionKind::Choice(_)
426 ));
427 assert!(matches!(
428 question(json!({"id": "s", "score": {"levels": 5}}))
429 .unwrap()
430 .kind,
431 QuestionKind::Score(_)
432 ));
433 }
434
435 #[test]
437 fn a_question_naming_two_primitives_is_refused() {
438 let err = question(json!({
439 "id": "x",
440 "noul": "Urgent?",
441 "choice": {"options": ["a", "b"]}
442 }))
443 .unwrap_err();
444
445 assert!(err.to_string().contains("exactly one"), "{err}");
446 }
447
448 #[test]
449 fn a_question_naming_no_primitive_is_refused() {
450 assert!(question(json!({"id": "x"})).is_err());
451 }
452
453 #[test]
455 fn unknown_fields_are_refused_at_every_level() {
456 assert!(question(json!({"id": "x", "chioce": {"options": ["a", "b"]}})).is_err());
457 assert!(question(json!({"id": "x", "choice": {"options": ["a"], "opts": []}})).is_err());
458
459 let request = json!({
460 "state": "s",
461 "calibraton": {"temperature": 2.0},
462 "questions": [{"id": "u", "noul": "Urgent?"}]
463 });
464 assert!(serde_json::from_value::<SystemOneRequest>(request).is_err());
465 }
466
467 #[test]
469 fn a_question_round_trips() {
470 let wire = json!({"id": "t", "choice": {"question": "Which?", "options": ["a", "b"]}});
471
472 let back = serde_json::to_value(question(wire.clone()).unwrap()).unwrap();
473
474 assert_eq!(back, wire);
475 }
476}