Skip to content

feat: make RunFlow component outputs dynamic#10327

Closed
rodrigosnader wants to merge 5 commits into
mainfrom
feat/dynamic-runflow-outputs
Closed

feat: make RunFlow component outputs dynamic#10327
rodrigosnader wants to merge 5 commits into
mainfrom
feat/dynamic-runflow-outputs

Conversation

@rodrigosnader
Copy link
Copy Markdown
Contributor

@rodrigosnader rodrigosnader commented Oct 18, 2025

RunFlow Component Dynamic Outputs

Problems Solved

Problem: RunFlow component always showed 3 fixed outputs (Data, DataFrame, Message) regardless of what the selected flow actually produced
Solution: Made RunFlow dynamically mirror the exact outputs of the selected flow

Problem: When flows had multiple output components, there was no way to distinguish between them
Solution: Display component names for single outputs and add unique IDs for multiple outputs

Problem: Output names didn't match what the selected flow produced (always showed generic names)
Solution: Use the actual component names from the flow (e.g., "Chat Output" instead of "Flow Message Output")

Problem: No visual feedback when a flow had no outputs
Solution: Show no outputs when the selected flow has no output components, accurately reflecting the flow structure

Technical Changes

  • Remove default 3 outputs (Data, DataFrame, Message) from base component
  • Add dynamic output extraction based on selected flow's output components
  • Extract output types from flow's graph structure using base_classes
  • Show component name for single output (e.g., 'Chat Output')
  • Show component name + ID suffix for multiple outputs (e.g., 'Chat Output - msvr6')
  • Ensure unique output names using component ID prefix internally
  • Add flow_output method that preserves original output type from selected flow

This change makes RunFlow dynamically mirror the outputs of the selected flow, showing the correct types and names instead of fixed default outputs.

Summary by CodeRabbit

  • Refactor
    • Streamlined flow output architecture by consolidating multiple methods into a single unified approach, improving consistency while preserving original data type information.
    • Enhanced dynamic output detection and configuration system to automatically populate outputs based on selected flow types at runtime, providing greater flexibility for diverse flow scenarios.

- Remove default 3 outputs (Data, DataFrame, Message) from base component
- Add dynamic output extraction based on selected flow's output components
- Extract output types from flow's graph structure using base_classes
- Show component name for single output (e.g., 'Chat Output')
- Show component name + ID suffix for multiple outputs (e.g., 'Chat Output - msvr6')
- Ensure unique output names using component ID prefix internally
- Add flow_output method that preserves original output type from selected flow

This change makes RunFlow dynamically mirror the outputs of the selected flow,
showing the correct types and names instead of fixed default outputs.
- Remove debug logging statements
- Simplify future output types comments
- Keep fallback logic for type inference when base_classes is not available
- Remove duplicate fallback logic in update_outputs
- Simplify code as output types are always extracted in update_build_config
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Oct 18, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Refactors flow output handling by consolidating multiple output methods into a single flow_output method and replacing predefined outputs with dynamic generation. The changes allow outputs to be populated at runtime based on selected flows, with defensive null handling for empty outputs.

Changes

Cohort / File(s) Summary
Base component output consolidation
src/lfx/src/lfx/base/tools/run_flow.py
Removes dataframe_output() and message_output() methods; replaces with unified flow_output() that returns the first output's result in its original type or None. Converts _base_outputs from a hard-coded three-output list to an empty list for dynamic population. Preserves data_output() behavior.
Dynamic output generation
src/lfx/src/lfx/components/logic/run_flow.py
Adds __init__() to initialize flow output type cache. Introduces update_outputs() to generate frontend outputs based on captured flow types. Adds extract_output_types_from_graph() to inspect flow graphs and build output descriptors. Updates update_build_config() to populate and reset the cache.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant RunFlowComponent
    participant FlowGraph
    participant RunFlowBaseComponent

    Note over RunFlowComponent: Old Flow (Static Outputs)
    User->>RunFlowComponent: Initialize
    RunFlowComponent->>RunFlowBaseComponent: _base_outputs (3 predefined outputs)
    
    Note over RunFlowComponent: New Flow (Dynamic Outputs)
    User->>RunFlowComponent: Select Flow & call update_build_config()
    RunFlowComponent->>FlowGraph: extract_output_types_from_graph()
    FlowGraph-->>RunFlowComponent: Output descriptors
    RunFlowComponent->>RunFlowComponent: Cache flow_output_types
    RunFlowComponent->>RunFlowComponent: update_outputs() for frontend
    RunFlowComponent->>RunFlowBaseComponent: _base_outputs (dynamically populated)
    User->>RunFlowBaseComponent: Call flow_output()
    RunFlowBaseComponent-->>User: First output's result (preserved type) or None
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

