1use indexmap::IndexMap;
7use serde::{Deserialize, Serialize};
8use sokuto_core::gating::GatingMetadata;
9use sokuto_core::schema::{Answer, Question};
10use sokuto_runtime::dag::schema::{
11 DagDefinition, DagExecutionResult, DagNode, DagNodeType, NodeCondition, NodeConditionOp,
12 StepExecutionResult,
13};
14use utoipa::ToSchema;
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
18pub struct DagRequest {
19 #[schema(value_type = Object)]
21 pub state: serde_json::Value,
22
23 pub dag: DagDefinitionDto,
25
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub timeout_ms: Option<u64>,
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
33pub struct DagDefinitionDto {
34 pub dag_id: String,
36
37 #[serde(default = "default_timeout_ms")]
39 pub timeout_ms: u64,
40
41 pub entry_node: String,
43
44 pub nodes: IndexMap<String, DagNodeDto>,
46}
47
48fn default_timeout_ms() -> u64 {
49 100
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
54#[serde(rename_all = "snake_case")]
55pub enum DagNodeTypeDto {
56 Inference,
58 Branch,
60 Parallel,
62 Escalate,
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
68pub struct DagNodeDto {
69 pub node_type: DagNodeTypeDto,
71
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub question: Option<Question>,
75
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub conditions: Vec<NodeConditionDto>,
79
80 #[serde(default, skip_serializing_if = "Vec::is_empty")]
82 pub parallel_nodes: Vec<String>,
83
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub join_node: Option<String>,
87
88 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub reason: Option<String>,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
95pub struct NodeConditionDto {
96 pub field: String,
98
99 pub op: NodeConditionOpDto,
101
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 #[schema(value_type = Option<Object>)]
105 pub value: Option<serde_json::Value>,
106
107 pub target_node: String,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
113#[serde(rename_all = "snake_case")]
114pub enum NodeConditionOpDto {
115 Eq,
117 Neq,
119 Gt,
121 Gte,
123 Lt,
125 Lte,
127 In,
129 ConfidenceGte,
131 ConfidenceLt,
133}
134
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
137pub struct DagResponse {
138 pub dag_id: String,
140
141 pub status: String,
143
144 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub final_node: Option<String>,
147
148 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub final_decision: Option<Answer>,
151
152 pub execution_path: Vec<String>,
154
155 pub steps: IndexMap<String, DagStepResultDto>,
157
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub escalation: Option<DagEscalationDetailDto>,
161
162 pub usage: DagUsageDto,
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
168pub struct DagStepResultDto {
169 pub node_id: String,
171
172 pub node_type: DagNodeTypeDto,
174
175 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub answer: Option<Answer>,
178
179 pub duration_ms: f64,
181
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub next_node: Option<String>,
185
186 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub reason: Option<String>,
189}
190
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
193pub struct DagEscalationDetailDto {
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub reason: Option<String>,
197
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub node_id: Option<String>,
201
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub prompt: Option<String>,
205
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub gating: Option<GatingMetadata>,
209}
210
211#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
213pub struct DagUsageDto {
214 pub total_duration_ms: f64,
216
217 pub steps_executed: usize,
219
220 pub completion_tokens: usize,
222}
223
224impl From<DagNodeTypeDto> for DagNodeType {
229 fn from(dto: DagNodeTypeDto) -> Self {
230 match dto {
231 DagNodeTypeDto::Inference => DagNodeType::Inference,
232 DagNodeTypeDto::Branch => DagNodeType::Branch,
233 DagNodeTypeDto::Parallel => DagNodeType::Parallel,
234 DagNodeTypeDto::Escalate => DagNodeType::Escalate,
235 }
236 }
237}
238
239impl From<DagNodeType> for DagNodeTypeDto {
240 fn from(kind: DagNodeType) -> Self {
241 match kind {
242 DagNodeType::Inference => DagNodeTypeDto::Inference,
243 DagNodeType::Branch => DagNodeTypeDto::Branch,
244 DagNodeType::Parallel => DagNodeTypeDto::Parallel,
245 DagNodeType::Escalate => DagNodeTypeDto::Escalate,
246 }
247 }
248}
249
250impl From<NodeConditionOpDto> for NodeConditionOp {
251 fn from(dto: NodeConditionOpDto) -> Self {
252 match dto {
253 NodeConditionOpDto::Eq => NodeConditionOp::Eq,
254 NodeConditionOpDto::Neq => NodeConditionOp::Neq,
255 NodeConditionOpDto::Gt => NodeConditionOp::Gt,
256 NodeConditionOpDto::Gte => NodeConditionOp::Gte,
257 NodeConditionOpDto::Lt => NodeConditionOp::Lt,
258 NodeConditionOpDto::Lte => NodeConditionOp::Lte,
259 NodeConditionOpDto::In => NodeConditionOp::In,
260 NodeConditionOpDto::ConfidenceGte => NodeConditionOp::ConfidenceGte,
261 NodeConditionOpDto::ConfidenceLt => NodeConditionOp::ConfidenceLt,
262 }
263 }
264}
265
266impl From<NodeConditionOp> for NodeConditionOpDto {
267 fn from(op: NodeConditionOp) -> Self {
268 match op {
269 NodeConditionOp::Eq => NodeConditionOpDto::Eq,
270 NodeConditionOp::Neq => NodeConditionOpDto::Neq,
271 NodeConditionOp::Gt => NodeConditionOpDto::Gt,
272 NodeConditionOp::Gte => NodeConditionOpDto::Gte,
273 NodeConditionOp::Lt => NodeConditionOpDto::Lt,
274 NodeConditionOp::Lte => NodeConditionOpDto::Lte,
275 NodeConditionOp::In => NodeConditionOpDto::In,
276 NodeConditionOp::ConfidenceGte => NodeConditionOpDto::ConfidenceGte,
277 NodeConditionOp::ConfidenceLt => NodeConditionOpDto::ConfidenceLt,
278 }
279 }
280}
281
282impl From<NodeConditionDto> for NodeCondition {
283 fn from(dto: NodeConditionDto) -> Self {
284 Self {
285 field: dto.field,
286 op: dto.op.into(),
287 value: dto.value,
288 target_node: dto.target_node,
289 }
290 }
291}
292
293impl From<NodeCondition> for NodeConditionDto {
294 fn from(c: NodeCondition) -> Self {
295 Self {
296 field: c.field,
297 op: c.op.into(),
298 value: c.value,
299 target_node: c.target_node,
300 }
301 }
302}
303
304impl From<DagNodeDto> for DagNode {
305 fn from(dto: DagNodeDto) -> Self {
306 Self {
307 node_type: dto.node_type.into(),
308 question: dto.question,
309 conditions: dto.conditions.into_iter().map(Into::into).collect(),
310 parallel_nodes: dto.parallel_nodes,
311 join_node: dto.join_node,
312 reason: dto.reason,
313 }
314 }
315}
316
317impl From<DagNode> for DagNodeDto {
318 fn from(node: DagNode) -> Self {
319 Self {
320 node_type: node.node_type.into(),
321 question: node.question,
322 conditions: node.conditions.into_iter().map(Into::into).collect(),
323 parallel_nodes: node.parallel_nodes,
324 join_node: node.join_node,
325 reason: node.reason,
326 }
327 }
328}
329
330impl From<DagDefinitionDto> for DagDefinition {
331 fn from(dto: DagDefinitionDto) -> Self {
332 let mut nodes = IndexMap::with_capacity(dto.nodes.len());
333 for (k, v) in dto.nodes {
334 nodes.insert(k, v.into());
335 }
336 Self {
337 dag_id: dto.dag_id,
338 timeout_ms: dto.timeout_ms,
339 entry_node: dto.entry_node,
340 nodes,
341 }
342 }
343}
344
345impl From<DagDefinition> for DagDefinitionDto {
346 fn from(dag: DagDefinition) -> Self {
347 let mut nodes = IndexMap::with_capacity(dag.nodes.len());
348 for (k, v) in dag.nodes {
349 nodes.insert(k, v.into());
350 }
351 Self {
352 dag_id: dag.dag_id,
353 timeout_ms: dag.timeout_ms,
354 entry_node: dag.entry_node,
355 nodes,
356 }
357 }
358}
359
360impl From<StepExecutionResult> for DagStepResultDto {
361 fn from(step: StepExecutionResult) -> Self {
362 Self {
363 node_id: step.node_id,
364 node_type: step.node_type.into(),
365 answer: step.answer,
366 duration_ms: step.duration_ms,
367 next_node: step.next_node,
368 reason: step.reason,
369 }
370 }
371}
372
373impl From<DagExecutionResult> for DagResponse {
374 fn from(res: DagExecutionResult) -> Self {
375 let status = if res.escalated {
376 "escalated".to_string()
377 } else {
378 "completed".to_string()
379 };
380
381 let final_node = res.execution_path.last().cloned();
382 let steps_executed = res.steps.len();
383
384 let mut steps = IndexMap::with_capacity(res.steps.len());
385 for (k, v) in res.steps {
386 steps.insert(k, v.into());
387 }
388
389 let escalation = if res.escalated {
390 Some(DagEscalationDetailDto {
391 reason: res.escalation_reason,
392 node_id: res.escalation_node_id,
393 prompt: res.escalation_prompt,
394 gating: res.escalation_gating,
395 })
396 } else {
397 None
398 };
399
400 Self {
401 dag_id: res.dag_id,
402 status,
403 final_node,
404 final_decision: res.final_answer,
405 execution_path: res.execution_path,
406 steps,
407 escalation,
408 usage: DagUsageDto {
409 total_duration_ms: res.total_duration_ms,
410 steps_executed,
411 completion_tokens: 0,
412 },
413 }
414 }
415}