-
Notifications
You must be signed in to change notification settings - Fork 8.2k
feat: make it possible to check for outdated components in JSON flows #9978
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
… function - Removed the call to analyze_component_dependencies from the build_component_metadata function to improve load times. - Added a TODO comment indicating the need to relocate this functionality to a more appropriate location in the codebase. These changes enhance performance while maintaining the existing functionality for future implementation.
- Introduced a new CLI command to detect and update outdated components in JSON flows. - Implemented functionality to load specific components based on flow metadata, check for outdated components using code hash comparison, and handle updates interactively or automatically. - Enhanced logging for better debugging and user feedback during the update process. This addition improves the usability of the framework by allowing users to maintain up-to-date components in their flows efficiently.
- Added an asynchronous function to check for outdated components in JSON flows before execution. - Introduced a new CLI option to enable this check, enhancing user experience by ensuring components are up to date. - Improved error handling to provide detailed feedback on outdated components found during the check. This addition promotes better maintenance of flow components and prevents potential runtime issues.
- Introduced a new command in the Langflow CLI to check for outdated components in flows, enhancing user experience by ensuring components are current. - Updated the CLI structure to include the check command alongside existing serve and run commands. - Improved documentation for the new command to facilitate user understanding and usage. This addition promotes better maintenance of flow components and aligns with recent enhancements for component checks.
|
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 WalkthroughAdds a new LFX CLI “check” command to inspect/update outdated components in flow JSONs and integrates a pre-run --check validation in the LFX run command. Langflow’s lfx sub-app now exposes the “check” command and defaults to help with no args. Starter project JSONs remove dependency metadata. Internal utils stop writing dependency metadata. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User
participant CLI as lfx CLI
participant CK as check module
participant ST as Settings/Components
participant FS as Flow JSON
U->>CLI: lfx check <flow.json> [--update|--force|-i|--output|--verbose]
CLI->>CK: check_command_sync(...)
CK->>FS: Load flow JSON
CK->>ST: Load specific/all component types
CK->>CK: Compare nodes vs latest templates<br/>• code hash/strings<br/>• inputs/outputs<br/>• diffs
alt updates requested
CK->>FS: Apply updates (auto/interactive)
CK-->>U: Report outdated and updates, write output file
else no updates
CK-->>U: Report findings (counts, diffs)
end
sequenceDiagram
autonumber
participant U as User
participant CLI as lfx run
participant CK as check module
participant EX as Executor
U->>CLI: lfx run <flow.json> --check
CLI->>CK: check_components_before_run(...)
alt any outdated
CK-->>CLI: Outdated list
CLI-->>U: Error with per-component details
else none outdated
CK-->>CLI: OK
CLI->>EX: Execute flow
EX-->>U: Run result
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches✅ Passed checks (3 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 #9978 +/- ##
==========================================
+ Coverage 31.62% 32.04% +0.41%
==========================================
Files 1350 1351 +1
Lines 61154 61632 +478
Branches 9142 9265 +123
==========================================
+ Hits 19341 19747 +406
- Misses 40897 40948 +51
- Partials 916 937 +21
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: 6
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/custom/utils.py (1)
599-605: Always set metadata (module, code_hash) — call build_component_metadata unconditionallyThe pre-run/CLI check relies on metadata.module and metadata.code_hash. Here, metadata is only built when module_name is passed, which can lead to missing hashes/modules for other paths. Build metadata regardless; build_component_metadata already handles module_name=None.
- if module_name: - frontend_node = build_component_metadata( - frontend_node, custom_component, module_name, custom_component.__class__.__name__ - ) + # Always build metadata. build_component_metadata handles module_name=None internally. + frontend_node = build_component_metadata( + frontend_node, custom_component, module_name, custom_component.__class__.__name__ + )
🧹 Nitpick comments (2)
src/lfx/src/lfx/cli/check.py (1)
447-455: Wrong “header skip” in diff parsing (uses MAX_CONTEXT_BLOCKS)MAX_CONTEXT_BLOCKS controls display, not the number of diff header lines. unified_diff produces 2 header lines; skip exactly 2 to avoid dropping content.
- for line in diff_lines[MAX_CONTEXT_BLOCKS:]: # Skip the file headers + for line in diff_lines[2:]: # Skip the '---' and '+++' headerssrc/lfx/src/lfx/cli/run.py (1)
42-45: Docstring “Raises” is inaccurate (ValueError, not typer.Exit)The function raises ValueError on failures; update the docstring accordingly.
- Raises: - typer.Exit: If outdated components are found or check fails + Raises: + ValueError: If outdated components are found or the check fails
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (38)
src/backend/base/langflow/__main__.py(1 hunks)src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Knowledge Ingestion.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Market Research.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Pokédex Agent.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Search agent.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json(0 hunks)src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json(0 hunks)src/lfx/src/lfx/__main__.py(2 hunks)src/lfx/src/lfx/cli/check.py(1 hunks)src/lfx/src/lfx/cli/run.py(4 hunks)src/lfx/src/lfx/custom/utils.py(2 hunks)
💤 Files with no reviewable changes (33)
- src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json
- src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json
- src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json
- src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json
- src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json
- src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json
- src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json
- src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json
- src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json
- src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json
- src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json
- src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json
- src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json
- src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json
- src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json
- src/backend/base/langflow/initial_setup/starter_projects/Knowledge Ingestion.json
- src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json
- src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json
- src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json
- src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json
- src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json
- src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json
- src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json
- src/backend/base/langflow/initial_setup/starter_projects/Search agent.json
- src/backend/base/langflow/initial_setup/starter_projects/Pokédex Agent.json
- src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json
- src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json
- src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json
- src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json
- src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json
- src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json
- src/backend/base/langflow/initial_setup/starter_projects/Market Research.json
- src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json
🧰 Additional context used
📓 Path-based instructions (1)
{src/backend/**/*.py,tests/**/*.py,Makefile}
📄 CodeRabbit inference engine (.cursor/rules/backend_development.mdc)
{src/backend/**/*.py,tests/**/*.py,Makefile}: Run make format_backend to format Python code before linting or committing changes
Run make lint to perform linting checks on backend Python code
Files:
src/backend/base/langflow/__main__.py
🧬 Code graph analysis (5)
src/lfx/src/lfx/cli/run.py (1)
src/lfx/src/lfx/cli/check.py (1)
check_flow_components(145-236)
src/lfx/src/lfx/__main__.py (1)
src/lfx/src/lfx/cli/check.py (1)
check_command_sync(739-756)
src/lfx/src/lfx/custom/utils.py (1)
src/backend/base/langflow/api/v1/endpoints.py (1)
custom_component(666-684)
src/backend/base/langflow/__main__.py (1)
src/lfx/src/lfx/cli/check.py (1)
check_command_sync(739-756)
src/lfx/src/lfx/cli/check.py (2)
src/backend/base/langflow/services/deps.py (1)
get_settings_service(111-124)src/lfx/src/lfx/custom/utils.py (1)
create_component_template(617-641)
⏰ 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). (50)
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 36/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 40/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 39/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 37/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 35/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 38/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 34/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 14/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 31/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 33/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 30/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 29/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 32/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 27/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 23/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 28/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 24/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 19/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 26/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 20/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 25/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 22/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 16/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 21/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 12/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 17/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 18/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 13/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 15/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 7/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 6/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 10/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 11/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 8/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 9/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 5/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 3/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 4/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 2/40
- GitHub Check: Run Frontend Tests / Playwright Tests - Shard 1/40
- GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 2
- GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 5
- GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 3
- GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 1
- GitHub Check: Run Backend Tests / Unit Tests - Python 3.10 - Group 4
- GitHub Check: Run Backend Tests / Integration Tests - Python 3.10
- GitHub Check: Lint Backend / Run Mypy (3.12)
- GitHub Check: Test Starter Templates
- GitHub Check: Optimize new Python code in this PR
- GitHub Check: test-starter-projects
🔇 Additional comments (3)
src/backend/base/langflow/__main__.py (1)
48-56: LGTM: lfx sub-app registers new “check” commandImport and registration look correct; no_args_is_help=True is consistent with existing sub-commands.
src/lfx/src/lfx/__main__.py (1)
5-19: LGTM: top-level LFX CLI exposes “check”Command wiring and help text look good.
src/lfx/src/lfx/cli/run.py (1)
243-246: LGTM: pre-run check gate for JSON flowsOpt-in check before loading JSON flows is correctly scoped behind --check and surfaces actionable guidance.
| import difflib | ||
| from typing import Any | ||
|
|
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.
Avoid shared state mutation when updating nodes — deep-copy components
apply_component_update mutates nested fields of the component dict copied from all_types_dict. Shallow copy risks modifying the cached template. Import copy to perform deep copies.
-import difflib
+import difflib
+import copy📝 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.
| import difflib | |
| from typing import Any | |
| import difflib | |
| import copy | |
| from typing import Any |
🤖 Prompt for AI Agents
In src/lfx/src/lfx/cli/check.py around lines 5 to 7, the code currently imports
difflib and typing but uses shallow copies when updating component dicts, which
can mutate the cached templates in all_types_dict; import the copy module at the
top (add import copy) and update apply_component_update to replace any shallow
copy (e.g., dict(...) or .copy()) with copy.deepcopy(...) when copying component
data before mutating, ensuring nested fields are not shared with the cached
template.
| for module_path in component_modules: | ||
| try: | ||
| # Import the module and get the component class | ||
| module_parts = module_path.split(".") | ||
| module_name = ".".join(module_parts[:-1]) | ||
| class_name = module_parts[-1] | ||
|
|
||
| await logger.adebug(f"Loading component {class_name} from {module_name}") | ||
|
|
||
| module = importlib.import_module(module_name) | ||
| component_class = getattr(module, class_name) | ||
|
|
||
| # Create component instance and template | ||
| component_instance = component_class() | ||
| comp_template, _ = create_component_template( | ||
| component_extractor=component_instance, module_name=module_path | ||
| ) | ||
|
|
||
| # Use the class name as the component type | ||
| components_dict[class_name] = comp_template | ||
|
|
||
| except (ImportError, AttributeError, RuntimeError) as e: | ||
| await logger.awarning(f"Failed to load component {module_path}: {e}") | ||
| continue | ||
|
|
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.
Unvalidated dynamic imports from flow metadata (RCE risk) and missing TypeError handling
Importing arbitrary module paths from flow JSON allows executing attacker‑controlled code during “check”. Restrict imports to trusted prefixes and handle constructor TypeError.
for module_path in component_modules:
try:
# Import the module and get the component class
module_parts = module_path.split(".")
module_name = ".".join(module_parts[:-1])
class_name = module_parts[-1]
+ # Security: only allow trusted component namespaces
+ allowed_prefixes = ("lfx.components.",)
+ if not module_name.startswith(allowed_prefixes):
+ await logger.awarning(
+ f"Skipping untrusted module path '{module_path}'. "
+ "Only 'lfx.components.*' modules are allowed for selective loading."
+ )
+ continue
+
+ if not module_name or not class_name:
+ await logger.awarning(f"Invalid component path '{module_path}'")
+ continue
+
await logger.adebug(f"Loading component {class_name} from {module_name}")
module = importlib.import_module(module_name)
component_class = getattr(module, class_name)
# Create component instance and template
- component_instance = component_class()
+ try:
+ component_instance = component_class()
+ except TypeError as te:
+ await logger.awarning(
+ f"Failed to instantiate component '{class_name}' from '{module_name}': {te}"
+ )
+ continue
comp_template, _ = create_component_template(
component_extractor=component_instance, module_name=module_path
)
# Use the class name as the component type
components_dict[class_name] = comp_template
- except (ImportError, AttributeError, RuntimeError) as e:
+ except (ImportError, AttributeError, RuntimeError) as e:
await logger.awarning(f"Failed to load component {module_path}: {e}")
continue📝 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.
| for module_path in component_modules: | |
| try: | |
| # Import the module and get the component class | |
| module_parts = module_path.split(".") | |
| module_name = ".".join(module_parts[:-1]) | |
| class_name = module_parts[-1] | |
| await logger.adebug(f"Loading component {class_name} from {module_name}") | |
| module = importlib.import_module(module_name) | |
| component_class = getattr(module, class_name) | |
| # Create component instance and template | |
| component_instance = component_class() | |
| comp_template, _ = create_component_template( | |
| component_extractor=component_instance, module_name=module_path | |
| ) | |
| # Use the class name as the component type | |
| components_dict[class_name] = comp_template | |
| except (ImportError, AttributeError, RuntimeError) as e: | |
| await logger.awarning(f"Failed to load component {module_path}: {e}") | |
| continue | |
| for module_path in component_modules: | |
| try: | |
| # Import the module and get the component class | |
| module_parts = module_path.split(".") | |
| module_name = ".".join(module_parts[:-1]) | |
| class_name = module_parts[-1] | |
| # Security: only allow trusted component namespaces | |
| allowed_prefixes = ("lfx.components.",) | |
| if not module_name.startswith(allowed_prefixes): | |
| await logger.awarning( | |
| f"Skipping untrusted module path '{module_path}'. " | |
| "Only 'lfx.components.*' modules are allowed for selective loading." | |
| ) | |
| continue | |
| if not module_name or not class_name: | |
| await logger.awarning(f"Invalid component path '{module_path}'") | |
| continue | |
| await logger.adebug(f"Loading component {class_name} from {module_name}") | |
| module = importlib.import_module(module_name) | |
| component_class = getattr(module, class_name) | |
| # Create component instance and template | |
| try: | |
| component_instance = component_class() | |
| except TypeError as te: | |
| await logger.awarning( | |
| f"Failed to instantiate component '{class_name}' from '{module_name}': {te}" | |
| ) | |
| continue | |
| comp_template, _ = create_component_template( | |
| component_extractor=component_instance, module_name=module_path | |
| ) | |
| # Use the class name as the component type | |
| components_dict[class_name] = comp_template | |
| except (ImportError, AttributeError, RuntimeError) as e: | |
| await logger.awarning(f"Failed to load component {module_path}: {e}") | |
| continue |
🤖 Prompt for AI Agents
In src/lfx/src/lfx/cli/check.py around lines 47 to 71, the code performs
unvalidated dynamic imports from flow metadata (RCE risk) and does not handle
TypeError from component constructors; validate module_path before importing by
only allowing module_name and class_name that match a configurable
allowlist/prefixes (e.g., check module_name.startswith(...) or membership in an
allowed set) and reject/log and skip any module_path that fails validation,
after import verify the attribute is a class and optionally that it subclasses
the expected component base type before instantiation, wrap the
component_class() call in a try/except that also catches TypeError and logs an
awarning with the error, and keep the existing
ImportError/AttributeError/RuntimeError handling intact.
| def find_component_in_types(component_type: str, all_types_dict: dict) -> dict | None: | ||
| """Find a component by searching across all categories in all_types_dict. | ||
| Args: | ||
| component_type: The component type to search for (e.g., 'Agent', 'CalculatorComponent') | ||
| all_types_dict: Dictionary of component categories and their components | ||
| Returns: | ||
| The component dict if found, None otherwise | ||
| """ | ||
| # First try direct lookup (for exact matches) | ||
| if component_type in all_types_dict: | ||
| return all_types_dict[component_type] | ||
|
|
||
| # Search across all categories | ||
| for category_components in all_types_dict.values(): | ||
| if isinstance(category_components, dict): | ||
| # Look for exact component name match | ||
| if component_type in category_components: | ||
| return category_components[component_type] | ||
|
|
||
| # Look for component by display name or class name | ||
| for comp_name, comp_data in category_components.items(): | ||
| if isinstance(comp_data, dict): | ||
| # Check display name | ||
| if comp_data.get("display_name") == component_type: | ||
| return comp_data | ||
| # Check if the component type matches any known mappings | ||
| if _matches_component_type(component_type, comp_name, comp_data): | ||
| return comp_data | ||
|
|
||
| return None |
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.
find_component_in_types fails for “flat” dicts from selective loads
When all_types_dict is a flat mapping {ComponentName: component_dict}, the current search wrongly treats component_dict as a “category” dict. Add a flat‑mapping search pass.
# First try direct lookup (for exact matches)
if component_type in all_types_dict:
return all_types_dict[component_type]
+ # Handle flat mapping: { "AgentComponent": { ...component... }, ... }
+ # Detect by checking for component-like keys in values.
+ try:
+ for comp_name, comp_data in all_types_dict.items():
+ if isinstance(comp_data, dict) and ("template" in comp_data or "outputs" in comp_data):
+ if comp_data.get("display_name") == component_type or _matches_component_type(
+ component_type, comp_name, comp_data
+ ):
+ return comp_data
+ except AttributeError:
+ # Not a flat mapping; fall through to category search
+ pass
+
# Search across all categories
for category_components in all_types_dict.values():
if isinstance(category_components, dict):
# Look for exact component name match
if component_type in category_components:
return category_components[component_type]
# Look for component by display name or class name
for comp_name, comp_data in category_components.items():
if isinstance(comp_data, dict):
# Check display name
if comp_data.get("display_name") == component_type:
return comp_data
# Check if the component type matches any known mappings
if _matches_component_type(component_type, comp_name, comp_data):
return comp_data📝 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.
| def find_component_in_types(component_type: str, all_types_dict: dict) -> dict | None: | |
| """Find a component by searching across all categories in all_types_dict. | |
| Args: | |
| component_type: The component type to search for (e.g., 'Agent', 'CalculatorComponent') | |
| all_types_dict: Dictionary of component categories and their components | |
| Returns: | |
| The component dict if found, None otherwise | |
| """ | |
| # First try direct lookup (for exact matches) | |
| if component_type in all_types_dict: | |
| return all_types_dict[component_type] | |
| # Search across all categories | |
| for category_components in all_types_dict.values(): | |
| if isinstance(category_components, dict): | |
| # Look for exact component name match | |
| if component_type in category_components: | |
| return category_components[component_type] | |
| # Look for component by display name or class name | |
| for comp_name, comp_data in category_components.items(): | |
| if isinstance(comp_data, dict): | |
| # Check display name | |
| if comp_data.get("display_name") == component_type: | |
| return comp_data | |
| # Check if the component type matches any known mappings | |
| if _matches_component_type(component_type, comp_name, comp_data): | |
| return comp_data | |
| return None | |
| def find_component_in_types(component_type: str, all_types_dict: dict) -> dict | None: | |
| """Find a component by searching across all categories in all_types_dict. | |
| Args: | |
| component_type: The component type to search for (e.g., 'Agent', 'CalculatorComponent') | |
| all_types_dict: Dictionary of component categories and their components | |
| Returns: | |
| The component dict if found, None otherwise | |
| """ | |
| # First try direct lookup (for exact matches) | |
| if component_type in all_types_dict: | |
| return all_types_dict[component_type] | |
| # Handle flat mapping: { "AgentComponent": { ...component... }, ... } | |
| # Detect by checking for component-like keys in values. | |
| try: | |
| for comp_name, comp_data in all_types_dict.items(): | |
| if isinstance(comp_data, dict) and ("template" in comp_data or "outputs" in comp_data): | |
| if comp_data.get("display_name") == component_type or _matches_component_type( | |
| component_type, comp_name, comp_data | |
| ): | |
| return comp_data | |
| except AttributeError: | |
| # Not a flat mapping; fall through to category search | |
| pass | |
| # Search across all categories | |
| for category_components in all_types_dict.values(): | |
| if isinstance(category_components, dict): | |
| # Look for exact component name match | |
| if component_type in category_components: | |
| return category_components[component_type] | |
| # Look for component by display name or class name | |
| for comp_name, comp_data in category_components.items(): | |
| if isinstance(comp_data, dict): | |
| # Check display name | |
| if comp_data.get("display_name") == component_type: | |
| return comp_data | |
| # Check if the component type matches any known mappings | |
| if _matches_component_type(component_type, comp_name, comp_data): | |
| return comp_data | |
| return None |
| if component_modules: | ||
| await logger.adebug(f"Found {len(component_modules)} component modules in flow metadata, loading selectively") | ||
| all_types_dict = await load_specific_components(component_modules) | ||
| else: | ||
| await logger.adebug("No module metadata found, loading all component types") | ||
| settings_service = get_settings_service() | ||
| try: | ||
| all_types_dict = await get_and_cache_all_types_dict(settings_service) | ||
| # Log available component types for debugging | ||
| if all_types_dict: | ||
| available_types = list(all_types_dict.keys()) | ||
| await logger.adebug(f"Loaded {len(available_types)} component types: {available_types[:10]}...") | ||
| else: | ||
| await logger.adebug("No component types loaded") | ||
| except (ImportError, RuntimeError) as e: | ||
| return { | ||
| "error": f"Failed to load component types: {e}", | ||
| "flow_path": flow_path, | ||
| } | ||
|
|
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.
Selective load can under-detect; add fallback to full catalog
If selective loading yields zero components (e.g., bad/unknown module paths), the check silently proceeds with an empty catalog. Fall back to get_and_cache_all_types_dict.
if component_modules:
await logger.adebug(f"Found {len(component_modules)} component modules in flow metadata, loading selectively")
all_types_dict = await load_specific_components(component_modules)
+ if not all_types_dict:
+ await logger.adebug(
+ "Selective component import returned no results; falling back to full catalog"
+ )
+ settings_service = get_settings_service()
+ try:
+ all_types_dict = await get_and_cache_all_types_dict(settings_service)
+ except (ImportError, RuntimeError) as e:
+ return {
+ "error": f"Failed to load component types: {e}",
+ "flow_path": flow_path,
+ }
else:📝 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.
| if component_modules: | |
| await logger.adebug(f"Found {len(component_modules)} component modules in flow metadata, loading selectively") | |
| all_types_dict = await load_specific_components(component_modules) | |
| else: | |
| await logger.adebug("No module metadata found, loading all component types") | |
| settings_service = get_settings_service() | |
| try: | |
| all_types_dict = await get_and_cache_all_types_dict(settings_service) | |
| # Log available component types for debugging | |
| if all_types_dict: | |
| available_types = list(all_types_dict.keys()) | |
| await logger.adebug(f"Loaded {len(available_types)} component types: {available_types[:10]}...") | |
| else: | |
| await logger.adebug("No component types loaded") | |
| except (ImportError, RuntimeError) as e: | |
| return { | |
| "error": f"Failed to load component types: {e}", | |
| "flow_path": flow_path, | |
| } | |
| if component_modules: | |
| await logger.adebug( | |
| f"Found {len(component_modules)} component modules in flow metadata, loading selectively" | |
| ) | |
| all_types_dict = await load_specific_components(component_modules) | |
| if not all_types_dict: | |
| await logger.adebug( | |
| "Selective component import returned no results; falling back to full catalog" | |
| ) | |
| settings_service = get_settings_service() | |
| try: | |
| all_types_dict = await get_and_cache_all_types_dict(settings_service) | |
| except (ImportError, RuntimeError) as e: | |
| return { | |
| "error": f"Failed to load component types: {e}", | |
| "flow_path": flow_path, | |
| } | |
| else: | |
| await logger.adebug("No module metadata found, loading all component types") | |
| settings_service = get_settings_service() | |
| try: | |
| all_types_dict = await get_and_cache_all_types_dict(settings_service) | |
| # Log available component types for debugging | |
| if all_types_dict: | |
| available_types = list(all_types_dict.keys()) | |
| await logger.adebug( | |
| f"Loaded {len(available_types)} component types: {available_types[:10]}..." | |
| ) | |
| else: | |
| await logger.adebug("No component types loaded") | |
| except (ImportError, RuntimeError) as e: | |
| return { | |
| "error": f"Failed to load component types: {e}", | |
| "flow_path": flow_path, | |
| } |
🤖 Prompt for AI Agents
In src/lfx/src/lfx/cli/check.py around lines 172 to 191, when component_modules
is provided but load_specific_components returns an empty dict the code
currently continues with an empty catalog; change the flow so that after calling
load_specific_components you check if all_types_dict is empty/falsy and, if so,
obtain settings_service and call get_and_cache_all_types_dict to fall back to
the full catalog (log the fallback with logger.adebug), preserving the existing
try/except behavior for get_and_cache_all_types_dict and returning the same
error structure on exceptions.
| # Update the node with the latest component data | ||
| node["data"]["node"] = found_component.copy() | ||
|
|
||
| # Restore user values where possible | ||
| updated_template = node["data"]["node"].get("template", {}) | ||
| for field_name, user_value in user_values.items(): | ||
| if field_name in updated_template and isinstance(updated_template[field_name], dict): | ||
| updated_template[field_name]["value"] = user_value | ||
|
|
||
| break |
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.
Deep copy updated component to prevent mutating the catalog
Shallow copying found_component means nested template dicts are shared with all_types_dict. Mutating 'value' will leak back into the catalog.
- node["data"]["node"] = found_component.copy()
+ node["data"]["node"] = copy.deepcopy(found_component)📝 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.
| # Update the node with the latest component data | |
| node["data"]["node"] = found_component.copy() | |
| # Restore user values where possible | |
| updated_template = node["data"]["node"].get("template", {}) | |
| for field_name, user_value in user_values.items(): | |
| if field_name in updated_template and isinstance(updated_template[field_name], dict): | |
| updated_template[field_name]["value"] = user_value | |
| break | |
| # Update the node with the latest component data | |
| node["data"]["node"] = copy.deepcopy(found_component) | |
| # Restore user values where possible | |
| updated_template = node["data"]["node"].get("template", {}) | |
| for field_name, user_value in user_values.items(): | |
| if field_name in updated_template and isinstance(updated_template[field_name], dict): | |
| updated_template[field_name]["value"] = user_value | |
| break |
🤖 Prompt for AI Agents
In src/lfx/src/lfx/cli/check.py around lines 632 to 641, the code uses a shallow
copy (found_component.copy()) when assigning node["data"]["node"], which allows
nested template dicts to remain shared with the catalog; change this to a deep
copy to avoid mutating shared structures (use copy.deepcopy(found_component))
and ensure the copy module is imported at the top of the file (import copy) so
subsequent updates to template[field]["value"] do not leak back into
all_types_dict.
| # TODO: Move this to the place where it is needed | ||
| # try: | ||
| # dependency_info = analyze_component_dependencies(custom_component._code) | ||
| # frontend_node.metadata["dependencies"] = dependency_info | ||
| # except (SyntaxError, TypeError, ValueError, ImportError) as exc: | ||
| # logger.warning(f"Failed to analyze dependencies for component {ctype_name}: {exc}") | ||
| # # Set minimal dependency info on failure | ||
| # frontend_node.metadata["dependencies"] = { | ||
| # "total_dependencies": 0, | ||
| # "dependencies": [], | ||
| # } | ||
|
|
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.
🧩 Analysis chain
Dependencies metadata removed — verify no consumers require metadata['dependencies']
You’ve commented out dependency analysis and no longer write metadata['dependencies']. Ensure no code paths or UI expect it to exist to avoid KeyErrors or feature regressions.
Run this repo-wide scan to confirm no hard dependency on metadata['dependencies'] remains:
🏁 Script executed:
#!/bin/bash
# Search for accesses to metadata["dependencies"] or .get("dependencies")
rg -n -C2 -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' \
-P 'metadata\s*[\[\.]\s*["'\'']dependencies["'\'']|\.get\(\s*["'\'']dependencies["'\'']'Length of output: 148
#!/bin/bash
# Search for literal metadata["dependencies"] or metadata['dependencies']
rg -F -n 'metadata["dependencies"]'
rg -F -n "metadata['dependencies']"
# Search for literal metadata.get("dependencies") or metadata.get('dependencies')
rg -F -n 'metadata.get("dependencies")'
rg -F -n "metadata.get('dependencies')"🏁 Script executed:
#!/bin/bash
# Search for direct dict access metadata['dependencies'] in all Python files
rg --type py -n -H -C1 "metadata\[\s*['\"]dependencies['\"]\s*\]"
# Search for dict.get('dependencies')
rg --type py -n -H -C1 "metadata\.get\(\s*['\"]dependencies['\"]\s*\)"Length of output: 2301
Restore or stub metadata['dependencies'] usage
Tests in src/lfx/tests/unit/custom/test_utils_metadata.py (e.g. lines 375, 398, 425, 460, 512) still reference metadata["dependencies"], so commenting out its assignment breaks CI. Re-enable at least a stub assignment in utils.py or update tests to use metadata.get("dependencies", …).
🤖 Prompt for AI Agents
In src/lfx/src/lfx/custom/utils.py around lines 528-539, the assignment to
frontend_node.metadata["dependencies"] was commented out causing tests to fail;
restore the logic so metadata["dependencies"] is always set: wrap the dependency
analysis in a try/except and on success set metadata["dependencies"] to the
analyzed info, and on any failure set a minimal stub like {"total_dependencies":
0, "dependencies": []} (and log the warning). Ensure the except catches the same
exceptions (SyntaxError, TypeError, ValueError, ImportError) and that the
metadata key is assigned in both branches.
- Introduced comprehensive unit tests for the check command, covering various scenarios including component detection, outdated checks, and error handling. - Implemented tests for both synchronous and asynchronous functions, ensuring robust validation of the check command's behavior. - Enhanced test coverage for component mapping and breaking change detection, contributing to improved reliability and maintainability of the codebase. - Included tests for integration with real flow files, ensuring the command operates correctly in practical scenarios. These additions strengthen the testing framework and promote confidence in the check command's functionality.
- Adjusted the `generate_code_diff` function to skip the correct headers in the diff output, enhancing accuracy in code comparison. - Replaced shallow copy with deep copy for component updates in `apply_component_update`, ensuring that nested structures are correctly preserved during updates. This change improves data integrity and prevents unintended mutations.
- Updated the `build_component_metadata` function to always initialize the `dependencies` field in the frontend node's metadata with default values, enhancing consistency and preventing potential issues when analysis is disabled.
- Added `@pytest.mark.xfail` to tests for building component metadata with external, optional, and real dependencies, indicating that the dependency analyzer is currently commented out and returns minimal stub data. This change helps clarify the expected behavior during testing and maintains test integrity until the analyzer is fully implemented.
- Added security checks to ensure only trusted component namespaces are loaded, preventing potential vulnerabilities. - Implemented error handling for component instantiation failures, logging warnings for invalid paths and instantiation issues to improve robustness and traceability during component loading.
…unction - Introduced a new test suite for the `load_specific_components` function to validate security measures against various attack vectors, including blocking malicious module imports and handling invalid paths. - Ensured that both valid and invalid component paths are processed correctly, maintaining robust security validation. - Implemented tests for case sensitivity and strict prefix validation to prevent potential bypass attempts.
# Conflicts: # src/lfx/src/lfx/_assets/component_index.json
jordanrfrazier
left a comment
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.
Code looks good to me. if it's tested and working manually then lgtm
Adds the check command which helps users check if their flows have outdated components and update them if they so choose.
Summary by CodeRabbit
New Features
Chores