1use indexmap::IndexMap;
7use serde::{Deserialize, Serialize};
8use sokuto_core::schema::{Answer, Question};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DagDefinition {
16 pub dag_id: String,
18 #[serde(default = "default_timeout_ms")]
20 pub timeout_ms: u64,
21 pub entry_node: String,
23 pub nodes: IndexMap<String, DagNode>,
25}
26
27fn default_timeout_ms() -> u64 {
28 100
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum DagNodeType {
35 Inference,
37 Branch,
39 Parallel,
41 Escalate,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct DagNode {
48 pub node_type: DagNodeType,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub question: Option<Question>,
53 #[serde(default, skip_serializing_if = "Vec::is_empty")]
55 pub conditions: Vec<NodeCondition>,
56 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub parallel_nodes: Vec<String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub join_node: Option<String>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub reason: Option<String>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct NodeCondition {
70 pub field: String,
72 pub op: NodeConditionOp,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub value: Option<serde_json::Value>,
77 pub target_node: String,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum NodeConditionOp {
85 Eq,
87 Neq,
89 Gt,
91 Gte,
93 Lt,
95 Lte,
97 In,
99 ConfidenceGte,
101 ConfidenceLt,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct StepExecutionResult {
108 pub node_id: String,
110 pub node_type: DagNodeType,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub answer: Option<Answer>,
115 pub duration_ms: f64,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub next_node: Option<String>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub reason: Option<String>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct DagExecutionResult {
128 pub dag_id: String,
130 pub execution_path: Vec<String>,
132 pub steps: IndexMap<String, StepExecutionResult>,
134 pub total_duration_ms: f64,
136 pub escalated: bool,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub escalation_reason: Option<String>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub escalation_node_id: Option<String>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub escalation_prompt: Option<String>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub escalation_gating: Option<sokuto_core::gating::GatingMetadata>,
150 #[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}