add status on building components

This commit is contained in:
cristhianzl 2024-02-19 20:43:58 -03:00
commit e395107473
23 changed files with 412 additions and 106 deletions

8
poetry.lock generated
View file

@ -385,13 +385,13 @@ lxml = ["lxml"]
[[package]] [[package]]
name = "bidict" name = "bidict"
version = "0.23.0" version = "0.23.1"
description = "The bidirectional mapping library for Python." description = "The bidirectional mapping library for Python."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "bidict-0.23.0-py3-none-any.whl", hash = "sha256:f5154a0e42926b122f9b2cb06aeeb9af317f626eb864bd34c1b9cdc5eca8b040"}, {file = "bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5"},
{file = "bidict-0.23.0.tar.gz", hash = "sha256:3959ca59d4d6997702d642bf1e5fd93cba299863723fc289545198f70c468578"}, {file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"},
] ]
[[package]] [[package]]
@ -9383,4 +9383,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.9,<3.12" python-versions = ">=3.9,<3.12"
content-hash = "e2791fcc55065da8d8d57b7b768a5301b1c967a905596a2944472fccfc3152d8" content-hash = "20b71a34763c614436d35902b6648a2a3bd970fc1abba31f21b19ec3ee4a2bba"

View file

@ -87,7 +87,7 @@ python-jose = "^3.3.0"
metaphor-python = "^0.1.11" metaphor-python = "^0.1.11"
pydantic = "^2.6.0" pydantic = "^2.6.0"
pydantic-settings = "^2.1.0" pydantic-settings = "^2.1.0"
zep-python = "*" zep-python = "1.5.0"
pywin32 = { version = "^306", markers = "sys_platform == 'win32'" } pywin32 = { version = "^306", markers = "sys_platform == 'win32'" }
loguru = "^0.7.1" loguru = "^0.7.1"
langfuse = "^2.9.0" langfuse = "^2.9.0"

View file

@ -284,14 +284,28 @@ async def get_vertices(
): ):
"""Check the flow_id is in the flow_data_store.""" """Check the flow_id is in the flow_data_store."""
try: try:
# First, we need to check if the flow_id is in the cache
graph = None
if cache := chat_service.get_cache(flow_id):
graph: Graph = cache.get("result")
flow: Flow = session.get(Flow, flow_id) flow: Flow = session.get(Flow, flow_id)
if not flow or not flow.data: if not flow or not flow.data:
raise ValueError("Invalid flow ID") raise ValueError("Invalid flow ID")
graph = Graph.from_payload(flow.data) other_graph = Graph.from_payload(flow.data)
if graph is None:
graph = other_graph
else:
graph = graph.update(other_graph)
chat_service.set_cache(flow_id, graph) chat_service.set_cache(flow_id, graph)
if component_id: if component_id:
vertices = graph.sort_up_to_vertex(component_id) try:
vertices = graph.sort_up_to_vertex(component_id)
except Exception as exc:
logger.error(f"IN DEVELOPMENT: Error getting vertices: {exc}")
logger.exception(exc)
vertices = graph.layered_topological_sort()
else: else:
vertices = graph.layered_topological_sort() vertices = graph.layered_topological_sort()
# Now vertices is a list of lists # Now vertices is a list of lists
@ -345,12 +359,15 @@ async def build_vertex(
duration=duration, duration=duration,
timedelta=timedelta, timedelta=timedelta,
) )
chat_service.set_cache(flow_id, graph)
except Exception as exc: except Exception as exc:
params = str(exc) params = str(exc)
valid = False valid = False
result_dict = ResultDict(results={}) result_dict = ResultDict(results={})
artifacts = {} artifacts = {}
chat_service.set_cache(flow_id, graph) # If there's an error building the vertex
# we need to clear the cache
chat_service.clear_cache(flow_id)
await log_vertex_build( await log_vertex_build(
flow_id=flow_id, flow_id=flow_id,
vertex_id=vertex_id, vertex_id=vertex_id,

View file

@ -161,7 +161,9 @@ class StreamData(BaseModel):
data: dict data: dict
def __str__(self) -> str: def __str__(self) -> str:
return f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n" return (
f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n"
)
class CustomComponentCode(BaseModel): class CustomComponentCode(BaseModel):
@ -246,4 +248,3 @@ class VertexBuildResponse(BaseModel):
class VerticesBuiltResponse(BaseModel): class VerticesBuiltResponse(BaseModel):
vertices: List[VertexBuildResponse] vertices: List[VertexBuildResponse]
vertices: List[VertexBuildResponse]

View file

@ -1,7 +1,8 @@
from typing import Optional from typing import Optional, Union
from langflow import CustomComponent from langflow import CustomComponent
from langflow.field_typing import Text from langflow.field_typing import Text
from langflow.schema import Record
class ChatInput(CustomComponent): class ChatInput(CustomComponent):
@ -10,18 +11,35 @@ class ChatInput(CustomComponent):
def build_config(self): def build_config(self):
return { return {
"message": {"input_types": ["Text"], "display_name": "Message"}, "message": {"input_types": ["Text"], "display_name": "Message","multiline": True},
"sender": {"options": ["Machine", "User"], "display_name": "Sender Type"}, "sender_type": {
"options": ["Machine", "User"],
"display_name": "Sender Type",
},
"sender_name": {"display_name": "Sender Name"}, "sender_name": {"display_name": "Sender Name"},
"as_record": {
"display_name": "As Record",
"info": "If true, the message will be returned as a Record.",
},
} }
def build( def build(
self, self,
sender: Optional[str] = "User", sender_type: Optional[str] = "User",
sender_name: Optional[str] = "You", sender_name: Optional[str] = "You",
message: Optional[str] = None, message: Optional[str] = None,
) -> Text: as_record: Optional[bool] = False,
self.repr_value = message ) -> Union[Text, Record]:
self.status = message
if as_record:
if isinstance(message, Record):
# Update the data of the record
message.data["sender"] = sender_type
message.data["sender_name"] = sender_name
return message
return Record(
text=message, data={"sender": sender_type, "sender_name": sender_name}
)
if not message: if not message:
message = "" message = ""
return message return message

View file

@ -1,7 +1,8 @@
from typing import Optional from typing import Optional, Union
from langflow import CustomComponent from langflow import CustomComponent
from langflow.field_typing import Text from langflow.field_typing import Text
from langflow.schema import Record
class ChatOutput(CustomComponent): class ChatOutput(CustomComponent):
@ -17,23 +18,47 @@ class ChatOutput(CustomComponent):
def build_config(self): def build_config(self):
return { return {
"message": {"input_types": ["Text"], "display_name": "Message"}, "message": {"input_types": ["Text"], "display_name": "Message"},
"sender": {"options": ["Machine", "User"], "display_name": "Sender Type"}, "sender_type": {
"options": ["Machine", "User"],
"display_name": "Sender Type",
},
"sender_name": {"display_name": "Sender Name"}, "sender_name": {"display_name": "Sender Name"},
"session_id": { "session_id": {
"display_name": "Session ID", "display_name": "Session ID",
"info": "Session ID of the chat history.", "info": "Session ID of the chat history.",
"input_types": ["Text"], "input_types": ["Text"],
}, },
"as_record": {
"display_name": "As Record",
"info": "If true, the message will be returned as a Record.",
},
} }
def build( def build(
self, self,
sender: Optional[str] = "Machine", sender_type: Optional[str] = "Machine",
sender_name: Optional[str] = "AI", sender_name: Optional[str] = "AI",
session_id: Optional[str] = None, session_id: Optional[str] = None,
message: Optional[str] = None, message: Optional[str] = None,
) -> Text: as_record: Optional[bool] = False,
self.repr_value = message ) -> Union[Text, Record]:
self.status = message
if as_record:
if isinstance(message, Record):
# Update the data of the record
message.data["sender"] = sender_type
message.data["sender_name"] = sender_name
message.data["session_id"] = session_id
return message
return Record(
text=message,
data={
"sender": sender_type,
"sender_name": sender_name,
"session_id": session_id,
},
)
if not message: if not message:
message = "" message = ""
return message return message

View file

@ -20,6 +20,7 @@ class PromptComponent(CustomComponent):
**kwargs, **kwargs,
) -> Text: ) -> Text:
prompt_template = PromptTemplate.from_template(template) prompt_template = PromptTemplate.from_template(template)
try: try:
formated_prompt = prompt_template.format(**kwargs) formated_prompt = prompt_template.format(**kwargs)
except Exception as exc: except Exception as exc:

View file

@ -0,0 +1,18 @@
import uuid
from langflow import CustomComponent
class UUIDGeneratorComponent(CustomComponent):
documentation: str = "http://docs.langflow.org/components/custom"
display_name = "Unique ID Generator"
description = "Generates a unique ID."
def generate(self, *args, **kwargs):
return str(uuid.uuid4().hex)
def build_config(self):
return {"unique_id": {"display_name": "Value", "value": self.generate}}
def build(self, unique_id: str) -> str:
return unique_id

View file

@ -99,7 +99,7 @@ class ChromaSearchComponent(CustomComponent):
chroma_server_grpc_port=chroma_server_grpc_port or None, chroma_server_grpc_port=chroma_server_grpc_port or None,
chroma_server_ssl_enabled=chroma_server_ssl_enabled, chroma_server_ssl_enabled=chroma_server_ssl_enabled,
) )
index_directory = self.resolve_path(index_directory)
chroma = Chroma( chroma = Chroma(
embedding_function=embedding, embedding_function=embedding,
collection_name=collection_name, collection_name=collection_name,

View file

@ -76,6 +76,28 @@ class Graph:
return False return False
return self.__repr__() == other.__repr__() return self.__repr__() == other.__repr__()
# update this graph with another graph by comparing the __repr__ of each vertex
# and if the __repr__ of a vertex is not the same as the other
# then update the .data of the vertex to the self
# both graphs have the same vertices and edges
# but the data of the vertices might be different
def update(self, other: "Graph", different_vertices: List[str] = None) -> None:
if different_vertices is None:
different_vertices = []
for vertex in self.vertices:
other_vertex = other.get_vertex(vertex.id)
if other_vertex is None:
continue
if (
vertex.id in different_vertices
or vertex.__repr__() != other.get_vertex(vertex.id).__repr__()
):
vertex.data = other.get_vertex(vertex.id).data
vertex._build_params()
vertex.graph = self
vertex._built = False
return self
def _build_graph(self) -> None: def _build_graph(self) -> None:
"""Builds the graph from the vertices and edges.""" """Builds the graph from the vertices and edges."""
self.vertices = self._build_vertices() self.vertices = self._build_vertices()
@ -314,9 +336,22 @@ class Graph:
"""Cuts the graph up to a given vertex.""" """Cuts the graph up to a given vertex."""
# Get the vertices that are connected to the vertex # Get the vertices that are connected to the vertex
# and the vertex itself # and the vertex itself
vertices = [self.get_vertex(vertex_id)] vertex = self.get_vertex(vertex_id)
for edge in self.get_vertex(edge.target_id).edges: vertices = [vertex]
vertices.append(self.get_vertex(edge.target_id)) for edge in vertex.edges:
if edge.target_id == vertex_id:
vertices.append(self.get_vertex(edge.source_id))
# Get the edges that are connected to the vertices
edges = []
for vertex in vertices:
edges.extend(self.get_vertex_edges(vertex.id))
source_vertex = self.get_vertex(edge.source_id)
target_vertex = self.get_vertex(edge.target_id)
if source_vertex not in vertices:
vertices.append(source_vertex)
if target_vertex not in vertices:
vertices.append(target_vertex)
edges = [edge for vertex in vertices for edge in vertex.edges] edges = [edge for vertex in vertices for edge in vertex.edges]

View file

@ -55,11 +55,14 @@ class Vertex:
self.parent_node_id: Optional[str] = self._data.get("parent_node_id") self.parent_node_id: Optional[str] = self._data.get("parent_node_id")
self.parent_is_top_level = False self.parent_is_top_level = False
self.layer = None self.layer = None
self.should_run = True
try: try:
self.is_interface_component = InterfaceComponentTypes(self.vertex_type) self.is_interface_component = InterfaceComponentTypes(self.vertex_type)
except ValueError: except ValueError:
self.is_interface_component = False self.is_interface_component = False
self.use_result = False
# Build a result dict for each edge # Build a result dict for each edge
# like so: {edge.target.id: {edge.target_param: self._built_object}} # like so: {edge.target.id: {edge.target_param: self._built_object}}
async def get_result_dict(self, force: bool = False) -> Dict[str, Dict[str, Any]]: async def get_result_dict(self, force: bool = False) -> Dict[str, Dict[str, Any]]:
@ -78,14 +81,18 @@ class Vertex:
): ):
if edge.target_id not in edge_results: if edge.target_id not in edge_results:
edge_results[edge.target_id] = {} edge_results[edge.target_id] = {}
edge_results[edge.target_id][edge.target_param] = await edge.get_result(source=self, target=target) edge_results[edge.target_id][edge.target_param] = await edge.get_result(
source=self, target=target
)
return edge_results return edge_results
def get_built_result(self): def get_built_result(self):
# If the Vertex.type is a power component # If the Vertex.type is a power component
# then we need to return the built object # then we need to return the built object
# instead of the result dict # instead of the result dict
if self.is_interface_component and not isinstance(self._built_object, UnbuiltObject): if self.is_interface_component and not isinstance(
self._built_object, UnbuiltObject
):
result = self._built_object result = self._built_object
# if it is not a dict or a string and hasattr model_dump then # if it is not a dict or a string and hasattr model_dump then
# return the model_dump # return the model_dump
@ -95,7 +102,11 @@ class Vertex:
if isinstance(self._built_result, UnbuiltResult): if isinstance(self._built_result, UnbuiltResult):
return {} return {}
return self._built_result if isinstance(self._built_result, dict) else {"result": self._built_result} return (
self._built_result
if isinstance(self._built_result, dict)
else {"result": self._built_result}
)
def set_artifacts(self) -> None: def set_artifacts(self) -> None:
pass pass
@ -149,18 +160,29 @@ class Vertex:
self.data = self._data["data"] self.data = self._data["data"]
self.output = self.data["node"]["base_classes"] self.output = self.data["node"]["base_classes"]
self.pinned = self.data["node"].get("pinned", False) self.pinned = self.data["node"].get("pinned", False)
template_dicts = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)} template_dicts = {
template_dicts = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)} key: value
for key, value in self.data["node"]["template"].items()
if isinstance(value, dict)
}
self.required_inputs = [ self.required_inputs = [
template_dicts[key]["type"] for key, value in template_dicts.items() if value["required"] template_dicts[key]["type"]
for key, value in template_dicts.items()
if value["required"]
] ]
self.optional_inputs = [ self.optional_inputs = [
template_dicts[key]["type"] for key, value in template_dicts.items() if not value["required"] template_dicts[key]["type"]
for key, value in template_dicts.items()
if not value["required"]
] ]
# Add the template_dicts[key]["input_types"] to the optional_inputs # Add the template_dicts[key]["input_types"] to the optional_inputs
self.optional_inputs.extend( self.optional_inputs.extend(
[input_type for value in template_dicts.values() for input_type in value.get("input_types", [])] [
input_type
for value in template_dicts.values()
for input_type in value.get("input_types", [])
]
) )
template_dict = self.data["node"]["template"] template_dict = self.data["node"]["template"]
@ -203,7 +225,11 @@ class Vertex:
if self.graph is None: if self.graph is None:
raise ValueError("Graph not found") raise ValueError("Graph not found")
template_dict = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)} template_dict = {
key: value
for key, value in self.data["node"]["template"].items()
if isinstance(value, dict)
}
params = self.params.copy() if self.params else {} params = self.params.copy() if self.params else {}
for edge in self.edges: for edge in self.edges:
@ -255,7 +281,11 @@ class Vertex:
# list of dicts, so we need to convert it to a dict # list of dicts, so we need to convert it to a dict
# before passing it to the build method # before passing it to the build method
if isinstance(val, list): if isinstance(val, list):
params[key] = {k: v for item in value.get("value", []) for k, v in item.items()} params[key] = {
k: v
for item in value.get("value", [])
for k, v in item.items()
}
elif isinstance(val, dict): elif isinstance(val, dict):
params[key] = val params[key] = val
elif value.get("type") == "int" and val is not None: elif value.get("type") == "int" and val is not None:
@ -292,7 +322,12 @@ class Vertex:
self._built = True self._built = True
async def _run(self, user_id: str, inputs: Optional[dict] = None, session_id: Optional[str] = None): async def _run(
self,
user_id: str,
inputs: Optional[dict] = None,
session_id: Optional[str] = None,
):
# user_id is just for compatibility with the other build methods # user_id is just for compatibility with the other build methods
inputs = inputs or {} inputs = inputs or {}
# inputs = {key: value or "" for key, value in inputs.items()} # inputs = {key: value or "" for key, value in inputs.items()}
@ -307,7 +342,9 @@ class Vertex:
if isinstance(self._built_object, str): if isinstance(self._built_object, str):
self._built_result = self._built_object self._built_result = self._built_object
result = await generate_result(self._built_object, inputs, self.has_external_output, session_id) result = await generate_result(
self._built_object, inputs, self.has_external_output, session_id
)
self._built_result = result self._built_result = result
async def _build_each_node_in_params_dict(self, user_id=None): async def _build_each_node_in_params_dict(self, user_id=None):
@ -335,11 +372,13 @@ class Vertex:
""" """
return all(self._is_node(node) for node in value) return all(self._is_node(node) for node in value)
async def get_result(self, requester: Optional["Vertex"] = None, user_id=None, timeout=None) -> Any: async def get_result(
self, requester: Optional["Vertex"] = None, user_id=None, timeout=None
) -> Any:
# PLEASE REVIEW THIS IF STATEMENT # PLEASE REVIEW THIS IF STATEMENT
# Check if the Vertex was built already # Check if the Vertex was built already
if self._built: if self._built:
return self._built_object return self._built_object if not self.use_result else self._built_result
if self.is_task and self.task_id is not None: if self.is_task and self.task_id is not None:
task = self.get_task() task = self.get_task()
@ -369,7 +408,9 @@ class Vertex:
self._extend_params_list_with_result(key, result) self._extend_params_list_with_result(key, result)
self.params[key] = result self.params[key] = result
async def _build_list_of_nodes_and_update_params(self, key, nodes: List["Vertex"], user_id=None): async def _build_list_of_nodes_and_update_params(
self, key, nodes: List["Vertex"], user_id=None
):
""" """
Iterates over a list of nodes, builds each and updates the params dictionary. Iterates over a list of nodes, builds each and updates the params dictionary.
""" """
@ -421,7 +462,9 @@ class Vertex:
self._update_built_object_and_artifacts(result) self._update_built_object_and_artifacts(result)
except Exception as exc: except Exception as exc:
logger.exception(exc) logger.exception(exc)
raise ValueError(f"Error building node {self.vertex_type}(ID:{self.id}): {str(exc)}") from exc raise ValueError(
f"Error building node {self.vertex_type}(ID:{self.id}): {str(exc)}"
) from exc
def _update_built_object_and_artifacts(self, result): def _update_built_object_and_artifacts(self, result):
""" """
@ -458,7 +501,7 @@ class Vertex:
requester: Optional["Vertex"] = None, requester: Optional["Vertex"] = None,
**kwargs, **kwargs,
) -> Any: ) -> Any:
if self.pinned: if self.pinned and self._built:
return self.get_requester_result(requester) return self.get_requester_result(requester)
self._reset() self._reset()
@ -480,9 +523,15 @@ class Vertex:
return self._built_object return self._built_object
# Get the requester edge # Get the requester edge
requester_edge = next((edge for edge in self.edges if edge.target_id == requester.id), None) requester_edge = next(
(edge for edge in self.edges if edge.target_id == requester.id), None
)
# Return the result of the requester edge # Return the result of the requester edge
return None if requester_edge is None else await requester_edge.get_result(source=self, target=requester) return (
None
if requester_edge is None
else await requester_edge.get_result(source=self, target=requester)
)
def add_edge(self, edge: "ContractEdge") -> None: def add_edge(self, edge: "ContractEdge") -> None:
if edge not in self.edges: if edge not in self.edges:
@ -502,7 +551,11 @@ class Vertex:
def _built_object_repr(self): def _built_object_repr(self):
# Add a message with an emoji, stars for sucess, # Add a message with an emoji, stars for sucess,
return "Built sucessfully ✨" if self._built_object is not None else "Failed to build 😵‍💫" return (
"Built sucessfully ✨"
if self._built_object is not None
else "Failed to build 😵‍💫"
)
class StatefulVertex(Vertex): class StatefulVertex(Vertex):
@ -511,9 +564,3 @@ class StatefulVertex(Vertex):
class StatelessVertex(Vertex): class StatelessVertex(Vertex):
pass pass
pass
pass
pass
pass
pass
pass

