-
Notifications
You must be signed in to change notification settings - Fork 8.2k
feat(lfx): Write components as plain Python functions with @component decorator #10798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
… class and function components
…ctions and decorators
…ocstring parameter parsing
…ations and edge cases
…and edge case handling
…tion and error handling
…d validate functionality
|
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 WalkthroughThis PR introduces FunctionComponent, a dynamic wrapper that converts Python functions into Langflow components with automatic UI generation from type hints and docstrings. It includes a component decorator, a from_function factory, InputConfig for customizing input widgets, integration with custom component evaluation and validation systems, and automatic wrapping of plain callables in component workflows. Changes
Sequence DiagramsequenceDiagram
participant User as Developer
participant FuncCode as Function Code
participant Validate as validate_code
participant Eval as eval_custom_component_code
participant Wrapper as FunctionComponentWrapper
participant Component as Component Instance
participant Graph as Graph
participant Invoke as invoke_function
User->>FuncCode: Write `@component` decorated function
User->>Validate: Submit code for validation
Validate->>Validate: _detect_code_type via AST
Validate->>Validate: Mark as function-based
Validate-->>User: Return validated result
User->>Eval: eval_custom_component_code
Eval->>Eval: _is_function_code checks AST
Eval->>Eval: _create_function_component_class executes code
Eval->>Wrapper: Create FunctionComponentWrapper subclass
Wrapper->>Wrapper: Introspect function signature & types
Wrapper->>Wrapper: Auto-generate inputs from annotations
Wrapper->>Wrapper: Auto-generate outputs from return type
Eval-->>Component: Return FunctionComponent instance
User->>Graph: Add function component to graph
User->>Graph: Connect inputs and set parameters
Graph->>Invoke: Call invoke_function during execution
Invoke->>Invoke: Gather input values, apply defaults
Invoke->>Invoke: Coerce types (Message→str, dict→Data)
Invoke->>Invoke: Call wrapped function (sync or async)
Invoke-->>Graph: Return normalized results
Graph-->>User: Graph execution complete
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Key areas requiring attention:
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (5 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 |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #10798 +/- ##
==========================================
+ Coverage 32.39% 32.78% +0.39%
==========================================
Files 1368 1369 +1
Lines 63414 63798 +384
Branches 9373 9477 +104
==========================================
+ Hits 20541 20917 +376
+ Misses 41840 41801 -39
- Partials 1033 1080 +47
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (4)
src/lfx/src/lfx/custom/validate.py (1)
103-134: Consider tightening the class detection pattern to avoid false positives.The pattern matching
any(pattern in base.id for pattern in ["Component", "LC"])at line 125 could match unintended classes likeMyLCHelperorComponentFactorythat aren't actual component base classes.Consider using more specific patterns:
- if isinstance(base, ast.Name) and any(pattern in base.id for pattern in ["Component", "LC"]): + # Match exact Component or classes ending with Component/starting with LC + if isinstance(base, ast.Name) and ( + base.id == "Component" + or base.id.endswith("Component") + or base.id.startswith("LC") + ):Note: The same pattern is used in
extract_class_name(line 578), so consider updating both for consistency.src/lfx/src/lfx/custom/eval.py (1)
115-118: Potential silent failure if function execution raises but doesn't propagate.If the function definition exists in the AST but
exec()fails to populatenamespace(e.g., due to an import error within the function body that doesn't raise immediately),funccould beNone. The current error message would be misleading in that case.Consider enhancing the error message to indicate potential execution issues:
func = namespace.get(func_name) if func is None: - msg = f"Function '{func_name}' not found in namespace after execution" + msg = ( + f"Function '{func_name}' not found in namespace after execution. " + f"This may indicate an import or syntax error within the function." + ) raise ValueError(msg)src/lfx/tests/unit/custom/test_eval.py (1)
340-402: Consider movingastimport to module level.The
import aststatement is repeated inside each test method. Sinceastis a standard library module used by multiple tests, moving it to the module-level imports would be cleaner.+import ast + import pytest from lfx.base.functions import FunctionComponent from lfx.custom.eval import (Then remove the
import astlines from within each test method.src/lfx/src/lfx/base/functions/function_component.py (1)
518-540: Decorator stripping may fail with multi-line decorator arguments.The
_strip_decorators_and_dedentmethod handles single-line decorators but may not correctly handle multi-line decorator arguments:@component( display_name="My Component", description="Long description" ) def my_func(): ...In this case, lines between
@component(and)would be incorrectly included infunc_lines.Consider using AST-based decorator stripping instead, which would be more robust:
def _strip_decorators_and_dedent(self, source: str) -> str: """Strip decorator lines and dedent the function source.""" import ast import textwrap try: tree = ast.parse(textwrap.dedent(source)) for node in ast.walk(tree): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): node.decorator_list = [] return ast.unparse(tree) except SyntaxError: # Fallback to line-based stripping # ... existing logic ...Note:
ast.unparse()requires Python 3.9+. Verify compatibility.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
src/lfx/src/lfx/base/functions/__init__.py(1 hunks)src/lfx/src/lfx/base/functions/function_component.py(1 hunks)src/lfx/src/lfx/custom/custom_component/component.py(3 hunks)src/lfx/src/lfx/custom/eval.py(2 hunks)src/lfx/src/lfx/custom/validate.py(2 hunks)src/lfx/tests/unit/base/functions/test_function_component.py(1 hunks)src/lfx/tests/unit/base/functions/test_function_component_integration.py(1 hunks)src/lfx/tests/unit/custom/test_eval.py(1 hunks)src/lfx/tests/unit/custom/test_validate.py(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/{test_*.py,*.test.ts,*.test.tsx}
📄 CodeRabbit inference engine (Custom checks)
Check that test files follow the project's naming conventions (test_*.py for backend, *.test.ts for frontend)
Files:
src/lfx/tests/unit/base/functions/test_function_component.pysrc/lfx/tests/unit/custom/test_validate.pysrc/lfx/tests/unit/base/functions/test_function_component_integration.pysrc/lfx/tests/unit/custom/test_eval.py
**/test_*.py
📄 CodeRabbit inference engine (Custom checks)
**/test_*.py: Backend tests should follow pytest structure with proper test_*.py naming
For async functions, ensure proper async testing patterns are used with pytest for backend
Files:
src/lfx/tests/unit/base/functions/test_function_component.pysrc/lfx/tests/unit/custom/test_validate.pysrc/lfx/tests/unit/base/functions/test_function_component_integration.pysrc/lfx/tests/unit/custom/test_eval.py
🧠 Learnings (14)
📚 Learning: 2025-11-24T19:46:09.104Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/backend_development.mdc:0-0
Timestamp: 2025-11-24T19:46:09.104Z
Learning: Applies to tests/unit/components/**/*.py : Create unit tests in `src/backend/tests/unit/components/` mirroring the component directory structure, using `ComponentTestBaseWithClient` or `ComponentTestBaseWithoutClient` base classes
Applied to files:
src/lfx/tests/unit/base/functions/test_function_component.pysrc/lfx/tests/unit/base/functions/test_function_component_integration.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Test component build config updates by calling `to_frontend_node()` to get the node template, then calling `update_build_config()` to apply configuration changes
Applied to files:
src/lfx/tests/unit/base/functions/test_function_component.pysrc/lfx/tests/unit/base/functions/test_function_component_integration.pysrc/lfx/tests/unit/custom/test_eval.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Inherit from the correct `ComponentTestBase` family class located in `src/backend/tests/base.py` based on API access needs: `ComponentTestBase` (no API), `ComponentTestBaseWithClient` (needs API), or `ComponentTestBaseWithoutClient` (pure logic). Provide three required fixtures: `component_class`, `default_kwargs`, and `file_names_mapping`
Applied to files:
src/lfx/tests/unit/base/functions/test_function_component.pysrc/lfx/tests/unit/base/functions/test_function_component_integration.pysrc/lfx/tests/unit/custom/test_eval.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Test component versioning and backward compatibility using `file_names_mapping` fixture with `VersionComponentMapping` objects mapping component files across Langflow versions
Applied to files:
src/lfx/tests/unit/base/functions/test_function_component.pysrc/lfx/tests/unit/base/functions/test_function_component_integration.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Test both sync and async code paths, mock external dependencies appropriately, test error handling and edge cases, validate input/output behavior, and test component initialization and configuration
Applied to files:
src/lfx/tests/unit/base/functions/test_function_component.pysrc/lfx/tests/unit/custom/test_validate.pysrc/lfx/tests/unit/base/functions/test_function_component_integration.pysrc/lfx/tests/unit/custom/test_eval.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: When adding a new component test, inherit from the correct `ComponentTestBase` class and provide the three required fixtures (`component_class`, `default_kwargs`, `file_names_mapping`) to greatly reduce boilerplate and enforce version compatibility
Applied to files:
src/lfx/tests/unit/base/functions/test_function_component.py
📚 Learning: 2025-08-05T22:51:27.961Z
Learnt from: edwinjosechittilappilly
Repo: langflow-ai/langflow PR: 0
File: :0-0
Timestamp: 2025-08-05T22:51:27.961Z
Learning: The TestComposioComponentAuth test in src/backend/tests/unit/components/bundles/composio/test_base_composio.py demonstrates proper integration testing patterns for external API components, including real API calls with mocking for OAuth completion, comprehensive resource cleanup, and proper environment variable handling with pytest.skip() fallbacks.
Applied to files:
src/lfx/tests/unit/base/functions/test_function_component.pysrc/lfx/tests/unit/base/functions/test_function_component_integration.pysrc/lfx/tests/unit/custom/test_eval.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Create comprehensive unit tests for all new backend components. If unit tests are incomplete, create a corresponding Markdown file documenting manual testing steps and expected outcomes
Applied to files:
src/lfx/tests/unit/base/functions/test_function_component.pysrc/lfx/tests/unit/custom/test_validate.pysrc/lfx/tests/unit/base/functions/test_function_component_integration.pysrc/lfx/tests/unit/custom/test_eval.py
📚 Learning: 2025-11-24T19:47:40.400Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2025-11-24T19:47:40.400Z
Learning: Applies to **/*.{test.ts,test.tsx,spec.ts,spec.tsx,test_*.py} : Tests should cover the main functionality being implemented
Applied to files:
src/lfx/tests/unit/base/functions/test_function_component.py
📚 Learning: 2025-11-24T19:46:09.104Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/backend_development.mdc:0-0
Timestamp: 2025-11-24T19:46:09.104Z
Learning: Applies to src/backend/base/langflow/components/**/__init__.py : Update `__init__.py` with alphabetically sorted imports when adding new components
Applied to files:
src/lfx/src/lfx/base/functions/__init__.pysrc/lfx/src/lfx/base/functions/function_component.py
📚 Learning: 2025-11-24T19:46:09.104Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/backend_development.mdc:0-0
Timestamp: 2025-11-24T19:46:09.104Z
Learning: Applies to src/backend/base/langflow/components/**/*.py : Add new components to the appropriate subdirectory under `src/backend/base/langflow/components/` (agents/, data/, embeddings/, input_output/, models/, processing/, prompts/, tools/, or vectorstores/)
Applied to files:
src/lfx/src/lfx/base/functions/function_component.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Be aware of ContextVar propagation in async tests; test both direct event loop execution and `asyncio.to_thread` scenarios; ensure proper context isolation between test cases
Applied to files:
src/lfx/tests/unit/base/functions/test_function_component_integration.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Use `pytest.mark.asyncio` decorator for async component tests and ensure async methods are properly awaited
Applied to files:
src/lfx/tests/unit/base/functions/test_function_component_integration.pysrc/lfx/tests/unit/custom/test_eval.py
📚 Learning: 2025-11-24T19:47:28.997Z
Learnt from: CR
Repo: langflow-ai/langflow PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-24T19:47:28.997Z
Learning: Applies to src/backend/tests/**/*.py : Use same filename as component with appropriate test prefix/suffix (e.g., `my_component.py` → `test_my_component.py`)
Applied to files:
src/lfx/tests/unit/base/functions/test_function_component_integration.py
🧬 Code graph analysis (5)
src/lfx/tests/unit/base/functions/test_function_component.py (7)
src/lfx/src/lfx/base/functions/function_component.py (3)
FunctionComponent(96-540)InputConfig(71-93)invoke_function(446-494)src/lfx/src/lfx/schema/data.py (1)
Data(26-288)src/lfx/src/lfx/schema/message.py (1)
Message(34-299)src/lfx/src/lfx/base/tools/flow_tool.py (1)
args(32-34)src/lfx/src/lfx/template/field/base.py (1)
Output(181-260)src/lfx/src/lfx/components/input_output/chat_output.py (1)
ChatOutput(22-184)src/lfx/src/lfx/custom/custom_component/component.py (1)
to_frontend_node(1021-1073)
src/lfx/tests/unit/custom/test_validate.py (1)
src/lfx/src/lfx/custom/validate.py (2)
_detect_code_type(103-134)validate_code(31-100)
src/lfx/src/lfx/custom/custom_component/component.py (1)
src/lfx/src/lfx/base/functions/function_component.py (1)
FunctionComponent(96-540)
src/lfx/src/lfx/base/functions/__init__.py (1)
src/lfx/src/lfx/base/functions/function_component.py (6)
FunctionComponent(96-540)InputConfig(71-93)component(565-565)component(569-574)component(577-629)from_function(543-561)
src/lfx/tests/unit/custom/test_eval.py (2)
src/lfx/src/lfx/base/functions/function_component.py (2)
FunctionComponent(96-540)invoke_function(446-494)src/lfx/src/lfx/custom/eval.py (4)
_create_function_component_class(58-127)_has_component_decorator(130-146)_is_function_code(25-55)eval_custom_component_code(11-22)
⏰ 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). (2)
- GitHub Check: Update Starter Projects
- GitHub Check: Update Component Index
🔇 Additional comments (33)
src/lfx/tests/unit/custom/test_validate.py (1)
1-210: LGTM! Comprehensive test coverage for code validation.The test file follows pytest conventions with proper
test_*.pynaming. It provides thorough coverage of:
- Function and async function detection
- Class-based component detection (including LC-prefixed classes)
- Priority logic (class over function when both exist)
- Edge cases (empty code, syntax errors, import errors)
- The internal
_detect_code_typehelperTests are synchronous, which is appropriate since the
validate_codefunction is synchronous. Based on learnings, this aligns with the guideline to "test error handling and edge cases, validate input/output behavior."src/lfx/src/lfx/custom/custom_component/component.py (4)
50-50: LGTM! Correct use of TYPE_CHECKING for conditional import.The
FunctionComponentimport underTYPE_CHECKINGavoids circular dependency at runtime while enabling proper type hints.
684-693: LGTM! Correct identification of plain callables.The method correctly filters to only plain Python functions and coroutine functions, excluding:
- Non-callables
- Component bound methods
- Built-in functions, classes, and other callable types
Lambdas will pass
inspect.isfunction(), which is appropriate behavior.
695-699: LGTM! Clean factory method with local import.The local import of
FunctionComponentinside the method correctly avoids circular import issues at module load time.
792-796: LGTM! Auto-wrapping of plain functions enables cleaner API.The auto-wrapping logic correctly:
- Detects plain callables (not already Component methods)
- Wraps them in
FunctionComponent- Connects via
fc.resultwhich returns the boundinvoke_functionmethodThis enables users to pass plain functions directly to
Component.set()without manually wrapping them.src/lfx/src/lfx/base/functions/__init__.py (1)
1-15: LGTM! Clean public API surface.The module correctly exposes the public API with:
- Explicit imports from the implementation module
__all__declaration for clear public interface- Alphabetically sorted exports (as per coding guidelines)
This provides a clean entry point for users:
from lfx.base.functions import FunctionComponent, componentsrc/lfx/tests/unit/base/functions/test_function_component.py (8)
1-12: LGTM! Well-organized test imports and structure.The test file follows pytest conventions with proper imports from the module under test. The use of
warningsmodule for testing warning behavior is appropriate.
14-136: LGTM! Thorough signature introspection tests.Excellent coverage of FunctionComponent creation including:
- Single/multiple parameters
- Default values
- Type hints (list, Literal, Optional)
- Warning for untyped parameters
- Skipping special parameters (self, cls, *args, **kwargs)
The use of
warnings.catch_warnings()for testing warning behavior is the correct pattern.
139-173: LGTM! Docstring parsing tests are appropriate.Tests cover Google-style docstring parsing and the no-docstring case. The assertion on
fc._descriptioncorrectly accounts for the underscore-prefixed attribute used by the Component base class.
176-342: LGTM! Comprehensive metadata and decorator tests.Good coverage of:
- Output type mapping (str→Message, dict→Data, no return type)
- Display name derivation from function name
- ID generation (auto-generated and custom)
- Factory function
from_function()- Decorator usage with and without parameters
InputConfigcustomization viaAnnotatedtypes
345-474: LGTM! Execution and serialization tests are well-structured.Proper use of
pytest.mark.asynciofor async tests. Good coverage of:
- Type coercion (Message→str input, dict→Data output)
- Sync and async function execution
- Exception propagation
- The
resultproperty for graph chaining- Source code capture for persistence
Based on learnings, this follows the guideline to "test both sync and async code paths."
477-740: LGTM! Auto-wrapping and connection tests are thorough.Good coverage of:
- Auto-wrapping plain functions via
Component.set()- Connecting FunctionComponents to ChatInput/ChatOutput
- Edge data verification (sourceHandle, targetHandle, types)
- Edge cases: None return, Union types, lambdas, generators, complex nested types
The tests correctly verify the new auto-wrapping behavior introduced in
component.py.
742-1014: LGTM! Serialization and type validation tests are comprehensive.Excellent coverage of:
- Frontend node serialization
- Edge serialization correctness
- Type mismatch detection (dict→str, int→str, list→single, Data→Message)
- Valid connection verification (str→str via Message type)
The tests verify that
Graphcorrectly rejects invalid type connections withValueError.
1017-1144: LGTM! Name attribute and simple docstring format tests.Good coverage of:
nameattribute consistency with_display_name- Custom display name propagation to
name- Simple docstring format ("param: description") without Args: section
- Priority of Args: section over simple format when both exist
The simple docstring format tests ensure backward compatibility with various documentation styles.
src/lfx/tests/unit/base/functions/test_function_component_integration.py (7)
1-19: LGTM! Clear test file organization and imports.The docstring clearly documents the test scope (graph execution, serialization, deserialization). Imports are minimal and appropriate.
21-159: LGTM! Graph execution tests are comprehensive.Good coverage of:
- Simple and decorated functions in graphs
- Chained FunctionComponents
- Multiple parameters with defaults
- Async functions
All tests correctly use
pytest.mark.asyncioand properly await async operations.
162-240: LGTM! Serialization tests verify structure correctness.Tests properly verify:
graph.dump()includes nodes and edgesgraph.dumps()produces valid parseable JSON- Edge references point to valid node IDs
243-386: LGTM! Deserialization and round-trip tests are thorough.Good coverage of:
- Simple function round-trip
- Explicit ID handling (with note about 'chat' prefix requirement)
- Chained function round-trip
- Structural verification (vertex/edge counts)
The comment about ChatInput IDs needing 'chat' prefix (lines 289-291, 297-298) is helpful documentation for test maintainability.
389-455: LGTM! Type handling tests cover common scenarios.Tests verify correct handling of:
- Integer parameters
- Boolean parameters
- Dict return values wrapped as Data
457-736: LGTM! Function-only pipeline tests are valuable.Excellent coverage of pipelines without ChatInput/ChatOutput:
- Various chain lengths (2, 3, 5 components)
- Mixed sync/async components
- Default parameter handling
- Round-trip serialization
- Graph structure verification (vertex IDs, edge pairs)
This demonstrates FunctionComponent can be used in pure function pipelines, not just chat-based workflows.
739-832: LGTM! Edge case tests ensure robustness.Good coverage of:
- Empty string handling
- Special characters (newlines, tabs, quotes, HTML entities)
- Unicode (emoji, Chinese, Russian characters)
- Multiple serialization round-trips (3x) without degradation
These tests ensure the implementation handles real-world input gracefully.
src/lfx/src/lfx/custom/validate.py (2)
31-53: Well-structured result dictionary for code validation.The refactored
validate_codefunction now returns a comprehensive result dictionary that supports both function-based and class-based component detection. The structure is clear and extensible.
89-89: Theisinstance()union type syntax is compatible with the project's minimum Python version.The project specifies
requires-python = ">=3.10,<3.14"in pyproject.toml. Theast.FunctionDef | ast.AsyncFunctionDefsyntax inisinstance()is supported in Python 3.10+, so no compatibility issue exists.src/lfx/src/lfx/custom/eval.py (2)
71-73: Security consideration: exec() on user-provided code.The
exec()call executes arbitrary code from the input string. While this appears intentional for custom component evaluation, ensure this code path is only accessible to authenticated users with appropriate permissions, and never exposed to untrusted input.The
# noqa: S102comment indicates awareness of this security implication. Please confirm that:
- This function is only called with code from trusted sources (e.g., authenticated user submissions)
- There are appropriate access controls at the API layer
130-145: Good coverage of decorator detection patterns.The
_has_component_decoratorfunction handles various decorator forms well:
@component@component(...)@lfx.base.functions.component@lfx.base.functions.component(...)src/lfx/tests/unit/custom/test_eval.py (3)
1-13: Comprehensive test coverage for function-based component evaluation.The test file follows pytest structure with proper
test_*.pynaming. Good organization with test classes grouped by functionality. As per coding guidelines, backend tests follow pytest structure correctly.
266-290: Proper async testing pattern used.The async tests correctly use
@pytest.mark.asynciodecorator as required for async component tests. Based on learnings, async functions should use proper async testing patterns with pytest.
293-334: Good round-trip serialization tests.These tests verify that
FunctionComponentinstances can be serialized viaset_class_code()and recreated througheval_custom_component_code. This is essential for persistence and UI integration.src/lfx/src/lfx/base/functions/function_component.py (5)
49-67: Well-defined type mappings for input/output conversion.The
TYPE_TO_INPUT_CLASSandTYPE_TO_OUTPUT_TYPEdictionaries provide clear mappings between Python types and Langflow UI components. This is a clean approach for type inference.
70-93: InputConfig dataclass provides flexible input customization.Good use of dataclass with sensible defaults. The
required: bool | None = Noneallowing inference from defaults is a nice touch.
564-628: Clean decorator implementation with proper overloads.The
@componentdecorator is well-implemented with:
- Proper
@overloadsignatures for type checking- Support for both
@componentand@component(...)syntax- Source code capture at decoration time
- Metadata preservation via
functools.update_wrapper
446-494: Robust invoke_function with proper async handling and type coercion.The method correctly:
- Handles async functions via
inspect.iscoroutine- Coerces
Message→strwhen expected- Coerces
dict→Datain results- Falls back to signature defaults when values are empty
460-460: Potential KeyError when accessingself._inputs[param_name].If
param_nameis a valid signature parameter but not present inself._inputs(e.g., due to a filtering condition during input building), this will raise aKeyError.Consider using
.get()with a fallback:- value = self._inputs[param_name].value if param_name in self._inputs else getattr(self, param_name, None) + input_obj = self._inputs.get(param_name) + value = input_obj.value if input_obj else getattr(self, param_name, None)This is safer and more idiomatic Python.
Likely an incorrect or invalid review comment.
| if origin in (list, List if "List" in dir() else list): # noqa: F821 | ||
| is_list = True | ||
| args = get_args(param_type) | ||
| param_type = args[0] if args else str |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dead code: List check will always evaluate to list.
The check List if "List" in dir() else list will always result in list because List is not imported in this module's scope. The same issue exists on line 436.
- if origin in (list, List if "List" in dir() else list): # noqa: F821
+ if origin is list:
is_list = TrueAnd similarly for line 436:
- if origin in (list, List if "List" in dir() else list): # noqa: F821
+ if origin is list:
return ["list"]Note: If you need to support typing.List annotations from older code, import it and check both:
from typing import List
# ...
if origin in (list, List):🤖 Prompt for AI Agents
In src/lfx/src/lfx/base/functions/function_component.py around lines 326-329
(and likewise update line 436), the conditional uses `List if "List" in dir()
else list` which always evaluates to `list`; import `List` from `typing` at the
top of the file and replace the conditional with a clear check that tests both
built-in list and typing.List (e.g., `if origin in (list, List):`) at both
locations so annotations using `typing.List` are correctly recognized.
- Add comprehensive module-level docstring with quick start guide - Document InputConfig attributes and both usage patterns (default value + Annotated) - Add examples for chaining, async functions, supported types - Implement InputConfig.default for cleaner default value syntax - Add password support using SecretStrInput - Add tests for InputConfig as default value syntax
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a powerful new feature that allows developers to create Langflow components from plain Python functions using a @component decorator, eliminating the need for class-based boilerplate. The implementation automatically converts function signatures to component inputs/outputs, supports async functions, and maintains full serialization/deserialization capabilities.
Key changes:
- New
FunctionComponentclass that wraps Python functions as Langflow components @componentdecorator for simple function-to-component transformation- Auto-detection of function vs class code in custom component evaluation
- Support for
InputConfigto customize input fields with type-safe annotations
Reviewed changes
Copilot reviewed 13 out of 15 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
src/lfx/src/lfx/base/functions/function_component.py |
Core implementation of FunctionComponent class with signature parsing, input/output generation, and execution logic |
src/lfx/src/lfx/base/functions/__init__.py |
Module exports for FunctionComponent, InputConfig, component decorator, and from_function factory |
src/lfx/src/lfx/custom/eval.py |
Extended eval logic to detect and handle function-based components alongside class-based components |
src/lfx/src/lfx/custom/validate.py |
Added code type detection (function vs class) to validation with new _detect_code_type helper |
src/lfx/src/lfx/custom/custom_component/component.py |
Added auto-wrapping of plain callables as FunctionComponents when using Component.set() |
src/lfx/tests/unit/base/functions/test_function_component.py |
Comprehensive unit tests covering FunctionComponent creation, configuration, and edge cases (1227 lines) |
src/lfx/tests/unit/base/functions/test_function_component_integration.py |
Integration tests for graph execution, serialization, and round-trip scenarios (832 lines) |
src/lfx/tests/unit/custom/test_eval.py |
Tests for custom component code evaluation with function/class detection (402 lines) |
src/lfx/tests/unit/custom/test_validate.py |
Tests for code validation and type detection logic (210 lines) |
Comments suppressed due to low confidence (1)
src/lfx/src/lfx/custom/custom_component/component.py:181
- This call to Component.set_class_code in an initialization method is overridden by FunctionComponent.set_class_code.
self.set_class_code()
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.
| # Get value from inputs first, fall back to attributes | ||
| # Note: We must use _inputs directly because getattr may return | ||
| # component attributes (like 'name' for display_name) instead of input values | ||
| value = self._inputs[param_name].value if param_name in self._inputs else getattr(self, param_name, None) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Potential performance issue: self._inputs[param_name].value is accessed for every parameter, but then falls back to getattr(self, param_name, None) which could be slow if there are many parameters. Consider caching the input values or restructuring to avoid repeated lookups.
The comment mentions avoiding getattr for components attributes, but then uses it anyway as a fallback.
| origin = get_origin(param_type) | ||
|
|
||
| # Check for list type | ||
| if origin in (list, List if "List" in dir() else list): # noqa: F821 |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition if origin in (list, List if "List" in dir() else list) has a problematic expression. Using "List" in dir() checks the local namespace which is unreliable. This should import List from typing at the top of the file and use a proper check.
Consider: if origin in (list, typing.List) after adding import typing or from typing import List
| if origin in (list, List if "List" in dir() else list): # noqa: F821 | |
| if origin in (list, List): |
| types.extend(self._get_output_types(arg)) | ||
| return types | ||
|
|
||
| if origin in (list, List if "List" in dir() else list): # noqa: F821 |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition if origin in (list, List if "List" in dir() else list) has a problematic expression. Using "List" in dir() checks the local namespace which is unreliable. This should import List from typing at the top of the file and use a proper check.
Consider: if origin in (list, typing.List) after adding import typing or from typing import List
| value = self._inputs[param_name].value if param_name in self._inputs else getattr(self, param_name, None) | ||
|
|
||
| # If value is None or empty string, use the default from function signature | ||
| if (value is None or value == "") and param.default is not inspect.Parameter.empty: |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition checks (value is None or value == "") which will treat empty strings as None for all parameter types. This could be problematic for optional string parameters where an empty string is a valid value distinct from None.
Consider checking the parameter type before treating empty string as None, or document this behavior explicitly.
| if isinstance(raw_default, InputConfig): | ||
| # InputConfig used as default value: `param: str = InputConfig(default="value")` | ||
| input_config_from_default = raw_default | ||
| default = raw_default.default |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing input validation for the default parameter in InputConfig. When default is set via InputConfig, if it doesn't match the parameter type, it could cause runtime errors during execution.
Consider adding type validation to ensure the default value is compatible with the parameter type.
| default = raw_default.default | |
| default = raw_default.default | |
| # Type validation for default value | |
| if default is not None: | |
| origin = get_origin(param_type) | |
| args = get_args(param_type) | |
| expected_type = param_type | |
| # Handle Optional[X] or Union[X, None] | |
| if origin is Union and type(None) in args: | |
| # Remove NoneType from args | |
| non_none_args = tuple(a for a in args if a is not type(None)) | |
| if len(non_none_args) == 1: | |
| expected_type = non_none_args[0] | |
| else: | |
| expected_type = Union[non_none_args] | |
| # For generic types, get the origin | |
| elif origin is not None: | |
| expected_type = origin | |
| # Now check type | |
| if not isinstance(default, expected_type): | |
| try: | |
| # Try to coerce | |
| default = expected_type(default) | |
| except Exception: | |
| warnings.warn( | |
| f"Default value '{default}' for parameter '{param_name}' in function '{self._func_name}' " | |
| f"does not match expected type '{expected_type.__name__}'.", | |
| stacklevel=3, | |
| ) |
|
|
||
| for node in tree.body: | ||
| # Check for function definitions | ||
| if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef): |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) uses a union type with isinstance(), but the second argument to isinstance() should be a tuple, not a union. This will raise a TypeError at runtime.
Change to: isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
| if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef): | |
| if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): |
|
|
||
| # Execute code to get the functions | ||
| namespace: dict = {} | ||
| exec(dedented_code, namespace) # noqa: S102 |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code uses exec() on user-provided code string without any restrictions. This is a critical security vulnerability that could allow arbitrary code execution. Consider using a more restricted execution environment or at minimum add explicit warnings about the security implications.
If this is intended for trusted code only, document this requirement clearly.
| in_decorator = False | ||
|
|
||
| for line in lines: | ||
| stripped = line.strip() | ||
| # Skip decorator lines (including multi-line decorators) | ||
| if stripped.startswith("@"): | ||
| in_decorator = True | ||
| continue | ||
| # Once we hit def/async def, we're past decorators | ||
| if in_decorator and stripped.startswith(("def ", "async def ")): | ||
| in_decorator = False | ||
| # Include non-decorator lines | ||
| if not in_decorator or stripped.startswith(("def ", "async def ")): | ||
| func_lines.append(line) | ||
| in_decorator = False |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The _strip_decorators_and_dedent method has a logic issue. Once in_decorator is set to False when encountering def/async def, it should not be reset again. The current logic may skip lines incorrectly.
The condition if not in_decorator or stripped.startswith(("def ", "async def ")) should be: if not in_decorator. The second part of the OR is redundant since in_decorator is already False at that point.
| in_decorator = False | |
| for line in lines: | |
| stripped = line.strip() | |
| # Skip decorator lines (including multi-line decorators) | |
| if stripped.startswith("@"): | |
| in_decorator = True | |
| continue | |
| # Once we hit def/async def, we're past decorators | |
| if in_decorator and stripped.startswith(("def ", "async def ")): | |
| in_decorator = False | |
| # Include non-decorator lines | |
| if not in_decorator or stripped.startswith(("def ", "async def ")): | |
| func_lines.append(line) | |
| in_decorator = False | |
| in_decorator = True | |
| for line in lines: | |
| stripped = line.strip() | |
| # Skip decorator lines (including multi-line decorators) | |
| if in_decorator: | |
| if stripped.startswith("@"): | |
| continue | |
| if stripped.startswith(("def ", "async def ")): | |
| in_decorator = False | |
| func_lines.append(line) | |
| # else: still in decorator section, skip line | |
| else: | |
| func_lines.append(line) |
| # Check for Component class definitions | ||
| elif isinstance(node, ast.ClassDef): | ||
| for base in node.bases: | ||
| if isinstance(base, ast.Name) and any(pattern in base.id for pattern in ["Component", "LC"]): |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern check any(pattern in base.id for pattern in ["Component", "LC"]) is overly broad and could match unintended classes. For example, a class named MyLCDisplay or ComponentHelper that doesn't inherit from the Langflow Component class would incorrectly match.
Consider using more specific checks like base.id.endswith("Component") or maintain a list of known base class names.
| if isinstance(base, ast.Name) and any(pattern in base.id for pattern in ["Component", "LC"]): | |
| if isinstance(base, ast.Name) and (base.id.endswith("Component") or base.id == "LC"): |
| has_function = True | ||
| elif isinstance(node, ast.ClassDef): | ||
| for base in node.bases: | ||
| if isinstance(base, ast.Name) and any(pattern in base.id for pattern in ["Component", "LC"]): |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern check any(pattern in base.id for pattern in ["Component", "LC"]) is overly broad and could match unintended classes. For example, a class named MyLCDisplay or ComponentHelper that doesn't inherit from the Langflow Component class would incorrectly match.
Consider using more specific checks like base.id.endswith("Component") or maintain a list of known base class names.
| if isinstance(base, ast.Name) and any(pattern in base.id for pattern in ["Component", "LC"]): | |
| if isinstance(base, ast.Name) and (base.id == "Component" or base.id == "LC" or base.id.endswith("Component")): |
What if building a Langflow component was as simple as writing a Python function?
Now it is.
That's it. No classes to inherit. No boilerplate. Just a function that becomes a fully-functional Langflow component with typed inputs, outputs, and graph connectivity.
The Problem
Creating custom components in Langflow requires understanding the
Componentclass hierarchy, defininginputsandoutputslists, and following specific patterns. For simple transformations or utilities, this overhead can feel excessive.The Solution
FunctionComponentautomatically transforms Python functions into Langflow components:name: strcreates a string input-> strcreates a string outputcount: int = 5creates an input with defaultasync defworks seamlesslyCustomize When Needed
Chain Components Together
Works in the UI Too
Paste function code directly in Custom Component - it just works. The system automatically detects whether you're writing a class-based component or a function-based one.
Key Features
.resultproperty for chainingFiles Changed
src/lfx/src/lfx/base/functions/- NewFunctionComponentand@componentdecoratorsrc/lfx/src/lfx/custom/eval.py- Function detection and wrapper generationsrc/lfx/src/lfx/custom/validate.py- Code type detectionsrc/lfx/tests/unit/- 140 tests covering all functionalityTest Plan
Unit tests for
FunctionComponentclass covering signature parsing, input generation, and execution. Integration tests for graph execution with function components. Tests for@componentdecorator with various configurations. Tests foreval_custom_component_codefunction/class detection. Tests for multi-function files with decorator selection. Tests for serialization round-trips. Edge case tests for async, no params, complex types, etc.Summary by CodeRabbit
Release Notes
@componentdecorator.✏️ Tip: You can customize this high-level summary in your review settings.