From 033ce41baf495f2ecb79db855baff9f03613ca8a Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Mon, 15 Jun 2026 12:00:19 -0700 Subject: [PATCH] fix: guard CUGA CodeAgent local execution (#13644) --- .../models_and_agents/test_cuga_agent.py | 109 +++++++++++++++++- src/lfx/src/lfx/_assets/component_index.json | 6 +- src/lfx/src/lfx/components/cuga/cuga_agent.py | 35 ++++++ 3 files changed, 146 insertions(+), 4 deletions(-) diff --git a/src/backend/tests/unit/components/models_and_agents/test_cuga_agent.py b/src/backend/tests/unit/components/models_and_agents/test_cuga_agent.py index 5b2bc1fd37..144ba1a1fa 100644 --- a/src/backend/tests/unit/components/models_and_agents/test_cuga_agent.py +++ b/src/backend/tests/unit/components/models_and_agents/test_cuga_agent.py @@ -1,9 +1,16 @@ +import sys +import types from typing import Any from uuid import uuid4 import pytest from langflow.custom import Component -from lfx.components.cuga import CugaComponent +from lfx.components.cuga.cuga_agent import ( + _CUGA_CODE_AGENT_GUARD_ATTR, + CugaComponent, + _install_cuga_code_agent_security_guard, + _validate_cuga_code_agent_source, +) from lfx.components.tools.calculator import CalculatorToolComponent from tests.base import ComponentTestBaseWithClient, ComponentTestBaseWithoutClient @@ -12,6 +19,106 @@ from tests.unit.mock_language_model import MockLanguageModel # Load environment variables from .env file +def _get_cuga_code_agent_security_classes() -> tuple[Any, Any]: + code_executor_module = pytest.importorskip("cuga.backend.cuga_graph.nodes.cuga_lite.executors.code_executor") + security_module = pytest.importorskip("cuga.backend.cuga_graph.nodes.cuga_lite.executors.common") + return code_executor_module.CodeExecutor, security_module.SecurityValidator + + +def test_cuga_code_agent_guard_allows_safe_codeagent_source(): + code_executor_cls, security_validator_cls = _get_cuga_code_agent_security_classes() + + safe_code = 'import json\nprint(json.dumps({"variable_name": "answer", "value": 42}))' + + _validate_cuga_code_agent_source(code_executor_cls, security_validator_cls, safe_code) + + +def test_cuga_code_agent_guard_blocks_object_graph_escape(): + code_executor_cls, security_validator_cls = _get_cuga_code_agent_security_classes() + + exploit_code = """ +import json +mod = None +for cls in ().__class__.__mro__[1].__subclasses__(): + globals_dict = getattr(getattr(cls, "__init__", None), "__globals__", None) + if isinstance(globals_dict, dict) and globals_dict.get("os") is not None: + mod = globals_dict["os"] + break +mod.system("mkdir -p /tmp/langflow-poc") +print(json.dumps({"variable_name": "proof", "value": "escaped"})) +""" + + with pytest.raises((ImportError, PermissionError), match=r"not allowed|Security violation|Suspicious"): + _validate_cuga_code_agent_source(code_executor_cls, security_validator_cls, exploit_code) + + +@pytest.mark.asyncio +async def test_cuga_code_agent_security_guard_validates_before_original_executor(monkeypatch): + validations: list[tuple[str, str]] = [] + calls: list[tuple[str, Any, Any]] = [] + + class FakeSecurityValidator: + @staticmethod + def validate_imports(code: str) -> None: + validations.append(("imports", code)) + + @staticmethod + def validate_wrapped_code(wrapped_code: str) -> None: + validations.append(("wrapped", wrapped_code)) + if "BLOCK" in wrapped_code: + msg = "blocked before execution" + raise PermissionError(msg) + + class FakeCodeExecutor: + @classmethod + def _wrap_code_for_code_agent(cls, code: str) -> str: + return f"wrapped:{code}" + + @classmethod + async def eval_for_code_agent(cls, code: str, state: Any, mode: Any = None) -> tuple[str, dict[str, Any]]: + calls.append((code, state, mode)) + return "executed", {} + + def package_module(name: str) -> types.ModuleType: + module = types.ModuleType(name) + module.__path__ = [] + return module + + package_names = [ + "cuga", + "cuga.backend", + "cuga.backend.cuga_graph", + "cuga.backend.cuga_graph.nodes", + "cuga.backend.cuga_graph.nodes.cuga_lite", + "cuga.backend.cuga_graph.nodes.cuga_lite.executors", + ] + for package_name in package_names: + monkeypatch.setitem(sys.modules, package_name, package_module(package_name)) + + code_executor_module = types.ModuleType("cuga.backend.cuga_graph.nodes.cuga_lite.executors.code_executor") + code_executor_module.CodeExecutor = FakeCodeExecutor + common_module = types.ModuleType("cuga.backend.cuga_graph.nodes.cuga_lite.executors.common") + common_module.SecurityValidator = FakeSecurityValidator + monkeypatch.setitem(sys.modules, code_executor_module.__name__, code_executor_module) + monkeypatch.setitem(sys.modules, common_module.__name__, common_module) + + _install_cuga_code_agent_security_guard() + _install_cuga_code_agent_security_guard() + + assert getattr(FakeCodeExecutor, _CUGA_CODE_AGENT_GUARD_ATTR) is True + assert await FakeCodeExecutor.eval_for_code_agent("safe", state="state", mode="local") == ("executed", {}) + with pytest.raises(PermissionError, match="blocked before execution"): + await FakeCodeExecutor.eval_for_code_agent("BLOCK", state="state", mode="local") + + assert calls == [("safe", "state", "local")] + assert validations == [ + ("imports", "safe"), + ("wrapped", "wrapped:safe"), + ("imports", "BLOCK"), + ("wrapped", "wrapped:BLOCK"), + ] + + class TestCugaComponent(ComponentTestBaseWithoutClient): """Test suite for CugaComponent without client dependencies. diff --git a/src/lfx/src/lfx/_assets/component_index.json b/src/lfx/src/lfx/_assets/component_index.json index 6bbc0604eb..f501507a67 100644 --- a/src/lfx/src/lfx/_assets/component_index.json +++ b/src/lfx/src/lfx/_assets/component_index.json @@ -56702,7 +56702,7 @@ "icon": "bot", "legacy": false, "metadata": { - "code_hash": "4435ad8f6ebf", + "code_hash": "b4694e582e90", "dependencies": { "dependencies": [ { @@ -56855,7 +56855,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import asyncio\nimport json\nimport traceback\nimport uuid\nfrom collections.abc import AsyncIterator\nfrom typing import TYPE_CHECKING, Any, cast\n\nfrom langchain_core.agents import AgentFinish\nfrom langchain_core.messages import AIMessage, HumanMessage\nfrom langchain_core.tools import StructuredTool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.models.model_input_constants import (\n ALL_PROVIDER_FIELDS,\n MODEL_DYNAMIC_UPDATE_FIELDS,\n MODEL_PROVIDERS,\n MODEL_PROVIDERS_DICT,\n MODELS_METADATA,\n)\nfrom lfx.base.models.model_utils import get_model_name\nfrom lfx.components.helpers import CurrentDateComponent\nfrom lfx.components.langchain_utilities.tool_calling import ToolCallingAgentComponent\nfrom lfx.components.models_and_agents.memory import MemoryComponent, aget_agent_chat_history\nfrom lfx.custom.custom_component.component import _get_component_toolkit\nfrom lfx.custom.utils import update_component_build_config\nfrom lfx.field_typing import Tool\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MultilineInput, Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\n\nif TYPE_CHECKING:\n from lfx.schema.log import SendMessageFunctionType\n\n\ndef set_advanced_true(component_input):\n \"\"\"Set the advanced flag to True for a component input.\n\n Args:\n component_input: The component input to modify\n\n Returns:\n The modified component input with advanced=True\n \"\"\"\n component_input.advanced = True\n return component_input\n\n\nMODEL_PROVIDERS_LIST = [\"OpenAI\"]\n\n\nclass CugaComponent(ToolCallingAgentComponent):\n \"\"\"Cuga Agent Component for advanced AI task execution.\n\n The Cuga component is an advanced AI agent that can execute complex tasks using\n various tools and browser automation. It supports custom instructions, web applications,\n and API interactions.\n\n Attributes:\n display_name: Human-readable name for the component\n description: Brief description of the component's purpose\n documentation: URL to component documentation\n icon: Icon identifier for the UI\n name: Internal component name\n \"\"\"\n\n display_name: str = \"Cuga\"\n description: str = \"Define the Cuga agent's instructions, then assign it a task.\"\n documentation: str = \"https://docs.langflow.org/bundles-cuga\"\n icon = \"bot\"\n name = \"Cuga\"\n\n memory_inputs = [set_advanced_true(component_input) for component_input in MemoryComponent().inputs]\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, \"Custom\"],\n value=\"OpenAI\",\n real_time_refresh=True,\n input_types=[],\n options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST] + [{\"icon\": \"brain\"}],\n ),\n *MODEL_PROVIDERS_DICT[\"OpenAI\"][\"inputs\"],\n MultilineInput(\n name=\"instructions\",\n display_name=\"Instructions\",\n info=(\n \"Custom instructions for the agent to adhere to during its operation.\\n\"\n \"Example:\\n\"\n \"## Plan\\n\"\n \"< planning instructions e.g. which tools and when to use>\\n\"\n \"## Answer\\n\"\n \"< final answer instructions how to answer>\"\n ),\n value=\"\",\n advanced=False,\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 *LCToolsAgentComponent.get_base_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 BoolInput(\n name=\"lite_mode\",\n display_name=\"Enable CugaLite\",\n info=\"Faster reasoning for simple tasks. Enable CugaLite for simple API tasks.\",\n value=True,\n advanced=True,\n ),\n IntInput(\n name=\"lite_mode_tool_threshold\",\n display_name=\"CugaLite Tool Threshold\",\n info=\"Route to CugaLite if app has fewer than this many tools.\",\n value=25,\n advanced=True,\n ),\n DropdownInput(\n name=\"decomposition_strategy\",\n display_name=\"Decomposition Strategy\",\n info=\"Strategy for task decomposition: 'flexible' allows multiple subtasks per app,\\n\"\n \" 'exact' enforces one subtask per app.\",\n options=[\"flexible\", \"exact\"],\n value=\"flexible\",\n advanced=True,\n ),\n BoolInput(\n name=\"browser_enabled\",\n display_name=\"Enable Browser\",\n info=\"Toggle to enable a built-in browser tool for web scraping and searching.\",\n value=False,\n advanced=True,\n ),\n MultilineInput(\n name=\"web_apps\",\n display_name=\"Web applications\",\n info=(\n \"Cuga will automatically start this web application when Enable Browser is true. \"\n \"Currently only supports one web application. Example: https://example.com\"\n ),\n value=\"\",\n advanced=True,\n ),\n ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\n ]\n\n async def call_agent(\n self, current_input: str, tools: list[Tool], history_messages: list[Message], llm\n ) -> AsyncIterator[dict[str, Any]]:\n \"\"\"Execute the Cuga agent with the given input and tools.\n\n This method initializes and runs the Cuga agent, processing the input through\n the agent's workflow and yielding events for real-time monitoring.\n\n Args:\n current_input: The user input to process\n tools: List of available tools for the agent\n history_messages: Previous conversation history\n llm: The language model instance to use\n\n Yields:\n dict: Agent events including tool usage, thinking, and final results\n\n Raises:\n ValueError: If there's an error in agent initialization\n TypeError: If there's a type error in processing\n RuntimeError: If there's a runtime error during execution\n ConnectionError: If there's a connection issue\n \"\"\"\n yield {\n \"event\": \"on_chain_start\",\n \"run_id\": str(uuid.uuid4()),\n \"name\": \"CUGA_initializing\",\n \"data\": {\"input\": {\"input\": current_input, \"chat_history\": []}},\n }\n logger.debug(f\"[CUGA] LLM MODEL TYPE: {type(llm)}\")\n if current_input:\n # Import settings first\n from cuga.config import settings\n\n # Use Dynaconf's set() method to update settings dynamically\n # This properly updates the settings object without corruption\n logger.debug(\"[CUGA] Updating CUGA settings via Dynaconf set() method\")\n\n settings.advanced_features.registry = False\n settings.advanced_features.lite_mode = self.lite_mode\n settings.advanced_features.lite_mode_tool_threshold = self.lite_mode_tool_threshold\n settings.advanced_features.decomposition_strategy = self.decomposition_strategy\n\n if self.browser_enabled:\n logger.debug(\"[CUGA] browser_enabled is true, setting mode to hybrid\")\n settings.advanced_features.mode = \"hybrid\"\n settings.advanced_features.use_vision = False\n else:\n logger.debug(\"[CUGA] browser_enabled is false, setting mode to api\")\n settings.advanced_features.mode = \"api\"\n\n from cuga.backend.activity_tracker.tracker import ActivityTracker\n from cuga.backend.cuga_graph.utils.agent_loop import StreamEvent\n from cuga.backend.cuga_graph.utils.controller import (\n AgentRunner as CugaAgent,\n )\n from cuga.backend.cuga_graph.utils.controller import (\n ExperimentResult as AgentResult,\n )\n from cuga.backend.llm.models import LLMManager\n from cuga.configurations.instructions_manager import InstructionsManager\n\n # Reset var_manager if this is the first message in history\n logger.debug(f\"[CUGA] Checking history_messages: count={len(history_messages) if history_messages else 0}\")\n if not history_messages or len(history_messages) == 0:\n logger.debug(\"[CUGA] First message in history detected, resetting var_manager\")\n else:\n logger.debug(f\"[CUGA] Continuing conversation with {len(history_messages)} previous messages\")\n\n llm_manager = LLMManager()\n llm_manager.set_llm(llm)\n instructions_manager = InstructionsManager()\n\n instructions_to_use = self.instructions or \"\"\n logger.debug(f\"[CUGA] instructions are: {instructions_to_use}\")\n instructions_manager.set_instructions_from_one_file(instructions_to_use)\n tracker = ActivityTracker()\n tracker.set_tools(tools)\n thread_id = self.graph.session_id\n logger.debug(f\"[CUGA] Using thread_id (session_id): {thread_id}\")\n cuga_agent = CugaAgent(browser_enabled=self.browser_enabled, thread_id=thread_id)\n if self.browser_enabled:\n await cuga_agent.initialize_freemode_env(start_url=self.web_apps.strip(), interface_mode=\"browser_only\")\n else:\n await cuga_agent.initialize_appworld_env()\n\n yield {\n \"event\": \"on_chain_start\",\n \"run_id\": str(uuid.uuid4()),\n \"name\": \"CUGA_thinking...\",\n \"data\": {\"input\": {\"input\": current_input, \"chat_history\": []}},\n }\n logger.debug(f\"[CUGA] current web apps are {self.web_apps}\")\n logger.debug(f\"[CUGA] Processing input: {current_input}\")\n try:\n # Convert history to LangChain format for the event\n logger.debug(f\"[CUGA] Converting {len(history_messages)} history messages to LangChain format\")\n lc_messages = []\n for i, msg in enumerate(history_messages):\n msg_text = getattr(msg, \"text\", \"N/A\")[:50] if hasattr(msg, \"text\") else \"N/A\"\n logger.debug(\n f\"[CUGA] Message {i}: type={type(msg)}, sender={getattr(msg, 'sender', 'N/A')}, \"\n f\"text={msg_text}...\"\n )\n if hasattr(msg, \"sender\") and msg.sender == \"Human\":\n lc_messages.append(HumanMessage(content=msg.text))\n else:\n lc_messages.append(AIMessage(content=msg.text))\n\n logger.debug(f\"[CUGA] Converted to {len(lc_messages)} LangChain messages\")\n await asyncio.sleep(0.5)\n\n # 2. Build final response\n response_parts = []\n\n response_parts.append(f\"Processed input: '{current_input}'\")\n response_parts.append(f\"Available tools: {len(tools)}\")\n last_event: StreamEvent | None = None\n tool_run_id: str | None = None\n # 3. Chain end event with AgentFinish\n async for event in cuga_agent.run_task_generic_yield(\n eval_mode=False, goal=current_input, chat_messages=lc_messages\n ):\n logger.debug(f\"[CUGA] recieved event {event}\")\n if last_event is not None and tool_run_id is not None:\n logger.debug(f\"[CUGA] last event {last_event}\")\n try:\n # TODO: Extract data\n data_dict = json.loads(last_event.data)\n except json.JSONDecodeError:\n data_dict = last_event.data\n if last_event.name == \"CodeAgent\" and \"code\" in data_dict:\n data_dict = data_dict[\"code\"]\n yield {\n \"event\": \"on_tool_end\",\n \"run_id\": tool_run_id,\n \"name\": last_event.name,\n \"data\": {\"output\": data_dict},\n }\n if isinstance(event, StreamEvent):\n tool_run_id = str(uuid.uuid4())\n last_event = StreamEvent(name=event.name, data=event.data)\n tool_event = {\n \"event\": \"on_tool_start\",\n \"run_id\": tool_run_id,\n \"name\": event.name,\n \"data\": {\"input\": {}},\n }\n logger.debug(f\"[CUGA] Yielding tool_start event: {event.name}\")\n yield tool_event\n\n if isinstance(event, AgentResult):\n task_result = event\n end_event = {\n \"event\": \"on_chain_end\",\n \"run_id\": str(uuid.uuid4()),\n \"name\": \"CugaAgent\",\n \"data\": {\"output\": AgentFinish(return_values={\"output\": task_result.answer}, log=\"\")},\n }\n answer_preview = task_result.answer[:100] if task_result.answer else \"None\"\n logger.info(f\"[CUGA] Yielding chain_end event with answer: {answer_preview}...\")\n yield end_event\n\n except (ValueError, TypeError, RuntimeError, ConnectionError) as e:\n logger.error(f\"[CUGA] An error occurred: {e!s}\")\n logger.error(f\"[CUGA] Traceback: {traceback.format_exc()}\")\n error_msg = f\"CUGA Agent error: {e!s}\"\n logger.error(f\"[CUGA] Error occurred: {error_msg}\")\n\n # Emit error event\n yield {\n \"event\": \"on_chain_error\",\n \"run_id\": str(uuid.uuid4()),\n \"name\": \"CugaAgent\",\n \"data\": {\"error\": error_msg},\n }\n\n async def message_response(self) -> Message:\n \"\"\"Generate a message response using the Cuga agent.\n\n This method processes the input through the Cuga agent and returns a structured\n message response. It handles agent initialization, tool setup, and event processing.\n\n Returns:\n Message: The agent's response message\n\n Raises:\n Exception: If there's an error during agent execution\n \"\"\"\n logger.debug(\"[CUGA] Starting Cuga agent run for message_response.\")\n logger.debug(f\"[CUGA] Agent input value: {self.input_value}\")\n\n # Validate input is not empty\n if not self.input_value or not str(self.input_value).strip():\n msg = \"Message cannot be empty. Please provide a valid message.\"\n raise ValueError(msg)\n\n try:\n from lfx.schema.content_block import ContentBlock\n from lfx.schema.message import MESSAGE_SENDER_AI\n\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n\n # Create agent message for event processing\n agent_message = Message(\n sender=MESSAGE_SENDER_AI,\n sender_name=\"Cuga\",\n properties={\"icon\": \"Bot\", \"state\": \"partial\"},\n content_blocks=[ContentBlock(title=\"Agent Steps\", contents=[])],\n session_id=self.graph.session_id or str(uuid.uuid4()),\n )\n\n # Pre-assign an ID for event processing, following the base agent pattern\n # This ensures streaming works even when not connected to ChatOutput\n if not self.is_connected_to_chat_output():\n # When not connected to ChatOutput, assign ID upfront for streaming support\n agent_message.data[\"id\"] = uuid.uuid4()\n\n # Get input text\n input_text = self.input_value.text if hasattr(self.input_value, \"text\") else str(self.input_value)\n\n # Create event iterator from call_agent\n event_iterator = self.call_agent(\n current_input=input_text, tools=self.tools or [], history_messages=self.chat_history, llm=llm_model\n )\n\n # Process events using the existing event processing system\n from lfx.base.agents.events import process_agent_events\n\n # Create a wrapper that forces DB updates for event handlers\n # This ensures the UI can see loading steps in real-time via polling\n async def force_db_update_send_message(message, id_=None, *, skip_db_update=False): # noqa: ARG001\n # Always persist to DB so polling-based UI shows loading steps in real-time\n content_blocks_len = len(message.content_blocks[0].contents) if message.content_blocks else 0\n logger.debug(\n f\"[CUGA] Sending message update - state: {message.properties.state}, \"\n f\"content_blocks: {content_blocks_len}\"\n )\n\n result = await self.send_message(message, id_=id_, skip_db_update=False)\n\n logger.debug(f\"[CUGA] Message processed with ID: {result.id}\")\n return result\n\n result = await process_agent_events(\n event_iterator, agent_message, cast(\"SendMessageFunctionType\", force_db_update_send_message)\n )\n\n logger.debug(\"[CUGA] Agent run finished successfully.\")\n logger.debug(f\"[CUGA] Agent output: {result}\")\n\n except Exception as e:\n logger.error(f\"[CUGA] Error in message_response: {e}\")\n logger.error(f\"[CUGA] An error occurred: {e!s}\")\n logger.error(f\"[CUGA] Traceback: {traceback.format_exc()}\")\n\n # Check if error is related to Playwright installation\n error_str = str(e).lower()\n if \"playwright install\" in error_str:\n msg = (\n \"Playwright is not installed. Please install Playwright Chromium using: \"\n \"uv run -m playwright install chromium\"\n )\n raise ValueError(msg) from e\n\n raise\n else:\n return result\n\n async def get_agent_requirements(self):\n \"\"\"Get the agent requirements for the Cuga agent.\n\n This method retrieves and configures all necessary components for the agent\n including the language model, chat history, and tools.\n\n Returns:\n tuple: A tuple containing (llm_model, chat_history, tools)\n\n Raises:\n ValueError: If no language model is selected or if there's an error\n in model initialization\n \"\"\"\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 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):\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\n # --- ADDED LOGGING START ---\n logger.debug(\"[CUGA] Retrieved agent requirements: LLM, chat history, and tools.\")\n logger.debug(f\"[CUGA] LLM model: {self.model_name}\")\n logger.debug(f\"[CUGA] Number of chat history messages: {len(self.chat_history)}\")\n logger.debug(f\"[CUGA] Tools available: {[tool.name for tool in self.tools]}\")\n logger.debug(f\"[CUGA] metadata: {[tool.metadata for tool in self.tools]}\")\n # --- ADDED LOGGING END ---\n\n return llm_model, self.chat_history, self.tools\n\n async def get_memory_data(self):\n \"\"\"Retrieve chat history messages.\n\n This method fetches the conversation history from memory, excluding the current\n input message to avoid duplication.\n\n Returns:\n list: List of Message objects representing the chat history\n \"\"\"\n logger.debug(\"[CUGA] Retrieving chat history messages.\")\n logger.debug(f\"[CUGA] Session ID: {self.graph.session_id}\")\n logger.debug(f\"[CUGA] n_messages: {self.n_messages}\")\n logger.debug(f\"[CUGA] input_value: {self.input_value}\")\n logger.debug(f\"[CUGA] input_value type: {type(self.input_value)}\")\n logger.debug(f\"[CUGA] input_value id: {getattr(self.input_value, 'id', None)}\")\n\n # Scope by flow_id (issue #13059): the ad-hoc MemoryComponent used previously\n # had no _vertex, so it could not see the running flow's flow_id and emitted\n # an unscoped query. The helper also honors n_messages == 0 as \"disabled\".\n messages = await aget_agent_chat_history(\n session_id=str(self.graph.session_id),\n flow_id=getattr(self.graph, \"flow_id\", None),\n n_messages=self.n_messages,\n )\n logger.debug(f\"[CUGA] Retrieved {len(messages)} messages from memory\")\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 \"\"\"Get language model for the Cuga agent.\n\n This method initializes and configures the language model based on the\n selected provider and parameters.\n\n Returns:\n tuple: A tuple containing (llm_model, display_name)\n\n Raises:\n ValueError: If the model provider is invalid or model initialization fails\n \"\"\"\n logger.debug(\"[CUGA] Getting language model for the agent.\")\n logger.debug(f\"[CUGA] Requested LLM provider: {self.agent_llm}\")\n\n if not isinstance(self.agent_llm, str):\n logger.debug(\"[CUGA] Agent LLM is already a model instance.\")\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 logger.debug(f\"[CUGA] Successfully built LLM model from provider: {self.agent_llm}\")\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\"[CUGA] 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 \"\"\"Build LLM model with parameters.\n\n This method constructs a language model instance using the provided component\n class and input parameters.\n\n Args:\n component: The LLM component class to instantiate\n inputs: List of input field definitions\n prefix: Optional prefix for parameter names\n\n Returns:\n The configured LLM model instance\n \"\"\"\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 \"\"\"Set component parameters based on provider.\n\n This method configures component parameters according to the selected\n model provider's requirements.\n\n Args:\n component: The component to configure\n\n Returns:\n The configured component\n \"\"\"\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 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)\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\n This method removes unwanted fields from the build configuration.\n\n Args:\n build_config: The build configuration dictionary\n fields: Fields to remove (can be dict or list of strings)\n \"\"\"\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\n This method ensures all fields in the build configuration have proper\n input types defined.\n\n Args:\n build_config: The build configuration to update\n\n Returns:\n dotdict: Updated build configuration with input types\n \"\"\"\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 \"\"\"Update build configuration based on field changes.\n\n This method dynamically updates the component's build configuration when\n certain fields change, particularly the model provider selection.\n\n Args:\n build_config: The current build configuration\n field_value: The new value for the field\n field_name: The name of the field being changed\n\n Returns:\n dotdict: Updated build configuration\n\n Raises:\n ValueError: If required keys are missing from the configuration\n \"\"\"\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 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 build_config[\"agent_llm\"][\"input_types\"] = []\n elif field_value == \"Custom\":\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 options=[*sorted(MODEL_PROVIDERS), \"Custom\"],\n value=\"Custom\",\n real_time_refresh=True,\n input_types=[\"LanguageModel\"],\n options_metadata=[MODELS_METADATA[key] for key in sorted(MODELS_METADATA.keys())]\n + [{\"icon\": \"brain\"}],\n )\n build_config.update({\"agent_llm\": custom_component.to_dict()})\n\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 \"instructions\",\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\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 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 \"\"\"Build agent tools.\n\n This method constructs the list of tools available to the Cuga agent,\n including component tools and any additional configured tools.\n\n Returns:\n list[Tool]: List of available tools for the agent\n \"\"\"\n logger.debug(\"[CUGA] Building agent tools.\")\n component_toolkit = _get_component_toolkit()\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_CugaAgent\", 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 logger.debug(f\"[CUGA] Tools built: {[tool.name for tool in tools]}\")\n return tools\n" + "value": "import asyncio\nimport json\nimport traceback\nimport uuid\nfrom collections.abc import AsyncIterator\nfrom typing import TYPE_CHECKING, Any, cast\n\nfrom langchain_core.agents import AgentFinish\nfrom langchain_core.messages import AIMessage, HumanMessage\nfrom langchain_core.tools import StructuredTool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.models.model_input_constants import (\n ALL_PROVIDER_FIELDS,\n MODEL_DYNAMIC_UPDATE_FIELDS,\n MODEL_PROVIDERS,\n MODEL_PROVIDERS_DICT,\n MODELS_METADATA,\n)\nfrom lfx.base.models.model_utils import get_model_name\nfrom lfx.components.helpers import CurrentDateComponent\nfrom lfx.components.langchain_utilities.tool_calling import ToolCallingAgentComponent\nfrom lfx.components.models_and_agents.memory import MemoryComponent, aget_agent_chat_history\nfrom lfx.custom.custom_component.component import _get_component_toolkit\nfrom lfx.custom.utils import update_component_build_config\nfrom lfx.field_typing import Tool\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MultilineInput, Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\n\nif TYPE_CHECKING:\n from lfx.schema.log import SendMessageFunctionType\n\n\ndef set_advanced_true(component_input):\n \"\"\"Set the advanced flag to True for a component input.\n\n Args:\n component_input: The component input to modify\n\n Returns:\n The modified component input with advanced=True\n \"\"\"\n component_input.advanced = True\n return component_input\n\n\nMODEL_PROVIDERS_LIST = [\"OpenAI\"]\n\n_CUGA_CODE_AGENT_GUARD_ATTR = \"_langflow_code_agent_guard_installed\"\n\n\ndef _validate_cuga_code_agent_source(code_executor_cls: Any, security_validator_cls: Any, code: str) -> None:\n \"\"\"Apply CUGA's strict validation to CodeAgent-generated Python before execution.\"\"\"\n security_validator_cls.validate_imports(code)\n wrapped_code = code_executor_cls._wrap_code_for_code_agent(code) # noqa: SLF001\n security_validator_cls.validate_wrapped_code(wrapped_code)\n\n\ndef _install_cuga_code_agent_security_guard() -> None:\n \"\"\"Guard CUGA CodeAgent execution with the stricter CUGA Lite validator.\"\"\"\n from cuga.backend.cuga_graph.nodes.cuga_lite.executors.code_executor import CodeExecutor\n from cuga.backend.cuga_graph.nodes.cuga_lite.executors.common import SecurityValidator\n\n if getattr(CodeExecutor, _CUGA_CODE_AGENT_GUARD_ATTR, False):\n return\n\n original_eval_for_code_agent = CodeExecutor.eval_for_code_agent\n\n async def guarded_eval_for_code_agent(\n cls,\n code: str,\n state: Any,\n mode: Any = None,\n **kwargs: Any,\n ) -> tuple[str, dict[str, Any]]:\n _validate_cuga_code_agent_source(cls, SecurityValidator, code)\n return await original_eval_for_code_agent(code=code, state=state, mode=mode, **kwargs)\n\n CodeExecutor.eval_for_code_agent = classmethod(guarded_eval_for_code_agent)\n setattr(CodeExecutor, _CUGA_CODE_AGENT_GUARD_ATTR, True)\n\n\nclass CugaComponent(ToolCallingAgentComponent):\n \"\"\"Cuga Agent Component for advanced AI task execution.\n\n The Cuga component is an advanced AI agent that can execute complex tasks using\n various tools and browser automation. It supports custom instructions, web applications,\n and API interactions.\n\n Attributes:\n display_name: Human-readable name for the component\n description: Brief description of the component's purpose\n documentation: URL to component documentation\n icon: Icon identifier for the UI\n name: Internal component name\n \"\"\"\n\n display_name: str = \"Cuga\"\n description: str = \"Define the Cuga agent's instructions, then assign it a task.\"\n documentation: str = \"https://docs.langflow.org/bundles-cuga\"\n icon = \"bot\"\n name = \"Cuga\"\n\n memory_inputs = [set_advanced_true(component_input) for component_input in MemoryComponent().inputs]\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, \"Custom\"],\n value=\"OpenAI\",\n real_time_refresh=True,\n input_types=[],\n options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST] + [{\"icon\": \"brain\"}],\n ),\n *MODEL_PROVIDERS_DICT[\"OpenAI\"][\"inputs\"],\n MultilineInput(\n name=\"instructions\",\n display_name=\"Instructions\",\n info=(\n \"Custom instructions for the agent to adhere to during its operation.\\n\"\n \"Example:\\n\"\n \"## Plan\\n\"\n \"< planning instructions e.g. which tools and when to use>\\n\"\n \"## Answer\\n\"\n \"< final answer instructions how to answer>\"\n ),\n value=\"\",\n advanced=False,\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 *LCToolsAgentComponent.get_base_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 BoolInput(\n name=\"lite_mode\",\n display_name=\"Enable CugaLite\",\n info=\"Faster reasoning for simple tasks. Enable CugaLite for simple API tasks.\",\n value=True,\n advanced=True,\n ),\n IntInput(\n name=\"lite_mode_tool_threshold\",\n display_name=\"CugaLite Tool Threshold\",\n info=\"Route to CugaLite if app has fewer than this many tools.\",\n value=25,\n advanced=True,\n ),\n DropdownInput(\n name=\"decomposition_strategy\",\n display_name=\"Decomposition Strategy\",\n info=\"Strategy for task decomposition: 'flexible' allows multiple subtasks per app,\\n\"\n \" 'exact' enforces one subtask per app.\",\n options=[\"flexible\", \"exact\"],\n value=\"flexible\",\n advanced=True,\n ),\n BoolInput(\n name=\"browser_enabled\",\n display_name=\"Enable Browser\",\n info=\"Toggle to enable a built-in browser tool for web scraping and searching.\",\n value=False,\n advanced=True,\n ),\n MultilineInput(\n name=\"web_apps\",\n display_name=\"Web applications\",\n info=(\n \"Cuga will automatically start this web application when Enable Browser is true. \"\n \"Currently only supports one web application. Example: https://example.com\"\n ),\n value=\"\",\n advanced=True,\n ),\n ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\n ]\n\n async def call_agent(\n self, current_input: str, tools: list[Tool], history_messages: list[Message], llm\n ) -> AsyncIterator[dict[str, Any]]:\n \"\"\"Execute the Cuga agent with the given input and tools.\n\n This method initializes and runs the Cuga agent, processing the input through\n the agent's workflow and yielding events for real-time monitoring.\n\n Args:\n current_input: The user input to process\n tools: List of available tools for the agent\n history_messages: Previous conversation history\n llm: The language model instance to use\n\n Yields:\n dict: Agent events including tool usage, thinking, and final results\n\n Raises:\n ValueError: If there's an error in agent initialization\n TypeError: If there's a type error in processing\n RuntimeError: If there's a runtime error during execution\n ConnectionError: If there's a connection issue\n \"\"\"\n yield {\n \"event\": \"on_chain_start\",\n \"run_id\": str(uuid.uuid4()),\n \"name\": \"CUGA_initializing\",\n \"data\": {\"input\": {\"input\": current_input, \"chat_history\": []}},\n }\n logger.debug(f\"[CUGA] LLM MODEL TYPE: {type(llm)}\")\n if current_input:\n # Import settings first\n from cuga.config import settings\n\n # Use Dynaconf's set() method to update settings dynamically\n # This properly updates the settings object without corruption\n logger.debug(\"[CUGA] Updating CUGA settings via Dynaconf set() method\")\n\n settings.advanced_features.registry = False\n settings.advanced_features.lite_mode = self.lite_mode\n settings.advanced_features.lite_mode_tool_threshold = self.lite_mode_tool_threshold\n settings.advanced_features.decomposition_strategy = self.decomposition_strategy\n\n if self.browser_enabled:\n logger.debug(\"[CUGA] browser_enabled is true, setting mode to hybrid\")\n settings.advanced_features.mode = \"hybrid\"\n settings.advanced_features.use_vision = False\n else:\n logger.debug(\"[CUGA] browser_enabled is false, setting mode to api\")\n settings.advanced_features.mode = \"api\"\n\n from cuga.backend.activity_tracker.tracker import ActivityTracker\n from cuga.backend.cuga_graph.utils.agent_loop import StreamEvent\n from cuga.backend.cuga_graph.utils.controller import (\n AgentRunner as CugaAgent,\n )\n from cuga.backend.cuga_graph.utils.controller import (\n ExperimentResult as AgentResult,\n )\n from cuga.backend.llm.models import LLMManager\n from cuga.configurations.instructions_manager import InstructionsManager\n\n _install_cuga_code_agent_security_guard()\n\n # Reset var_manager if this is the first message in history\n logger.debug(f\"[CUGA] Checking history_messages: count={len(history_messages) if history_messages else 0}\")\n if not history_messages or len(history_messages) == 0:\n logger.debug(\"[CUGA] First message in history detected, resetting var_manager\")\n else:\n logger.debug(f\"[CUGA] Continuing conversation with {len(history_messages)} previous messages\")\n\n llm_manager = LLMManager()\n llm_manager.set_llm(llm)\n instructions_manager = InstructionsManager()\n\n instructions_to_use = self.instructions or \"\"\n logger.debug(f\"[CUGA] instructions are: {instructions_to_use}\")\n instructions_manager.set_instructions_from_one_file(instructions_to_use)\n tracker = ActivityTracker()\n tracker.set_tools(tools)\n thread_id = self.graph.session_id\n logger.debug(f\"[CUGA] Using thread_id (session_id): {thread_id}\")\n cuga_agent = CugaAgent(browser_enabled=self.browser_enabled, thread_id=thread_id)\n if self.browser_enabled:\n await cuga_agent.initialize_freemode_env(start_url=self.web_apps.strip(), interface_mode=\"browser_only\")\n else:\n await cuga_agent.initialize_appworld_env()\n\n yield {\n \"event\": \"on_chain_start\",\n \"run_id\": str(uuid.uuid4()),\n \"name\": \"CUGA_thinking...\",\n \"data\": {\"input\": {\"input\": current_input, \"chat_history\": []}},\n }\n logger.debug(f\"[CUGA] current web apps are {self.web_apps}\")\n logger.debug(f\"[CUGA] Processing input: {current_input}\")\n try:\n # Convert history to LangChain format for the event\n logger.debug(f\"[CUGA] Converting {len(history_messages)} history messages to LangChain format\")\n lc_messages = []\n for i, msg in enumerate(history_messages):\n msg_text = getattr(msg, \"text\", \"N/A\")[:50] if hasattr(msg, \"text\") else \"N/A\"\n logger.debug(\n f\"[CUGA] Message {i}: type={type(msg)}, sender={getattr(msg, 'sender', 'N/A')}, \"\n f\"text={msg_text}...\"\n )\n if hasattr(msg, \"sender\") and msg.sender == \"Human\":\n lc_messages.append(HumanMessage(content=msg.text))\n else:\n lc_messages.append(AIMessage(content=msg.text))\n\n logger.debug(f\"[CUGA] Converted to {len(lc_messages)} LangChain messages\")\n await asyncio.sleep(0.5)\n\n # 2. Build final response\n response_parts = []\n\n response_parts.append(f\"Processed input: '{current_input}'\")\n response_parts.append(f\"Available tools: {len(tools)}\")\n last_event: StreamEvent | None = None\n tool_run_id: str | None = None\n # 3. Chain end event with AgentFinish\n async for event in cuga_agent.run_task_generic_yield(\n eval_mode=False, goal=current_input, chat_messages=lc_messages\n ):\n logger.debug(f\"[CUGA] recieved event {event}\")\n if last_event is not None and tool_run_id is not None:\n logger.debug(f\"[CUGA] last event {last_event}\")\n try:\n # TODO: Extract data\n data_dict = json.loads(last_event.data)\n except json.JSONDecodeError:\n data_dict = last_event.data\n if last_event.name == \"CodeAgent\" and \"code\" in data_dict:\n data_dict = data_dict[\"code\"]\n yield {\n \"event\": \"on_tool_end\",\n \"run_id\": tool_run_id,\n \"name\": last_event.name,\n \"data\": {\"output\": data_dict},\n }\n if isinstance(event, StreamEvent):\n tool_run_id = str(uuid.uuid4())\n last_event = StreamEvent(name=event.name, data=event.data)\n tool_event = {\n \"event\": \"on_tool_start\",\n \"run_id\": tool_run_id,\n \"name\": event.name,\n \"data\": {\"input\": {}},\n }\n logger.debug(f\"[CUGA] Yielding tool_start event: {event.name}\")\n yield tool_event\n\n if isinstance(event, AgentResult):\n task_result = event\n end_event = {\n \"event\": \"on_chain_end\",\n \"run_id\": str(uuid.uuid4()),\n \"name\": \"CugaAgent\",\n \"data\": {\"output\": AgentFinish(return_values={\"output\": task_result.answer}, log=\"\")},\n }\n answer_preview = task_result.answer[:100] if task_result.answer else \"None\"\n logger.info(f\"[CUGA] Yielding chain_end event with answer: {answer_preview}...\")\n yield end_event\n\n except (ValueError, TypeError, RuntimeError, ConnectionError) as e:\n logger.error(f\"[CUGA] An error occurred: {e!s}\")\n logger.error(f\"[CUGA] Traceback: {traceback.format_exc()}\")\n error_msg = f\"CUGA Agent error: {e!s}\"\n logger.error(f\"[CUGA] Error occurred: {error_msg}\")\n\n # Emit error event\n yield {\n \"event\": \"on_chain_error\",\n \"run_id\": str(uuid.uuid4()),\n \"name\": \"CugaAgent\",\n \"data\": {\"error\": error_msg},\n }\n\n async def message_response(self) -> Message:\n \"\"\"Generate a message response using the Cuga agent.\n\n This method processes the input through the Cuga agent and returns a structured\n message response. It handles agent initialization, tool setup, and event processing.\n\n Returns:\n Message: The agent's response message\n\n Raises:\n Exception: If there's an error during agent execution\n \"\"\"\n logger.debug(\"[CUGA] Starting Cuga agent run for message_response.\")\n logger.debug(f\"[CUGA] Agent input value: {self.input_value}\")\n\n # Validate input is not empty\n if not self.input_value or not str(self.input_value).strip():\n msg = \"Message cannot be empty. Please provide a valid message.\"\n raise ValueError(msg)\n\n try:\n from lfx.schema.content_block import ContentBlock\n from lfx.schema.message import MESSAGE_SENDER_AI\n\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n\n # Create agent message for event processing\n agent_message = Message(\n sender=MESSAGE_SENDER_AI,\n sender_name=\"Cuga\",\n properties={\"icon\": \"Bot\", \"state\": \"partial\"},\n content_blocks=[ContentBlock(title=\"Agent Steps\", contents=[])],\n session_id=self.graph.session_id or str(uuid.uuid4()),\n )\n\n # Pre-assign an ID for event processing, following the base agent pattern\n # This ensures streaming works even when not connected to ChatOutput\n if not self.is_connected_to_chat_output():\n # When not connected to ChatOutput, assign ID upfront for streaming support\n agent_message.data[\"id\"] = uuid.uuid4()\n\n # Get input text\n input_text = self.input_value.text if hasattr(self.input_value, \"text\") else str(self.input_value)\n\n # Create event iterator from call_agent\n event_iterator = self.call_agent(\n current_input=input_text, tools=self.tools or [], history_messages=self.chat_history, llm=llm_model\n )\n\n # Process events using the existing event processing system\n from lfx.base.agents.events import process_agent_events\n\n # Create a wrapper that forces DB updates for event handlers\n # This ensures the UI can see loading steps in real-time via polling\n async def force_db_update_send_message(message, id_=None, *, skip_db_update=False): # noqa: ARG001\n # Always persist to DB so polling-based UI shows loading steps in real-time\n content_blocks_len = len(message.content_blocks[0].contents) if message.content_blocks else 0\n logger.debug(\n f\"[CUGA] Sending message update - state: {message.properties.state}, \"\n f\"content_blocks: {content_blocks_len}\"\n )\n\n result = await self.send_message(message, id_=id_, skip_db_update=False)\n\n logger.debug(f\"[CUGA] Message processed with ID: {result.id}\")\n return result\n\n result = await process_agent_events(\n event_iterator, agent_message, cast(\"SendMessageFunctionType\", force_db_update_send_message)\n )\n\n logger.debug(\"[CUGA] Agent run finished successfully.\")\n logger.debug(f\"[CUGA] Agent output: {result}\")\n\n except Exception as e:\n logger.error(f\"[CUGA] Error in message_response: {e}\")\n logger.error(f\"[CUGA] An error occurred: {e!s}\")\n logger.error(f\"[CUGA] Traceback: {traceback.format_exc()}\")\n\n # Check if error is related to Playwright installation\n error_str = str(e).lower()\n if \"playwright install\" in error_str:\n msg = (\n \"Playwright is not installed. Please install Playwright Chromium using: \"\n \"uv run -m playwright install chromium\"\n )\n raise ValueError(msg) from e\n\n raise\n else:\n return result\n\n async def get_agent_requirements(self):\n \"\"\"Get the agent requirements for the Cuga agent.\n\n This method retrieves and configures all necessary components for the agent\n including the language model, chat history, and tools.\n\n Returns:\n tuple: A tuple containing (llm_model, chat_history, tools)\n\n Raises:\n ValueError: If no language model is selected or if there's an error\n in model initialization\n \"\"\"\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 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):\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\n # --- ADDED LOGGING START ---\n logger.debug(\"[CUGA] Retrieved agent requirements: LLM, chat history, and tools.\")\n logger.debug(f\"[CUGA] LLM model: {self.model_name}\")\n logger.debug(f\"[CUGA] Number of chat history messages: {len(self.chat_history)}\")\n logger.debug(f\"[CUGA] Tools available: {[tool.name for tool in self.tools]}\")\n logger.debug(f\"[CUGA] metadata: {[tool.metadata for tool in self.tools]}\")\n # --- ADDED LOGGING END ---\n\n return llm_model, self.chat_history, self.tools\n\n async def get_memory_data(self):\n \"\"\"Retrieve chat history messages.\n\n This method fetches the conversation history from memory, excluding the current\n input message to avoid duplication.\n\n Returns:\n list: List of Message objects representing the chat history\n \"\"\"\n logger.debug(\"[CUGA] Retrieving chat history messages.\")\n logger.debug(f\"[CUGA] Session ID: {self.graph.session_id}\")\n logger.debug(f\"[CUGA] n_messages: {self.n_messages}\")\n logger.debug(f\"[CUGA] input_value: {self.input_value}\")\n logger.debug(f\"[CUGA] input_value type: {type(self.input_value)}\")\n logger.debug(f\"[CUGA] input_value id: {getattr(self.input_value, 'id', None)}\")\n\n # Scope by flow_id (issue #13059): the ad-hoc MemoryComponent used previously\n # had no _vertex, so it could not see the running flow's flow_id and emitted\n # an unscoped query. The helper also honors n_messages == 0 as \"disabled\".\n messages = await aget_agent_chat_history(\n session_id=str(self.graph.session_id),\n flow_id=getattr(self.graph, \"flow_id\", None),\n n_messages=self.n_messages,\n )\n logger.debug(f\"[CUGA] Retrieved {len(messages)} messages from memory\")\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 \"\"\"Get language model for the Cuga agent.\n\n This method initializes and configures the language model based on the\n selected provider and parameters.\n\n Returns:\n tuple: A tuple containing (llm_model, display_name)\n\n Raises:\n ValueError: If the model provider is invalid or model initialization fails\n \"\"\"\n logger.debug(\"[CUGA] Getting language model for the agent.\")\n logger.debug(f\"[CUGA] Requested LLM provider: {self.agent_llm}\")\n\n if not isinstance(self.agent_llm, str):\n logger.debug(\"[CUGA] Agent LLM is already a model instance.\")\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 logger.debug(f\"[CUGA] Successfully built LLM model from provider: {self.agent_llm}\")\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\"[CUGA] 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 \"\"\"Build LLM model with parameters.\n\n This method constructs a language model instance using the provided component\n class and input parameters.\n\n Args:\n component: The LLM component class to instantiate\n inputs: List of input field definitions\n prefix: Optional prefix for parameter names\n\n Returns:\n The configured LLM model instance\n \"\"\"\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 \"\"\"Set component parameters based on provider.\n\n This method configures component parameters according to the selected\n model provider's requirements.\n\n Args:\n component: The component to configure\n\n Returns:\n The configured component\n \"\"\"\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 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)\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\n This method removes unwanted fields from the build configuration.\n\n Args:\n build_config: The build configuration dictionary\n fields: Fields to remove (can be dict or list of strings)\n \"\"\"\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\n This method ensures all fields in the build configuration have proper\n input types defined.\n\n Args:\n build_config: The build configuration to update\n\n Returns:\n dotdict: Updated build configuration with input types\n \"\"\"\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 \"\"\"Update build configuration based on field changes.\n\n This method dynamically updates the component's build configuration when\n certain fields change, particularly the model provider selection.\n\n Args:\n build_config: The current build configuration\n field_value: The new value for the field\n field_name: The name of the field being changed\n\n Returns:\n dotdict: Updated build configuration\n\n Raises:\n ValueError: If required keys are missing from the configuration\n \"\"\"\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 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 build_config[\"agent_llm\"][\"input_types\"] = []\n elif field_value == \"Custom\":\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 options=[*sorted(MODEL_PROVIDERS), \"Custom\"],\n value=\"Custom\",\n real_time_refresh=True,\n input_types=[\"LanguageModel\"],\n options_metadata=[MODELS_METADATA[key] for key in sorted(MODELS_METADATA.keys())]\n + [{\"icon\": \"brain\"}],\n )\n build_config.update({\"agent_llm\": custom_component.to_dict()})\n\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 \"instructions\",\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\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 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 \"\"\"Build agent tools.\n\n This method constructs the list of tools available to the Cuga agent,\n including component tools and any additional configured tools.\n\n Returns:\n list[Tool]: List of available tools for the agent\n \"\"\"\n logger.debug(\"[CUGA] Building agent tools.\")\n component_toolkit = _get_component_toolkit()\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_CugaAgent\", 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 logger.debug(f\"[CUGA] Tools built: {[tool.name for tool in tools]}\")\n return tools\n" }, "decomposition_strategy": { "_input_type": "DropdownInput", @@ -118467,6 +118467,6 @@ "num_components": 354, "num_modules": 95 }, - "sha256": "d0337af9864fc373eec4952146e477e4f6e6d1912106b8e6e4bd21e642cd645e", + "sha256": "843ba7bdc516522eccbfc466970f8b40cc260abeccfda302436404d505fe8565", "version": "1.10.1" } diff --git a/src/lfx/src/lfx/components/cuga/cuga_agent.py b/src/lfx/src/lfx/components/cuga/cuga_agent.py index ea05730dd5..d2976ac839 100644 --- a/src/lfx/src/lfx/components/cuga/cuga_agent.py +++ b/src/lfx/src/lfx/components/cuga/cuga_agent.py @@ -48,6 +48,39 @@ def set_advanced_true(component_input): MODEL_PROVIDERS_LIST = ["OpenAI"] +_CUGA_CODE_AGENT_GUARD_ATTR = "_langflow_code_agent_guard_installed" + + +def _validate_cuga_code_agent_source(code_executor_cls: Any, security_validator_cls: Any, code: str) -> None: + """Apply CUGA's strict validation to CodeAgent-generated Python before execution.""" + security_validator_cls.validate_imports(code) + wrapped_code = code_executor_cls._wrap_code_for_code_agent(code) # noqa: SLF001 + security_validator_cls.validate_wrapped_code(wrapped_code) + + +def _install_cuga_code_agent_security_guard() -> None: + """Guard CUGA CodeAgent execution with the stricter CUGA Lite validator.""" + from cuga.backend.cuga_graph.nodes.cuga_lite.executors.code_executor import CodeExecutor + from cuga.backend.cuga_graph.nodes.cuga_lite.executors.common import SecurityValidator + + if getattr(CodeExecutor, _CUGA_CODE_AGENT_GUARD_ATTR, False): + return + + original_eval_for_code_agent = CodeExecutor.eval_for_code_agent + + async def guarded_eval_for_code_agent( + cls, + code: str, + state: Any, + mode: Any = None, + **kwargs: Any, + ) -> tuple[str, dict[str, Any]]: + _validate_cuga_code_agent_source(cls, SecurityValidator, code) + return await original_eval_for_code_agent(code=code, state=state, mode=mode, **kwargs) + + CodeExecutor.eval_for_code_agent = classmethod(guarded_eval_for_code_agent) + setattr(CodeExecutor, _CUGA_CODE_AGENT_GUARD_ATTR, True) + class CugaComponent(ToolCallingAgentComponent): """Cuga Agent Component for advanced AI task execution. @@ -221,6 +254,8 @@ class CugaComponent(ToolCallingAgentComponent): from cuga.backend.llm.models import LLMManager from cuga.configurations.instructions_manager import InstructionsManager + _install_cuga_code_agent_security_guard() + # Reset var_manager if this is the first message in history logger.debug(f"[CUGA] Checking history_messages: count={len(history_messages) if history_messages else 0}") if not history_messages or len(history_messages) == 0: