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

1from __future__ import annotations 

2 

3"""Flow — Workflow orchestration engine for Step-based workflows. 

4 

5Flowはステップベースワークフロー用のワークフローオーケストレーションエンジンです。 

6同期・非同期両方のインターフェースを提供し、CLI、GUI、チャットボット対応します。 

7""" 

8 

9import asyncio 

10import logging 

11from typing import Any, Dict, List, Optional, Callable, Union 

12from datetime import datetime 

13import traceback 

14 

15from .context import Context 

16from .step import Step, ParallelStep 

17from ...core.trace_registry import get_global_registry, TraceRegistry 

18 

19 

20logger = logging.getLogger(__name__) 

21 

22 

23class FlowExecutionError(Exception): 

24 """ 

25 Exception raised during flow execution 

26 フロー実行中に発生する例外 

27 """ 

28 pass 

29 

30 

31class Flow: 

32 """ 

33 Flow orchestration engine for Step-based workflows 

34 ステップベースワークフロー用フローオーケストレーションエンジン 

35  

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 """ 

43 

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を初期化 

56  

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) 

62  

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 

87 

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") 

91 

92 step_name = step.name 

93 self.steps[step_name] = step 

94 

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 

100 

101 prev_step_name = step_name 

102 

103 # Start with first step 

104 # 最初のステップから開始 

105 self.start = steps[0].name 

106 

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") 

115 

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") 

125 

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() 

130 

131 # Initialize context 

132 # コンテキストを初期化 

133 self.context.trace_id = self.trace_id 

134 self.context.next_label = self.start 

135 

136 # Execution state 

137 # 実行状態 

138 self._running = False 

139 self._run_loop_task: Optional[asyncio.Task] = None 

140 self._execution_lock = asyncio.Lock() 

141 

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]] = [] 

147 

148 # Register trace in global registry 

149 # グローバルレジストリにトレースを登録 

150 self._register_trace() 

151 

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に変換 

156  

157 Args: 

158 steps_def: Step definitions which may contain parallel structures 

159 並列構造を含む可能性があるステップ定義 

160  

161 Returns: 

162 Dict[str, Step]: Processed step definitions with ParallelStep instances 

163 ParallelStepインスタンスを含む処理済みステップ定義 

164 """ 

165 processed_steps = {} 

166 

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}'") 

174 

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") 

180 

181 # Get next step from definition 

182 # 定義から次ステップを取得 

183 next_step = step_def.get("next_step") 

184 max_workers = step_def.get("max_workers") 

185 

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 ) 

194 

195 processed_steps[step_name] = parallel_step_instance 

196 

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)}") 

203 

204 return processed_steps 

205 

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}") 

222 

223 def _extract_agent_names(self) -> List[str]: 

224 """ 

225 Extract agent names from steps 

226 ステップからエージェント名を抽出 

227  

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}") 

246 

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}") 

254 

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) 

259 

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}") 

266 

267 return list(set(agent_names)) # Remove duplicates 

268 

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() 

277 

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}") 

288 

289 def _update_trace_on_error(self, step_name: str, error: Exception) -> None: 

290 """ 

291 Update trace registry when flow encounters error 

292 フローがエラーに遭遇した時にトレースレジストリを更新 

293  

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() 

301 

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}") 

316 

317 def _generate_trace_id(self) -> str: 

318 """ 

319 Generate a unique trace ID based on flow name and timestamp 

320 フロー名とタイムスタンプに基づいてユニークなトレースIDを生成 

321  

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}" 

335 

336 @property 

337 def finished(self) -> bool: 

338 """ 

339 Check if flow is finished 

340 フローが完了しているかチェック 

341  

342 Returns: 

343 bool: True if finished / 完了している場合True 

344 """ 

345 return self.context.is_finished() 

346 

347 @property 

348 def current_step_name(self) -> Optional[str]: 

349 """ 

350 Get current step name 

351 現在のステップ名を取得 

352  

353 Returns: 

354 str | None: Current step name / 現在のステップ名 

355 """ 

356 return self.context.current_step 

357 

358 @property 

359 def next_step_name(self) -> Optional[str]: 

360 """ 

361 Get next step name 

362 次のステップ名を取得 

363  

364 Returns: 

365 str | None: Next step name / 次のステップ名 

366 """ 

367 return self.context.next_label 

368 

369 @property 

370 def flow_id(self) -> str: 

371 """ 

372 Get flow identifier (trace_id) 

373 フロー識別子(trace_id)を取得 

374  

375 Returns: 

376 str: Flow identifier / フロー識別子 

377 """ 

378 return self.trace_id 

379 

380 @property 

381 def flow_name(self) -> Optional[str]: 

382 """ 

383 Get flow name 

384 フロー名を取得 

385  

386 Returns: 

387 str | None: Flow name / フロー名 

388 """ 

389 return self.name 

390 

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 ユーザー入力調整なしでフローを完了まで実行 

395  

396 This is for non-interactive workflows that don't require user input. 

397 これはユーザー入力が不要な非対話的ワークフロー用です。 

398  

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を使用) 

402  

403 Returns: 

404 Context: Final context / 最終コンテキスト 

405  

406 Raises: 

407 FlowExecutionError: If execution fails / 実行失敗時 

408 """ 

409 async with self._execution_lock: 

410 try: 

411 self._running = True 

412 

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 

418 

419 # Determine input to use (input_data takes precedence) 

420 # 使用する入力を決定(input_dataが優先) 

421 effective_input = input_data or initial_input 

422 

423 # Add input if provided 

424 # 入力が提供されている場合は追加 

425 if effective_input: 

426 self.context.add_user_message(effective_input) 

427 

428 current_input = effective_input 

429 step_count = 0 

430 

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 

436 

437 step = self.steps[step_name] 

438 

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 

445 

446 # If step is waiting for user input, break 

447 # ステップがユーザー入力を待機している場合、中断 

448 if self.context.awaiting_user_input: 

449 break 

450 

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 

455 

456 # Check for infinite loop 

457 # 無限ループのチェック 

458 if step_count >= self.max_steps: 

459 raise FlowExecutionError(f"Flow exceeded maximum steps ({self.max_steps})") 

460 

461 # Finalize any remaining span when flow completes 

462 # フロー完了時に残りのスパンを終了 

463 self.context.finalize_flow_span() 

464 

465 # Update trace registry 

466 # トレースレジストリを更新 

467 self._update_trace_on_completion() 

468 

469 return self.context 

470 

471 finally: 

472 self._running = False 

473 

474 async def run_loop(self) -> None: 

475 """ 

476 Run flow as background task with user input coordination 

477 ユーザー入力調整を含むバックグラウンドタスクとしてフローを実行 

478  

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 

487 

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 

493 

494 step_count = 0 

495 current_input = None 

496 

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 

502 

503 step = self.steps[step_name] 

504 

505 # Execute step 

506 # ステップを実行 

507 try: 

508 await self._execute_step(step, current_input) 

509 current_input = None 

510 step_count += 1 

511 

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 

520 

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 

525 

526 # Check for infinite loop 

527 # 無限ループのチェック 

528 if step_count >= self.max_steps: 

529 raise FlowExecutionError(f"Flow exceeded maximum steps ({self.max_steps})") 

530 

531 # Finalize any remaining span when flow completes 

532 # フロー完了時に残りのスパンを終了 

533 self.context.finalize_flow_span() 

534 

535 # Update trace registry 

536 # トレースレジストリを更新 

537 self._update_trace_on_completion() 

538 

539 finally: 

540 self._running = False 

541 

542 def next_prompt(self) -> Optional[str]: 

543 """ 

544 Get next prompt for synchronous CLI usage 

545 同期CLI使用用の次のプロンプトを取得 

546  

547 Returns: 

548 str | None: Prompt if waiting for user input / ユーザー入力待ちの場合のプロンプト 

549 """ 

550 return self.context.clear_prompt() 

551 

552 def feed(self, user_input: str) -> None: 

553 """ 

554 Provide user input to the flow 

555 フローにユーザー入力を提供 

556  

557 Args: 

558 user_input: User input text / ユーザー入力テキスト 

559 """ 

560 self.context.provide_user_input(user_input) 

