refactor: runnable_vertices_manager.py (#2646)
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
parent
537e358b65
commit
d93382e90a
6 changed files with 290 additions and 156 deletions
|
|
@ -12,7 +12,6 @@ from langflow.services.store.schema import StoreComponentCreate
|
||||||
from langflow.services.store.utils import get_lf_version_from_pypi
|
from langflow.services.store.utils import get_lf_version_from_pypi
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from langflow.graph.vertex.base import Vertex
|
|
||||||
from langflow.services.database.models.flow.model import Flow
|
from langflow.services.database.models.flow.model import Flow
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -179,43 +178,6 @@ def format_exception_message(exc: Exception) -> str:
|
||||||
return str(exc)
|
return str(exc)
|
||||||
|
|
||||||
|
|
||||||
async def get_next_runnable_vertices(
|
|
||||||
graph: Graph,
|
|
||||||
vertex: "Vertex",
|
|
||||||
vertex_id: str,
|
|
||||||
chat_service: ChatService,
|
|
||||||
flow_id: str,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Retrieves the next runnable vertices in the graph for a given vertex.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
graph (Graph): The graph object representing the flow.
|
|
||||||
vertex (Vertex): The current vertex.
|
|
||||||
vertex_id (str): The ID of the current vertex.
|
|
||||||
chat_service (ChatService): The chat service object.
|
|
||||||
flow_id (str): The ID of the flow.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: A list of IDs of the next runnable vertices.
|
|
||||||
|
|
||||||
"""
|
|
||||||
async with chat_service._async_cache_locks[flow_id] as lock:
|
|
||||||
graph.remove_from_predecessors(vertex_id)
|
|
||||||
direct_successors_ready = [v for v in vertex.successors_ids if graph.is_vertex_runnable(v)]
|
|
||||||
if not direct_successors_ready:
|
|
||||||
# No direct successors ready, look for runnable predecessors of successors
|
|
||||||
next_runnable_vertices = graph.find_runnable_predecessors_for_successors(vertex_id)
|
|
||||||
else:
|
|
||||||
next_runnable_vertices = direct_successors_ready
|
|
||||||
|
|
||||||
for v_id in set(next_runnable_vertices): # Use set to avoid duplicates
|
|
||||||
graph.vertices_to_run.remove(v_id)
|
|
||||||
graph.remove_from_predecessors(v_id)
|
|
||||||
await chat_service.set_cache(key=flow_id, data=graph, lock=lock)
|
|
||||||
return next_runnable_vertices
|
|
||||||
|
|
||||||
|
|
||||||
def get_top_level_vertices(graph, vertices_ids):
|
def get_top_level_vertices(graph, vertices_ids):
|
||||||
"""
|
"""
|
||||||
Retrieves the top-level vertices from the given graph based on the provided vertex IDs.
|
Retrieves the top-level vertices from the given graph based on the provided vertex IDs.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
from functools import partial
|
|
||||||
from typing import TYPE_CHECKING, Annotated, Optional
|
from typing import TYPE_CHECKING, Annotated, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException
|
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException
|
||||||
|
|
@ -202,11 +201,8 @@ async def build_vertex(
|
||||||
inputs_dict=inputs.model_dump() if inputs else {},
|
inputs_dict=inputs.model_dump() if inputs else {},
|
||||||
files=files,
|
files=files,
|
||||||
)
|
)
|
||||||
set_cache_coro = partial(get_chat_service().set_cache, key=flow_id_str)
|
next_runnable_vertices = await graph.get_next_runnable_vertices(lock, vertex=vertex, cache=False)
|
||||||
next_runnable_vertices = await graph.run_manager.get_next_runnable_vertices(
|
top_level_vertices = graph.get_top_level_vertices(next_runnable_vertices)
|
||||||
lock, set_cache_coro, graph=graph, vertex=vertex, cache=False
|
|
||||||
)
|
|
||||||
top_level_vertices = graph.run_manager.get_top_level_vertices(graph, next_runnable_vertices)
|
|
||||||
|
|
||||||
result_data_response = ResultDataResponse.model_validate(result_dict, from_attributes=True)
|
result_data_response = ResultDataResponse.model_validate(result_dict, from_attributes=True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
@ -292,7 +288,7 @@ async def build_vertex(
|
||||||
componentErrorMessage=str(exc),
|
componentErrorMessage=str(exc),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
logger.error(f"Error building Component:\n\n{exc}")
|
logger.error(f"Error building Component: \n\n{exc}")
|
||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
message = parse_exception(exc)
|
message = parse_exception(exc)
|
||||||
raise HTTPException(status_code=500, detail=message) from exc
|
raise HTTPException(status_code=500, detail=message) from exc
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,8 @@ class Graph:
|
||||||
for successor in successors:
|
for successor in successors:
|
||||||
if successor.params.get("stream") or successor.params.get("streaming"):
|
if successor.params.get("stream") or successor.params.get("streaming"):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Components {vertex.display_name} and {successor.display_name} are connected and both have stream or streaming set to True"
|
f"Components {vertex.display_name} and {successor.display_name} "
|
||||||
|
"are connected and both have stream or streaming set to True"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -438,7 +439,7 @@ class Graph:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
inputs (list[Dict[str, str]]): The input values for the graph.
|
inputs (list[Dict[str, str]]): The input values for the graph.
|
||||||
inputs_components (Optional[list[list[str]]], optional): The components to run for the inputs. Defaults to None.
|
inputs_components (Optional[list[list[str]]], optional): Components to run for the inputs. Defaults to None.
|
||||||
outputs (Optional[list[str]], optional): The outputs to retrieve from the graph. Defaults to None.
|
outputs (Optional[list[str]], optional): The outputs to retrieve from the graph. Defaults to None.
|
||||||
session_id (Optional[str], optional): The session ID for the graph. Defaults to None.
|
session_id (Optional[str], optional): The session ID for the graph. Defaults to None.
|
||||||
stream (bool, optional): Whether to stream the results or not. Defaults to False.
|
stream (bool, optional): Whether to stream the results or not. Defaults to False.
|
||||||
|
|
@ -909,7 +910,7 @@ class Graph:
|
||||||
return result_dict, params, valid, artifacts, vertex
|
return result_dict, params, valid, artifacts, vertex
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if not isinstance(exc, ComponentBuildException):
|
if not isinstance(exc, ComponentBuildException):
|
||||||
logger.exception(f"Error building Component:\n\n{exc}")
|
logger.exception(f"Error building Component: \n\n{exc}")
|
||||||
flow_id = self.flow_id
|
flow_id = self.flow_id
|
||||||
log_transaction(flow_id, vertex, status="failure", error=str(exc))
|
log_transaction(flow_id, vertex, status="failure", error=str(exc))
|
||||||
raise exc
|
raise exc
|
||||||
|
|
@ -987,6 +988,29 @@ class Graph:
|
||||||
logger.debug("Graph processing complete")
|
logger.debug("Graph processing complete")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def find_next_runnable_vertices(self, vertex_id: str, vertex_successors_ids: List[str]) -> List[str]:
|
||||||
|
direct_successors_ready = [v_id for v_id in vertex_successors_ids if self.is_vertex_runnable(v_id)]
|
||||||
|
if not direct_successors_ready:
|
||||||
|
return self.find_runnable_predecessors_for_successors(vertex_id)
|
||||||
|
return direct_successors_ready
|
||||||
|
|
||||||
|
async def get_next_runnable_vertices(self, lock: asyncio.Lock, vertex: "Vertex", cache: bool = True) -> List[str]:
|
||||||
|
v_id = vertex.id
|
||||||
|
v_successors_ids = vertex.successors_ids
|
||||||
|
async with lock:
|
||||||
|
self.run_manager.remove_vertex_from_runnables(v_id)
|
||||||
|
next_runnable_vertices = self.find_next_runnable_vertices(v_id, v_successors_ids)
|
||||||
|
|
||||||
|
for i in set(next_runnable_vertices): # Use set to avoid duplicates
|
||||||
|
if i == v_id:
|
||||||
|
next_runnable_vertices.remove(v_id)
|
||||||
|
else:
|
||||||
|
self.run_manager.add_to_vertices_being_run(v_id)
|
||||||
|
if cache:
|
||||||
|
set_cache_coro = partial(get_chat_service().set_cache, key=self.flow_id)
|
||||||
|
await set_cache_coro(self, lock)
|
||||||
|
return next_runnable_vertices
|
||||||
|
|
||||||
async def _execute_tasks(self, tasks: List[asyncio.Task], lock: asyncio.Lock) -> List[str]:
|
async def _execute_tasks(self, tasks: List[asyncio.Task], lock: asyncio.Lock) -> List[str]:
|
||||||
"""Executes tasks in parallel, handling exceptions for each task."""
|
"""Executes tasks in parallel, handling exceptions for each task."""
|
||||||
results = []
|
results = []
|
||||||
|
|
@ -1012,11 +1036,8 @@ class Graph:
|
||||||
# This could usually happen with input vertices like ChatInput
|
# This could usually happen with input vertices like ChatInput
|
||||||
self.run_manager.remove_vertex_from_runnables(v.id)
|
self.run_manager.remove_vertex_from_runnables(v.id)
|
||||||
|
|
||||||
set_cache_coro = partial(get_chat_service().set_cache, key=self.flow_id)
|
|
||||||
for v in vertices:
|
for v in vertices:
|
||||||
next_runnable_vertices = await self.run_manager.get_next_runnable_vertices(
|
next_runnable_vertices = await self.get_next_runnable_vertices(lock, vertex=v, cache=False)
|
||||||
lock, set_cache_coro, graph=self, vertex=v, cache=False
|
|
||||||
)
|
|
||||||
results.extend(next_runnable_vertices)
|
results.extend(next_runnable_vertices)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
@ -1197,7 +1218,7 @@ class Graph:
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
vertex_ids = [vertex.id for vertex in self.vertices]
|
vertex_ids = [vertex.id for vertex in self.vertices]
|
||||||
edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges])
|
edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges])
|
||||||
return f"Graph:\nNodes: {vertex_ids}\nConnections:\n{edges_repr}"
|
return f"Graph: \nNodes: {vertex_ids}\nConnections: \n{edges_repr}"
|
||||||
|
|
||||||
def layered_topological_sort(
|
def layered_topological_sort(
|
||||||
self,
|
self,
|
||||||
|
|
@ -1412,7 +1433,8 @@ class Graph:
|
||||||
|
|
||||||
def is_vertex_runnable(self, vertex_id: str) -> bool:
|
def is_vertex_runnable(self, vertex_id: str) -> bool:
|
||||||
"""Returns whether a vertex is runnable."""
|
"""Returns whether a vertex is runnable."""
|
||||||
return self.run_manager.is_vertex_runnable(self.get_vertex(vertex_id))
|
is_active = self.get_vertex(vertex_id).is_active()
|
||||||
|
return self.run_manager.is_vertex_runnable(vertex_id, is_active)
|
||||||
|
|
||||||
def build_run_map(self):
|
def build_run_map(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1432,7 +1454,26 @@ class Graph:
|
||||||
This checks the direct predecessors of each successor to identify any that are
|
This checks the direct predecessors of each successor to identify any that are
|
||||||
immediately runnable, expanding the search to ensure progress can be made.
|
immediately runnable, expanding the search to ensure progress can be made.
|
||||||
"""
|
"""
|
||||||
return self.run_manager.find_runnable_predecessors_for_successors(self.get_vertex(vertex_id))
|
runnable_vertices = []
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
def find_runnable_predecessors(predecessor: "Vertex"):
|
||||||
|
predecessor_id = predecessor.id
|
||||||
|
if predecessor_id in visited:
|
||||||
|
return
|
||||||
|
visited.add(predecessor_id)
|
||||||
|
is_active = self.get_vertex(predecessor_id).is_active()
|
||||||
|
if self.run_manager.is_vertex_runnable(predecessor_id, is_active):
|
||||||
|
runnable_vertices.append(predecessor_id)
|
||||||
|
else:
|
||||||
|
for pred_pred_id in self.run_manager.run_predecessors.get(predecessor_id, []):
|
||||||
|
find_runnable_predecessors(self.get_vertex(pred_pred_id))
|
||||||
|
|
||||||
|
for successor_id in self.run_manager.run_map.get(vertex_id, []):
|
||||||
|
for predecessor_id in self.run_manager.run_predecessors.get(successor_id, []):
|
||||||
|
find_runnable_predecessors(self.get_vertex(predecessor_id))
|
||||||
|
|
||||||
|
return runnable_vertices
|
||||||
|
|
||||||
def remove_from_predecessors(self, vertex_id: str):
|
def remove_from_predecessors(self, vertex_id: str):
|
||||||
self.run_manager.remove_from_predecessors(vertex_id)
|
self.run_manager.remove_from_predecessors(vertex_id)
|
||||||
|
|
@ -1440,6 +1481,26 @@ class Graph:
|
||||||
def remove_vertex_from_runnables(self, vertex_id: str):
|
def remove_vertex_from_runnables(self, vertex_id: str):
|
||||||
self.run_manager.remove_vertex_from_runnables(vertex_id)
|
self.run_manager.remove_vertex_from_runnables(vertex_id)
|
||||||
|
|
||||||
|
def get_top_level_vertices(self, vertices_ids):
|
||||||
|
"""
|
||||||
|
Retrieves the top-level vertices from the given graph based on the provided vertex IDs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vertices_ids (list): A list of vertex IDs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of top-level vertex IDs.
|
||||||
|
|
||||||
|
"""
|
||||||
|
top_level_vertices = []
|
||||||
|
for vertex_id in vertices_ids:
|
||||||
|
vertex = self.get_vertex(vertex_id)
|
||||||
|
if vertex.parent_is_top_level:
|
||||||
|
top_level_vertices.append(vertex.parent_node_id)
|
||||||
|
else:
|
||||||
|
top_level_vertices.append(vertex_id)
|
||||||
|
return top_level_vertices
|
||||||
|
|
||||||
def build_in_degree(self, edges: List[ContractEdge]) -> Dict[str, int]:
|
def build_in_degree(self, edges: List[ContractEdge]) -> Dict[str, int]:
|
||||||
in_degree: Dict[str, int] = defaultdict(int)
|
in_degree: Dict[str, int] = defaultdict(int)
|
||||||
for edge in edges:
|
for edge in edges:
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
import asyncio
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import TYPE_CHECKING, Callable, Coroutine, List
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from langflow.graph.graph.base import Graph
|
|
||||||
from langflow.graph.vertex.base import Vertex
|
|
||||||
|
|
||||||
|
|
||||||
class RunnableVerticesManager:
|
class RunnableVerticesManager:
|
||||||
|
|
@ -19,6 +13,7 @@ class RunnableVerticesManager:
|
||||||
"run_map": self.run_map,
|
"run_map": self.run_map,
|
||||||
"run_predecessors": self.run_predecessors,
|
"run_predecessors": self.run_predecessors,
|
||||||
"vertices_to_run": self.vertices_to_run,
|
"vertices_to_run": self.vertices_to_run,
|
||||||
|
"vertices_being_run": self.vertices_being_run,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -27,6 +22,7 @@ class RunnableVerticesManager:
|
||||||
instance.run_map = data["run_map"]
|
instance.run_map = data["run_map"]
|
||||||
instance.run_predecessors = data["run_predecessors"]
|
instance.run_predecessors = data["run_predecessors"]
|
||||||
instance.vertices_to_run = data["vertices_to_run"]
|
instance.vertices_to_run = data["vertices_to_run"]
|
||||||
|
instance.vertices_being_run = data["vertices_being_run"]
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def __getstate__(self) -> object:
|
def __getstate__(self) -> object:
|
||||||
|
|
@ -34,12 +30,14 @@ class RunnableVerticesManager:
|
||||||
"run_map": self.run_map,
|
"run_map": self.run_map,
|
||||||
"run_predecessors": self.run_predecessors,
|
"run_predecessors": self.run_predecessors,
|
||||||
"vertices_to_run": self.vertices_to_run,
|
"vertices_to_run": self.vertices_to_run,
|
||||||
|
"vertices_being_run": self.vertices_being_run,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __setstate__(self, state: dict) -> None:
|
def __setstate__(self, state: dict) -> None:
|
||||||
self.run_map = state["run_map"]
|
self.run_map = state["run_map"]
|
||||||
self.run_predecessors = state["run_predecessors"]
|
self.run_predecessors = state["run_predecessors"]
|
||||||
self.vertices_to_run = state["vertices_to_run"]
|
self.vertices_to_run = state["vertices_to_run"]
|
||||||
|
self.vertices_being_run = state["vertices_being_run"]
|
||||||
|
|
||||||
def all_predecessors_are_fulfilled(self) -> bool:
|
def all_predecessors_are_fulfilled(self) -> bool:
|
||||||
return all(not value for value in self.run_predecessors.values())
|
return all(not value for value in self.run_predecessors.values())
|
||||||
|
|
@ -49,42 +47,21 @@ class RunnableVerticesManager:
|
||||||
self.vertices_to_run.update(vertices_to_run)
|
self.vertices_to_run.update(vertices_to_run)
|
||||||
self.build_run_map(self.run_predecessors, self.vertices_to_run)
|
self.build_run_map(self.run_predecessors, self.vertices_to_run)
|
||||||
|
|
||||||
def is_vertex_runnable(self, vertex: "Vertex") -> bool:
|
def is_vertex_runnable(self, vertex_id: str, is_active: bool) -> bool:
|
||||||
"""Determines if a vertex is runnable."""
|
"""Determines if a vertex is runnable."""
|
||||||
|
if not is_active:
|
||||||
return (
|
return False
|
||||||
vertex.is_active()
|
if vertex_id in self.vertices_being_run:
|
||||||
and self.are_all_predecessors_fulfilled(vertex.id)
|
return False
|
||||||
and vertex.id in self.vertices_to_run
|
if vertex_id not in self.vertices_to_run:
|
||||||
and vertex.id not in self.vertices_being_run
|
return False
|
||||||
)
|
if not self.are_all_predecessors_fulfilled(vertex_id):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def are_all_predecessors_fulfilled(self, vertex_id: str) -> bool:
|
def are_all_predecessors_fulfilled(self, vertex_id: str) -> bool:
|
||||||
return not any(self.run_predecessors.get(vertex_id, []))
|
return not any(self.run_predecessors.get(vertex_id, []))
|
||||||
|
|
||||||
def find_runnable_predecessors_for_successors(self, vertex: "Vertex") -> List[str]:
|
|
||||||
"""Finds runnable predecessors for the successors of a given vertex."""
|
|
||||||
runnable_vertices = []
|
|
||||||
visited = set()
|
|
||||||
get_vertex = vertex.graph.get_vertex
|
|
||||||
|
|
||||||
def find_runnable_predecessors(predecessor: "Vertex"):
|
|
||||||
predecessor_id = predecessor.id
|
|
||||||
if predecessor_id in visited:
|
|
||||||
return
|
|
||||||
visited.add(predecessor_id)
|
|
||||||
if self.is_vertex_runnable(predecessor):
|
|
||||||
runnable_vertices.append(predecessor_id)
|
|
||||||
else:
|
|
||||||
for pred_pred_id in self.run_predecessors.get(predecessor_id, []):
|
|
||||||
find_runnable_predecessors(get_vertex(pred_pred_id))
|
|
||||||
|
|
||||||
for successor_id in self.run_map.get(vertex.id, []):
|
|
||||||
for predecessor_id in self.run_predecessors.get(successor_id, []):
|
|
||||||
find_runnable_predecessors(get_vertex(predecessor_id))
|
|
||||||
|
|
||||||
return runnable_vertices
|
|
||||||
|
|
||||||
def remove_from_predecessors(self, vertex_id: str):
|
def remove_from_predecessors(self, vertex_id: str):
|
||||||
"""Removes a vertex from the predecessor list of its successors."""
|
"""Removes a vertex from the predecessor list of its successors."""
|
||||||
predecessors = self.run_map.get(vertex_id, [])
|
predecessors = self.run_map.get(vertex_id, [])
|
||||||
|
|
@ -108,71 +85,9 @@ class RunnableVerticesManager:
|
||||||
else:
|
else:
|
||||||
self.vertices_being_run.discard(vertex_id)
|
self.vertices_being_run.discard(vertex_id)
|
||||||
|
|
||||||
async def get_next_runnable_vertices(
|
|
||||||
self,
|
|
||||||
lock: asyncio.Lock,
|
|
||||||
set_cache_coro: Callable[["Graph", asyncio.Lock], Coroutine],
|
|
||||||
graph: "Graph",
|
|
||||||
vertex: "Vertex",
|
|
||||||
cache: bool = True,
|
|
||||||
) -> List[str]:
|
|
||||||
"""
|
|
||||||
Retrieves the next runnable vertices in the graph for a given vertex.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
lock (asyncio.Lock): The lock object to be used for synchronization.
|
|
||||||
set_cache_coro (Callable): The coroutine function to set the cache.
|
|
||||||
graph (Graph): The graph object containing the vertices.
|
|
||||||
vertex (Vertex): The vertex object for which the next runnable vertices are to be retrieved.
|
|
||||||
cache (bool, optional): A flag to indicate if the cache should be updated. Defaults to True.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: A list of IDs of the next runnable vertices.
|
|
||||||
|
|
||||||
"""
|
|
||||||
async with lock:
|
|
||||||
self.remove_vertex_from_runnables(vertex.id)
|
|
||||||
direct_successors_ready = [v for v in vertex.successors_ids if self.is_vertex_runnable(graph.get_vertex(v))]
|
|
||||||
if not direct_successors_ready:
|
|
||||||
# No direct successors ready, look for runnable predecessors of successors
|
|
||||||
next_runnable_vertices = self.find_runnable_predecessors_for_successors(vertex)
|
|
||||||
else:
|
|
||||||
next_runnable_vertices = direct_successors_ready
|
|
||||||
|
|
||||||
for v_id in set(next_runnable_vertices): # Use set to avoid duplicates
|
|
||||||
if vertex.id == v_id:
|
|
||||||
next_runnable_vertices.remove(v_id)
|
|
||||||
else:
|
|
||||||
self.add_to_vertices_being_run(v_id)
|
|
||||||
if cache:
|
|
||||||
await set_cache_coro(data=graph, lock=lock) # type: ignore
|
|
||||||
return next_runnable_vertices
|
|
||||||
|
|
||||||
def remove_vertex_from_runnables(self, v_id):
|
def remove_vertex_from_runnables(self, v_id):
|
||||||
self.update_vertex_run_state(v_id, is_runnable=False)
|
self.update_vertex_run_state(v_id, is_runnable=False)
|
||||||
self.remove_from_predecessors(v_id)
|
self.remove_from_predecessors(v_id)
|
||||||
|
|
||||||
def add_to_vertices_being_run(self, v_id):
|
def add_to_vertices_being_run(self, v_id):
|
||||||
self.vertices_being_run.add(v_id)
|
self.vertices_being_run.add(v_id)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_top_level_vertices(graph, vertices_ids):
|
|
||||||
"""
|
|
||||||
Retrieves the top-level vertices from the given graph based on the provided vertex IDs.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
graph (Graph): The graph object containing the vertices.
|
|
||||||
vertices_ids (list): A list of vertex IDs.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: A list of top-level vertex IDs.
|
|
||||||
|
|
||||||
"""
|
|
||||||
top_level_vertices = []
|
|
||||||
for vertex_id in vertices_ids:
|
|
||||||
vertex = graph.get_vertex(vertex_id)
|
|
||||||
if vertex.parent_is_top_level:
|
|
||||||
top_level_vertices.append(vertex.parent_node_id)
|
|
||||||
else:
|
|
||||||
top_level_vertices.append(vertex_id)
|
|
||||||
return top_level_vertices
|
|
||||||
|
|
|
||||||
200
tests/unit/graph/graph/test_runnable_vertices_manager.py
Normal file
200
tests/unit/graph/graph/test_runnable_vertices_manager.py
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
import pickle
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from langflow.graph.graph.runnable_vertices_manager import RunnableVerticesManager
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def data():
|
||||||
|
run_map: defaultdict(list) = {"A": ["B", "C"], "B": ["D"], "C": ["D"], "D": []}
|
||||||
|
run_predecessors: defaultdict(set) = {"A": set(), "B": {"A"}, "C": {"A"}, "D": {"B", "C"}}
|
||||||
|
vertices_to_run: set = {"A", "B", "C"}
|
||||||
|
vertices_being_run = {"A"}
|
||||||
|
return {
|
||||||
|
"run_map": run_map,
|
||||||
|
"run_predecessors": run_predecessors,
|
||||||
|
"vertices_to_run": vertices_to_run,
|
||||||
|
"vertices_being_run": vertices_being_run,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_dict(data):
|
||||||
|
result = RunnableVerticesManager.from_dict(data).to_dict()
|
||||||
|
|
||||||
|
assert all(key in result.keys() for key in data.keys())
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_dict(data):
|
||||||
|
result = RunnableVerticesManager.from_dict(data)
|
||||||
|
|
||||||
|
assert isinstance(result, RunnableVerticesManager)
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_dict_without_run_map__bad_case(data):
|
||||||
|
data.pop("run_map")
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
RunnableVerticesManager.from_dict(data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_dict_without_run_predecessors__bad_case(data):
|
||||||
|
data.pop("run_predecessors")
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
RunnableVerticesManager.from_dict(data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_dict_without_vertices_to_run__bad_case(data):
|
||||||
|
data.pop("vertices_to_run")
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
RunnableVerticesManager.from_dict(data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_dict_without_vertices_being_run__bad_case(data):
|
||||||
|
data.pop("vertices_being_run")
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
RunnableVerticesManager.from_dict(data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pickle(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
|
||||||
|
binary = pickle.dumps(manager)
|
||||||
|
result = pickle.loads(binary)
|
||||||
|
|
||||||
|
assert result.run_map == manager.run_map
|
||||||
|
assert result.run_predecessors == manager.run_predecessors
|
||||||
|
assert result.vertices_to_run == manager.vertices_to_run
|
||||||
|
assert result.vertices_being_run == manager.vertices_being_run
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_run_state(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
run_predecessors = {"E": {"D"}}
|
||||||
|
vertices_to_run = {"D"}
|
||||||
|
|
||||||
|
manager.update_run_state(run_predecessors, vertices_to_run)
|
||||||
|
|
||||||
|
assert "D" in manager.run_map
|
||||||
|
assert "D" in manager.vertices_to_run
|
||||||
|
assert "D" in manager.run_predecessors["E"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_vertex_runnable(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
vertex_id = "A"
|
||||||
|
is_active = True
|
||||||
|
|
||||||
|
result = manager.is_vertex_runnable(vertex_id, is_active)
|
||||||
|
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_vertex_runnable__wrong_is_active(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
vertex_id = "A"
|
||||||
|
is_active = False
|
||||||
|
|
||||||
|
result = manager.is_vertex_runnable(vertex_id, is_active)
|
||||||
|
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_vertex_runnable__wrong_vertices_to_run(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
vertex_id = "D"
|
||||||
|
is_active = True
|
||||||
|
|
||||||
|
result = manager.is_vertex_runnable(vertex_id, is_active)
|
||||||
|
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_vertex_runnable__wrong_run_predecessors(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
vertex_id = "C"
|
||||||
|
is_active = True
|
||||||
|
|
||||||
|
result = manager.is_vertex_runnable(vertex_id, is_active)
|
||||||
|
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_are_all_predecessors_fulfilled(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
vertex_id = "A"
|
||||||
|
|
||||||
|
result = manager.are_all_predecessors_fulfilled(vertex_id)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_are_all_predecessors_fulfilled__wrong(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
vertex_id = "D"
|
||||||
|
|
||||||
|
result = manager.are_all_predecessors_fulfilled(vertex_id)
|
||||||
|
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_from_predecessors(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
vertex_id = "A"
|
||||||
|
|
||||||
|
manager.remove_from_predecessors(vertex_id)
|
||||||
|
|
||||||
|
assert all(vertex_id not in predecessors for predecessors in manager.run_predecessors.values())
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_run_map(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
vertices_to_run = {}
|
||||||
|
predecessor_map = {"Z": set(), "X": {"Z"}, "Y": {"Z"}, "W": {"X", "Y"}}
|
||||||
|
|
||||||
|
manager.build_run_map(predecessor_map, vertices_to_run)
|
||||||
|
|
||||||
|
assert all(v in manager.run_map.keys() for v in ["Z", "X", "Y"])
|
||||||
|
assert "W" not in manager.run_map.keys()
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_vertex_run_state(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
vertex_id = "C"
|
||||||
|
is_runnable = True
|
||||||
|
|
||||||
|
manager.update_vertex_run_state(vertex_id, is_runnable)
|
||||||
|
|
||||||
|
assert vertex_id in manager.vertices_to_run
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_vertex_run_state__bad_case(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
vertex_id = "C"
|
||||||
|
is_runnable = False
|
||||||
|
|
||||||
|
manager.update_vertex_run_state(vertex_id, is_runnable)
|
||||||
|
|
||||||
|
assert vertex_id not in manager.vertices_being_run
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_vertex_from_runnables(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
vertex_id = "C"
|
||||||
|
|
||||||
|
manager.remove_vertex_from_runnables(vertex_id)
|
||||||
|
|
||||||
|
assert vertex_id not in manager.vertices_being_run
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_to_vertices_being_run(data):
|
||||||
|
manager = RunnableVerticesManager.from_dict(data)
|
||||||
|
vertex_id = "C"
|
||||||
|
|
||||||
|
manager.add_to_vertices_being_run(vertex_id)
|
||||||
|
|
||||||
|
assert vertex_id in manager.vertices_being_run
|
||||||
|
|
@ -184,10 +184,10 @@ def test_directory_without_mocks():
|
||||||
# check if the directory component can load them
|
# check if the directory component can load them
|
||||||
# just check if the number of results is the same as the number of files
|
# just check if the number of results is the same as the number of files
|
||||||
directory_component = data.DirectoryComponent()
|
directory_component = data.DirectoryComponent()
|
||||||
docs_path = Path(__file__).parent.parent.parent / "docs" / "docs" / "components"
|
docs_path = Path(__file__).parent.parent.parent / "docs" / "docs" / "Components"
|
||||||
directory_component.set_attributes({"path": str(docs_path), "use_multithreading": False})
|
directory_component.set_attributes({"path": str(docs_path), "use_multithreading": False})
|
||||||
results = directory_component.load_directory()
|
results = directory_component.load_directory()
|
||||||
docs_files = list(docs_path.glob("*.mdx"))
|
docs_files = list(docs_path.glob("*.md")) + list(docs_path.glob("*.json"))
|
||||||
assert len(results) == len(docs_files)
|
assert len(results) == len(docs_files)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue