Skip to content

Commit

Permalink
tests: Enhance component testing and error handling with dynamic retr…
Browse files Browse the repository at this point in the history
…ieval (#4526)

* Add module parameter to build_component_instance_for_tests function for dynamic component retrieval

* Enhance component test base with detailed version mapping and error handling

- Introduced `VersionComponentMapping` TypedDict for structured version mapping.
- Updated `FILE_NAMES_MAPPING` to use a list of `VersionComponentMapping`.
- Added comprehensive error messages for missing or invalid mappings in `test_all_versions_have_a_file_name_defined`.
- Improved `test_component_versions` with detailed exception handling and error reporting.
- Ensured `component_class` is defined before running tests.

* Refactor FILE_NAMES_MAPPING to use a list of dictionaries for better structure and readability in test_prompt_component.py

* refactor: Enhance ComponentTestBase with fixture validation and improved version handling

* Refactor test setup in `test_prompt_component.py` to use fixtures for improved modularity and readability

* fix: Add PlaceholderGraph NamedTuple and handle 'graph' attribute in Component class

* Add attribute checks for 'graph' and 'vertex' to prevent errors

* Handle missing 'graph' attribute in 'store_message' method to prevent errors.

* Handle missing 'graph' attribute in Message creation to prevent errors

* Handle missing 'graph' attribute in chat message flow ID assignment

* Add component code to test instance creation and error logging

* Update SUPPORTED_VERSIONS to remove older versions

* test: add unit tests for ChatInput and TextInputComponent

Implement comprehensive tests for both ChatInput and TextInputComponent to ensure proper functionality, including message responses and handling of various input scenarios. This enhances reliability and aids in future development.

* test: add unit tests for ChatOutput and TextOutputComponent

Implement comprehensive tests for ChatOutput and TextOutputComponent, validating message responses, source properties, and behavior with various input types to ensure reliability and consistency across output components.

* Update JSON files to improve code readability and add missing info fields

- Added missing `info` fields to various input components to provide better context and descriptions.
- Improved code readability by ensuring consistent formatting and structure across JSON files.
- Updated `message_response` method to handle cases where `graph` attribute might not be present.
- Enhanced `build_vectorize_options` method to set `authentication` and `parameters` to `None` if no values are provided.
- Refined `AgentComponent` to include `info` for `agent_llm` and other fields, improving clarity on their purpose.

* Refactor: update attribute access to use private `_vertex` attribute

* test: enhance TextInputComponent tests and update properties assertions

* Remove redundant unit tests for output components in test_output_components.py

* feat: add PlaceholderGraph for backwards compatibility and enhance Component attributes

* fix: improve run_id assignment and ensure user_id is a string in PlaceholderGraph

* Add check for non-empty incoming_edges in get_properties_from_source_component
  • Loading branch information
ogabrielluiz authored Nov 12, 2024
1 parent ae966ab commit 0dc6cce
Show file tree
Hide file tree
Showing 24 changed files with 407 additions and 56 deletions.
7 changes: 4 additions & 3 deletions src/backend/base/langflow/base/io/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def build_with_data(

self.status = message_text
if session_id and isinstance(message, Message) and isinstance(message.text, str):
messages = store_message(message, flow_id=self.graph.flow_id)
flow_id = self.graph.flow_id if hasattr(self, "graph") else None
messages = store_message(message, flow_id=flow_id)
self.status = messages
self._send_messages_events(messages)

Expand All @@ -50,8 +51,8 @@ def _send_messages_events(self, messages) -> None:
self._send_message_event(message=stored_message, id_=id_)

def get_properties_from_source_component(self):
if self.vertex.incoming_edges:
source_id = self.vertex.incoming_edges[0].source_id
if hasattr(self, "_vertex") and hasattr(self._vertex, "incoming_edges") and self._vertex.incoming_edges:
source_id = self._vertex.incoming_edges[0].source_id
_source_vertex = self.graph.get_vertex(source_id)
component = _source_vertex.custom_component
source = component.display_name
Expand Down
2 changes: 1 addition & 1 deletion src/backend/base/langflow/base/tools/flow_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async def _arun(
"""Use the tool asynchronously."""
tweaks = self.build_tweaks_dict(args, kwargs)
try:
run_id = self.graph.run_id if self.graph else None
run_id = self.graph.run_id if hasattr(self, "graph") and self.graph else None
except Exception: # noqa: BLE001
logger.opt(exception=True).warning("Failed to set run_id")
run_id = None
Expand Down
5 changes: 2 additions & 3 deletions src/backend/base/langflow/components/deactivated/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ def build(
session_id: str | None = None,
text: str = "",
) -> Message:
message = Message(
text=text, sender=sender, sender_name=sender_name, flow_id=self.graph.flow_id, session_id=session_id
)
flow_id = self.graph.flow_id if hasattr(self, "graph") else None
message = Message(text=text, sender=sender, sender_name=sender_name, flow_id=flow_id, session_id=session_id)

self.status = message
return message
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ def build(
self,
message: Message,
) -> Message:
store_message(message, flow_id=self.graph.flow_id)
flow_id = self.graph.flow_id if hasattr(self, "graph") else None
store_message(message, flow_id=flow_id)
self.status = get_messages()

return message
2 changes: 1 addition & 1 deletion src/backend/base/langflow/components/outputs/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def message_response(self) -> Message:
message.sender = self.sender
message.sender_name = self.sender_name
message.session_id = self.session_id
message.flow_id = self.graph.flow_id
message.flow_id = self.graph.flow_id if hasattr(self, "graph") else None
message.properties.source = self._build_source(_source_id, _display_name, _source)
message.properties.icon = _icon
message.properties.background_color = _background_color
Expand Down
44 changes: 38 additions & 6 deletions src/backend/base/langflow/custom/custom_component/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from collections.abc import AsyncIterator, Iterator
from copy import deepcopy
from textwrap import dedent
from typing import TYPE_CHECKING, Any, ClassVar, get_type_hints
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, get_type_hints

import nanoid
import yaml
Expand Down Expand Up @@ -59,6 +59,29 @@ def _get_component_toolkit():
CONFIG_ATTRIBUTES = ["_display_name", "_description", "_icon", "_name", "_metadata"]


class PlaceholderGraph(NamedTuple):
"""A placeholder graph structure for components, providing backwards compatibility.
and enabling component execution without a full graph object.
This lightweight structure contains essential information typically found in a complete graph,
allowing components to function in isolation or in simplified contexts.
Attributes:
flow_id (str | None): Unique identifier for the flow, if applicable.
user_id (str | None): Identifier of the user associated with the flow, if any.
session_id (str | None): Identifier for the current session, if applicable.
context (dict): Additional contextual information for the component's execution.
flow_name (str | None): Name of the flow, if available.
"""

flow_id: str | None
user_id: str | None
session_id: str | None
context: dict
flow_name: str | None


class Component(CustomComponent):
inputs: list[InputTypes] = []
outputs: list[Output] = []
Expand All @@ -67,6 +90,8 @@ class Component(CustomComponent):
_current_output: str = ""
_metadata: dict = {}
_ctx: dict = {}
_code: str | None = None
_logs: list[Log] = []

def __init__(self, **kwargs) -> None:
# if key starts with _ it is a config
Expand Down Expand Up @@ -187,7 +212,7 @@ def _instance_getter(_):
_instance_getter.__annotations__["return"] = state_model
return _instance_getter

def __deepcopy__(self, memo):
def __deepcopy__(self, memo: dict) -> Component:
if id(self) in memo:
return memo[id(self)]
kwargs = deepcopy(self.__config, memo)
Expand All @@ -201,7 +226,7 @@ def __deepcopy__(self, memo):
new_component._parameters = self._parameters
new_component._attributes = self._attributes
new_component._output_logs = self._output_logs
new_component._logs = self._logs
new_component._logs = self._logs # type: ignore[attr-defined]
memo[id(self)] = new_component
return new_component

Expand Down Expand Up @@ -627,6 +652,15 @@ def __getattr__(self, name: str) -> Any:
return self.__dict__[f"_{name}"]
if name.startswith("_") and name[1:] in BACKWARDS_COMPATIBLE_ATTRIBUTES:
return self.__dict__[name]
if name == "graph":
# If it got up to here it means it was going to raise
session_id = self._session_id if hasattr(self, "_session_id") else None
user_id = self._user_id if hasattr(self, "_user_id") else None
flow_name = self._flow_name if hasattr(self, "_flow_name") else None
flow_id = self._flow_id if hasattr(self, "_flow_id") else None
return PlaceholderGraph(
flow_id=flow_id, user_id=str(user_id), session_id=session_id, context={}, flow_name=flow_name
)
msg = f"{name} not found in {self.__class__.__name__}"
raise AttributeError(msg)

Expand Down Expand Up @@ -1032,9 +1066,7 @@ def _update_stored_message(self, stored_message: Message) -> Message:
msg = "Only one message can be updated at a time."
raise ValueError(msg)
message_table = message_tables[0]
updated_message = Message(**message_table.model_dump())
self.vertex._added_message = updated_message
return updated_message
return Message(**message_table.model_dump())

def _stream_message(self, iterator: AsyncIterator | Iterator, message: Message) -> str:
if not isinstance(iterator, AsyncIterator | Iterator):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from langflow.base.io.chat import ChatComponent\nfrom langflow.inputs import BoolInput\nfrom langflow.io import DropdownInput, MessageInput, MessageTextInput, Output\nfrom langflow.schema.message import Message\nfrom langflow.schema.properties import Source\nfrom langflow.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI, MESSAGE_SENDER_USER\n\n\nclass ChatOutput(ChatComponent):\n display_name = \"Chat Output\"\n description = \"Display a chat message in the Playground.\"\n icon = \"MessagesSquare\"\n name = \"ChatOutput\"\n\n inputs = [\n MessageInput(\n name=\"input_value\",\n display_name=\"Text\",\n info=\"Message to be passed as output.\",\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_AI,\n advanced=True,\n info=\"Type of sender.\",\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_AI,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"data_template\",\n display_name=\"Data Template\",\n value=\"{text}\",\n advanced=True,\n info=\"Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.\",\n ),\n MessageTextInput(\n name=\"background_color\",\n display_name=\"Background Color\",\n info=\"The background color of the icon.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"chat_icon\",\n display_name=\"Icon\",\n info=\"The icon of the message.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"text_color\",\n display_name=\"Text Color\",\n info=\"The text color of the name\",\n advanced=True,\n ),\n ]\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"message\",\n method=\"message_response\",\n ),\n ]\n\n def _build_source(self, _id: str | None, display_name: str | None, source: str | None) -> Source:\n source_dict = {}\n if _id:\n source_dict[\"id\"] = _id\n if display_name:\n source_dict[\"display_name\"] = display_name\n if source:\n source_dict[\"source\"] = source\n return Source(**source_dict)\n\n def message_response(self) -> Message:\n _source, _icon, _display_name, _source_id = self.get_properties_from_source_component()\n _background_color = self.background_color\n _text_color = self.text_color\n if self.chat_icon:\n _icon = self.chat_icon\n message = self.input_value if isinstance(self.input_value, Message) else Message(text=self.input_value)\n message.sender = self.sender\n message.sender_name = self.sender_name\n message.session_id = self.session_id\n message.flow_id = self.graph.flow_id\n message.properties.source = self._build_source(_source_id, _display_name, _source)\n message.properties.icon = _icon\n message.properties.background_color = _background_color\n message.properties.text_color = _text_color\n if self.session_id and isinstance(message, Message) and self.should_store_message:\n stored_message = self.send_message(\n message,\n )\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n"
"value": "from langflow.base.io.chat import ChatComponent\nfrom langflow.inputs import BoolInput\nfrom langflow.io import DropdownInput, MessageInput, MessageTextInput, Output\nfrom langflow.schema.message import Message\nfrom langflow.schema.properties import Source\nfrom langflow.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI, MESSAGE_SENDER_USER\n\n\nclass ChatOutput(ChatComponent):\n display_name = \"Chat Output\"\n description = \"Display a chat message in the Playground.\"\n icon = \"MessagesSquare\"\n name = \"ChatOutput\"\n\n inputs = [\n MessageInput(\n name=\"input_value\",\n display_name=\"Text\",\n info=\"Message to be passed as output.\",\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_AI,\n advanced=True,\n info=\"Type of sender.\",\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_AI,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"data_template\",\n display_name=\"Data Template\",\n value=\"{text}\",\n advanced=True,\n info=\"Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.\",\n ),\n MessageTextInput(\n name=\"background_color\",\n display_name=\"Background Color\",\n info=\"The background color of the icon.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"chat_icon\",\n display_name=\"Icon\",\n info=\"The icon of the message.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"text_color\",\n display_name=\"Text Color\",\n info=\"The text color of the name\",\n advanced=True,\n ),\n ]\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"message\",\n method=\"message_response\",\n ),\n ]\n\n def _build_source(self, _id: str | None, display_name: str | None, source: str | None) -> Source:\n source_dict = {}\n if _id:\n source_dict[\"id\"] = _id\n if display_name:\n source_dict[\"display_name\"] = display_name\n if source:\n source_dict[\"source\"] = source\n return Source(**source_dict)\n\n def message_response(self) -> Message:\n _source, _icon, _display_name, _source_id = self.get_properties_from_source_component()\n _background_color = self.background_color\n _text_color = self.text_color\n if self.chat_icon:\n _icon = self.chat_icon\n message = self.input_value if isinstance(self.input_value, Message) else Message(text=self.input_value)\n message.sender = self.sender\n message.sender_name = self.sender_name\n message.session_id = self.session_id\n message.flow_id = self.graph.flow_id if hasattr(self, \"graph\") else None\n message.properties.source = self._build_source(_source_id, _display_name, _source)\n message.properties.icon = _icon\n message.properties.background_color = _background_color\n message.properties.text_color = _text_color\n if self.session_id and isinstance(message, Message) and self.should_store_message:\n stored_message = self.send_message(\n message,\n )\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n"
},
"data_template": {
"_input_type": "MessageTextInput",
Expand Down
Loading

0 comments on commit 0dc6cce

Please # to comment.