View file

@ -3,10 +3,10 @@ import json
from typing import Callable, Dict, List, Optional, Union from typing import Callable, Dict, List, Optional, Union
from langchain_core.messages import AIMessage from langchain_core.messages import AIMessage
from langflow.graph.utils import UnbuiltObject, flatten_list from langflow.graph.utils import UnbuiltObject, flatten_list
from langflow.graph.vertex.base import StatefulVertex, StatelessVertex from langflow.graph.vertex.base import StatefulVertex, StatelessVertex
from langflow.interface.utils import extract_input_variables_from_prompt from langflow.interface.utils import extract_input_variables_from_prompt
from langflow.schema import Record
from langflow.utils.schemas import ChatOutputResponse from langflow.utils.schemas import ChatOutputResponse
@ -118,9 +118,11 @@ class DocumentLoaderVertex(StatefulVertex):
# show how many documents are in the list? # show how many documents are in the list?
if not isinstance(self._built_object, UnbuiltObject): if not isinstance(self._built_object, UnbuiltObject):
avg_length = sum(len(doc.page_content) for doc in self._built_object if hasattr(doc, "page_content")) / len( avg_length = sum(
self._built_object len(doc.page_content)
) for doc in self._built_object
if hasattr(doc, "page_content")
) / len(self._built_object)
return f"""{self.vertex_type}({len(self._built_object)} documents) return f"""{self.vertex_type}({len(self._built_object)} documents)
\nAvg. Document Length (characters): {int(avg_length)} \nAvg. Document Length (characters): {int(avg_length)}
Documents: {self._built_object[:3]}...""" Documents: {self._built_object[:3]}..."""
@ -193,7 +195,9 @@ class TextSplitterVertex(StatefulVertex):
# show how many documents are in the list? # show how many documents are in the list?
if not isinstance(self._built_object, UnbuiltObject): if not isinstance(self._built_object, UnbuiltObject):
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(self._built_object) avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(
self._built_object
)
return f"""{self.vertex_type}({len(self._built_object)} documents) return f"""{self.vertex_type}({len(self._built_object)} documents)
\nAvg. Document Length (characters): {int(avg_length)} \nAvg. Document Length (characters): {int(avg_length)}
\nDocuments: {self._built_object[:3]}...""" \nDocuments: {self._built_object[:3]}..."""
@ -240,18 +244,27 @@ class PromptVertex(StatelessVertex):
user_id = kwargs.get("user_id", None) user_id = kwargs.get("user_id", None)
tools = kwargs.get("tools", []) tools = kwargs.get("tools", [])
if not self._built or force: if not self._built or force:
if "input_variables" not in self.params or self.params["input_variables"] is None: if (
"input_variables" not in self.params
or self.params["input_variables"] is None
):
self.params["input_variables"] = [] self.params["input_variables"] = []
# Check if it is a ZeroShotPrompt and needs a tool # Check if it is a ZeroShotPrompt and needs a tool
if "ShotPrompt" in self.vertex_type: if "ShotPrompt" in self.vertex_type:
tools = [tool_node.build(user_id=user_id) for tool_node in tools] if tools is not None else [] tools = (
[tool_node.build(user_id=user_id) for tool_node in tools]
if tools is not None
else []
)
# flatten the list of tools if it is a list of lists # flatten the list of tools if it is a list of lists
# first check if it is a list # first check if it is a list
if tools and isinstance(tools, list) and isinstance(tools[0], list): if tools and isinstance(tools, list) and isinstance(tools[0], list):
tools = flatten_list(tools) tools = flatten_list(tools)
self.params["tools"] = tools self.params["tools"] = tools
prompt_params = [ prompt_params = [
key for key, value in self.params.items() if isinstance(value, str) and key != "format_instructions" key
for key, value in self.params.items()
if isinstance(value, str) and key != "format_instructions"
] ]
else: else:
prompt_params = ["template"] prompt_params = ["template"]
@ -261,14 +274,20 @@ class PromptVertex(StatelessVertex):
prompt_text = self.params[param] prompt_text = self.params[param]
variables = extract_input_variables_from_prompt(prompt_text) variables = extract_input_variables_from_prompt(prompt_text)
self.params["input_variables"].extend(variables) self.params["input_variables"].extend(variables)
self.params["input_variables"] = list(set(self.params["input_variables"])) self.params["input_variables"] = list(
set(self.params["input_variables"])
)
elif isinstance(self.params, dict): elif isinstance(self.params, dict):
self.params.pop("input_variables", None) self.params.pop("input_variables", None)
await self._build(user_id=user_id) await self._build(user_id=user_id)
def _built_object_repr(self): def _built_object_repr(self):
if not self.artifacts or self._built_object is None or not hasattr(self._built_object, "format"): if (
not self.artifacts
or self._built_object is None
or not hasattr(self._built_object, "format")
):
return super()._built_object_repr() return super()._built_object_repr()
elif isinstance(self._built_object, UnbuiltObject): elif isinstance(self._built_object, UnbuiltObject):
return super()._built_object_repr() return super()._built_object_repr()
@ -280,7 +299,9 @@ class PromptVertex(StatelessVertex):
# so the prompt format doesn't break # so the prompt format doesn't break
artifacts.pop("handle_keys", None) artifacts.pop("handle_keys", None)
try: try:
if not hasattr(self._built_object, "template") and hasattr(self._built_object, "prompt"): if not hasattr(self._built_object, "template") and hasattr(
self._built_object, "prompt"
):
template = self._built_object.prompt.template template = self._built_object.prompt.template
else: else:
template = self._built_object.template template = self._built_object.template
@ -288,7 +309,11 @@ class PromptVertex(StatelessVertex):
if value: if value:
replace_key = "{" + key + "}" replace_key = "{" + key + "}"
template = template.replace(replace_key, value) template = template.replace(replace_key, value)
return template if isinstance(template, str) else f"{self.vertex_type}({template})" return (
template
if isinstance(template, str)
else f"{self.vertex_type}({template})"
)
except KeyError: except KeyError:
return str(self._built_object) return str(self._built_object)
@ -337,12 +362,16 @@ class ChatVertex(StatelessVertex):
if isinstance(self._built_object, dict): if isinstance(self._built_object, dict):
# Turn the dict into a pleasing to # Turn the dict into a pleasing to
# read JSON inside a code block # read JSON inside a code block
self._built_object = dict_to_codeblock(self._built_object) message = dict_to_codeblock(self._built_object)
elif isinstance(self._built_object, Record):
message = self._built_object.text
elif not isinstance(self._built_object, str): elif not isinstance(self._built_object, str):
self._built_object = str(self._built_object) message = str(self._built_object)
else:
message = self._built_object
artifacts = ChatOutputResponse( artifacts = ChatOutputResponse(
message=self._built_object, message=message,
sender=sender, sender=sender,
sender_name=sender_name, sender_name=sender_name,
) )
@ -354,6 +383,52 @@ class ChatVertex(StatelessVertex):
await super()._run(*args, **kwargs) await super()._run(*args, **kwargs)
class RoutingVertex(StatelessVertex):
def __init__(self, data: Dict, graph):
super().__init__(data, graph=graph, base_type="routing")
self.use_result = True
self.steps = [self._build, self._run]
def _built_object_repr(self):
if self.artifacts and "repr" in self.artifacts:
return self.artifacts["repr"] or super()._built_object_repr()
return super()._built_object_repr()
def _build(self, *args, **kwargs):
super()._build(*args, **kwargs)
# After building, the _built_object should be a dict with
# {"result": Any, "condition": bool}
# if true, we need to set should_run attr in the target of true edge
# to true and should_run attr in the target of false edge to false
# TODO: Add support for multiple conditions
def _run(self, *args, **kwargs):
if self._built_object:
condition = self._built_object.get("condition")
result = self._built_object.get("result")
if condition is not None:
for edge in self.edges:
if edge.source_id == self.id:
target_vertex = self.graph.get_vertex(edge.target_id)
# source_handle.channel and condition should be the same
channel_bool = edge.source_handle.channel == "true"
if condition == channel_bool:
target_vertex.should_run = True
else:
target_vertex.should_run = False
else:
raise ValueError(
f"RoutingVertex {self.id} must have a condition in the _built_object"
)
self._built_result = result
else:
raise ValueError(
f"RoutingVertex {self.id} must have a _built_object with a condition and a result"
)
def dict_to_codeblock(d: dict) -> str: def dict_to_codeblock(d: dict) -> str:
from langflow.api.utils import serialize_field from langflow.api.utils import serialize_field