The changes involve logic density in output type extraction and dynamic cache management across two files, with new methods requiring careful review for correctness. However, the patterns are consistent and localized, making this moderate complexity.

Possibly related PRs

Suggested labels

lgtm

Suggested reviewers

  • edwinjosechittilappilly
  • Cristhianzl

Pre-merge checks and finishing touches

❌ Failed checks (1 error, 3 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Test Coverage For New Implementations ❌ Error The PR implements significant architectural changes to RunFlow components, including removing existing methods (dataframe_output and message_output), adding new public methods (flow_output, update_outputs, extract_output_types_from_graph), modifying the _base_outputs attribute from a concrete list to an empty list, and introducing dynamic output generation based on selected flows. The shell script results confirm that only two Python implementation files were modified—src/lfx/src/lfx/base/tools/run_flow.py and src/lfx/src/lfx/components/logic/run_flow.py—with no corresponding test files added or updated. The repository demonstrates clear test organization conventions with test files located at src/lfx/tests/unit/ matching the source structure, as evidenced by existing test files like test_loop.py for the Loop component. The absence of any new or updated test files means these substantial functional changes lack test coverage to verify correctness and prevent regressions. Create test files following Langflow's established naming conventions: test_run_flow.py in src/lfx/tests/unit/base/tools/ to test RunFlowBaseComponent changes, and test_run_flow_component.py in src/lfx/tests/unit/components/logic/ to test RunFlowComponent changes. Test coverage should include: unit tests for the new flow_output() method ensuring it correctly returns the first output with preserved type, tests for extract_output_types_from_graph() to verify correct extraction of output descriptors from flow graphs, tests for update_outputs() to validate dynamic output generation when flows are selected, tests verifying correct routing of multiple outputs to prevent all outputs from returning identical values (as noted in previous review comments), and regression tests ensuring backward compatibility for removed methods or their replacements.
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Test Quality And Coverage ⚠️ Warning The PR introduces significant new functionality but lacks any test coverage. Three key methods were added: (1) flow_output() in the base component which is an async method returning flow output results, (2) update_outputs() in RunFlowComponent for dynamic output generation based on selected flows, and (3) extract_output_types_from_graph() to introspect flow graphs. However, no test files were created to validate these implementations. The codebase follows pytest patterns with @pytest.mark.asyncio decorators for async functions and stores tests in src/backend/tests and src/lfx/tests directories. Critically, a review comment identified that flow_output() cannot properly route to different outputs when a flow has multiple outputs—all outputs would receive the same value instead of their correct values. This architectural flaw highlights the absence of multi-output scenario tests that would have caught this issue. Similar logic components like Loop and ConditionalRouter have corresponding test files (test_loop.py, test_conditional_router.py) with output validation, establishing a clear project pattern that this PR does not follow. Create a comprehensive test file at src/backend/tests/unit/components/logic/test_run_flow.py following the project's established patterns (using pytest and @pytest.mark.asyncio for async methods). Tests must cover: (1) flow_output() with mock flows containing single and multiple outputs to verify each output receives its correct distinct value, not the same value for all outputs (addressing the critical routing issue raised in review), (2) update_outputs() with various flow selections and no flow selected, validating that outputs are correctly created or cleared, (3) extract_output_types_from_graph() with different graph structures and testing the base_class fallback logic, and (4) edge cases like empty outputs returning None, missing graph data, and invalid component configurations. Use the existing Memory component tests and data file component tests as reference patterns for structure and assertions.
Test File Naming And Structure ⚠️ Warning The PR modifies only src/lfx/src/lfx/components/logic/run_flow.py (confirmed by git diff) with no test files added or modified. This represents a significant architectural change including removal of dataframe_output and message_output methods, introduction of a new flow_output method, dynamic output generation, and changes to base_outputs from three outputs to an empty list. However, the established pattern for logic component tests (evidenced by test_loop.py, test_conditional_router.py, test_flow_tool.py) is completely absent for this component. The tests use pytest async structures with TestComponentName classes inheriting from ComponentTestBaseWithClient or ComponentTestBaseWithoutClient, with standard fixtures (component_class, default_kwargs, file_names_mapping) and async test* methods. Additionally, the review comments identified trailing whitespace issues and a critical architectural flaw with output routing that requires test coverage. Add test file src/backend/tests/unit/components/logic/test_run_flow.py with TestRunFlowComponent class inheriting from ComponentTestBaseWithClient, including required pytest fixtures (component_class returning RunFlowComponent, default_kwargs with required initialization parameters, file_names_mapping returning empty list). Include async test_* methods with descriptive names: test_component_initialization validating display_name and attributes, test_inputs_configuration, test_outputs_configuration, test_single_output_scenario, test_multiple_outputs_scenario, test_empty_outputs_scenario, test_flow_selection_and_graph_extraction, test_output_routing_with_multiple_outputs (critical for architectural fix), and test_error_handling. Remove trailing whitespace from both modified files as identified in review comments.
Excessive Mock Usage Warning ❓ Inconclusive The custom check for excessive mock usage cannot be properly evaluated because the PR does not include dedicated unit tests for the new RunFlow dynamic output functionality. While the codebase has established test patterns (as evidenced by test_flow_tool.py and test_loop.py in the backend), and Langflow documentation requires component tests to follow specific conventions, no corresponding test_run_flow.py file was found for the RunFlowComponent or RunFlowBaseComponent changes. The PR introduces three new methods (flow_output(), update_outputs(), extract_output_types_from_graph()) and makes structural changes to output handling, yet there is no visible test coverage. This absence means excessive mock usage cannot be assessed, but more critically, the critical architectural issue raised in review comments—that flow_output() cannot properly route multiple outputs—remains untested and unvalidated. The PR must include a comprehensive test file (src/backend/tests/unit/components/logic/test_run_flow.py) following Langflow's established testing conventions that inherit from appropriate base test classes. Tests should cover: (1) dynamic output extraction from flow graphs with real graph structures; (2) proper routing of multiple outputs to ensure each receives distinct values rather than duplicates; (3) the fragile base_classes fallback logic identified in review comments; (4) edge cases like empty flows and missing output components; and (5) integration scenarios validating end-to-end output type preservation. Mocks should be used appropriately only for external dependencies (like alist_flows()), not for core graph parsing and output routing logic.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: make RunFlow component outputs dynamic" directly and clearly summarizes the main architectural change in the changeset. The PR transitions the RunFlow component from exposing three fixed outputs (Data, DataFrame, Message) to dynamically mirroring the selected flow's actual outputs, which is precisely what the title conveys. The title is concise, specific, and avoids vague terminology, making it immediately clear to reviewers scanning history what the primary change accomplishes.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the enhancement New feature or request label Oct 18, 2025
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Oct 18, 2025
@github-actions
Copy link
Copy Markdown
Contributor

Component index has been automatically updated due to changes in src/lfx/src/lfx/components/

New Index:
🧩 Components: 357
📁 Categories: 92

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lfx/src/lfx/base/tools/run_flow.py (1)

12-12: Remove unused DataFrame import.

The DataFrame import is no longer used after removing the dataframe_output method.

Apply this diff:

 from lfx.schema.data import Data
-from lfx.schema.dataframe import DataFrame
 from lfx.schema.dotdict import dotdict
🧹 Nitpick comments (1)
src/lfx/src/lfx/components/logic/run_flow.py (1)

42-42: Consider simplifying the condition.

Since flow_output_types is always initialized in __init__, the hasattr check is redundant. You can simplify to just check if the list is non-empty.

Apply this diff:

-        if hasattr(self, 'flow_output_types') and self.flow_output_types:
+        if self.flow_output_types:
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 621c103 and 2527bf7.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • src/lfx/src/lfx/base/tools/run_flow.py (2 hunks)
  • src/lfx/src/lfx/components/logic/run_flow.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/lfx/src/lfx/components/logic/run_flow.py (1)
src/lfx/src/lfx/custom/custom_component/custom_component.py (1)
  • graph (185-186)
src/lfx/src/lfx/base/tools/run_flow.py (1)
src/lfx/src/lfx/components/logic/run_flow.py (1)
  • run_flow_with_tweaks (135-166)
🪛 GitHub Actions: Ruff Style Check
src/lfx/src/lfx/base/tools/run_flow.py

[error] 12-12: Ruff: F401 'lfx.schema.dataframe.DataFrame' imported but unused. Remove the unused import or use it.

🪛 GitHub Check: Ruff Style Check (3.13)
src/lfx/src/lfx/components/logic/run_flow.py

[failure] 38-38: Ruff (W293)
src/lfx/src/lfx/components/logic/run_flow.py:38:1: W293 Blank line contains whitespace


[failure] 33-33: Ruff (W293)
src/lfx/src/lfx/components/logic/run_flow.py:33:1: W293 Blank line contains whitespace


[failure] 31-31: Ruff (W293)
src/lfx/src/lfx/components/logic/run_flow.py:31:1: W293 Blank line contains whitespace


[failure] 22-22: Ruff (W293)
src/lfx/src/lfx/components/logic/run_flow.py:22:1: W293 Blank line contains whitespace

src/lfx/src/lfx/base/tools/run_flow.py

[failure] 78-78: Ruff (W293)
src/lfx/src/lfx/base/tools/run_flow.py:78:1: W293 Blank line contains whitespace


[failure] 73-73: Ruff (W293)
src/lfx/src/lfx/base/tools/run_flow.py:73:1: W293 Blank line contains whitespace


[failure] 70-70: Ruff (W293)
src/lfx/src/lfx/base/tools/run_flow.py:70:1: W293 Blank line contains whitespace


[failure] 67-67: Ruff (W293)
src/lfx/src/lfx/base/tools/run_flow.py:67:1: W293 Blank line contains whitespace

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: Test Docker Images / Test docker images
  • GitHub Check: Lint Backend / Run Mypy (3.11)
  • GitHub Check: Lint Backend / Run Mypy (3.12)
  • GitHub Check: Run Backend Tests / Test CLI - Python 3.10
  • GitHub Check: Lint Backend / Run Mypy (3.10)
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 5
  • GitHub Check: Lint Backend / Run Mypy (3.13)
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 3
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 4
  • GitHub Check: Run Backend Tests / Integration Tests - Python 3.10
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 2
  • GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 1
  • GitHub Check: Run Backend Tests / LFX Tests - Python 3.10
  • GitHub Check: Test Starter Templates
  • GitHub Check: Run Ruff Check and Format
  • GitHub Check: Update Starter Projects
  • GitHub Check: update-index
🔇 Additional comments (5)
src/lfx/src/lfx/components/logic/run_flow.py (4)

23-25: LGTM!

The initialization correctly sets up the cache for flow output types.


27-54: Dynamic output generation looks good.

The method correctly generates frontend outputs based on the selected flow's output types, with appropriate early returns for edge cases.


56-112: Solid implementation of output extraction.

The method handles multiple vs. single outputs elegantly and includes good error handling. The display name strategy (adding ID suffix for multiple outputs) improves UX clarity.


125-131: Good cache management.

Populating flow_output_types after building the graph and resetting on error ensures consistent state.

src/lfx/src/lfx/base/tools/run_flow.py (1)

42-42: Excellent change to enable dynamic outputs.

Changing _base_outputs to an empty list with a clear comment about dynamic population is the right approach for this refactoring.

Comment on lines +64 to +79
async def flow_output(self):
"""Return the output from the selected flow with the same type as the flow's output component."""
run_outputs = await self.run_flow_with_tweaks()
first_output = run_outputs[0]

if isinstance(first_output, DataFrame):
return first_output

# just adaptive output Message
_, message_result = next(iter(run_outputs[0].outputs[0].results.items()))
message_data = message_result.data
return DataFrame(data=message_data if isinstance(message_data, list) else [message_data])

async def message_output(self) -> Message:
"""Return the message output."""
run_outputs = await self.run_flow_with_tweaks()
_, message_result = next(iter(run_outputs[0].outputs[0].results.items()))
if isinstance(message_result, Message):
return message_result
if isinstance(message_result, str):
return Message(text=message_result)
return Message(text=message_result.data["text"])

if not run_outputs or not run_outputs[0].outputs:
return None

# Get the first output and return its result with the original type
first_output = run_outputs[0].outputs[0]

# Extract the result from the output and return it as-is to preserve the original type
if first_output.results:
_, result = next(iter(first_output.results.items()))
return result

return None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Clarify handling of multiple outputs.

The method always returns the first output (run_outputs[0].outputs[0]), but according to the PR objectives, flows can have multiple outputs. When update_outputs creates multiple Output objects (e.g., "Chat Output - msvr6", "Text Output - abc12"), they all reference method="flow_output", yet this method doesn't differentiate between them.

Please verify:

  1. How does the framework route to the correct output when multiple outputs exist?
  2. Should flow_output accept parameters to identify which specific output to return?
  3. Or does each output get a separate method name in the actual implementation?

Run this script to check how the method attribute is used in the Output creation:


🏁 Script executed:

#!/bin/bash
# Search for how Output objects with method="flow_output" are created and used
rg -nP --type=py -C5 'method\s*=\s*["\']flow_output["\']'

Length of output: 68


I'll fix the shell script syntax error and gather more information about how the framework handles multiple outputs.


🏁 Script executed:

#!/bin/bash
# Search for Output objects with method="flow_output" - simplified pattern
rg -nP --type=py -C5 'method.*flow_output' | head -50

Length of output: 951


🏁 Script executed:

#!/bin/bash
# Search for Output class definition and instantiation
rg -nP --type=py 'class Output|Output\(' | head -50

Length of output: 5317


🏁 Script executed:

#!/bin/bash
# Search for update_outputs method to understand how multiple outputs are created
rg -nP --type=py -C10 'def update_outputs' | head -100

Length of output: 9126


🏁 Script executed:

#!/bin/bash
# Search for run_flow_with_tweaks to understand output structure
rg -nP --type=py -C10 'def run_flow_with_tweaks' | head -100

Length of output: 3581


I need to understand how the framework routes to the correct output method when multiple outputs reference the same method name.


🏁 Script executed:

#!/bin/bash
# Find how Output.method is invoked or called
rg -nP --type=py '\.method' | grep -i output | head -50

Length of output: 4848


🏁 Script executed:

#!/bin/bash
# Search for how framework executes output methods - look for getattr or method calls
rg -nP --type=py 'getattr.*method|method.*getattr' | head -30

Length of output: 1891


🏁 Script executed:

#!/bin/bash
# Look at llm_conditional_router implementation to see how it handles multiple outputs with same method
rg -nP --type=py -A20 'def get_route_output' src/lfx/src/lfx/components/logic/llm_conditional_router.py

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the actual Output model definition to see all fields
rg -nP --type=py -A15 'class Output\(BaseModel\)' src/lfx/src/lfx/template/field/base.py

Length of output: 551


I need to find how the framework actually invokes output methods during execution, especially when multiple outputs share the same method.


🏁 Script executed:

#!/bin/bash
# Search for where output methods are called in execution flow - look for component execution or run methods
rg -nP --type=py 'for output in.*outputs|for.*output.*in' src/lfx/src/lfx/base/tools/component_tool.py | head -20

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Look at component_tool.py where output_method is retrieved - see how it's actually called
rg -nP --type=py -B5 -A10 'output_method.*getattr' src/lfx/src/lfx/base/tools/component_tool.py

Length of output: 787


🏁 Script executed:

#!/bin/bash
# Check how llm_conditional_router routes outputs - find the method that handles routing
rg -nP --type=py -C15 'class LLMConditionalRouter' src/lfx/src/lfx/components/logic/llm_conditional_router.py | head -80

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for how multiple outputs map to a single method in tests
rg -nP --type=py -B5 -A10 'all using the same method' src/lfx/src/lfx/components/logic/llm_conditional_router.py

Length of output: 835


I found a key difference! The llm_conditional_router uses group_outputs=True when creating multiple outputs with the same method. Let me investigate this pattern.


🏁 Script executed:

#!/bin/bash
# Check Output model for group_outputs field
rg -nP --type=py 'group_outputs' src/lfx/src/lfx/template/field/base.py

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Find process_case method in llm_conditional_router to see how it handles routing
rg -nP --type=py -A25 'def process_case' src/lfx/src/lfx/components/logic/llm_conditional_router.py

Length of output: 1209


🏁 Script executed:

#!/bin/bash
# Check if flow_output in run_flow.py uses group_outputs
rg -nP --type=py -B5 -A5 'group_outputs' src/lfx/src/lfx/components/logic/run_flow.py

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get full Output class definition including all fields
rg -nP --type=py -A40 'class Output\(BaseModel\):' src/lfx/src/lfx/template/field/base.py

Length of output: 1526


I found that group_outputs is UI-related. Let me search for the actual execution and routing logic that determines which output gets which result.


🏁 Script executed:

#!/bin/bash
# Look for field mapping or how Output.value gets set during execution
rg -nP --type=py 'output\.value\s*=' src/lfx/src/lfx/custom/custom_component/component.py | head -20

Length of output: 79


🏁 Script executed:

#!/bin/bash
# Search for how output methods are called and results mapped - look for execution flow
rg -nP --type=py -B5 -A15 'def run\(' src/lfx/src/lfx/custom/custom_component/component.py | head -100

Length of output: 639


🏁 Script executed:

#!/bin/bash
# Look for tests of llm_conditional_router to see how it actually routes outputs
fd 'test.*llm_conditional_router|llm_conditional_router.*test' --type f

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check how component results are distributed to outputs in execution
rg -nP --type=py 'fields\[output\.name\]' src/lfx/src/lfx/custom/custom_component/component.py -B5 -A5

Length of output: 532


Let me find the actual _run() implementation to understand how output methods are invoked:


🏁 Script executed:

#!/bin/bash
# Find _run implementation
rg -nP --type=py -A30 'async def _run\(' src/lfx/src/lfx/custom/custom_component/component.py | head -100

Length of output: 1790


🏁 Script executed:

#!/bin/bash
# Look for where the state model is actually executed/called
rg -nP --type=py 'state_model|_state_model' src/lfx/src/lfx/custom/custom_component/component.py | head -30

Length of output: 602


🏁 Script executed:

#!/bin/bash
# Search for how component execution results are returned to outputs
rg -nP --type=py -B10 -A10 'output.value = result' src/lfx/src/lfx/custom/custom_component/component.py

Length of output: 868


🏁 Script executed:

#!/bin/bash
# Check if there's a method that retrieves results from outputs by name
rg -nP --type=py 'def.*output|output.*result' src/lfx/src/lfx/base/tools/run_flow.py | head -20

Length of output: 347


I found the crucial method: resolve_output(output_name). Let me see how it invokes the output methods:


🏁 Script executed:

#!/bin/bash
# Find full resolve_output implementation
rg -nP --type=py -A20 'async def resolve_output' src/lfx/src/lfx/custom/custom_component/component.py

Length of output: 1071


🏁 Script executed:

#!/bin/bash
# Check if output methods receive any context about which output is requesting them
rg -nP --type=py 'def resolve_output' src/lfx/src/lfx/base/tools/component_tool.py -A20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at how other components with multiple outputs handle this - check memory component
rg -nP --type=py -A15 'def retrieve_messages' src/lfx/src/lfx/components/helpers/memory.py | head -40

Length of output: 1579


🏁 Script executed:

#!/bin/bash
# Check the actual Output objects created in run_flow to understand the pattern
rg -nP --type=py -B10 -A10 'for output_info in self.flow_output_types' src/lfx/src/lfx/components/logic/run_flow.py

Length of output: 916


Let me check _get_output_result to see exactly how the method is invoked:


🏁 Script executed:

#!/bin/bash
# Find _get_output_result implementation - this is where method is actually called
rg -nP --type=py -B5 -A25 'def _get_output_result' src/lfx/src/lfx/custom/custom_component/component.py

Length of output: 1492


🏁 Script executed:

#!/bin/bash
# Double-check: does flow_output method receive any parameters?
rg -nP --type=py -B2 -A15 'async def flow_output' src/lfx/src/lfx/base/tools/run_flow.py

Length of output: 774


🏁 Script executed:

#!/bin/bash
# Check if RunFlowBaseComponent (parent class) has different implementation
rg -nP --type=py -B5 -A20 'class RunFlowBaseComponent' src/lfx/src/lfx/base/tools/run_flow.py

Length of output: 794


Perfect! I have now verified the review comment thoroughly. The concern is valid and correctly identifies a real architectural issue.

The core problem:
The flow_output() method has no way to know which output is requesting it. When the framework calls resolve_output() for different outputs:

  1. Both call the same flow_output() method (line 1194 in component.py: result = await method())
  2. The method receives no parameters to indicate which output is being resolved
  3. flow_output() always returns run_outputs[0].outputs[0] (the first output)
  4. Result: All outputs receive the same value (incorrect)

Why this is a problem:
According to the PR objectives, flows can have multiple outputs (e.g., "Chat Output", "Text Output"). The current implementation creates separate Output objects for each, but they all reference method="flow_output" without any routing logic. This is different from properly designed multi-output components like Memory, which use unique method names per output (retrieve_messages_as_text, retrieve_messages_dataframe).

Fix the output routing for multiple flow outputs.

The flow_output() method at lines 64-79 in src/lfx/src/lfx/base/tools/run_flow.py cannot differentiate between multiple outputs, so all outputs return the same value. Either:

  1. Assign unique method names to each dynamically created output in update_outputs() (like the Memory component pattern)
  2. Pass output context as a parameter to flow_output() (requires framework modification)
  3. Implement group_outputs=True with different logic to handle multiple outputs from a single method call

Recommend option 1 for consistency with existing component patterns in the codebase.

🧰 Tools
🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 78-78: Ruff (W293)
src/lfx/src/lfx/base/tools/run_flow.py:78:1: W293 Blank line contains whitespace


[failure] 73-73: Ruff (W293)
src/lfx/src/lfx/base/tools/run_flow.py:73:1: W293 Blank line contains whitespace


[failure] 70-70: Ruff (W293)
src/lfx/src/lfx/base/tools/run_flow.py:70:1: W293 Blank line contains whitespace


[failure] 67-67: Ruff (W293)
src/lfx/src/lfx/base/tools/run_flow.py:67:1: W293 Blank line contains whitespace

🤖 Prompt for AI Agents
In src/lfx/src/lfx/base/tools/run_flow.py around lines 64-79, flow_output()
cannot distinguish which output is being resolved and always returns the first
output; update_outputs() must assign unique method names per dynamically created
output (following the Memory component pattern) so each Output object references
a distinct handler (e.g., flow_output_<name_or_index>), then implement
corresponding handler functions or dispatchers that return that specific
output's value; do not change flow_output() signature—generate per-output method
names in update_outputs() and wire them to return the correct
run_outputs[i].outputs[j] result.

Comment thread src/lfx/src/lfx/base/tools/run_flow.py Outdated
if isinstance(message_result, str):
return Message(text=message_result)
return Message(text=message_result.data["text"])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove trailing whitespace.

Lines 67, 70, 73, and 78 contain trailing whitespace.

Also applies to: 70-70, 73-73, 78-78

🧰 Tools
🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 67-67: Ruff (W293)
src/lfx/src/lfx/base/tools/run_flow.py:67:1: W293 Blank line contains whitespace

🤖 Prompt for AI Agents
In src/lfx/src/lfx/base/tools/run_flow.py around lines 67, 70, 73, and 78, there
are trailing whitespace characters at the ends of those lines; remove the
trailing spaces on each of those lines (or run your editor/IDE
trim-trailing-whitespace action or a formatting tool like black/trim) so the
lines end cleanly with no extra spaces.


inputs = RunFlowBaseComponent.get_base_inputs()
outputs = RunFlowBaseComponent.get_base_outputs()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove trailing whitespace.

Line 22 contains trailing whitespace.

Apply this diff:

-    
+
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
🧰 Tools
🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 22-22: Ruff (W293)
src/lfx/src/lfx/components/logic/run_flow.py:22:1: W293 Blank line contains whitespace

🤖 Prompt for AI Agents
In src/lfx/src/lfx/components/logic/run_flow.py around line 22, there's trailing
whitespace at the end of the line; remove the trailing space(s) so the line ends
cleanly with no extra whitespace and save the file.

"""Dynamically update outputs based on the selected flow."""
if field_name != "flow_name_selected":
return frontend_node

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove trailing whitespace.

Lines 31, 33, and 38 contain trailing whitespace.

Also applies to: 33-33, 38-38

🧰 Tools
🪛 GitHub Check: Ruff Style Check (3.13)

[failure] 31-31: Ruff (W293)
src/lfx/src/lfx/components/logic/run_flow.py:31:1: W293 Blank line contains whitespace

🤖 Prompt for AI Agents
In src/lfx/src/lfx/components/logic/run_flow.py around lines 31, 33, and 38,
there are trailing whitespace characters; remove the trailing spaces at those
exact lines (and any other stray trailing whitespace in the file), save the
file, and re-run the project linter/formatter to ensure no trailing whitespace
remains.

Comment on lines +91 to +101
if not base_classes:
# Fallback: infer from vertex ID or display name
vertex_id = vertex.id or ""
if "ChatOutput" in vertex_id:
base_classes = ["Message"]
elif "TextOutput" in vertex_id:
base_classes = ["Message"]
# Future output types - add here when they exist
else:
# Default fallback
base_classes = ["Message"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fallback logic for base_classes is fragile.

The current fallback only recognizes "ChatOutput" and "TextOutput" by string matching in the vertex ID, defaulting all others to ["Message"]. As new output types are introduced, this logic will need manual updates, creating a maintenance burden.

Consider one of these approaches:

  1. Require explicit base_classes: Log a warning when base_classes is missing and use a more generic fallback like ["Data"] or ["Any"].
  2. Use a registry pattern: Maintain a mapping of vertex types to base_classes.
  3. Document the requirement: Ensure all output components provide base_classes in their node data.

Do you want me to help implement one of these solutions?

@rodrigosnader
Copy link
Copy Markdown
Contributor Author

Closing - no longer needed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant