Coverage for src/refinire/agents/flow/flow.py: 78%
451 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-15 18:51 +0900
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-15 18:51 +0900
1from __future__ import annotations
3"""Flow — Workflow orchestration engine for Step-based workflows.
5Flowはステップベースワークフロー用のワークフローオーケストレーションエンジンです。
6同期・非同期両方のインターフェースを提供し、CLI、GUI、チャットボット対応します。
7"""
9import asyncio
10import logging
11from typing import Any, Dict, List, Optional, Callable, Union
12from datetime import datetime
13import traceback
15from .context import Context
16from .step import Step, ParallelStep
17from ...core.trace_registry import get_global_registry, TraceRegistry
20logger = logging.getLogger(__name__)
23class FlowExecutionError(Exception):
24 """
25 Exception raised during flow execution
26 フロー実行中に発生する例外
27 """
28 pass
31class Flow:
32 """
33 Flow orchestration engine for Step-based workflows
34 ステップベースワークフロー用フローオーケストレーションエンジン
36 This class provides:
37 このクラスは以下を提供します:
38 - Declarative step-based workflow definition / 宣言的ステップベースワークフロー定義
39 - Synchronous and asynchronous execution modes / 同期・非同期実行モード
40 - User input coordination for interactive workflows / 対話的ワークフロー用ユーザー入力調整
41 - Error handling and observability / エラーハンドリングとオブザーバビリティ
42 """
44 def __init__(
45 self,
46 start: Optional[str] = None,
47 steps: Optional[Union[Dict[str, Step], List[Step], Step]] = None,
48 context: Optional[Context] = None,
49 max_steps: int = 1000,
50 trace_id: Optional[str] = None,
51 name: Optional[str] = None
52 ):
53 """
54 Initialize Flow with flexible step definitions
55 柔軟なステップ定義でFlowを初期化
57 This constructor now supports three ways to define steps:
58 このコンストラクタは3つの方法でステップを定義できます:
59 1. Traditional: start step name + Dict[str, Step]
60 2. Sequential: List[Step] (creates sequential workflow)
61 3. Single: Single Step (creates single-step workflow)
63 Args:
64 start: Start step label (optional for List/Single mode) / 開始ステップラベル(List/Singleモードでは省略可)
65 steps: Step definitions - Dict[str, Step], List[Step], or Step / ステップ定義 - Dict[str, Step]、List[Step]、またはStep
66 context: Initial context (optional) / 初期コンテキスト(オプション)
67 max_steps: Maximum number of steps to prevent infinite loops / 無限ループ防止のための最大ステップ数
68 trace_id: Trace ID for observability / オブザーバビリティ用トレースID
69 name: Flow name for identification / 識別用フロー名
70 """
71 # Handle flexible step definitions
72 # 柔軟なステップ定義を処理
73 if isinstance(steps, dict):
74 # Traditional mode: Dict[str, Step] with parallel support
75 # 従来モード: 並列サポート付きDict[str, Step]
76 if start is None:
77 raise ValueError("start parameter is required when steps is a dictionary")
78 self.start = start
79 self.steps = self._process_dag_structure(steps)
80 elif isinstance(steps, list):
81 # Sequential mode: List[Step]
82 # シーケンシャルモード: List[Step]
83 if not steps:
84 raise ValueError("Steps list cannot be empty")
85 self.steps = {}
86 prev_step_name = None
88 for i, step in enumerate(steps):
89 if not hasattr(step, 'name'):
90 raise ValueError(f"Step at index {i} must have a 'name' attribute")
92 step_name = step.name
93 self.steps[step_name] = step
95 # Set sequential flow: each step goes to next step
96 # シーケンシャルフロー設定: 各ステップが次のステップに進む
97 if prev_step_name is not None and hasattr(self.steps[prev_step_name], 'next_step'):
98 if self.steps[prev_step_name].next_step is None:
99 self.steps[prev_step_name].next_step = step_name
101 prev_step_name = step_name
103 # Start with first step
104 # 最初のステップから開始
105 self.start = steps[0].name
107 elif steps is not None:
108 # Check if it's a Step instance
109 # Stepインスタンスかどうかをチェック
110 if isinstance(steps, Step):
111 # Single step mode: Step
112 # 単一ステップモード: Step
113 if not hasattr(steps, 'name'):
114 raise ValueError("Step must have a 'name' attribute")
116 step_name = steps.name
117 self.start = step_name
118 self.steps = {step_name: steps}
119 else:
120 # Not a valid type
121 # 有効なタイプではない
122 raise ValueError("steps must be Dict[str, Step], List[Step], or Step")
123 else:
124 raise ValueError("steps parameter cannot be None")
126 self.context = context or Context()
127 self.max_steps = max_steps
128 self.name = name
129 self.trace_id = trace_id or self._generate_trace_id()
131 # Initialize context
132 # コンテキストを初期化
133 self.context.trace_id = self.trace_id
134 self.context.next_label = self.start
136 # Execution state
137 # 実行状態
138 self._running = False
139 self._run_loop_task: Optional[asyncio.Task] = None
140 self._execution_lock = asyncio.Lock()
142 # Hooks for observability
143 # オブザーバビリティ用フック
144 self.before_step_hooks: List[Callable[[str, Context], None]] = []
145 self.after_step_hooks: List[Callable[[str, Context, Any], None]] = []
146 self.error_hooks: List[Callable[[str, Context, Exception], None]] = []
148 # Register trace in global registry
149 # グローバルレジストリにトレースを登録
150 self._register_trace()
152 def _process_dag_structure(self, steps_def: Dict[str, Any]) -> Dict[str, Step]:
153 """
154 Process DAG structure and convert parallel definitions to ParallelStep
155 DAG構造を処理し、並列定義をParallelStepに変換
157 Args:
158 steps_def: Step definitions which may contain parallel structures
159 並列構造を含む可能性があるステップ定義
161 Returns:
162 Dict[str, Step]: Processed step definitions with ParallelStep instances
163 ParallelStepインスタンスを含む処理済みステップ定義
164 """
165 processed_steps = {}
167 for step_name, step_def in steps_def.items():
168 if isinstance(step_def, dict) and "parallel" in step_def:
169 # Handle parallel step definition
170 # 並列ステップ定義を処理
171 parallel_steps = step_def["parallel"]
172 if not isinstance(parallel_steps, list):
173 raise ValueError(f"'parallel' value must be a list of steps for step '{step_name}'")
175 # Validate all parallel steps are Step instances
176 # 全並列ステップがStepインスタンスであることを検証
177 for i, parallel_step in enumerate(parallel_steps):
178 if not isinstance(parallel_step, Step):
179 raise ValueError(f"Parallel step {i} in '{step_name}' must be a Step instance")
181 # Get next step from definition
182 # 定義から次ステップを取得
183 next_step = step_def.get("next_step")
184 max_workers = step_def.get("max_workers")
186 # Create ParallelStep
187 # ParallelStepを作成
188 parallel_step_instance = ParallelStep(
189 name=step_name,
190 parallel_steps=parallel_steps,
191 next_step=next_step,
192 max_workers=max_workers
193 )
195 processed_steps[step_name] = parallel_step_instance
197 elif isinstance(step_def, Step):
198 # Regular step
199 # 通常ステップ
200 processed_steps[step_name] = step_def
201 else:
202 raise ValueError(f"Invalid step definition for '{step_name}': {type(step_def)}")
204 return processed_steps
206 def _register_trace(self) -> None:
207 """
208 Register trace in global registry
209 グローバルレジストリにトレースを登録
210 """
211 try:
212 registry = get_global_registry()
213 registry.register_trace(
214 trace_id=self.trace_id,
215 flow_name=self.name,
216 flow_id=self.flow_id,
217 agent_names=self._extract_agent_names(),
218 tags={"flow_type": "default"}
219 )
220 except Exception as e:
221 logger.warning(f"Failed to register trace: {e}")
223 def _extract_agent_names(self) -> List[str]:
224 """
225 Extract agent names from steps
226 ステップからエージェント名を抽出
228 Returns:
229 List[str]: List of agent names / エージェント名のリスト
230 """
231 agent_names = []
232 for step in self.steps.values():
233 # Check for AgentPipelineStep
234 # AgentPipelineStepをチェック
235 if hasattr(step, 'pipeline'):
236 # Try to get agent name from pipeline
237 # パイプラインからエージェント名を取得しようとする
238 if hasattr(step.pipeline, 'name'):
239 agent_names.append(step.pipeline.name)
240 elif hasattr(step.pipeline, 'agent') and hasattr(step.pipeline.agent, 'name'):
241 agent_names.append(step.pipeline.agent.name)
242 else:
243 # Use step name as agent name
244 # ステップ名をエージェント名として使用
245 agent_names.append(f"Pipeline_{step.name}")
247 # Check for direct agent reference
248 # 直接のエージェント参照をチェック
249 elif hasattr(step, 'agent'):
250 if hasattr(step.agent, 'name'):
251 agent_names.append(step.agent.name)
252 else:
253 agent_names.append(f"Agent_{step.name}")
255 # Check for agent-like step names
256 # エージェントライクなステップ名をチェック
257 elif hasattr(step, 'name') and any(keyword in step.name.lower() for keyword in ['agent', 'ai', 'llm', 'bot']):
258 agent_names.append(step.name)
260 # Check for function steps that might be agent-related
261 # エージェント関連の可能性がある関数ステップをチェック
262 elif hasattr(step, 'function') and hasattr(step.function, '__name__'):
263 func_name = step.function.__name__
264 if any(keyword in func_name.lower() for keyword in ['agent', 'ai', 'llm', 'generate', 'analyze', 'process']):
265 agent_names.append(f"Function_{func_name}")
267 return list(set(agent_names)) # Remove duplicates
269 def _update_trace_on_completion(self) -> None:
270 """
271 Update trace registry when flow completes
272 フロー完了時にトレースレジストリを更新
273 """
274 try:
275 registry = get_global_registry()
276 trace_summary = self.context.get_trace_summary()
278 registry.update_trace(
279 trace_id=self.trace_id,
280 status="completed",
281 total_spans=trace_summary.get("total_spans", 0),
282 error_count=trace_summary.get("error_spans", 0),
283 artifacts=dict(self.context.artifacts),
284 add_agent_names=self._extract_agent_names()
285 )
286 except Exception as e:
287 logger.warning(f"Failed to update trace on completion: {e}")
289 def _update_trace_on_error(self, step_name: str, error: Exception) -> None:
290 """
291 Update trace registry when flow encounters error
292 フローがエラーに遭遇した時にトレースレジストリを更新
294 Args:
295 step_name: Name of the failed step / 失敗したステップ名
296 error: The error that occurred / 発生したエラー
297 """
298 try:
299 registry = get_global_registry()
300 trace_summary = self.context.get_trace_summary()
302 registry.update_trace(
303 trace_id=self.trace_id,
304 status="error",
305 total_spans=trace_summary.get("total_spans", 0),
306 error_count=trace_summary.get("error_spans", 0),
307 artifacts=dict(self.context.artifacts),
308 add_tags={
309 "error_step": step_name,
310 "error_type": type(error).__name__,
311 "error_message": str(error)
312 }
313 )
314 except Exception as e:
315 logger.warning(f"Failed to update trace on error: {e}")
317 def _generate_trace_id(self) -> str:
318 """
319 Generate a unique trace ID based on flow name and timestamp
320 フロー名とタイムスタンプに基づいてユニークなトレースIDを生成
322 Returns:
323 str: Generated trace ID / 生成されたトレースID
324 """
325 # Use full microsecond precision for uniqueness
326 # ユニーク性のために完全なマイクロ秒精度を使用
327 timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
328 if self.name:
329 # Use flow name in trace ID for easier identification
330 # 識別しやすくするためにフロー名をトレースIDに含める
331 safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in self.name.lower())
332 return f"{safe_name}_{timestamp}"
333 else:
334 return f"flow_{timestamp}"
336 @property
337 def finished(self) -> bool:
338 """
339 Check if flow is finished
340 フローが完了しているかチェック
342 Returns:
343 bool: True if finished / 完了している場合True
344 """
345 return self.context.is_finished()
347 @property
348 def current_step_name(self) -> Optional[str]:
349 """
350 Get current step name
351 現在のステップ名を取得
353 Returns:
354 str | None: Current step name / 現在のステップ名
355 """
356 return self.context.current_step
358 @property
359 def next_step_name(self) -> Optional[str]:
360 """
361 Get next step name
362 次のステップ名を取得
364 Returns:
365 str | None: Next step name / 次のステップ名
366 """
367 return self.context.next_label
369 @property
370 def flow_id(self) -> str:
371 """
372 Get flow identifier (trace_id)
373 フロー識別子(trace_id)を取得
375 Returns:
376 str: Flow identifier / フロー識別子
377 """
378 return self.trace_id
380 @property
381 def flow_name(self) -> Optional[str]:
382 """
383 Get flow name
384 フロー名を取得
386 Returns:
387 str | None: Flow name / フロー名
388 """
389 return self.name
391 async def run(self, input_data: Optional[str] = None, initial_input: Optional[str] = None) -> Context:
392 """
393 Run flow to completion without user input coordination
394 ユーザー入力調整なしでフローを完了まで実行
396 This is for non-interactive workflows that don't require user input.
397 これはユーザー入力が不要な非対話的ワークフロー用です。
399 Args:
400 input_data: Input data to the flow (preferred parameter name) / フローへの入力データ(推奨パラメータ名)
401 initial_input: Initial input to the flow (deprecated, use input_data) / フローへの初期入力(非推奨、input_dataを使用)
403 Returns:
404 Context: Final context / 最終コンテキスト
406 Raises:
407 FlowExecutionError: If execution fails / 実行失敗時
408 """
409 async with self._execution_lock:
410 try:
411 self._running = True
413 # Reset context for new execution
414 # 新しい実行用にコンテキストをリセット
415 if self.context.step_count > 0:
416 self.context = Context(trace_id=self.trace_id)
417 self.context.next_label = self.start
419 # Determine input to use (input_data takes precedence)
420 # 使用する入力を決定(input_dataが優先)
421 effective_input = input_data or initial_input
423 # Add input if provided
424 # 入力が提供されている場合は追加
425 if effective_input:
426 self.context.add_user_message(effective_input)
428 current_input = effective_input
429 step_count = 0
431 while not self.finished and step_count < self.max_steps:
432 step_name = self.context.next_label
433 if not step_name or step_name not in self.steps:
434 self.context.finish() # Finish flow when no next step or unknown step
435 break
437 step = self.steps[step_name]
439 # Execute step
440 # ステップを実行
441 try:
442 await self._execute_step(step, current_input)
443 current_input = None # Only use initial input for first step
444 step_count += 1
446 # If step is waiting for user input, break
447 # ステップがユーザー入力を待機している場合、中断
448 if self.context.awaiting_user_input:
449 break
451 except Exception as e:
452 logger.error(f"Error executing step {step_name}: {e}")
453 self._handle_step_error(step_name, e)
454 break
456 # Check for infinite loop
457 # 無限ループのチェック
458 if step_count >= self.max_steps:
459 raise FlowExecutionError(f"Flow exceeded maximum steps ({self.max_steps})")
461 # Finalize any remaining span when flow completes
462 # フロー完了時に残りのスパンを終了
463 self.context.finalize_flow_span()
465 # Update trace registry
466 # トレースレジストリを更新
467 self._update_trace_on_completion()
469 return self.context
471 finally:
472 self._running = False
474 async def run_loop(self) -> None:
475 """
476 Run flow as background task with user input coordination
477 ユーザー入力調整を含むバックグラウンドタスクとしてフローを実行
479 This method runs the flow continuously, pausing when user input is needed.
480 このメソッドはフローを継続的に実行し、ユーザー入力が必要な時に一時停止します。
481 Use feed() to provide user input when the flow is waiting.
482 フローが待機している時はfeed()を使用してユーザー入力を提供してください。
483 """
484 async with self._execution_lock:
485 try:
486 self._running = True
488 # Reset context for new execution
489 # 新しい実行用にコンテキストをリセット
490 if self.context.step_count > 0:
491 self.context = Context(trace_id=self.trace_id)
492 self.context.next_label = self.start
494 step_count = 0
495 current_input = None
497 while not self.finished and step_count < self.max_steps:
498 step_name = self.context.next_label
499 if not step_name or step_name not in self.steps:
500 self.context.finish() # Finish flow when no next step or unknown step
501 break
503 step = self.steps[step_name]
505 # Execute step
506 # ステップを実行
507 try:
508 await self._execute_step(step, current_input)
509 current_input = None
510 step_count += 1
512 # If step is waiting for user input, wait for feed()
513 # ステップがユーザー入力を待機している場合、feed()を待つ
514 if self.context.awaiting_user_input:
515 await self.context.wait_for_user_input()
516 # After receiving input, continue with the same step
517 # 入力受信後、同じステップで継続
518 current_input = self.context.last_user_input
519 continue
521 except Exception as e:
522 logger.error(f"Error executing step {step_name}: {e}")
523 self._handle_step_error(step_name, e)
524 break
526 # Check for infinite loop
527 # 無限ループのチェック
528 if step_count >= self.max_steps:
529 raise FlowExecutionError(f"Flow exceeded maximum steps ({self.max_steps})")
531 # Finalize any remaining span when flow completes
532 # フロー完了時に残りのスパンを終了
533 self.context.finalize_flow_span()
535 # Update trace registry
536 # トレースレジストリを更新
537 self._update_trace_on_completion()
539 finally:
540 self._running = False
542 def next_prompt(self) -> Optional[str]:
543 """
544 Get next prompt for synchronous CLI usage
545 同期CLI使用用の次のプロンプトを取得
547 Returns:
548 str | None: Prompt if waiting for user input / ユーザー入力待ちの場合のプロンプト
549 """
550 return self.context.clear_prompt()
552 def feed(self, user_input: str) -> None:
553 """
554 Provide user input to the flow
555 フローにユーザー入力を提供
557 Args:
558 user_input: User input text / ユーザー入力テキスト
559 """
560 self.context.provide_user_input(user_input)
562 def step(self) -> None:
563 """
564 Execute one step synchronously
565 1ステップを同期的に実行
567 This method executes one step and returns immediately.
568 このメソッドは1ステップを実行してすぐに返ります。
569 Use for synchronous CLI applications.
570 同期CLIアプリケーション用に使用してください。
571 """
572 if self.finished:
573 return
575 step_name = self.context.next_label
576 if not step_name or step_name not in self.steps:
577 self.context.finish()
578 return
580 step = self.steps[step_name]
582 # Run step in event loop
583 # イベントループでステップを実行
584 try:
585 loop = asyncio.get_event_loop()
586 if loop.is_running():
587 # If loop is running, create a task
588 # ループが実行中の場合、タスクを作成
589 task = asyncio.create_task(self._execute_step(step, None))
590 # This is a synchronous method, so we can't await
591 # これは同期メソッドなので、awaitできない
592 # The task will run in the background
593 # タスクはバックグラウンドで実行される
594 else:
595 # If no loop is running, run until complete
596 # ループが実行されていない場合、完了まで実行
597 loop.run_until_complete(self._execute_step(step, None))
598 except Exception as e:
599 logger.error(f"Error executing step {step_name}: {e}")
600 self._handle_step_error(step_name, e)
602 async def _execute_step(self, step: Step, user_input: Optional[str]) -> None:
603 """
604 Execute a single step with hooks and error handling
605 フックとエラーハンドリングで単一ステップを実行
607 Args:
608 step: Step to execute / 実行するステップ
609 user_input: User input if any / ユーザー入力(あれば)
610 """
611 step_name = step.name
613 # Before step hooks
614 # ステップ前フック
615 for hook in self.before_step_hooks:
616 try:
617 hook(step_name, self.context)
618 except Exception as e:
619 logger.warning(f"Before step hook error: {e}")
621 start_time = datetime.now()
622 result = None
623 error = None
625 try:
626 # Execute step
627 # ステップを実行
628 result = await step.run(user_input, self.context)
629 if result != self.context:
630 # Step returned a new context, use it
631 # ステップが新しいコンテキストを返した場合、それを使用
632 self.context = result
634 logger.debug(f"Step {step_name} completed in {datetime.now() - start_time}")
636 except Exception as e:
637 error = e
638 logger.error(f"Step {step_name} failed: {e}")
639 logger.debug(traceback.format_exc())
641 # Add error to context
642 # エラーをコンテキストに追加
643 self.context.add_system_message(f"Step {step_name} failed: {str(e)}")
645 # Call error hooks
646 # エラーフックを呼び出し
647 for hook in self.error_hooks:
648 try:
649 hook(step_name, self.context, e)
650 except Exception as hook_error:
651 logger.warning(f"Error hook failed: {hook_error}")
653 raise e
655 finally:
656 # After step hooks
657 # ステップ後フック
658 for hook in self.after_step_hooks:
659 try:
660 hook(step_name, self.context, result)
661 except Exception as e:
662 logger.warning(f"After step hook error: {e}")
664 def _handle_step_error(self, step_name: str, error: Exception) -> None:
665 """
666 Handle step execution error
667 ステップ実行エラーを処理
669 Args:
670 step_name: Name of the failed step / 失敗したステップの名前
671 error: The error that occurred / 発生したエラー
672 """
673 # Finalize current span with error status
674 # エラーステータスで現在のスパンを終了
675 self.context._finalize_current_span("error", str(error))
677 # Mark flow as finished on error
678 # エラー時はフローを完了としてマーク
679 self.context.finish()
680 self.context.set_artifact("error", {
681 "step": step_name,
682 "error": str(error),
683 "type": type(error).__name__
684 })
686 # Update trace registry with error
687 # エラーでトレースレジストリを更新
688 self._update_trace_on_error(step_name, error)
690 def add_hook(
691 self,
692 hook_type: str,
693 callback: Callable
694 ) -> None:
695 """
696 Add observability hook
697 オブザーバビリティフックを追加
699 Args:
700 hook_type: Type of hook ("before_step", "after_step", "error") / フックタイプ
701 callback: Callback function / コールバック関数
702 """
703 if hook_type == "before_step":
704 self.before_step_hooks.append(callback)
705 elif hook_type == "after_step":
706 self.after_step_hooks.append(callback)
707 elif hook_type == "error":
708 self.error_hooks.append(callback)
709 else:
710 raise ValueError(f"Unknown hook type: {hook_type}")
712 def get_step_history(self) -> List[Dict[str, Any]]:
713 """
714 Get execution history
715 実行履歴を取得
717 Returns:
718 List[Dict[str, Any]]: Step execution history / ステップ実行履歴
719 """
720 # Use span_history from context as primary source
721 # コンテキストのspan_historyを主要ソースとして使用
722 if hasattr(self.context, 'span_history') and self.context.span_history:
723 return self.context.span_history
725 # Fallback: extract from messages
726 # フォールバック: メッセージから抽出
727 history = []
728 for msg in self.context.messages:
729 if msg.role == "system" and "Step" in msg.content:
730 # Try to extract step name from message
731 # メッセージからステップ名を抽出しようとする
732 step_name = None
733 if "executing step:" in msg.content.lower():
734 parts = msg.content.split(":")
735 if len(parts) > 1:
736 step_name = parts[1].strip()
738 history.append({
739 "timestamp": msg.timestamp,
740 "step_name": step_name or "Unknown",
741 "message": msg.content,
742 "metadata": msg.metadata
743 })
745 return history
747 def get_flow_summary(self) -> Dict[str, Any]:
748 """
749 Get flow execution summary
750 フロー実行サマリーを取得
752 Returns:
753 Dict[str, Any]: Flow summary / フローサマリー
754 """
755 trace_summary = self.context.get_trace_summary()
756 return {
757 "flow_id": self.flow_id,
758 "flow_name": self.flow_name,
759 "trace_id": self.trace_id,
760 "current_span_id": self.context.current_span_id,
761 "start_step": self.start,
762 "current_step": self.current_step_name,
763 "next_step": self.next_step_name,
764 "step_count": self.context.step_count,
765 "finished": self.finished,
766 "start_time": self.context.start_time,
767 "execution_history": self.get_step_history(),
768 "span_history": self.context.get_span_history(),
769 "trace_summary": trace_summary,
770 "artifacts": self.context.artifacts,
771 "message_count": len(self.context.messages)
772 }
774 def reset(self) -> None:
775 """
776 Reset flow to initial state
777 フローを初期状態にリセット
778 """
779 self.context = Context(trace_id=self.trace_id)
780 self.context.next_label = self.start
781 self._running = False
782 if self._run_loop_task:
783 self._run_loop_task.cancel()
784 self._run_loop_task = None
786 def show(self, format: str = "mermaid", include_history: bool = True) -> str:
787 """
788 Show flow structure and execution path as a diagram.
789 フロー構造と実行パスを図として表示します。
791 Args:
792 format: Output format ("mermaid" or "text") / 出力形式("mermaid" または "text")
793 include_history: Whether to include execution history / 実行履歴を含めるかどうか
795 Returns:
796 str: Flow diagram representation / フロー図の表現
797 """
798 if format == "mermaid":
799 return self._generate_mermaid_diagram(include_history)
800 elif format == "text":
801 return self._generate_text_diagram(include_history)
802 else:
803 raise ValueError(f"Unsupported format: {format}")
805 def get_possible_routes(self, step_name: str) -> List[str]:
806 """
807 Get possible routes from a given step.
808 指定されたステップから可能なルートを取得します。
810 Args:
811 step_name: Name of the step / ステップ名
813 Returns:
814 List[str]: List of possible next step names / 可能な次のステップ名のリスト
815 """
816 if step_name not in self.steps:
817 return []
819 step = self.steps[step_name]
820 routes = []
822 # Check different step types for routing information
823 # 様々なステップタイプのルーティング情報をチェック
824 if hasattr(step, 'next_step') and step.next_step:
825 routes.append(step.next_step)
827 if hasattr(step, 'if_true') and hasattr(step, 'if_false'):
828 # ConditionStep
829 routes.extend([step.if_true, step.if_false])
831 if hasattr(step, 'branches'):
832 # ForkStep
833 routes.extend(step.branches)
835 if hasattr(step, 'config') and hasattr(step.config, 'routes'):
836 # RouterAgent
837 routes.extend(step.config.routes.values())
839 return list(set(routes)) # Remove duplicates
841 def _generate_mermaid_diagram(self, include_history: bool) -> str:
842 """
843 Generate Mermaid flowchart diagram.
844 Mermaidフローチャート図を生成します。
846 Args:
847 include_history: Whether to include execution history / 実行履歴を含めるかどうか
849 Returns:
850 str: Mermaid diagram code / Mermaid図のコード
851 """
852 lines = ["graph TD"]
853 visited_nodes = set()
854 execution_path = []
856 # Get execution history if available
857 # 実行履歴があれば取得
858 if include_history:
859 step_history = self.get_step_history()
860 execution_path = [step['step_name'] for step in step_history if 'step_name' in step]
862 # Add nodes and connections
863 # ノードと接続を追加
864 def add_node_and_connections(step_name: str, depth: int = 0):
865 if step_name in visited_nodes or depth > 10: # Prevent infinite recursion
866 return
868 visited_nodes.add(step_name)
870 if step_name not in self.steps:
871 # End node
872 lines.append(f' {step_name}["{step_name}<br/>(END)"]')
873 return
875 step = self.steps[step_name]
877 # Determine node style based on step type and execution
878 # ステップタイプと実行状況に基づいてノードスタイルを決定
879 node_style = self._get_node_style(step, step_name, execution_path, include_history)
880 lines.append(f' {step_name}["{step_name}<br/>({step.__class__.__name__})"]{node_style}')
882 # Add connections based on step type
883 # ステップタイプに基づいて接続を追加
884 possible_routes = self.get_possible_routes(step_name)
886 if isinstance(step, self._get_condition_step_class()):
887 # ConditionStep with labeled edges
888 lines.append(f' {step_name} -->|"True"| {step.if_true}')
889 lines.append(f' {step_name} -->|"False"| {step.if_false}')
890 add_node_and_connections(step.if_true, depth + 1)
891 add_node_and_connections(step.if_false, depth + 1)
893 elif hasattr(step, 'config') and hasattr(step.config, 'routes'):
894 # RouterAgent with route labels
895 for route_key, next_step in step.config.routes.items():
896 lines.append(f' {step_name} -->|"{route_key}"| {next_step}')
897 add_node_and_connections(next_step, depth + 1)
899 elif hasattr(step, 'branches'):
900 # ForkStep
901 for branch in step.branches:
902 lines.append(f' {step_name} --> {branch}')
903 add_node_and_connections(branch, depth + 1)
905 else:
906 # Simple step with next_step
907 for next_step in possible_routes:
908 lines.append(f' {step_name} --> {next_step}')
909 add_node_and_connections(next_step, depth + 1)
911 # Start from the beginning
912 # 開始点から始める
913 add_node_and_connections(self.start)
915 # Add execution path highlighting if history is included
916 # 履歴が含まれる場合は実行パスをハイライト
917 if include_history and execution_path:
918 lines.append("")
919 lines.append(" %% Execution path highlighting")
920 for i, step_name in enumerate(execution_path):
921 if i > 0:
922 prev_step = execution_path[i-1]
923 lines.append(f' linkStyle {i-1} stroke:#ff3,stroke-width:4px')
925 return "\n".join(lines)
927 def _generate_text_diagram(self, include_history: bool) -> str:
928 """
929 Generate text-based flow diagram.
930 テキストベースのフロー図を生成します。
932 Args:
933 include_history: Whether to include execution history / 実行履歴を含めるかどうか
935 Returns:
936 str: Text diagram / テキスト図
937 """
938 lines = ["Flow Diagram:"]
939 lines.append("=" * 50)
941 visited = set()
943 def add_step_info(step_name: str, indent: int = 0):
944 if step_name in visited:
945 lines.append(" " * indent + f"→ {step_name} (already shown)")
946 return
948 visited.add(step_name)
949 prefix = " " * indent
951 if step_name not in self.steps:
952 lines.append(f"{prefix}→ {step_name} (END)")
953 return
955 step = self.steps[step_name]
956 step_type = step.__class__.__name__
958 lines.append(f"{prefix}→ {step_name} ({step_type})")
960 # Show routing information
961 # ルーティング情報を表示
962 if hasattr(step, 'config') and hasattr(step.config, 'routes'):
963 lines.append(f"{prefix} Routes:")
964 for route_key, next_step in step.config.routes.items():
965 lines.append(f"{prefix} {route_key} → {next_step}")
967 elif isinstance(step, self._get_condition_step_class()):
968 lines.append(f"{prefix} True → {step.if_true}")
969 lines.append(f"{prefix} False → {step.if_false}")
971 elif hasattr(step, 'branches'):
972 lines.append(f"{prefix} Branches:")
973 for branch in step.branches:
974 lines.append(f"{prefix} → {branch}")
976 # Recursively show next steps
977 # 次のステップを再帰的に表示
978 possible_routes = self.get_possible_routes(step_name)
979 for next_step in possible_routes:
980 add_step_info(next_step, indent + 1)
982 add_step_info(self.start)
984 # Add execution history if requested
985 # 要求された場合は実行履歴を追加
986 if include_history:
987 step_history = self.get_step_history()
988 if step_history:
989 lines.append("")
990 lines.append("Execution History:")
991 lines.append("-" * 30)
992 for i, step_info in enumerate(step_history):
993 step_name = step_info.get('step_name', 'Unknown')
994 timestamp = step_info.get('timestamp', '')
995 lines.append(f"{i+1}. {step_name} ({timestamp})")
997 return "\n".join(lines)
999 def _get_node_style(self, step, step_name: str, execution_path: List[str], include_history: bool) -> str:
1000 """
1001 Get Mermaid node style based on step type and execution status.
1002 ステップタイプと実行状況に基づいてMermaidノードスタイルを取得します。
1003 """
1004 if include_history and step_name in execution_path:
1005 return ":::executed"
1006 elif step_name == self.start:
1007 return ":::start"
1008 elif hasattr(step, 'config') and hasattr(step.config, 'routes'):
1009 return ":::router"
1010 elif isinstance(step, self._get_condition_step_class()):
1011 return ":::condition"
1012 else:
1013 return ""
1015 def _get_condition_step_class(self):
1016 """Get ConditionStep class for type checking."""
1017 try:
1018 from .step import ConditionStep
1019 return ConditionStep
1020 except ImportError:
1021 return type(None) # Fallback if import fails
1023 def stop(self) -> None:
1024 """
1025 Stop flow execution
1026 フロー実行を停止
1027 """
1028 self._running = False
1029 self.context.finalize_flow_span() # Finalize current span before stopping
1030 self.context.finish()
1031 if self._run_loop_task:
1032 self._run_loop_task.cancel()
1033 self._run_loop_task = None
1035 async def start_background_task(self) -> asyncio.Task:
1036 """
1037 Start flow as background task
1038 フローをバックグラウンドタスクとして開始
1040 Returns:
1041 asyncio.Task: Background task / バックグラウンドタスク
1042 """
1043 if self._run_loop_task and not self._run_loop_task.done():
1044 raise RuntimeError("Flow is already running as background task")
1046 self._run_loop_task = asyncio.create_task(self.run_loop())
1047 return self._run_loop_task
1049 def __str__(self) -> str:
1050 """String representation of flow"""
1051 return f"Flow(start={self.start}, steps={len(self.steps)}, finished={self.finished})"
1053 def __repr__(self) -> str:
1054 return self.__str__()
1057# Utility functions for flow creation
1058# フロー作成用ユーティリティ関数
1060def create_simple_flow(
1061 steps: List[tuple[str, Step]],
1062 context: Optional[Context] = None,
1063 name: Optional[str] = None
1064) -> Flow:
1065 """
1066 Create a simple linear flow from a list of steps
1067 ステップのリストから簡単な線形フローを作成
1069 Args:
1070 steps: List of (name, step) tuples / (名前, ステップ)タプルのリスト
1071 context: Initial context / 初期コンテキスト
1072 name: Flow name for identification / 識別用フロー名
1074 Returns:
1075 Flow: Created flow / 作成されたフロー
1076 """
1077 if not steps:
1078 raise ValueError("At least one step is required")
1080 step_dict = {}
1081 for i, (step_name, step) in enumerate(steps):
1082 # Set next step for each step
1083 # 各ステップの次ステップを設定
1084 if hasattr(step, 'next_step') and step.next_step is None:
1085 if i < len(steps) - 1:
1086 step.next_step = steps[i + 1][0]
1087 step_dict[step_name] = step
1089 return Flow(
1090 start=steps[0][0],
1091 steps=step_dict,
1092 context=context,
1093 name=name
1094 )
1097def create_conditional_flow(
1098 initial_step: Step,
1099 condition_step: Step,
1100 true_branch: List[Step],
1101 false_branch: List[Step],
1102 context: Optional[Context] = None,
1103 name: Optional[str] = None
1104) -> Flow:
1105 """
1106 Create a conditional flow with true/false branches
1107 true/falseブランチを持つ条件付きフローを作成
1109 Args:
1110 initial_step: Initial step / 初期ステップ
1111 condition_step: Condition step / 条件ステップ
1112 true_branch: Steps for true branch / trueブランチのステップ
1113 false_branch: Steps for false branch / falseブランチのステップ
1114 context: Initial context / 初期コンテキスト
1115 name: Flow name for identification / 識別用フロー名
1117 Returns:
1118 Flow: Created flow / 作成されたフロー
1119 """
1120 steps = {
1121 "start": initial_step,
1122 "condition": condition_step
1123 }
1125 # Add true branch steps
1126 # trueブランチステップを追加
1127 for i, step in enumerate(true_branch):
1128 step_name = f"true_{i}"
1129 steps[step_name] = step
1130 if i == 0 and hasattr(condition_step, 'if_true'):
1131 condition_step.if_true = step_name
1133 # Add false branch steps
1134 # falseブランチステップを追加
1135 for i, step in enumerate(false_branch):
1136 step_name = f"false_{i}"
1137 steps[step_name] = step
1138 if i == 0 and hasattr(condition_step, 'if_false'):
1139 condition_step.if_false = step_name
1141 # Connect initial step to condition
1142 # 初期ステップを条件に接続
1143 if hasattr(initial_step, 'next_step'):
1144 initial_step.next_step = "condition"
1146 return Flow(
1147 start="start",
1148 steps=steps,
1149 context=context,
1150 name=name
1151 )