View file

@ -1,4 +1,4 @@
from typing import Optional from typing import Any, Optional
from langchain_core.documents import Document from langchain_core.documents import Document
from pydantic import BaseModel from pydantic import BaseModel
@ -38,6 +38,24 @@ class Record(BaseModel):
""" """
return Document(page_content=self.text, metadata=self.data) return Document(page_content=self.text, metadata=self.data)
def __call__(self, *args: Any, **kwds: Any) -> Any:
"""
Returns the text of the record.
Returns:
Any: The text of the record.
"""
return self.text
def __str__(self) -> str:
"""
Returns the text of the record.
Returns:
str: The text of the record.
"""
return self.text
def docs_to_records(documents: list[Document]) -> list[Record]: def docs_to_records(documents: list[Document]) -> list[Record]:
""" """

View file

@ -59,7 +59,9 @@ class ChatService(Service):
"""Send the last chat message to the client.""" """Send the last chat message to the client."""
client_id = self.chat_cache.current_client_id client_id = self.chat_cache.current_client_id
if client_id in self.active_connections: if client_id in self.active_connections:
chat_response = self.chat_history.get_history(client_id, filter_messages=False)[-1] chat_response = self.chat_history.get_history(
client_id, filter_messages=False
)[-1]
if chat_response.is_bot: if chat_response.is_bot:
# Process FileResponse # Process FileResponse
if isinstance(chat_response, FileResponse): if isinstance(chat_response, FileResponse):
@ -86,7 +88,9 @@ class ChatService(Service):
data_type=self.last_cached_object_dict["type"], data_type=self.last_cached_object_dict["type"],
) )
self.chat_history.add_message(self.chat_cache.current_client_id, chat_response) self.chat_history.add_message(
self.chat_cache.current_client_id, chat_response
)
async def connect(self, client_id: str, websocket: WebSocket): async def connect(self, client_id: str, websocket: WebSocket):
self.active_connections[client_id] = websocket self.active_connections[client_id] = websocket
@ -209,7 +213,9 @@ class ChatService(Service):
await self.process_message(client_id, payload, build_result) await self.process_message(client_id, payload, build_result)
else: else:
raise RuntimeError(f"Could not find a build result for client_id {client_id}") raise RuntimeError(
f"Could not find a build result for client_id {client_id}"
)
except Exception as exc: except Exception as exc:
# Handle any exceptions that might occur # Handle any exceptions that might occur
logger.exception(f"Error handling websocket: {exc}") logger.exception(f"Error handling websocket: {exc}")
@ -241,6 +247,12 @@ class ChatService(Service):
""" """
return self.cache_service.get(client_id) return self.cache_service.get(client_id)
def clear_cache(self, client_id: str):
"""
Clear the cache for a client.
"""
self.cache_service.delete(client_id)
def dict_to_markdown_table(my_dict): def dict_to_markdown_table(my_dict):
markdown_table = "| Key | Value |\n|---|---|\n" markdown_table = "| Key | Value |\n|---|---|\n"

View file

@ -7,7 +7,9 @@ from pydantic import BaseModel, Field, field_serializer, validator
class TransactionModel(BaseModel): class TransactionModel(BaseModel):
id: Optional[int] = Field(default=None, alias="id") id: Optional[int] = Field(default=None, alias="id")
timestamp: Optional[datetime] = Field(default_factory=datetime.now, alias="timestamp") timestamp: Optional[datetime] = Field(
default_factory=datetime.now, alias="timestamp"
)
source: str source: str
target: str target: str
target_args: dict target_args: dict
@ -89,18 +91,13 @@ class VertexBuildModel(BaseModel):
return v return v
# create a function that turns dicts into a class VertexBuildResponseModel(VertexBuildModel):
# dict like this:
# my_map_dict = { @field_serializer("data", "artifacts")
# "key": [ def serialize_dict(v):
# 1, 2, 3 return v
# ],
# "value": [
# "one", "two", "three"
# ]
# }
# so map has a "key" and a "value" list
# containing the keys and values of the dict
def to_map(value: dict): def to_map(value: dict):
keys = list(value.keys()) keys = list(value.keys())
values = list(value.values()) values = list(value.values())
@ -108,13 +105,13 @@ def to_map(value: dict):
class VertexBuildMapModel(BaseModel): class VertexBuildMapModel(BaseModel):
vertex_builds: dict[str, list[VertexBuildModel]] vertex_builds: dict[str, list[VertexBuildResponseModel]]
@classmethod @classmethod
def from_list_of_dicts(cls, vertex_build_dicts): def from_list_of_dicts(cls, vertex_build_dicts):
vertex_build_map = {} vertex_build_map = {}
for vertex_build_dict in vertex_build_dicts: for vertex_build_dict in vertex_build_dicts:
vertex_build = VertexBuildModel(**vertex_build_dict) vertex_build = VertexBuildResponseModel(**vertex_build_dict)
if vertex_build.id not in vertex_build_map: if vertex_build.id not in vertex_build_map:
vertex_build_map[vertex_build.id] = [] vertex_build_map[vertex_build.id] = []
vertex_build_map[vertex_build.id].append(vertex_build) vertex_build_map[vertex_build.id].append(vertex_build)

View file

@ -1,9 +1,10 @@
import os
import yaml
from langflow.services.base import Service from langflow.services.base import Service
from langflow.services.settings.auth import AuthSettings from langflow.services.settings.auth import AuthSettings
from langflow.services.settings.base import Settings from langflow.services.settings.base import Settings
from loguru import logger from loguru import logger
import os
import yaml
class SettingsService(Service): class SettingsService(Service):
@ -11,8 +12,8 @@ class SettingsService(Service):
def __init__(self, settings: Settings, auth_settings: AuthSettings): def __init__(self, settings: Settings, auth_settings: AuthSettings):
super().__init__() super().__init__()
self.settings = settings self.settings: Settings = settings
self.auth_settings = auth_settings self.auth_settings: AuthSettings = auth_settings
@classmethod @classmethod
def load_settings_from_yaml(cls, file_path: str) -> "SettingsService": def load_settings_from_yaml(cls, file_path: str) -> "SettingsService":
@ -30,7 +31,9 @@ class SettingsService(Service):
for key in settings_dict: for key in settings_dict:
if key not in Settings.model_fields.keys(): if key not in Settings.model_fields.keys():
raise KeyError(f"Key {key} not found in settings") raise KeyError(f"Key {key} not found in settings")
logger.debug(f"Loading {len(settings_dict[key])} {key} from {file_path}") logger.debug(
f"Loading {len(settings_dict[key])} {key} from {file_path}"
)
settings = Settings(**settings_dict) settings = Settings(**settings_dict)
if not settings.CONFIG_DIR: if not settings.CONFIG_DIR:

View file

@ -406,13 +406,27 @@ export default function ParameterComponent({
data-testid={"textarea-" + data.node.template[name].name} data-testid={"textarea-" + data.node.template[name].name}
/> />
) : ( ) : (
<InputComponent <div className="mt-2 flex w-full items-center">
id={"input-" + index} <div className="w-5/6 flex-grow">
disabled={disabled} <InputComponent
password={data.node?.template[name].password ?? false} id={"input-" + index}
value={data.node?.template[name].value ?? ""} disabled={disabled}
onChange={handleOnNewValue} password={data.node?.template[name].password ?? false}
/> value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
/>
</div>
{data.node?.template[name].refresh && (
<button
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
onClick={() => {
handleUpdateValues(name, data);
}}
>
<IconComponent name="RefreshCcw" />
</button>
)}
</div>
)} )}
</div> </div>
) : left === true && type === "bool" ? ( ) : left === true && type === "bool" ? (

View file

@ -86,6 +86,18 @@ export default function GenericNode({
// State for outline color // State for outline color
const isBuilding = useFlowStore((state) => state.isBuilding); const isBuilding = useFlowStore((state) => state.isBuilding);
// should be empty string if no duration
// else should be `Duration: ${duration}`
const getDurationString = (duration: number | null): string => {
if (duration === null) {
return "";
} else {
return `Duration: ${duration}`;
}
};
const durationString = getDurationString(validationStatus?.data.duration);
useEffect(() => { useEffect(() => {
setNodeDescription(data.node!.description); setNodeDescription(data.node!.description);
}, [data.node!.description]); }, [data.node!.description]);
@ -400,19 +412,19 @@ export default function GenericNode({
Build{" "} Build{" "}
<IconComponent <IconComponent
name="Play" name="Play"
className=" h-5 fill-build-trigger stroke-build-trigger stroke-1" className=" h-5 stroke-build-trigger stroke-2"
/>{" "} />{" "}
flow to validate status. flow to validate status.
</span> </span>
) : ( ) : (
<div className="max-h-96 overflow-auto"> <div className="max-h-96 overflow-auto">
{typeof validationStatus.params === "string" {typeof validationStatus.params === "string"
? `Duration: ${validationStatus.data.duration}\n${validationStatus.params}` ? `${durationString}\n${validationStatus.params}`
.split("\n") .split("\n")
.map((line, index) => ( .map((line, index) => (
<div key={index}>{line}</div> <div key={index}>{line}</div>
)) ))
: ""} : durationString}
</div> </div>
) )
} }

View file

@ -855,9 +855,16 @@ export async function requestLogout() {
} }
export async function getVerticesOrder( export async function getVerticesOrder(
flowId: string flowId: string,
nodeId?: string | null
): Promise<AxiosResponse<VerticesOrderTypeAPI>> { ): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
return await api.get(`${BASE_URL_API}build/${flowId}/vertices`); // nodeId is optional and is a query parameter
// if nodeId is not provided, the API will return all vertices
const config = {};
if (nodeId) {
config["params"] = { component_id: nodeId };
}
return await api.get(`${BASE_URL_API}build/${flowId}/vertices`, config);
} }
export async function postBuildVertex( export async function postBuildVertex(

View file

@ -378,6 +378,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
const currentFlow = useFlowsManagerStore.getState().currentFlow; const currentFlow = useFlowsManagerStore.getState().currentFlow;
const setSuccessData = useAlertStore.getState().setSuccessData; const setSuccessData = useAlertStore.getState().setSuccessData;
const setErrorData = useAlertStore.getState().setErrorData; const setErrorData = useAlertStore.getState().setErrorData;
const setNoticeData = useAlertStore.getState().setNoticeData;
function handleBuildUpdate(data: any) { function handleBuildUpdate(data: any) {
get().addDataToFlowPool(data.data[data.id], data.id); get().addDataToFlowPool(data.data[data.id], data.id);
useFlowStore.getState().updateBuildStatus([data.id], BuildStatus.BUILDED); useFlowStore.getState().updateBuildStatus([data.id], BuildStatus.BUILDED);
@ -392,6 +393,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
name: currentFlow!.name, name: currentFlow!.name,
description: currentFlow!.description, description: currentFlow!.description,
}); });
setNoticeData({ title: "Running components" });
return buildVertices({ return buildVertices({
flowId: currentFlow!.id, flowId: currentFlow!.id,
nodeId, nodeId,

View file

@ -293,13 +293,13 @@
@apply opacity-100 animate-wiggle; @apply opacity-100 animate-wiggle;
} }
.green-status { .green-status {
@apply generic-node-status text-status-green fill-status-green; @apply generic-node-status text-status-green;
} }
.red-status { .red-status {
@apply generic-node-status text-status-red fill-status-red; @apply generic-node-status text-status-red;
} }
.yellow-status { .yellow-status {
@apply generic-node-status text-status-yellow fill-status-yellow; @apply generic-node-status text-status-yellow;
} }
.status-build-animation { .status-build-animation {
@apply opacity-0; @apply opacity-0;

View file

@ -23,7 +23,7 @@ export async function buildVertices({
onBuildError, onBuildError,
onBuildStart, onBuildStart,
}: BuildVerticesParams) { }: BuildVerticesParams) {
let orderResponse = await getVerticesOrder(flowId); let orderResponse = await getVerticesOrder(flowId, nodeId);
let verticesOrder: Array<Array<string>> = orderResponse.data.ids; let verticesOrder: Array<Array<string>> = orderResponse.data.ids;
let vertices: Array<Array<string>> = []; let vertices: Array<Array<string>> = [];

View file

@ -110,6 +110,8 @@ import {
X, X,
XCircle, XCircle,
Zap, Zap,
FlaskConical,
AlertCircle,
} from "lucide-react"; } from "lucide-react";
import { FaApple, FaGithub } from "react-icons/fa"; import { FaApple, FaGithub } from "react-icons/fa";
import { AWSIcon } from "../icons/AWS"; import { AWSIcon } from "../icons/AWS";
@ -400,4 +402,6 @@ export const nodeIconsLucide: iconsType = {
io: Cable, io: Cable,
ScreenShare, ScreenShare,
Code, Code,
FlaskConical,
AlertCircle,
}; };