561 

562 def step(self) -> None: 

563 """ 

564 Execute one step synchronously 

565 1ステップを同期的に実行 

566  

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 

574 

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 

579 

580 step = self.steps[step_name] 

581 

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) 

601 

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 フックとエラーハンドリングで単一ステップを実行 

606  

607 Args: 

608 step: Step to execute / 実行するステップ 

609 user_input: User input if any / ユーザー入力(あれば) 

610 """ 

611 step_name = step.name 

612 

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}") 

620 

621 start_time = datetime.now() 

622 result = None 

623 error = None 

624 

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 

633 

634 logger.debug(f"Step {step_name} completed in {datetime.now() - start_time}") 

635 

636 except Exception as e: 

637 error = e 

638 logger.error(f"Step {step_name} failed: {e}") 

639 logger.debug(traceback.format_exc()) 

640 

641 # Add error to context 

642 # エラーをコンテキストに追加 

643 self.context.add_system_message(f"Step {step_name} failed: {str(e)}") 

644 

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}") 

652 

653 raise e 

654 

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}") 

663 

664 def _handle_step_error(self, step_name: str, error: Exception) -> None: 

665 """ 

666 Handle step execution error 

667 ステップ実行エラーを処理 

668  

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)) 

676 

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 }) 

685 

686 # Update trace registry with error 

687 # エラーでトレースレジストリを更新 

688 self._update_trace_on_error(step_name, error) 

689 

690 def add_hook( 

691 self, 

692 hook_type: str, 

693 callback: Callable 

694 ) -> None: 

695 """ 

696 Add observability hook 

697 オブザーバビリティフックを追加 

698  

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}") 

711 

712 def get_step_history(self) -> List[Dict[str, Any]]: 

713 """ 

714 Get execution history 

715 実行履歴を取得 

716  

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 

724 

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() 

737 

738 history.append({ 

739 "timestamp": msg.timestamp, 

740 "step_name": step_name or "Unknown", 

741 "message": msg.content, 

742 "metadata": msg.metadata 

743 }) 

744 

745 return history 

746 

747 def get_flow_summary(self) -> Dict[str, Any]: 

748 """ 

749 Get flow execution summary 

750 フロー実行サマリーを取得 

751  

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 } 

773 

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 

785 

786 def show(self, format: str = "mermaid", include_history: bool = True) -> str: 

787 """ 

788 Show flow structure and execution path as a diagram. 

789 フロー構造と実行パスを図として表示します。 

790  

791 Args: 

792 format: Output format ("mermaid" or "text") / 出力形式("mermaid" または "text") 

793 include_history: Whether to include execution history / 実行履歴を含めるかどうか 

794  

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}") 

804 

805 def get_possible_routes(self, step_name: str) -> List[str]: 

806 """ 

807 Get possible routes from a given step. 

808 指定されたステップから可能なルートを取得します。 

809  

810 Args: 

811 step_name: Name of the step / ステップ名 

812  

813 Returns: 

814 List[str]: List of possible next step names / 可能な次のステップ名のリスト 

815 """ 

816 if step_name not in self.steps: 

817 return [] 

818 

819 step = self.steps[step_name] 

820 routes = [] 

821 

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) 

826 

827 if hasattr(step, 'if_true') and hasattr(step, 'if_false'): 

828 # ConditionStep 

829 routes.extend([step.if_true, step.if_false]) 

830 

831 if hasattr(step, 'branches'): 

832 # ForkStep 

833 routes.extend(step.branches) 

834 

835 if hasattr(step, 'config') and hasattr(step.config, 'routes'): 

836 # RouterAgent 

837 routes.extend(step.config.routes.values()) 

838 

839 return list(set(routes)) # Remove duplicates 

840 

841 def _generate_mermaid_diagram(self, include_history: bool) -> str: 

842 """ 

843 Generate Mermaid flowchart diagram. 

844 Mermaidフローチャート図を生成します。 

845  

846 Args: 

847 include_history: Whether to include execution history / 実行履歴を含めるかどうか 

848  

849 Returns: 

850 str: Mermaid diagram code / Mermaid図のコード 

