|
|
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: |
|
|
if validate: |
|
|
__validate_method(method) |
|
|
methods_class = method.__self__ |
|
|
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(): |
|
|
|
|
|
if callable(value): |
|
|
|
|
|
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 ("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: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
config_dict = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True) |
|
|
return create_model(model_name, __config__=config_dict, **fields) |
|
|
|