merge fix
This commit is contained in:
commit
652780a6ee
33 changed files with 757 additions and 467 deletions
|
|
@ -1,32 +0,0 @@
|
|||
from langchain.llms.base import BaseLLM
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class PromptRunner(CustomComponent):
|
||||
display_name: str = "Prompt Runner"
|
||||
description: str = "Run a Chain with the given PromptTemplate"
|
||||
beta: bool = True
|
||||
field_config = {
|
||||
"llm": {"display_name": "LLM"},
|
||||
"prompt": {
|
||||
"display_name": "Prompt Template",
|
||||
"info": "Make sure the prompt has all variables filled.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {}) -> Text:
|
||||
chain = prompt | llm
|
||||
# The input is an empty dict because the prompt is already filled
|
||||
result_message: BaseMessage = chain.invoke(input=inputs)
|
||||
if hasattr(result_message, "content"):
|
||||
result: str = result_message.content
|
||||
elif isinstance(result_message, str):
|
||||
result = result_message
|
||||
else:
|
||||
result = str(result_message)
|
||||
self.repr_value = result
|
||||
return result
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
from langflow import CustomComponent
|
||||
from typing import Callable, Union
|
||||
from langflow.field_typing import BasePromptTemplate, BaseLanguageModel, Chain
|
||||
from langchain_community.utilities.sql_database import SQLDatabase
|
||||
from langchain_experimental.sql.base import SQLDatabaseChain
|
||||
|
||||
|
||||
class SQLDatabaseChainComponent(CustomComponent):
|
||||
display_name = "SQLDatabaseChain"
|
||||
description = ""
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"db": {"display_name": "Database"},
|
||||
"llm": {"display_name": "LLM"},
|
||||
"prompt": {"display_name": "Prompt"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
db: SQLDatabase,
|
||||
llm: BaseLanguageModel,
|
||||
prompt: BasePromptTemplate,
|
||||
) -> Union[Chain, Callable, SQLDatabaseChain]:
|
||||
return SQLDatabaseChain.from_llm(llm=llm, db=db, prompt=prompt)
|
||||
62
src/backend/langflow/components/chains/SQLGenerator.py
Normal file
62
src/backend/langflow/components/chains/SQLGenerator.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.chains import create_sql_query_chain
|
||||
from langchain_community.utilities.sql_database import SQLDatabase
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, Text
|
||||
|
||||
|
||||
class SQLGeneratorComponent(CustomComponent):
|
||||
display_name = "Natural Language to SQL"
|
||||
description = "Generate SQL from natural language."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"db": {"display_name": "Database"},
|
||||
"llm": {"display_name": "LLM"},
|
||||
"prompt": {
|
||||
"display_name": "Prompt",
|
||||
"info": "The prompt must contain `{question}`.",
|
||||
},
|
||||
"top_k": {
|
||||
"display_name": "Top K",
|
||||
"info": "The number of results per select statement to return. If 0, no limit.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
inputs: Text,
|
||||
db: SQLDatabase,
|
||||
llm: BaseLanguageModel,
|
||||
top_k: int = 5,
|
||||
prompt: Optional[PromptTemplate] = None,
|
||||
) -> Text:
|
||||
if top_k > 0:
|
||||
kwargs = {
|
||||
"k": top_k,
|
||||
}
|
||||
if not prompt:
|
||||
sql_query_chain = create_sql_query_chain(llm=llm, db=db, **kwargs)
|
||||
else:
|
||||
template = prompt.template if hasattr(prompt, "template") else prompt
|
||||
# Check if {question} is in the prompt
|
||||
if (
|
||||
"{question}" not in template
|
||||
or "question" not in template.input_variables
|
||||
):
|
||||
raise ValueError(
|
||||
"Prompt must contain `{question}` to be used with Natural Language to SQL."
|
||||
)
|
||||
sql_query_chain = create_sql_query_chain(
|
||||
llm=llm, db=db, prompt=prompt, **kwargs
|
||||
)
|
||||
query_writer = sql_query_chain | {
|
||||
"query": lambda x: x.replace("SQLQuery:", "").strip()
|
||||
}
|
||||
response = query_writer.invoke({"question": inputs})
|
||||
query = response.get("query")
|
||||
self.status = query
|
||||
return query
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.llms.huggingface_endpoint import HuggingFaceEndpoint
|
||||
|
||||
from langchain.llms.base import BaseLLM
|
||||
from langchain.llms.huggingface_endpoint import HuggingFaceEndpoint
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class HuggingFaceEndpointsComponent(CustomComponent):
|
||||
|
|
@ -31,11 +32,11 @@ class HuggingFaceEndpointsComponent(CustomComponent):
|
|||
model_kwargs: Optional[dict] = None,
|
||||
) -> BaseLLM:
|
||||
try:
|
||||
output = HuggingFaceEndpoint(
|
||||
output = HuggingFaceEndpoint( # type: ignore
|
||||
endpoint_url=endpoint_url,
|
||||
task=task,
|
||||
huggingfacehub_api_token=huggingfacehub_api_token,
|
||||
model_kwargs=model_kwargs,
|
||||
model_kwargs=model_kwargs or {},
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValueError("Could not connect to HuggingFace Endpoints API.") from e
|
||||
|
|
|
|||
22
src/backend/langflow/components/utilities/SQLDatabase.py
Normal file
22
src/backend/langflow/components/utilities/SQLDatabase.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from langchain_experimental.sql.base import SQLDatabase
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class SQLDatabaseComponent(CustomComponent):
|
||||
display_name = "SQLDatabase"
|
||||
description = "SQL Database"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"uri": {"display_name": "URI", "info": "URI to the database."},
|
||||
}
|
||||
|
||||
def clean_up_uri(self, uri: str) -> str:
|
||||
if uri.startswith("postgresql://"):
|
||||
uri = uri.replace("postgresql://", "postgres://")
|
||||
return uri.strip()
|
||||
|
||||
def build(self, uri: str) -> SQLDatabase:
|
||||
uri = self.clean_up_uri(uri)
|
||||
return SQLDatabase.from_uri(uri)
|
||||
56
src/backend/langflow/components/utilities/SQLExecutor.py
Normal file
56
src/backend/langflow/components/utilities/SQLExecutor.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from langchain_community.tools.sql_database.tool import QuerySQLDataBaseTool
|
||||
from langchain_experimental.sql.base import SQLDatabase
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class SQLExecutorComponent(CustomComponent):
|
||||
display_name = "SQL Executor"
|
||||
description = "Execute SQL query."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"database": {"display_name": "Database"},
|
||||
"include_columns": {
|
||||
"display_name": "Include Columns",
|
||||
"info": "Include columns in the result.",
|
||||
},
|
||||
"passthrough": {
|
||||
"display_name": "Passthrough",
|
||||
"info": "If an error occurs, return the query instead of raising an exception.",
|
||||
},
|
||||
"add_error": {
|
||||
"display_name": "Add Error",
|
||||
"info": "Add the error to the result.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
query: str,
|
||||
database: SQLDatabase,
|
||||
include_columns: bool = False,
|
||||
passthrough: bool = False,
|
||||
add_error: bool = False,
|
||||
) -> Text:
|
||||
error = None
|
||||
try:
|
||||
tool = QuerySQLDataBaseTool(db=database)
|
||||
result = tool.run(query, include_columns=include_columns)
|
||||
self.status = result
|
||||
except Exception as e:
|
||||
result = str(e)
|
||||
self.status = result
|
||||
if not passthrough:
|
||||
raise e
|
||||
error = repr(e)
|
||||
|
||||
if add_error and error is not None:
|
||||
result = f"{result}\n\nError: {error}\n\nQuery: {query}"
|
||||
elif error is not None:
|
||||
# Then we won't add the error to the result
|
||||
# but since we are in passthrough mode, we will return the query
|
||||
result = query
|
||||
|
||||
return result
|
||||
|
|
@ -85,7 +85,9 @@ class Graph:
|
|||
def build_parent_child_map(self):
|
||||
parent_child_map = defaultdict(list)
|
||||
for vertex in self.vertices:
|
||||
parent_child_map[vertex.id] = [child.id for child in self.get_successors(vertex)]
|
||||
parent_child_map[vertex.id] = [
|
||||
child.id for child in self.get_successors(vertex)
|
||||
]
|
||||
return parent_child_map
|
||||
|
||||
def increment_run_count(self):
|
||||
|
|
@ -149,6 +151,18 @@ class Graph:
|
|||
# both graphs have the same vertices and edges
|
||||
# but the data of the vertices might be different
|
||||
|
||||
def vertex_data_is_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool:
|
||||
return vertex.__repr__() == other_vertex.__repr__()
|
||||
|
||||
def vertex_edges_are_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool:
|
||||
same_length = len(vertex.edges) == len(other_vertex.edges)
|
||||
if not same_length:
|
||||
return False
|
||||
for edge in vertex.edges:
|
||||
if edge not in other_vertex.edges:
|
||||
return False
|
||||
return True
|
||||
|
||||
def update(self, other: "Graph") -> None:
|
||||
# Existing vertices in self graph
|
||||
existing_vertex_ids = set(vertex.id for vertex in self.vertices)
|
||||
|
|
@ -161,11 +175,14 @@ class Graph:
|
|||
# Find vertices that are in self but not in other (removed vertices)
|
||||
removed_vertex_ids = existing_vertex_ids - other_vertex_ids
|
||||
|
||||
# Create a set for new edges
|
||||
new_edges = set()
|
||||
|
||||
# Update existing vertices that have changed
|
||||
for vertex_id in existing_vertex_ids.intersection(other_vertex_ids):
|
||||
self_vertex = self.get_vertex(vertex_id)
|
||||
other_vertex = other.get_vertex(vertex_id)
|
||||
if self_vertex.__repr__() != other_vertex.__repr__():
|
||||
if not self.vertex_data_is_identical(self_vertex, other_vertex):
|
||||
self_vertex._data = other_vertex._data
|
||||
self_vertex._parse_data()
|
||||
self_vertex.params = {}
|
||||
|
|
@ -179,6 +196,14 @@ class Graph:
|
|||
self_vertex.artifacts = None
|
||||
self_vertex.set_top_level(self.top_level_vertices)
|
||||
self.reset_all_edges_of_vertex(self_vertex)
|
||||
if not self.vertex_edges_are_identical(self_vertex, other_vertex):
|
||||
new_edges.update(other_vertex.edges)
|
||||
|
||||
# Add new edges
|
||||
# to self.edges if they are not already in self.edges
|
||||
for edge in new_edges:
|
||||
if edge not in self.edges:
|
||||
self.edges.append(edge)
|
||||
|
||||
# Remove vertices
|
||||
for vertex_id in removed_vertex_ids:
|
||||
|
|
@ -255,7 +280,11 @@ class Graph:
|
|||
return
|
||||
self.vertices.remove(vertex)
|
||||
self.vertex_map.pop(vertex_id)
|
||||
self.edges = [edge for edge in self.edges if edge.source_id != vertex_id and edge.target_id != vertex_id]
|
||||
self.edges = [
|
||||
edge
|
||||
for edge in self.edges
|
||||
if edge.source_id != vertex_id and edge.target_id != vertex_id
|
||||
]
|
||||
|
||||
def _build_vertex_params(self) -> None:
|
||||
"""Identifies and handles the LLM vertex within the graph."""
|
||||
|
|
@ -276,7 +305,9 @@ class Graph:
|
|||
return
|
||||
for vertex in self.vertices:
|
||||
if not self._validate_vertex(vertex):
|
||||
raise ValueError(f"{vertex.vertex_type} is not connected to any other components")
|
||||
raise ValueError(
|
||||
f"{vertex.vertex_type} is not connected to any other components"
|
||||
)
|
||||
|
||||
def _validate_vertex(self, vertex: Vertex) -> bool:
|
||||
"""Validates a vertex."""
|
||||
|
|
@ -292,7 +323,11 @@ class Graph:
|
|||
|
||||
def get_vertex_edges(self, vertex_id: str) -> List[ContractEdge]:
|
||||
"""Returns a list of edges for a given vertex."""
|
||||
return [edge for edge in self.edges if edge.source_id == vertex_id or edge.target_id == vertex_id]
|
||||
return [
|
||||
edge
|
||||
for edge in self.edges
|
||||
if edge.source_id == vertex_id or edge.target_id == vertex_id
|
||||
]
|
||||
|
||||
def get_vertices_with_target(self, vertex_id: str) -> List[Vertex]:
|
||||
"""Returns the vertices connected to a vertex."""
|
||||
|
|
@ -330,7 +365,9 @@ class Graph:
|
|||
def dfs(vertex):
|
||||
if state[vertex] == 1:
|
||||
# We have a cycle
|
||||
raise ValueError("Graph contains a cycle, cannot perform topological sort")
|
||||
raise ValueError(
|
||||
"Graph contains a cycle, cannot perform topological sort"
|
||||
)
|
||||
if state[vertex] == 0:
|
||||
state[vertex] = 1
|
||||
for edge in vertex.edges:
|
||||
|
|
@ -354,11 +391,17 @@ class Graph:
|
|||
|
||||
def get_predecessors(self, vertex):
|
||||
"""Returns the predecessors of a vertex."""
|
||||
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_successors(self, vertex):
|
||||
"""Returns the successors of a vertex."""
|
||||
return [self.get_vertex(target_id) for target_id in self.successor_map.get(vertex.id, [])]
|
||||
return [
|
||||
self.get_vertex(target_id)
|
||||
for target_id in self.successor_map.get(vertex.id, [])
|
||||
]
|
||||
|
||||
def get_vertex_neighbors(self, vertex: Vertex) -> Dict[Vertex, int]:
|
||||
"""Returns the neighbors of a vertex."""
|
||||
|
|
@ -397,7 +440,9 @@ class Graph:
|
|||
edges.append(ContractEdge(source, target, edge))
|
||||
return edges
|
||||
|
||||
def _get_vertex_class(self, node_type: str, node_base_type: str, node_id: str) -> Type[Vertex]:
|
||||
def _get_vertex_class(
|
||||
self, node_type: str, node_base_type: str, node_id: str
|
||||
) -> Type[Vertex]:
|
||||
"""Returns the node class based on the node type."""
|
||||
# First we check for the node_base_type
|
||||
node_name = node_id.split("-")[0]
|
||||
|
|
@ -428,14 +473,18 @@ class Graph:
|
|||
vertex_type: str = vertex_data["type"] # type: ignore
|
||||
vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore
|
||||
|
||||
VertexClass = self._get_vertex_class(vertex_type, vertex_base_type, vertex_data["id"])
|
||||
VertexClass = self._get_vertex_class(
|
||||
vertex_type, vertex_base_type, vertex_data["id"]
|
||||
)
|
||||
vertex_instance = VertexClass(vertex, graph=self)
|
||||
vertex_instance.set_top_level(self.top_level_vertices)
|
||||
vertices.append(vertex_instance)
|
||||
|
||||
return vertices
|
||||
|
||||
def get_children_by_vertex_type(self, vertex: Vertex, vertex_type: str) -> List[Vertex]:
|
||||
def get_children_by_vertex_type(
|
||||
self, vertex: Vertex, vertex_type: str
|
||||
) -> List[Vertex]:
|
||||
"""Returns the children of a vertex based on the vertex type."""
|
||||
children = []
|
||||
vertex_types = [vertex.data["type"]]
|
||||
|
|
@ -447,7 +496,9 @@ class Graph:
|
|||
|
||||
def __repr__(self):
|
||||
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}"
|
||||
|
||||
def sort_up_to_vertex(self, vertex_id: str) -> "Graph":
|
||||
|
|
@ -478,7 +529,9 @@ class Graph:
|
|||
"""Performs a layered topological sort of the vertices in the graph."""
|
||||
|
||||
# Queue for vertices with no incoming edges
|
||||
queue = deque(vertex.id for vertex in vertices if self.in_degree_map[vertex.id] == 0)
|
||||
queue = deque(
|
||||
vertex.id for vertex in vertices if self.in_degree_map[vertex.id] == 0
|
||||
)
|
||||
layers = []
|
||||
|
||||
current_layer = 0
|
||||
|
|
@ -534,7 +587,9 @@ class Graph:
|
|||
|
||||
return refined_layers
|
||||
|
||||
def sort_chat_inputs_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
|
||||
def sort_chat_inputs_first(
|
||||
self, vertices_layers: List[List[str]]
|
||||
) -> List[List[str]]:
|
||||
chat_inputs_first = []
|
||||
for layer in vertices_layers:
|
||||
for vertex_id in layer:
|
||||
|
|
@ -562,11 +617,15 @@ class Graph:
|
|||
self.increment_run_count()
|
||||
return vertices_layers
|
||||
|
||||
def sort_interface_components_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
|
||||
def sort_interface_components_first(
|
||||
self, vertices_layers: List[List[str]]
|
||||
) -> List[List[str]]:
|
||||
"""Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first."""
|
||||
|
||||
def contains_interface_component(vertex):
|
||||
return any(component.value in vertex for component in InterfaceComponentTypes)
|
||||
return any(
|
||||
component.value in vertex for component in InterfaceComponentTypes
|
||||
)
|
||||
|
||||
# Sort each inner list so that vertices containing ChatInput or ChatOutput come first
|
||||
sorted_vertices = [
|
||||
|
|
@ -585,9 +644,13 @@ class Graph:
|
|||
"""Sorts the vertices in the graph so that vertices with the lowest average build time come first."""
|
||||
if len(vertices_ids) == 1:
|
||||
return vertices_ids
|
||||
vertices_ids.sort(key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time)
|
||||
vertices_ids.sort(
|
||||
key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time
|
||||
)
|
||||
|
||||
return vertices_ids
|
||||
|
||||
sorted_vertices = [sort_layer_by_avg_build_time(layer) for layer in vertices_layers]
|
||||
sorted_vertices = [
|
||||
sort_layer_by_avg_build_time(layer) for layer in vertices_layers
|
||||
]
|
||||
return sorted_vertices
|
||||
|
|
|
|||
|
|
@ -27,14 +27,18 @@ from langflow.utils import validate
|
|||
from langflow.utils.util import get_base_classes
|
||||
|
||||
|
||||
def add_output_types(frontend_node: CustomComponentFrontendNode, return_types: List[str]):
|
||||
def add_output_types(
|
||||
frontend_node: CustomComponentFrontendNode, return_types: List[str]
|
||||
):
|
||||
"""Add output types to the frontend node"""
|
||||
for return_type in return_types:
|
||||
if return_type is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": ("Invalid return type. Please check your code and try again."),
|
||||
"error": (
|
||||
"Invalid return type. Please check your code and try again."
|
||||
),
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
)
|
||||
|
|
@ -63,14 +67,18 @@ def reorder_fields(frontend_node: CustomComponentFrontendNode, field_order: List
|
|||
frontend_node.template.fields = reordered_fields
|
||||
|
||||
|
||||
def add_base_classes(frontend_node: CustomComponentFrontendNode, return_types: List[str]):
|
||||
def add_base_classes(
|
||||
frontend_node: CustomComponentFrontendNode, return_types: List[str]
|
||||
):
|
||||
"""Add base classes to the frontend node"""
|
||||
for return_type_instance in return_types:
|
||||
if return_type_instance is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": ("Invalid return type. Please check your code and try again."),
|
||||
"error": (
|
||||
"Invalid return type. Please check your code and try again."
|
||||
),
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
)
|
||||
|
|
@ -145,10 +153,14 @@ def add_new_custom_field(
|
|||
# If options is a list, then it's a dropdown
|
||||
# If options is None, then it's a list of strings
|
||||
is_list = isinstance(field_config.get("options"), list)
|
||||
field_config["is_list"] = is_list or field_config.get("is_list", False) or field_contains_list
|
||||
field_config["is_list"] = (
|
||||
is_list or field_config.get("is_list", False) or field_contains_list
|
||||
)
|
||||
|
||||
if "name" in field_config:
|
||||
warnings.warn("The 'name' key in field_config is used to build the object and can't be changed.")
|
||||
warnings.warn(
|
||||
"The 'name' key in field_config is used to build the object and can't be changed."
|
||||
)
|
||||
required = field_config.pop("required", field_required)
|
||||
placeholder = field_config.pop("placeholder", "")
|
||||
|
||||
|
|
@ -179,7 +191,9 @@ def add_extra_fields(frontend_node, field_config, function_args):
|
|||
if "name" not in extra_field or extra_field["name"] == "self":
|
||||
continue
|
||||
|
||||
field_name, field_type, field_value, field_required = get_field_properties(extra_field)
|
||||
field_name, field_type, field_value, field_required = get_field_properties(
|
||||
extra_field
|
||||
)
|
||||
config = field_config.get(field_name, {})
|
||||
frontend_node = add_new_custom_field(
|
||||
frontend_node,
|
||||
|
|
@ -217,7 +231,9 @@ def run_build_config(
|
|||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": ("Invalid type convertion. Please check your code and try again."),
|
||||
"error": (
|
||||
"Invalid type convertion. Please check your code and try again."
|
||||
),
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
) from exc
|
||||
|
|
@ -245,7 +261,9 @@ def run_build_config(
|
|||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": ("Invalid type convertion. Please check your code and try again."),
|
||||
"error": (
|
||||
"Invalid type convertion. Please check your code and try again."
|
||||
),
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
) from exc
|
||||
|
|
@ -300,16 +318,24 @@ def build_custom_component_template(
|
|||
frontend_node = build_frontend_node(custom_component.template_config)
|
||||
|
||||
logger.debug("Updated attributes")
|
||||
field_config, custom_instance = run_build_config(custom_component, user_id=user_id, update_field=update_field)
|
||||
field_config, custom_instance = run_build_config(
|
||||
custom_component, user_id=user_id, update_field=update_field
|
||||
)
|
||||
logger.debug("Built field config")
|
||||
entrypoint_args = custom_component.get_function_entrypoint_args
|
||||
|
||||
add_extra_fields(frontend_node, field_config, entrypoint_args)
|
||||
|
||||
frontend_node = add_code_field(frontend_node, custom_component.code, field_config.get("code", {}))
|
||||
frontend_node = add_code_field(
|
||||
frontend_node, custom_component.code, field_config.get("code", {})
|
||||
)
|
||||
|
||||
add_base_classes(frontend_node, custom_component.get_function_entrypoint_return_type)
|
||||
add_output_types(frontend_node, custom_component.get_function_entrypoint_return_type)
|
||||
add_base_classes(
|
||||
frontend_node, custom_component.get_function_entrypoint_return_type
|
||||
)
|
||||
add_output_types(
|
||||
frontend_node, custom_component.get_function_entrypoint_return_type
|
||||
)
|
||||
logger.debug("Added base classes")
|
||||
|
||||
reorder_fields(frontend_node, custom_instance._get_field_order())
|
||||
|
|
@ -321,7 +347,9 @@ def build_custom_component_template(
|
|||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": ("Invalid type convertion. Please check your code and try again."),
|
||||
"error": (
|
||||
"Invalid type convertion. Please check your code and try again."
|
||||
),
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
) from exc
|
||||
|
|
@ -345,7 +373,9 @@ def build_custom_components(settings_service):
|
|||
if not settings_service.settings.COMPONENTS_PATH:
|
||||
return {}
|
||||
|
||||
logger.info(f"Building custom components from {settings_service.settings.COMPONENTS_PATH}")
|
||||
logger.info(
|
||||
f"Building custom components from {settings_service.settings.COMPONENTS_PATH}"
|
||||
)
|
||||
custom_components_from_file = {}
|
||||
processed_paths = set()
|
||||
for path in settings_service.settings.COMPONENTS_PATH:
|
||||
|
|
@ -356,7 +386,9 @@ def build_custom_components(settings_service):
|
|||
custom_component_dict = build_custom_component_list_from_path(path_str)
|
||||
if custom_component_dict:
|
||||
category = next(iter(custom_component_dict))
|
||||
logger.info(f"Loading {len(custom_component_dict[category])} component(s) from category {category}")
|
||||
logger.info(
|
||||
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
|
||||
)
|
||||
custom_components_from_file = merge_nested_dicts_with_renaming(
|
||||
custom_components_from_file, custom_component_dict
|
||||
)
|
||||
|
|
@ -373,7 +405,7 @@ def update_field_dict(field_dict):
|
|||
field_dict["refresh"] = True
|
||||
|
||||
if "value" in field_dict and callable(field_dict["value"]):
|
||||
field_dict["value"] = field_dict["value"](field_dict.get("options", []))
|
||||
field_dict["value"] = field_dict["value"]()
|
||||
field_dict["refresh"] = True
|
||||
|
||||
# Let's check if "range_spec" is a RangeSpec object
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ from cachetools import LRUCache, cached
|
|||
|
||||
from langflow.interface.agents.base import agent_creator
|
||||
from langflow.interface.chains.base import chain_creator
|
||||
from langflow.interface.custom.directory_reader.utils import merge_nested_dicts_with_renaming
|
||||
from langflow.interface.custom.directory_reader.utils import \
|
||||
merge_nested_dicts_with_renaming
|
||||
from langflow.interface.custom.utils import build_custom_components
|
||||
from langflow.interface.document_loaders.base import documentloader_creator
|
||||
from langflow.interface.embeddings.base import embedding_creator
|
||||
from langflow.interface.llms.base import llm_creator
|
||||
from langflow.interface.memories.base import memory_creator
|
||||
from langflow.interface.output_parsers.base import output_parser_creator
|
||||
from langflow.interface.prompts.base import prompt_creator
|
||||
from langflow.interface.retrievers.base import retriever_creator
|
||||
from langflow.interface.text_splitters.base import textsplitter_creator
|
||||
from langflow.interface.toolkits.base import toolkits_creator
|
||||
|
|
@ -39,7 +39,7 @@ def build_langchain_types_dict(): # sourcery skip: dict-assign-update-to-union
|
|||
creators = [
|
||||
chain_creator,
|
||||
agent_creator,
|
||||
prompt_creator,
|
||||
# prompt_creator,
|
||||
llm_creator,
|
||||
memory_creator,
|
||||
tool_creator,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from typing import ClassVar, Dict, List, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from langchain_community.utilities import requests, sql_database
|
||||
from langchain_community.utilities import requests
|
||||
from loguru import logger
|
||||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
|
|
@ -10,13 +10,11 @@ from langflow.utils.util import build_template_from_class, build_template_from_m
|
|||
class WrapperCreator(LangChainTypeCreator):
|
||||
type_name: str = "wrappers"
|
||||
|
||||
from_method_nodes: ClassVar[Dict] = {"SQLDatabase": "from_uri"}
|
||||
|
||||
@property
|
||||
def type_to_loader_dict(self) -> Dict:
|
||||
if self.type_dict is None:
|
||||
self.type_dict = {
|
||||
wrapper.__name__: wrapper for wrapper in [requests.TextRequestsWrapper, sql_database.SQLDatabase]
|
||||
wrapper.__name__: wrapper for wrapper in [requests.TextRequestsWrapper]
|
||||
}
|
||||
return self.type_dict
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,9 @@ class TemplateField(BaseModel):
|
|||
refresh: Optional[bool] = None
|
||||
"""Specifies if the field should be refreshed. Defaults to False."""
|
||||
|
||||
range_spec: Optional[RangeSpec] = Field(default=None, serialization_alias="rangeSpec")
|
||||
range_spec: Optional[RangeSpec] = Field(
|
||||
default=None, serialization_alias="rangeSpec"
|
||||
)
|
||||
"""Range specification for the field. Defaults to None."""
|
||||
|
||||
title_case: bool = False
|
||||
|
|
@ -115,6 +117,10 @@ class TemplateField(BaseModel):
|
|||
if not isinstance(value, list):
|
||||
raise ValueError("file_types must be a list")
|
||||
return [
|
||||
(f".{file_type}" if isinstance(file_type, str) and not file_type.startswith(".") else file_type)
|
||||
(
|
||||
f".{file_type}"
|
||||
if isinstance(file_type, str) and not file_type.startswith(".")
|
||||
else file_type
|
||||
)
|
||||
for file_type in value
|
||||
]
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export default function GenericNode({
|
|||
const [nodeDescription, setNodeDescription] = useState(
|
||||
data.node?.description!
|
||||
);
|
||||
const buildStatus = useFlowStore((state) =>state.flowBuildStatus[data.id]);
|
||||
const buildStatus = useFlowStore((state) => state.flowBuildStatus[data.id]);
|
||||
const [validationStatus, setValidationStatus] =
|
||||
useState<validationStatusType | null>(null);
|
||||
const [handles, setHandles] = useState<number>(0);
|
||||
|
|
@ -122,7 +122,6 @@ export default function GenericNode({
|
|||
}, [flowPool, data.id]);
|
||||
|
||||
const showNode = data.showNode ?? true;
|
||||
const pinned = data.node?.pinned ?? false;
|
||||
|
||||
const nameEditable = data.node?.flow || data.type === "CustomComponent";
|
||||
|
||||
|
|
@ -162,7 +161,7 @@ export default function GenericNode({
|
|||
const getIconPlayOrPauseComponent = (name, className) => (
|
||||
<IconComponent
|
||||
name={name}
|
||||
className={`absolute h-5 stroke-2 ${className} ml-0.5`}
|
||||
className={`h-4 fill-current stroke-2 ${className}`}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -181,15 +180,18 @@ export default function GenericNode({
|
|||
return "red-status";
|
||||
} else if (!validationStatus && buildStatus === BuildStatus.TO_BUILD) {
|
||||
return "green-status";
|
||||
} else if (buildStatus === BuildStatus.BUILDING) {
|
||||
return "status-build-animation";
|
||||
} else {
|
||||
return "green-status";
|
||||
return "yellow-status";
|
||||
}
|
||||
};
|
||||
|
||||
const isDark = useDarkStore((state) => state.dark);
|
||||
console.log(isDark);
|
||||
const renderIconStatusComponents = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null
|
||||
) => {
|
||||
const className = getStatusClassName(buildStatus, validationStatus);
|
||||
return <>{getIconPlayOrPauseComponent("CircleDot", className)}</>;
|
||||
};
|
||||
const renderIconPlayOrPauseComponents = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null
|
||||
|
|
@ -197,8 +199,12 @@ export default function GenericNode({
|
|||
if (buildStatus === BuildStatus.BUILDING) {
|
||||
return <Loading />;
|
||||
} else {
|
||||
const className = getStatusClassName(buildStatus, validationStatus);
|
||||
return <>{getIconPlayOrPauseComponent("Play", className)}</>;
|
||||
return (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground hover:text-medium-indigo"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -332,28 +338,37 @@ export default function GenericNode({
|
|||
</div>
|
||||
) : (
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
onDoubleClick={(event) => {
|
||||
if (nameEditable) {
|
||||
setInputName(true);
|
||||
}
|
||||
takeSnapshot();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-testid={"title-" + data.node?.display_name}
|
||||
className="generic-node-tooltip-div text-primary"
|
||||
>
|
||||
{data.node?.display_name}
|
||||
</div>
|
||||
<div className="group flex items-center gap-2.5">
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div
|
||||
onDoubleClick={(event) => {
|
||||
if (nameEditable) {
|
||||
setInputName(true);
|
||||
}
|
||||
takeSnapshot();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
data-testid={"title-" + data.node?.display_name}
|
||||
className="generic-node-tooltip-div text-primary"
|
||||
>
|
||||
{data.node?.display_name}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
{nameEditable && (
|
||||
<IconComponent
|
||||
name="Pencil"
|
||||
className="h-4 w-4 text-ring"
|
||||
/>
|
||||
<div
|
||||
onClick={(event) => {
|
||||
setInputName(true);
|
||||
takeSnapshot();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="Pencil"
|
||||
className="hidden h-4 w-4 animate-pulse text-status-blue group-hover:block"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
|
|
@ -451,44 +466,11 @@ export default function GenericNode({
|
|||
</div>
|
||||
{showNode && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-9 px-1.5"
|
||||
onClick={() => {
|
||||
setNode(data.id, (old) => ({
|
||||
...old,
|
||||
data: {
|
||||
...old.data,
|
||||
node: {
|
||||
...old.data.node,
|
||||
pinned: old.data?.node?.pinned ? false : true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title={<span>{pinned ? "Pin Output" : "Unpin Output"}</span>}
|
||||
>
|
||||
<div className="generic-node-status-position flex items-center">
|
||||
<IconComponent
|
||||
name={"Pin"}
|
||||
className={cn(
|
||||
"h-5 fill-transparent stroke-chat-trigger stroke-2 transition-all",
|
||||
pinned ? "animate-wiggle fill-chat-trigger" : ""
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
)}
|
||||
{showNode && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={"h-9 px-1.5"}
|
||||
variant="secondary"
|
||||
className={"group h-9 px-1.5"}
|
||||
onClick={() => {
|
||||
if (buildStatus === BuildStatus.BUILDING || isBuilding)
|
||||
return;
|
||||
|
||||
buildFlow(data.id);
|
||||
}}
|
||||
>
|
||||
|
|
@ -529,6 +511,32 @@ export default function GenericNode({
|
|||
</div>
|
||||
</Button>
|
||||
)}
|
||||
<div className="">
|
||||
<Tooltip
|
||||
title={
|
||||
data?.buildStatus === BuildStatus.BUILDING ? (
|
||||
<span>Building...</span>
|
||||
) : !validationStatus ? (
|
||||
<span className="flex">Build to validate status.</span>
|
||||
) : (
|
||||
<div className="max-h-96 overflow-auto">
|
||||
{typeof validationStatus.params === "string"
|
||||
? `${durationString}\n${validationStatus.params}`
|
||||
.split("\n")
|
||||
.map((line, index) => <div key={index}>{line}</div>)
|
||||
: durationString}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
{renderIconStatusComponents(
|
||||
data?.buildStatus,
|
||||
validationStatus
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export default function IOInputField({
|
|||
}
|
||||
}
|
||||
return (
|
||||
<div className="font-xl flex h-full w-full flex-col gap-4 p-4 font-semibold">
|
||||
<div className="font-xl flex items-start h-full w-full flex-col gap-4 p-4 font-semibold">
|
||||
{inputType}
|
||||
{handleInputType()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export default function IOOutputView({
|
|||
}
|
||||
}
|
||||
return (
|
||||
<div className="font-xl flex h-full w-full flex-col gap-4 p-4 font-semibold">
|
||||
<div className="font-xl flex h-full items-start w-full flex-col gap-4 p-4 font-semibold">
|
||||
{outputType}
|
||||
{handleOutputType()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { ReactNode, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import BaseModal from "../../modals/baseModal";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { NodeType } from "../../types/flow";
|
||||
import { isInputType, isOutputType } from "../../utils/reactflowUtils";
|
||||
import { cn } from "../../utils/utils";
|
||||
import AccordionComponent from "../AccordionComponent";
|
||||
import IOInputField from "../IOInputField";
|
||||
|
|
@ -12,62 +10,40 @@ import IconComponent from "../genericIconComponent";
|
|||
import NewChatView from "../newChatView";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Button } from "../ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
|
||||
export default function IOView({ children, open, setOpen }): JSX.Element {
|
||||
const inputs = useFlowStore((state) => state.inputs);
|
||||
const outputs = useFlowStore((state) => state.outputs);
|
||||
const inputIds = inputs.map((obj) => obj.id);
|
||||
const outputIds = outputs.map((obj) => obj.id);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const categories = getCategories();
|
||||
const [selectedCategory, setSelectedCategory] = useState<number>(0);
|
||||
const [showChat, setShowChat] = useState<boolean>(false);
|
||||
const [selectedView, setSelectedView] = useState<{
|
||||
type: string;
|
||||
id?: string;
|
||||
}>(handleInitialView());
|
||||
const inputs = useFlowStore((state) => state.inputs).filter(
|
||||
(input) => input.type !== "ChatInput"
|
||||
);
|
||||
const outputs = useFlowStore((state) => state.outputs).filter(
|
||||
(output) => output.type !== "ChatOutput"
|
||||
);
|
||||
const nodes = useFlowStore((state) => state.nodes).filter(
|
||||
(node) =>
|
||||
(inputs.some((input) => input.id === node.id) ||
|
||||
outputs.some((output) => output.id === node.id)) &&
|
||||
node.type !== "ChatInput" &&
|
||||
node.type !== "ChatOutput"
|
||||
);
|
||||
const haveChat = useFlowStore((state) => state.outputs).some(
|
||||
(output) => output.type === "ChatOutput"
|
||||
);
|
||||
const [selectedTab, setSelectedTab] = useState(
|
||||
inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0
|
||||
);
|
||||
const [selectedViewField, setSelectedViewField] = useState<
|
||||
{ type: string; id: string } | undefined
|
||||
>(undefined);
|
||||
|
||||
type CategoriesType = { name: string; icon: string };
|
||||
|
||||
function handleInitialView() {
|
||||
if (
|
||||
outputs.map((output) => output.type).includes("ChatOutput") ||
|
||||
inputs.map((input) => input.type).includes("ChatInput")
|
||||
) {
|
||||
return { type: "ChatOutput" };
|
||||
}
|
||||
return { type: "" };
|
||||
}
|
||||
|
||||
function getCategories() {
|
||||
const categories: CategoriesType[] = [];
|
||||
if (inputs.filter((input) => input.type !== "ChatInput").length > 0)
|
||||
categories.push({ name: "Inputs", icon: "TextCursorInput" });
|
||||
if (outputs.filter((output) => output.type !== "ChatOutput").length > 0)
|
||||
categories.push({ name: "Outputs", icon: "TerminalSquare" });
|
||||
return categories;
|
||||
}
|
||||
|
||||
function handleSelectChange(): ReactNode {
|
||||
const { type, id } = selectedView;
|
||||
if (type === "ChatOutput") return <NewChatView />;
|
||||
if (isInputType(type))
|
||||
return <IOInputField inputId={id!} inputType={type} />;
|
||||
if (isOutputType(type))
|
||||
return <IOOutputView outputId={id!} outputType={type} />;
|
||||
else return undefined;
|
||||
}
|
||||
|
||||
function UpdateAccordion() {
|
||||
return (categories[selectedCategory]?.name ?? "Inputs") === "Inputs"
|
||||
? inputs
|
||||
: outputs;
|
||||
}
|
||||
useEffect(() => {
|
||||
setSelectedViewField(undefined);
|
||||
setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0);
|
||||
}, [inputs.length, outputs.length]);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
size={handleSelectChange() ? "large" : "small"}
|
||||
size={haveChat ? "large" : "small"}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
>
|
||||
|
|
@ -85,125 +61,173 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
|
|||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<div className="flex-max-width mt-2 h-[80vh]">
|
||||
<div
|
||||
className={cn(
|
||||
"mr-6 flex h-full w-2/6 flex-col justify-start overflow-auto scrollbar-hide",
|
||||
handleSelectChange() ? "w-2/6" : "w-full"
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between py-2">
|
||||
<div className="flex items-start gap-4">
|
||||
{categories.map((category, index) => {
|
||||
return (
|
||||
//hide chat button if chat is alredy on the view
|
||||
<Button
|
||||
onClick={() => setSelectedCategory(index)}
|
||||
variant={
|
||||
index === selectedCategory ? "primary" : "secondary"
|
||||
}
|
||||
key={index}
|
||||
>
|
||||
<IconComponent
|
||||
name={category.icon}
|
||||
className=" file-component-variable"
|
||||
/>
|
||||
<span className="file-component-variables-span text-md">
|
||||
{category.name}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{(outputs.map((output) => output.type).includes("ChatOutput") ||
|
||||
inputs.map((output) => output.type).includes("chatInput")) &&
|
||||
selectedView.type !== "ChatOutput" && (
|
||||
<Button
|
||||
onClick={() => setSelectedView({ type: "ChatOutput" })}
|
||||
variant="outline"
|
||||
key={"chat"}
|
||||
className="self-end px-2.5"
|
||||
>
|
||||
<IconComponent
|
||||
name="MessageSquareMore"
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mx-2 mb-2 mt-4 flex items-center gap-2 font-semibold">
|
||||
{categories[selectedCategory]?.name === "Inputs" && (
|
||||
<>
|
||||
<IconComponent name={"FormInput"} />
|
||||
Text Inputs
|
||||
</>
|
||||
{selectedTab !== 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start overflow-auto scrollbar-hide",
|
||||
haveChat ? "w-2/6" : "w-full"
|
||||
)}
|
||||
{categories[selectedCategory]?.name === "Outputs" && (
|
||||
<>
|
||||
<IconComponent name={"ChevronRightSquare"} />
|
||||
Prompt Outputs
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{UpdateAccordion()
|
||||
.filter(
|
||||
(input) =>
|
||||
input.type !== "ChatInput" && input.type !== "ChatOutput"
|
||||
)
|
||||
.map((input, index) => {
|
||||
const node: NodeType = nodes.find(
|
||||
(node) => node.id === input.id
|
||||
)!;
|
||||
return (
|
||||
<div className="file-component-accordion-div" key={index}>
|
||||
<AccordionComponent
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<Badge variant="gray" size="md">
|
||||
{input.id}
|
||||
</Badge>
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedView({
|
||||
type: input.type,
|
||||
id: input.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={input.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{node &&
|
||||
(categories[selectedCategory]?.name === "Inputs" ? (
|
||||
<IOInputField
|
||||
inputType={input.type}
|
||||
inputId={input.id}
|
||||
/>
|
||||
) : (
|
||||
<IOOutputView
|
||||
outputType={input.type}
|
||||
outputId={input.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
>
|
||||
<Tabs
|
||||
value={selectedTab.toString()}
|
||||
className={"api-modal-tabs "}
|
||||
onValueChange={(value) => {
|
||||
setSelectedTab(Number(value));
|
||||
}}
|
||||
>
|
||||
<div className="api-modal-tablist-div">
|
||||
<TabsList>
|
||||
{inputs.length > 0 && (
|
||||
<TabsTrigger value={"1"}>Inputs</TabsTrigger>
|
||||
)}
|
||||
{outputs.length > 0 && (
|
||||
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent
|
||||
value={"1"}
|
||||
className="api-modal-tabs-content mt-4"
|
||||
>
|
||||
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
|
||||
<IconComponent className="h-4 w-4" name={"Type"} />
|
||||
Text Inputs
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{handleSelectChange() ? (
|
||||
handleSelectChange()
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
inputs.some((input) => input.id === node.id)
|
||||
)
|
||||
.map((node, index) => {
|
||||
const input = inputs.find(
|
||||
(input) => input.id === node.id
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div"
|
||||
key={index}
|
||||
>
|
||||
<AccordionComponent
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<Badge variant="gray" size="md">
|
||||
{input.id}
|
||||
</Badge>
|
||||
{haveChat && (
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(input);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={input.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{input && (
|
||||
<IOInputField
|
||||
inputType={input.type}
|
||||
inputId={input.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value={"2"}
|
||||
className="api-modal-tabs-content mt-4"
|
||||
>
|
||||
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
|
||||
<IconComponent className="h-4 w-4" name={"Braces"} />
|
||||
Prompt Outputs
|
||||
</div>
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
outputs.some((output) => output.id === node.id)
|
||||
)
|
||||
.map((node, index) => {
|
||||
const output = outputs.find(
|
||||
(output) => output.id === node.id
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div"
|
||||
key={index}
|
||||
>
|
||||
<AccordionComponent
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<Badge variant="gray" size="md">
|
||||
{output.id}
|
||||
</Badge>
|
||||
{haveChat && (
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(output);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={output.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{output && (
|
||||
<IOOutputView
|
||||
outputType={output.type}
|
||||
outputId={output.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{haveChat ? (
|
||||
selectedViewField ? (
|
||||
inputs.some((input) => input.id === selectedViewField.id) ? (
|
||||
<IOInputField
|
||||
inputType={selectedViewField.type!}
|
||||
inputId={selectedViewField.id!}
|
||||
/>
|
||||
) : (
|
||||
<IOOutputView
|
||||
outputType={selectedViewField.type!}
|
||||
outputId={selectedViewField.id!}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<NewChatView />
|
||||
)
|
||||
) : (
|
||||
<div className="absolute bottom-8 right-8">
|
||||
<Button className="px-3">
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import useAlertStore from "../../../../stores/alertStore";
|
|||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
import Tooltip from "../../../TooltipComponent";
|
||||
import ShadTooltip from "../../../ShadTooltipComponent";
|
||||
import IconComponent from "../../../genericIconComponent";
|
||||
import { Button } from "../../../ui/button";
|
||||
|
||||
|
|
@ -125,8 +125,8 @@ export const MenuBar = ({
|
|||
setOpen={setOpenSettings}
|
||||
></FlowSettingsModal>
|
||||
</div>
|
||||
<Tooltip
|
||||
title={
|
||||
<ShadTooltip
|
||||
content={
|
||||
"Last saved at " +
|
||||
new Date(currentFlow.updated_at ?? "").toLocaleString("en-US", {
|
||||
hour: "numeric",
|
||||
|
|
@ -134,8 +134,10 @@ export const MenuBar = ({
|
|||
second: "numeric",
|
||||
})
|
||||
}
|
||||
side="bottom"
|
||||
styleClasses="cursor-default"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<div className="flex cursor-default items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<IconComponent
|
||||
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
|
||||
className={cn(
|
||||
|
|
@ -145,7 +147,7 @@ export const MenuBar = ({
|
|||
/>
|
||||
{printByBuildStatus()}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { classNames } from "../../utils/utils";
|
|||
import ChatInput from "./chatInput";
|
||||
import ChatMessage from "./chatMessage";
|
||||
|
||||
export default function newChatView(): JSX.Element {
|
||||
export default function NewChatView(): JSX.Element {
|
||||
const [chatValue, setChatValue] = useState("");
|
||||
const {
|
||||
flowPool,
|
||||
|
|
|
|||
|
|
@ -681,4 +681,4 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
|
|||
export const priorityFields = new Set(["code", "template"]);
|
||||
|
||||
export const INPUT_TYPES = new Set(["ChatInput", "TextInput"]);
|
||||
export const OUTPUT_TYPES = new Set(["ChatOutput"]);
|
||||
export const OUTPUT_TYPES = new Set(["ChatOutput", "PromptTemplate"]);
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ export default function GenericModal({
|
|||
{myModalTitle}
|
||||
</span>
|
||||
<IconComponent
|
||||
name="FileText"
|
||||
name={myModalTitle === "Edit Prompt" ? "TerminalSquare" : "FileText"}
|
||||
className="h-6 w-6 pl-1 text-primary "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
|
@ -255,7 +255,7 @@ export default function GenericModal({
|
|||
>
|
||||
<div className="flex flex-wrap items-center">
|
||||
<IconComponent
|
||||
name="Variable"
|
||||
name="Braces"
|
||||
className=" -ml-px mr-1 flex h-4 w-4 text-primary"
|
||||
/>
|
||||
<span className="text-md font-semibold text-primary">
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
generateNodeFromFlow,
|
||||
getNodeId,
|
||||
isValidConnection,
|
||||
reconnectEdges,
|
||||
validateSelection,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
|
||||
|
|
@ -394,7 +395,7 @@ export default function Page({
|
|||
if (
|
||||
validateSelection(lastSelection!, edges).length === 0
|
||||
) {
|
||||
const { newFlow } = generateFlow(
|
||||
const { newFlow, removedEdges } = generateFlow(
|
||||
lastSelection!,
|
||||
nodes,
|
||||
edges,
|
||||
|
|
@ -404,6 +405,10 @@ export default function Page({
|
|||
newFlow,
|
||||
getNodeId
|
||||
);
|
||||
const newEdges = reconnectEdges(
|
||||
newGroupNode,
|
||||
removedEdges
|
||||
);
|
||||
setNodes((oldNodes) => [
|
||||
...oldNodes.filter(
|
||||
(oldNodes) =>
|
||||
|
|
@ -414,16 +419,17 @@ export default function Page({
|
|||
),
|
||||
newGroupNode,
|
||||
]);
|
||||
setEdges((oldEdges) =>
|
||||
oldEdges.filter(
|
||||
setEdges((oldEdges) => [
|
||||
...oldEdges.filter(
|
||||
(oldEdge) =>
|
||||
!lastSelection!.nodes.some(
|
||||
(selectionNode) =>
|
||||
selectionNode.id === oldEdge.target ||
|
||||
selectionNode.id === oldEdge.source
|
||||
)
|
||||
)
|
||||
);
|
||||
),
|
||||
...newEdges,
|
||||
]);
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Invalid selection",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
expandGroupNode,
|
||||
updateFlowPosition,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { classNames } from "../../../../utils/utils";
|
||||
import { classNames, cn } from "../../../../utils/utils";
|
||||
|
||||
export default function NodeToolbarComponent({
|
||||
data,
|
||||
|
|
@ -60,6 +60,7 @@ export default function NodeToolbarComponent({
|
|||
const isMinimal = numberOfHandles <= 1;
|
||||
const isGroup = data.node?.flow ? true : false;
|
||||
|
||||
const pinned = data.node?.pinned ?? false;
|
||||
const paste = useFlowStore((state) => state.paste);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
|
|
@ -107,6 +108,9 @@ export default function NodeToolbarComponent({
|
|||
takeSnapshot();
|
||||
setShowNode(data.showNode ?? true ? false : true);
|
||||
break;
|
||||
case "Share":
|
||||
if (hasApiKey || hasStore) setShowconfirmShare(true);
|
||||
break;
|
||||
case "Download":
|
||||
downloadNode(flowComponent!);
|
||||
break;
|
||||
|
|
@ -222,7 +226,7 @@ export default function NodeToolbarComponent({
|
|||
id={"code-input-node-toolbar-" + name}
|
||||
/>
|
||||
</div>
|
||||
<IconComponent name="Code2" className="h-4 w-4" />
|
||||
<IconComponent name="TerminalSquare" className="h-4 w-4" />
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
) : (
|
||||
|
|
@ -269,22 +273,35 @@ export default function NodeToolbarComponent({
|
|||
<IconComponent name="Copy" className="h-4 w-4" />
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
{hasStore && (
|
||||
<ShadTooltip content="Share" side="top">
|
||||
<button
|
||||
className={classNames(
|
||||
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
|
||||
!hasApiKey || !validApiKey ? " text-muted-foreground" : ""
|
||||
|
||||
<ShadTooltip content="Pin" side="top">
|
||||
<button
|
||||
className={classNames(
|
||||
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
setNode(data.id, (old) => ({
|
||||
...old,
|
||||
data: {
|
||||
...old.data,
|
||||
node: {
|
||||
...old.data.node,
|
||||
pinned: old.data?.node?.pinned ? false : true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="Pin"
|
||||
className={cn(
|
||||
"h-4 w-4 transition-all",
|
||||
pinned ? "animate-wiggle fill-current" : ""
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
if (hasApiKey || hasStore) setShowconfirmShare(true);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="Share3" className="-m-1 h-6 w-6" />
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
|
||||
<Select onValueChange={handleSelectChange} value="">
|
||||
<ShadTooltip content="More" side="top">
|
||||
|
|
@ -340,6 +357,20 @@ export default function NodeToolbarComponent({
|
|||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
{hasStore && (
|
||||
<SelectItem
|
||||
value={"Share"}
|
||||
disabled={!hasApiKey || !validApiKey}
|
||||
>
|
||||
<div className="flex" data-testid="save-button-modal">
|
||||
<IconComponent
|
||||
name="Share3"
|
||||
className="relative top-0.5 -m-1 mr-1 h-6 w-6"
|
||||
/>{" "}
|
||||
Share{" "}
|
||||
</div>{" "}
|
||||
</SelectItem>
|
||||
)}
|
||||
{!hasStore && (
|
||||
<SelectItem value={"Download"}>
|
||||
<div className="flex">
|
||||
|
|
|
|||
|
|
@ -845,7 +845,7 @@
|
|||
@apply flex h-full max-w-full flex-col overflow-hidden rounded-md border bg-muted text-center sm:w-[75vw] md:w-[75vw] lg:w-[75vw] xl:w-[76vw] 2xl:w-full;
|
||||
}
|
||||
.api-modal-tablist-div {
|
||||
@apply flex items-center justify-between px-2;
|
||||
@apply flex items-center justify-between px-2 py-2;
|
||||
}
|
||||
.api-modal-tabs-content {
|
||||
@apply -mt-1 h-full w-full overflow-hidden px-4 pb-4;
|
||||
|
|
|
|||
|
|
@ -597,8 +597,7 @@ export function generateFlow(
|
|||
const newFlowData = { nodes, edges, viewport: { zoom: 1, x: 0, y: 0 } };
|
||||
const uid = new ShortUniqueId({ length: 5 });
|
||||
/* remove edges that are not connected to selected nodes on both ends
|
||||
in future we can save this edges to when ungrouping reconect to the old nodes
|
||||
*/
|
||||
*/
|
||||
newFlowData.edges = selection.edges.filter(
|
||||
(edge) =>
|
||||
selection.nodes.some((node) => node.id === edge.target) &&
|
||||
|
|
@ -619,12 +618,48 @@ export function generateFlow(
|
|||
// in the future we can use a better aproach using a set
|
||||
return {
|
||||
newFlow,
|
||||
removedEdges: selection.edges.filter(
|
||||
(edge) => !newFlowData.edges.includes(edge)
|
||||
removedEdges: edges.filter(
|
||||
(edge) =>
|
||||
(selection.nodes.some((node) => node.id === edge.target) ||
|
||||
selection.nodes.some((node) => node.id === edge.source)) &&
|
||||
newFlowData.edges.every((e) => e.id !== edge.id)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function reconnectEdges(groupNode: NodeType, excludedEdges: Edge[]) {
|
||||
let newEdges = cloneDeep(excludedEdges);
|
||||
if (!groupNode.data.node!.flow) return [];
|
||||
const { nodes, edges } = groupNode.data.node!.flow!.data!;
|
||||
const lastNode = findLastNode(groupNode.data.node!.flow!.data!);
|
||||
newEdges.forEach((edge) => {
|
||||
if (lastNode && edge.source === lastNode.id) {
|
||||
edge.source = groupNode.id;
|
||||
let newSourceHandle: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
newSourceHandle.id = groupNode.id;
|
||||
edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
|
||||
edge.data.sourceHandle = newSourceHandle;
|
||||
}
|
||||
if (nodes.some((node) => node.id === edge.target)) {
|
||||
const targetNode = nodes.find((node) => node.id === edge.target)!;
|
||||
console.log("targetNode", targetNode);
|
||||
const targetHandle: targetHandleType = scapeJSONParse(edge.targetHandle!);
|
||||
console.log("targetHandle", targetHandle);
|
||||
const proxy = { id: targetNode.id, field: targetHandle.fieldName };
|
||||
let newTargetHandle: targetHandleType = cloneDeep(targetHandle);
|
||||
newTargetHandle.id = groupNode.id;
|
||||
newTargetHandle.proxy = proxy;
|
||||
edge.target = groupNode.id;
|
||||
newTargetHandle.fieldName = targetHandle.fieldName + "_" + targetNode.id;
|
||||
edge.targetHandle = scapedJSONStringfy(newTargetHandle);
|
||||
edge.data.targetHandle = newTargetHandle;
|
||||
}
|
||||
});
|
||||
return newEdges;
|
||||
}
|
||||
|
||||
export function filterFlow(
|
||||
selection: OnSelectionChangeParams,
|
||||
setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
BookmarkPlus,
|
||||
Bot,
|
||||
Boxes,
|
||||
Braces,
|
||||
Cable,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
|
|
@ -18,6 +19,7 @@ import {
|
|||
ChevronsRight,
|
||||
ChevronsUpDown,
|
||||
Circle,
|
||||
CircleDot,
|
||||
Clipboard,
|
||||
Code,
|
||||
Code2,
|
||||
|
|
@ -102,6 +104,7 @@ import {
|
|||
TextCursorInput,
|
||||
ToyBrick,
|
||||
Trash2,
|
||||
Type,
|
||||
Undo,
|
||||
Ungroup,
|
||||
Unplug,
|
||||
|
|
@ -345,6 +348,7 @@ export const nodeIconsLucide: iconsType = {
|
|||
FileSearch2,
|
||||
ChevronRight,
|
||||
Circle,
|
||||
CircleDot,
|
||||
Clipboard,
|
||||
Code2,
|
||||
Variable,
|
||||
|
|
@ -414,6 +418,8 @@ export const nodeIconsLucide: iconsType = {
|
|||
Sliders,
|
||||
ScreenShare,
|
||||
Code,
|
||||
Type,
|
||||
Braces,
|
||||
FlaskConical,
|
||||
AlertCircle,
|
||||
Bot,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue