Skip to main content

sokuto_runtime/dag/
schema.rs

1//! # 宣言的 DAG 定義スキーマモジュール
2//!
3//! 非自己回帰型判断エンジンにおける複合ルールや反事実判定を有向非巡回グラフとして
4//! 表現するためのデータ構造、ノード種別、および条件判定規則を定義する。
5
6use indexmap::IndexMap;
7use serde::{Deserialize, Serialize};
8use sokuto_core::schema::{Answer, Question};
9
10/// 宣言的 DAG 定義構造体。
11///
12/// 複数の推論ノード、分岐ノード、並行ノード、エスカレーションノードから構成され、
13/// 各種ルールの決定論的オーケストレーションを定義する。
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DagDefinition {
16    /// DAG の一意識別子。
17    pub dag_id: String,
18    /// 全体タイムアウト時間 (ミリ秒)。既定値は 100ms。
19    #[serde(default = "default_timeout_ms")]
20    pub timeout_ms: u64,
21    /// 実行開始ノードの識別子。
22    pub entry_node: String,
23    /// DAG を構成するノードマップ。
24    pub nodes: IndexMap<String, DagNode>,
25}
26
27fn default_timeout_ms() -> u64 {
28    100
29}
30
31/// DAG ノード種別。
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum DagNodeType {
35    /// 単一質問のモデル推論を実行するノード。
36    Inference,
37    /// 先行ノードの判定値・確信度のみに基づいて遷移先を決定する純粋な条件分岐ノード。
38    Branch,
39    /// 独立した複数の質問を並行実行し合流するノード。
40    Parallel,
41    /// 確信度不足や例外検知時に上位 LLM または人手へ委ねるエスカレーションノード。
42    Escalate,
43}
44
45/// DAG を構成する個々のノード定義。
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct DagNode {
48    /// ノードの種別。
49    pub node_type: DagNodeType,
50    /// 推論ノード (`Inference`) で実行する質問仕様。
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub question: Option<Question>,
53    /// ノード完了後の条件付き遷移規則リスト。先頭から順に評価される。
54    #[serde(default, skip_serializing_if = "Vec::is_empty")]
55    pub conditions: Vec<NodeCondition>,
56    /// 並行実行対象となるノード ID 一覧 (`Parallel` ノード用)。
57    #[serde(default, skip_serializing_if = "Vec::is_empty")]
58    pub parallel_nodes: Vec<String>,
59    /// 並行実行完了後の合流先ノード ID (`Parallel` ノード用)。
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub join_node: Option<String>,
62    /// エスカレーションまたは分岐の理由説明文。
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub reason: Option<String>,
65}
66
67/// 条件付き遷移規則。
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct NodeCondition {
70    /// 評価対象フィールド (`"answer"`, `"confidence"`, `"route"` など)。
71    pub field: String,
72    /// 比較演算子。
73    pub op: NodeConditionOp,
74    /// 比較対象の値。
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub value: Option<serde_json::Value>,
77    /// 条件合致時の遷移先ノード ID。
78    pub target_node: String,
79}
80
81/// 条件評価演算子。
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum NodeConditionOp {
85    /// 等価 (`==`)。
86    Eq,
87    /// 不等価 (`!=`)。
88    Neq,
89    /// より大きい (`>`)。
90    Gt,
91    /// 以上 (`>=`)。
92    Gte,
93    /// より小さい (`<`)。
94    Lt,
95    /// 以下 (`<=`)。
96    Lte,
97    /// 配列に含まれる (`in`)。
98    In,
99    /// 確信度が指定値以上。
100    ConfidenceGte,
101    /// 確信度が指定値未満。
102    ConfidenceLt,
103}
104
105/// 単一ステップの実行結果記録。
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct StepExecutionResult {
108    /// 実行ノード ID。
109    pub node_id: String,
110    /// ノード種別。
111    pub node_type: DagNodeType,
112    /// 推論ノードの場合の判定結果。
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub answer: Option<Answer>,
115    /// ステップ実行所要時間 (ミリ秒)。
116    pub duration_ms: f64,
117    /// 次に遷移したノード ID。終端の場合は `None`。
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub next_node: Option<String>,
120    /// ノードの理由または補足メッセージ。
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub reason: Option<String>,
123}
124
125/// DAG 全体の実行結果。
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct DagExecutionResult {
128    /// 実行された DAG の識別子。
129    pub dag_id: String,
130    /// 辿ったノード ID の実行経路。
131    pub execution_path: Vec<String>,
132    /// 各ステップの詳細実行結果マップ。
133    pub steps: IndexMap<String, StepExecutionResult>,
134    /// DAG 全体の実行所要時間 (ミリ秒)。
135    pub total_duration_ms: f64,
136    /// エスカレーションノードに到達して終了したか否か。
137    pub escalated: bool,
138    /// エスカレーション理由 (該当する場合)。
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub escalation_reason: Option<String>,
141    /// エスカレーションをトリガーした直前ノード ID。
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub escalation_node_id: Option<String>,
144    /// 上位 LLM (System 2) 向けの診断 Guided CoT プロンプト案。
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub escalation_prompt: Option<String>,
147    /// エスカレーション要因となった確信度ゲーティング詳細メタデータ。
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub escalation_gating: Option<sokuto_core::gating::GatingMetadata>,
150    /// 最終決定 Answer (最後の推論ノードの結果)。
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub final_answer: Option<Answer>,
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_deserialize_dag_definition() {
161        let json_data = r#"{
162            "dag_id": "test_dag",
163            "timeout_ms": 50,
164            "entry_node": "step1",
165            "nodes": {
166                "step1": {
167                    "node_type": "inference",
168                    "question": {
169                        "type": "noul",
170                        "instructions": "セール品ですか?"
171                    },
172                    "conditions": [
173                        { "field": "answer", "op": "eq", "value": true, "target_node": "escalate" },
174                        { "field": "answer", "op": "eq", "value": false, "target_node": "step2" }
175                    ]
176                },
177                "step2": {
178                    "node_type": "branch",
179                    "conditions": []
180                },
181                "escalate": {
182                    "node_type": "escalate",
183                    "reason": "セール品のため返品不可。"
184                }
185            }
186        }"#;
187
188        let dag: DagDefinition = serde_json::from_str(json_data).expect("パース成功すること。");
189        assert_eq!(dag.dag_id, "test_dag");
190        assert_eq!(dag.timeout_ms, 50);
191        assert_eq!(dag.entry_node, "step1");
192        assert_eq!(dag.nodes.len(), 3);
193        assert_eq!(dag.nodes["step1"].node_type, DagNodeType::Inference);
194        assert_eq!(dag.nodes["escalate"].node_type, DagNodeType::Escalate);
195    }
196}