Coverage for haystack/core/pipeline/pipeline.py: 94%

331 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-12 09:42 +0000

1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> 

2# 

3# SPDX-License-Identifier: Apache-2.0 

4 

5import asyncio 

6import contextlib 

7from collections.abc import AsyncGenerator, AsyncIterator, Mapping 

8from typing import Any, ClassVar, cast 

9 

10from haystack import logging, tracing 

11from haystack.core.component import Component 

12from haystack.core.errors import BreakpointException, PipelineInvalidPipelineSnapshotError, PipelineRuntimeError 

13from haystack.core.pipeline.base import ( 

14 _COMPONENT_INPUT, 

15 _COMPONENT_OUTPUT, 

16 _COMPONENT_VISITS, 

17 ComponentPriority, 

18 PipelineBase, 

19 _validate_component_output_keys, 

20) 

21from haystack.core.pipeline.breakpoint import ( 

22 SnapshotCallback, 

23 _create_pipeline_snapshot, 

24 _save_pipeline_snapshot, 

25 _validate_break_point_against_pipeline, 

26 _validate_pipeline_snapshot_against_pipeline, 

27) 

28from haystack.core.pipeline.utils import _deepcopy_with_exceptions 

29from haystack.core.serialization_security import mark_deserialization_internal 

30from haystack.dataclasses import AsyncStreamingCallbackT, StreamingCallbackT, StreamingChunk, select_streaming_callback 

31from haystack.dataclasses.breakpoints import INTERNAL_INPUTS_FORMAT, Breakpoint, PipelineSnapshot 

32from haystack.dataclasses.streaming_chunk import _invoke_streaming_callback 

33from haystack.telemetry import pipeline_running 

34from haystack.utils import _deserialize_value_with_schema 

35from haystack.utils.async_utils import _execute_component_async 

36from haystack.utils.misc import _get_output_dir 

37 

38logger = logging.getLogger(__name__) 

39 

40 

41class _EndOfStream: 

42 """Sentinel type indicating no more chunks will arrive on the stream.""" 

43 

44 

45class PipelineStreamHandle: 

46 """ 

47 Handle returned by `Pipeline.stream()`. 

48 

49 Async-iterable over `StreamingChunk`s produced by streaming components in the pipeline. After iteration ends, 

50 `result` holds the final pipeline output dict. 

51 

52 By default, iteration cleans up automatically: if the consumer abandons iteration, the underlying pipeline task is 

53 cancelled. `aclose()` is also available for explicit cleanup. 

54 """ 

55 

56 _END_OF_STREAM: ClassVar[_EndOfStream] = _EndOfStream() 

57 _CLEANUP_TIMEOUT_SECONDS: ClassVar[float] = 1.0 

58 

59 def __init__( 

60 self, 

61 queue: asyncio.Queue["StreamingChunk | _EndOfStream"], 

62 task: asyncio.Task[dict[str, Any]], 

63 cancel_on_abandon: bool = True, 

64 ) -> None: 

65 self._queue = queue 

66 self._task = task 

67 self._cancel_on_abandon = cancel_on_abandon 

68 

69 async def __aiter__(self) -> AsyncIterator[StreamingChunk]: 

70 """ 

71 Drain the queue and cancel the pipeline task if iteration is abandoned. 

72 

73 `__aiter__` is an async generator function: each `async for` call gets a generator and `try/finally` runs on 

74 exit. When `cancel_on_abandon` is True (default), abandoned iteration cancels the pipeline task; when False, 

75 the task is left running to completion. 

76 """ 

77 try: 

78 while True: 

79 item = await self._queue.get() 

80 if item is self._END_OF_STREAM: 

81 await self._task # called to make exceptions surface 

82 return 

83 yield cast(StreamingChunk, item) # at this point, item is guaranteed to be a StreamingChunk 

84 

85 finally: 

86 if self._cancel_on_abandon: 

87 await self.aclose() 

88 

89 @property 

90 def result(self) -> dict[str, Any]: 

91 """ 

92 Final pipeline output dict, available only after a successful, complete run. 

93 

94 Raises a `RuntimeError` if the pipeline has not finished or was cancelled. If the pipeline failed, re-raises the 

95 original exception. 

96 """ 

97 if not self._task.done(): 

98 raise RuntimeError("Pipeline has not finished; iterate the handle first.") 

99 if self._task.cancelled(): 

100 raise RuntimeError("Pipeline was cancelled; no result available.") 

101 exc = self._task.exception() 

102 if exc is not None: 

103 raise exc 

104 return self._task.result() 

105 

106 async def aclose(self) -> None: 

107 """ 

108 Cancel the underlying pipeline task. 

109 

110 Bounded by `_CLEANUP_TIMEOUT_SECONDS` so that components cannot block cleanup indefinitely. 

111 """ 

112 if not self._task.done(): 

113 self._task.cancel() 

114 with contextlib.suppress(BaseException): 

115 await asyncio.wait_for(self._task, timeout=self._CLEANUP_TIMEOUT_SECONDS) 

116 

117 

118class Pipeline(PipelineBase): 

119 """ 

120 Orchestration engine that runs components according to the execution graph. 

121 

122 Supports both a synchronous run path (`run`) and an asynchronous run path 

123 (`run_async`, `run_async_generator`, `stream`). 

124 """ 

125 

126 @staticmethod 

127 def _run_component( 

128 component_name: str, 

129 component: dict[str, Any], 

130 inputs: dict[str, Any], 

131 component_visits: dict[str, int], 

132 parent_span: tracing.Span | None = None, 

133 *, 

134 break_point: Breakpoint | None = None, 

135 ) -> Mapping[str, Any]: 

136 """ 

137 Runs a Component with the given inputs. 

138 

139 :param component_name: Name of the Component. 

140 :param component: Component with component metadata. 

141 :param inputs: Inputs for the Component. 

142 :param component_visits: Current state of component visits. 

143 :param parent_span: The parent span to use for the newly created span. 

144 This is to allow tracing to be correctly linked to the pipeline run. 

145 :param break_point: An optional breakpoint. If it targets this component and its visit 

146 count matches the current one, a `BreakpointException` is raised before the Component runs. 

147 :raises PipelineRuntimeError: If Component doesn't return a dictionary. 

148 :return: The output of the Component. 

149 """ 

150 if ( 

151 isinstance(break_point, Breakpoint) 

152 and break_point.component_name == component_name 

153 and break_point.visit_count == component_visits[component_name] 

154 ): 

155 raise BreakpointException.from_triggered_breakpoint(break_point=break_point) 

156 

157 instance: Component = component["instance"] 

158 

159 with PipelineBase._create_component_span( 

160 component_name=component_name, instance=instance, inputs=inputs, parent_span=parent_span 

161 ) as span: 

162 # deepcopy inputs before passing to the tracer so that even if a tracer mutates them 

163 # the component always receives the original unmodified values 

164 inputs_copy = _deepcopy_with_exceptions(inputs) 

165 span.set_content_tag(_COMPONENT_INPUT, inputs) 

166 logger.info("Running component {component_name}", component_name=component_name) 

167 

168 try: 

169 component_output = instance.run(**inputs_copy) 

170 except BreakpointException as error: 

171 # Re-raise BreakpointException to preserve the original exception context 

172 # This is important when Agent components internally use Pipeline._run_component 

173 # and trigger breakpoints that need to bubble up to the main pipeline 

174 raise error 

175 

176 # Any components that internally use Pipeline._run_component could raise a PipelineRuntimeError with 

177 # additional context (e.g. Agent raises an agent snapshot) so we re-raise here instead of wrapping it in 

178 # another PipelineRuntimeError 

179 

180 except PipelineRuntimeError as runtime_error: 

181 raise runtime_error 

182 

183 # Catch all other exceptions and wrap them in a PipelineRuntimeError 

184 except Exception as error: 

185 raise PipelineRuntimeError.from_exception(component_name, instance.__class__, error) from error 

186 

187 component_visits[component_name] += 1 

188 

189 if not isinstance(component_output, Mapping): 

190 raise PipelineRuntimeError.from_invalid_output(component_name, instance.__class__, component_output) 

191 

192 _validate_component_output_keys(component_name, component, component_output) 

193 

194 span.set_tag(_COMPONENT_VISITS, component_visits[component_name]) 

195 span.set_content_tag(_COMPONENT_OUTPUT, component_output) 

196 

197 return component_output 

198 

199 @mark_deserialization_internal 

200 def run( # noqa: PLR0915, PLR0912, C901 

201 self, 

202 data: dict[str, Any], 

203 include_outputs_from: set[str] | None = None, 

204 *, 

205 break_point: Breakpoint | None = None, 

206 pipeline_snapshot: PipelineSnapshot | None = None, 

207 snapshot_callback: SnapshotCallback | None = None, 

208 ) -> dict[str, Any]: 

209 """ 

210 Runs the Pipeline with given input data. 

211 

212 `run` executes synchronously and blocks the calling thread until the run completes. In an async context, 

213 use `run_async` instead. 

214 

215 Usage: 

216 ```python 

217 from haystack import Pipeline, Document 

218 from haystack.components.builders.answer_builder import AnswerBuilder 

219 from haystack.components.builders.chat_prompt_builder import ChatPromptBuilder 

220 from haystack.components.generators.chat import OpenAIResponsesChatGenerator 

221 from haystack.components.retrievers.in_memory import InMemoryBM25Retriever 

222 from haystack.dataclasses import ChatMessage 

223 from haystack.document_stores.in_memory import InMemoryDocumentStore 

224 from haystack.utils import Secret 

225 

226 # Write documents to InMemoryDocumentStore 

227 document_store = InMemoryDocumentStore() 

228 document_store.write_documents([ 

229 Document(content="My name is Jean and I live in Paris."), 

230 Document(content="My name is Mark and I live in Berlin."), 

231 Document(content="My name is Giorgio and I live in Rome.") 

232 ]) 

233 

234 retriever = InMemoryBM25Retriever(document_store=document_store) 

235 

236 prompt_template = \"\"\" 

237 Given these documents, answer the question. 

238 Documents: 

239 {% for doc in documents %} 

240 {{ doc.content }} 

241 {% endfor %} 

242 Question: {{question}} 

243 Answer: 

244 \"\"\" 

245 

246 template = [ChatMessage.from_user(prompt_template)] 

247 prompt_builder = ChatPromptBuilder( 

248 template=template, 

249 required_variables=["question", "documents"], 

250 variables=["question", "documents"] 

251 ) 

252 

253 llm = OpenAIResponsesChatGenerator() 

254 rag_pipeline = Pipeline() 

255 rag_pipeline.add_component("retriever", retriever) 

256 rag_pipeline.add_component("prompt_builder", prompt_builder) 

257 rag_pipeline.add_component("llm", llm) 

258 rag_pipeline.connect("retriever", "prompt_builder.documents") 

259 rag_pipeline.connect("prompt_builder", "llm") 

260 

261 question = "Who lives in Paris?" 

262 results = rag_pipeline.run( 

263 { 

264 "retriever": {"query": question}, 

265 "prompt_builder": {"question": question}, 

266 } 

267 ) 

268 

269 print(results["llm"]["replies"][0].text) 

270 # Jean lives in Paris 

271 ``` 

272 

273 :param data: 

274 A dictionary of inputs for the pipeline's components. Each key is a component name 

275 and its value is a dictionary of that component's input parameters: 

276 ``` 

277 data = { 

278 "comp1": {"input1": 1, "input2": 2}, 

279 } 

280 ``` 

281 For convenience, this format is also supported when input names are unique: 

282 ``` 

283 data = { 

284 "input1": 1, "input2": 2, 

285 } 

286 ``` 

287 :param include_outputs_from: 

288 Set of component names whose individual outputs are to be 

289 included in the pipeline's output. For components that are 

290 invoked multiple times (in a loop), only the last-produced 

291 output is included. 

292 

293 :param break_point: 

294 A breakpoint that pauses execution before the specified component runs by raising a 

295 `BreakpointException` carrying a `PipelineSnapshot` of the current pipeline state. 

296 

297 :param pipeline_snapshot: 

298 A snapshot of a previously interrupted pipeline execution to resume from. Can be combined with 

299 `break_point` to step through a pipeline: resume from the snapshot and pause again at the next 

300 breakpoint. The `break_point` must target a different component or visit count than the one the 

301 snapshot was created at, otherwise it would trigger again before any progress is made. 

302 

303 :param snapshot_callback: 

304 Optional callback function that is invoked when a pipeline snapshot is created. 

305 The callback receives a `PipelineSnapshot` object and can return an optional string 

306 (e.g., a file path or identifier). 

307 If provided, the callback is used instead of the default file-saving behavior, 

308 allowing custom handling of snapshots (e.g., saving to a database, sending to a remote service). 

309 If not provided, the default behavior saves snapshots to a JSON file. 

310 

311 :returns: 

312 A dictionary where each entry corresponds to a component name 

313 and its output. If `include_outputs_from` is `None`, this dictionary 

314 will only contain the outputs of leaf components, i.e., components 

315 without outgoing connections. 

316 

317 :raises ValueError: 

318 If invalid inputs are provided to the pipeline. 

319 :raises PipelineRuntimeError: 

320 If the Pipeline contains cycles with unsupported connections that would cause 

321 it to get stuck and fail running. 

322 Or if a Component fails or returns output in an unsupported type. 

323 :raises PipelineMaxComponentRuns: 

324 If a Component reaches the maximum number of times it can be run in this Pipeline. 

325 :raises PipelineBreakpointException: 

326 When a pipeline_breakpoint is triggered. Contains the component name, state, and partial results. 

327 """ 

328 if ( 

329 break_point 

330 and pipeline_snapshot 

331 and break_point.component_name == pipeline_snapshot.break_point.component_name 

332 and break_point.visit_count == pipeline_snapshot.break_point.visit_count 

333 ): 

334 msg = ( 

335 "The provided break_point targets the same component and visit count as the break_point of the " 

336 "pipeline_snapshot. It would trigger again before the resumed component runs, so the pipeline " 

337 "could not make any progress. Provide a break_point with a different component or visit count." 

338 ) 

339 raise PipelineInvalidPipelineSnapshotError(message=msg) 

340 

341 # make sure all breakpoints are valid, i.e. reference components in the pipeline 

342 if break_point: 

343 _validate_break_point_against_pipeline(break_point, self.graph) 

344 

345 # warm up the pipeline by running each component's warm_up method 

346 self.warm_up() 

347 

348 # after warm-up, so toolsets that load their tools in warm_up() report them 

349 pipeline_running(self) # telemetry 

350 

351 if include_outputs_from is None: 

352 include_outputs_from = set() 

353 

354 pipeline_outputs: dict[str, Any] = {} 

355 # Set when resuming from a snapshot that predates `INTERNAL_INPUTS_FORMAT` and therefore lost the sender of 

356 # each input. Cleared as soon as the paused component has run. 

357 legacy_resume_component: str | None = None 

358 

359 if not pipeline_snapshot: 

360 # normalize `data` 

361 data = self._prepare_component_input_data(data) 

362 

363 # Raise ValueError if input is malformed in some way 

364 self.validate_input(data) 

365 

366 # We create a list of components in the pipeline sorted by name, so that the algorithm runs 

367 # deterministically and independent of insertion order into the pipeline. 

368 ordered_component_names = sorted(self.graph.nodes.keys()) 

369 

370 # We track component visits to decide if a component can run. 

371 component_visits = dict.fromkeys(ordered_component_names, 0) 

372 

373 inputs = self._convert_to_internal_format(pipeline_inputs=data) 

374 

375 else: 

376 # Validate the pipeline snapshot against the current pipeline graph 

377 _validate_pipeline_snapshot_against_pipeline(pipeline_snapshot, self.graph) 

378 

379 # Handle resuming the pipeline from a snapshot 

380 component_visits = pipeline_snapshot.pipeline_state.component_visits 

381 ordered_component_names = pipeline_snapshot.ordered_component_names 

382 data = _deserialize_value_with_schema(pipeline_snapshot.original_input_data) 

383 

384 if pipeline_snapshot.pipeline_state.inputs_format == INTERNAL_INPUTS_FORMAT: 

385 inputs = _deserialize_value_with_schema(pipeline_snapshot.pipeline_state.inputs) 

386 else: 

387 # A legacy snapshot lost the sender of each input, so the only thing we can do is treat them as if 

388 # they came from outside the pipeline. The paused component then has to be let through once. 

389 inputs = self._convert_to_internal_format( 

390 pipeline_inputs=_deserialize_value_with_schema(pipeline_snapshot.pipeline_state.inputs) 

391 ) 

392 legacy_resume_component = pipeline_snapshot.break_point.component_name 

393 

394 # include_outputs_from from the snapshot when resuming 

395 include_outputs_from = pipeline_snapshot.include_outputs_from 

396 

397 # also intermediate_outputs from the snapshot when resuming 

398 pipeline_outputs = _deserialize_value_with_schema(pipeline_snapshot.pipeline_state.pipeline_outputs) 

399 

400 cached_topological_sort = None 

401 # We need to access a component's receivers multiple times during a pipeline run. 

402 # We store them here for easy access. 

403 cached_receivers = {name: self._find_receivers_from(name) for name in ordered_component_names} 

404 

405 with tracing.tracer.trace( 

406 "haystack.pipeline.run", 

407 tags={ 

408 "haystack.pipeline.input_data": data, 

409 "haystack.pipeline.metadata": self.metadata, 

410 "haystack.pipeline.max_runs_per_component": self._max_runs_per_component, 

411 "haystack.pipeline.execution_mode": "sync", 

412 }, 

413 ) as span: 

414 priority_queue = self._fill_queue(ordered_component_names, inputs, component_visits) 

415 

416 # check if pipeline is blocked before execution 

417 self.validate_pipeline(priority_queue) 

418 

419 while True: 

420 candidate = self._get_next_runnable_component(priority_queue, component_visits) 

421 

422 # If there are no runnable components left, we can exit the loop. 

423 # In practice this rarely happens because the queue is constantly refilled even with components that 

424 # have already run. They just get a BLOCKED priority since their inputs have already been consumed. 

425 if candidate is None: 

426 break 

427 

428 priority, component_name, component = candidate 

429 

430 # If the next component is blocked, we do a check to see if the pipeline is possibly blocked and raise 

431 # a warning if it is. 

432 if priority == ComponentPriority.BLOCKED: 

433 if self._is_pipeline_possibly_blocked(current_pipeline_outputs=pipeline_outputs): 

434 # Pipeline is most likely blocked (most likely a configuration issue) so we raise a warning. 

435 self._find_components_blocking_pipeline( 

436 priority_queue=priority_queue, component_visits=component_visits, inputs=inputs 

437 ) 

438 # We always exit the loop since we cannot run the next component. 

439 break 

440 

441 if len(priority_queue) > 0 and priority == ComponentPriority.DEFER: 

442 component_name, topological_sort = self._tiebreak_waiting_components( 

443 component_name=component_name, 

444 priority=priority, 

445 priority_queue=priority_queue, 

446 topological_sort=cached_topological_sort, 

447 ) 

448 cached_topological_sort = topological_sort 

449 component = self._get_component_with_graph_metadata_and_visits( 

450 component_name, component_visits[component_name] 

451 ) 

452 

453 is_resume = legacy_resume_component == component_name 

454 if is_resume: 

455 # Only the first execution of the paused component needs the legacy handling. Later visits of the 

456 # same component inside a loop receive regular inputs and must consume them normally. 

457 legacy_resume_component = None 

458 

459 # A snapshot has to store the component's inputs as they were before the component consumed them, so 

460 # that resuming re-triggers the component the same way this run did. 

461 component_inputs_before_consume = inputs.get(component_name, {}) 

462 component_inputs = self._consume_component_inputs( 

463 component_name=component_name, component=component, inputs=inputs, is_resume=is_resume 

464 ) 

465 

466 # We need to add missing defaults using default values from input sockets because the run signature 

467 # might not provide these defaults for components with inputs defined dynamically upon component 

468 # initialization 

469 component_inputs = self._add_missing_input_defaults(component_inputs, component["input_sockets"]) 

470 

471 try: 

472 component_outputs = self._run_component( 

473 component_name=component_name, 

474 component=component, 

475 inputs=component_inputs, # the inputs to the current component 

476 component_visits=component_visits, 

477 parent_span=span, 

478 break_point=break_point, 

479 ) 

480 except (BreakpointException, PipelineRuntimeError) as error: 

481 saved_break_point: Breakpoint 

482 if isinstance(error, PipelineRuntimeError): 

483 saved_break_point = Breakpoint( 

484 component_name=component_name, 

485 visit_count=component_visits[component_name], 

486 snapshot_file_path=_get_output_dir("pipeline_snapshot"), 

487 ) 

488 else: 

489 saved_break_point = error.break_point 

490 

491 # Create a snapshot of the state of the pipeline before the error occurred. 

492 pipeline_snapshot = _create_pipeline_snapshot( 

493 inputs=_deepcopy_with_exceptions(inputs), 

494 component_inputs=_deepcopy_with_exceptions(component_inputs_before_consume), 

495 break_point=saved_break_point, 

496 component_visits=component_visits, 

497 original_input_data=data, 

498 ordered_component_names=ordered_component_names, 

499 include_outputs_from=include_outputs_from, 

500 pipeline_outputs=pipeline_outputs, 

501 ) 

502 

503 # Attach the pipeline snapshot to the error before re-raising 

504 error.pipeline_snapshot = pipeline_snapshot 

505 full_file_path = _save_pipeline_snapshot( 

506 pipeline_snapshot=pipeline_snapshot, 

507 raise_on_failure=isinstance(error, BreakpointException), 

508 snapshot_callback=snapshot_callback, 

509 ) 

510 error.pipeline_snapshot_file_path = full_file_path 

511 raise error 

512 

513 # Updates global input state with component outputs and returns outputs that should go to 

514 # pipeline outputs. 

515 component_pipeline_outputs = self._write_component_outputs( 

516 component_name=component_name, 

517 component_outputs=component_outputs, 

518 inputs=inputs, 

519 receivers=cached_receivers[component_name], 

520 include_outputs_from=include_outputs_from, 

521 ) 

