1use std::fmt;
7
8use indexmap::IndexMap;
9use serde::{Deserialize, Serialize};
10
11use crate::error::{CoreError, Result};
12use crate::gating::{DecisionRoute, GatingConfig, GatingMetadata, SystemRoutingSummary};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
18#[cfg_attr(feature = "openapi", schema(rename_all = "lowercase"))]
19pub enum QuestionType {
20 Choice,
22 Score,
24 Noul,
26}
27
28impl fmt::Display for QuestionType {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::Choice => write!(f, "choice"),
32 Self::Score => write!(f, "score"),
33 Self::Noul => write!(f, "noul"),
34 }
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42#[serde(untagged)]
43#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
44pub enum Criteria {
45 Map(IndexMap<String, String>),
47 List(Vec<String>),
49 None,
51}
52
53impl Criteria {
54 pub fn as_map(&self) -> Option<&IndexMap<String, String>> {
56 match self {
57 Self::Map(map) => Some(map),
58 _ => None,
59 }
60 }
61
62 pub fn as_list(&self) -> Option<&[String]> {
64 match self {
65 Self::List(list) => Some(list),
66 _ => None,
67 }
68 }
69
70 pub fn is_none(&self) -> bool {
72 matches!(self, Self::None)
73 }
74
75 pub fn len(&self) -> usize {
77 match self {
78 Self::Map(map) => map.len(),
79 Self::List(list) => list.len(),
80 Self::None => 0,
81 }
82 }
83
84 pub fn is_empty(&self) -> bool {
86 self.len() == 0
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
93pub struct Question {
94 #[serde(rename = "type")]
96 #[cfg_attr(feature = "openapi", schema(rename = "type"))]
97 pub question_type: QuestionType,
98
99 pub instructions: String,
101
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub criteria: Option<Criteria>,
105}
106
107impl Question {
108 pub fn new_choice(instructions: impl Into<String>, criteria: IndexMap<String, String>) -> Self {
110 Self {
111 question_type: QuestionType::Choice,
112 instructions: instructions.into(),
113 criteria: Some(Criteria::Map(criteria)),
114 }
115 }
116
117 pub fn new_score(instructions: impl Into<String>, criteria: Vec<String>) -> Self {
119 Self {
120 question_type: QuestionType::Score,
121 instructions: instructions.into(),
122 criteria: Some(Criteria::List(criteria)),
123 }
124 }
125
126 pub fn new_noul(instructions: impl Into<String>) -> Self {
128 Self {
129 question_type: QuestionType::Noul,
130 instructions: instructions.into(),
131 criteria: None,
132 }
133 }
134
135 pub fn validate(&self, _question_id: &str) -> Result<()> {
137 match self.question_type {
138 QuestionType::Choice => {
139 let criteria =
140 self.criteria
141 .as_ref()
142 .ok_or_else(|| CoreError::MissingCriteria {
143 question_type: "choice".to_string(),
144 })?;
145 match criteria {
146 Criteria::Map(map) => {
147 let count = map.len();
148 if count == 0 || count > 255 {
149 return Err(CoreError::InvalidChoiceCount { count });
150 }
151 }
152 Criteria::List(_) => {
153 return Err(CoreError::InvalidCriteriaType {
154 question_type: "choice".to_string(),
155 expected: "Map (オブジェクト)",
156 actual: "List (配列)",
157 });
158 }
159 Criteria::None => {
160 return Err(CoreError::MissingCriteria {
161 question_type: "choice".to_string(),
162 });
163 }
164 }
165 }
166 QuestionType::Score => {
167 let criteria =
168 self.criteria
169 .as_ref()
170 .ok_or_else(|| CoreError::MissingCriteria {
171 question_type: "score".to_string(),
172 })?;
173 match criteria {
174 Criteria::List(list) => {
175 let count = list.len();
176 if !(2..=10).contains(&count) {
177 return Err(CoreError::InvalidScoreLevelCount { count });
178 }
179 }
180 Criteria::Map(_) => {
181 return Err(CoreError::InvalidCriteriaType {
182 question_type: "score".to_string(),
183 expected: "List (配列)",
184 actual: "Map (オブジェクト)",
185 });
186 }
187 Criteria::None => {
188 return Err(CoreError::MissingCriteria {
189 question_type: "score".to_string(),
190 });
191 }
192 }
193 }
194 QuestionType::Noul => {
195 }
197 }
198 Ok(())
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
205pub struct SystemOneRequest {
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub model: Option<String>,
209
210 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
216 pub state: serde_json::Value,
217
218 pub questions: IndexMap<String, Question>,
220
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub gating: Option<GatingConfig>,
224
225 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub auto_hierarchical: Option<bool>,
228
229 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub state_hash: Option<String>,
232}
233
234impl SystemOneRequest {
235 pub fn new(state: impl Into<serde_json::Value>, questions: IndexMap<String, Question>) -> Self {
237 Self {
238 model: None,
239 state: state.into(),
240 questions,
241 gating: None,
242 auto_hierarchical: None,
243 state_hash: None,
244 }
245 }
246
247 pub fn with_model(
249 model: impl Into<String>,
250 state: impl Into<serde_json::Value>,
251 questions: IndexMap<String, Question>,
252 ) -> Self {
253 Self {
254 model: Some(model.into()),
255 state: state.into(),
256 questions,
257 gating: None,
258 auto_hierarchical: None,
259 state_hash: None,
260 }
261 }
262
263 pub fn with_auto_hierarchical(mut self, auto_hierarchical: bool) -> Self {
265 self.auto_hierarchical = Some(auto_hierarchical);
266 self
267 }
268
269 pub fn with_gating(mut self, gating: GatingConfig) -> Self {
271 self.gating = Some(gating);
272 self
273 }
274
275 pub fn with_state_hash(mut self, state_hash: impl Into<String>) -> Self {
277 self.state_hash = Some(state_hash.into());
278 self
279 }
280
281 pub fn validate(&self) -> Result<()> {
283 if self.questions.is_empty() {
284 return Err(CoreError::EmptyQuestions);
285 }
286 for (question_id, question) in &self.questions {
287 question.validate(question_id)?;
288 }
289 if let Some(ref gating) = self.gating {
290 gating.validate()?;
291 }
292 Ok(())
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
298#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
299pub struct Answer {
300 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub choice: Option<String>,
303
304 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub score: Option<f64>,
307
308 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub noul: Option<f64>,
311
312 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub probabilities: Option<IndexMap<String, f64>>,
315
316 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub confidence: Option<f64>,
319
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub gating: Option<GatingMetadata>,
323}
324
325impl Answer {
326 pub fn choice(
328 selected: impl Into<String>,
329 probabilities: IndexMap<String, f64>,
330 confidence: f64,
331 ) -> Self {
332 Self {
333 choice: Some(selected.into()),
334 score: None,
335 noul: None,
336 probabilities: Some(probabilities),
337 confidence: Some(confidence),
338 gating: None,
339 }
340 }
341
342 pub fn score(value: f64, probabilities: IndexMap<String, f64>, confidence: f64) -> Self {
344 Self {
345 choice: None,
346 score: Some(value),
347 noul: None,
348 probabilities: Some(probabilities),
349 confidence: Some(confidence),
350 gating: None,
351 }
352 }
353
354 pub fn noul(probability: f64) -> Self {
356 Self {
357 choice: None,
358 score: None,
359 noul: Some(probability),
360 probabilities: None,
361 confidence: None,
362 gating: None,
363 }
364 }
365
366 pub fn with_gating(mut self, gating: GatingMetadata) -> Self {
368 self.gating = Some(gating);
369 self
370 }
371
372 pub fn route(&self) -> Option<DecisionRoute> {
374 self.gating.as_ref().map(|g| g.route)
375 }
376
377 pub fn effective_confidence(&self) -> Option<f64> {
387 if let Some(conf) = self.confidence {
388 return Some(conf);
389 }
390 if let Some(p) = self.noul {
391 return Some((2.0 * p - 1.0).abs().clamp(0.0, 1.0));
392 }
393 None
394 }
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
401#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
402pub struct Usage {
403 pub prompt_tokens: usize,
405
406 pub completion_tokens: usize,
408
409 pub total_tokens: usize,
411}
412
413impl Usage {
414 pub fn new(prompt_tokens: usize) -> Self {
416 Self {
417 prompt_tokens,
418 completion_tokens: 0,
419 total_tokens: prompt_tokens,
420 }
421 }
422}
423
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
426#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
427pub struct SystemOneResponse {
428 pub answers: IndexMap<String, Answer>,
430
431 pub usage: Usage,
433
434 #[serde(default, skip_serializing_if = "Option::is_none")]
436 pub routing: Option<SystemRoutingSummary>,
437}
438
439impl SystemOneResponse {
440 pub fn new(answers: IndexMap<String, Answer>, usage: Usage) -> Self {
442 Self {
443 answers,
444 usage,
445 routing: None,
446 }
447 }
448
449 pub fn with_routing(mut self, routing: SystemRoutingSummary) -> Self {
451 self.routing = Some(routing);
452 self
453 }
454}
455
456#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
458#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
459pub struct StateBatchRequest {
460 #[serde(default, skip_serializing_if = "Option::is_none")]
462 pub model: Option<String>,
463
464 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
466 pub state: serde_json::Value,
467
468 pub questions: IndexMap<String, Question>,
470
471 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub gating: Option<GatingConfig>,
474
475 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub auto_hierarchical: Option<bool>,
478
479 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub state_hash: Option<String>,
482}
483
484impl StateBatchRequest {
485 pub fn new(state: impl Into<serde_json::Value>, questions: IndexMap<String, Question>) -> Self {
487 Self {
488 model: None,
489 state: state.into(),
490 questions,
491 gating: None,
492 auto_hierarchical: None,
493 state_hash: None,
494 }
495 }
496
497 pub fn with_state_hash(mut self, state_hash: impl Into<String>) -> Self {
499 self.state_hash = Some(state_hash.into());
500 self
501 }
502
503 pub fn with_model(mut self, model: impl Into<String>) -> Self {
505 self.model = Some(model.into());
506 self
507 }
508
509 pub fn with_gating(mut self, gating: GatingConfig) -> Self {
511 self.gating = Some(gating);
512 self
513 }
514
515 pub fn with_auto_hierarchical(mut self, auto_hierarchical: bool) -> Self {
517 self.auto_hierarchical = Some(auto_hierarchical);
518 self
519 }
520
521 pub fn validate(&self) -> Result<()> {
523 if self.questions.is_empty() {
524 return Err(CoreError::EmptyQuestions);
525 }
526 for (question_id, question) in &self.questions {
527 question.validate(question_id)?;
528 }
529 if let Some(ref gating) = self.gating {
530 gating.validate()?;
531 }
532 Ok(())
533 }
534}
535
536#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
538#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
539pub struct StateBatchResponse {
540 pub answers: IndexMap<String, Answer>,
542
543 pub usage: Usage,
545
546 #[serde(default, skip_serializing_if = "Option::is_none")]
548 pub routing: Option<SystemRoutingSummary>,
549
550 pub cache_hit: bool,
552
553 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub state_hash: Option<String>,
556}
557
558impl StateBatchResponse {
559 pub fn new(answers: IndexMap<String, Answer>, usage: Usage, cache_hit: bool) -> Self {
561 Self {
562 answers,
563 usage,
564 routing: None,
565 cache_hit,
566 state_hash: None,
567 }
568 }
569
570 pub fn with_routing(mut self, routing: SystemRoutingSummary) -> Self {
572 self.routing = Some(routing);
573 self
574 }
575
576 pub fn with_state_hash(mut self, state_hash: impl Into<String>) -> Self {
578 self.state_hash = Some(state_hash.into());
579 self
580 }
581}