Tai Truong
fix readme
d202ada
from collections.abc import Callable
from typing import Any, get_type_hints
from pydantic import ConfigDict, computed_field, create_model
from pydantic.fields import FieldInfo
def __validate_method(method: Callable) -> None:
"""Validates a method by checking if it has the required attributes.
This function ensures that the given method belongs to a class with the necessary
structure for output handling. It checks for the presence of a __self__ attribute
on the method and a get_output_by_method attribute on the method's class.
Args:
method (Callable): The method to be validated.
Raises:
ValueError: If the method does not have a __self__ attribute or if the method's
class does not have a get_output_by_method attribute.
Example:
>>> class ValidClass:
... def get_output_by_method(self):
... pass
... def valid_method(self):
... pass
>>> __validate_method(ValidClass().valid_method) # This will pass
>>> __validate_method(lambda x: x) # This will raise a ValueError
"""
if not hasattr(method, "__self__"):
msg = f"Method {method} does not have a __self__ attribute."
raise ValueError(msg)
if not hasattr(method.__self__, "get_output_by_method"):
msg = f"Method's class {method.__self__} must have a get_output_by_method attribute."
raise ValueError(msg)
def build_output_getter(method: Callable, *, validate: bool = True) -> Callable:
"""Builds an output getter function for a given method in a graph component.
This function creates a new callable that, when invoked, retrieves the output
of the specified method using the get_output_by_method of the method's class.
It's used in creating dynamic state models for graph components.
Args:
method (Callable): The method for which to build the output getter.
validate (bool, optional): Whether to validate the method before building
the getter. Defaults to True.
Returns:
Callable: The output getter function. When called, this function returns
the value of the output associated with the original method.
Raises:
ValueError: If the method has no return type annotation or if validation fails.
Notes:
- The getter function returns UNDEFINED if the output has not been set.
- When validate is True, the method must belong to a class with a
'get_output_by_method' attribute.
- This function is typically used internally by create_state_model.
Example:
>>> class ChatComponent:
... def get_output_by_method(self, method):
... return type('Output', (), {'value': "Hello, World!"})()
... def get_message(self) -> str:
... pass
>>> component = ChatComponent()
>>> getter = build_output_getter(component.get_message)
>>> print(getter(None)) # This will print "Hello, World!"
"""
def output_getter(_):
if validate:
__validate_method(method)
methods_class = method.__self__
output = methods_class.get_output_by_method(method)
return output.value
return_type = get_type_hints(method).get("return", None)
if return_type is None:
msg = f"Method {method.__name__} has no return type annotation."
raise ValueError(msg)
output_getter.__annotations__["return"] = return_type
return output_getter
def build_output_setter(method: Callable, *, validate: bool = True) -> Callable:
"""Build an output setter function for a given method in a graph component.
This function creates a new callable that, when invoked, sets the output
of the specified method using the get_output_by_method of the method's class.
It's used in creating dynamic state models for graph components, allowing
for the modification of component states.
Args:
method (Callable): The method for which the output setter is being built.
validate (bool, optional): Flag indicating whether to validate the method
before building the setter. Defaults to True.
Returns:
Callable: The output setter function. When called with a value, this function
sets the output associated with the original method to that value.
Raises:
ValueError: If validation fails when validate is True.
Notes:
- When validate is True, the method must belong to a class with a
'get_output_by_method' attribute.
- This function is typically used internally by create_state_model.
- The setter allows for dynamic updating of component states in a graph.
Example:
>>> class ChatComponent:
... def get_output_by_method(self, method):
... return type('Output', (), {'value': None})()
... def set_message(self):
... pass
>>> component = ChatComponent()
>>> setter = build_output_setter(component.set_message)
>>> setter(component, "New message")
>>> print(component.get_output_by_method(component.set_message).value) # Prints "New message"
"""
def output_setter(self, value) -> None: # noqa: ARG001
if validate:
__validate_method(method)
methods_class = method.__self__ # type: ignore[attr-defined]
output = methods_class.get_output_by_method(method)
output.value = value
return output_setter
def create_state_model(model_name: str = "State", *, validate: bool = True, **kwargs) -> type:
"""Create a dynamic Pydantic state model based on the provided keyword arguments.
This function generates a Pydantic model class with fields corresponding to the
provided keyword arguments. It can handle various types of field definitions,
including callable methods (which are converted to properties), FieldInfo objects,
and type-default value tuples.
Args:
model_name (str, optional): The name of the model. Defaults to "State".
validate (bool, optional): Whether to validate the methods when converting
them to properties. Defaults to True.
**kwargs: Keyword arguments representing the fields of the model. Each argument
can be a callable method, a FieldInfo object, or a tuple of (type, default).
Returns:
type: The dynamically created Pydantic state model class.
Raises:
ValueError: If the provided field value is invalid or cannot be processed.
Examples:
>>> from langflow.components.inputs import ChatInput
>>> from langflow.components.outputs.ChatOutput import ChatOutput
>>> from pydantic import Field
>>>
>>> chat_input = ChatInput()
>>> chat_output = ChatOutput()
>>>
>>> # Create a model with a method from a component
>>> StateModel = create_state_model(method_one=chat_input.message_response)
>>> state = StateModel()
>>> assert state.method_one is UNDEFINED
>>> chat_input.set_output_value("message", "test")
>>> assert state.method_one == "test"
>>>
>>> # Create a model with multiple components and a Pydantic Field
>>> NewStateModel = create_state_model(
... model_name="NewStateModel",
... first_method=chat_input.message_response,
... second_method=chat_output.message_response,
... my_attribute=Field(None)
... )
>>> new_state = NewStateModel()
>>> new_state.first_method = "test"
>>> new_state.my_attribute = 123
>>> assert new_state.first_method == "test"
>>> assert new_state.my_attribute == 123
>>>
>>> # Create a model with tuple-based field definitions
>>> TupleStateModel = create_state_model(field_one=(str, "default"), field_two=(int, 123))
>>> tuple_state = TupleStateModel()
>>> assert tuple_state.field_one == "default"
>>> assert tuple_state.field_two == 123
Notes:
- The function handles empty keyword arguments gracefully.
- For tuple-based field definitions, the first element must be a valid Python type.
- Unsupported value types in keyword arguments will raise a ValueError.
- Callable methods must have proper return type annotations and belong to a class
with a 'get_output_by_method' attribute when validate is True.
"""
fields = {}
for name, value in kwargs.items():
# Extract the return type from the method's type annotations
if callable(value):
# Define the field with the return type
try:
__validate_method(value)
getter = build_output_getter(value, validate=validate)
setter = build_output_setter(value, validate=validate)
property_method = property(getter, setter)
except ValueError as e:
# If the method is not valid,assume it is already a getter
if ("get_output_by_method" not in str(e) and "__self__" not in str(e)) or validate:
raise
property_method = value
fields[name] = computed_field(property_method)
elif isinstance(value, FieldInfo):
field_tuple = (value.annotation or Any, value)
fields[name] = field_tuple
elif isinstance(value, tuple) and len(value) == 2: # noqa: PLR2004
# Fields are defined by one of the following tuple forms:
# (<type>, <default value>)
# (<type>, Field(...))
# typing.Annotated[<type>, Field(...)]
if not isinstance(value[0], type):
msg = f"Invalid type for field {name}: {type(value[0])}"
raise TypeError(msg)
fields[name] = (value[0], value[1])
else:
msg = f"Invalid value type {type(value)} for field {name}"
raise ValueError(msg)
# Create the model dynamically
config_dict = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
return create_model(model_name, __config__=config_dict, **fields)