522 

523 if component_pipeline_outputs or component_name in include_outputs_from: 

524 pipeline_outputs[component_name] = component_pipeline_outputs 

525 if self._is_queue_stale(priority_queue): 

526 priority_queue = self._fill_queue(ordered_component_names, inputs, component_visits) 

527 

528 if isinstance(break_point, Breakpoint): 

529 logger.warning( 

530 "The given breakpoint {break_point} was never triggered. This is because:\n" 

531 "1. The provided component is not a part of the pipeline execution path.\n" 

532 "2. The component did not reach the visit count specified in the pipeline_breakpoint", 

533 break_point=break_point, 

534 ) 

535 

536 # Set here so the tag reflects the final outputs. 

537 span.set_content_tag("haystack.pipeline.output_data", pipeline_outputs) 

538 

539 return pipeline_outputs 

540 

541 @staticmethod 

542 async def _run_component_async( 

543 component_name: str, 

544 component: dict[str, Any], 

545 component_inputs: dict[str, Any], 

546 component_visits: dict[str, int], 

547 parent_span: tracing.Span | None = None, 

548 *, 

549 break_point: Breakpoint | None = None, 

550 ) -> Mapping[str, Any]: 

551 """ 

552 Executes a single component asynchronously. 

553 

554 If the component supports async execution, it is awaited directly as it will run async; 

555 otherwise the component is offloaded to executor. 

556 

557 The method also updates the `visits` count of the component, writes outputs to `inputs_state`, 

558 and returns pruned outputs that get stored in `pipeline_outputs`. 

559 

560 :param component_name: The name of the component. 

561 :param component_inputs: Inputs for the component. 

562 :returns: Outputs from the component that can be yielded from run_async_generator. 

563 """ 

564 if ( 

565 isinstance(break_point, Breakpoint) 

566 and break_point.component_name == component_name 

567 and break_point.visit_count == component_visits[component_name] 

568 ): 

569 raise BreakpointException.from_triggered_breakpoint(break_point=break_point) 

570 

571 instance: Component = component["instance"] 

572 

573 with PipelineBase._create_component_span( 

574 component_name=component_name, instance=instance, inputs=component_inputs, parent_span=parent_span 

575 ) as span: 

576 # deepcopy inputs before passing to the tracer so that even if a tracer mutates them 

577 # the component always receives the original unmodified values 

578 component_inputs_copy = _deepcopy_with_exceptions(component_inputs) 

579 span.set_content_tag(_COMPONENT_INPUT, component_inputs) 

580 logger.info("Running component {component_name}", component_name=component_name) 

581 

582 try: 

583 # For sync-only components, _run_component_async dispatches to a thread via asyncio.to_thread, 

584 # which copies the current contextvars context — preserving e.g. the active tracing span. 

585 outputs = await _execute_component_async(instance, **component_inputs_copy) 

586 except BreakpointException as error: 

587 # Re-raise BreakpointException to preserve the original exception context, matching 

588 # the sync _run_component: a breakpoint triggered by a nested component (e.g. an Agent) 

589 # must bubble up to the main pipeline instead of being wrapped. 

590 raise error 

591 

592 # A component that internally uses Pipeline._run_component_async could raise a PipelineRuntimeError 

593 # carrying additional context (e.g. an agent snapshot); re-raise it instead of wrapping it in 

594 # another PipelineRuntimeError, matching the sync _run_component. 

595 except PipelineRuntimeError as runtime_error: 

596 raise runtime_error 

597 

598 except Exception as error: 

599 raise PipelineRuntimeError.from_exception(component_name, instance.__class__, error) from error 

600 

601 component_visits[component_name] += 1 

602 

603 if not isinstance(outputs, Mapping): 

604 raise PipelineRuntimeError.from_invalid_output(component_name, instance.__class__, outputs) 

605 

606 _validate_component_output_keys(component_name, component, outputs) 

607 

608 span.set_tag(_COMPONENT_VISITS, component_visits[component_name]) 

609 span.set_content_tag(_COMPONENT_OUTPUT, outputs) 

610 

611 return outputs 

612 

613 @staticmethod 

614 async def _wait_for_tasks( 

615 running_tasks: dict[asyncio.Task, str], scheduled_components: set[str], *, return_when: str 

616 ) -> AsyncIterator[dict[str, Any]]: 

617 """ 

618 Waits for running tasks to finish and yields their partial outputs. 

619 

620 :param running_tasks: Mapping of in-flight tasks to the name of the component they run. Finished tasks are 

621 removed in place. 

622 :param scheduled_components: Set of component names that are scheduled but not yet finished. Finished 

623 components are discarded in place. 

624 :param return_when: Either `asyncio.FIRST_COMPLETED` to wait for a single task or `asyncio.ALL_COMPLETED` to 

625 wait for every running task. 

626 :returns: An async iterator of partial outputs, one per finished component that produced an output. 

627 """ 

628 if not running_tasks: 

629 return 

630 

631 done, _pending = await asyncio.wait(running_tasks.keys(), return_when=return_when) 

632 for finished in done: 

633 finished_component_name = running_tasks.pop(finished) 

634 try: 

635 partial_result = finished.result() 

636 except Exception: 

637 # A component failed. Cancel and drain the remaining in-flight tasks so they don't keep running in 

638 # the background (and leak) after the run is aborted, then re-raise the original error. 

639 await Pipeline._cancel_in_flight_tasks(running_tasks, scheduled_components) 

640 raise 

641 scheduled_components.discard(finished_component_name) 

642 if partial_result: 

643 yield {finished_component_name: _deepcopy_with_exceptions(partial_result)} 

644 

645 @staticmethod 

646 async def _cancel_in_flight_tasks(running_tasks: dict[asyncio.Task, str], scheduled_components: set[str]) -> None: 

647 """ 

648 Cancels all in-flight tasks and waits for the cancellations to settle. 

649 

650 Called when a component fails or when the run is abandoned early so that sibling tasks don't keep running in 

651 the background after the pipeline run is aborted. Exceptions from the cancelled tasks are suppressed since we 

652 are already unwinding. 

653 

654 Note: cancellation is only effective for components that run natively async. Sync components are offloaded to 

655 a thread via `asyncio.to_thread` and a running thread cannot be interrupted: cancelling its task abandons 

656 the await, but the thread keeps running until the component's `run` returns. Its outputs are then discarded 

657 (the task never writes them to the pipeline state), so state stays consistent, but side effects (e.g. API 

658 calls) still complete and the thread can outlive this cleanup. 

659 

660 :param running_tasks: Mapping of in-flight tasks to component names. Cleared in place. 

661 :param scheduled_components: Set of scheduled-but-unfinished component names. Cleared in place. 

662 """ 

663 for task in running_tasks: 

664 task.cancel() 

665 # return_exceptions=True so a failing or cancelled sibling doesn't mask the original error we re-raise. 

666 await asyncio.gather(*running_tasks.keys(), return_exceptions=True) 

667 for component_name in running_tasks.values(): 

668 scheduled_components.discard(component_name) 

669 running_tasks.clear() 

670 

671 async def _run_component_in_isolation( 

672 self, 

673 *, 

674 component_name: str, 

675 inputs: dict[str, dict[str, list[dict[str, Any]]]], 

676 pipeline_outputs: dict[str, Any], 

677 component_visits: dict[str, int], 

678 running_tasks: dict[asyncio.Task, str], 

679 scheduled_components: set[str], 

680 cached_receivers: dict[str, Any], 

681 include_outputs_from: set[str], 

682 parent_span: tracing.Span | None, 

683 ) -> AsyncIterator[dict[str, Any]]: 

684 """ 

685 Runs a component with HIGHEST priority in isolation. 

686 

687 We need to run components with HIGHEST priority (i.e. components with a GreedyVariadic input socket) by 

688 themselves, without any other components running concurrently. Otherwise, downstream components could produce 

689 additional inputs for the GreedyVariadic socket. 

690 

691 :param component_name: The name of the component to run. 

692 :param inputs: The global input state shared by all components. Mutated in place. 

693 :param pipeline_outputs: The accumulated pipeline outputs. Mutated in place. 

694 :param component_visits: Current state of component visits. Mutated in place. 

695 :param running_tasks: Mapping of in-flight tasks to component names. Drained in place before running. 

696 :param scheduled_components: Set of scheduled-but-unfinished component names. Mutated in place. 

697 :param cached_receivers: Precomputed mapping of component name to its downstream receivers. 

698 :param include_outputs_from: Set of component names whose outputs should always be included in the output. 

699 :param parent_span: The parent tracing span for the pipeline run. 

700 :returns: An async iterator of partial outputs. 

701 """ 

702 # 1) Wait for all in-flight tasks to finish so the HIGHEST component runs alone. 

703 async for partial_outputs in self._wait_for_tasks( 

704 running_tasks, scheduled_components, return_when=asyncio.ALL_COMPLETED 

705 ): 

706 yield partial_outputs 

707 

708 if component_name in scheduled_components: 

709 # If it's already scheduled for some reason, skip. 

710 return 

711 

712 # 2) Run the HIGHEST component by itself. 

713 scheduled_components.add(component_name) 

714 component = self._get_component_with_graph_metadata_and_visits(component_name, component_visits[component_name]) 

715 component_inputs = self._consume_component_inputs(component_name, component, inputs) 

716 component_inputs = self._add_missing_input_defaults(component_inputs, component["input_sockets"]) 

717 

718 component_outputs = await self._run_component_async( 

719 component_name=component_name, 

720 component=component, 

721 component_inputs=component_inputs, 

722 component_visits=component_visits, 

723 parent_span=parent_span, 

724 ) 

725 

726 pruned = self._write_component_outputs( 

727 component_name=component_name, 

728 component_outputs=component_outputs, 

729 inputs=inputs, 

730 receivers=cached_receivers[component_name], 

731 include_outputs_from=include_outputs_from, 

732 ) 

733 if pruned or component_name in include_outputs_from: 

734 pipeline_outputs[component_name] = pruned 

735 

736 scheduled_components.remove(component_name) 

737 if pruned or component_name in include_outputs_from: 

738 yield {component_name: _deepcopy_with_exceptions(pruned)} 

739 

740 def _schedule_component( 

741 self, 

742 *, 

743 component_name: str, 

744 inputs: dict[str, dict[str, list[dict[str, Any]]]], 

745 pipeline_outputs: dict[str, Any], 

746 component_visits: dict[str, int], 

747 running_tasks: dict[asyncio.Task, str], 

748 scheduled_components: set[str], 

749 ready_sem: asyncio.Semaphore, 

750 cached_receivers: dict[str, Any], 

751 include_outputs_from: set[str], 

752 parent_span: tracing.Span | None, 

753 ) -> None: 

754 """ 

755 Schedules a component to run as a background task without waiting for it to finish. 

756 

757 Inputs are consumed synchronously here (before the task is created) so that other components scheduled in the 

758 same iteration of the scheduling loop observe the updated input state and don't race for the same inputs. 

759 

760 :param component_name: The name of the component to schedule. 

761 :param inputs: The global input state shared by all components. Mutated in place. 

762 :param pipeline_outputs: The accumulated pipeline outputs. Mutated in place by the task once it finishes. 

763 :param component_visits: Current state of component visits. Mutated in place by the task once it finishes. 

764 :param running_tasks: Mapping of in-flight tasks to component names. The new task is registered here. 

765 :param scheduled_components: Set of scheduled-but-unfinished component names. Mutated in place. 

766 :param ready_sem: Semaphore bounding how many components run concurrently. 

767 :param cached_receivers: Precomputed mapping of component name to its downstream receivers. 

768 :param include_outputs_from: Set of component names whose outputs should always be included in the output. 

769 :param parent_span: The parent tracing span for the pipeline run. 

770 """ 

771 if component_name in scheduled_components: 

772 return # already scheduled, do nothing 

773 

774 scheduled_components.add(component_name) 

775 

776 component = self._get_component_with_graph_metadata_and_visits(component_name, component_visits[component_name]) 

777 component_inputs = self._consume_component_inputs(component_name, component, inputs) 

778 component_inputs = self._add_missing_input_defaults(component_inputs, component["input_sockets"]) 

779 

780 async def _runner() -> Mapping[str, Any]: 

781 async with ready_sem: 

782 component_outputs = await self._run_component_async( 

783 component_name=component_name, 

784 component=component, 

785 component_inputs=component_inputs, 

786 component_visits=component_visits, 

787 parent_span=parent_span, 

788 ) 

789 

790 pruned = self._write_component_outputs( 

791 component_name=component_name, 

792 component_outputs=component_outputs, 

793 inputs=inputs, 

794 receivers=cached_receivers[component_name], 

795 include_outputs_from=include_outputs_from, 

796 ) 

797 if pruned or component_name in include_outputs_from: 

798 pipeline_outputs[component_name] = pruned 

799 

800 scheduled_components.remove(component_name) 

801 return pruned 

802 

803 task = asyncio.create_task(_runner()) 

804 running_tasks[task] = component_name 

805 

806 @mark_deserialization_internal 

807 async def run_async_generator( # noqa: PLR0915,C901 

808 self, data: dict[str, Any], include_outputs_from: set[str] | None = None, concurrency_limit: int = 4 

809 ) -> AsyncGenerator[dict[str, Any], None]: 

810 """ 

811 Executes the pipeline step by step asynchronously, yielding partial outputs when any component finishes. 

812 

813 Usage: 

814 ```python 

815 from haystack import Document 

816 from haystack.components.builders import ChatPromptBuilder 

817 from haystack.dataclasses import ChatMessage 

818 from haystack.utils import Secret 

819 from haystack.document_stores.in_memory import InMemoryDocumentStore 

820 from haystack.components.retrievers.in_memory import InMemoryBM25Retriever 

821 from haystack.components.generators.chat import OpenAIResponsesChatGenerator 

822 from haystack.components.builders.prompt_builder import PromptBuilder 

823 from haystack import Pipeline 

824 import asyncio 

825 

826 # Write documents to InMemoryDocumentStore 

827 document_store = InMemoryDocumentStore() 

828 document_store.write_documents([ 

829 Document(content="My name is Jean and I live in Paris."), 

830 Document(content="My name is Mark and I live in Berlin."), 

831 Document(content="My name is Giorgio and I live in Rome.") 

832 ]) 

833 

834 prompt_template = [ 

835 ChatMessage.from_user( 

836 ''' 

837 Given these documents, answer the question. 

838 Documents: 

839 {% for doc in documents %} 

840 {{ doc.content }} 

841 {% endfor %} 

842 Question: {{question}} 

843 Answer: 

844 ''') 

845 ] 

846 

847 # Create and connect pipeline components 

848 retriever = InMemoryBM25Retriever(document_store=document_store) 

849 prompt_builder = ChatPromptBuilder(template=prompt_template) 

850 llm = OpenAIResponsesChatGenerator() 

851 

852 rag_pipeline = Pipeline() 

853 rag_pipeline.add_component("retriever", retriever) 

854 rag_pipeline.add_component("prompt_builder", prompt_builder) 

855 rag_pipeline.add_component("llm", llm) 

856 rag_pipeline.connect("retriever", "prompt_builder.documents") 

857 rag_pipeline.connect("prompt_builder", "llm") 

858 

859 # Prepare input data 

860 question = "Who lives in Paris?" 

861 data = { 

862 "retriever": {"query": question}, 

863 "prompt_builder": {"question": question}, 

864 } 

865 

866 

867 # Process results as they become available 

868 async def process_results(): 

869 async for partial_output in rag_pipeline.run_async_generator( 

870 data=data, 

871 include_outputs_from={"retriever", "llm"} 

872 ): 

873 # Each partial_output contains the results from a completed component 

874 if "retriever" in partial_output: 

875 print("Retrieved documents:", len(partial_output["retriever"]["documents"])) 

876 if "llm" in partial_output: 

877 print("Generated answer:", partial_output["llm"]["replies"][0]) 

878 

879 

880 asyncio.run(process_results()) 

881 ``` 

882 

883 :param data: Initial input data to the pipeline. 

884 :param concurrency_limit: The maximum number of components that are allowed to run concurrently. 

885 :param include_outputs_from: 

886 Set of component names whose individual outputs are to be 

887 included in the pipeline's output. For components that are 

888 invoked multiple times (in a loop), only the last-produced 

889 output is included. 

890 :return: An async iterator containing partial (and final) outputs. 

891 

892 :raises ValueError: 

893 If invalid inputs are provided to the pipeline, or if `concurrency_limit` is less than 1. 

894 :raises PipelineMaxComponentRuns: 

895 If a component exceeds the maximum number of allowed executions within the pipeline. 

896 :raises PipelineRuntimeError: 

897 If the Pipeline contains cycles with unsupported connections that would cause 

898 it to get stuck and fail running. 

899 Or if a Component fails or returns output in an unsupported type. 

900 """ 

901 if concurrency_limit < 1: 

902 raise ValueError("concurrency_limit must be greater than or equal to 1.") 

903 

904 # warm up the pipeline by running each component's warm_up_async (or warm_up) method 

905 await self.warm_up_async() 

906 

907 # after warm-up, so toolsets that load their tools in warm_up() report them 

908 pipeline_running(self) # telemetry 

909 

910 if include_outputs_from is None: 

911 include_outputs_from = set() 

912 

913 pipeline_outputs: dict[str, Any] = {} 

914 

915 # Normalize `data` and raise ValueError if the input is malformed in some way. 

916 data = self._prepare_component_input_data(data) 

917 

918 # Raise ValueError if input is malformed in some way 

919 self.validate_input(data) 

920 

921 # We create a list of components in the pipeline sorted by name, so that the algorithm runs 

922 # deterministically and independent of insertion order into the pipeline. 

923 ordered_component_names = sorted(self.graph.nodes.keys()) 

924 

925 # We track component visits to decide if a component can run. 

926 component_visits = dict.fromkeys(ordered_component_names, 0) 

927 

928 cached_topological_sort = None 

929 # We need to access a component's receivers multiple times during a pipeline run. 

930 # We store them here for easy access. 

931 cached_receivers = {name: self._find_receivers_from(name) for name in ordered_component_names} 

932 

933 # Ephemeral concurrency state shared (and mutated in place) by the scheduling helpers below. 

934 ready_sem = asyncio.Semaphore(concurrency_limit) 

935 running_tasks: dict[asyncio.Task, str] = {} 

936 # A set of component names that have been scheduled but not finished. 

937 scheduled_components: set[str] = set() 

938 

939 with tracing.tracer.trace( 

940 "haystack.pipeline.run", 

941 tags={ 

942 "haystack.pipeline.input_data": data, 

943 "haystack.pipeline.metadata": self.metadata, 

944 "haystack.pipeline.max_runs_per_component": self._max_runs_per_component, 

945 "haystack.pipeline.execution_mode": "async", 

946 }, 

947 ) as parent_span: 

948 inputs = self._convert_to_internal_format(pipeline_inputs=data) 

949 

950 # check if pipeline is blocked before execution 

951 self.validate_pipeline(self._fill_queue(ordered_component_names, inputs, component_visits)) 

952 

953 try: 

954 while True: 

955 # We rebuild the priority queue every iteration: each iteration waits for one or more concurrent 

956 # tasks to finish, which mutates `inputs` and can change many components' priorities at once, so 

957 # we rebuild to give every scheduling decision an up-to-date view. 

958 priority_queue = self._fill_queue(ordered_component_names, inputs, component_visits) 

959 candidate = self._get_next_runnable_component(priority_queue, component_visits) 

960 

961 # If we can't make progress with the queue but tasks are running, we wait for one to finish and 

962 # retry to potentially unblock the priority queue. 

963 if (candidate is None or candidate[0] == ComponentPriority.BLOCKED) and running_tasks: 

964 async for partial_outputs in self._wait_for_tasks( 

965 running_tasks, scheduled_components, return_when=asyncio.FIRST_COMPLETED 

966 ): 

967 yield partial_outputs 

968 continue 

969 

970 # If there are no runnable components left and nothing is running, we can exit the loop. 

971 if candidate is None and not running_tasks: 

972 break 

973 

974 priority, component_name, component = candidate # type: ignore 

975 

976 # If the next component is blocked, we do a check to see if the pipeline is possibly blocked and 

977 # raise a warning if it is. 

978 if priority == ComponentPriority.BLOCKED and not running_tasks: 

979 if self._is_pipeline_possibly_blocked(current_pipeline_outputs=pipeline_outputs): 

980 # Pipeline is most likely blocked (most likely a configuration issue) so we raise a warning. 

981 self._find_components_blocking_pipeline( 

982 priority_queue=priority_queue, component_visits=component_visits, inputs=inputs 

983 ) 

984 # We always exit the loop since we cannot run the next component. 

985 break 

986 

987 # If the next component is already scheduled, we wait for a task to finish to make progress. 

988 if component_name in scheduled_components: 

989 async for partial_outputs in self._wait_for_tasks( 

990 running_tasks, scheduled_components, return_when=asyncio.FIRST_COMPLETED 

991 ): 

992 yield partial_outputs 

993 continue 

994 

995 if priority == ComponentPriority.HIGHEST: 

996 # A HIGHEST priority component must run alone, so we hand off to the isolation helper. 

997 async for partial_outputs in self._run_component_in_isolation( 

998 component_name=component_name, 

999 inputs=inputs, 

1000 pipeline_outputs=pipeline_outputs, 

1001 component_visits=component_visits, 

1002 running_tasks=running_tasks, 

1003 scheduled_components=scheduled_components, 

1004 cached_receivers=cached_receivers, 

1005 include_outputs_from=include_outputs_from, 

1006 parent_span=parent_span, 

1007 ): 

1008 yield partial_outputs 

1009 continue 

1010 

1011 if priority == ComponentPriority.READY: 

1012 # Schedule this component, then schedule as many additional READY components as concurrency 

1013 # allows. 

1014 self._schedule_component( 

1015 component_name=component_name, 

1016 inputs=inputs, 

1017 pipeline_outputs=pipeline_outputs, 

1018 component_visits=component_visits, 

1019 running_tasks=running_tasks, 

1020 scheduled_components=scheduled_components, 

1021 ready_sem=ready_sem, 

1022 cached_receivers=cached_receivers, 

1023 include_outputs_from=include_outputs_from, 

1024 parent_span=parent_span, 

1025 ) 

1026 

1027 # Possibly schedule more READY tasks if concurrency not fully used 

1028 while len(priority_queue) > 0 and not ready_sem.locked(): 

1029 peek_priority, peek_name = priority_queue.peek() 

1030 if peek_priority != ComponentPriority.READY: 

1031 # We stop scheduling: the next component is BLOCKED (can't run), HIGHEST (must run 

1032 # alone), or DEFER (waiting for more inputs - we only schedule it once it becomes 

1033 # READY). 

1034 break 

1035 priority_queue.pop() 

1036 self._schedule_component( 

1037 component_name=peek_name, 

1038 inputs=inputs, 

1039 pipeline_outputs=pipeline_outputs, 

1040 component_visits=component_visits, 

1041 running_tasks=running_tasks, 

1042 scheduled_components=scheduled_components, 

1043 ready_sem=ready_sem, 

1044 cached_receivers=cached_receivers, 

1045 include_outputs_from=include_outputs_from, 

1046 parent_span=parent_span, 

1047 ) 

1048 

1049 # We only schedule components with priority DEFER when no other tasks are running. 

1050 elif priority == ComponentPriority.DEFER and not running_tasks: 

1051 if len(priority_queue) > 0: 

1052 component_name, cached_topological_sort = self._tiebreak_waiting_components( 

1053 component_name=component_name, 

1054 priority=priority, 

1055 priority_queue=priority_queue, 

1056 topological_sort=cached_topological_sort, 

1057 ) 

1058 

1059 self._schedule_component( 

1060 component_name=component_name, 

1061 inputs=inputs, 

1062 pipeline_outputs=pipeline_outputs, 

1063 component_visits=component_visits, 

1064 running_tasks=running_tasks, 

1065 scheduled_components=scheduled_components, 

1066 ready_sem=ready_sem, 

1067 cached_receivers=cached_receivers, 

1068 include_outputs_from=include_outputs_from, 

1069 parent_span=parent_span, 

1070 ) 

1071 

1072 # To make progress, we wait for one task to complete before restarting the loop. 

1073 async for partial_outputs in self._wait_for_tasks( 

1074 running_tasks, scheduled_components, return_when=asyncio.FIRST_COMPLETED 

1075 ): 

1076 yield partial_outputs 

1077 

1078 # Safety net: drain any leftover tasks once the scheduling loop has finished. With the current loop 

1079 # both `break` paths require `running_tasks` to be empty, so this is a no-op. We keep it so that a 

1080 # future change adding a `break` that leaves tasks in flight doesn't lose outputs. 

1081 async for partial_outputs in self._wait_for_tasks( 

1082 running_tasks, scheduled_components, return_when=asyncio.ALL_COMPLETED 

1083 ): 

1084 yield partial_outputs 

1085 

1086 # Set here so the tag reflects the final outputs. 

1087 parent_span.set_content_tag("haystack.pipeline.output_data", pipeline_outputs) 

1088 

1089 # Yield the final pipeline outputs. 

1090 yield pipeline_outputs 

1091 finally: 

1092 # If iteration is abandoned early (e.g. the consumer stops iterating the generator and closes it) or 

1093 # the run is cancelled, cancel any tasks still in flight so they don't leak. 

1094 # This is a no-op on normal completion and on a component error, since no tasks are left running by then 

1095 await self._cancel_in_flight_tasks(running_tasks, scheduled_components) 

1096 

1097 @mark_deserialization_internal 

1098 async def run_async( 

1099 self, data: dict[str, Any], include_outputs_from: set[str] | None = None, concurrency_limit: int = 4 

1100 ) -> dict[str, Any]: 

1101 """ 

1102 Provides an asynchronous interface to run the pipeline with provided input data. 

1103 

1104 This method allows the pipeline to be integrated into an asynchronous workflow, enabling non-blocking 

1105 execution of pipeline components. 

1106 

1107 Usage: 

1108 ```python 

1109 import asyncio 

1110 

1111 from haystack import Document 

1112 from haystack.components.builders import ChatPromptBuilder 

1113 from haystack.components.generators.chat import OpenAIResponsesChatGenerator 

1114 from haystack.components.retrievers.in_memory import InMemoryBM25Retriever 

1115 from haystack import Pipeline 

1116 from haystack.dataclasses import ChatMessage 

1117 from haystack.document_stores.in_memory import InMemoryDocumentStore 

1118 

1119 # Write documents to InMemoryDocumentStore 

1120 document_store = InMemoryDocumentStore() 

1121 document_store.write_documents([ 

1122 Document(content="My name is Jean and I live in Paris."), 

1123 Document(content="My name is Mark and I live in Berlin."), 

1124 Document(content="My name is Giorgio and I live in Rome.") 

1125 ]) 

1126 

1127 prompt_template = [ 

1128 ChatMessage.from_user( 

1129 ''' 

1130 Given these documents, answer the question. 

1131 Documents: 

1132 {% for doc in documents %} 

1133 {{ doc.content }} 

1134 {% endfor %} 

1135 Question: {{question}} 

1136 Answer: 

1137 ''') 

1138 ] 

1139 

1140 retriever = InMemoryBM25Retriever(document_store=document_store) 

1141 prompt_builder = ChatPromptBuilder(template=prompt_template) 

1142 llm = OpenAIResponsesChatGenerator() 

1143 

1144 rag_pipeline = Pipeline() 

1145 rag_pipeline.add_component("retriever", retriever) 

1146 rag_pipeline.add_component("prompt_builder", prompt_builder) 

1147 rag_pipeline.add_component("llm", llm) 

1148 rag_pipeline.connect("retriever", "prompt_builder.documents") 

1149 rag_pipeline.connect("prompt_builder", "llm") 

1150 

1151 # Ask a question 

1152 question = "Who lives in Paris?" 

1153 

1154 async def run_inner(data, include_outputs_from): 

1155 return await rag_pipeline.run_async(data=data, include_outputs_from=include_outputs_from) 

1156 

1157 data = { 

1158 "retriever": {"query": question}, 

1159 "prompt_builder": {"question": question}, 

1160 } 

1161 

1162 results = asyncio.run(run_inner(data, include_outputs_from={"retriever", "llm"})) 

1163 

1164 print(results["llm"]["replies"][0].text) 

1165 # >> Jean lives in Paris. 

1166 ``` 

1167 

1168 :param data: 

1169 A dictionary of inputs for the pipeline's components. Each key is a component name 

1170 and its value is a dictionary of that component's input parameters: 

1171 ``` 

1172 data = { 

1173 "comp1": {"input1": 1, "input2": 2}, 

1174 } 

1175 ``` 

1176 For convenience, this format is also supported when input names are unique: 

1177 ``` 

1178 data = { 

1179 "input1": 1, "input2": 2, 

1180 } 

1181 ``` 

1182 :param include_outputs_from: 

1183 Set of component names whose individual outputs are to be 

1184 included in the pipeline's output. For components that are 

1185 invoked multiple times (in a loop), only the last-produced 

1186 output is included. 

1187 :param concurrency_limit: The maximum number of components that should be allowed to run concurrently. 

1188 :returns: 

1189 A dictionary where each entry corresponds to a component name 

1190 and its output. If `include_outputs_from` is `None`, this dictionary 

1191 will only contain the outputs of leaf components, i.e., components 

1192 without outgoing connections. 

1193 

1194 :raises ValueError: 

1195 If invalid inputs are provided to the pipeline, or if `concurrency_limit` is less than 1. 

1196 :raises PipelineRuntimeError: 

1197 If the Pipeline contains cycles with unsupported connections that would cause 

1198 it to get stuck and fail running. 

1199 Or if a Component fails or returns output in an unsupported type. 

1200 :raises PipelineMaxComponentRuns: 

1201 If a Component reaches the maximum number of times it can be run in this Pipeline. 

1202 """ 

1203 final: dict[str, Any] = {} 

1204 async for partial in self.run_async_generator( 

1205 data=data, concurrency_limit=concurrency_limit, include_outputs_from=include_outputs_from 

1206 ): 

1207 final = partial 

1208 return final or {} 

1209 

1210 @mark_deserialization_internal 

1211 def stream( 

1212 self, 

1213 data: dict[str, Any], 

1214 *, 

1215 streaming_components: list[str] | None = None, 

1216 include_outputs_from: set[str] | None = None, 

1217 concurrency_limit: int = 4, 

1218 cancel_on_abandon: bool = True, 

1219 ) -> PipelineStreamHandle: 

1220 """ 

1221 Run the pipeline and return a handle that streams `StreamingChunk`s as they arrive. 

1222 

1223 Iterate the handle with `async for` to consume chunks; after iteration ends, `handle.result` holds the final 

1224 pipeline output dict (same as `run_async`). By default, if iteration is abandoned, the underlying pipeline task 

1225 is cancelled automatically. Pass `cancel_on_abandon=False` to instead let the pipeline run to completion. 

1226 

1227 For every async-capable component that exposes a `streaming_callback` input socket, a forwarder is injected at 

1228 runtime that pushes chunks onto the handle's queue. If a `streaming_callback` is provided at component init or 

1229 at runtime (inside `data`, e.g. `data={"llm": {"streaming_callback": cb}}`), it is also invoked for each chunk. 

1230 Async callbacks are preferred; a sync callback is accepted but will run synchronously on the event loop and 

1231 may block it. 

1232 

1233 Usage: 

1234 ```python 

1235 import asyncio 

1236 

1237 from haystack.components.builders import ChatPromptBuilder 

1238 from haystack.components.generators.chat import OpenAIResponsesChatGenerator 

1239 from haystack import Pipeline 

1240 from haystack.dataclasses import ChatMessage 

1241 

1242 pipe = Pipeline() 

1243 pipe.add_component( 

1244 "prompt_builder", 

1245 ChatPromptBuilder(template=[ChatMessage.from_user("Tell me about {{topic}}")]), 

1246 ) 

1247 pipe.add_component("llm", OpenAIResponsesChatGenerator()) 

1248 pipe.connect("prompt_builder.prompt", "llm.messages") 

1249 

1250 async def main(): 

1251 handle = pipe.stream(data={"prompt_builder": {"topic": "Italy"}}) 

1252 async for chunk in handle: 

1253 print(chunk.content, end="", flush=True) 

1254 return handle.result 

1255 

1256 result = asyncio.run(main()) 

1257 print(result["llm"]["replies"]) 

1258 ``` 

1259 

1260 :param data: 

1261 A dictionary of inputs for the pipeline's components. Each key is a component name 

1262 and its value is a dictionary of that component's input parameters: 

1263 ``` 

1264 data = { 

1265 "comp1": {"input1": 1, "input2": 2}, 

1266 } 

1267 ``` 

1268 For convenience, this format is also supported when input names are unique: 

1269 ``` 

1270 data = { 

1271 "input1": 1, "input2": 2, 

1272 } 

1273 ``` 

1274 :param streaming_components: Names of components to stream from. If `None` (default), every streaming-capable 

1275 component is forwarded. If a list, only the listed components are forwarded; unknown names or names of 

1276 components that do not support streaming raise `ValueError`. 

1277 :param include_outputs_from: 

1278 Set of component names whose individual outputs are to be 

1279 included in the pipeline's output. For components that are 

1280 invoked multiple times (in a loop), only the last-produced 

1281 output is included. 

1282 :param concurrency_limit: The maximum number of components that should be allowed to run concurrently. 

1283 :param cancel_on_abandon: If `True` (default), the underlying pipeline task is cancelled when iteration is 

1284 abandoned. If `False`, the pipeline runs to completion even when the consumer stops reading. 

1285 :returns: 

1286 A `PipelineStreamHandle` that is async-iterable over `StreamingChunk`s. After iteration ends, 

1287 `handle.result` holds the final pipeline output dict (same shape as `run_async`). 

1288 

1289 :raises ValueError: 

1290 If `streaming_components` contains unknown component names or components that do not support streaming, 

1291 or if invalid inputs are provided to the pipeline, or if `concurrency_limit` is less than 1. 

1292 :raises PipelineRuntimeError: 

1293 Surfaced during iteration. If the Pipeline contains cycles with unsupported connections that would cause 

1294 it to get stuck and fail running, or if a Component fails or returns output in an unsupported type. 

1295 :raises PipelineMaxComponentRuns: 

1296 Surfaced during iteration. If a Component reaches the maximum number of times it can be run in this 

1297 Pipeline. 

1298 """ 

1299 streaming_capable = { 

1300 name 

1301 for name in self.graph.nodes 

1302 if getattr(self.graph.nodes[name]["instance"], "__haystack_supports_async__", False) 

1303 and "streaming_callback" in self.graph.nodes[name]["instance"].__haystack_input__ 

1304 } 

1305 if streaming_components is not None: 

1306 requested = set(streaming_components) 

1307 unknown = requested - set(self.graph.nodes) 

1308 non_streaming = requested - unknown - streaming_capable 

1309 if unknown: 

1310 raise ValueError(f"Unknown components in streaming_components: {sorted(unknown)}") 

1311 if non_streaming: 

1312 raise ValueError(f"These components do not support streaming: {sorted(non_streaming)}") 

1313 

1314 queue: asyncio.Queue[StreamingChunk | _EndOfStream] = asyncio.Queue() 

1315 

1316 def make_forwarder(user_callback: StreamingCallbackT | None) -> AsyncStreamingCallbackT: 

1317 async def forwarder(chunk: StreamingChunk) -> None: 

1318 await queue.put(chunk) 

1319 if user_callback is not None: 

1320 await _invoke_streaming_callback(user_callback, chunk) 

1321 

1322 return forwarder 

1323 

1324 new_data: dict[str, Any] = self._prepare_component_input_data(data) 

1325 for name in streaming_capable: 

1326 if streaming_components is not None and name not in streaming_components: 

1327 continue 

1328 instance = self.graph.nodes[name]["instance"] 

1329 comp_inputs = new_data.setdefault(name, {}) 

1330 

1331 user_callback = select_streaming_callback( 

1332 init_callback=getattr(instance, "streaming_callback", None), 

1333 runtime_callback=comp_inputs.get("streaming_callback"), 

1334 requires_async=True, 

1335 ) 

1336 comp_inputs["streaming_callback"] = make_forwarder(user_callback) 

1337 

1338 async def runner() -> dict[str, Any]: 

1339 try: 

1340 return await self.run_async( 

1341 new_data, include_outputs_from=include_outputs_from, concurrency_limit=concurrency_limit 

1342 ) 

1343 finally: 

1344 await queue.put(PipelineStreamHandle._END_OF_STREAM) 

1345 

1346 task = asyncio.create_task(runner()) 

1347 return PipelineStreamHandle(queue=queue, task=task, cancel_on_abandon=cancel_on_abandon)