feat: improve Listen and Notify related logic and components (#7969)
* refactor: remove unused property and state management from StateVertex class * feat: extend next runnable vertices for activated vertices in state vertices * refactor: simplify ListenComponent by removing unused methods and updating inheritance * refactor: update NotifyComponent to use new input/output structure and improve context handling * feat: add is_state attribute to StateVertex for improved state management * refactor: update input validation to exclude custom attributes from reserved words * refactor: enhance ListenComponent and NotifyComponent with improved input types and output caching - Updated ListenComponent to include additional input types for context_key. - Modified NotifyComponent to use HandleInput for input_value and added input types. - Set cache to False for the output in NotifyComponent to ensure fresh data retrieval. * refactor: update state management in Graph class to improve handling of state vertices - Changed _is_state_vertices to be initialized as None and updated its type hint. - Added a property is_state_vertices to lazily compute state vertices when accessed. - Refactored activate_state_vertices method to utilize the new property for better clarity and performance. * refactor: update ListenComponent to return Data with text instead of data - Modified the build method in ListenComponent to return Data initialized with a text field instead of a data field, enhancing clarity in data handling. * refactor: update NotifyComponent to use input_value for data handling - Modified the build method in NotifyComponent to replace input_data with input_value for improved clarity and consistency in data processing. - Ensured that the context handling and status updates reflect the changes in input handling. * refactor: simplify context key check in Graph class for better readability * refactor: enhance Graph class to include recursive predecessor retrieval and update edge handling * 📝 Add docstrings to `try-to-fix-listen-notify` (#8326) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * refactor: clean up unused imports and improve docstring formatting across components * refactor: rename build method to notify_components and improve input handling in NotifyComponent * refactor: rename build method to listen_for_data in ListenComponent for clarity * fix: correct status assignment logic in NotifyComponent to ensure accurate state updates * fix: handle None input value in NotifyComponent to ensure default Data initialization * Make context_key required Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * 📝 Add docstrings to `try-to-fix-listen-notify` (#8442) Co-authored-by: Jordan Frazier <jordan.frazier@datastax.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Edwin Jose <edwin.jose@datastax.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> fix: remove duplicated import statement in nvidia_ingest (#8439) * ✨ Implement caching logic for Listen/Notify components and enhance docstring for edge retrieval - Added `_set_cache_if_listen_notify_components` method to disable caching for all vertices if Listen/Notify components are present, ensuring proper real-time communication. - Improved docstring for `_get_edges_as_list_of_tuples` to clarify the return value and structure. * 🔧 Refactor Graph class by removing state management methods - Removed `get_state` and `update_state` methods from the `Graph` class to streamline state management. - This change simplifies the class structure and focuses on enhancing the overall design of the graph handling. * fix(listen): restrict input types to "Message" for ListenComponent * refactor: remove deprecated components from deactivated module --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jordan Frazier <jordan.frazier@datastax.com> Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
parent
8ae85abb9f
commit
6f521e17ea
6 changed files with 272 additions and 58 deletions
|
|
@ -9,9 +9,7 @@ __all__ = [
|
||||||
"ExtractKeyFromDataComponent",
|
"ExtractKeyFromDataComponent",
|
||||||
"FlowToolComponent",
|
"FlowToolComponent",
|
||||||
"ListFlowsComponent",
|
"ListFlowsComponent",
|
||||||
"ListenComponent",
|
|
||||||
"MergeDataComponent",
|
"MergeDataComponent",
|
||||||
"NotifyComponent",
|
|
||||||
"PythonFunctionComponent",
|
"PythonFunctionComponent",
|
||||||
"RunFlowComponent",
|
"RunFlowComponent",
|
||||||
"SQLExecutorComponent",
|
"SQLExecutorComponent",
|
||||||
|
|
|
||||||
29
src/backend/base/langflow/components/logic/listen.py
Normal file
29
src/backend/base/langflow/components/logic/listen.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from langflow.custom import Component
|
||||||
|
from langflow.io import Output, StrInput
|
||||||
|
from langflow.schema.data import Data
|
||||||
|
|
||||||
|
|
||||||
|
class ListenComponent(Component):
|
||||||
|
display_name = "Listen"
|
||||||
|
description = "A component to listen for a notification."
|
||||||
|
name = "Listen"
|
||||||
|
beta: bool = True
|
||||||
|
icon = "Radio"
|
||||||
|
inputs = [
|
||||||
|
StrInput(
|
||||||
|
name="context_key",
|
||||||
|
display_name="Context Key",
|
||||||
|
info="The key of the context to listen for.",
|
||||||
|
input_types=["Message"],
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
outputs = [Output(name="data", display_name="Data", method="listen_for_data", cache=False)]
|
||||||
|
|
||||||
|
def listen_for_data(self) -> Data:
|
||||||
|
"""Retrieves a Data object from the component context using the provided context key.
|
||||||
|
|
||||||
|
If the specified context key does not exist in the context, returns an empty Data object.
|
||||||
|
"""
|
||||||
|
return self.ctx.get(self.context_key, Data(text=""))
|
||||||
88
src/backend/base/langflow/components/logic/notify.py
Normal file
88
src/backend/base/langflow/components/logic/notify.py
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from langflow.custom import Component
|
||||||
|
from langflow.io import BoolInput, HandleInput, Output, StrInput
|
||||||
|
from langflow.schema.data import Data
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyComponent(Component):
|
||||||
|
display_name = "Notify"
|
||||||
|
description = "A component to generate a notification to Get Notified component."
|
||||||
|
icon = "Notify"
|
||||||
|
name = "Notify"
|
||||||
|
beta: bool = True
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
StrInput(
|
||||||
|
name="context_key",
|
||||||
|
display_name="Context Key",
|
||||||
|
info="The key of the context to store the notification.",
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
HandleInput(
|
||||||
|
name="input_value",
|
||||||
|
display_name="Input Data",
|
||||||
|
info="The data to store.",
|
||||||
|
required=False,
|
||||||
|
input_types=["Data", "Message", "DataFrame"],
|
||||||
|
),
|
||||||
|
BoolInput(
|
||||||
|
name="append",
|
||||||
|
display_name="Append",
|
||||||
|
info="If True, the record will be appended to the notification.",
|
||||||
|
value=False,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
outputs = [
|
||||||
|
Output(
|
||||||
|
display_name="Data",
|
||||||
|
name="result",
|
||||||
|
method="notify_components",
|
||||||
|
cache=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
async def notify_components(self) -> Data:
|
||||||
|
"""Processes and stores a notification in the component's context.
|
||||||
|
|
||||||
|
Normalizes the input value to a `Data` object and stores it under the
|
||||||
|
specified context key. If `append` is True, adds the value to a list
|
||||||
|
of notifications; otherwise, replaces the existing value. Updates the
|
||||||
|
component's status and activates related state vertices in the graph.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The processed `Data` object stored in the context.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the component is not part of a graph.
|
||||||
|
"""
|
||||||
|
if not self._vertex:
|
||||||
|
msg = "Notify component must be used in a graph."
|
||||||
|
raise ValueError(msg)
|
||||||
|
input_value: Data | str | dict | None = self.input_value
|
||||||
|
if input_value is None:
|
||||||
|
input_value = Data(text="")
|
||||||
|
elif not isinstance(input_value, Data):
|
||||||
|
if isinstance(input_value, str):
|
||||||
|
input_value = Data(text=input_value)
|
||||||
|
elif isinstance(input_value, dict):
|
||||||
|
input_value = Data(data=input_value)
|
||||||
|
else:
|
||||||
|
input_value = Data(text=str(input_value))
|
||||||
|
if input_value:
|
||||||
|
if self.append:
|
||||||
|
current_data = self.ctx.get(self.context_key, [])
|
||||||
|
if not isinstance(current_data, list):
|
||||||
|
current_data = [current_data]
|
||||||
|
current_data.append(input_value)
|
||||||
|
self.update_ctx({self.context_key: current_data})
|
||||||
|
else:
|
||||||
|
self.update_ctx({self.context_key: input_value})
|
||||||
|
self.status = input_value
|
||||||
|
else:
|
||||||
|
self.status = "No record provided."
|
||||||
|
self._vertex.is_state = True
|
||||||
|
self.graph.activate_state_vertices(name=self.context_key, caller=self._id)
|
||||||
|
return cast(Data, input_value)
|
||||||
|
|
@ -873,6 +873,11 @@ class Component(CustomComponent):
|
||||||
|
|
||||||
def _validate_inputs(self, params: dict) -> None:
|
def _validate_inputs(self, params: dict) -> None:
|
||||||
# Params keys are the `name` attribute of the Input objects
|
# Params keys are the `name` attribute of the Input objects
|
||||||
|
"""Validates and assigns input values from the provided parameters dictionary.
|
||||||
|
|
||||||
|
For each parameter matching a defined input, sets the input's value and updates the parameter
|
||||||
|
dictionary with the validated value.
|
||||||
|
"""
|
||||||
for key, value in params.copy().items():
|
for key, value in params.copy().items():
|
||||||
if key not in self._inputs:
|
if key not in self._inputs:
|
||||||
continue
|
continue
|
||||||
|
|
@ -883,10 +888,16 @@ class Component(CustomComponent):
|
||||||
params[input_.name] = input_.value
|
params[input_.name] = input_.value
|
||||||
|
|
||||||
def set_attributes(self, params: dict) -> None:
|
def set_attributes(self, params: dict) -> None:
|
||||||
|
"""Sets component attributes from the given parameters, preventing conflicts with reserved attribute names.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If a parameter name matches a reserved attribute not managed in _attributes and its
|
||||||
|
value differs from the current attribute value.
|
||||||
|
"""
|
||||||
self._validate_inputs(params)
|
self._validate_inputs(params)
|
||||||
attributes = {}
|
attributes = {}
|
||||||
for key, value in params.items():
|
for key, value in params.items():
|
||||||
if key in self.__dict__ and value != getattr(self, key):
|
if key in self.__dict__ and key not in self._attributes and value != getattr(self, key):
|
||||||
msg = (
|
msg = (
|
||||||
f"{self.__class__.__name__} defines an input parameter named '{key}' "
|
f"{self.__class__.__name__} defines an input parameter named '{key}' "
|
||||||
f"that is a reserved word and cannot be used."
|
f"that is a reserved word and cannot be used."
|
||||||
|
|
|
||||||
|
|
@ -69,17 +69,12 @@ class Graph:
|
||||||
log_config: LogConfig | None = None,
|
log_config: LogConfig | None = None,
|
||||||
context: dict[str, Any] | None = None,
|
context: dict[str, Any] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initializes a new instance of the Graph class.
|
"""Initializes a new Graph instance.
|
||||||
|
|
||||||
Args:
|
If both start and end components are provided, the graph is initialized and prepared for execution.
|
||||||
start: The start component.
|
If only one is provided, a ValueError is raised. The context must be a dictionary if specified,
|
||||||
end: The end component.
|
otherwise a TypeError is raised. Internal data structures for vertices, edges, state management,
|
||||||
flow_id: The ID of the flow. Defaults to None.
|
run management, and tracing are set up during initialization.
|
||||||
flow_name: The flow name.
|
|
||||||
description: The graph description.
|
|
||||||
user_id: The user ID.
|
|
||||||
log_config: The log configuration.
|
|
||||||
context: Additional context for the graph. Defaults to None.
|
|
||||||
"""
|
"""
|
||||||
if log_config:
|
if log_config:
|
||||||
configure(**log_config)
|
configure(**log_config)
|
||||||
|
|
@ -96,7 +91,7 @@ class Graph:
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self._is_input_vertices: list[str] = []
|
self._is_input_vertices: list[str] = []
|
||||||
self._is_output_vertices: list[str] = []
|
self._is_output_vertices: list[str] = []
|
||||||
self._is_state_vertices: list[str] = []
|
self._is_state_vertices: list[str] | None = None
|
||||||
self.has_session_id_vertices: list[str] = []
|
self.has_session_id_vertices: list[str] = []
|
||||||
self._sorted_vertices_layers: list[list[str]] = []
|
self._sorted_vertices_layers: list[list[str]] = []
|
||||||
self._run_id = ""
|
self._run_id = ""
|
||||||
|
|
@ -491,26 +486,34 @@ class Graph:
|
||||||
self.build_graph_maps(self.edges)
|
self.build_graph_maps(self.edges)
|
||||||
self.define_vertices_lists()
|
self.define_vertices_lists()
|
||||||
|
|
||||||
def activate_state_vertices(self, name: str, caller: str) -> None:
|
@property
|
||||||
"""Activates the state vertices in the graph with the given name and caller.
|
def is_state_vertices(self) -> list[str]:
|
||||||
|
"""Returns a cached list of vertex IDs for vertices marked as state vertices.
|
||||||
|
|
||||||
Args:
|
The list is computed on first access by filtering vertices with `is_state` set to True and is
|
||||||
name (str): The name of the state.
|
cached for future calls.
|
||||||
caller (str): The ID of the vertex that is updating the state.
|
"""
|
||||||
|
if self._is_state_vertices is None:
|
||||||
|
self._is_state_vertices = [vertex.id for vertex in self.vertices if vertex.is_state]
|
||||||
|
return self._is_state_vertices
|
||||||
|
|
||||||
|
def activate_state_vertices(self, name: str, caller: str) -> None:
|
||||||
|
"""Activates vertices associated with a given state name.
|
||||||
|
|
||||||
|
Marks vertices with the specified state name, as well as their successors and related
|
||||||
|
predecessors. The state manager is then updated with the new state record.
|
||||||
"""
|
"""
|
||||||
vertices_ids = set()
|
vertices_ids = set()
|
||||||
new_predecessor_map = {}
|
new_predecessor_map = {}
|
||||||
for vertex_id in self._is_state_vertices:
|
activated_vertices = []
|
||||||
|
for vertex_id in self.is_state_vertices:
|
||||||
caller_vertex = self.get_vertex(caller)
|
caller_vertex = self.get_vertex(caller)
|
||||||
vertex = self.get_vertex(vertex_id)
|
vertex = self.get_vertex(vertex_id)
|
||||||
if vertex_id == caller or vertex.display_name == caller_vertex.display_name:
|
if vertex_id == caller or vertex.display_name == caller_vertex.display_name:
|
||||||
continue
|
continue
|
||||||
if (
|
ctx_key = vertex.raw_params.get("context_key")
|
||||||
isinstance(vertex.raw_params["name"], str)
|
if isinstance(ctx_key, str) and name in ctx_key and vertex_id != caller and isinstance(vertex, StateVertex):
|
||||||
and name in vertex.raw_params["name"]
|
activated_vertices.append(vertex_id)
|
||||||
and vertex_id != caller
|
|
||||||
and isinstance(vertex, StateVertex)
|
|
||||||
):
|
|
||||||
vertices_ids.add(vertex_id)
|
vertices_ids.add(vertex_id)
|
||||||
successors = self.get_all_successors(vertex, flat=True)
|
successors = self.get_all_successors(vertex, flat=True)
|
||||||
# Update run_manager.run_predecessors because we are activating vertices
|
# Update run_manager.run_predecessors because we are activating vertices
|
||||||
|
|
@ -519,8 +522,12 @@ class Graph:
|
||||||
# So we need to get all edges of the vertex and successors
|
# So we need to get all edges of the vertex and successors
|
||||||
# and run self.build_adjacency_maps(edges) to get the new predecessor map
|
# and run self.build_adjacency_maps(edges) to get the new predecessor map
|
||||||
# that is not complete but we can use to update the run_predecessors
|
# that is not complete but we can use to update the run_predecessors
|
||||||
|
successors_predecessors = set()
|
||||||
|
for sucessor in successors:
|
||||||
|
successors_predecessors.update(self.get_all_predecessors(sucessor))
|
||||||
|
|
||||||
edges_set = set()
|
edges_set = set()
|
||||||
for _vertex in [vertex, *successors]:
|
for _vertex in [vertex, *successors, *successors_predecessors]:
|
||||||
edges_set.update(_vertex.edges)
|
edges_set.update(_vertex.edges)
|
||||||
if _vertex.state == VertexStates.INACTIVE:
|
if _vertex.state == VertexStates.INACTIVE:
|
||||||
_vertex.set_state("ACTIVE")
|
_vertex.set_state("ACTIVE")
|
||||||
|
|
@ -533,7 +540,7 @@ class Graph:
|
||||||
vertices_ids.update(new_predecessor_map.keys())
|
vertices_ids.update(new_predecessor_map.keys())
|
||||||
vertices_ids.update(v_id for value_list in new_predecessor_map.values() for v_id in value_list)
|
vertices_ids.update(v_id for value_list in new_predecessor_map.values() for v_id in value_list)
|
||||||
|
|
||||||
self.activated_vertices = list(vertices_ids)
|
self.activated_vertices = activated_vertices
|
||||||
self.vertices_to_run.update(vertices_ids)
|
self.vertices_to_run.update(vertices_ids)
|
||||||
self.run_manager.update_run_state(
|
self.run_manager.update_run_state(
|
||||||
run_predecessors=new_predecessor_map,
|
run_predecessors=new_predecessor_map,
|
||||||
|
|
@ -649,17 +656,21 @@ class Graph:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_vertices_layers(self) -> list[list[str]]:
|
def sorted_vertices_layers(self) -> list[list[str]]:
|
||||||
"""The sorted layers of vertices in the graph.
|
"""Returns the sorted layers of vertex IDs by type.
|
||||||
|
|
||||||
Returns:
|
Each layer in the returned list contains vertex IDs grouped by their classification,
|
||||||
List[List[str]]: The sorted layers of vertices.
|
such as input, output, session, or state vertices. Sorting is performed if not already done.
|
||||||
"""
|
"""
|
||||||
if not self._sorted_vertices_layers:
|
if not self._sorted_vertices_layers:
|
||||||
self.sort_vertices()
|
self.sort_vertices()
|
||||||
return self._sorted_vertices_layers
|
return self._sorted_vertices_layers
|
||||||
|
|
||||||
def define_vertices_lists(self) -> None:
|
def define_vertices_lists(self) -> None:
|
||||||
"""Defines the lists of vertices that are inputs, outputs, and have session_id."""
|
"""Populates internal lists of input, output, session ID, and state vertex IDs.
|
||||||
|
|
||||||
|
Iterates over all vertices and appends their IDs to the corresponding internal lists
|
||||||
|
based on their classification.
|
||||||
|
"""
|
||||||
for vertex in self.vertices:
|
for vertex in self.vertices:
|
||||||
if vertex.is_input:
|
if vertex.is_input:
|
||||||
self._is_input_vertices.append(vertex.id)
|
self._is_input_vertices.append(vertex.id)
|
||||||
|
|
@ -668,9 +679,17 @@ class Graph:
|
||||||
if vertex.has_session_id:
|
if vertex.has_session_id:
|
||||||
self.has_session_id_vertices.append(vertex.id)
|
self.has_session_id_vertices.append(vertex.id)
|
||||||
if vertex.is_state:
|
if vertex.is_state:
|
||||||
|
if self._is_state_vertices is None:
|
||||||
|
self._is_state_vertices = []
|
||||||
self._is_state_vertices.append(vertex.id)
|
self._is_state_vertices.append(vertex.id)
|
||||||
|
|
||||||
def _set_inputs(self, input_components: list[str], inputs: dict[str, str], input_type: InputType | None) -> None:
|
def _set_inputs(self, input_components: list[str], inputs: dict[str, str], input_type: InputType | None) -> None:
|
||||||
|
"""Updates input vertices' parameters with the provided inputs, filtering by component list and input type.
|
||||||
|
|
||||||
|
Only vertices whose IDs or display names match the specified input components and whose IDs contain
|
||||||
|
the input type (unless input type is 'any' or None) are updated. Raises a ValueError if a specified
|
||||||
|
vertex is not found.
|
||||||
|
"""
|
||||||
for vertex_id in self._is_input_vertices:
|
for vertex_id in self._is_input_vertices:
|
||||||
vertex = self.get_vertex(vertex_id)
|
vertex = self.get_vertex(vertex_id)
|
||||||
# If the vertex is not in the input_components list
|
# If the vertex is not in the input_components list
|
||||||
|
|
@ -1194,6 +1213,7 @@ class Graph:
|
||||||
self._build_vertex_params()
|
self._build_vertex_params()
|
||||||
self._instantiate_components_in_vertices()
|
self._instantiate_components_in_vertices()
|
||||||
self._set_cache_to_vertices_in_cycle()
|
self._set_cache_to_vertices_in_cycle()
|
||||||
|
self._set_cache_if_listen_notify_components()
|
||||||
for vertex in self.vertices:
|
for vertex in self.vertices:
|
||||||
if vertex.id in self.cycle_vertices:
|
if vertex.id in self.cycle_vertices:
|
||||||
self.run_manager.add_to_cycle_vertices(vertex.id)
|
self.run_manager.add_to_cycle_vertices(vertex.id)
|
||||||
|
|
@ -1201,9 +1221,29 @@ class Graph:
|
||||||
self.assert_streaming_sequence()
|
self.assert_streaming_sequence()
|
||||||
|
|
||||||
def _get_edges_as_list_of_tuples(self) -> list[tuple[str, str]]:
|
def _get_edges_as_list_of_tuples(self) -> list[tuple[str, str]]:
|
||||||
"""Returns the edges of the graph as a list of tuples."""
|
"""Returns the edges of the graph as a list of tuples.
|
||||||
|
|
||||||
|
Each tuple contains the source and target handle IDs from the edge data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[tuple[str, str]]: List of (source_id, target_id) tuples representing graph edges.
|
||||||
|
"""
|
||||||
return [(e["data"]["sourceHandle"]["id"], e["data"]["targetHandle"]["id"]) for e in self._edges]
|
return [(e["data"]["sourceHandle"]["id"], e["data"]["targetHandle"]["id"]) for e in self._edges]
|
||||||
|
|
||||||
|
def _set_cache_if_listen_notify_components(self) -> None:
|
||||||
|
"""Disables caching for all vertices if Listen/Notify components are present.
|
||||||
|
|
||||||
|
If the graph contains any Listen or Notify components, caching is disabled for all vertices
|
||||||
|
by setting cache=False on their outputs. This ensures proper handling of real-time
|
||||||
|
communication between components.
|
||||||
|
"""
|
||||||
|
has_listen_or_notify_component = any(
|
||||||
|
vertex.id.split("-")[0] in {"Listen", "Notify"} for vertex in self.vertices
|
||||||
|
)
|
||||||
|
if has_listen_or_notify_component:
|
||||||
|
for vertex in self.vertices:
|
||||||
|
vertex.apply_on_outputs(lambda output_object: setattr(output_object, "cache", False))
|
||||||
|
|
||||||
def _set_cache_to_vertices_in_cycle(self) -> None:
|
def _set_cache_to_vertices_in_cycle(self) -> None:
|
||||||
"""Sets the cache to the vertices in cycle."""
|
"""Sets the cache to the vertices in cycle."""
|
||||||
edges = self._get_edges_as_list_of_tuples()
|
edges = self._get_edges_as_list_of_tuples()
|
||||||
|
|
@ -1528,6 +1568,11 @@ class Graph:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def find_next_runnable_vertices(self, vertex_successors_ids: list[str]) -> list[str]:
|
def find_next_runnable_vertices(self, vertex_successors_ids: list[str]) -> list[str]:
|
||||||
|
"""Determines the next set of runnable vertices from a list of successor vertex IDs.
|
||||||
|
|
||||||
|
For each successor, if it is not runnable, recursively finds its runnable
|
||||||
|
predecessors; otherwise, includes the successor itself. Returns a sorted list of all such vertex IDs.
|
||||||
|
"""
|
||||||
next_runnable_vertices = set()
|
next_runnable_vertices = set()
|
||||||
for v_id in sorted(vertex_successors_ids):
|
for v_id in sorted(vertex_successors_ids):
|
||||||
if not self.is_vertex_runnable(v_id):
|
if not self.is_vertex_runnable(v_id):
|
||||||
|
|
@ -1538,6 +1583,19 @@ class Graph:
|
||||||
return sorted(next_runnable_vertices)
|
return sorted(next_runnable_vertices)
|
||||||
|
|
||||||
async def get_next_runnable_vertices(self, lock: asyncio.Lock, vertex: Vertex, *, cache: bool = True) -> list[str]:
|
async def get_next_runnable_vertices(self, lock: asyncio.Lock, vertex: Vertex, *, cache: bool = True) -> list[str]:
|
||||||
|
"""Determines the next set of runnable vertex IDs after a vertex completes execution.
|
||||||
|
|
||||||
|
If the completed vertex is a state vertex, any recently activated state vertices are also included.
|
||||||
|
Updates the run manager to reflect the new runnable state and optionally caches the updated graph state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lock: An asyncio lock for thread-safe updates.
|
||||||
|
vertex: The vertex that has just finished execution.
|
||||||
|
cache: If True, caches the updated graph state.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of vertex IDs that are ready to be executed next.
|
||||||
|
"""
|
||||||
v_id = vertex.id
|
v_id = vertex.id
|
||||||
v_successors_ids = vertex.successors_ids
|
v_successors_ids = vertex.successors_ids
|
||||||
self.run_manager.ran_at_least_once.add(v_id)
|
self.run_manager.ran_at_least_once.add(v_id)
|
||||||
|
|
@ -1553,25 +1611,15 @@ class Graph:
|
||||||
if cache and self.flow_id is not None:
|
if cache and self.flow_id is not None:
|
||||||
set_cache_coro = partial(get_chat_service().set_cache, key=self.flow_id)
|
set_cache_coro = partial(get_chat_service().set_cache, key=self.flow_id)
|
||||||
await set_cache_coro(data=self, lock=lock)
|
await set_cache_coro(data=self, lock=lock)
|
||||||
|
if vertex.is_state:
|
||||||
|
next_runnable_vertices.extend(self.activated_vertices)
|
||||||
return next_runnable_vertices
|
return next_runnable_vertices
|
||||||
|
|
||||||
async def _log_vertex_build_from_exception(self, vertex_id: str, result: Exception) -> None:
|
async def _log_vertex_build_from_exception(self, vertex_id: str, result: Exception) -> None:
|
||||||
"""Log a vertex build failure caused by an exception.
|
"""Logs detailed information about a vertex build exception.
|
||||||
|
|
||||||
This method handles formatting and logging errors that occur during vertex building.
|
Formats the exception message and stack trace, constructs an error output,
|
||||||
It creates appropriate error output structures and logs the build failure.
|
and records the failure using the vertex build logging system.
|
||||||
|
|
||||||
Args:
|
|
||||||
vertex_id: The ID of the vertex that failed to build
|
|
||||||
result: The exception that caused the build failure
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
|
|
||||||
Side effects:
|
|
||||||
- Logs the exception details
|
|
||||||
- Creates error output structures
|
|
||||||
- Calls log_vertex_build to record the failure
|
|
||||||
"""
|
"""
|
||||||
if isinstance(result, ComponentBuildError):
|
if isinstance(result, ComponentBuildError):
|
||||||
params = result.message
|
params = result.message
|
||||||
|
|
@ -1706,6 +1754,17 @@ class Graph:
|
||||||
return [self.get_vertex(source_id) for source_id in self.predecessor_map.get(vertex.id, [])]
|
return [self.get_vertex(source_id) for source_id in self.predecessor_map.get(vertex.id, [])]
|
||||||
|
|
||||||
def get_all_successors(self, vertex: Vertex, *, recursive=True, flat=True, visited=None):
|
def get_all_successors(self, vertex: Vertex, *, recursive=True, flat=True, visited=None):
|
||||||
|
"""Returns all successors of a given vertex, optionally recursively and as a flat or nested list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vertex: The vertex whose successors are to be retrieved.
|
||||||
|
recursive: If True, retrieves successors recursively; otherwise, only immediate successors.
|
||||||
|
flat: If True, returns a flat list of successors; if False, returns a nested list structure.
|
||||||
|
visited: Internal set used to track visited vertices and prevent cycles.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of successor vertices, either flat or nested depending on the `flat` parameter.
|
||||||
|
"""
|
||||||
if visited is None:
|
if visited is None:
|
||||||
visited = set()
|
visited = set()
|
||||||
|
|
||||||
|
|
@ -1739,11 +1798,37 @@ class Graph:
|
||||||
return successors_result
|
return successors_result
|
||||||
|
|
||||||
def get_successors(self, vertex: Vertex) -> list[Vertex]:
|
def get_successors(self, vertex: Vertex) -> list[Vertex]:
|
||||||
"""Returns the successors of a vertex."""
|
"""Returns the immediate successor vertices of the given vertex.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vertex: The vertex whose successors are to be retrieved.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of vertices that are direct successors of the specified vertex.
|
||||||
|
"""
|
||||||
return [self.get_vertex(target_id) for target_id in self.successor_map.get(vertex.id, set())]
|
return [self.get_vertex(target_id) for target_id in self.successor_map.get(vertex.id, set())]
|
||||||
|
|
||||||
|
def get_all_predecessors(self, vertex: Vertex, *, recursive: bool = True) -> list[Vertex]:
|
||||||
|
"""Retrieves all predecessor vertices of a given vertex.
|
||||||
|
|
||||||
|
If `recursive` is True, returns both direct and indirect predecessors by
|
||||||
|
traversing the graph recursively. If False, returns only the immediate predecessors.
|
||||||
|
"""
|
||||||
|
_predecessors = self.predecessor_map.get(vertex.id, [])
|
||||||
|
predecessors = [self.get_vertex(v_id) for v_id in _predecessors]
|
||||||
|
if recursive:
|
||||||
|
for predecessor in _predecessors:
|
||||||
|
predecessors.extend(self.get_all_predecessors(self.get_vertex(predecessor), recursive=recursive))
|
||||||
|
else:
|
||||||
|
predecessors.extend([self.get_vertex(predecessor) for predecessor in _predecessors])
|
||||||
|
return predecessors
|
||||||
|
|
||||||
def get_vertex_neighbors(self, vertex: Vertex) -> dict[Vertex, int]:
|
def get_vertex_neighbors(self, vertex: Vertex) -> dict[Vertex, int]:
|
||||||
"""Returns the neighbors of a vertex."""
|
"""Returns a dictionary mapping each direct neighbor of a vertex to the count of connecting edges.
|
||||||
|
|
||||||
|
A neighbor is any vertex directly connected to the input vertex, either as a source or target.
|
||||||
|
The count reflects the number of edges between the input vertex and each neighbor.
|
||||||
|
"""
|
||||||
neighbors: dict[Vertex, int] = {}
|
neighbors: dict[Vertex, int] = {}
|
||||||
for edge in self.edges:
|
for edge in self.edges:
|
||||||
if edge.source_id == vertex.id:
|
if edge.source_id == vertex.id:
|
||||||
|
|
|
||||||
|
|
@ -463,18 +463,21 @@ class InterfaceVertex(ComponentVertex):
|
||||||
|
|
||||||
class StateVertex(ComponentVertex):
|
class StateVertex(ComponentVertex):
|
||||||
def __init__(self, data: NodeData, graph):
|
def __init__(self, data: NodeData, graph):
|
||||||
|
"""Initializes a StateVertex with the provided node data and graph.
|
||||||
|
|
||||||
|
Sets up the build steps and marks the vertex as a state vertex.
|
||||||
|
"""
|
||||||
super().__init__(data, graph=graph)
|
super().__init__(data, graph=graph)
|
||||||
self.steps = [self._build]
|
self.steps = [self._build]
|
||||||
self.is_state = False
|
self.is_state = True
|
||||||
|
|
||||||
@property
|
|
||||||
def successors_ids(self) -> list[str]:
|
|
||||||
if self._successors_ids is None:
|
|
||||||
self.is_state = False
|
|
||||||
return super().successors_ids
|
|
||||||
return self._successors_ids
|
|
||||||
|
|
||||||
def built_object_repr(self):
|
def built_object_repr(self):
|
||||||
|
"""Returns a string representation of the built object from the artifacts if available.
|
||||||
|
|
||||||
|
If the artifacts dictionary contains a non-empty "repr" key, its value is returned.
|
||||||
|
If the "repr" value is falsy, falls back to the superclass representation.
|
||||||
|
Returns None if no representation is available.
|
||||||
|
"""
|
||||||
if self.artifacts and "repr" in self.artifacts:
|
if self.artifacts and "repr" in self.artifacts:
|
||||||
return self.artifacts["repr"] or super().built_object_repr()
|
return self.artifacts["repr"] or super().built_object_repr()
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue