feat: make RunFlow component outputs dynamic#10327
Conversation
- 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
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughRefactors flow output handling by consolidating multiple output methods into a single Changes
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
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
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 error, 3 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
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. Comment |
|
✅ Component index has been automatically updated due to changes in New Index: |
|
There was a problem hiding this comment.
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_outputmethod.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_typesis always initialized in__init__, thehasattrcheck 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
⛔ Files ignored due to path filters (1)
uv.lockis 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_typesafter 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_outputsto an empty list with a clear comment about dynamic population is the right approach for this refactoring.
| 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 |
There was a problem hiding this comment.
🧩 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:
- How does the framework route to the correct output when multiple outputs exist?
- Should
flow_outputaccept parameters to identify which specific output to return? - 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 -50Length of output: 951
🏁 Script executed:
#!/bin/bash
# Search for Output class definition and instantiation
rg -nP --type=py 'class Output|Output\(' | head -50Length 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 -100Length 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 -100Length 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 -50Length 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 -30Length 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.pyLength 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.pyLength 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 -20Length 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.pyLength 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 -80Length 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.pyLength 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.pyLength 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.pyLength 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.pyLength 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.pyLength 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 -20Length 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 -100Length 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 fLength 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 -A5Length 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 -100Length 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 -30Length 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.pyLength 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 -20Length 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.pyLength 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 -A20Length 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 -40Length 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.pyLength 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.pyLength 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.pyLength 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.pyLength 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:
- Both call the same
flow_output()method (line 1194 in component.py:result = await method()) - The method receives no parameters to indicate which output is being resolved
flow_output()always returnsrun_outputs[0].outputs[0](the first output)- 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:
- Assign unique method names to each dynamically created output in
update_outputs()(like the Memory component pattern) - Pass output context as a parameter to
flow_output()(requires framework modification) - Implement
group_outputs=Truewith 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.
| if isinstance(message_result, str): | ||
| return Message(text=message_result) | ||
| return Message(text=message_result.data["text"]) | ||
|
|
There was a problem hiding this comment.
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() | ||
|
|
There was a problem hiding this comment.
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.
🧰 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 | ||
|
|
There was a problem hiding this comment.
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.
| 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"] |
There was a problem hiding this comment.
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:
- Require explicit base_classes: Log a warning when base_classes is missing and use a more generic fallback like
["Data"]or["Any"]. - Use a registry pattern: Maintain a mapping of vertex types to base_classes.
- 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?
|
Closing - no longer needed |



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