1use std::sync::Arc;
7use std::time::{Duration, Instant};
8
9use indexmap::IndexMap;
10use sokuto_core::contract::calibration::CalibrationConfig;
11use sokuto_core::gating::DecisionRoute;
12use sokuto_core::schema::Answer;
13
14use crate::dag::interpolate::{NodeContextValue, interpolate_prompt, interpolate_question};
15use crate::dag::pool::PoolError;
16use crate::dag::schema::{
17 DagDefinition, DagExecutionResult, DagNodeType, NodeCondition, NodeConditionOp,
18 StepExecutionResult,
19};
20use crate::dag::validator::{DagValidationError, validate_dag};
21use crate::engine::InferenceEngine;
22use crate::error::RuntimeError;
23use crate::tokenizer::JevTokenizer;
24
25#[derive(Debug, thiserror::Error)]
27pub enum DagError {
28 #[error("DAG 検証エラー: {0}。")]
30 Validation(#[from] DagValidationError),
31
32 #[error("セッションプールエラー: {0}。")]
34 Pool(#[from] PoolError),
35
36 #[error("DAG 実行がタイムアウトしました ({0}ms)。")]
38 Timeout(u64),
39
40 #[error("DAG 実行ステップ数が上限 ({0}) を超過しました。")]
42 StepLimitExceeded(usize),
43
44 #[error("ノード '{0}' が見つかりません。")]
46 NodeNotFound(String),
47
48 #[error("推論ランタイムエラー: {0}。")]
50 Runtime(#[from] RuntimeError),
51
52 #[error("DAG 実行失敗: {0}。")]
54 ExecutionFailed(String),
55}
56
57pub struct InProcessDagExecutor {
62 engine: Arc<InferenceEngine>,
63 tokenizer: Arc<JevTokenizer>,
64 calib_config: CalibrationConfig,
65 max_steps: usize,
66}
67
68impl InProcessDagExecutor {
69 pub fn new(
76 engine: Arc<InferenceEngine>,
77 tokenizer: Arc<JevTokenizer>,
78 calib_config: CalibrationConfig,
79 ) -> Self {
80 Self {
81 engine,
82 tokenizer,
83 calib_config,
84 max_steps: 50,
85 }
86 }
87
88 pub fn with_max_steps(mut self, max_steps: usize) -> Self {
90 self.max_steps = max_steps;
91 self
92 }
93
94 pub async fn execute(
102 &self,
103 state: &str,
104 dag: &DagDefinition,
105 ) -> Result<DagExecutionResult, DagError> {
106 validate_dag(dag)?;
108
109 let timeout_duration = Duration::from_millis(dag.timeout_ms);
110 let start_time = Instant::now();
111
112 let exec_result = tokio::time::timeout(timeout_duration, self.run_dag(state, dag)).await;
114
115 match exec_result {
116 Ok(Ok(mut res)) => {
117 res.total_duration_ms = start_time.elapsed().as_secs_f64() * 1000.0;
118 Ok(res)
119 }
120 Ok(Err(err)) => Err(err),
121 Err(_) => Err(DagError::Timeout(dag.timeout_ms)),
122 }
123 }
124
125 async fn run_dag(
127 &self,
128 initial_state: &str,
129 dag: &DagDefinition,
130 ) -> Result<DagExecutionResult, DagError> {
131 let mut context: IndexMap<String, NodeContextValue> = IndexMap::new();
132 let mut steps: IndexMap<String, StepExecutionResult> = IndexMap::new();
133 let mut execution_path: Vec<String> = Vec::new();
134
135 let mut scratch_state = String::with_capacity(initial_state.len() + 64);
137
138 let mut curr_node_id = dag.entry_node.clone();
139 let mut step_count = 0;
140 let mut final_answer: Option<Answer> = None;
141 let mut escalated = false;
142 let mut escalation_reason = None;
143 let mut escalation_node_id = None;
144 let mut escalation_prompt = None;
145 let mut escalation_gating = None;
146
147 while step_count < self.max_steps {
148 step_count += 1;
149 let node = dag
150 .nodes
151 .get(&curr_node_id)
152 .ok_or_else(|| DagError::NodeNotFound(curr_node_id.clone()))?;
153
154 execution_path.push(curr_node_id.clone());
155 let step_start = Instant::now();
156
157 match node.node_type {
158 DagNodeType::Inference => {
159 let raw_question = node
160 .question
161 .as_ref()
162 .ok_or_else(|| DagError::NodeNotFound(curr_node_id.clone()))?;
163
164 interpolate_prompt(initial_state, &context, &mut scratch_state);
166 let interpolated_question = interpolate_question(raw_question, &context);
167
168 let engine = self.engine.clone();
170 let tokenizer = self.tokenizer.clone();
171 let calib_config = self.calib_config.clone();
172 let current_state = scratch_state.clone();
173
174 let answer = tokio::task::spawn_blocking(move || {
175 engine.predict_single_question(
176 &tokenizer,
177 ¤t_state,
178 &interpolated_question,
179 &calib_config,
180 )
181 })
182 .await
183 .map_err(|e| DagError::ExecutionFailed(format!("スレッド結合失敗: {e}。")))?
184 .map_err(DagError::Runtime)?;
185
186 let ctx_val = answer_to_context_value(&answer);
188 context.insert(curr_node_id.clone(), ctx_val);
189 final_answer = Some(answer.clone());
190
191 let next_node = evaluate_conditions(&node.conditions, Some(&answer), &context);
193 let duration_ms = step_start.elapsed().as_secs_f64() * 1000.0;
194
195 steps.insert(
196 curr_node_id.clone(),
197 StepExecutionResult {
198 node_id: curr_node_id.clone(),
199 node_type: DagNodeType::Inference,
200 answer: Some(answer),
201 duration_ms,
202 next_node: next_node.clone(),
203 reason: node.reason.clone(),
204 },
205 );
206
207 match next_node {
208 Some(nxt) => curr_node_id = nxt,
209 None => break,
210 }
211 }
212 DagNodeType::Branch => {
213 let next_node =
214 evaluate_conditions(&node.conditions, final_answer.as_ref(), &context);
215 let duration_ms = step_start.elapsed().as_secs_f64() * 1000.0;
216
217 steps.insert(
218 curr_node_id.clone(),
219 StepExecutionResult {
220 node_id: curr_node_id.clone(),
221 node_type: DagNodeType::Branch,
222 answer: None,
223 duration_ms,
224 next_node: next_node.clone(),
225 reason: node.reason.clone(),
226 },
227 );
228
229 match next_node {
230 Some(nxt) => curr_node_id = nxt,
231 None => break,
232 }
233 }
234 DagNodeType::Parallel => {
235 let mut tasks = Vec::with_capacity(node.parallel_nodes.len());
236
237 for p_node_id in &node.parallel_nodes {
238 let p_node = dag
239 .nodes
240 .get(p_node_id)
241 .ok_or_else(|| DagError::NodeNotFound(p_node_id.clone()))?;
242
243 let raw_question = p_node.question.as_ref().ok_or_else(|| {
244 DagError::ExecutionFailed(format!(
245 "並行ノード '{p_node_id}' に question が存在しません。"
246 ))
247 })?;
248
249 interpolate_prompt(initial_state, &context, &mut scratch_state);
251 let interpolated_question = interpolate_question(raw_question, &context);
252
253 let engine = self.engine.clone();
254 let tokenizer = self.tokenizer.clone();
255 let calib_config = self.calib_config.clone();
256 let node_id_owned = p_node_id.clone();
257 let state_owned = scratch_state.clone();
258
259 let handle = tokio::task::spawn_blocking(move || {
260 let ans = engine.predict_single_question(
261 &tokenizer,
262 &state_owned,
263 &interpolated_question,
264 &calib_config,
265 );
266 (node_id_owned, ans)
267 });
268 tasks.push(handle);
269 }
270
271 let results = futures_util::future::join_all(tasks).await;
273
274 for res in results {
275 let (p_id, ans_res) = res.map_err(|e| {
276 DagError::ExecutionFailed(format!("並行タスク結合失敗: {e}。"))
277 })?;
278 let ans = ans_res.map_err(DagError::Runtime)?;
279 let ctx_val = answer_to_context_value(&ans);
280 context.insert(p_id.clone(), ctx_val);
281
282 steps.insert(
283 p_id.clone(),
284 StepExecutionResult {
285 node_id: p_id,
286 node_type: DagNodeType::Inference,
287 answer: Some(ans),
288 duration_ms: step_start.elapsed().as_secs_f64() * 1000.0,
289 next_node: node.join_node.clone(),
290 reason: None,
291 },
292 );
293 }
294
295 let duration_ms = step_start.elapsed().as_secs_f64() * 1000.0;
296 let next_node = node.join_node.clone();
297
298 steps.insert(
299 curr_node_id.clone(),
300 StepExecutionResult {
301 node_id: curr_node_id.clone(),
302 node_type: DagNodeType::Parallel,
303 answer: None,
304 duration_ms,
305 next_node: next_node.clone(),
306 reason: node.reason.clone(),
307 },
308 );
309
310 match next_node {
311 Some(nxt) => curr_node_id = nxt,
312 None => break,
313 }
314 }
315 DagNodeType::Escalate => {
316 let duration_ms = step_start.elapsed().as_secs_f64() * 1000.0;
317 escalated = true;
318 escalation_reason = node.reason.clone();
319
320 let trigger_node_id = execution_path.iter().rev().nth(1).cloned();
322 escalation_node_id = trigger_node_id.clone();
323
324 escalation_gating = final_answer.as_ref().and_then(|a| a.gating.clone());
326
327 if let Some(ref g) = escalation_gating
329 && let Some(ref esc_ctx) = g.escalation
330 && let Some(ref tmpl) = esc_ctx.prompt_template
331 {
332 escalation_prompt = Some(tmpl.clone());
333 } else if let Some(ref trig_id) = trigger_node_id
334 && let Some(trig_node) = dag.nodes.get(trig_id)
335 && let Some(ref q) = trig_node.question
336 && let Some(ref ans) = final_answer
337 && let Some(ref g) = escalation_gating
338 {
339 use sokuto_core::gating::CandidateProbability;
340 let candidates: Vec<CandidateProbability> = ans
341 .probabilities
342 .as_ref()
343 .map(|p| {
344 let mut list: Vec<_> = p
345 .iter()
346 .map(|(k, &v)| CandidateProbability {
347 candidate: k.clone(),
348 probability: v,
349 })
350 .collect();
351 list.sort_by(|a, b| {
352 b.probability
353 .partial_cmp(&a.probability)
354 .unwrap_or(std::cmp::Ordering::Equal)
355 });
356 list
357 })
358 .unwrap_or_default();
359
360 use crate::engine::escalation::build_rich_escalation_prompt;
361 escalation_prompt = Some(build_rich_escalation_prompt(
362 Some(initial_state),
363 Some(trig_id.as_str()),
364 Some(q),
365 g,
366 &candidates,
367 ));
368 } else {
369 escalation_prompt = node.reason.clone();
370 }
371
372 steps.insert(
373 curr_node_id.clone(),
374 StepExecutionResult {
375 node_id: curr_node_id.clone(),
376 node_type: DagNodeType::Escalate,
377 answer: None,
378 duration_ms,
379 next_node: None,
380 reason: node.reason.clone(),
381 },
382 );
383 break;
384 }
385 }
386 }
387
388 if step_count >= self.max_steps {
389 return Err(DagError::StepLimitExceeded(self.max_steps));
390 }
391
392 Ok(DagExecutionResult {
393 dag_id: dag.dag_id.clone(),
394 execution_path,
395 steps,
396 total_duration_ms: 0.0,
397 escalated,
398 escalation_reason,
399 escalation_node_id,
400 escalation_prompt,
401 escalation_gating,
402 final_answer,
403 })
404 }
405}
406
407fn answer_to_context_value(answer: &Answer) -> NodeContextValue {
409 let conf = answer.confidence.unwrap_or(0.0);
410 if let Some(ref c) = answer.choice {
411 NodeContextValue::new(c.clone(), conf)
412 } else if let Some(n) = answer.noul {
413 NodeContextValue::new(if n >= 0.5 { "true" } else { "false" }, conf)
414 } else if let Some(s) = answer.score {
415 NodeContextValue::new(format!("{s}"), conf)
416 } else {
417 NodeContextValue::new("null", conf)
418 }
419}
420
421pub fn evaluate_conditions(
423 conditions: &[NodeCondition],
424 answer: Option<&Answer>,
425 context: &IndexMap<String, NodeContextValue>,
426) -> Option<String> {
427 for cond in conditions {
428 if matches_condition(cond, answer, context) {
429 return Some(cond.target_node.clone());
430 }
431 }
432 None
433}
434
435fn matches_condition(
437 cond: &NodeCondition,
438 answer: Option<&Answer>,
439 context: &IndexMap<String, NodeContextValue>,
440) -> bool {
441 let field = cond.field.as_str();
442
443 if field == "confidence" {
445 let conf = answer.and_then(|a| a.confidence).unwrap_or(0.0);
446 let target_val = match &cond.value {
447 Some(serde_json::Value::Number(n)) => n.as_f64().unwrap_or(0.0),
448 _ => 0.0,
449 };
450 return match cond.op {
451 NodeConditionOp::ConfidenceGte | NodeConditionOp::Gte => conf >= target_val - 1e-6,
452 NodeConditionOp::ConfidenceLt | NodeConditionOp::Lt => conf < target_val - 1e-6,
453 NodeConditionOp::Gt => conf > target_val + 1e-6,
454 NodeConditionOp::Lte => conf <= target_val + 1e-6,
455 NodeConditionOp::Eq => (conf - target_val).abs() < 1e-6,
456 NodeConditionOp::Neq => (conf - target_val).abs() >= 1e-6,
457 _ => false,
458 };
459 }
460
461 if field == "route" {
463 let route_str = answer
464 .and_then(|a| a.gating.as_ref())
465 .map(|g| match g.route {
466 DecisionRoute::AutoExecute => "auto_execute",
467 DecisionRoute::ConfirmOrEscalate => "confirm_or_escalate",
468 DecisionRoute::Fallback => "fallback",
469 })
470 .unwrap_or("auto_execute");
471
472 let target_str = cond.value.as_ref().and_then(|v| v.as_str()).unwrap_or("");
473 return match cond.op {
474 NodeConditionOp::Eq => route_str == target_str,
475 NodeConditionOp::Neq => route_str != target_str,
476 _ => false,
477 };
478 }
479
480 if (field == "answer" || field == "value")
482 && let Some(ans) = answer
483 {
484 if let Some(noul_val) = ans.noul {
486 let target_bool = match &cond.value {
488 Some(serde_json::Value::Bool(b)) => Some(*b),
489 Some(serde_json::Value::String(s)) => match s.as_str() {
490 "true" => Some(true),
491 "false" => Some(false),
492 _ => None,
493 },
494 _ => None,
495 };
496 if let Some(tb) = target_bool {
497 let is_true = noul_val >= 0.5;
498 return match cond.op {
499 NodeConditionOp::Eq => is_true == tb,
500 NodeConditionOp::Neq => is_true != tb,
501 _ => false,
502 };
503 }
504
505 if let Some(target_num) = cond.value.as_ref().and_then(|v| v.as_f64()) {
507 return match cond.op {
508 NodeConditionOp::Eq => (noul_val - target_num).abs() < 1e-6,
509 NodeConditionOp::Neq => (noul_val - target_num).abs() >= 1e-6,
510 NodeConditionOp::Gt => noul_val > target_num + 1e-6,
511 NodeConditionOp::Gte => noul_val >= target_num - 1e-6,
512 NodeConditionOp::Lt => noul_val < target_num - 1e-6,
513 NodeConditionOp::Lte => noul_val <= target_num + 1e-6,
514 _ => false,
515 };
516 }
517 }
518
519 if let Some(ref choice_val) = ans.choice {
521 match cond.op {
522 NodeConditionOp::Eq => {
523 let target_str = cond.value.as_ref().and_then(|v| v.as_str()).unwrap_or("");
524 return choice_val == target_str;
525 }
526 NodeConditionOp::Neq => {
527 let target_str = cond.value.as_ref().and_then(|v| v.as_str()).unwrap_or("");
528 return choice_val != target_str;
529 }
530 NodeConditionOp::In => {
531 if let Some(serde_json::Value::Array(arr)) = &cond.value {
532 return arr
533 .iter()
534 .any(|item| item.as_str().is_some_and(|s| s == choice_val));
535 }
536 }
537 _ => {}
538 }
539 }
540
541 if let Some(score_val) = ans.score {
543 let target_num = cond.value.as_ref().and_then(|v| v.as_f64()).unwrap_or(0.0);
544 return match cond.op {
545 NodeConditionOp::Eq => (score_val - target_num).abs() < 1e-6,
546 NodeConditionOp::Neq => (score_val - target_num).abs() >= 1e-6,
547 NodeConditionOp::Gt => score_val > target_num + 1e-6,
548 NodeConditionOp::Gte => score_val >= target_num - 1e-6,
549 NodeConditionOp::Lt => score_val < target_num - 1e-6,
550 NodeConditionOp::Lte => score_val <= target_num + 1e-6,
551 _ => false,
552 };
553 }
554 }
555
556 if let Some((node_id, prop)) = field.split_once('.')
558 && let Some(ctx_val) = context.get(node_id)
559 {
560 match prop {
561 "value" | "answer" => {
562 let target_str = cond.value.as_ref().and_then(|v| v.as_str()).unwrap_or("");
563 return match cond.op {
564 NodeConditionOp::Eq => ctx_val.value == target_str,
565 NodeConditionOp::Neq => ctx_val.value != target_str,
566 _ => false,
567 };
568 }
569 "confidence" => {
570 let target_num = cond.value.as_ref().and_then(|v| v.as_f64()).unwrap_or(0.0);
571 return match cond.op {
572 NodeConditionOp::ConfidenceGte | NodeConditionOp::Gte => {
573 ctx_val.confidence >= target_num - 1e-6
574 }
575 NodeConditionOp::ConfidenceLt | NodeConditionOp::Lt => {
576 ctx_val.confidence < target_num - 1e-6
577 }
578 _ => false,
579 };
580 }
581 _ => {}
582 }
583 }
584
585 false
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 #[test]
593 fn test_evaluate_conditions_confidence() {
594 let answer = Answer {
595 choice: Some("transfer".to_string()),
596 confidence: Some(0.62),
597 probabilities: None,
598 score: None,
599 noul: None,
600 gating: None,
601 };
602 let context = IndexMap::new();
603
604 let conditions = vec![
605 NodeCondition {
606 field: "confidence".to_string(),
607 op: NodeConditionOp::ConfidenceLt,
608 value: Some(serde_json::json!(0.65)),
609 target_node: "escalate_low_confidence".to_string(),
610 },
611 NodeCondition {
612 field: "answer".to_string(),
613 op: NodeConditionOp::Eq,
614 value: Some(serde_json::json!("transfer")),
615 target_node: "transfer_flow".to_string(),
616 },
617 ];
618
619 let target = evaluate_conditions(&conditions, Some(&answer), &context);
620 assert_eq!(target, Some("escalate_low_confidence".to_string()));
621 }
622
623 #[test]
624 fn test_evaluate_conditions_noul() {
625 let answer = Answer {
626 choice: None,
627 confidence: Some(0.98),
628 probabilities: None,
629 score: None,
630 noul: Some(0.02),
631 gating: None,
632 };
633 let context = IndexMap::new();
634
635 let conditions = vec![
636 NodeCondition {
637 field: "answer".to_string(),
638 op: NodeConditionOp::Eq,
639 value: Some(serde_json::json!(true)),
640 target_node: "reject_flow".to_string(),
641 },
642 NodeCondition {
643 field: "answer".to_string(),
644 op: NodeConditionOp::Eq,
645 value: Some(serde_json::json!(false)),
646 target_node: "continue_flow".to_string(),
647 },
648 ];
649
650 let target = evaluate_conditions(&conditions, Some(&answer), &context);
651 assert_eq!(target, Some("continue_flow".to_string()));
652 }
653}