Skip to main content

sokuto_core/
schema.rs

1//! # Jev 互換 API スキーマおよび型定義モジュール
2//!
3//! TypeSafe AI の `POST /v1/systemone` リクエストおよびレスポンスと
4//! 完全な互換性を持つデータ構造を定義する。
5
6use 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/// 質問の決定プリミティブ種別。
15#[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    /// 候補選択(離散選択肢から最適な1つを選択)。
21    Choice,
22    /// 順序尺度評価(2〜10段階の評価尺度から期待実数値を算出)。
23    Score,
24    /// 真偽確率判定(言明が真である確率を直接算出)。
25    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/// 柔軟な評価基準(Criteria)データ構造。
39///
40/// 質問タイプに応じて Map、List、または未指定(None)のいずれかをとる。
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42#[serde(untagged)]
43#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
44pub enum Criteria {
45    /// Choice 型向け: 候補識別子(キー)と説明文(値)の順序付きマップ。
46    Map(IndexMap<String, String>),
47    /// Score 型向け: 順序付けられた評価段階基準の配列(2〜10段階)。
48    List(Vec<String>),
49    /// Noul 型向け、または評価基準が指定されていない状態。
50    None,
51}
52
53impl Criteria {
54    /// Map 形式の参照を取得する。
55    pub fn as_map(&self) -> Option<&IndexMap<String, String>> {
56        match self {
57            Self::Map(map) => Some(map),
58            _ => None,
59        }
60    }
61
62    /// List 形式の参照を取得する。
63    pub fn as_list(&self) -> Option<&[String]> {
64        match self {
65            Self::List(list) => Some(list),
66            _ => None,
67        }
68    }
69
70    /// 未指定(None)であるかどうかを判定する。
71    pub fn is_none(&self) -> bool {
72        matches!(self, Self::None)
73    }
74
75    /// 候補数または段階数を取得する。
76    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    /// 要素が空であるかどうかを判定する。
85    pub fn is_empty(&self) -> bool {
86        self.len() == 0
87    }
88}
89
90/// 単一の質問定義。
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
93pub struct Question {
94    /// 質問プリミティブ種別(`choice`, `score`, `noul`)。
95    #[serde(rename = "type")]
96    #[cfg_attr(feature = "openapi", schema(rename = "type"))]
97    pub question_type: QuestionType,
98
99    /// 評価内容に関する自然言語の指示文。
100    pub instructions: String,
101
102    /// 型依存の評価基準(Choice 時は Map、Score 時は List、Noul 時は省略可能)。
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub criteria: Option<Criteria>,
105}
106
107impl Question {
108    /// 新しい Choice 質問を作成する。
109    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    /// 新しい Score 質問を作成する。
118    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    /// 新しい Noul 質問を作成する。
127    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    /// 質問スキーマの整合性を検証する。
136    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                // Noul 型は criteria を要求しない。指定されている場合でも無視または許容する。
196            }
197        }
198        Ok(())
199    }
200}
201
202/// Jev 互換 `POST /v1/systemone` リクエストペイロード。
203#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
205pub struct SystemOneRequest {
206    /// 使用するローカルモデル識別子(任意)。
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub model: Option<String>,
209
210    /// 判断の材料となる非構造化コンテキストデータ(文字列、オブジェクト、配列など)。
211    ///
212    /// # 備考
213    /// サーバー側の前処理ガードレールパイプライン通過後は、推論バックボーンへ供給するために
214    /// 常に自然言語文字列 (`Value::String`) として標準化および注釈付加が行われる。
215    #[cfg_attr(feature = "openapi", schema(value_type = Object))]
216    pub state: serde_json::Value,
217
218    /// 評価対象となる質問群のマップ(キーは質問識別子)。
219    pub questions: IndexMap<String, Question>,
220
221    /// 確信度ゲーティング処理の設定パラメータ(任意)。
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    pub gating: Option<GatingConfig>,
224
225    /// 候補数 K > 16 の Choice 質問に対する透過的粗密階層ルーティングの制御フラグ (任意)。
226    #[serde(default, skip_serializing_if = "Option::is_none")]
227    pub auto_hierarchical: Option<bool>,
228
229    /// `State` 中間特徴量キャッシュのキー識別子 (`xxHash3` 16進文字列、任意)。
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub state_hash: Option<String>,
232}
233
234impl SystemOneRequest {
235    /// 新規リクエストを作成する。
236    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    /// モデル名を指定してリクエストを作成する。
248    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    /// 透過的粗密階層ルーティングの有効/無効を指定してリクエストを作成する。
264    pub fn with_auto_hierarchical(mut self, auto_hierarchical: bool) -> Self {
265        self.auto_hierarchical = Some(auto_hierarchical);
266        self
267    }
268
269    /// ゲーティング設定を指定してリクエストを作成する。
270    pub fn with_gating(mut self, gating: GatingConfig) -> Self {
271        self.gating = Some(gating);
272        self
273    }
274
275    /// `State` キャッシュキーを指定してリクエストを作成する。
276    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    /// リクエスト全体の整合性を検証する。
282    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/// 単一の質問に対する判定結果。
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
298#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
299pub struct Answer {
300    /// Choice 型の採択候補ラベル。
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub choice: Option<String>,
303
304    /// Score 型の加重平均スコア値。
305    #[serde(default, skip_serializing_if = "Option::is_none")]
306    pub score: Option<f64>,
307
308    /// Noul 型の言明真実確率値(0.0 <= P <= 1.0)。
309    #[serde(default, skip_serializing_if = "Option::is_none")]
310    pub noul: Option<f64>,
311
312    /// 各候補または各段階レベルの確率分布マップ。
313    #[serde(default, skip_serializing_if = "Option::is_none")]
314    pub probabilities: Option<IndexMap<String, f64>>,
315
316    /// 分布の尖り度に基づく正規化確信度(0.0 <= C <= 1.0)。
317    #[serde(default, skip_serializing_if = "Option::is_none")]
318    pub confidence: Option<f64>,
319
320    /// 確信度ゲーティングによるルーティングおよび監査メタデータ(任意)。
321    #[serde(default, skip_serializing_if = "Option::is_none")]
322    pub gating: Option<GatingMetadata>,
323}
324
325impl Answer {
326    /// Choice 判定結果を生成する。
327    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    /// Score 判定結果を生成する。
343    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    /// Noul 判定結果を生成する。
355    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    /// ゲーティングメタデータを付与して自身を返却する。
367    pub fn with_gating(mut self, gating: GatingMetadata) -> Self {
368        self.gating = Some(gating);
369        self
370    }
371
372    /// 割り当てられたルーティング種別を取得する。
373    pub fn route(&self) -> Option<DecisionRoute> {
374        self.gating.as_ref().map(|g| g.route)
375    }
376
377    /// ゲーティング処理等に向けた実効確信度(Effective Confidence)を取得する。
378    ///
379    /// # 概要
380    /// - Choice / Score 型の場合は、構造体に保持されている `confidence` 値をそのまま返却する。
381    /// - Noul 型の場合は、Jev 公式スキーマでは `confidence` フィールドが `None` となる規約があるため、
382    ///   真実確率 $P(\text{true})$ から尖り度 $|2P - 1.0|$ を即座に算出して返却する。
383    ///
384    /// # 戻り値
385    /// - 判定結果に対応する 0.0〜1.0 の確信度実数値。判定結果が存在しない場合は `None`。
386    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/// トークン消費量メタデータ。
398///
399/// System One モデルは文章生成を行わないため、`completion_tokens` は常に 0 となる。
400#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
401#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
402pub struct Usage {
403    /// 入力コンテキスト(State + Questions)のトークン総数。
404    pub prompt_tokens: usize,
405
406    /// 生成完了トークン数(常に 0)。
407    pub completion_tokens: usize,
408
409    /// 処理に要した総トークン数(prompt_tokens と一致)。
410    pub total_tokens: usize,
411}
412
413impl Usage {
414    /// 新規 Usage メタデータを生成する。
415    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/// Jev 互換 `POST /v1/systemone` レスポンスペイロード。
425#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
426#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
427pub struct SystemOneResponse {
428    /// 各 question_id に対応する判定結果マップ。
429    pub answers: IndexMap<String, Answer>,
430
431    /// トークン使用統計情報。
432    pub usage: Usage,
433
434    /// リクエスト全体の集約ルーティングサマリー(任意)。
435    #[serde(default, skip_serializing_if = "Option::is_none")]
436    pub routing: Option<SystemRoutingSummary>,
437}
438
439impl SystemOneResponse {
440    /// 新規レスポンスを生成する。
441    pub fn new(answers: IndexMap<String, Answer>, usage: Usage) -> Self {
442        Self {
443            answers,
444            usage,
445            routing: None,
446        }
447    }
448
449    /// 集約ルーティングサマリーを付与して新規レスポンスを生成する。
450    pub fn with_routing(mut self, routing: SystemRoutingSummary) -> Self {
451        self.routing = Some(routing);
452        self
453    }
454}
455
456/// 単一 `State` に対する一括バッチ質問リクエストペイロード (`POST /v1/systemone/state-batch`)。
457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
458#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
459pub struct StateBatchRequest {
460    /// 使用するローカルモデル識別子 (任意)。
461    #[serde(default, skip_serializing_if = "Option::is_none")]
462    pub model: Option<String>,
463
464    /// 判断の材料となる単一の長文コンテキストデータ。
465    #[cfg_attr(feature = "openapi", schema(value_type = Object))]
466    pub state: serde_json::Value,
467
468    /// 評価対象となる複数質問群のマップ (キーは質問識別子)。
469    pub questions: IndexMap<String, Question>,
470
471    /// 確信度ゲーティング処理の設定パラメータ (任意)。
472    #[serde(default, skip_serializing_if = "Option::is_none")]
473    pub gating: Option<GatingConfig>,
474
475    /// 候補数 $K > 16$ の `Choice` 質問に対する透過的粗密階層ルーティングの制御フラグ (任意)。
476    #[serde(default, skip_serializing_if = "Option::is_none")]
477    pub auto_hierarchical: Option<bool>,
478
479    /// クライアント側事前計算または以前取得した `State` キャッシュキー (任意)。
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub state_hash: Option<String>,
482}
483
484impl StateBatchRequest {
485    /// 新規バッチリクエストを作成する。
486    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    /// `State` キャッシュキーを指定してバッチリクエストを作成する。
498    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    /// モデル名を指定してバッチリクエストを作成する。
504    pub fn with_model(mut self, model: impl Into<String>) -> Self {
505        self.model = Some(model.into());
506        self
507    }
508
509    /// ゲーティング設定を指定してバッチリクエストを作成する。
510    pub fn with_gating(mut self, gating: GatingConfig) -> Self {
511        self.gating = Some(gating);
512        self
513    }
514
515    /// 粗密階層ルーティング制御フラグを指定してバッチリクエストを作成する。
516    pub fn with_auto_hierarchical(mut self, auto_hierarchical: bool) -> Self {
517        self.auto_hierarchical = Some(auto_hierarchical);
518        self
519    }
520
521    /// リクエスト全体の整合性を検証する。
522    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/// 単一 `State` に対する一括バッチ質問レスポンスペイロード (`POST /v1/systemone/state-batch`)。
537#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
538#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
539pub struct StateBatchResponse {
540    /// 各 `question_id` に対応する判定結果マップ。
541    pub answers: IndexMap<String, Answer>,
542
543    /// トークン使用統計情報。
544    pub usage: Usage,
545
546    /// リクエスト全体の集約ルーティングサマリー (任意)。
547    #[serde(default, skip_serializing_if = "Option::is_none")]
548    pub routing: Option<SystemRoutingSummary>,
549
550    /// `State` 特徴量キャッシュがヒットしたかどうかのフラグ。
551    pub cache_hit: bool,
552
553    /// 処理対象となった `State` のキャッシュハッシュ値 (16進文字列、任意)。
554    #[serde(default, skip_serializing_if = "Option::is_none")]
555    pub state_hash: Option<String>,
556}
557
558impl StateBatchResponse {
559    /// 新規バッチレスポンスを生成する。
560    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    /// 集約ルーティングサマリーを付与する。
571    pub fn with_routing(mut self, routing: SystemRoutingSummary) -> Self {
572        self.routing = Some(routing);
573        self
574    }
575
576    /// `State` ハッシュを付与する。
577    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}