feat: Add IBM Watsonx and Ollama to Agent Component#10357
Conversation
Replaces direct settings access with a lazy-loading function for the knowledge bases root path in Knowledge Ingestion and Knowledge Retrieval starter projects. This improves reliability and consistency when accessing the knowledge base directory, and updates all usages to the new helper function.
Introduces IBM watsonx.ai as a selectable model provider in multiple starter project JSONs. Adds new input fields for 'base_url', 'project_id', and 'max_output_tokens' to support IBM watsonx.ai integration. Updates agent component code to handle new provider and its required parameters.
Ollama has been added as a supported provider alongside Anthropic, Google Generative AI, OpenAI, and IBM watsonx.ai in all starter project JSON files. This expands the available options for LLM integration in initial setup templates.
Refactored model_input_constants.py to update the OLLAMA_MODEL_INPUTS and OLLAMA_MODEL_INPUTS_MAP. Modified ollama.py to use the new input mapping and improved input handling for Ollama components.
|
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 WalkthroughStarter projects are updated with three new Agent inputs (base_url, project_id, max_output_tokens) and expanded model provider options for IBM watsonx.ai and Ollama. Knowledge base components refactored for lazy-loaded root path resolution. Model provider constants extended with new providers and conditional input processing. Agent, Watsonx, and Ollama components updated with new/renamed public inputs. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes The PR spans multiple heterogeneous changes across diverse file types: 13 starter project JSON files with similar but distinct input/provider updates requiring individual validation, two knowledge base components with refactored path resolution logic, model provider constants with new conditional logic, and component-level input/parameter modifications. While many changes follow repetitive patterns (particularly in starter projects), each requires validation for correctness, and the provider-specific logic in model_input_constants and component refactoring demand careful review. Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 error, 4 warnings)
✅ 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 |
Added Notion-related components to the component index, including AddContentToPage, NotionDatabaseProperties, NotionListPages, NotionPageContent, NotionPageCreator, NotionPageUpdate, and NotionSearch. These components enable interaction with Notion databases and pages, supporting operations such as querying, creating, updating, and retrieving content.
Refactored the AgentComponent code in multiple starter project JSON files to remove IBM watsonx.ai-specific logic and ensure consistent OpenAI input filtering. Updated the code_hash metadata to reflect the new code version.
There was a problem hiding this comment.
Actionable comments posted: 24
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json (1)
2090-2107: Add metadata entries for new provider options.The Agent model provider dropdown now includes "IBM watsonx.ai" and "Ollama" (lines 2090-2095), but the
options_metadataarray (lines 2097-2107) only has three entries corresponding to "Anthropic", "Google Generative AI", and "OpenAI". This mismatch may cause UI rendering issues or missing provider icons when the new options are selected.Extend the
options_metadataarray to include metadata objects for "IBM watsonx.ai" and "Ollama":"options_metadata": [ { "icon": "Anthropic" }, { "icon": "GoogleGenerativeAI" }, { "icon": "OpenAI" + }, + { + "icon": "IBMWatsonx" + }, + { + "icon": "Ollama" } ],Verify the exact icon names match the icon registry; use "IBMWatsonx" and "Ollama" as placeholders if you need to confirm the correct identifiers.
src/backend/base/langflow/initial_setup/starter_projects/Market Research.json (1)
1658-1678: Provider options and metadata count mismatch.The
agent_llmdropdown defines 5 provider options (Anthropic, Google Generative AI, OpenAI, IBM watsonx.ai, Ollama) but provides only 4 metadata entries. This mismatch can cause UI rendering issues or missing icons for the Ollama provider.Additionally, the metadata at line 1676 reuses the "OpenAI" icon for what appears to be IBM watsonx.ai. Provide metadata for all 5 providers with appropriate icons.
Apply this fix to align metadata with options:
"options": [ "Anthropic", "Google Generative AI", "OpenAI", "IBM watsonx.ai", "Ollama" ], "options_metadata": [ { "icon": "Anthropic" }, { "icon": "GoogleGenerativeAI" }, { "icon": "OpenAI" }, - { + { - "icon": "OpenAI" + "icon": "IBM" + }, + { + "icon": "Ollama" } ],src/lfx/src/lfx/base/models/model_input_constants.py (1)
24-57: Don’t disable model_name refresh for OllamaOllama relies on real_time_refresh/refresh_button to populate models. Current logic disables it for all except IBM.
Fix:
- elif component_data.name == "model_name": - if provider_name not in ["IBM watsonx.ai"]: - component_data = set_real_time_refresh_false(component_data) + elif component_data.name == "model_name": + if provider_name not in ["IBM watsonx.ai", "Ollama"]: + component_data = set_real_time_refresh_false(component_data) component_data = add_combobox_true(component_data)Alternatively, don’t override real_time_refresh at all and let components define it.
src/backend/base/langflow/initial_setup/starter_projects/Pokédex Agent.json (1)
1347-1364: [Duplicate from SaaS Pricing] Provider options expanded without corresponding metadata entries.The
agent_llmoptions list (lines 1347–1353) includes 5 providers ("Anthropic", "Google Generative AI", "OpenAI", "IBM watsonx.ai", "Ollama"), butoptions_metadata(lines 1354–1364) contains only 3 entries. This identical mismatch as in the SaaS Pricing starter project will cause rendering or indexing issues for the new providers.Add metadata entries for IBM watsonx.ai and Ollama to maintain consistency.
src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json (1)
976-993: Provider options expanded without corresponding metadata entries.The
agent_llmoptions list (lines 976–982) includes 5 providers ("Anthropic", "Google Generative AI", "OpenAI", "IBM watsonx.ai", "Ollama"), butoptions_metadata(lines 983–993) contains only 3 entries with icons for Anthropic, GoogleGenerativeAI, and OpenAI.Add metadata entries for "IBM watsonx.ai" and "Ollama" with their corresponding icons:
{ "icon": "IBMWatsonx" }, { "icon": "Ollama" }
🧹 Nitpick comments (9)
src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json (1)
949-950: Strip the leftover debug print
print(watsonx_inputs_filtered)will spam stdout every time the component is imported. Please remove the print (or replace it with structured logging if you genuinely need the signal) before shipping.src/backend/base/langflow/initial_setup/starter_projects/Search agent.json (1)
1135-1136: Remove the debug printThis starter carries the same
print(watsonx_inputs_filtered)side effect. Please delete it or replace it with proper logging before release.src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json (1)
1265-1268: Provider options extended — add icons for new entriesIBM watsonx.ai and Ollama added, but options_metadata lacks their icons; add for visual consistency.
src/lfx/src/lfx/components/ibm/watsonx.py (1)
24-31: Static endpoints OK; allow custom endpoint tooList is fine; consider combobox True on base_url to allow custom regions/private gateways.
src/lfx/src/lfx/base/models/model_input_constants.py (2)
17-22: Style nitAdd space after commas in function signatures for readability.
355-360: Dynamic fields alignmentMODEL_DYNAMIC_UPDATE_FIELDS includes "watsonx_endpoint" and "url", while components use "base_url". Align names to avoid dead updates.
src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json (1)
1289-1292: Provider options extended — add icons for new entriesAdd icons metadata for IBM watsonx.ai and Ollama to match others.
src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json (1)
1023-1041: Required fields withshow: falsedepend on conditional display logic.Lines 1023–1041 (base_url) and 1386–1404 (project_id) are both marked as
required: truebutshow: false. These fields must become visible and enabled when users select IBM watsonx.ai or Ollama from the provider dropdown. This visibility toggling is likely handled by theupdate_build_configmethod in the embedded Python code.Recommendation: Verify that the
update_build_configmethod (or equivalent logic in the Agent component) correctly setsshow: truefor these fields when the corresponding provider is selected, otherwise users will encounter validation errors on submission.Also applies to: 1386-1404
src/backend/base/langflow/initial_setup/starter_projects/Pokédex Agent.json (1)
1394-1412: [Duplicate from SaaS Pricing] Required fields withshow: falsedepend on conditional display logic.Lines 1394–1412 (base_url) and 1757–1775 (project_id) are marked as
required: truebutshow: false, replicating the same pattern as SaaS Pricing. Ensure the Agent component'supdate_build_configmethod correctly toggles visibility when IBM watsonx.ai or Ollama providers are selected.Also applies to: 1757-1775
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json(6 hunks)src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json(6 hunks)src/backend/base/langflow/initial_setup/starter_projects/Knowledge Ingestion.json(2 hunks)src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json(2 hunks)src/backend/base/langflow/initial_setup/starter_projects/Market Research.json(6 hunks)src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json(6 hunks)src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json(6 hunks)src/backend/base/langflow/initial_setup/starter_projects/Pokédex Agent.json(6 hunks)src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json(6 hunks)src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json(6 hunks)src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json(6 hunks)src/backend/base/langflow/initial_setup/starter_projects/Search agent.json(6 hunks)src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json(6 hunks)src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json(6 hunks)src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json(6 hunks)src/lfx/src/lfx/base/models/model_input_constants.py(5 hunks)src/lfx/src/lfx/components/agents/agent.py(4 hunks)src/lfx/src/lfx/components/ibm/watsonx.py(4 hunks)src/lfx/src/lfx/components/ollama/ollama.py(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/lfx/src/lfx/components/ollama/ollama.py (1)
src/lfx/src/lfx/inputs/inputs.py (1)
BoolInput(414-426)
src/lfx/src/lfx/components/ibm/watsonx.py (2)
src/lfx/src/lfx/custom/custom_component/component.py (1)
get_base_inputs(167-170)src/lfx/src/lfx/inputs/inputs.py (1)
DropdownInput(465-490)
src/lfx/src/lfx/components/agents/agent.py (1)
src/lfx/src/lfx/inputs/inputs.py (3)
SecretStrInput(286-341)StrInput(126-182)IntInput(344-376)
src/lfx/src/lfx/base/models/model_input_constants.py (3)
src/lfx/src/lfx/custom/custom_component/component.py (1)
get_base_inputs(167-170)src/lfx/src/lfx/components/ollama/ollama.py (1)
ChatOllamaComponent(18-327)src/lfx/src/lfx/components/ibm/watsonx.py (1)
WatsonxAIComponent(16-207)
🔇 Additional comments (20)
src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json (2)
514-514: ✓ code_hash update is appropriate.The metadata hash has been updated to reflect the refactored implementation with lazy-loaded path resolution. This ensures the component metadata accurately represents the current code state.
608-609: ✓ Lazy-loading refactor is well-executed.The implementation correctly defers knowledge bases root path initialization:
- Global cache variable
_KNOWLEDGE_BASES_ROOT_PATHis initialized on first use_get_knowledge_bases_root_path()safely guards initialization with a None check and proper global declaration- Both
update_build_config()andretrieve_data()call the getter instead of accessing the path directly- Error handling is preserved (ValueError when knowledge directory is not set)
- Path expansion with
.expanduser()handles home directory references appropriatelyThe refactor improves module startup performance by deferring settings resolution to first use, which is particularly valuable for components that may not always be instantiated.
src/backend/base/langflow/initial_setup/starter_projects/Knowledge Ingestion.json (3)
740-740: Approved: Lazy-loaded knowledge base root path refactoring.The introduction of
_get_knowledge_bases_root_path()and_get_kb_root()improves initialization timing and reduces startup overhead by deferring path resolution until actually needed. The caching pattern in_cached_kb_pathis well-implemented.Also applies to: 869-870
869-870: Consider thread-safety implications of global mutable state.The module-level
_KNOWLEDGE_BASES_ROOT_PATHglobal is mutated during lazy initialization. While the pattern is common and includes a None-check guard, in concurrent scenarios (e.g., multiple threads calling_get_knowledge_bases_root_path()simultaneously), a race condition could occur where the path is computed multiple times. If this component runs in a multi-threaded context, consider using thread-safe initialization (e.g.,threading.Lockorthreading.local()).Does the Langflow backend execute component code in a multi-threaded environment where concurrent access to
_get_knowledge_bases_root_path()is possible?
740-740: Verify file alignment with PR objectives.The PR objectives reference updating "Instagram Copywriter.json" with new Agent inputs (base_url, project_id, max_output_tokens) and expanded provider support (IBM watsonx.ai, Ollama). However, the file under review is "Knowledge Ingestion.json" with knowledge base refactoring changes.
Please confirm whether this is the intended file for review or if "Instagram Copywriter.json" should be provided instead.
src/lfx/src/lfx/components/ollama/ollama.py (2)
194-194: LGTM! Defensive fallback added.The parameter mapping correctly uses the new field name
enable_verbose_outputwhile maintaining the underlying library parameter nameverbose. Theor Falsefallback ensures a valid boolean value is always passed.
115-115: <!-- [scratchpad_update]
[task verify_breaking_change_impact 3/3 - FINAL]
Web search provided context about Ollama's verbose usage but no specific LFX/Langflow migration info. Result 9-1 shows LangChain's Ollama class has a "verbose" property. However, this is LangChain's verbose (debugging), not an input parameter name.Key finding: The review comment treats this as a critical breaking change but there is:
- NO evidence of existing "verbose" field in LFX Ollama component historically
- NO migration path mentioned anywhere
- Component input is BoolInput with name="enable_verbose_output" - this is the ONLY reference found
- This appears to be either: (a) new component, (b) preemptive refactoring for clarity before release, or (c) misunderstanding in original review
Conclusion: The original review comment is likely INCORRECT or based on outdated information. The field name "enable_verbose_output" appears to be the current/only state. No evidence of previous "verbose" field or migration path needed.
[verdict]: Original review comment overstates the issue. This is NOT a breaking change for existing flows because:
- No historical "verbose" field found in LFX codebase
- This is a local component input rename
- No public API impact unless flows were serialized with old name
- Descriptive naming is actually a positive improvement
[scratchpad_end] -->No migration path needed - field name appears to be current state only.
Based on verification of the codebase and web search results, the original review comment appears to overstate the concern. The Ollama API does support a "verbose" parameter, but there is no evidence that the LFX Ollama component previously had a
verbosefield that was renamed toenable_verbose_output.The script output confirms line 115 shows:
BoolInput(name="enable_verbose_output", display_name="Ollama Verbose Output", info="Whether to print out response text.", advanced=True),This is the only field name found in the codebase. No
verbosefield, deprecated aliases, or migration logic exists in the component. The more descriptive field nameenable_verbose_outputis actually a UX improvement over a genericverbosename, especially since verbose is context-dependent (LangChain also uses verbose for debug logging).There are no existing flows using an old
verbosefield name that would be affected by this change.Likely an incorrect or invalid review comment.
src/lfx/src/lfx/components/agents/agent.py (2)
499-503: LGTM! Defensive programming enhancement.The addition of
build_config is not Nonecheck prevents potentialAttributeErrorifdelete_fieldsis called with aNonebuild_config. This is good defensive programming.
23-23: LGTM! Required imports added.The imports of
SecretStrInputandStrInputare necessary for the new input fields added to the AgentComponent.src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json (1)
1038-1044: Provider registration verified.Both "IBM watsonx.ai" and "Ollama" are properly registered in
MODEL_PROVIDERS_DICTwith configuration at lines 321 and 335 respectively inmodel_input_constants.py, and both appear inMODEL_PROVIDERS_LIST. The corresponding components exist with test coverage. No further action required.src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json (2)
2137-2155: Verify conditional visibility logic for provider-specific fields.Three new fields—
base_url(line 2137),project_id(line 2500), andmax_output_tokens(line 2281)—are configured with"show": false. These appear intended for conditional display based on the selected provider (e.g., only show for IBM watsonx.ai or Ollama). Ensure that the component'supdate_build_configmethod correctly setsshow: truefor these fields when the corresponding provider is selected.Verify that the component logic in the Agent class (lines 2189–2645) conditionally updates these field visibilities. Look for code that toggles the
showproperty based onself.agent_llmprovider selection. If this logic is missing, the fields will remain hidden and users won't be able to configure them.Also applies to: 2281-2298, 2500-2518
1966-1966: Confirm code_hash update reflects all component changes.The
code_hashmetadata was updated frombdb8b8db6375to6f2517926f3d(line 1966). This reflects the changes to the Agent component inputs and code logic. Verify this hash is regenerated from the actual component code to avoid inconsistencies that could cause version mismatch issues in deployment.src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json (1)
1714-1741: Add missing options_metadata entries using repository icon aliases.Options include "IBM watsonx.ai" and "Ollama" but options_metadata only lists three icons; add the two missing entries and use the repo's alias for IBM watsonx.ai ("WatsonxAI"):
"options_metadata": [ { "icon": "Anthropic" }, { "icon": "GoogleGenerativeAI" }, { "icon": "OpenAI" } + { + "icon": "WatsonxAI" + }, + { + "icon": "Ollama" + } ],Likely an incorrect or invalid review comment.
src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json (2)
1154-1154: Metadata bump OKcode_hash update acknowledged.
1453-1470: IntInput default as empty string — confirm type handlingmax_output_tokens has value:"" for an int field. Ensure renderer/parsing tolerates empty string; otherwise prefer null/omitted.
src/lfx/src/lfx/components/ibm/watsonx.py (2)
60-63: Model Name refresh wiring looks goodreal_time_refresh + refresh_button for model_name is appropriate.
200-207: build_model will receive list if base_url remains listThis is fixed by the base_url value change above; otherwise ChatWatsonx(...) will break at runtime.
src/lfx/src/lfx/base/models/model_input_constants.py (1)
318-345: Provider registrations look goodIBM watsonx.ai and Ollama entries added correctly with icons and active flags.
src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json (2)
1178-1178: Metadata bump OKcode_hash update acknowledged.
1477-1494: IntInput default as empty string — confirm type handlingmax_output_tokens has value:"" for an int field. Verify UI/parser accepts empty string.
| "title_case": false, | ||
| "type": "code", | ||
| "value": "import json\nimport re\n\nfrom langchain_core.tools import StructuredTool, Tool\nfrom pydantic import ValidationError\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.model_input_constants import (\n ALL_PROVIDER_FIELDS,\n MODEL_DYNAMIC_UPDATE_FIELDS,\n MODEL_PROVIDERS_DICT,\n MODEL_PROVIDERS_LIST,\n MODELS_METADATA,\n)\nfrom lfx.base.models.model_utils import get_model_name\nfrom lfx.components.helpers.current_date import CurrentDateComponent\nfrom lfx.components.helpers.memory import MemoryComponent\nfrom lfx.components.langchain_utilities.tool_calling import ToolCallingAgentComponent\nfrom lfx.custom.custom_component.component import get_component_toolkit\nfrom lfx.custom.utils import update_component_build_config\nfrom lfx.helpers.base_model import build_model_from_schema\nfrom lfx.inputs.inputs import BoolInput\nfrom lfx.io import DropdownInput, IntInput, MessageTextInput, MultilineInput, Output, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(ToolCallingAgentComponent):\n display_name: str = \"Agent\"\n description: str = \"Define the agent's instructions, then enter a task to complete using tools.\"\n documentation: str = \"https://docs.langflow.org/agents\"\n icon = \"bot\"\n beta = False\n name = \"Agent\"\n\n memory_inputs = [set_advanced_true(component_input) for component_input in MemoryComponent().inputs]\n\n # Filter out json_mode from OpenAI inputs since we handle structured output differently\n if \"OpenAI\" in MODEL_PROVIDERS_DICT:\n openai_inputs_filtered = [\n input_field\n for input_field in MODEL_PROVIDERS_DICT[\"OpenAI\"][\"inputs\"]\n if not (hasattr(input_field, \"name\") and input_field.name == \"json_mode\")\n ]\n else:\n openai_inputs_filtered = []\n\n inputs = [\n DropdownInput(\n name=\"agent_llm\",\n display_name=\"Model Provider\",\n info=\"The provider of the language model that the agent will use to generate responses.\",\n options=[*MODEL_PROVIDERS_LIST],\n value=\"OpenAI\",\n real_time_refresh=True,\n refresh_button=False,\n input_types=[],\n options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST if key in MODELS_METADATA],\n external_options={\n \"fields\": {\n \"data\": {\n \"node\": {\n \"name\": \"connect_other_models\",\n \"display_name\": \"Connect other models\",\n \"icon\": \"CornerDownLeft\",\n }\n }\n },\n },\n ),\n *openai_inputs_filtered,\n MultilineInput(\n name=\"system_prompt\",\n display_name=\"Agent Instructions\",\n info=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\n advanced=False,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Chat History Messages\",\n value=100,\n info=\"Number of chat history messages to retrieve.\",\n advanced=True,\n show=True,\n ),\n MultilineInput(\n name=\"format_instructions\",\n display_name=\"Output Format Instructions\",\n info=\"Generic Template for structured output formatting. Valid only with Structured response.\",\n value=(\n \"You are an AI that extracts structured JSON objects from unstructured text. \"\n \"Use a predefined schema with expected types (str, int, float, bool, dict). \"\n \"Extract ALL relevant instances that match the schema - if multiple patterns exist, capture them all. \"\n \"Fill missing or ambiguous values with defaults: null for missing values. \"\n \"Remove exact duplicates but keep variations that have different field values. \"\n \"Always return valid JSON in the expected format, never throw errors. \"\n \"If multiple objects can be extracted, return them all in the structured format.\"\n ),\n advanced=True,\n ),\n TableInput(\n name=\"output_schema\",\n display_name=\"Output Schema\",\n info=(\n \"Schema Validation: Define the structure and data types for structured output. \"\n \"No validation if no output schema.\"\n ),\n advanced=True,\n required=False,\n value=[],\n table_schema=[\n {\n \"name\": \"name\",\n \"display_name\": \"Name\",\n \"type\": \"str\",\n \"description\": \"Specify the name of the output field.\",\n \"default\": \"field\",\n \"edit_mode\": EditMode.INLINE,\n },\n {\n \"name\": \"description\",\n \"display_name\": \"Description\",\n \"type\": \"str\",\n \"description\": \"Describe the purpose of the output field.\",\n \"default\": \"description of field\",\n \"edit_mode\": EditMode.POPOVER,\n },\n {\n \"name\": \"type\",\n \"display_name\": \"Type\",\n \"type\": \"str\",\n \"edit_mode\": EditMode.INLINE,\n \"description\": (\"Indicate the data type of the output field (e.g., str, int, float, bool, dict).\"),\n \"options\": [\"str\", \"int\", \"float\", \"bool\", \"dict\"],\n \"default\": \"str\",\n },\n {\n \"name\": \"multiple\",\n \"display_name\": \"As List\",\n \"type\": \"boolean\",\n \"description\": \"Set to True if this output field should be a list of the specified type.\",\n \"default\": \"False\",\n \"edit_mode\": EditMode.INLINE,\n },\n ],\n ),\n *LCToolsAgentComponent._base_inputs,\n # removed memory inputs from agent component\n # *memory_inputs,\n BoolInput(\n name=\"add_current_date_tool\",\n display_name=\"Current Date\",\n advanced=True,\n info=\"If true, will add a tool to the agent that returns the current date.\",\n value=True,\n ),\n ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\n ]\n\n async def get_agent_requirements(self):\n \"\"\"Get the agent requirements for the agent.\"\"\"\n llm_model, display_name = await self.get_llm()\n if llm_model is None:\n msg = \"No language model selected. Please choose a model to proceed.\"\n raise ValueError(msg)\n self.model_name = get_model_name(llm_model, display_name=display_name)\n\n # Get memory data\n self.chat_history = await self.get_memory_data()\n await logger.adebug(f\"Retrieved {len(self.chat_history)} chat history messages\")\n if isinstance(self.chat_history, Message):\n self.chat_history = [self.chat_history]\n\n # Add current date tool if enabled\n if self.add_current_date_tool:\n if not isinstance(self.tools, list): # type: ignore[has-type]\n self.tools = []\n current_date_tool = (await CurrentDateComponent(**self.get_base_args()).to_toolkit()).pop(0)\n if not isinstance(current_date_tool, StructuredTool):\n msg = \"CurrentDateComponent must be converted to a StructuredTool\"\n raise TypeError(msg)\n self.tools.append(current_date_tool)\n return llm_model, self.chat_history, self.tools\n\n async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=self.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components else \"\"\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\n )\n return [\n message for message in messages if getattr(message, \"id\", None) != getattr(self.input_value, \"id\", None)\n ]\n\n async def get_llm(self):\n if not isinstance(self.agent_llm, str):\n return self.agent_llm, None\n\n try:\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if not provider_info:\n msg = f\"Invalid model provider: {self.agent_llm}\"\n raise ValueError(msg)\n\n component_class = provider_info.get(\"component_class\")\n display_name = component_class.display_name\n inputs = provider_info.get(\"inputs\")\n prefix = provider_info.get(\"prefix\", \"\")\n\n return self._build_llm_model(component_class, inputs, prefix), display_name\n\n except (AttributeError, ValueError, TypeError, RuntimeError) as e:\n await logger.aerror(f\"Error building {self.agent_llm} language model: {e!s}\")\n msg = f\"Failed to initialize language model: {e!s}\"\n raise ValueError(msg) from e\n\n def _build_llm_model(self, component, inputs, prefix=\"\"):\n model_kwargs = {}\n for input_ in inputs:\n if hasattr(self, f\"{prefix}{input_.name}\"):\n model_kwargs[input_.name] = getattr(self, f\"{prefix}{input_.name}\")\n return component.set(**model_kwargs).build_model()\n\n def set_component_params(self, component):\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if provider_info:\n inputs = provider_info.get(\"inputs\")\n prefix = provider_info.get(\"prefix\")\n # Filter out json_mode and only use attributes that exist on this component\n model_kwargs = {}\n for input_ in inputs:\n if hasattr(self, f\"{prefix}{input_.name}\"):\n model_kwargs[input_.name] = getattr(self, f\"{prefix}{input_.name}\")\n\n return component.set(**model_kwargs)\n return component\n\n def delete_fields(self, build_config: dotdict, fields: dict | list[str]) -> None:\n \"\"\"Delete specified fields from build_config.\"\"\"\n for field in fields:\n build_config.pop(field, None)\n\n def update_input_types(self, build_config: dotdict) -> dotdict:\n \"\"\"Update input types for all fields in build_config.\"\"\"\n for key, value in build_config.items():\n if isinstance(value, dict):\n if value.get(\"input_types\") is None:\n build_config[key][\"input_types\"] = []\n elif hasattr(value, \"input_types\") and value.input_types is None:\n value.input_types = []\n return build_config\n\n async def update_build_config(\n self, build_config: dotdict, field_value: str, field_name: str | None = None\n ) -> dotdict:\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n # Existing logic for updating build_config\n if field_name in (\"agent_llm\",):\n build_config[\"agent_llm\"][\"value\"] = field_value\n provider_info = MODEL_PROVIDERS_DICT.get(field_value)\n if provider_info:\n component_class = provider_info.get(\"component_class\")\n if component_class and hasattr(component_class, \"update_build_config\"):\n # Call the component class's update_build_config method\n build_config = await update_component_build_config(\n component_class, build_config, field_value, \"model_name\"\n )\n\n provider_configs: dict[str, tuple[dict, list[dict]]] = {\n provider: (\n MODEL_PROVIDERS_DICT[provider][\"fields\"],\n [\n MODEL_PROVIDERS_DICT[other_provider][\"fields\"]\n for other_provider in MODEL_PROVIDERS_DICT\n if other_provider != provider\n ],\n )\n for provider in MODEL_PROVIDERS_DICT\n }\n if field_value in provider_configs:\n fields_to_add, fields_to_delete = provider_configs[field_value]\n\n # Delete fields from other providers\n for fields in fields_to_delete:\n self.delete_fields(build_config, fields)\n\n # Add provider-specific fields\n if field_value == \"OpenAI\" and not any(field in build_config for field in fields_to_add):\n build_config.update(fields_to_add)\n else:\n build_config.update(fields_to_add)\n # Reset input types for agent_llm\n build_config[\"agent_llm\"][\"input_types\"] = []\n build_config[\"agent_llm\"][\"display_name\"] = \"Model Provider\"\n elif field_value == \"connect_other_models\":\n # Delete all provider fields\n self.delete_fields(build_config, ALL_PROVIDER_FIELDS)\n # # Update with custom component\n custom_component = DropdownInput(\n name=\"agent_llm\",\n display_name=\"Language Model\",\n info=\"The provider of the language model that the agent will use to generate responses.\",\n options=[*MODEL_PROVIDERS_LIST],\n real_time_refresh=True,\n refresh_button=False,\n input_types=[\"LanguageModel\"],\n placeholder=\"Awaiting model input.\",\n options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST if key in MODELS_METADATA],\n external_options={\n \"fields\": {\n \"data\": {\n \"node\": {\n \"name\": \"connect_other_models\",\n \"display_name\": \"Connect other models\",\n \"icon\": \"CornerDownLeft\",\n },\n }\n },\n },\n )\n build_config.update({\"agent_llm\": custom_component.to_dict()})\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"agent_llm\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\n ]\n missing_keys = [key for key in default_keys if key not in build_config]\n if missing_keys:\n msg = f\"Missing required keys in build_config: {missing_keys}\"\n raise ValueError(msg)\n if (\n isinstance(self.agent_llm, str)\n and self.agent_llm in MODEL_PROVIDERS_DICT\n and field_name in MODEL_DYNAMIC_UPDATE_FIELDS\n ):\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if provider_info:\n component_class = provider_info.get(\"component_class\")\n component_class = self.set_component_params(component_class)\n prefix = provider_info.get(\"prefix\")\n if component_class and hasattr(component_class, \"update_build_config\"):\n # Call each component class's update_build_config method\n # remove the prefix from the field_name\n if isinstance(field_name, str) and isinstance(prefix, str):\n field_name = field_name.replace(prefix, \"\")\n build_config = await update_component_build_config(\n component_class, build_config, field_value, \"model_name\"\n )\n return dotdict({k: v.to_dict() if hasattr(v, \"to_dict\") else v for k, v in build_config.items()})\n\n async def _get_tools(self) -> list[Tool]:\n component_toolkit = get_component_toolkit()\n tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\n callbacks=self.get_langchain_callbacks(),\n )\n if hasattr(self, \"tools_metadata\"):\n tools = component_toolkit(component=self, metadata=self.tools_metadata).update_tools_metadata(tools=tools)\n return tools\n" | ||
| "value": "import json\nimport re\n\nfrom langchain_core.tools import StructuredTool, Tool\nfrom pydantic import ValidationError\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.model_input_constants import (\n ALL_PROVIDER_FIELDS,\n MODEL_DYNAMIC_UPDATE_FIELDS,\n MODEL_PROVIDERS_DICT,\n MODEL_PROVIDERS_LIST,\n MODELS_METADATA,\n)\nfrom lfx.base.models.model_utils import get_model_name\nfrom lfx.components.helpers.current_date import CurrentDateComponent\nfrom lfx.components.helpers.memory import MemoryComponent\nfrom lfx.components.langchain_utilities.tool_calling import ToolCallingAgentComponent\nfrom lfx.custom.custom_component.component import get_component_toolkit\nfrom lfx.custom.utils import update_component_build_config\nfrom lfx.helpers.base_model import build_model_from_schema\nfrom lfx.inputs.inputs import BoolInput, SecretStrInput, StrInput\nfrom lfx.io import DropdownInput, IntInput, MessageTextInput, MultilineInput, Output, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(ToolCallingAgentComponent):\n display_name: str = \"Agent\"\n description: str = \"Define the agent's instructions, then enter a task to complete using tools.\"\n documentation: str = \"https://docs.langflow.org/agents\"\n icon = \"bot\"\n beta = False\n name = \"Agent\"\n\n memory_inputs = [set_advanced_true(component_input) for component_input in MemoryComponent().inputs]\n\n # Filter out json_mode from OpenAI inputs since we handle structured output differently\n if \"OpenAI\" in MODEL_PROVIDERS_DICT:\n openai_inputs_filtered = [\n input_field\n for input_field in MODEL_PROVIDERS_DICT[\"OpenAI\"][\"inputs\"]\n if not (hasattr(input_field, \"name\") and input_field.name == \"json_mode\")\n ]\n if \"IBM watsonx.ai\" in MODEL_PROVIDERS_DICT:\n watsonx_inputs_filtered = list(MODEL_PROVIDERS_DICT[\"IBM watsonx.ai\"][\"inputs\"])\n print(watsonx_inputs_filtered)\n else:\n openai_inputs_filtered = []\n\n inputs = [\n DropdownInput(\n name=\"agent_llm\",\n display_name=\"Model Provider\",\n info=\"The provider of the language model that the agent will use to generate responses.\",\n options=[*MODEL_PROVIDERS_LIST],\n value=\"OpenAI\",\n real_time_refresh=True,\n refresh_button=False,\n input_types=[],\n options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST if key in MODELS_METADATA],\n external_options={\n \"fields\": {\n \"data\": {\n \"node\": {\n \"name\": \"connect_other_models\",\n \"display_name\": \"Connect other models\",\n \"icon\": \"CornerDownLeft\",\n }\n }\n },\n },\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"The API key to use for the model.\",\n required=True,\n ),\n StrInput(\n name=\"base_url\",\n display_name=\"Base URL\",\n info=\"The base URL of the API.\",\n required=True,\n show=False,\n ),\n StrInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"The project ID of the model.\",\n required=True,\n show=False,\n ),\n IntInput(\n name=\"max_output_tokens\", display_name=\"Max Output Tokens\", info=\"The maximum number of tokens to generate.\",\n show=False,\n ),\n *openai_inputs_filtered,\n # *watsonx_inputs_filtered,\n MultilineInput(\n name=\"system_prompt\",\n display_name=\"Agent Instructions\",\n info=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\n advanced=False,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Chat History Messages\",\n value=100,\n info=\"Number of chat history messages to retrieve.\",\n advanced=True,\n show=True,\n ),\n MultilineInput(\n name=\"format_instructions\",\n display_name=\"Output Format Instructions\",\n info=\"Generic Template for structured output formatting. Valid only with Structured response.\",\n value=(\n \"You are an AI that extracts structured JSON objects from unstructured text. \"\n \"Use a predefined schema with expected types (str, int, float, bool, dict). \"\n \"Extract ALL relevant instances that match the schema - if multiple patterns exist, capture them all. \"\n \"Fill missing or ambiguous values with defaults: null for missing values. \"\n \"Remove exact duplicates but keep variations that have different field values. \"\n \"Always return valid JSON in the expected format, never throw errors. \"\n \"If multiple objects can be extracted, return them all in the structured format.\"\n ),\n advanced=True,\n ),\n TableInput(\n name=\"output_schema\",\n display_name=\"Output Schema\",\n info=(\n \"Schema Validation: Define the structure and data types for structured output. \"\n \"No validation if no output schema.\"\n ),\n advanced=True,\n required=False,\n value=[],\n table_schema=[\n {\n \"name\": \"name\",\n \"display_name\": \"Name\",\n \"type\": \"str\",\n \"description\": \"Specify the name of the output field.\",\n \"default\": \"field\",\n \"edit_mode\": EditMode.INLINE,\n },\n {\n \"name\": \"description\",\n \"display_name\": \"Description\",\n \"type\": \"str\",\n \"description\": \"Describe the purpose of the output field.\",\n \"default\": \"description of field\",\n \"edit_mode\": EditMode.POPOVER,\n },\n {\n \"name\": \"type\",\n \"display_name\": \"Type\",\n \"type\": \"str\",\n \"edit_mode\": EditMode.INLINE,\n \"description\": (\"Indicate the data type of the output field (e.g., str, int, float, bool, dict).\"),\n \"options\": [\"str\", \"int\", \"float\", \"bool\", \"dict\"],\n \"default\": \"str\",\n },\n {\n \"name\": \"multiple\",\n \"display_name\": \"As List\",\n \"type\": \"boolean\",\n \"description\": \"Set to True if this output field should be a list of the specified type.\",\n \"default\": \"False\",\n \"edit_mode\": EditMode.INLINE,\n },\n ],\n ),\n *LCToolsAgentComponent._base_inputs,\n # removed memory inputs from agent component\n # *memory_inputs,\n BoolInput(\n name=\"add_current_date_tool\",\n display_name=\"Current Date\",\n advanced=True,\n info=\"If true, will add a tool to the agent that returns the current date.\",\n value=True,\n ),\n ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\n ]\n\n async def get_agent_requirements(self):\n \"\"\"Get the agent requirements for the agent.\"\"\"\n llm_model, display_name = await self.get_llm()\n if llm_model is None:\n msg = \"No language model selected. Please choose a model to proceed.\"\n raise ValueError(msg)\n self.model_name = get_model_name(llm_model, display_name=display_name)\n\n # Get memory data\n self.chat_history = await self.get_memory_data()\n await logger.adebug(f\"Retrieved {len(self.chat_history)} chat history messages\")\n if isinstance(self.chat_history, Message):\n self.chat_history = [self.chat_history]\n\n # Add current date tool if enabled\n if self.add_current_date_tool:\n if not isinstance(self.tools, list): # type: ignore[has-type]\n self.tools = []\n current_date_tool = (await CurrentDateComponent(**self.get_base_args()).to_toolkit()).pop(0)\n if not isinstance(current_date_tool, StructuredTool):\n msg = \"CurrentDateComponent must be converted to a StructuredTool\"\n raise TypeError(msg)\n self.tools.append(current_date_tool)\n return llm_model, self.chat_history, self.tools\n\n async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=self.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components else \"\"\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\n )\n return [\n message for message in messages if getattr(message, \"id\", None) != getattr(self.input_value, \"id\", None)\n ]\n\n async def get_llm(self):\n if not isinstance(self.agent_llm, str):\n return self.agent_llm, None\n\n try:\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if not provider_info:\n msg = f\"Invalid model provider: {self.agent_llm}\"\n raise ValueError(msg)\n\n component_class = provider_info.get(\"component_class\")\n display_name = component_class.display_name\n inputs = provider_info.get(\"inputs\")\n prefix = provider_info.get(\"prefix\", \"\")\n\n return self._build_llm_model(component_class, inputs, prefix), display_name\n\n except (AttributeError, ValueError, TypeError, RuntimeError) as e:\n await logger.aerror(f\"Error building {self.agent_llm} language model: {e!s}\")\n msg = f\"Failed to initialize language model: {e!s}\"\n raise ValueError(msg) from e\n\n def _build_llm_model(self, component, inputs, prefix=\"\"):\n model_kwargs = {}\n for input_ in inputs:\n if hasattr(self, f\"{prefix}{input_.name}\"):\n model_kwargs[input_.name] = getattr(self, f\"{prefix}{input_.name}\")\n return component.set(**model_kwargs).build_model()\n\n def set_component_params(self, component):\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if provider_info:\n inputs = provider_info.get(\"inputs\")\n prefix = provider_info.get(\"prefix\")\n # Filter out json_mode and only use attributes that exist on this component\n model_kwargs = {}\n for input_ in inputs:\n if hasattr(self, f\"{prefix}{input_.name}\"):\n model_kwargs[input_.name] = getattr(self, f\"{prefix}{input_.name}\")\n\n return component.set(**model_kwargs)\n return component\n\n def delete_fields(self, build_config: dotdict, fields: dict | list[str]) -> None:\n \"\"\"Delete specified fields from build_config.\"\"\"\n for field in fields:\n if build_config is not None and field in build_config:\n build_config.pop(field, None)\n\n def update_input_types(self, build_config: dotdict) -> dotdict:\n \"\"\"Update input types for all fields in build_config.\"\"\"\n for key, value in build_config.items():\n if isinstance(value, dict):\n if value.get(\"input_types\") is None:\n build_config[key][\"input_types\"] = []\n elif hasattr(value, \"input_types\") and value.input_types is None:\n value.input_types = []\n return build_config\n\n async def update_build_config(\n self, build_config: dotdict, field_value: str, field_name: str | None = None\n ) -> dotdict:\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n # Existing logic for updating build_config\n if field_name in (\"agent_llm\",):\n build_config[\"agent_llm\"][\"value\"] = field_value\n provider_info = MODEL_PROVIDERS_DICT.get(field_value)\n if provider_info:\n component_class = provider_info.get(\"component_class\")\n if component_class and hasattr(component_class, \"update_build_config\"):\n # Call the component class's update_build_config method\n build_config = await update_component_build_config(\n component_class, build_config, field_value, \"model_name\"\n )\n\n provider_configs: dict[str, tuple[dict, list[dict]]] = {\n provider: (\n MODEL_PROVIDERS_DICT[provider][\"fields\"],\n [\n MODEL_PROVIDERS_DICT[other_provider][\"fields\"]\n for other_provider in MODEL_PROVIDERS_DICT\n if other_provider != provider\n ],\n )\n for provider in MODEL_PROVIDERS_DICT\n }\n if field_value in provider_configs:\n fields_to_add, fields_to_delete = provider_configs[field_value]\n\n # Delete fields from other providers\n for fields in fields_to_delete:\n self.delete_fields(build_config, fields)\n\n # Add provider-specific fields\n if field_value == \"OpenAI\" and not any(field in build_config for field in fields_to_add):\n build_config.update(fields_to_add)\n else:\n build_config.update(fields_to_add)\n # Reset input types for agent_llm\n build_config[\"agent_llm\"][\"input_types\"] = []\n build_config[\"agent_llm\"][\"display_name\"] = \"Model Provider\"\n elif field_value == \"connect_other_models\":\n # Delete all provider fields\n self.delete_fields(build_config, ALL_PROVIDER_FIELDS)\n # # Update with custom component\n custom_component = DropdownInput(\n name=\"agent_llm\",\n display_name=\"Language Model\",\n info=\"The provider of the language model that the agent will use to generate responses.\",\n options=[*MODEL_PROVIDERS_LIST],\n real_time_refresh=True,\n refresh_button=False,\n input_types=[\"LanguageModel\"],\n placeholder=\"Awaiting model input.\",\n options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST if key in MODELS_METADATA],\n external_options={\n \"fields\": {\n \"data\": {\n \"node\": {\n \"name\": \"connect_other_models\",\n \"display_name\": \"Connect other models\",\n \"icon\": \"CornerDownLeft\",\n },\n }\n },\n },\n )\n build_config.update({\"agent_llm\": custom_component.to_dict()})\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"agent_llm\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\n ]\n missing_keys = [key for key in default_keys if key not in build_config]\n if missing_keys:\n msg = f\"Missing required keys in build_config: {missing_keys}\"\n raise ValueError(msg)\n if (\n isinstance(self.agent_llm, str)\n and self.agent_llm in MODEL_PROVIDERS_DICT\n and field_name in MODEL_DYNAMIC_UPDATE_FIELDS\n ):\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if provider_info:\n component_class = provider_info.get(\"component_class\")\n component_class = self.set_component_params(component_class)\n prefix = provider_info.get(\"prefix\")\n if component_class and hasattr(component_class, \"update_build_config\"):\n # Call each component class's update_build_config method\n # remove the prefix from the field_name\n if isinstance(field_name, str) and isinstance(prefix, str):\n field_name = field_name.replace(prefix, \"\")\n build_config = await update_component_build_config(\n component_class, build_config, field_value, \"model_name\"\n )\n return dotdict({k: v.to_dict() if hasattr(v, \"to_dict\") else v for k, v in build_config.items()})\n\n async def _get_tools(self) -> list[Tool]:\n component_toolkit = get_component_toolkit()\n tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\n callbacks=self.get_langchain_callbacks(),\n )\n if hasattr(self, \"tools_metadata\"):\n tools = component_toolkit(component=self, metadata=self.tools_metadata).update_tools_metadata(tools=tools)\n return tools\n" |
There was a problem hiding this comment.
Remove debug print statement from embedded code.
Line 2172 contains a debug print(watsonx_inputs_filtered) statement in the component code. This should be removed before release.
Apply this diff:
if \"IBM watsonx.ai\" in MODEL_PROVIDERS_DICT:
watsonx_inputs_filtered = list(MODEL_PROVIDERS_DICT[\"IBM watsonx.ai\"][\"inputs\"])
- print(watsonx_inputs_filtered)
else:
openai_inputs_filtered = []🤖 Prompt for AI Agents
src/backend/base/langflow/initial_setup/starter_projects/Instagram
Copywriter.json around line 2172: the embedded Python code contains a stray
debug print statement print(watsonx_inputs_filtered) which should be removed;
edit the file to delete that print call (ensuring surrounding logic and variable
assignment remain intact) so no debug output is emitted from the component.
| "base_url": { | ||
| "_input_type": "StrInput", | ||
| "advanced": false, | ||
| "display_name": "Base URL", | ||
| "dynamic": false, | ||
| "info": "The base URL of the API.", | ||
| "list": false, | ||
| "list_add_label": "Add More", | ||
| "load_from_db": false, | ||
| "name": "base_url", | ||
| "placeholder": "", | ||
| "required": true, | ||
| "show": false, | ||
| "title_case": false, | ||
| "tool_mode": false, | ||
| "trace_as_metadata": true, | ||
| "type": "str", | ||
| "value": "" | ||
| }, |
There was a problem hiding this comment.
Hidden + required field will block runs
base_url is required:true but show:false and default:"". This can fail validation when provider ≠ IBM/Ollama.
Recommended change (make non-required at base level; provider components will enforce when selected):
"base_url": {
- "required": true,
+ "required": false,
"show": false,
"value": ""
}📝 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.
| "base_url": { | |
| "_input_type": "StrInput", | |
| "advanced": false, | |
| "display_name": "Base URL", | |
| "dynamic": false, | |
| "info": "The base URL of the API.", | |
| "list": false, | |
| "list_add_label": "Add More", | |
| "load_from_db": false, | |
| "name": "base_url", | |
| "placeholder": "", | |
| "required": true, | |
| "show": false, | |
| "title_case": false, | |
| "tool_mode": false, | |
| "trace_as_metadata": true, | |
| "type": "str", | |
| "value": "" | |
| }, | |
| "base_url": { | |
| "_input_type": "StrInput", | |
| "advanced": false, | |
| "display_name": "Base URL", | |
| "dynamic": false, | |
| "info": "The base URL of the API.", | |
| "list": false, | |
| "list_add_label": "Add More", | |
| "load_from_db": false, | |
| "name": "base_url", | |
| "placeholder": "", | |
| "required": false, | |
| "show": false, | |
| "title_case": false, | |
| "tool_mode": false, | |
| "trace_as_metadata": true, | |
| "type": "str", | |
| "value": "" | |
| }, |
🤖 Prompt for AI Agents
In src/backend/base/langflow/initial_setup/starter_projects/Invoice
Summarizer.json around lines 1309 to 1327, the base_url field is marked
required:true while show:false and value:"", which will block runs when
providers other than IBM/Ollama are used; change the base-level field to
required:false (so it no longer enforces validation globally) and keep
provider-specific components responsible for marking base_url required when
those providers are selected.
| "value": "import json\nimport re\n\nfrom langchain_core.tools import StructuredTool, Tool\nfrom pydantic import ValidationError\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.model_input_constants import (\n ALL_PROVIDER_FIELDS,\n MODEL_DYNAMIC_UPDATE_FIELDS,\n MODEL_PROVIDERS_DICT,\n MODEL_PROVIDERS_LIST,\n MODELS_METADATA,\n)\nfrom lfx.base.models.model_utils import get_model_name\nfrom lfx.components.helpers.current_date import CurrentDateComponent\nfrom lfx.components.helpers.memory import MemoryComponent\nfrom lfx.components.langchain_utilities.tool_calling import ToolCallingAgentComponent\nfrom lfx.custom.custom_component.component import get_component_toolkit\nfrom lfx.custom.utils import update_component_build_config\nfrom lfx.helpers.base_model import build_model_from_schema\nfrom lfx.inputs.inputs import BoolInput, SecretStrInput, StrInput\nfrom lfx.io import DropdownInput, IntInput, MessageTextInput, MultilineInput, Output, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(ToolCallingAgentComponent):\n display_name: str = \"Agent\"\n description: str = \"Define the agent's instructions, then enter a task to complete using tools.\"\n documentation: str = \"https://docs.langflow.org/agents\"\n icon = \"bot\"\n beta = False\n name = \"Agent\"\n\n memory_inputs = [set_advanced_true(component_input) for component_input in MemoryComponent().inputs]\n\n # Filter out json_mode from OpenAI inputs since we handle structured output differently\n if \"OpenAI\" in MODEL_PROVIDERS_DICT:\n openai_inputs_filtered = [\n input_field\n for input_field in MODEL_PROVIDERS_DICT[\"OpenAI\"][\"inputs\"]\n if not (hasattr(input_field, \"name\") and input_field.name == \"json_mode\")\n ]\n if \"IBM watsonx.ai\" in MODEL_PROVIDERS_DICT:\n watsonx_inputs_filtered = list(MODEL_PROVIDERS_DICT[\"IBM watsonx.ai\"][\"inputs\"])\n print(watsonx_inputs_filtered)\n else:\n openai_inputs_filtered = []\n\n inputs = [\n DropdownInput(\n name=\"agent_llm\",\n display_name=\"Model Provider\",\n info=\"The provider of the language model that the agent will use to generate responses.\",\n options=[*MODEL_PROVIDERS_LIST],\n value=\"OpenAI\",\n real_time_refresh=True,\n refresh_button=False,\n input_types=[],\n options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST if key in MODELS_METADATA],\n external_options={\n \"fields\": {\n \"data\": {\n \"node\": {\n \"name\": \"connect_other_models\",\n \"display_name\": \"Connect other models\",\n \"icon\": \"CornerDownLeft\",\n }\n }\n },\n },\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"The API key to use for the model.\",\n required=True,\n ),\n StrInput(\n name=\"base_url\",\n display_name=\"Base URL\",\n info=\"The base URL of the API.\",\n required=True,\n show=False,\n ),\n StrInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"The project ID of the model.\",\n required=True,\n show=False,\n ),\n IntInput(\n name=\"max_output_tokens\", display_name=\"Max Output Tokens\", info=\"The maximum number of tokens to generate.\",\n show=False,\n ),\n *openai_inputs_filtered,\n # *watsonx_inputs_filtered,\n MultilineInput(\n name=\"system_prompt\",\n display_name=\"Agent Instructions\",\n info=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\n advanced=False,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Chat History Messages\",\n value=100,\n info=\"Number of chat history messages to retrieve.\",\n advanced=True,\n show=True,\n ),\n MultilineInput(\n name=\"format_instructions\",\n display_name=\"Output Format Instructions\",\n info=\"Generic Template for structured output formatting. Valid only with Structured response.\",\n value=(\n \"You are an AI that extracts structured JSON objects from unstructured text. \"\n \"Use a predefined schema with expected types (str, int, float, bool, dict). \"\n \"Extract ALL relevant instances that match the schema - if multiple patterns exist, capture them all. \"\n \"Fill missing or ambiguous values with defaults: null for missing values. \"\n \"Remove exact duplicates but keep variations that have different field values. \"\n \"Always return valid JSON in the expected format, never throw errors. \"\n \"If multiple objects can be extracted, return them all in the structured format.\"\n ),\n advanced=True,\n ),\n TableInput(\n name=\"output_schema\",\n display_name=\"Output Schema\",\n info=(\n \"Schema Validation: Define the structure and data types for structured output. \"\n \"No validation if no output schema.\"\n ),\n advanced=True,\n required=False,\n value=[],\n table_schema=[\n {\n \"name\": \"name\",\n \"display_name\": \"Name\",\n \"type\": \"str\",\n \"description\": \"Specify the name of the output field.\",\n \"default\": \"field\",\n \"edit_mode\": EditMode.INLINE,\n },\n {\n \"name\": \"description\",\n \"display_name\": \"Description\",\n \"type\": \"str\",\n \"description\": \"Describe the purpose of the output field.\",\n \"default\": \"description of field\",\n \"edit_mode\": EditMode.POPOVER,\n },\n {\n \"name\": \"type\",\n \"display_name\": \"Type\",\n \"type\": \"str\",\n \"edit_mode\": EditMode.INLINE,\n \"description\": (\"Indicate the data type of the output field (e.g., str, int, float, bool, dict).\"),\n \"options\": [\"str\", \"int\", \"float\", \"bool\", \"dict\"],\n \"default\": \"str\",\n },\n {\n \"name\": \"multiple\",\n \"display_name\": \"As List\",\n \"type\": \"boolean\",\n \"description\": \"Set to True if this output field should be a list of the specified type.\",\n \"default\": \"False\",\n \"edit_mode\": EditMode.INLINE,\n },\n ],\n ),\n *LCToolsAgentComponent._base_inputs,\n # removed memory inputs from agent component\n # *memory_inputs,\n BoolInput(\n name=\"add_current_date_tool\",\n display_name=\"Current Date\",\n advanced=True,\n info=\"If true, will add a tool to the agent that returns the current date.\",\n value=True,\n ),\n ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\n ]\n\n async def get_agent_requirements(self):\n \"\"\"Get the agent requirements for the agent.\"\"\"\n llm_model, display_name = await self.get_llm()\n if llm_model is None:\n msg = \"No language model selected. Please choose a model to proceed.\"\n raise ValueError(msg)\n self.model_name = get_model_name(llm_model, display_name=display_name)\n\n # Get memory data\n self.chat_history = await self.get_memory_data()\n await logger.adebug(f\"Retrieved {len(self.chat_history)} chat history messages\")\n if isinstance(self.chat_history, Message):\n self.chat_history = [self.chat_history]\n\n # Add current date tool if enabled\n if self.add_current_date_tool:\n if not isinstance(self.tools, list): # type: ignore[has-type]\n self.tools = []\n current_date_tool = (await CurrentDateComponent(**self.get_base_args()).to_toolkit()).pop(0)\n if not isinstance(current_date_tool, StructuredTool):\n msg = \"CurrentDateComponent must be converted to a StructuredTool\"\n raise TypeError(msg)\n self.tools.append(current_date_tool)\n return llm_model, self.chat_history, self.tools\n\n async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=self.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components else \"\"\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\n )\n return [\n message for message in messages if getattr(message, \"id\", None) != getattr(self.input_value, \"id\", None)\n ]\n\n async def get_llm(self):\n if not isinstance(self.agent_llm, str):\n return self.agent_llm, None\n\n try:\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if not provider_info:\n msg = f\"Invalid model provider: {self.agent_llm}\"\n raise ValueError(msg)\n\n component_class = provider_info.get(\"component_class\")\n display_name = component_class.display_name\n inputs = provider_info.get(\"inputs\")\n prefix = provider_info.get(\"prefix\", \"\")\n\n return self._build_llm_model(component_class, inputs, prefix), display_name\n\n except (AttributeError, ValueError, TypeError, RuntimeError) as e:\n await logger.aerror(f\"Error building {self.agent_llm} language model: {e!s}\")\n msg = f\"Failed to initialize language model: {e!s}\"\n raise ValueError(msg) from e\n\n def _build_llm_model(self, component, inputs, prefix=\"\"):\n model_kwargs = {}\n for input_ in inputs:\n if hasattr(self, f\"{prefix}{input_.name}\"):\n model_kwargs[input_.name] = getattr(self, f\"{prefix}{input_.name}\")\n return component.set(**model_kwargs).build_model()\n\n def set_component_params(self, component):\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if provider_info:\n inputs = provider_info.get(\"inputs\")\n prefix = provider_info.get(\"prefix\")\n # Filter out json_mode and only use attributes that exist on this component\n model_kwargs = {}\n for input_ in inputs:\n if hasattr(self, f\"{prefix}{input_.name}\"):\n model_kwargs[input_.name] = getattr(self, f\"{prefix}{input_.name}\")\n\n return component.set(**model_kwargs)\n return component\n\n def delete_fields(self, build_config: dotdict, fields: dict | list[str]) -> None:\n \"\"\"Delete specified fields from build_config.\"\"\"\n for field in fields:\n if build_config is not None and field in build_config:\n build_config.pop(field, None)\n\n def update_input_types(self, build_config: dotdict) -> dotdict:\n \"\"\"Update input types for all fields in build_config.\"\"\"\n for key, value in build_config.items():\n if isinstance(value, dict):\n if value.get(\"input_types\") is None:\n build_config[key][\"input_types\"] = []\n elif hasattr(value, \"input_types\") and value.input_types is None:\n value.input_types = []\n return build_config\n\n async def update_build_config(\n self, build_config: dotdict, field_value: str, field_name: str | None = None\n ) -> dotdict:\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n # Existing logic for updating build_config\n if field_name in (\"agent_llm\",):\n build_config[\"agent_llm\"][\"value\"] = field_value\n provider_info = MODEL_PROVIDERS_DICT.get(field_value)\n if provider_info:\n component_class = provider_info.get(\"component_class\")\n if component_class and hasattr(component_class, \"update_build_config\"):\n # Call the component class's update_build_config method\n build_config = await update_component_build_config(\n component_class, build_config, field_value, \"model_name\"\n )\n\n provider_configs: dict[str, tuple[dict, list[dict]]] = {\n provider: (\n MODEL_PROVIDERS_DICT[provider][\"fields\"],\n [\n MODEL_PROVIDERS_DICT[other_provider][\"fields\"]\n for other_provider in MODEL_PROVIDERS_DICT\n if other_provider != provider\n ],\n )\n for provider in MODEL_PROVIDERS_DICT\n }\n if field_value in provider_configs:\n fields_to_add, fields_to_delete = provider_configs[field_value]\n\n # Delete fields from other providers\n for fields in fields_to_delete:\n self.delete_fields(build_config, fields)\n\n # Add provider-specific fields\n if field_value == \"OpenAI\" and not any(field in build_config for field in fields_to_add):\n build_config.update(fields_to_add)\n else:\n build_config.update(fields_to_add)\n # Reset input types for agent_llm\n build_config[\"agent_llm\"][\"input_types\"] = []\n build_config[\"agent_llm\"][\"display_name\"] = \"Model Provider\"\n elif field_value == \"connect_other_models\":\n # Delete all provider fields\n self.delete_fields(build_config, ALL_PROVIDER_FIELDS)\n # # Update with custom component\n custom_component = DropdownInput(\n name=\"agent_llm\",\n display_name=\"Language Model\",\n info=\"The provider of the language model that the agent will use to generate responses.\",\n options=[*MODEL_PROVIDERS_LIST],\n real_time_refresh=True,\n refresh_button=False,\n input_types=[\"LanguageModel\"],\n placeholder=\"Awaiting model input.\",\n options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST if key in MODELS_METADATA],\n external_options={\n \"fields\": {\n \"data\": {\n \"node\": {\n \"name\": \"connect_other_models\",\n \"display_name\": \"Connect other models\",\n \"icon\": \"CornerDownLeft\",\n },\n }\n },\n },\n )\n build_config.update({\"agent_llm\": custom_component.to_dict()})\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"agent_llm\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\n ]\n missing_keys = [key for key in default_keys if key not in build_config]\n if missing_keys:\n msg = f\"Missing required keys in build_config: {missing_keys}\"\n raise ValueError(msg)\n if (\n isinstance(self.agent_llm, str)\n and self.agent_llm in MODEL_PROVIDERS_DICT\n and field_name in MODEL_DYNAMIC_UPDATE_FIELDS\n ):\n provider_info = MODEL_PROVIDERS_DICT.get(self.agent_llm)\n if provider_info:\n component_class = provider_info.get(\"component_class\")\n component_class = self.set_component_params(component_class)\n prefix = provider_info.get(\"prefix\")\n if component_class and hasattr(component_class, \"update_build_config\"):\n # Call each component class's update_build_config method\n # remove the prefix from the field_name\n if isinstance(field_name, str) and isinstance(prefix, str):\n field_name = field_name.replace(prefix, \"\")\n build_config = await update_component_build_config(\n component_class, build_config, field_value, \"model_name\"\n )\n return dotdict({k: v.to_dict() if hasattr(v, \"to_dict\") else v for k, v in build_config.items()})\n\n async def _get_tools(self) -> list[Tool]:\n component_toolkit = get_component_toolkit()\n tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\n callbacks=self.get_langchain_callbacks(),\n )\n if hasattr(self, \"tools_metadata\"):\n tools = component_toolkit(component=self, metadata=self.tools_metadata).update_tools_metadata(tools=tools)\n return tools\n" | ||
| }, |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Agent code: stray print and logic bug wiping OpenAI inputs
- print(watsonx_inputs_filtered) is a debug artifact.
- Else branch tied to IBM check resets openai_inputs_filtered = [] and can remove OpenAI inputs unexpectedly.
Apply inside the code string:
- if "OpenAI" in MODEL_PROVIDERS_DICT:
- openai_inputs_filtered = [
- input_field
- for input_field in MODEL_PROVIDERS_DICT["OpenAI"]["inputs"]
- if not (hasattr(input_field, "name") and input_field.name == "json_mode")
- ]
- if "IBM watsonx.ai" in MODEL_PROVIDERS_DICT:
- watsonx_inputs_filtered = list(MODEL_PROVIDERS_DICT["IBM watsonx.ai"]["inputs"])
- print(watsonx_inputs_filtered)
- else:
- openai_inputs_filtered = []
+ openai_inputs_filtered = []
+ if "OpenAI" in MODEL_PROVIDERS_DICT:
+ openai_inputs_filtered = [
+ input_field
+ for input_field in MODEL_PROVIDERS_DICT["OpenAI"]["inputs"]
+ if not (hasattr(input_field, "name") and input_field.name == "json_mode")
+ ]
+ if "IBM watsonx.ai" in MODEL_PROVIDERS_DICT:
+ watsonx_inputs_filtered = list(MODEL_PROVIDERS_DICT["IBM watsonx.ai"]["inputs"])| "base_url": { | ||
| "_input_type": "StrInput", | ||
| "advanced": false, | ||
| "display_name": "Base URL", | ||
| "dynamic": false, | ||
| "info": "The base URL of the API.", | ||
| "list": false, | ||
| "list_add_label": "Add More", | ||
| "load_from_db": false, | ||
| "name": "base_url", | ||
| "placeholder": "", | ||
| "required": true, | ||
| "show": false, | ||
| "title_case": false, | ||
| "tool_mode": false, | ||
| "trace_as_metadata": true, | ||
| "type": "str", | ||
| "value": "" | ||
| }, |
There was a problem hiding this comment.
Required fields should not be hidden by default.
The base_url field is marked as required: true but show: false. This creates a UX issue—users cannot fulfill a required field that is hidden. If this field should only be required for certain providers, use conditional visibility in the update_build_config method or set required: false if optional.
Consider adjusting the configuration:
"base_url": {
"_input_type": "StrInput",
"advanced": false,
"display_name": "Base URL",
"dynamic": false,
"info": "The base URL of the API.",
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "base_url",
"placeholder": "",
- "required": true,
+ "required": false,
"show": false,🤖 Prompt for AI Agents
In src/backend/base/langflow/initial_setup/starter_projects/Market Research.json
around lines 1708 to 1726, the base_url property is marked required: true but
show: false, which prevents users from entering a required value; change this by
either setting "show": true so the field is visible when required, or set
"required": false if it is truly optional, or implement conditional visibility
in update_build_config so base_url is only required (required: true and show:
true) for providers that need it and hidden/optional for others—apply one of
these fixes and keep metadata (display_name/info) consistent.
| SecretStrInput( | ||
| name="api_key", | ||
| display_name="API Key", | ||
| info="The API key to use for the model.", | ||
| required=True, | ||
| ), | ||
| StrInput( | ||
| name="base_url", | ||
| display_name="Base URL", | ||
| info="The base URL of the API.", | ||
| required=True, | ||
| show=False, | ||
| ), | ||
| StrInput( | ||
| name="project_id", | ||
| display_name="Project ID", | ||
| info="The project ID of the model.", | ||
| required=True, | ||
| show=False, | ||
| ), | ||
| IntInput( | ||
| name="max_output_tokens", display_name="Max Output Tokens", info="The maximum number of tokens to generate.", | ||
| show=False, | ||
| ), |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Implement provider-specific conditional visibility for new fields.
The new fields base_url, project_id, and max_output_tokens are added as universal inputs, but they should be conditionally shown/required based on the selected provider. For example:
base_urlandproject_idare specific to IBM watsonx.aibase_urlmay also be relevant for Ollamamax_output_tokensis provider-specific
The update_build_config method should dynamically show/hide and mark these fields as required based on the selected provider. Consider this pattern:
async def update_build_config(self, build_config: dotdict, field_value: str, field_name: str | None = None) -> dotdict:
if field_name == "agent_llm":
# Hide all provider-specific fields by default
build_config["base_url"]["show"] = False
build_config["base_url"]["required"] = False
build_config["project_id"]["show"] = False
build_config["project_id"]["required"] = False
build_config["max_output_tokens"]["show"] = False
# Show/require fields based on selected provider
if field_value == "IBM watsonx.ai":
build_config["base_url"]["show"] = True
build_config["base_url"]["required"] = True
build_config["project_id"]["show"] = True
build_config["project_id"]["required"] = True
build_config["max_output_tokens"]["show"] = True
elif field_value == "Ollama":
build_config["base_url"]["show"] = True
# Ollama has default, so not required
build_config["base_url"]["required"] = False
# ... rest of the methodAlso update the initial field definitions to have required=False:
StrInput(
name="base_url",
display_name="Base URL",
info="The base URL of the API.",
- required=True,
+ required=False,
show=False,
),
StrInput(
name="project_id",
display_name="Project ID",
info="The project ID of the model.",
- required=True,
+ required=False,
show=False,
),🤖 Prompt for AI Agents
In src/lfx/src/lfx/components/agents/agent.py around lines 83 to 106, the new
inputs base_url, project_id, and max_output_tokens are always visible/required
but must be provider-specific; change their initial definitions to
required=False, and update the update_build_config method to, when field_name ==
"agent_llm", default-hide and mark not required for these fields then
conditionally set show=True and required=True as appropriate for each provider
(e.g., IBM watsonx.ai: show+require base_url and project_id and show
max_output_tokens; Ollama: show base_url but keep required=False), leaving other
providers hidden.
There was a problem hiding this comment.
why required=False?
There was a problem hiding this comment.
@edwinjosechittilappilly Good question! The reason for required=False as the initial/default value is because these fields are provider-specific:
base_urlandproject_idare only needed for IBM watsonx.ai (andbase_urlfor Ollama)max_output_tokensis only relevant for certain providers
If we keep required=True as the default, then when a user selects OpenAI or Anthropic (which don't need these fields), they would still be marked as required and the validation would force users to provide values even though those providers don't use them.
The pattern is:
- Start with
required=False(universal safe default for all providers) - In
update_build_config, whenfield_name == "agent_llm", dynamically setrequired=Truefor the specific providers that actually need those fields
This way:
- IBM watsonx.ai selected →
base_urlandproject_idbecomerequired=Trueandshow=True - Ollama selected →
base_urlbecomesshow=Truebut staysrequired=False(has a default) - OpenAI selected → these fields stay
required=Falseandshow=False(hidden and not validated)
This ensures proper validation only when the fields are relevant to the selected provider! 🐰
| show=False, | ||
| ), | ||
| *openai_inputs_filtered, | ||
| # *watsonx_inputs_filtered, |
There was a problem hiding this comment.
Clarify incomplete Watsonx integration.
The Watsonx inputs are filtered (lines 54-56) but never used in the inputs list (line 108 is commented out). This suggests incomplete implementation.
Either:
- Complete the Watsonx integration by uncommenting this line and ensuring proper provider-specific field handling, or
- Remove the unused filtering code (lines 54-56) and add a TODO comment for future implementation, or
- Document why this is prepared but not activated
*openai_inputs_filtered,
- # *watsonx_inputs_filtered,
+ # TODO: Watsonx inputs integration pending provider-specific field handling
MultilineInput(📝 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.
| # *watsonx_inputs_filtered, | |
| *openai_inputs_filtered, | |
| # TODO: Watsonx inputs integration pending provider-specific field handling | |
| MultilineInput( |
🤖 Prompt for AI Agents
In src/lfx/src/lfx/components/agents/agent.py around line 108 (and referencing
the filtering at lines 54-56), the watsonx_inputs are filtered but never used
(the inclusion at line 108 is commented out), indicating incomplete integration;
fix by either (A) re-enable watsonx inputs by uncommenting that line and ensure
provider-specific fields from the filtered watsonx_inputs are correctly mapped
into the inputs list and validated for watsonx expectations, or (B) remove the
unused filtering code at lines 54-56 and add a clear TODO comment at line 108
noting that watsonx support is pending, or (C) leave the filtering but add a
concise explanatory comment at both locations documenting why watsonx inputs are
prepared but intentionally not included, then run tests to ensure no
unused-variable or lint warnings remain.
| name="base_url", | ||
| display_name="watsonx API Endpoint", | ||
| info="The base URL of the API.", | ||
| value=None, | ||
| options=[ | ||
| "https://us-south.ml.cloud.ibm.com", | ||
| "https://eu-de.ml.cloud.ibm.com", | ||
| "https://eu-gb.ml.cloud.ibm.com", | ||
| "https://au-syd.ml.cloud.ibm.com", | ||
| "https://jp-tok.ml.cloud.ibm.com", | ||
| "https://ca-tor.ml.cloud.ibm.com", | ||
| ], | ||
| value=[], | ||
| options=_urls, | ||
| real_time_refresh=True, | ||
| required=True, | ||
| ), |
There was a problem hiding this comment.
base_url type/value mismatch (list vs str) and UX
Dropdown value=[] will pass a list into build_model; ChatWatsonx.url expects a string. Also no default selected.
Fix:
DropdownInput(
- name="base_url",
+ name="base_url",
display_name="watsonx API Endpoint",
info="The base URL of the API.",
- value=[],
- options=_urls,
+ value=_urls[0],
+ options=_urls,
+ combobox=True,
real_time_refresh=True,
required=True,
),📝 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.
| name="base_url", | |
| display_name="watsonx API Endpoint", | |
| info="The base URL of the API.", | |
| value=None, | |
| options=[ | |
| "https://us-south.ml.cloud.ibm.com", | |
| "https://eu-de.ml.cloud.ibm.com", | |
| "https://eu-gb.ml.cloud.ibm.com", | |
| "https://au-syd.ml.cloud.ibm.com", | |
| "https://jp-tok.ml.cloud.ibm.com", | |
| "https://ca-tor.ml.cloud.ibm.com", | |
| ], | |
| value=[], | |
| options=_urls, | |
| real_time_refresh=True, | |
| required=True, | |
| ), | |
| name="base_url", | |
| display_name="watsonx API Endpoint", | |
| info="The base URL of the API.", | |
| value=_urls[0], | |
| options=_urls, | |
| combobox=True, | |
| real_time_refresh=True, | |
| required=True, | |
| ), |
🤖 Prompt for AI Agents
In src/lfx/src/lfx/components/ibm/watsonx.py around lines 35 to 42, the
"base_url" field sets value=[] (a list) but ChatWatsonx.url expects a string and
there is no default selected; change the field to provide a string value that
matches the options (e.g., set value to the first entry of _urls or an empty
string ""), ensuring the value type is str and not a list, and confirm _urls
contains string URLs so the dropdown passes a single string into build_model.
There was a problem hiding this comment.
By Default I do not need any values
There was a problem hiding this comment.
@edwinjosechittilappilly, understood! If you don't want a pre-selected default, that's perfectly fine since the field is marked required=True and users will need to select a value before building.
One small note: for a dropdown with no default, the value should typically be None or "" (empty string) rather than [] (empty list) to maintain type consistency, since self.base_url will eventually be passed as a string to ChatWatsonx(url=self.base_url, ...).
If the current implementation with value=[] works correctly in your validation flow, then no changes are needed. Just wanted to flag the type consideration in case it causes any unexpected behavior.
| if field_name == "base_url" and field_value: | ||
| try: | ||
| models = self.fetch_models(base_url=build_config.url.value) | ||
| build_config.model_name.options = models | ||
| if build_config.model_name.value: | ||
| build_config.model_name.value = models[0] | ||
| info_message = f"Updated model options: {len(models)} models found in {build_config.url.value}" | ||
| models = self.fetch_models(base_url=field_value) | ||
| build_config["model_name"]["options"] = models | ||
| if build_config["model_name"]["value"]: | ||
| build_config["model_name"]["value"] = models[0] | ||
| info_message = f"Updated model options: {len(models)} models found in {field_value}" | ||
| logger.info(info_message) | ||
| except Exception: # noqa: BLE001 | ||
| logger.exception("Error updating model options.") | ||
| if field_name == "model_name" and field_value and field_value in WatsonxAIComponent._urls: | ||
| build_config["model_name"]["options"] = self.fetch_models(base_url=field_value) | ||
| build_config["model_name"]["value"] = "" | ||
| return build_config |
There was a problem hiding this comment.
update_build_config: wrong condition and defaulting
- Ties model_name updates to field_name=="model_name" and checks value in _urls; should key off base_url.
- Defaulting only when value is truthy skips setting a valid default.
Apply:
- if field_name == "base_url" and field_value:
+ if field_name == "base_url" and field_value:
try:
models = self.fetch_models(base_url=field_value)
- build_config["model_name"]["options"] = models
- if build_config["model_name"]["value"]:
- build_config["model_name"]["value"] = models[0]
+ build_config["model_name"]["options"] = models
+ current = build_config["model_name"].get("value")
+ build_config["model_name"]["value"] = models[0] if not current or current not in models else current
info_message = f"Updated model options: {len(models)} models found in {field_value}"
logger.info(info_message)
except Exception: # noqa: BLE001
logger.exception("Error updating model options.")
- if field_name == "model_name" and field_value and field_value in WatsonxAIComponent._urls:
- build_config["model_name"]["options"] = self.fetch_models(base_url=field_value)
- build_config["model_name"]["value"] = ""
+ # No-op for model_name here; refresh is driven by base_url changes.
return build_config🤖 Prompt for AI Agents
In src/lfx/src/lfx/components/ibm/watsonx.py around lines 160 to 173, the second
conditional incorrectly keys off field_name == "model_name" and only sets a
default when the incoming value is truthy; change that conditional to check the
base_url (e.g., if field_name == "base_url" and the base_url value exists in
WatsonxAIComponent._urls), fetch model options for that base_url and assign them
to build_config["model_name"]["options"], and always set a sensible default for
build_config["model_name"]["value"] (preferably models[0] when models exist, or
"" if none) instead of only setting it when a truthy value is provided. Ensure
you reuse/avoid duplicate fetches if the earlier base_url branch already covered
this, and keep exception handling around the fetch as in the first block.
There was a problem hiding this comment.
I tried the earlier approach but this seems to work with agents well.
There was a problem hiding this comment.
@edwinjosechittilappilly, thank you for the clarification! I appreciate that you've tested this approach and confirmed it works well with agents. Since you have practical experience showing this implementation is effective, that's what matters most.
The second conditional (checking field_name == "model_name" with field_value in _urls) is a bit non-obvious at first glance, so you might consider adding a brief comment explaining the scenario it handles (e.g., "Handle refresh when model_name receives a URL value from the refresh button" or similar). But that's entirely optional—if it's working as expected, you're good to go!
Feel free to resolve this thread when you're ready. 🐰
… into lf-agents-ibm
… into lf-agents-ibm
Updated type hints for provider_name parameters to use the 'str | None' syntax instead of 'str = None' for better clarity and compliance with modern Python standards. Also added a docstring line for provider_name in process_inputs.
Deleted a commented-out line referencing watsonx_inputs_filtered in AgentComponent to clean up the code.
Expanded agent configuration to support IBM watsonx.ai and Ollama as model providers. Added new input fields for base_url, project_id, and max_output_tokens to agent components in starter projects. Updated code logic to handle these new fields and providers for improved flexibility and integration.
* Refactor knowledge base path initialization Replaces direct settings access with a lazy-loading function for the knowledge bases root path in Knowledge Ingestion and Knowledge Retrieval starter projects. This improves reliability and consistency when accessing the knowledge base directory, and updates all usages to the new helper function. * Update component_index.json * Add IBM watsonx.ai support to starter projects Introduces IBM watsonx.ai as a selectable model provider in multiple starter project JSONs. Adds new input fields for 'base_url', 'project_id', and 'max_output_tokens' to support IBM watsonx.ai integration. Updates agent component code to handle new provider and its required parameters. * Add Ollama to supported LLM providers in starter projects Ollama has been added as a supported provider alongside Anthropic, Google Generative AI, OpenAI, and IBM watsonx.ai in all starter project JSON files. This expands the available options for LLM integration in initial setup templates. * Update Ollama model input constants and logic Refactored model_input_constants.py to update the OLLAMA_MODEL_INPUTS and OLLAMA_MODEL_INPUTS_MAP. Modified ollama.py to use the new input mapping and improved input handling for Ollama components. * Add Notion integration components Added Notion-related components to the component index, including AddContentToPage, NotionDatabaseProperties, NotionListPages, NotionPageContent, NotionPageCreator, NotionPageUpdate, and NotionSearch. These components enable interaction with Notion databases and pages, supporting operations such as querying, creating, updating, and retrieving content. * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) * Update agent code and code_hash in starter projects Refactored the AgentComponent code in multiple starter project JSON files to remove IBM watsonx.ai-specific logic and ensure consistent OpenAI input filtering. Updated the code_hash metadata to reflect the new code version. * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * Update component_index.json * Use PEP 604 union syntax for provider_name type hints Updated type hints for provider_name parameters to use the 'str | None' syntax instead of 'str = None' for better clarity and compliance with modern Python standards. Also added a docstring line for provider_name in process_inputs. * Remove commented watsonx_inputs_filtered line Deleted a commented-out line referencing watsonx_inputs_filtered in AgentComponent to clean up the code. * Update component_index.json * Add support for new LLM providers and agent config fields Expanded agent configuration to support IBM watsonx.ai and Ollama as model providers. Added new input fields for base_url, project_id, and max_output_tokens to agent components in starter projects. Updated code logic to handle these new fields and providers for improved flexibility and integration. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This pull request updates the
Instagram Copywriter.jsonstarter project to expand support for additional AI providers and improve configuration options. The most significant changes include the addition of new provider options and a new API base URL setting.Provider support enhancements:
IBM watsonx.aiandOllamato the list of available provider options, allowing users to select these new AI services in addition to existing ones.Configuration improvements:
base_urlfield to the configuration, enabling users to specify the API base URL for their selected provider. This makes the setup more flexible and adaptable to different environments.Metadata update:
code_hashvalue in the metadata to reflect the new changes in the configuration.Summary by CodeRabbit