851 """ 

852 lines = ["graph TD"] 

853 visited_nodes = set() 

854 execution_path = [] 

855 

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] 

861 

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 

867 

868 visited_nodes.add(step_name) 

869 

870 if step_name not in self.steps: 

871 # End node 

872 lines.append(f' {step_name}["{step_name}<br/>(END)"]') 

873 return 

874 

875 step = self.steps[step_name] 

876 

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}') 

881 

882 # Add connections based on step type 

883 # ステップタイプに基づいて接続を追加 

884 possible_routes = self.get_possible_routes(step_name) 

885 

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) 

892 

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) 

898 

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) 

904 

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) 

910 

911 # Start from the beginning 

912 # 開始点から始める 

913 add_node_and_connections(self.start) 

914 

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') 

924 

925 return "\n".join(lines) 

926 

927 def _generate_text_diagram(self, include_history: bool) -> str: 

928 """ 

929 Generate text-based flow diagram. 

930 テキストベースのフロー図を生成します。 

931  

932 Args: 

933 include_history: Whether to include execution history / 実行履歴を含めるかどうか 

934  

935 Returns: 

936 str: Text diagram / テキスト図 

937 """ 

938 lines = ["Flow Diagram:"] 

939 lines.append("=" * 50) 

940 

941 visited = set() 

942 

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 

947 

948 visited.add(step_name) 

949 prefix = " " * indent 

950 

951 if step_name not in self.steps: 

952 lines.append(f"{prefix}→ {step_name} (END)") 

953 return 

954 

955 step = self.steps[step_name] 

956 step_type = step.__class__.__name__ 

957 

958 lines.append(f"{prefix}→ {step_name} ({step_type})") 

959 

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}") 

966 

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}") 

970 

971 elif hasattr(step, 'branches'): 

972 lines.append(f"{prefix} Branches:") 

973 for branch in step.branches: 

974 lines.append(f"{prefix} → {branch}") 

975 

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) 

981 

982 add_step_info(self.start) 

983 

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})") 

996 

997 return "\n".join(lines) 

998 

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 "" 

1014 

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 

1022 

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 

1034 

1035 async def start_background_task(self) -> asyncio.Task: 

1036 """ 

1037 Start flow as background task 

1038 フローをバックグラウンドタスクとして開始 

1039  

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") 

1045 

1046 self._run_loop_task = asyncio.create_task(self.run_loop()) 

1047 return self._run_loop_task 

1048 

1049 def __str__(self) -> str: 

1050 """String representation of flow""" 

1051 return f"Flow(start={self.start}, steps={len(self.steps)}, finished={self.finished})" 

1052 

1053 def __repr__(self) -> str: 

1054 return self.__str__() 

1055 

1056 

1057# Utility functions for flow creation 

1058# フロー作成用ユーティリティ関数 

1059 

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 ステップのリストから簡単な線形フローを作成 

1068  

1069 Args: 

1070 steps: List of (name, step) tuples / (名前, ステップ)タプルのリスト 

1071 context: Initial context / 初期コンテキスト 

1072 name: Flow name for identification / 識別用フロー名 

1073  

1074 Returns: 

1075 Flow: Created flow / 作成されたフロー 

1076 """ 

1077 if not steps: 

1078 raise ValueError("At least one step is required") 

1079 

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 

1088 

1089 return Flow( 

1090 start=steps[0][0], 

1091 steps=step_dict, 

1092 context=context, 

1093 name=name 

1094 ) 

1095 

1096 

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ブランチを持つ条件付きフローを作成 

1108  

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 / 識別用フロー名 

1116  

1117 Returns: 

1118 Flow: Created flow / 作成されたフロー 

1119 """ 

1120 steps = { 

1121 "start": initial_step, 

1122 "condition": condition_step 

1123 } 

1124 

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 

1132 

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 

1140 

1141 # Connect initial step to condition 

1142 # 初期ステップを条件に接続 

1143 if hasattr(initial_step, 'next_step'): 

1144 initial_step.next_step = "condition" 

1145 

1146 return Flow( 

1147 start="start", 

1148 steps=steps, 

1149 context=context, 

1150 name=name 

1151 )