Merge zustand/io/migration into codeShortcut

This commit is contained in:
igorrCarvalho 2024-03-04 15:59:11 -03:00
commit 2760bd5428
122 changed files with 2731 additions and 2020 deletions

View file

@ -56,6 +56,13 @@ LANGFLOW_REMOVE_API_KEYS=
# LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600) # LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600)
LANGFLOW_CACHE_TYPE= LANGFLOW_CACHE_TYPE=
# Set AUTO_LOGIN to false if you want to disable auto login
# and use the login form to login. LANGFLOW_SUPERUSER and LANGFLOW_SUPERUSER_PASSWORD
# must be set if AUTO_LOGIN is set to false
# Values: true, false
LANGFLOW_AUTO_LOGIN=
# Superuser username # Superuser username
# Example: LANGFLOW_SUPERUSER=admin # Example: LANGFLOW_SUPERUSER=admin
LANGFLOW_SUPERUSER= LANGFLOW_SUPERUSER=

View file

@ -68,8 +68,6 @@ INVALID_CHARACTERS = {
")", ")",
"[", "[",
"]", "]",
"{",
"}",
} }
INVALID_NAMES = { INVALID_NAMES = {
@ -88,73 +86,110 @@ def validate_prompt(template: str):
# Check if there are invalid characters in the input_variables # Check if there are invalid characters in the input_variables
input_variables = check_input_variables(input_variables) input_variables = check_input_variables(input_variables)
if any(var in INVALID_NAMES for var in input_variables): if any(var in INVALID_NAMES for var in input_variables):
raise ValueError(f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. ") raise ValueError(
f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. "
)
try: try:
PromptTemplate(template=template, input_variables=input_variables) PromptTemplate(template=template, input_variables=input_variables)
except Exception as exc: except Exception as exc:
raise ValueError(str(exc)) from exc raise ValueError(f"Invalid prompt: {exc}") from exc
return input_variables return input_variables
def check_input_variables(input_variables: list): def is_json_like(var):
if var.startswith("{{") and var.endswith("}}"):
# If it is a double brance variable
# we don't want to validate any of its content
return True
# the above doesn't work on all cases because the json string can be multiline
# or indented which can add \n or spaces at the start or end of the string
# test_case_3 new_var == '\n{{\n "test": "hello",\n "text": "world"\n}}\n'
# what we can do is to remove the \n and spaces from the start and end of the string
# and then check if the string starts with {{ and ends with }}
var = var.strip()
var = var.replace("\n", "")
var = var.replace(" ", "")
# Now it should be a valid json string
return var.startswith("{{") and var.endswith("}}")
def fix_variable(var, invalid_chars, wrong_variables):
if not var:
return var, invalid_chars, wrong_variables
new_var = var
# Handle variables starting with a number
if var[0].isdigit():
invalid_chars.append(var[0])
new_var, invalid_chars, wrong_variables = fix_variable(
var[1:], invalid_chars, wrong_variables
)
# Temporarily replace {{ and }} to avoid treating them as invalid
new_var = new_var.replace("{{", "ᴛᴇᴍᴘᴏᴘᴇɴ").replace("}}", "ᴛᴇᴍᴘᴄʟᴏsᴇ")
# Remove invalid characters
for char in new_var:
if char in INVALID_CHARACTERS:
invalid_chars.append(char)
new_var = new_var.replace(char, "")
if var not in wrong_variables: # Avoid duplicating entries
wrong_variables.append(var)
# Restore {{ and }}
new_var = new_var.replace("ᴛᴇᴍᴘᴏᴘᴇɴ", "{{").replace("ᴛᴇᴍᴘᴄʟᴏsᴇ", "}}")
return new_var, invalid_chars, wrong_variables
def check_variable(var, invalid_chars, wrong_variables, empty_variables):
if any(char in invalid_chars for char in var):
wrong_variables.append(var)
elif var == "":
empty_variables.append(var)
return wrong_variables, empty_variables
def check_for_errors(
input_variables, fixed_variables, wrong_variables, empty_variables
):
if any(var for var in input_variables if var not in fixed_variables):
error_message = (
f"Error: Input variables contain invalid characters or formats. \n"
f"Invalid variables: {', '.join(wrong_variables)}.\n"
f"Empty variables: {', '.join(empty_variables)}. \n"
f"Fixed variables: {', '.join(fixed_variables)}."
)
raise ValueError(error_message)
def check_input_variables(input_variables):
invalid_chars = [] invalid_chars = []
fixed_variables = [] fixed_variables = []
wrong_variables = [] wrong_variables = []
empty_variables = [] empty_variables = []
for variable in input_variables: variables_to_check = []
new_var = variable
# if variable is empty, then we should add that to the wrong variables for var in input_variables:
if not variable: # First, let's check if the variable is a JSON string
empty_variables.append(variable) # because if it is, it won't be considered a variable
# and we don't need to validate it
if is_json_like(var):
continue continue
# if variable starts with a number we should add that to the invalid chars new_var, wrong_variables, empty_variables = fix_variable(
# and wrong variables var, invalid_chars, wrong_variables
if variable[0].isdigit():
invalid_chars.append(variable[0])
new_var = new_var.replace(variable[0], "")
wrong_variables.append(variable)
else:
for char in INVALID_CHARACTERS:
if char in variable:
invalid_chars.append(char)
new_var = new_var.replace(char, "")
wrong_variables.append(variable)
fixed_variables.append(new_var)
# If any of the input_variables is not in the fixed_variables, then it means that
# there are invalid characters in the input_variables
if any(var not in fixed_variables for var in input_variables):
error_message = build_error_message(
input_variables,
invalid_chars,
wrong_variables,
fixed_variables,
empty_variables,
) )
raise ValueError(error_message) wrong_variables, empty_variables = check_variable(
return input_variables var, INVALID_CHARACTERS, wrong_variables, empty_variables
)
fixed_variables.append(new_var)
variables_to_check.append(var)
check_for_errors(
variables_to_check, fixed_variables, wrong_variables, empty_variables
)
def build_error_message(input_variables, invalid_chars, wrong_variables, fixed_variables, empty_variables): return fixed_variables
input_variables_str = ", ".join([f"'{var}'" for var in input_variables])
error_string = f"Invalid input variables: {input_variables_str}. "
if wrong_variables and invalid_chars:
# fix the wrong variables replacing invalid chars and find them in the fixed variables
error_string_vars = "You can fix them by replacing the invalid characters: "
wvars = wrong_variables.copy()
for i, wrong_var in enumerate(wvars):
for char in invalid_chars:
wrong_var = wrong_var.replace(char, "")
if wrong_var in fixed_variables:
error_string_vars += f"'{wrong_variables[i]}' -> '{wrong_var}'"
error_string += error_string_vars
elif empty_variables:
error_string += f" There are {len(empty_variables)} empty variable{'s' if len(empty_variables) > 1 else ''}."
elif len(set(fixed_variables)) != len(fixed_variables):
error_string += "There are duplicate variables."
return error_string

View file

@ -22,10 +22,10 @@ from langflow.services.auth.utils import get_current_active_user
from langflow.services.chat.service import ChatService from langflow.services.chat.service import ChatService
from langflow.services.deps import get_chat_service, get_session, get_session_service from langflow.services.deps import get_chat_service, get_session, get_session_service
from langflow.services.monitor.utils import log_vertex_build from langflow.services.monitor.utils import log_vertex_build
from langflow.services.session.service import SessionService
if TYPE_CHECKING: if TYPE_CHECKING:
from langflow.graph.vertex.types import ChatVertex from langflow.graph.vertex.types import ChatVertex
from langflow.services.session.service import SessionService
router = APIRouter(tags=["Chat"]) router = APIRouter(tags=["Chat"])
@ -49,7 +49,8 @@ async def try_running_celery_task(vertex, user_id):
@router.get("/build/{flow_id}/vertices", response_model=VerticesOrderResponse) @router.get("/build/{flow_id}/vertices", response_model=VerticesOrderResponse)
async def get_vertices( async def get_vertices(
flow_id: str, flow_id: str,
component_id: Optional[str] = None, stop_component_id: Optional[str] = None,
start_component_id: Optional[str] = None,
chat_service: "ChatService" = Depends(get_chat_service), chat_service: "ChatService" = Depends(get_chat_service),
session=Depends(get_session), session=Depends(get_session),
): ):
@ -60,9 +61,9 @@ async def get_vertices(
if cache := chat_service.get_cache(flow_id): if cache := chat_service.get_cache(flow_id):
graph = cache.get("result") graph = cache.get("result")
graph = build_and_cache_graph(flow_id, session, chat_service, graph) graph = build_and_cache_graph(flow_id, session, chat_service, graph)
if component_id: if stop_component_id or start_component_id:
try: try:
vertices = graph.sort_vertices(component_id) vertices = graph.sort_vertices(stop_component_id, start_component_id)
except Exception as exc: except Exception as exc:
logger.error(exc) logger.error(exc)
vertices = graph.sort_vertices() vertices = graph.sort_vertices()
@ -94,6 +95,7 @@ async def build_vertex(
"""Build a vertex instead of the entire graph.""" """Build a vertex instead of the entire graph."""
{"inputs": {"input_value": "some value"}} {"inputs": {"input_value": "some value"}}
start_time = time.perf_counter() start_time = time.perf_counter()
next_vertices_ids = []
try: try:
start_time = time.perf_counter() start_time = time.perf_counter()
cache = chat_service.get_cache(flow_id) cache = chat_service.get_cache(flow_id)
@ -119,6 +121,10 @@ async def build_vertex(
artifacts = vertex.artifacts artifacts = vertex.artifacts
else: else:
raise ValueError(f"No result found for vertex {vertex_id}") raise ValueError(f"No result found for vertex {vertex_id}")
next_vertices_ids = vertex.successors_ids
next_vertices_ids = [
v for v in next_vertices_ids if graph.should_run_vertex(v)
]
result_data_response = ResultDataResponse(**result_dict.model_dump()) result_data_response = ResultDataResponse(**result_dict.model_dump())
@ -155,7 +161,15 @@ async def build_vertex(
graph.reset_inactive_vertices() graph.reset_inactive_vertices()
chat_service.set_cache(flow_id, graph) chat_service.set_cache(flow_id, graph)
# graph.stop_vertex tells us if the user asked
# to stop the build of the graph at a certain vertex
# if it is in next_vertices_ids, we need to remove other
# vertices from next_vertices_ids
if graph.stop_vertex and graph.stop_vertex in next_vertices_ids:
next_vertices_ids = [graph.stop_vertex]
build_response = VertexBuildResponse( build_response = VertexBuildResponse(
next_vertices_ids=next_vertices_ids,
inactive_vertices=inactive_vertices, inactive_vertices=inactive_vertices,
valid=valid, valid=valid,
params=params, params=params,

View file

@ -44,12 +44,15 @@ def get_all(
logger.debug("Building langchain types dict") logger.debug("Building langchain types dict")
try: try:
return get_all_types_dict(settings_service) all_types_dict = get_all_types_dict(settings_service)
return all_types_dict
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc raise HTTPException(status_code=500, detail=str(exc)) from exc
@router.post("/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True) @router.post(
"/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True
)
async def run_flow_with_caching( async def run_flow_with_caching(
session: Annotated[Session, Depends(get_session)], session: Annotated[Session, Depends(get_session)],
flow_id: str, flow_id: str,
@ -67,7 +70,9 @@ async def run_flow_with_caching(
input_values_dict = {} input_values_dict = {}
if session_id: if session_id:
session_data = await session_service.load_session(session_id, flow_id=flow_id) session_data = await session_service.load_session(
session_id, flow_id=flow_id
)
graph, artifacts = session_data if session_data else (None, None) graph, artifacts = session_data if session_data else (None, None)
task_result: Any = None task_result: Any = None
if not graph: if not graph:
@ -85,7 +90,11 @@ async def run_flow_with_caching(
else: else:
# Get the flow that matches the flow_id and belongs to the user # Get the flow that matches the flow_id and belongs to the user
# flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first() # flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first()
flow = session.exec(select(Flow).where(Flow.id == flow_id).where(Flow.user_id == api_key_user.id)).first() flow = session.exec(
select(Flow)
.where(Flow.id == flow_id)
.where(Flow.user_id == api_key_user.id)
).first()
if flow is None: if flow is None:
raise ValueError(f"Flow {flow_id} not found") raise ValueError(f"Flow {flow_id} not found")
@ -108,12 +117,18 @@ async def run_flow_with_caching(
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string') # StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
if "badly formed hexadecimal UUID string" in str(exc): if "badly formed hexadecimal UUID string" in str(exc):
# This means the Flow ID is not a valid UUID which means it can't find the flow # This means the Flow ID is not a valid UUID which means it can't find the flow
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
) from exc
except ValueError as exc: except ValueError as exc:
if f"Flow {flow_id} not found" in str(exc): if f"Flow {flow_id} not found" in str(exc):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
) from exc
else: else:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
) from exc
@router.post( @router.post(
@ -142,7 +157,8 @@ async def process(
""" """
# Raise a depreciation warning # Raise a depreciation warning
logger.warning( logger.warning(
"The /process endpoint is deprecated and will be removed in a future version. " "Please use /run instead." "The /process endpoint is deprecated and will be removed in a future version. "
"Please use /run instead."
) )
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@ -214,12 +230,16 @@ async def custom_component(
built_frontend_node = build_custom_component_template(component, user_id=user.id) built_frontend_node = build_custom_component_template(component, user_id=user.id)
built_frontend_node = update_frontend_node_with_template_values(built_frontend_node, raw_code.frontend_node) built_frontend_node = update_frontend_node_with_template_values(
built_frontend_node, raw_code.frontend_node
)
return built_frontend_node return built_frontend_node
@router.post("/custom_component/reload", status_code=HTTPStatus.OK) @router.post("/custom_component/reload", status_code=HTTPStatus.OK)
async def reload_custom_component(path: str, user: User = Depends(get_current_active_user)): async def reload_custom_component(
path: str, user: User = Depends(get_current_active_user)
):
from langflow.interface.custom.utils import build_custom_component_template from langflow.interface.custom.utils import build_custom_component_template
try: try:
@ -241,6 +261,8 @@ async def custom_component_update(
): ):
component = CustomComponent(code=raw_code.code) component = CustomComponent(code=raw_code.code)
component_node = build_custom_component_template(component, user_id=user.id, update_field=raw_code.field) component_node = build_custom_component_template(
component, user_id=user.id, update_field=raw_code.field
)
# Update the field # Update the field
return component_node return component_node

View file

@ -158,7 +158,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):
@ -214,7 +216,7 @@ class ApiKeyCreateRequest(BaseModel):
class VerticesOrderResponse(BaseModel): class VerticesOrderResponse(BaseModel):
ids: List[List[str]] ids: List[str]
run_id: UUID run_id: UUID
@ -227,9 +229,10 @@ class ResultDataResponse(BaseModel):
class VertexBuildResponse(BaseModel): class VertexBuildResponse(BaseModel):
id: Optional[str] = None id: Optional[str] = None
next_vertices_ids: Optional[List[str]] = None
inactive_vertices: Optional[List[str]] = None inactive_vertices: Optional[List[str]] = None
valid: bool valid: bool
params: Optional[str] params: Optional[Any] = Field(default_factory=dict)
"""JSON string of the params.""" """JSON string of the params."""
data: ResultDataResponse data: ResultDataResponse
"""Mapping of vertex ids to result dict containing the param name and result value.""" """Mapping of vertex ids to result dict containing the param name and result value."""

View file

@ -31,16 +31,12 @@ class ConversationChainComponent(CustomComponent):
chain = ConversationChain(llm=llm) chain = ConversationChain(llm=llm)
else: else:
chain = ConversationChain(llm=llm, memory=memory) chain = ConversationChain(llm=llm, memory=memory)
result = chain.invoke({chain.input_key: input_value}) result = chain.invoke(inputs)
# result is an AIMessage which is a subclass of BaseMessage
# We need to check if it is a string or a BaseMessage
result_str: Text = ""
if hasattr(result, "content") and isinstance(result.content, str): if hasattr(result, "content") and isinstance(result.content, str):
result_str = result.content result = result.content
elif isinstance(result, str): elif isinstance(result, str):
result_str = result result = result
else: else:
# is dict result = result.get("response")
result_str = Text(result.get("response")) self.status = result
self.status = result_str return result
return result_str

View file

@ -1,6 +1,7 @@
from langchain_core.documents import Document from typing import List
from langflow import CustomComponent from langflow import CustomComponent
from langflow.schema import Record
from langflow.utils.constants import LOADERS_INFO from langflow.utils.constants import LOADERS_INFO
@ -10,7 +11,9 @@ class FileLoaderComponent(CustomComponent):
beta = True beta = True
def build_config(self): def build_config(self):
loader_options = ["Automatic"] + [loader_info["name"] for loader_info in LOADERS_INFO] loader_options = ["Automatic"] + [
loader_info["name"] for loader_info in LOADERS_INFO
]
file_types = [] file_types = []
suffixes = [] suffixes = []
@ -74,7 +77,7 @@ class FileLoaderComponent(CustomComponent):
"code": {"show": False}, "code": {"show": False},
} }
def build(self, file_path: str, loader: str) -> Document: def build(self, file_path: str, loader: str) -> List[Record]:
file_type = file_path.split(".")[-1] file_type = file_path.split(".")[-1]
# Map the loader to the correct loader class # Map the loader to the correct loader class
@ -102,7 +105,9 @@ class FileLoaderComponent(CustomComponent):
if isinstance(selected_loader_info, dict): if isinstance(selected_loader_info, dict):
loader_import: str = selected_loader_info["import"] loader_import: str = selected_loader_info["import"]
else: else:
raise ValueError(f"Loader info for {loader} is not a dict\nLoader info:\n{selected_loader_info}") raise ValueError(
f"Loader info for {loader} is not a dict\nLoader info:\n{selected_loader_info}"
)
module_name, class_name = loader_import.rsplit(".", 1) module_name, class_name = loader_import.rsplit(".", 1)
try: try:
@ -110,7 +115,10 @@ class FileLoaderComponent(CustomComponent):
loader_module = __import__(module_name, fromlist=[class_name]) loader_module = __import__(module_name, fromlist=[class_name])
loader_instance = getattr(loader_module, class_name) loader_instance = getattr(loader_module, class_name)
except ImportError as e: except ImportError as e:
raise ValueError(f"Loader {loader} could not be imported\nLoader info:\n{selected_loader_info}") from e raise ValueError(
f"Loader {loader} could not be imported\nLoader info:\n{selected_loader_info}"
) from e
result = loader_instance(file_path=file_path) result = loader_instance(file_path=file_path)
return result.load() docs = result.load()
return self.to_records(docs)

View file

@ -70,11 +70,17 @@ class GatherRecordsComponent(CustomComponent):
glob = "**/*" if recursive else "*" glob = "**/*" if recursive else "*"
paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob) paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob)
file_paths = [Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)] file_paths = [
Text(p)
for p in paths
if p.is_file() and match_types(p) and is_not_hidden(p)
]
return file_paths return file_paths
def parse_file_to_record(self, file_path: str, silent_errors: bool) -> Optional[Record]: def parse_file_to_record(
self, file_path: str, silent_errors: bool
) -> Optional[Record]:
# Use the partition function to load the file # Use the partition function to load the file
from unstructured.partition.auto import partition # type: ignore from unstructured.partition.auto import partition # type: ignore
@ -100,9 +106,14 @@ class GatherRecordsComponent(CustomComponent):
use_multithreading: bool, use_multithreading: bool,
) -> List[Optional[Record]]: ) -> List[Optional[Record]]:
if use_multithreading: if use_multithreading:
records = self.parallel_load_records(file_paths, silent_errors, max_concurrency) records = self.parallel_load_records(
file_paths, silent_errors, max_concurrency
)
else: else:
records = [self.parse_file_to_record(file_path, silent_errors) for file_path in file_paths] records = [
self.parse_file_to_record(file_path, silent_errors)
for file_path in file_paths
]
records = list(filter(None, records)) records = list(filter(None, records))
return records return records
@ -131,13 +142,20 @@ class GatherRecordsComponent(CustomComponent):
if types is None: if types is None:
types = [] types = []
resolved_path = self.resolve_path(path) resolved_path = self.resolve_path(path)
file_paths = self.retrieve_file_paths(resolved_path, types, load_hidden, recursive, depth) file_paths = self.retrieve_file_paths(
resolved_path, types, load_hidden, recursive, depth
)
loaded_records = [] loaded_records = []
if use_multithreading: if use_multithreading:
loaded_records = self.parallel_load_records(file_paths, silent_errors, max_concurrency) loaded_records = self.parallel_load_records(
file_paths, silent_errors, max_concurrency
)
else: else:
loaded_records = [self.parse_file_to_record(file_path, silent_errors) for file_path in file_paths] loaded_records = [
self.parse_file_to_record(file_path, silent_errors)
for file_path in file_paths
]
loaded_records = list(filter(None, loaded_records)) loaded_records = list(filter(None, loaded_records))
self.status = loaded_records self.status = loaded_records
return loaded_records return loaded_records

View file

@ -11,8 +11,8 @@ class ChatOutput(ChatComponent):
def build( def build(
self, self,
sender: Optional[str] = "User", sender: Optional[str] = "Machine",
sender_name: Optional[str] = "User", sender_name: Optional[str] = "AI",
input_value: Optional[str] = None, input_value: Optional[str] = None,
session_id: Optional[str] = None, session_id: Optional[str] = None,
return_record: Optional[bool] = False, return_record: Optional[bool] = False,

View file

@ -1,19 +1,12 @@
from typing import Optional from typing import Optional
from langflow import CustomComponent from langflow.components.io.base.text import TextComponent
from langflow.field_typing import Text from langflow.field_typing import Text
class TextInput(CustomComponent): class TextInput(TextComponent):
display_name = "Text Input" display_name = "Text Input"
description = "Used to pass text input to the next component." description = "Used to pass text input to the next component."
field_config = {
"input_value": {"display_name": "Value", "multiline": True},
}
def build(self, input_value: Optional[str] = "") -> Text: def build(self, input_value: Optional[str] = "") -> Text:
self.status = input_value return super().build(input_value=input_value)
if not input_value:
input_value = ""
return input_value

View file

@ -1,19 +1,16 @@
from typing import Optional from typing import Optional
from langflow import CustomComponent from langflow.components.io.base.text import TextComponent
from langflow.field_typing import Text from langflow.field_typing import Text
class TextOutput(CustomComponent): class TextOutput(TextComponent):
display_name = "Text Output" display_name = "Text Output"
description = "Used to pass text output to the next component." description = "Used to pass text output to the next component."
field_config = { field_config = {
"value": {"display_name": "Value"}, "input_value": {"display_name": "Value"},
} }
def build(self, value: Optional[str] = "") -> Text: def build(self, input_value: Optional[Text] = "") -> Text:
self.status = value return super().build(input_value=input_value)
if not value:
value = ""
return value

View file

@ -45,7 +45,9 @@ class ChatComponent(CustomComponent):
return [] return []
if not session_id or not sender or not sender_name: if not session_id or not sender or not sender_name:
raise ValueError("All of session_id, sender, and sender_name must be provided.") raise ValueError(
"All of session_id, sender, and sender_name must be provided."
)
if isinstance(message, Record): if isinstance(message, Record):
record = message record = message
record.data.update( record.data.update(

View file

@ -0,0 +1,19 @@
from typing import Optional
from langflow import CustomComponent
from langflow.field_typing import Text
class TextComponent(CustomComponent):
display_name = "Text Component"
description = "Used to pass text to the next component."
field_config = {
"input_value": {"display_name": "Value", "multiline": True},
}
def build(self, input_value: Optional[str] = "") -> Text:
self.status = input_value
if not input_value:
input_value = ""
return input_value

View file

@ -23,7 +23,7 @@ class PromptComponent(CustomComponent):
prompt_template = PromptTemplate.from_template(Text(template)) prompt_template = PromptTemplate.from_template(Text(template))
attributes_to_check = ["text", "page_content"] attributes_to_check = ["text", "page_content"]
for key, value in kwargs.items(): for key, value in kwargs.copy().items():
for attribute in attributes_to_check: for attribute in attributes_to_check:
if hasattr(value, attribute): if hasattr(value, attribute):
kwargs[key] = getattr(value, attribute) kwargs[key] = getattr(value, attribute)

View file

@ -0,0 +1,19 @@
from typing import List
from langflow import CustomComponent
from langflow.schema import Record
class ListFlowsComponent(CustomComponent):
display_name = "List Flows"
description = "A component to list all available flows."
def build_config(self):
return {}
def build(
self,
) -> List[Record]:
flows = self.list_flows()
self.status = flows
return flows

View file

@ -1,5 +1,6 @@
from langflow import CustomComponent from langflow import CustomComponent
from langflow.field_typing import Text from langflow.field_typing import Text
from langflow.helpers.record import records_to_text
from langflow.schema import Record from langflow.schema import Record
@ -27,7 +28,6 @@ class RecordsAsTextComponent(CustomComponent):
if isinstance(records, Record): if isinstance(records, Record):
records = [records] records = [records]
formated_records = [template.format(text=record.text, data=record.data, **record.data) for record in records] result_string = records_to_text(template, records)
result_string = "\n".join(formated_records)
self.status = result_string self.status = result_string
return result_string return result_string

View file

@ -0,0 +1,58 @@
from typing import List, Optional
from langflow import CustomComponent
from langflow.field_typing import NestedDict, Text
from langflow.graph.schema import ResultData
from langflow.schema import Record
class RunFlowComponent(CustomComponent):
display_name = "Run Flow"
description = "A component to run a flow."
def get_flow_names(self) -> List[str]:
flow_records = self.list_flows()
return [flow_record.data["name"] for flow_record in flow_records]
def build_config(self):
return {
"input_value": {
"display_name": "Input Value",
"multiline": True,
},
"flow_name": {
"display_name": "Flow Name",
"info": "The name of the flow to run.",
"options": self.get_flow_names,
},
"tweaks": {
"display_name": "Tweaks",
"info": "Tweaks to apply to the flow.",
},
}
def build_records_from_result_data(self, result_data: ResultData) -> Record:
messages = result_data.messages
records = []
for message in messages:
record = Record(text=message.get("text", ""), data={"result": result_data})
records.append(record)
return records
async def build(
self, input_value: Text, flow_name: str, tweaks: NestedDict
) -> Record:
results: List[Optional[ResultData]] = await self.run_flow(
input_value=input_value, flow_name=flow_name, tweaks=tweaks
)
if isinstance(results, list):
records = []
for result in results:
if result:
records.extend(self.build_records_from_result_data(result))
else:
records = self.build_records_from_result_data(results)
self.status = records
return records

View file

@ -0,0 +1,38 @@
from typing import Union
from langflow import CustomComponent
from langflow.field_typing import Text
from langflow.schema import Record
class SharedState(CustomComponent):
display_name = "Shared State"
description = "A component to share state between components."
def build_config(self):
return {
"name": {"display_name": "Name", "info": "The name of the state."},
"record": {"display_name": "Record", "info": "The record to store."},
"append": {
"display_name": "Append",
"info": "If True, the record will be appended to the state.",
},
}
def build(
self, name: str, record: Union[Text, Record], append: bool = False
) -> Record:
if append:
self.append_state(name, record)
else:
self.update_state(name, record)
state = self.get_state(name)
if not isinstance(state, Record):
if isinstance(state, str):
state = Record(text=state)
elif isinstance(state, dict):
state = Record(data=state)
else:
state = Record(text=str(state))
return state

View file

@ -93,7 +93,8 @@ class ChromaSearchComponent(LCVectorStoreComponent):
if chroma_server_host is not None: if chroma_server_host is not None:
chroma_settings = chromadb.config.Settings( chroma_settings = chromadb.config.Settings(
chroma_server_cors_allow_origins=chroma_server_cors_allow_origins or None, chroma_server_cors_allow_origins=chroma_server_cors_allow_origins
or None,
chroma_server_host=chroma_server_host, chroma_server_host=chroma_server_host,
chroma_server_port=chroma_server_port or None, chroma_server_port=chroma_server_port or None,
chroma_server_grpc_port=chroma_server_grpc_port or None, chroma_server_grpc_port=chroma_server_grpc_port or None,

View file

@ -34,7 +34,9 @@ class FAISSSearchComponent(LCVectorStoreComponent):
if not folder_path: if not folder_path:
raise ValueError("Folder path is required to save the FAISS index.") raise ValueError("Folder path is required to save the FAISS index.")
path = self.resolve_path(folder_path) path = self.resolve_path(folder_path)
vector_store = FAISS.load_local(folder_path=Text(path), embeddings=embedding, index_name=index_name) vector_store = FAISS.load_local(
folder_path=Text(path), embeddings=embedding, index_name=index_name
)
if not vector_store: if not vector_store:
raise ValueError("Failed to load the FAISS index.") raise ValueError("Failed to load the FAISS index.")

View file

@ -38,7 +38,9 @@ class SupabaseSearchComponent(LCVectorStoreComponent):
supabase_url: str = "", supabase_url: str = "",
table_name: str = "", table_name: str = "",
) -> List[Record]: ) -> List[Record]:
supabase: Client = create_client(supabase_url, supabase_key=supabase_service_key) supabase: Client = create_client(
supabase_url, supabase_key=supabase_service_key
)
vector_store = SupabaseVectorStore( vector_store = SupabaseVectorStore(
client=supabase, client=supabase,
embedding=embedding, embedding=embedding,

View file

@ -11,7 +11,9 @@ from langflow.schema import Record
class VectaraSearchComponent(VectaraComponent, LCVectorStoreComponent): class VectaraSearchComponent(VectaraComponent, LCVectorStoreComponent):
display_name: str = "Vectara Search" display_name: str = "Vectara Search"
description: str = "Search a Vectara Vector Store for similar documents." description: str = "Search a Vectara Vector Store for similar documents."
documentation = "https://python.langchain.com/docs/integrations/vectorstores/vectara" documentation = (
"https://python.langchain.com/docs/integrations/vectorstores/vectara"
)
beta = True beta = True
icon = "Vectara" icon = "Vectara"

View file

@ -11,7 +11,9 @@ from langflow.schema import Record
class WeaviateSearchVectorStore(WeaviateVectorStoreComponent, LCVectorStoreComponent): class WeaviateSearchVectorStore(WeaviateVectorStoreComponent, LCVectorStoreComponent):
display_name: str = "Weaviate Search" display_name: str = "Weaviate Search"
description: str = "Search a Weaviate Vector Store for similar documents." description: str = "Search a Weaviate Vector Store for similar documents."
documentation = "https://python.langchain.com/docs/integrations/vectorstores/weaviate" documentation = (
"https://python.langchain.com/docs/integrations/vectorstores/weaviate"
)
beta = True beta = True
icon = "Weaviate" icon = "Weaviate"

View file

@ -6,7 +6,8 @@ from langchain_core.vectorstores import VectorStore
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, docs_to_records from langflow.helpers.record import docs_to_records
from langflow.schema import Record
class LCVectorStoreComponent(CustomComponent): class LCVectorStoreComponent(CustomComponent):

View file

@ -15,7 +15,9 @@ class PGVectorSearchComponent(PGVectorComponent, LCVectorStoreComponent):
display_name: str = "PGVector Search" display_name: str = "PGVector Search"
description: str = "Search a PGVector Store for similar documents." description: str = "Search a PGVector Store for similar documents."
documentation = "https://python.langchain.com/docs/integrations/vectorstores/pgvector" documentation = (
"https://python.langchain.com/docs/integrations/vectorstores/pgvector"
)
def build_config(self): def build_config(self):
""" """

View file

@ -1,19 +1,20 @@
from typing import TYPE_CHECKING, Any, List, Optional from typing import TYPE_CHECKING, Any, List, Optional
from loguru import logger
from pydantic import BaseModel, Field
from langflow.graph.edge.utils import build_clean_params from langflow.graph.edge.utils import build_clean_params
from langflow.graph.schema import INPUT_FIELD_NAME from langflow.graph.schema import INPUT_FIELD_NAME
from langflow.services.deps import get_monitor_service from langflow.services.deps import get_monitor_service
from langflow.services.monitor.utils import log_message from langflow.services.monitor.utils import log_message
from loguru import logger
from pydantic import BaseModel, Field
if TYPE_CHECKING: if TYPE_CHECKING:
from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.base import Vertex
class SourceHandle(BaseModel): class SourceHandle(BaseModel):
baseClasses: List[str] = Field(..., description="List of base classes for the source handle.") baseClasses: List[str] = Field(
..., description="List of base classes for the source handle."
)
dataType: str = Field(..., description="Data type for the source handle.") dataType: str = Field(..., description="Data type for the source handle.")
id: str = Field(..., description="Unique identifier for the source handle.") id: str = Field(..., description="Unique identifier for the source handle.")
@ -21,7 +22,9 @@ class SourceHandle(BaseModel):
class TargetHandle(BaseModel): class TargetHandle(BaseModel):
fieldName: str = Field(..., description="Field name for the target handle.") fieldName: str = Field(..., description="Field name for the target handle.")
id: str = Field(..., description="Unique identifier for the target handle.") id: str = Field(..., description="Unique identifier for the target handle.")
inputTypes: Optional[List[str]] = Field(None, description="List of input types for the target handle.") inputTypes: Optional[List[str]] = Field(
None, description="List of input types for the target handle."
)
type: str = Field(..., description="Type of the target handle.") type: str = Field(..., description="Type of the target handle.")
@ -50,16 +53,24 @@ class Edge:
def validate_handles(self, source, target) -> None: def validate_handles(self, source, target) -> None:
if self.target_handle.inputTypes is None: if self.target_handle.inputTypes is None:
self.valid_handles = self.target_handle.type in self.source_handle.baseClasses self.valid_handles = (
self.target_handle.type in self.source_handle.baseClasses
)
else: else:
self.valid_handles = ( self.valid_handles = (
any(baseClass in self.target_handle.inputTypes for baseClass in self.source_handle.baseClasses) any(
baseClass in self.target_handle.inputTypes
for baseClass in self.source_handle.baseClasses
)
or self.target_handle.type in self.source_handle.baseClasses or self.target_handle.type in self.source_handle.baseClasses
) )
if not self.valid_handles: if not self.valid_handles:
logger.debug(self.source_handle) logger.debug(self.source_handle)
logger.debug(self.target_handle) logger.debug(self.target_handle)
raise ValueError(f"Edge between {source.vertex_type} and {target.vertex_type} " f"has invalid handles") raise ValueError(
f"Edge between {source.vertex_type} and {target.vertex_type} "
f"has invalid handles"
)
def __setstate__(self, state): def __setstate__(self, state):
self.source_id = state["source_id"] self.source_id = state["source_id"]
@ -76,7 +87,11 @@ class Edge:
# Both lists contain strings and sometimes a string contains the value we are # Both lists contain strings and sometimes a string contains the value we are
# looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"] # looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"]
# so we need to check if any of the strings in source_types is in target_reqs # so we need to check if any of the strings in source_types is in target_reqs
self.valid = any(output in target_req for output in self.source_types for target_req in self.target_reqs) self.valid = any(
output in target_req
for output in self.source_types
for target_req in self.target_reqs
)
# Get what type of input the target node is expecting # Get what type of input the target node is expecting
self.matched_type = next( self.matched_type = next(
@ -87,7 +102,10 @@ class Edge:
if no_matched_type: if no_matched_type:
logger.debug(self.source_types) logger.debug(self.source_types)
logger.debug(self.target_reqs) logger.debug(self.target_reqs)
raise ValueError(f"Edge between {source.vertex_type} and {target.vertex_type} " f"has no matched type") raise ValueError(
f"Edge between {source.vertex_type} and {target.vertex_type} "
f"has no matched type"
)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -98,8 +116,12 @@ class Edge:
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(self.__repr__()) return hash(self.__repr__())
def __eq__(self, __value: object) -> bool: def __eq__(self, __o: object) -> bool:
return self.__repr__() == __value.__repr__() if isinstance(__value, Edge) else False # Create a better way to compare edges
return (
self._source_handle == __o._source_handle
and self._target_handle == __o._target_handle
)
class ContractEdge(Edge): class ContractEdge(Edge):
@ -156,7 +178,9 @@ class ContractEdge(Edge):
return f"{self.source_id} -[{self.target_param}]-> {self.target_id}" return f"{self.source_id} -[{self.target_param}]-> {self.target_id}"
def log_transaction(edge: ContractEdge, source: "Vertex", target: "Vertex", status, error=None): def log_transaction(
edge: ContractEdge, source: "Vertex", target: "Vertex", status, error=None
):
try: try:
monitor_service = get_monitor_service() monitor_service = get_monitor_service()
clean_params = build_clean_params(target) clean_params = build_clean_params(target)

View file

@ -1,5 +1,6 @@
import asyncio import asyncio
from collections import defaultdict, deque from collections import defaultdict, deque
from itertools import chain
from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Type, Union from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Type, Union
from langchain.chains.base import Chain from langchain.chains.base import Chain
@ -54,6 +55,10 @@ class Graph:
self._vertices = self._graph_data["nodes"] self._vertices = self._graph_data["nodes"]
self._edges = self._graph_data["edges"] self._edges = self._graph_data["edges"]
self.vertices_layers = []
self.vertices_to_run = set()
self.stop_vertex = None
self.inactive_vertices: set = set() self.inactive_vertices: set = set()
self.edges: List[ContractEdge] = [] self.edges: List[ContractEdge] = []
self.vertices: List[Vertex] = [] self.vertices: List[Vertex] = []
@ -104,7 +109,12 @@ class Graph:
vertex = self.get_vertex(vertex_id) vertex = self.get_vertex(vertex_id)
if vertex is None: if vertex is None:
raise ValueError(f"Vertex {vertex_id} not found") raise ValueError(f"Vertex {vertex_id} not found")
if not stream and hasattr(vertex, "consume_async_generator"):
if (
not vertex.result
and not stream
and hasattr(vertex, "consume_async_generator")
):
await vertex.consume_async_generator() await vertex.consume_async_generator()
outputs.append(vertex.result) outputs.append(vertex.result)
return outputs return outputs
@ -126,6 +136,14 @@ class Graph:
outputs.extend(run_outputs) outputs.extend(run_outputs)
return outputs return outputs
# vertices_layers is a list of lists ordered by the order the vertices
# should be built.
# We need to create a new method that will take the vertices_layers
# and return the next vertex to be built.
def next_vertex_to_build(self):
"""Returns the next vertex to be built."""
yield from chain.from_iterable(self.vertices_layers)
@property @property
def metadata(self): def metadata(self):
return { return {
@ -236,7 +254,7 @@ class Graph:
self.edges = new_edges self.edges = new_edges
def vertex_data_is_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool: def vertex_data_is_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool:
data_is_equivalent = vertex.__repr__() == other_vertex.__repr__() data_is_equivalent = vertex == other_vertex
if not data_is_equivalent: if not data_is_equivalent:
return False return False
return self.vertex_edges_are_identical(vertex, other_vertex) return self.vertex_edges_are_identical(vertex, other_vertex)
@ -262,40 +280,64 @@ class Graph:
# Find vertices that are in self but not in other (removed vertices) # Find vertices that are in self but not in other (removed vertices)
removed_vertex_ids = existing_vertex_ids - other_vertex_ids removed_vertex_ids = existing_vertex_ids - other_vertex_ids
# Update existing vertices that have changed # Remove vertices that are not in the other graph
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 not self.vertex_data_is_identical(self_vertex, other_vertex):
self_vertex._data = other_vertex._data
self_vertex._parse_data()
# Now we update the edges of the vertex
self.update_edges_from_vertex(self_vertex, other_vertex)
self_vertex.params = {}
self_vertex._build_params()
self_vertex.graph = self
# If the vertex is pinned, we don't want
# to reset the results nor the _built attribute
if not self_vertex.pinned:
self_vertex._built = False
self_vertex.result = None
self_vertex.artifacts = {}
self_vertex.set_top_level(self.top_level_vertices)
self.reset_all_edges_of_vertex(self_vertex)
# Remove vertices
for vertex_id in removed_vertex_ids: for vertex_id in removed_vertex_ids:
self.remove_vertex(vertex_id) self.remove_vertex(vertex_id)
# The order here matters because adding the vertex is required
# if any of them have edges that point to any of the new vertices
# By adding them first, them adding the edges we ensure that the
# edges have valid vertices to point to
# Add new vertices # Add new vertices
for vertex_id in new_vertex_ids: for vertex_id in new_vertex_ids:
new_vertex = other.get_vertex(vertex_id) new_vertex = other.get_vertex(vertex_id)
self._add_vertex(new_vertex) self._add_vertex(new_vertex)
# Now update the edges
for vertex_id in new_vertex_ids:
new_vertex = other.get_vertex(vertex_id)
self._update_edges(new_vertex)
# Graph is set at the end because the edges come from the graph
# and the other graph is where the new edges and vertices come from
new_vertex.graph = self
# 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 the vertices are not identical, update the vertex
if not self.vertex_data_is_identical(self_vertex, other_vertex):
self.update_vertex_from_another(self_vertex, other_vertex)
self.build_graph_maps() self.build_graph_maps()
self.increment_update_count() self.increment_update_count()
return self return self
def update_vertex_from_another(self, vertex: Vertex, other_vertex: Vertex) -> None:
"""
Updates a vertex from another vertex.
Args:
vertex (Vertex): The vertex to be updated.
other_vertex (Vertex): The vertex to update from.
"""
vertex._data = other_vertex._data
vertex._parse_data()
# Now we update the edges of the vertex
self.update_edges_from_vertex(vertex, other_vertex)
vertex.params = {}
vertex._build_params()
vertex.graph = self
# If the vertex is pinned, we don't want
# to reset the results nor the _built attribute
if not vertex.pinned:
vertex._built = False
vertex.result = None
vertex.artifacts = {}
vertex.set_top_level(self.top_level_vertices)
self.reset_all_edges_of_vertex(vertex)
def reset_all_edges_of_vertex(self, vertex: Vertex) -> None: def reset_all_edges_of_vertex(self, vertex: Vertex) -> None:
"""Resets all the edges of a vertex.""" """Resets all the edges of a vertex."""
for edge in vertex.edges: for edge in vertex.edges:
@ -306,12 +348,24 @@ class Graph:
_vertex._build_params() _vertex._build_params()
def _add_vertex(self, vertex: Vertex) -> None: def _add_vertex(self, vertex: Vertex) -> None:
"""Adds a new vertex to the graph.""" """Adds a vertex to the graph."""
self.vertices.append(vertex) self.vertices.append(vertex)
self.vertex_map[vertex.id] = vertex self.vertex_map[vertex.id] = vertex
def add_vertex(self, vertex: Vertex) -> None:
"""Adds a new vertex to the graph."""
self._add_vertex(vertex)
self._update_edges(vertex)
def _update_edges(self, vertex: Vertex) -> None:
"""Updates the edges of a vertex."""
# Vertex has edges, so we need to update the edges # Vertex has edges, so we need to update the edges
for edge in vertex.edges: for edge in vertex.edges:
if edge.source_id in self.vertex_map and edge.target_id in self.vertex_map: if (
edge not in self.edges
and edge.source_id in self.vertex_map
and edge.target_id in self.vertex_map
):
self.edges.append(edge) self.edges.append(edge)
def _build_graph(self) -> None: def _build_graph(self) -> None:
@ -409,13 +463,19 @@ class Graph:
async def process(self) -> "Graph": async def process(self) -> "Graph":
"""Processes the graph with vertices in each layer run in parallel.""" """Processes the graph with vertices in each layer run in parallel."""
vertices_layers = self.sorted_vertices_layers vertices_layers = self.sorted_vertices_layers
vertex_task_run_count = {}
for layer_index, layer in enumerate(vertices_layers): for layer_index, layer in enumerate(vertices_layers):
tasks = [] tasks = []
for vertex_id in layer: for vertex_id in layer:
vertex = self.get_vertex(vertex_id) vertex = self.get_vertex(vertex_id)
task = asyncio.create_task(vertex.build(), name=f"layer-{layer_index}-vertex-{vertex_id}") task = asyncio.create_task(
vertex.build(),
name=f"{vertex.display_name} Run {vertex_task_run_count.get(vertex_id, 0)}",
)
tasks.append(task) tasks.append(task)
vertex_task_run_count[vertex_id] = (
vertex_task_run_count.get(vertex_id, 0) + 1
)
logger.debug(f"Running layer {layer_index} with {len(tasks)} tasks") logger.debug(f"Running layer {layer_index} with {len(tasks)} tasks")
await self._execute_tasks(tasks) await self._execute_tasks(tasks)
logger.debug("Graph processing complete") logger.debug("Graph processing complete")
@ -434,6 +494,10 @@ class Graph:
# coroutine has not attribute get_name # coroutine has not attribute get_name
task_name = tasks[i].get_name() task_name = tasks[i].get_name()
logger.error(f"Task {task_name} failed with exception: {e}") logger.error(f"Task {task_name} failed with exception: {e}")
# Cancel all remaining tasks
for t in tasks[i:]:
t.cancel()
raise e
return results return results
def topological_sort(self) -> List[Vertex]: def topological_sort(self) -> List[Vertex]:
@ -573,22 +637,54 @@ class Graph:
edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges]) edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges])
return f"Graph:\nNodes: {vertex_ids}\nConnections:\n{edges_repr}" return f"Graph:\nNodes: {vertex_ids}\nConnections:\n{edges_repr}"
def sort_up_to_vertex(self, vertex_id: str) -> List[Vertex]: def sort_up_to_vertex(self, vertex_id: str, is_start: bool = False) -> List[Vertex]:
"""Cuts the graph up to a given vertex and sorts the resulting subgraph.""" """Cuts the graph up to a given vertex and sorts the resulting subgraph."""
# Initial setup # Initial setup
visited = set() # To keep track of visited vertices visited = set() # To keep track of visited vertices
excluded = set() # To keep track of vertices that should be excluded
stack = [vertex_id] # Use a list as a stack for DFS stack = [vertex_id] # Use a list as a stack for DFS
def get_successors(vertex, recursive=True):
# Recursively get the successors of the current vertex
successors = vertex.successors
if not successors:
return []
successors_result = []
for successor in successors:
# Just return a list of successors
if recursive:
next_successors = get_successors(successor)
successors_result.extend(next_successors)
successors_result.append(successor)
return successors_result
# DFS to collect all vertices that can reach the specified vertex # DFS to collect all vertices that can reach the specified vertex
while stack: while stack:
current_id = stack.pop() current_id = stack.pop()
if current_id not in visited: if current_id not in visited and current_id not in excluded:
visited.add(current_id) visited.add(current_id)
current_vertex = self.get_vertex(current_id) current_vertex = self.get_vertex(current_id)
# Assuming get_predecessors is a method that returns all vertices with edges to current_vertex # Assuming get_predecessors is a method that returns all vertices with edges to current_vertex
for predecessor in current_vertex.predecessors: for predecessor in current_vertex.predecessors:
stack.append(predecessor.id) stack.append(predecessor.id)
if current_id == vertex_id:
# We should add to visited all the vertices that are successors of the current vertex
# and their successors and so on
# if the vertex is a start, it means we are starting from the beginning
# and getting successors
for successor in current_vertex.successors:
if is_start:
stack.append(successor.id)
else:
excluded.add(successor.id)
all_successors = get_successors(successor)
for successor in all_successors:
if is_start:
stack.append(successor.id)
else:
excluded.add(successor.id)
# Filter the original graph's vertices and edges to keep only those in `visited` # Filter the original graph's vertices and edges to keep only those in `visited`
vertices_to_keep = [self.get_vertex(vid) for vid in visited] vertices_to_keep = [self.get_vertex(vid) for vid in visited]
@ -679,19 +775,41 @@ class Graph:
return vertices_layers return vertices_layers
def sort_vertices(self, component_id: Optional[str] = None) -> List[List[str]]: def sort_vertices(
self,
stop_component_id: Optional[str] = None,
start_component_id: Optional[str] = None,
) -> List[str]:
"""Sorts the vertices in the graph.""" """Sorts the vertices in the graph."""
self.mark_all_vertices("ACTIVE") self.mark_all_vertices("ACTIVE")
if component_id: if stop_component_id:
vertices = self.sort_up_to_vertex(component_id) self.stop_vertex = stop_component_id
vertices = self.sort_up_to_vertex(stop_component_id)
elif start_component_id:
vertices = self.sort_up_to_vertex(start_component_id, is_start=True)
else: else:
vertices = self.vertices vertices = self.vertices
vertices_layers = self.layered_topological_sort(vertices) vertices_layers = self.layered_topological_sort(vertices)
vertices_layers = self.sort_by_avg_build_time(vertices_layers) vertices_layers = self.sort_by_avg_build_time(vertices_layers)
vertices_layers = self.sort_chat_inputs_first(vertices_layers) # vertices_layers = self.sort_chat_inputs_first(vertices_layers)
self.increment_run_count() self.increment_run_count()
self._sorted_vertices_layers = vertices_layers self._sorted_vertices_layers = vertices_layers
return vertices_layers first_layer = vertices_layers[0]
# save the only the rest
self.vertices_layers = vertices_layers[1:]
self.vertices_to_run = {
vertex for vertex in chain.from_iterable(vertices_layers)
}
# Return just the first layer
return first_layer
def should_run_vertex(self, vertex_id: str) -> bool:
"""Returns whether a component should be run."""
should_run = vertex_id in self.vertices_to_run
if should_run:
self.vertices_to_run.remove(vertex_id)
return should_run
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.""" """Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first."""

View file

@ -47,7 +47,10 @@ class VertexTypesDict(LazyLoadDictBase):
**{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()}, **{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()},
**{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()}, **{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()},
**{t: types.OutputParserVertex for t in output_parser_creator.to_list()}, **{t: types.OutputParserVertex for t in output_parser_creator.to_list()},
**{t: types.CustomComponentVertex for t in custom_component_creator.to_list()}, **{
t: types.CustomComponentVertex
for t in custom_component_creator.to_list()
},
**{t: types.RetrieverVertex for t in retriever_creator.to_list()}, **{t: types.RetrieverVertex for t in retriever_creator.to_list()},
**{t: types.ChatVertex for t in CHAT_COMPONENTS}, **{t: types.ChatVertex for t in CHAT_COMPONENTS},
**{t: types.RoutingVertex for t in ROUTING_COMPONENTS}, **{t: types.RoutingVertex for t in ROUTING_COMPONENTS},

View file

@ -4,12 +4,13 @@ from typing import Any, Optional
from pydantic import BaseModel, Field, field_serializer from pydantic import BaseModel, Field, field_serializer
from langflow.graph.utils import serialize_field from langflow.graph.utils import serialize_field
from langflow.utils.schemas import ContainsEnumMeta from langflow.utils.schemas import ChatOutputResponse, ContainsEnumMeta
class ResultData(BaseModel): class ResultData(BaseModel):
results: Optional[Any] = Field(default_factory=dict) results: Optional[Any] = Field(default_factory=dict)
artifacts: Optional[Any] = Field(default_factory=dict) artifacts: Optional[Any] = Field(default_factory=dict)
messages: Optional[list[ChatOutputResponse]] = Field(default_factory=list)
timedelta: Optional[float] = None timedelta: Optional[float] = None
duration: Optional[str] = None duration: Optional[str] = None

View file

@ -2,7 +2,17 @@ import ast
import inspect import inspect
import types import types
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Dict, List, Optional from typing import (
TYPE_CHECKING,
Any,
AsyncIterator,
Callable,
Coroutine,
Dict,
Iterator,
List,
Optional,
)
from loguru import logger from loguru import logger
@ -18,6 +28,7 @@ from langflow.interface.initialize import loading
from langflow.interface.listing import lazy_load_dict from langflow.interface.listing import lazy_load_dict
from langflow.services.deps import get_storage_service from langflow.services.deps import get_storage_service
from langflow.utils.constants import DIRECT_TYPES from langflow.utils.constants import DIRECT_TYPES
from langflow.utils.schemas import ChatOutputResponse
from langflow.utils.util import sync_to_async from langflow.utils.util import sync_to_async
if TYPE_CHECKING: if TYPE_CHECKING:
@ -120,6 +131,8 @@ class Vertex:
if not isinstance(result, (dict, str)) and hasattr(result, "content"): if not isinstance(result, (dict, str)) and hasattr(result, "content"):
return result.content return result.content
return result return result
if isinstance(self._built_object, str):
self._built_result = self._built_object
if isinstance(self._built_result, UnbuiltResult): if isinstance(self._built_result, UnbuiltResult):
return {} return {}
@ -140,6 +153,10 @@ class Vertex:
def successors(self) -> List["Vertex"]: def successors(self) -> List["Vertex"]:
return self.graph.get_successors(self) return self.graph.get_successors(self)
@property
def successors_ids(self) -> List[str]:
return self.graph.successor_map.get(self.id, [])
def __getstate__(self): def __getstate__(self):
return { return {
"_data": self._data, "_data": self._data,
@ -360,15 +377,43 @@ class Vertex:
self._built = True self._built = True
def extract_messages_from_artifacts(self, artifacts: Dict[str, Any]) -> List[str]:
"""
Extracts messages from the artifacts.
Args:
artifacts (Dict[str, Any]): The artifacts to extract messages from.
Returns:
List[str]: The extracted messages.
"""
messages = []
for key, artifact in artifacts.items():
if not isinstance(artifact, dict):
continue
if "message" in artifact:
chat_output_response = ChatOutputResponse(
message=artifact["message"],
sender=artifact.get("sender"),
sender_name=artifact.get("sender_name"),
session_id=artifact.get("session_id"),
component_id=self.id,
)
messages.append(chat_output_response.model_dump(exclude_none=True))
return messages
def _finalize_build(self): def _finalize_build(self):
result_dict = self.get_built_result() result_dict = self.get_built_result()
# We need to set the artifacts to pass information # We need to set the artifacts to pass information
# to the frontend # to the frontend
self.set_artifacts() self.set_artifacts()
artifacts = self.artifacts artifacts = self.artifacts
messages = self.extract_messages_from_artifacts(artifacts)
result_dict = ResultData( result_dict = ResultData(
results=result_dict, results=result_dict,
artifacts=artifacts, artifacts=artifacts,
messages=messages,
) )
self.set_result(result_dict) self.set_result(result_dict)
@ -463,12 +508,21 @@ class Vertex:
self.params[key] = [] self.params[key] = []
for node in nodes: for node in nodes:
built = await node.get_result(requester=self, user_id=user_id) built = await node.get_result(requester=self, user_id=user_id)
# Weird check to see if the params[key] is a list
# because sometimes it is a Record and breaks the code
if not isinstance(self.params[key], list):
self.params[key] = [self.params[key]]
if isinstance(built, list): if isinstance(built, list):
if key not in self.params:
self.params[key] = []
self.params[key].extend(built) self.params[key].extend(built)
else: else:
self.params[key].append(built) try:
self.params[key].append(built)
except AttributeError as e:
logger.exception(e)
raise ValueError(
f"Error building node {self.display_name}: {str(e)}"
) from e
def _handle_func(self, key, result): def _handle_func(self, key, result):
""" """
@ -536,6 +590,11 @@ class Vertex:
message += " Make sure your build method returns a component." message += " Make sure your build method returns a component."
logger.warning(message) logger.warning(message)
elif isinstance(self._built_object, (Iterator, AsyncIterator)):
if self.display_name in ["Text Output"]:
raise ValueError(
f"You are trying to stream to a {self.display_name}. Try using a Chat Output instead."
)
def _reset(self, params_update: Optional[Dict[str, Any]] = None): def _reset(self, params_update: Optional[Dict[str, Any]] = None):
self._built = False self._built = False
@ -545,6 +604,9 @@ class Vertex:
self.steps_ran = [] self.steps_ran = []
self._build_params() self._build_params()
def _is_chat_input(self):
return False
def build_inactive(self): def build_inactive(self):
# Just set the results to None # Just set the results to None
self._built = True self._built = True
@ -567,7 +629,7 @@ class Vertex:
return self.get_requester_result(requester) return self.get_requester_result(requester)
self._reset() self._reset()
if self.is_input and inputs is not None: if self._is_chat_input() and inputs is not None:
self.update_raw_params(inputs) self.update_raw_params(inputs)
# Run steps # Run steps
@ -603,7 +665,15 @@ class Vertex:
def __eq__(self, __o: object) -> bool: def __eq__(self, __o: object) -> bool:
try: try:
return self.id == __o.id if isinstance(__o, Vertex) else False if not isinstance(__o, Vertex):
return False
# We should create a more robust comparison
# for the Vertex class
ids_are_equal = self.id == __o.id
# self._data is a dict and we need to compare them
# to check if they are equal
data_are_equal = self.data == __o.data
return ids_are_equal and data_are_equal
except AttributeError: except AttributeError:
return False return False

View file

@ -1,12 +1,13 @@
import ast import ast
import json import json
from typing import AsyncIterator, Callable, Dict, Iterator, List, Optional, Union from typing import (AsyncIterator, Callable, Dict, Iterator, List, Optional,
Union)
import yaml import yaml
from langchain_core.messages import AIMessage from langchain_core.messages import AIMessage
from loguru import logger from loguru import logger
from langflow.graph.schema import INPUT_FIELD_NAME from langflow.graph.schema import INPUT_FIELD_NAME, InterfaceComponentTypes
from langflow.graph.utils import UnbuiltObject, flatten_list, serialize_field from langflow.graph.utils import UnbuiltObject, flatten_list, serialize_field
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
@ -123,12 +124,14 @@ 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(record.text)
) for record in self._built_object
return f"""{self.display_name}({len(self._built_object)} documents) if hasattr(record, "text")
\nAvg. Document Length (characters): {int(avg_length)} ) / len(self._built_object)
Documents: {self._built_object[:3]}...""" return f"""{self.display_name}({len(self._built_object)} records)
\nAvg. Record Length (characters): {int(avg_length)}
Records: {self._built_object[:3]}..."""
return f"{self.vertex_type}()" return f"{self.vertex_type}()"
@ -198,7 +201,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]}..."""
@ -245,18 +250,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"]
@ -266,14 +280,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()
@ -285,7 +305,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
@ -293,7 +315,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)
@ -430,18 +456,30 @@ class ChatVertex(StatelessVertex):
async for _ in self.stream(): async for _ in self.stream():
pass pass
def _is_chat_input(self):
return self.vertex_type == InterfaceComponentTypes.ChatInput and self.is_input
class RoutingVertex(StatelessVertex): class RoutingVertex(StatelessVertex):
def __init__(self, data: Dict, graph): def __init__(self, data: Dict, graph):
super().__init__(data, graph=graph, base_type="custom_components") super().__init__(data, graph=graph, base_type="custom_components")
self.use_result = True self.use_result = True
self.steps = [self._build, self._run] self.steps = [self._build]
def _built_object_repr(self): def _built_object_repr(self):
if self.artifacts and "repr" in self.artifacts: if self.artifacts and "repr" in self.artifacts:
return self.artifacts["repr"] or super()._built_object_repr() return self.artifacts["repr"] or super()._built_object_repr()
return super()._built_object_repr() return super()._built_object_repr()
@property
def successors_ids(self):
if isinstance(self._built_object, bool):
ids = super().successors_ids
if self._built_object:
return ids
return []
raise ValueError("RoutingVertex should return a boolean value.")
def _run(self, *args, **kwargs): def _run(self, *args, **kwargs):
if self._built_object: if self._built_object:
condition = self._built_object.get("condition") condition = self._built_object.get("condition")

View file

@ -0,0 +1,3 @@
from .record import docs_to_records, records_to_text
__all__ = ["docs_to_records", "records_to_text"]

View file

@ -0,0 +1,37 @@
from langchain_core.documents import Document
from langflow.schema import Record
def docs_to_records(documents: list[Document]) -> list[Record]:
"""
Converts a list of Documents to a list of Records.
Args:
documents (list[Document]): The list of Documents to convert.
Returns:
list[Record]: The converted list of Records.
"""
return [Record.from_document(document) for document in documents]
def records_to_text(template: str, records: list[Record]) -> list[str]:
"""
Converts a list of Records to a list of texts.
Args:
records (list[Record]): The list of Records to convert.
Returns:
list[str]: The converted list of texts.
"""
if isinstance(records, Record):
records = [records]
# Check if there are any format strings in the template
formated_records = [
template.format(text=record.text, data=record.data, **record.data)
for record in records
]
return "\n".join(formated_records)

View file

@ -21,7 +21,9 @@ class ComponentFunctionEntrypointNameNullError(HTTPException):
class Component: class Component:
ERROR_CODE_NULL: ClassVar[str] = "Python code must be provided." ERROR_CODE_NULL: ClassVar[str] = "Python code must be provided."
ERROR_FUNCTION_ENTRYPOINT_NAME_NULL: ClassVar[str] = "The name of the entrypoint function must be provided." ERROR_FUNCTION_ENTRYPOINT_NAME_NULL: ClassVar[str] = (
"The name of the entrypoint function must be provided."
)
code: Optional[str] = None code: Optional[str] = None
_function_entrypoint_name: str = "build" _function_entrypoint_name: str = "build"
@ -39,7 +41,8 @@ class Component:
def __setattr__(self, key, value): def __setattr__(self, key, value):
if key == "_user_id" and hasattr(self, "_user_id"): if key == "_user_id" and hasattr(self, "_user_id"):
warnings.warn("user_id is immutable and cannot be changed.") warnings.warn("user_id is immutable and cannot be changed.")
super().__setattr__(key, value) else:
super().__setattr__(key, value)
@cachedmethod(cache=operator.attrgetter("cache")) @cachedmethod(cache=operator.attrgetter("cache"))
def get_code_tree(self, code: str): def get_code_tree(self, code: str):

View file

@ -16,6 +16,7 @@ import yaml
from cachetools import TTLCache, cachedmethod from cachetools import TTLCache, cachedmethod
from fastapi import HTTPException from fastapi import HTTPException
from langchain_core.documents import Document from langchain_core.documents import Document
from pydantic import BaseModel
from sqlmodel import select from sqlmodel import select
from langflow.interface.custom.code_parser.utils import ( from langflow.interface.custom.code_parser.utils import (
@ -73,6 +74,7 @@ class CustomComponent(Component):
user_id: Optional[Union[UUID, str]] = None user_id: Optional[Union[UUID, str]] = None
status: Optional[Any] = None status: Optional[Any] = None
"""The status of the component. This is displayed on the frontend. Defaults to None.""" """The status of the component. This is displayed on the frontend. Defaults to None."""
_flows_records: Optional[List[Record]] = None
_tree: Optional[dict] = None _tree: Optional[dict] = None
@ -110,7 +112,7 @@ class CustomComponent(Component):
return yaml.dump(self.repr_value) return yaml.dump(self.repr_value)
if isinstance(self.repr_value, str): if isinstance(self.repr_value, str):
return self.repr_value return self.repr_value
return str(self.repr_value) return self.repr_value
def build_config(self): def build_config(self):
return self.field_config return self.field_config
@ -119,34 +121,57 @@ class CustomComponent(Component):
def tree(self): def tree(self):
return self.get_code_tree(self.code or "") return self.get_code_tree(self.code or "")
def to_records(self, data: Any, text_key: str = "text", data_key: str = "data") -> List[Record]: def to_records(
self, data: Any, text_key: str = "text", data_key: str = "data"
) -> List[Record]:
""" """
Convert data into a list of records. Converts input data into a list of Record objects.
Args: Args:
data (Any): The input data to be converted. data (Any): The input data to be converted. It can be a single item or a sequence of items.
text_key (str, optional): The key to extract the text from a dictionary item. Defaults to "text". If the input data is a Langchain Document, text_key and data_key are ignored.
data_key (str, optional): The key to extract the data from a dictionary item. Defaults to "data".
text_key (str, optional): The key to access the text value in each item. Defaults to "text".
data_key (str, optional): The key to access the data value in each item. Defaults to "data".
Returns: Returns:
List[dict]: A list of records, where each record is a dictionary with 'text' and 'data' keys. List[Record]: A list of Record objects.
Raises:
ValueError: If the input data is not of a valid type or if the specified keys are not found in the data.
""" """
records = [] records = []
if not isinstance(data, Sequence): if not isinstance(data, Sequence):
data = [data] data = [data]
for item in data: for item in data:
if isinstance(item, str): if isinstance(item, Document):
records.append(Record(text=item)) item = {"text": item.page_content, "data": item.metadata}
elif isinstance(item, BaseModel):
model_dump = item.model_dump()
if text_key not in model_dump:
raise ValueError(f"Key '{text_key}' not found in BaseModel item.")
if data_key not in model_dump:
raise ValueError(f"Key '{data_key}' not found in BaseModel item.")
item = {"text": model_dump[text_key], "data": model_dump[data_key]}
elif isinstance(item, str):
item = {"text": item, "data": {}}
elif isinstance(item, dict): elif isinstance(item, dict):
records.append(Record(text=item.get(text_key), data=item.get(data_key))) if text_key not in item:
elif isinstance(item, Document): raise ValueError(f"Key '{text_key}' not found in dictionary item.")
records.append(Record(text=item.page_content, data=item.metadata)) if data_key not in item:
raise ValueError(f"Key '{data_key}' not found in dictionary item.")
item = {"text": item[text_key], "data": item[data_key]}
else: else:
raise ValueError(f"Invalid data type: {type(item)}") raise ValueError(f"Invalid data type: {type(item)}")
records.append(Record(**item))
return records return records
def create_references_from_records(self, records: List[Record], include_data: bool = False) -> str: def create_references_from_records(
self, records: List[Record], include_data: bool = False
) -> str:
""" """
Create references from a list of records. Create references from a list of records.
@ -181,7 +206,8 @@ class CustomComponent(Component):
detail={ detail={
"error": "Type hint Error", "error": "Type hint Error",
"traceback": ( "traceback": (
"Prompt type is not supported in the build method." " Try using PromptTemplate instead." "Prompt type is not supported in the build method."
" Try using PromptTemplate instead."
), ),
}, },
) )
@ -195,14 +221,20 @@ class CustomComponent(Component):
if not self.code: if not self.code:
return {} return {}
component_classes = [cls for cls in self.tree["classes"] if self.code_class_base_inheritance in cls["bases"]] component_classes = [
cls
for cls in self.tree["classes"]
if self.code_class_base_inheritance in cls["bases"]
]
if not component_classes: if not component_classes:
return {} return {}
# Assume the first Component class is the one we're interested in # Assume the first Component class is the one we're interested in
component_class = component_classes[0] component_class = component_classes[0]
build_methods = [ build_methods = [
method for method in component_class["methods"] if method["name"] == self.function_entrypoint_name method
for method in component_class["methods"]
if method["name"] == self.function_entrypoint_name
] ]
return build_methods[0] if build_methods else {} return build_methods[0] if build_methods else {}
@ -259,7 +291,9 @@ class CustomComponent(Component):
# Retrieve and decrypt the credential by name for the current user # Retrieve and decrypt the credential by name for the current user
db_service = get_db_service() db_service = get_db_service()
with session_getter(db_service) as session: with session_getter(db_service) as session:
return credential_service.get_credential(user_id=self._user_id or "", name=name, session=session) return credential_service.get_credential(
user_id=self._user_id or "", name=name, session=session
)
return get_credential return get_credential
@ -269,7 +303,9 @@ class CustomComponent(Component):
credential_service = get_credential_service() credential_service = get_credential_service()
db_service = get_db_service() db_service = get_db_service()
with session_getter(db_service) as session: with session_getter(db_service) as session:
return credential_service.list_credentials(user_id=self._user_id, session=session) return credential_service.list_credentials(
user_id=self._user_id, session=session
)
def index(self, value: int = 0): def index(self, value: int = 0):
"""Returns a function that returns the value at the given index in the iterable.""" """Returns a function that returns the value at the given index in the iterable."""
@ -299,24 +335,51 @@ class CustomComponent(Component):
async def run_flow( async def run_flow(
self, self,
input_value: Union[str, list[str]], input_value: Union[str, list[str]],
flow_id: str, flow_id: Optional[str] = None,
flow_name: Optional[str] = None,
tweaks: Optional[dict] = None, tweaks: Optional[dict] = None,
) -> Any: ) -> Any:
if not flow_id and not flow_name:
raise ValueError("Flow ID or Flow Name is required")
if not self._flows_records:
self.list_flows()
if not flow_id and self._flows_records:
flow_ids = [
flow.data["id"]
for flow in self._flows_records
if flow.data["name"] == flow_name
]
if not flow_ids:
raise ValueError(f"Flow {flow_name} not found")
elif len(flow_ids) > 1:
raise ValueError(f"Multiple flows found with the name {flow_name}")
flow_id = flow_ids[0]
if not flow_id:
raise ValueError(f"Flow {flow_name} not found")
graph = await self.load_flow(flow_id, tweaks) graph = await self.load_flow(flow_id, tweaks)
input_value_dict = {"input_value": input_value} input_value_dict = {"input_value": input_value}
return await graph.run(input_value_dict, stream=False) return await graph.run(input_value_dict, stream=False)
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]: def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Record]:
if not self._user_id: if not self._user_id:
raise ValueError("Session is invalid") raise ValueError("Session is invalid")
try: try:
get_session = get_session or session_getter get_session = get_session or session_getter
db_service = get_db_service() db_service = get_db_service()
with get_session(db_service) as session: with get_session(db_service) as session:
flows = session.exec(select(Flow).where(Flow.user_id == self._user_id)).all() flows = session.exec(
return flows select(Flow)
.where(Flow.user_id == self._user_id)
.where(Flow.is_component == False)
).all()
flows_records = [flow.to_record() for flow in flows]
self._flows_records = flows_records
return flows_records
except Exception as e: except Exception as e:
raise ValueError("Session is invalid") from e raise ValueError(f"Error listing flows: {e}")
def build(self, *args: Any, **kwargs: Any) -> Any: def build(self, *args: Any, **kwargs: Any) -> Any:
raise NotImplementedError raise NotImplementedError

View file

@ -80,9 +80,13 @@ class DirectoryReader:
except Exception as e: except Exception as e:
logger.error(f"Error while loading component: {e}") logger.error(f"Error while loading component: {e}")
continue continue
items.append({"name": menu["name"], "path": menu["path"], "components": components}) items.append(
{"name": menu["name"], "path": menu["path"], "components": components}
)
filtered = [menu for menu in items if menu["components"]] filtered = [menu for menu in items if menu["components"]]
logger.debug(f'Filtered components {"with errors" if with_errors else ""}: {len(filtered)}') logger.debug(
f'Filtered components {"with errors" if with_errors else ""}: {len(filtered)}'
)
return {"menu": filtered} return {"menu": filtered}
def validate_code(self, file_content): def validate_code(self, file_content):
@ -115,7 +119,9 @@ class DirectoryReader:
Walk through the directory path and return a list of all .py files. Walk through the directory path and return a list of all .py files.
""" """
if not (safe_path := self.get_safe_path()): if not (safe_path := self.get_safe_path()):
raise CustomComponentPathValueError(f"The path needs to start with '{self.base_path}'.") raise CustomComponentPathValueError(
f"The path needs to start with '{self.base_path}'."
)
file_list = [] file_list = []
safe_path_obj = Path(safe_path) safe_path_obj = Path(safe_path)
@ -125,7 +131,11 @@ class DirectoryReader:
# any folders below [folder] will be ignored # any folders below [folder] will be ignored
# basically the parent folder of the file should be a # basically the parent folder of the file should be a
# folder in the safe_path # folder in the safe_path
if file_path.is_file() and file_path.parent.parent == safe_path_obj and not file_path.name.startswith("__"): if (
file_path.is_file()
and file_path.parent.parent == safe_path_obj
and not file_path.name.startswith("__")
):
file_list.append(str(file_path)) file_list.append(str(file_path))
return file_list return file_list
@ -163,7 +173,9 @@ class DirectoryReader:
for node in ast.walk(module): for node in ast.walk(module):
if isinstance(node, ast.FunctionDef): if isinstance(node, ast.FunctionDef):
for arg in node.args.args: for arg in node.args.args:
if self._is_type_hint_in_arg_annotation(arg.annotation, type_hint_name): if self._is_type_hint_in_arg_annotation(
arg.annotation, type_hint_name
):
return True return True
except SyntaxError: except SyntaxError:
# Returns False if the code is not valid Python # Returns False if the code is not valid Python
@ -181,14 +193,16 @@ class DirectoryReader:
and annotation.value.id == type_hint_name and annotation.value.id == type_hint_name
) )
def is_type_hint_used_but_not_imported(self, type_hint_name: str, code: str) -> bool: def is_type_hint_used_but_not_imported(
self, type_hint_name: str, code: str
) -> bool:
""" """
Check if a type hint is used but not imported in the given code. Check if a type hint is used but not imported in the given code.
""" """
try: try:
return self._is_type_hint_used_in_args(type_hint_name, code) and not self._is_type_hint_imported( return self._is_type_hint_used_in_args(
type_hint_name, code type_hint_name, code
) ) and not self._is_type_hint_imported(type_hint_name, code)
except SyntaxError: except SyntaxError:
# Returns True if there's something wrong with the code # Returns True if there's something wrong with the code
# TODO : Find a better way to handle this # TODO : Find a better way to handle this
@ -209,9 +223,9 @@ class DirectoryReader:
return False, "Syntax error" return False, "Syntax error"
elif not self.validate_build(file_content): elif not self.validate_build(file_content):
return False, "Missing build function" return False, "Missing build function"
elif self._is_type_hint_used_in_args("Optional", file_content) and not self._is_type_hint_imported( elif self._is_type_hint_used_in_args(
"Optional", file_content "Optional", file_content
): ) and not self._is_type_hint_imported("Optional", file_content):
return ( return (
False, False,
"Type hint 'Optional' is used but not imported in the code.", "Type hint 'Optional' is used but not imported in the code.",
@ -227,14 +241,18 @@ class DirectoryReader:
from the .py files in the directory. from the .py files in the directory.
""" """
response = {"menu": []} response = {"menu": []}
logger.debug("-------------------- Building component menu list --------------------") logger.debug(
"-------------------- Building component menu list --------------------"
)
for file_path in file_paths: for file_path in file_paths:
menu_name = os.path.basename(os.path.dirname(file_path)) menu_name = os.path.basename(os.path.dirname(file_path))
filename = os.path.basename(file_path) filename = os.path.basename(file_path)
validation_result, result_content = self.process_file(file_path) validation_result, result_content = self.process_file(file_path)
if not validation_result: if not validation_result:
logger.error(f"Error while processing file {file_path}: {result_content}") logger.error(
f"Error while processing file {file_path}: {result_content}"
)
menu_result = self.find_menu(response, menu_name) or { menu_result = self.find_menu(response, menu_name) or {
"name": menu_name, "name": menu_name,
@ -247,7 +265,9 @@ class DirectoryReader:
# first check if it's already CamelCase # first check if it's already CamelCase
if "_" in component_name: if "_" in component_name:
component_name_camelcase = " ".join(word.title() for word in component_name.split("_")) component_name_camelcase = " ".join(
word.title() for word in component_name.split("_")
)
else: else:
component_name_camelcase = component_name component_name_camelcase = component_name
@ -255,7 +275,9 @@ class DirectoryReader:
try: try:
output_types = self.get_output_types_from_code(result_content) output_types = self.get_output_types_from_code(result_content)
except Exception as exc: except Exception as exc:
logger.exception(f"Error while getting output types from code: {str(exc)}") logger.exception(
f"Error while getting output types from code: {str(exc)}"
)
output_types = [component_name_camelcase] output_types = [component_name_camelcase]
else: else:
output_types = [component_name_camelcase] output_types = [component_name_camelcase]
@ -271,7 +293,9 @@ class DirectoryReader:
if menu_result not in response["menu"]: if menu_result not in response["menu"]:
response["menu"].append(menu_result) response["menu"].append(menu_result)
logger.debug("-------------------- Component menu list built --------------------") logger.debug(
"-------------------- Component menu list built --------------------"
)
return response return response
@staticmethod @staticmethod
@ -281,5 +305,6 @@ class DirectoryReader:
""" """
custom_component = CustomComponent(code=code) custom_component = CustomComponent(code=code)
types_list = custom_component.get_function_entrypoint_return_type types_list = custom_component.get_function_entrypoint_return_type
# Get the name of types classes # Get the name of types classes
return [type_.__name__ for type_ in types_list if hasattr(type_, "__name__")] return [type_.__name__ for type_ in types_list if hasattr(type_, "__name__")]

View file

@ -78,6 +78,8 @@ def add_base_classes(frontend_node: CustomComponentFrontendNode, return_types: L
) )
base_classes = get_base_classes(return_type_instance) base_classes = get_base_classes(return_type_instance)
if return_type_instance == str:
base_classes.append("Text")
for base_class in base_classes: for base_class in base_classes:
frontend_node.add_base_class(base_class) frontend_node.add_base_class(base_class)
@ -232,10 +234,12 @@ def run_build_config(
# Allow user to build TemplateField as well # Allow user to build TemplateField as well
# as a dict with the same keys as TemplateField # as a dict with the same keys as TemplateField
field_dict = get_field_dict(field) field_dict = get_field_dict(field)
# This has to be done to set refresh if options or value are callable
update_field_dict(field_dict)
if update_field is not None and field_name != update_field: if update_field is not None and field_name != update_field:
continue continue
try: try:
update_field_dict(field_dict) update_field_dict(field_dict, call=True)
build_config[field_name] = field_dict build_config[field_name] = field_dict
except Exception as exc: except Exception as exc:
logger.error(f"Error while getting build_config: {str(exc)}") logger.error(f"Error while getting build_config: {str(exc)}")
@ -330,11 +334,13 @@ def create_component_template(component):
"""Create a template for a component.""" """Create a template for a component."""
component_code = component["code"] component_code = component["code"]
component_output_types = component["output_types"] component_output_types = component["output_types"]
# remove
component_extractor = CustomComponent(code=component_code) component_extractor = CustomComponent(code=component_code)
component_template = build_custom_component_template(component_extractor) component_template = build_custom_component_template(component_extractor)
component_template["output_types"] = component_output_types if not component_template["output_types"] and component_output_types:
component_template["output_types"] = component_output_types
return component_template return component_template
@ -364,15 +370,17 @@ def build_custom_components(settings_service):
return custom_components_from_file return custom_components_from_file
def update_field_dict(field_dict): def update_field_dict(field_dict, call=False):
"""Update the field dictionary by calling options() or value() if they are callable""" """Update the field dictionary by calling options() or value() if they are callable"""
if "options" in field_dict and callable(field_dict["options"]): if "options" in field_dict and callable(field_dict["options"]):
field_dict["options"] = field_dict["options"]() if call:
field_dict["options"] = field_dict["options"]()
# Also update the "refresh" key # Also update the "refresh" key
field_dict["refresh"] = True field_dict["refresh"] = True
if "value" in field_dict and callable(field_dict["value"]): if "value" in field_dict and callable(field_dict["value"]):
field_dict["value"] = field_dict["value"]() if call:
field_dict["value"] = field_dict["value"]()
field_dict["refresh"] = True field_dict["refresh"] = True
# Let's check if "range_spec" is a RangeSpec object # Let's check if "range_spec" is a RangeSpec object

View file

@ -1,14 +1,14 @@
import base64 import base64
import json import json
import os import os
from io import BytesIO
import re import re
from io import BytesIO
import yaml import yaml
from langchain.base_language import BaseLanguageModel from langchain.base_language import BaseLanguageModel
from PIL.Image import Image
from loguru import logger from loguru import logger
from PIL.Image import Image
from langflow.services.chat.config import ChatConfig from langflow.services.chat.config import ChatConfig
from langflow.services.deps import get_settings_service from langflow.services.deps import get_settings_service
@ -43,7 +43,9 @@ def try_setting_streaming_options(langchain_object):
llm = None llm = None
if hasattr(langchain_object, "llm"): if hasattr(langchain_object, "llm"):
llm = langchain_object.llm llm = langchain_object.llm
elif hasattr(langchain_object, "llm_chain") and hasattr(langchain_object.llm_chain, "llm"): elif hasattr(langchain_object, "llm_chain") and hasattr(
langchain_object.llm_chain, "llm"
):
llm = langchain_object.llm_chain.llm llm = langchain_object.llm_chain.llm
if isinstance(llm, BaseLanguageModel): if isinstance(llm, BaseLanguageModel):
@ -56,8 +58,37 @@ def try_setting_streaming_options(langchain_object):
def extract_input_variables_from_prompt(prompt: str) -> list[str]: def extract_input_variables_from_prompt(prompt: str) -> list[str]:
"""Extract input variables from prompt.""" variables = []
return re.findall(r"{(.*?)}", prompt) remaining_text = prompt
# Pattern to match single {var} and double {{var}} braces.
pattern = r"\{\{(.*?)\}\}|\{([^{}]+)\}"
while True:
match = re.search(pattern, remaining_text)
if not match:
break
# Extract the variable name from either the single or double brace match
if match.group(1): # Match found in double braces
variable_name = (
"{{" + match.group(1) + "}}"
) # Re-add single braces for JSON strings
else: # Match found in single braces
variable_name = match.group(2)
if variable_name is not None:
# This means there is a match
# but there is nothing inside the braces
variables.append(variable_name)
# Remove the matched text from the remaining_text
start, end = match.span()
remaining_text = remaining_text[:start] + remaining_text[end:]
# Proceed to the next match until no more matches are found
# No need to compare remaining "{}" instances because we are re-adding braces for JSON compatibility
return variables
def setup_llm_caching(): def setup_llm_caching():
@ -73,11 +104,14 @@ def setup_llm_caching():
def set_langchain_cache(settings): def set_langchain_cache(settings):
from langchain.globals import set_llm_cache from langchain.globals import set_llm_cache
from langflow.interface.importing.utils import import_class from langflow.interface.importing.utils import import_class
if cache_type := os.getenv("LANGFLOW_LANGCHAIN_CACHE"): if cache_type := os.getenv("LANGFLOW_LANGCHAIN_CACHE"):
try: try:
cache_class = import_class(f"langchain.cache.{cache_type or settings.LANGCHAIN_CACHE}") cache_class = import_class(
f"langchain.cache.{cache_type or settings.LANGCHAIN_CACHE}"
)
logger.debug(f"Setting up LLM caching with {cache_class.__name__}") logger.debug(f"Setting up LLM caching with {cache_class.__name__}")
set_llm_cache(cache_class()) set_llm_cache(cache_class())

View file

@ -1,9 +1,10 @@
from typing import Optional, Union from typing import Optional, Union
from loguru import logger
from langflow.schema import Record from langflow.schema import Record
from langflow.services.deps import get_monitor_service from langflow.services.deps import get_monitor_service
from langflow.services.monitor.schema import MessageModel from langflow.services.monitor.schema import MessageModel
from loguru import logger
def get_messages( def get_messages(

View file

@ -0,0 +1,3 @@
from .schema import Record
__all__ = ["Record"]

View file

@ -1,4 +1,4 @@
from typing import Any 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
@ -13,7 +13,7 @@ class Record(BaseModel):
data (dict, optional): Additional data associated with the record. data (dict, optional): Additional data associated with the record.
""" """
text: str text: Optional[str] = ""
data: dict = {} data: dict = {}
@classmethod @classmethod
@ -52,19 +52,6 @@ class Record(BaseModel):
Returns the text of the record. Returns the text of the record.
Returns: Returns:
str: The text of the record. str: The text and data of the record.
""" """
return self.text return self.model_dump_json(indent=2)
def docs_to_records(documents: list[Document]) -> list[Record]:
"""
Converts a list of Documents to a list of Records.
Args:
documents (list[Document]): The list of Documents to convert.
Returns:
list[Record]: The converted list of Records.
"""
return [Record.from_document(document) for document in documents]

View file

@ -7,6 +7,8 @@ from uuid import UUID, uuid4
from pydantic import field_serializer, field_validator from pydantic import field_serializer, field_validator
from sqlmodel import JSON, Column, Field, Relationship, SQLModel from sqlmodel import JSON, Column, Field, Relationship, SQLModel
from langflow.schema.schema import Record
if TYPE_CHECKING: if TYPE_CHECKING:
from langflow.services.database.models.user import User from langflow.services.database.models.user import User
@ -16,7 +18,9 @@ class FlowBase(SQLModel):
description: Optional[str] = Field(index=True, nullable=True, default=None) description: Optional[str] = Field(index=True, nullable=True, default=None)
data: Optional[Dict] = Field(default=None, nullable=True) data: Optional[Dict] = Field(default=None, nullable=True)
is_component: Optional[bool] = Field(default=False, nullable=True) is_component: Optional[bool] = Field(default=False, nullable=True)
updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow, nullable=True) updated_at: Optional[datetime] = Field(
default_factory=datetime.utcnow, nullable=True
)
folder: Optional[str] = Field(default=None, nullable=True) folder: Optional[str] = Field(default=None, nullable=True)
@field_validator("data") @field_validator("data")
@ -57,6 +61,18 @@ class Flow(FlowBase, table=True):
user_id: UUID = Field(index=True, foreign_key="user.id", nullable=True) user_id: UUID = Field(index=True, foreign_key="user.id", nullable=True)
user: "User" = Relationship(back_populates="flows") user: "User" = Relationship(back_populates="flows")
def to_record(self):
serialized = self.model_dump()
data = {
"id": serialized.pop("id"),
"data": serialized.pop("data"),
"name": serialized.pop("name"),
"description": serialized.pop("description"),
"updated_at": serialized.pop("updated_at"),
}
record = Record(text=data.get("name"), data=data)
return record
class FlowCreate(FlowBase): class FlowCreate(FlowBase):
user_id: Optional[UUID] = None user_id: Optional[UUID] = None

View file

@ -36,7 +36,10 @@ class DatabaseService(Service):
def _create_engine(self) -> "Engine": def _create_engine(self) -> "Engine":
"""Create the engine for the database.""" """Create the engine for the database."""
settings_service = get_settings_service() settings_service = get_settings_service()
if settings_service.settings.DATABASE_URL and settings_service.settings.DATABASE_URL.startswith("sqlite"): if (
settings_service.settings.DATABASE_URL
and settings_service.settings.DATABASE_URL.startswith("sqlite")
):
connect_args = {"check_same_thread": False} connect_args = {"check_same_thread": False}
else: else:
connect_args = {} connect_args = {}
@ -48,7 +51,9 @@ class DatabaseService(Service):
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None: # If an exception has been raised if exc_type is not None: # If an exception has been raised
logger.error(f"Session rollback because of exception: {exc_type.__name__} {exc_value}") logger.error(
f"Session rollback because of exception: {exc_type.__name__} {exc_value}"
)
self._session.rollback() self._session.rollback()
else: else:
self._session.commit() self._session.commit()
@ -65,7 +70,9 @@ class DatabaseService(Service):
settings_service = get_settings_service() settings_service = get_settings_service()
if settings_service.auth_settings.AUTO_LOGIN: if settings_service.auth_settings.AUTO_LOGIN:
with Session(self.engine) as session: with Session(self.engine) as session:
flows = session.exec(select(models.Flow).where(models.Flow.user_id is None)).all() flows = session.exec(
select(models.Flow).where(models.Flow.user_id is None)
).all()
if flows: if flows:
logger.debug("Migrating flows to default superuser") logger.debug("Migrating flows to default superuser")
username = settings_service.auth_settings.SUPERUSER username = settings_service.auth_settings.SUPERUSER
@ -95,7 +102,9 @@ class DatabaseService(Service):
expected_columns = list(model.model_fields.keys()) expected_columns = list(model.model_fields.keys())
try: try:
available_columns = [col["name"] for col in inspector.get_columns(table)] available_columns = [
col["name"] for col in inspector.get_columns(table)
]
except sa.exc.NoSuchTableError: except sa.exc.NoSuchTableError:
logger.error(f"Missing table: {table}") logger.error(f"Missing table: {table}")
return False return False
@ -152,14 +161,16 @@ class DatabaseService(Service):
try: try:
command.check(alembic_cfg) command.check(alembic_cfg)
except Exception as exc: except Exception as exc:
if isinstance(exc, (util.exc.CommandError, util.exc.AutogenerateDiffsDetected)): if isinstance(
exc, (util.exc.CommandError, util.exc.AutogenerateDiffsDetected)
):
command.upgrade(alembic_cfg, "head") command.upgrade(alembic_cfg, "head")
time.sleep(3) time.sleep(3)
try: try:
command.check(alembic_cfg) command.check(alembic_cfg)
except util.exc.AutogenerateDiffsDetected as e: except util.exc.AutogenerateDiffsDetected as e:
logger.error("AutogenerateDiffsDetected: {exc}") logger.error(f"AutogenerateDiffsDetected: {exc}")
if not fix: if not fix:
raise RuntimeError( raise RuntimeError(
"Something went wrong running migrations. Please, run `langflow migration --fix`" "Something went wrong running migrations. Please, run `langflow migration --fix`"
@ -188,7 +199,10 @@ class DatabaseService(Service):
# We will check that all models are in the database # We will check that all models are in the database
# and that the database is up to date with all columns # and that the database is up to date with all columns
sql_models = [models.Flow, models.User, models.ApiKey] sql_models = [models.Flow, models.User, models.ApiKey]
return [TableResults(sql_model.__tablename__, self.check_table(sql_model)) for sql_model in sql_models] return [
TableResults(sql_model.__tablename__, self.check_table(sql_model))
for sql_model in sql_models
]
def check_table(self, model): def check_table(self, model):
results = [] results = []
@ -197,7 +211,9 @@ class DatabaseService(Service):
expected_columns = list(model.__fields__.keys()) expected_columns = list(model.__fields__.keys())
available_columns = [] available_columns = []
try: try:
available_columns = [col["name"] for col in inspector.get_columns(table_name)] available_columns = [
col["name"] for col in inspector.get_columns(table_name)
]
results.append(Result(name=table_name, type="table", success=True)) results.append(Result(name=table_name, type="table", success=True))
except sa.exc.NoSuchTableError: except sa.exc.NoSuchTableError:
logger.error(f"Missing table: {table_name}") logger.error(f"Missing table: {table_name}")
@ -228,7 +244,9 @@ class DatabaseService(Service):
try: try:
table.create(self.engine, checkfirst=True) table.create(self.engine, checkfirst=True)
except OperationalError as oe: except OperationalError as oe:
logger.warning(f"Table {table} already exists, skipping. Exception: {oe}") logger.warning(
f"Table {table} already exists, skipping. Exception: {oe}"
)
except Exception as exc: except Exception as exc:
logger.error(f"Error creating table {table}: {exc}") logger.error(f"Error creating table {table}: {exc}")
raise RuntimeError(f"Error creating table {table}") from exc raise RuntimeError(f"Error creating table {table}") from exc
@ -240,7 +258,9 @@ class DatabaseService(Service):
if table not in table_names: if table not in table_names:
logger.error("Something went wrong creating the database and tables.") logger.error("Something went wrong creating the database and tables.")
logger.error("Please check your database settings.") logger.error("Please check your database settings.")
raise RuntimeError("Something went wrong creating the database and tables.") raise RuntimeError(
"Something went wrong creating the database and tables."
)
logger.debug("Database and tables created successfully") logger.debug("Database and tables created successfully")

View file

@ -10,7 +10,9 @@ if TYPE_CHECKING:
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
@ -51,8 +53,12 @@ class MessageModel(BaseModel):
@classmethod @classmethod
def from_record(cls, record: "Record"): def from_record(cls, record: "Record"):
# first check if the record has all the required fields # first check if the record has all the required fields
if not record.data or ("sender" not in record.data and "sender_name" not in record.data): if not record.data or (
raise ValueError("The record does not have the required fields 'sender' and 'sender_name' in the data.") "sender" not in record.data and "sender_name" not in record.data
):
raise ValueError(
"The record does not have the required fields 'sender' and 'sender_name' in the data."
)
return cls( return cls(
sender=record.data["sender"], sender=record.data["sender"],
sender_name=record.data["sender_name"], sender_name=record.data["sender_name"],

View file

@ -11,7 +11,9 @@ class ChatOutputResponse(BaseModel):
message: Union[str, List[Union[str, Dict]]] message: Union[str, List[Union[str, Dict]]]
sender: Optional[str] = "Machine" sender: Optional[str] = "Machine"
sender_name: Optional[str] = "AI" sender_name: Optional[str] = "AI"
session_id: Optional[str] = None
stream_url: Optional[str] = None stream_url: Optional[str] = None
component_id: Optional[str] = None
@classmethod @classmethod
def from_message( def from_message(

View file

@ -24,3 +24,7 @@ yarn-error.log*
/test-results/ /test-results/
/playwright-report/*/ /playwright-report/*/
/playwright/.cache/ /playwright/.cache/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -48,6 +48,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.331.0", "lucide-react": "^0.331.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"playwright": "^1.42.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-ace": "^10.1.0", "react-ace": "^10.1.0",
"react-cookie": "^4.1.1", "react-cookie": "^4.1.1",
@ -77,7 +78,7 @@
"zustand": "^4.4.7" "zustand": "^4.4.7"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.41.2", "@playwright/test": "^1.42.0",
"@swc/cli": "^0.1.62", "@swc/cli": "^0.1.62",
"@swc/core": "^1.3.80", "@swc/core": "^1.3.80",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
@ -1492,12 +1493,12 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.41.2", "version": "1.42.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.0.tgz",
"integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", "integrity": "sha512-2k1HzC28Fs+HiwbJOQDUwrWMttqSLUVdjCqitBOjdCD0svWOMQUVqrXX6iFD7POps6xXAojsX/dGBpKnjZctLA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"playwright": "1.41.2" "playwright": "1.42.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8719,12 +8720,11 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.41.2", "version": "1.42.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.0.tgz",
"integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", "integrity": "sha512-Ko7YRUgj5xBHbntrgt4EIw/nE//XBHOKVKnBjO1KuZkmkhlbgyggTe5s9hjqQ1LpN+Xg+kHsQyt5Pa0Bw5XpvQ==",
"dev": true,
"dependencies": { "dependencies": {
"playwright-core": "1.41.2" "playwright-core": "1.42.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8737,10 +8737,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.41.2", "version": "1.42.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.0.tgz",
"integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", "integrity": "sha512-0HD9y8qEVlcbsAjdpBaFjmaTHf+1FeIddy8VJLeiqwhcNqGCBe4Wp2e8knpqiYbzxtxarxiXyNDw2cG8sCaNMQ==",
"dev": true,
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
}, },

View file

@ -43,6 +43,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.331.0", "lucide-react": "^0.331.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"playwright": "^1.42.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-ace": "^10.1.0", "react-ace": "^10.1.0",
"react-cookie": "^4.1.1", "react-cookie": "^4.1.1",
@ -99,7 +100,7 @@
}, },
"proxy": "http://127.0.0.1:7860", "proxy": "http://127.0.0.1:7860",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.41.2", "@playwright/test": "^1.42.0",
"@swc/cli": "^0.1.62", "@swc/cli": "^0.1.62",
"@swc/core": "^1.3.80", "@swc/core": "^1.3.80",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",

View file

@ -10,9 +10,6 @@
<li> <li>
<a href="./e2e/index.html">e2e report</a> <a href="./e2e/index.html">e2e report</a>
</li> </li>
<li>
<a href="./onlyFront/index.html">frontEnd Only report</a>
</li>
</ul> </ul>
</body> </body>
</html> </html>

View file

@ -27,8 +27,16 @@ terminate_process_by_port() {
echo "Process terminated." echo "Process terminated."
} }
delete_temp() {
cd ../../
echo "Deleting temp database"
rm temp
echo "Temp database deleted."
}
# Trap signals to ensure cleanup on script termination # Trap signals to ensure cleanup on script termination
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000' EXIT trap 'terminate_process_by_port 7860; terminate_process_by_port 3000; delete_temp' EXIT
# install playwright if there is not installed yet # install playwright if there is not installed yet
npx playwright install npx playwright install
@ -42,26 +50,16 @@ make frontend &
# Give some time for the frontend to start (adjust sleep duration as needed) # Give some time for the frontend to start (adjust sleep duration as needed)
sleep 10 sleep 10
# Navigate to the test directory #install backend
cd src/frontend poetry install --extras deploy
# Run frontend only Playwright tests with or without UI based on the --ui flag
if [ "$ui" = true ]; then
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --ui --project=chromium
else
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --project=chromium
fi
# Navigate back to the project root directory
cd ../../
# Start the backend using 'make backend' in the background # Start the backend using 'make backend' in the background
make backend & LANGFLOW_DATABASE_URL=sqlite:///./temp LANGFLOW_AUTO_LOGIN=True poetry run langflow run --backend-only --port 7860 --host 0.0.0.0 --no-open-browser &
# Give some time for the backend to start (adjust sleep duration as needed) # Give some time for the backend to start (adjust sleep duration as needed)
sleep 25 sleep 25
# Navigate back to the test directory # Navigate to the test directory
cd src/frontend cd src/frontend
# Run Playwright tests with or without UI based on the --ui flag # Run Playwright tests with or without UI based on the --ui flag

View file

@ -16,11 +16,12 @@ import PromptAreaComponent from "../../../../components/promptComponent";
import TextAreaComponent from "../../../../components/textAreaComponent"; import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent"; import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button"; import { Button } from "../../../../components/ui/button";
import { RefreshButton } from "../../../../components/ui/refreshButton";
import { import {
INPUT_HANDLER_HOVER,
LANGFLOW_SUPPORTED_TYPES, LANGFLOW_SUPPORTED_TYPES,
OUTPUT_HANDLER_HOVER,
TOOLTIP_EMPTY, TOOLTIP_EMPTY,
inputHandleHover,
outputHandleHover,
} from "../../../../constants/constants"; } from "../../../../constants/constants";
import { postCustomComponentUpdate } from "../../../../controllers/API"; import { postCustomComponentUpdate } from "../../../../controllers/API";
import useAlertStore from "../../../../stores/alertStore"; import useAlertStore from "../../../../stores/alertStore";
@ -68,7 +69,7 @@ export default function ParameterComponent({
const nodes = useFlowStore((state) => state.nodes); const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges); const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode); const setNode = useFlowStore((state) => state.setNode);
const [isLoading, setIsLoading] = useState(false);
const flow = currentFlow?.data?.nodes ?? null; const flow = currentFlow?.data?.nodes ?? null;
const groupedEdge = useRef(null); const groupedEdge = useRef(null);
@ -85,7 +86,12 @@ export default function ParameterComponent({
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const handleUpdateValues = async (name: string, data: NodeDataType) => { const handleUpdateValues = async (
name: string,
data: NodeDataType,
delayAnimation: boolean = true
) => {
setIsLoading(true);
const code = data.node?.template["code"]?.value; const code = data.node?.template["code"]?.value;
if (!code) { if (!code) {
console.error("Code not found in the template"); console.error("Code not found in the template");
@ -95,13 +101,48 @@ export default function ParameterComponent({
try { try {
const res = await postCustomComponentUpdate(code, name); const res = await postCustomComponentUpdate(code, name);
if (res.status === 200 && data.node?.template) { if (res.status === 200 && data.node?.template) {
data.node!.template[name] = res.data.template[name]; setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template[name] = res.data.template[name];
return newNode;
});
} }
} catch (err) { } catch (err) {
setErrorData(err as { title: string; list?: Array<string> }); setErrorData(err as { title: string; list?: Array<string> });
} }
renderTooltips();
if (delayAnimation) {
try {
// Wait for at least 500 milliseconds
await new Promise((resolve) => setTimeout(resolve, 500));
// Continue with the request
// If the request takes longer than 500 milliseconds, it will not wait an additional 500 milliseconds
} catch (error) {
console.error("Error occurred while waiting for refresh:", error);
} finally {
setIsLoading(false);
}
} else setIsLoading(false);
}; };
useEffect(() => {
function fetchData() {
if (
data.node?.template[name]?.refresh &&
Object.keys(data.node?.template[name]?.options ?? {}).length === 0
) {
handleUpdateValues(name, data, false);
}
}
fetchData();
}, []);
const handleOnNewValue = ( const handleOnNewValue = (
newValue: string | string[] | boolean | Object[] newValue: string | string[] | boolean | Object[]
): void => { ): void => {
@ -182,7 +223,7 @@ export default function ParameterComponent({
return ( return (
<div key={index}> <div key={index}>
{index === 0 && ( {index === 0 && (
<span>{left ? inputHandleHover : outputHandleHover}</span> <span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
)} )}
<span <span
key={index} key={index}
@ -212,15 +253,15 @@ export default function ParameterComponent({
{item.display_name === "" ? "" : " - "} {item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2 {item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => ( ? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + index}> <React.Fragment key={el + name}>
<span> <span>
{index === {index ===
item.display_name.split(", ").length - 1 item.display_name.split(", ").length - 1
? el ? el
: (el += `, `)} : (el += `, `)}
</span> </span>
</React.Fragment> </React.Fragment>
)) ))
: item.display_name} : item.display_name}
</span> </span>
) : ( ) : (
@ -229,14 +270,14 @@ export default function ParameterComponent({
{item.type === "" ? "" : " - "} {item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2 {item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => ( ? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + index}> <React.Fragment key={el + name}>
<span> <span>
{index === item.type.split(", ").length - 1 {index === item.type.split(", ").length - 1
? el ? el
: (el += `, `)} : (el += `, `)}
</span> </span>
</React.Fragment> </React.Fragment>
)) ))
: item.type} : item.type}
</span> </span>
)} )}
@ -304,24 +345,31 @@ export default function ParameterComponent({
ref={ref} ref={ref}
className={ className={
"relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" + "relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" +
(name === "code" ? " hidden " : "") ((name === "code" && type === "code") ||
(name.includes("code") && proxy)
? " hidden "
: "")
} }
> >
<> <>
<div <div
className={ className={
"w-full truncate text-sm" + "w-full truncate text-sm" +
(left ? "" : " text-end") + (left ? "" : " gap-2 flex justify-end items-center") +
(info !== "" ? " flex items-center" : "") (info !== "" ? " flex items-center" : "")
} }
> >
{!left && data.node?.pinned &&
<div>
<IconComponent className="w-5 h-5 text-ice" name={"Snowflake"} />
</div>}
{proxy ? ( {proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}> <ShadTooltip content={<span>{proxy.id}</span>}>
<span>{title}</span> <span className={!left && data.node?.pinned?" text-ice":""}>{title}</span>
</ShadTooltip> </ShadTooltip>
) : ( ) : (
title <span className={!left && data.node?.pinned?" text-ice":""}>{title}</span>
)} )}
<span className={(info === "" ? "" : "ml-1 ") + " text-status-red"}> <span className={(info === "" ? "" : "ml-1 ") + " text-status-red"}>
{required ? " *" : ""} {required ? " *" : ""}
</span> </span>
@ -383,20 +431,35 @@ export default function ParameterComponent({
)} )}
{left === true && {left === true &&
type === "str" && type === "str" &&
!data.node?.template[name].options ? ( !data.node?.template[name].options ? (
<div className="mt-2 w-full"> <div className="mt-2 w-full">
{data.node?.template[name].list ? ( {data.node?.template[name].list ? (
<InputListComponent <div className="w-5/6 flex-grow">
disabled={disabled} <InputListComponent
value={ disabled={disabled}
!data.node.template[name].value || value={
data.node.template[name].value === "" !data.node.template[name].value ||
? [""] data.node.template[name].value === ""
: data.node.template[name].value ? [""]
} : data.node.template[name].value
onChange={handleOnNewValue} }
/> onChange={handleOnNewValue}
/>
{data.node?.template[name].refresh && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleUpdateValues}
id={"refresh-button-" + name}
/>
</div>
)}
</div>
) : data.node?.template[name].multiline ? ( ) : data.node?.template[name].multiline ? (
<TextAreaComponent <TextAreaComponent
disabled={disabled} disabled={disabled}
@ -409,7 +472,7 @@ export default function ParameterComponent({
<div className="mt-2 flex w-full items-center"> <div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow"> <div className="w-5/6 flex-grow">
<InputComponent <InputComponent
id={"input-" + index} id={"input-" + name}
disabled={disabled} disabled={disabled}
password={data.node?.template[name].password ?? false} password={data.node?.template[name].password ?? false}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
@ -417,14 +480,17 @@ export default function ParameterComponent({
/> />
</div> </div>
{data.node?.template[name].refresh && ( {data.node?.template[name].refresh && (
<button <div className="w-1/6">
className="extra-side-bar-buttons ml-2 mt-1 w-1/6" <RefreshButton
onClick={() => { isLoading={isLoading}
handleUpdateValues(name, data); disabled={disabled}
}} name={name}
> data={data}
<IconComponent name="RefreshCcw" /> className="extra-side-bar-buttons ml-2 mt-1"
</button> handleUpdateValues={handleUpdateValues}
id={"refresh-button-" + name}
/>
</div>
)} )}
</div> </div>
)} )}
@ -432,11 +498,12 @@ export default function ParameterComponent({
) : left === true && type === "bool" ? ( ) : left === true && type === "bool" ? (
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<ToggleShadComponent <ToggleShadComponent
id={"toggle-" + index} id={"toggle-" + name}
disabled={disabled} disabled={disabled}
enabled={data.node?.template[name].value ?? false} enabled={data.node?.template[name].value ?? false}
setEnabled={handleOnNewValue} setEnabled={handleOnNewValue}
size="large" size="large"
editNode={false}
/> />
</div> </div>
) : left === true && type === "float" ? ( ) : left === true && type === "float" ? (
@ -450,26 +517,31 @@ export default function ParameterComponent({
</div> </div>
) : left === true && ) : left === true &&
type === "str" && type === "str" &&
data.node?.template[name].options ? ( (data.node?.template[name].options ||
data.node?.template[name]?.refresh) ? (
// TODO: Improve CSS // TODO: Improve CSS
<div className="mt-2 flex w-full items-center"> <div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow"> <div className="w-5/6 flex-grow">
<Dropdown <Dropdown
isLoading={isLoading}
options={data.node.template[name].options} options={data.node.template[name].options}
onSelect={handleOnNewValue} onSelect={handleOnNewValue}
value={data.node.template[name].value ?? "Choose an option"} value={data.node.template[name].value ?? "Choose an option"}
id={"dropdown-" + index} id={"dropdown-" + name}
/> />
</div> </div>
{data.node?.template[name].refresh && ( {data.node?.template[name].refresh && (
<button <div className="w-1/6">
className="extra-side-bar-buttons ml-2 mt-1 w-1/6" <RefreshButton
onClick={() => { isLoading={isLoading}
handleUpdateValues(name, data); disabled={disabled}
}} name={name}
> data={data}
<IconComponent name="RefreshCcw" /> className="extra-side-bar-buttons ml-2 mt-1"
</button> handleUpdateValues={handleUpdateValues}
id={"refresh-button-" + name}
/>
</div>
)} )}
</div> </div>
) : left === true && type === "code" ? ( ) : left === true && type === "code" ? (
@ -486,7 +558,7 @@ export default function ParameterComponent({
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"code-input-" + index} id={"code-input-" + name}
/> />
</div> </div>
) : left === true && type === "file" ? ( ) : left === true && type === "file" ? (
@ -507,7 +579,7 @@ export default function ParameterComponent({
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"int-input-" + index} id={"int-input-" + name}
/> />
</div> </div>
) : left === true && type === "prompt" ? ( ) : left === true && type === "prompt" ? (
@ -520,8 +592,8 @@ export default function ParameterComponent({
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"prompt-input-" + index} id={"prompt-input-" + name}
data-testid={"prompt-input-" + index} data-testid={"prompt-input-" + name}
/> />
</div> </div>
) : left === true && type === "NestedDict" ? ( ) : left === true && type === "NestedDict" ? (
@ -531,10 +603,10 @@ export default function ParameterComponent({
editNode={false} editNode={false}
value={ value={
!data.node!.template[name].value || !data.node!.template[name].value ||
data.node!.template[name].value?.toString() === "{}" data.node!.template[name].value?.toString() === "{}"
? { ? {
yourkey: "value", yourkey: "value",
} }
: data.node!.template[name].value : data.node!.template[name].value
} }
onChange={handleOnNewValue} onChange={handleOnNewValue}
@ -548,7 +620,7 @@ export default function ParameterComponent({
editNode={false} editNode={false}
value={ value={
data.node!.template[name].value?.length === 0 || data.node!.template[name].value?.length === 0 ||
!data.node!.template[name].value !data.node!.template[name].value
? [{ "": "" }] ? [{ "": "" }]
: convertObjToArray(data.node!.template[name].value) : convertObjToArray(data.node!.template[name].value)
} }

View file

@ -9,9 +9,10 @@ import Loading from "../../components/ui/loading";
import { Textarea } from "../../components/ui/textarea"; import { Textarea } from "../../components/ui/textarea";
import Xmark from "../../components/ui/xmark"; import Xmark from "../../components/ui/xmark";
import { import {
RUN_TIMESTAMP_PREFIX,
STATUS_BUILD,
STATUS_BUILDING,
priorityFields, priorityFields,
statusBuild,
statusBuilding,
} from "../../constants/constants"; } from "../../constants/constants";
import { BuildStatus } from "../../constants/enums"; import { BuildStatus } from "../../constants/enums";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent"; import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
@ -49,11 +50,18 @@ export default function GenericNode({
const [nodeDescription, setNodeDescription] = useState( const [nodeDescription, setNodeDescription] = useState(
data.node?.description! data.node?.description!
); );
const buildStatus = useFlowStore((state) => state.flowBuildStatus[data.id]); const buildStatus = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.status
);
const lastRunTime = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.timestamp
);
const [validationStatus, setValidationStatus] = const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null); useState<validationStatusType | null>(null);
const [handles, setHandles] = useState<number>(0); const [handles, setHandles] = useState<number>(0);
const [validationString, setValidationString] = useState<string>("");
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
function countHandles(): void { function countHandles(): void {
@ -100,7 +108,7 @@ export default function GenericNode({
if (duration === undefined) { if (duration === undefined) {
return ""; return "";
} else { } else {
return `Duration: ${duration}`; return `${duration}`;
} }
}; };
const durationString = getDurationString(validationStatus?.data.duration); const durationString = getDurationString(validationStatus?.data.duration);
@ -126,6 +134,18 @@ export default function GenericNode({
} }
}, [flowPool[data.id], data.id]); }, [flowPool[data.id], data.id]);
useEffect(() => {
if (validationStatus?.params) {
// if it is not a string turn it into a string
let newValidationString = validationStatus.params;
if (typeof newValidationString !== "string") {
newValidationString = JSON.stringify(validationStatus.params);
}
setValidationString(newValidationString);
}
}, [validationStatus, validationStatus?.params]);
const showNode = data.showNode ?? true; const showNode = data.showNode ?? true;
const nameEditable = true; const nameEditable = true;
@ -332,8 +352,8 @@ export default function GenericNode({
/> />
</div> </div>
) : ( ) : (
<div className="group flex items-center gap-2.5"> <div className="group flex items-start gap-1.5">
<ShadTooltip content={data.id}> <ShadTooltip content={data.node?.display_name}>
<div <div
onDoubleClick={(event) => { onDoubleClick={(event) => {
if (nameEditable) { if (nameEditable) {
@ -359,8 +379,8 @@ export default function GenericNode({
}} }}
> >
<IconComponent <IconComponent
name="Pencil" name="PencilLine"
className="hidden h-4 w-4 animate-pulse text-status-blue group-hover:block" className="hidden h-3 w-3 text-status-blue group-hover:block"
/> />
</div> </div>
)} )}
@ -390,13 +410,34 @@ export default function GenericNode({
})} })}
data={data} data={data}
color={ color={
nodeColors[ data.node?.template[templateField].input_types &&
types[data.node?.template[templateField].type!] data.node?.template[templateField].input_types!
] ?? .length > 0
nodeColors[ ? nodeColors[
data.node?.template[templateField].type! data.node?.template[templateField]
] ?? .input_types![
nodeColors.unknown data.node?.template[templateField]
.input_types!.length - 1
]
] ??
nodeColors[
types[
data.node?.template[templateField]
.input_types![
data.node?.template[templateField]
.input_types!.length - 1
]
]
]
: nodeColors[
data.node?.template[templateField].type!
] ??
nodeColors[
types[
data.node?.template[templateField].type!
]
] ??
nodeColors.unknown
} }
title={getFieldTitle( title={getFieldTitle(
data.node?.template!, data.node?.template!,
@ -458,38 +499,55 @@ export default function GenericNode({
)} )}
</div> </div>
{showNode && ( {showNode && (
<Button <Button variant="secondary" className={"group h-9 px-1.5"}>
variant="secondary"
className={"group h-9 px-1.5"}
onClick={() => {
if (buildStatus === BuildStatus.BUILDING || isBuilding)
return;
setValidationStatus(null);
buildFlow({ nodeId: data.id });
}}
>
<div> <div>
<ShadTooltip <ShadTooltip
content={ content={
buildStatus === BuildStatus.BUILDING ? ( buildStatus === BuildStatus.BUILDING ? (
<span> {statusBuilding} </span> <span> {STATUS_BUILDING} </span>
) : !validationStatus ? ( ) : !validationStatus ? (
<span className="flex">{statusBuild}</span> <span className="flex">{STATUS_BUILD}</span>
) : ( ) : (
<div className="max-h-96 overflow-auto"> <div className="max-h-100">
{typeof validationStatus.params === "string" <div>
? `${durationString}\n${validationStatus.params}` {lastRunTime && (
.split("\n") <div className="justify-left flex text-muted-foreground">
.map((line, index) => ( <div>{RUN_TIMESTAMP_PREFIX}</div>
<div key={index}>{line}</div> <div className="ml-1 text-status-blue">
)) {lastRunTime}
: durationString} </div>
</div>
)}
</div>
<div className="justify-left flex text-muted-foreground">
<div>Duration:</div>
<div className="ml-1 text-status-blue">
{validationStatus?.data.duration}
</div>
</div>
<hr />
<span className="flex justify-center text-muted-foreground ">
Output
</span>
<div className="max-h-96 overflow-auto custom-scroll">
{validationString.split("\n").map((line, index) => (
<div key={index}>{line}</div>
))}
</div>
</div> </div>
) )
} }
side="bottom" side="bottom"
> >
<div className="generic-node-status-position flex items-center justify-center"> <div
onClick={() => {
if (buildStatus === BuildStatus.BUILDING || isBuilding)
return;
setValidationStatus(null);
buildFlow({ stopNodeId: data.id });
}}
className="generic-node-status-position flex items-center justify-center"
>
{renderIconStatus(buildStatus, validationStatus)} {renderIconStatus(buildStatus, validationStatus)}
</div> </div>
</ShadTooltip> </ShadTooltip>
@ -607,13 +665,18 @@ export default function GenericNode({
data.node?.template[templateField].input_types! data.node?.template[templateField].input_types!
.length > 0 .length > 0
? nodeColors[ ? nodeColors[
data.node?.template[templateField] data.node?.template[templateField].input_types![
.input_types![0] data.node?.template[templateField]
.input_types!.length - 1
]
] ?? ] ??
nodeColors[ nodeColors[
types[ types[
data.node?.template[templateField] data.node?.template[templateField]
.input_types![0] .input_types![
data.node?.template[templateField]
.input_types!.length - 1
]
] ]
] ]
: nodeColors[ : nodeColors[

View file

@ -6,7 +6,7 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "../../components/ui/popover"; } from "../../components/ui/popover";
import { zeroNotifications } from "../../constants/constants"; import { ZERO_NOTIFICATIONS } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import { AlertDropdownType } from "../../types/alerts"; import { AlertDropdownType } from "../../types/alerts";
import SingleAlert from "./components/singleAlertComponent"; import SingleAlert from "./components/singleAlertComponent";
@ -70,7 +70,7 @@ export default function AlertDropdown({
)) ))
) : ( ) : (
<div className="flex h-full w-full items-center justify-center pb-16 text-ring"> <div className="flex h-full w-full items-center justify-center pb-16 text-ring">
{zeroNotifications} {ZERO_NOTIFICATIONS}
</div> </div>
)} )}
</div> </div>

View file

@ -49,8 +49,10 @@ export default function AccordionComponent({
> >
{trigger} {trigger}
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="AccordionContent flex flex-col"> <AccordionContent>
{children} <div className="AccordionContent flex flex-col">
{children}
</div>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>

View file

@ -1,17 +1,19 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
CHAT_FORM_DIALOG_SUBTITLE, CHAT_FORM_DIALOG_SUBTITLE,
outputsModalTitle, OUTPUTS_MODAL_TITLE,
textInputModalTitle, TEXT_INPUT_MODAL_TITLE,
} from "../../constants/constants"; } from "../../constants/constants";
import BaseModal from "../../modals/baseModal"; import BaseModal from "../../modals/baseModal";
import useFlowStore from "../../stores/flowStore"; import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { NodeType } from "../../types/flow";
import { updateVerticesOrder } from "../../utils/buildUtils"; import { updateVerticesOrder } from "../../utils/buildUtils";
import { cn } from "../../utils/utils"; import { cn } from "../../utils/utils";
import AccordionComponent from "../AccordionComponent"; import AccordionComponent from "../AccordionComponent";
import IOInputField from "../IOInputField"; import IOInputField from "../IOInputField";
import IOOutputView from "../IOOutputView"; import IOOutputView from "../IOOutputView";
import ShadTooltip from "../ShadTooltipComponent";
import IconComponent from "../genericIconComponent"; import IconComponent from "../genericIconComponent";
import NewChatView from "../newChatView"; import NewChatView from "../newChatView";
import { Badge } from "../ui/badge"; import { Badge } from "../ui/badge";
@ -50,6 +52,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
const [chatValue, setChatValue] = useState(""); const [chatValue, setChatValue] = useState("");
const isBuilding = useFlowStore((state) => state.isBuilding); const isBuilding = useFlowStore((state) => state.isBuilding);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setNode = useFlowStore((state) => state.setNode);
async function updateVertices() { async function updateVertices() {
return updateVerticesOrder(currentFlow!.id, null); return updateVerticesOrder(currentFlow!.id, null);
@ -67,12 +70,22 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
setLockChat(true); setLockChat(true);
setChatValue(""); setChatValue("");
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
await buildFlow({ input_value: chatValue }).catch((err) => { await buildFlow({
input_value: chatValue,
startNodeId: chatInput?.id,
}).catch((err) => {
console.error(err); console.error(err);
setLockChat(false); setLockChat(false);
}); });
} }
setLockChat(false); setLockChat(false);
if (chatInput) {
setNode(chatInput.id, (node: NodeType) => {
const newNode = { ...node };
newNode.data.node!.template["input_value"].value = chatValue;
return newNode;
});
}
} }
useEffect(() => { useEffect(() => {
@ -134,7 +147,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
> >
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold"> <div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
<IconComponent className="h-4 w-4" name={"Type"} /> <IconComponent className="h-4 w-4" name={"Type"} />
{textInputModalTitle} {TEXT_INPUT_MODAL_TITLE}
</div> </div>
{nodes {nodes
.filter((node) => .filter((node) =>
@ -195,7 +208,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
> >
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold"> <div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
<IconComponent className="h-4 w-4" name={"FileType2"} /> <IconComponent className="h-4 w-4" name={"FileType2"} />
{outputsModalTitle} {OUTPUTS_MODAL_TITLE}
</div> </div>
{nodes {nodes
.filter((node) => .filter((node) =>
@ -213,9 +226,16 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
<AccordionComponent <AccordionComponent
trigger={ trigger={
<div className="file-component-badge-div"> <div className="file-component-badge-div">
<Badge variant="gray" size="md"> <ShadTooltip
{output.id} content={output.id}
</Badge> styleClasses="z-50"
>
<div>
<Badge variant="gray" size="md">
{node.data.node.display_name}
</Badge>
</div>
</ShadTooltip>
{haveChat && ( {haveChat && (
<div <div
className="-mb-1 pr-4" className="-mb-1 pr-4"
@ -255,7 +275,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
)} )}
{haveChat ? ( {haveChat ? (
<div className="flex h-full w-full"> <div className="flex h-full min-w-96 flex-grow">
{selectedViewField && ( {selectedViewField && (
<div <div
className={cn( className={cn(
@ -272,7 +292,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
</button> </button>
{selectedViewField.type} {selectedViewField.type}
</div> </div>
<div className="h-full"> <div className="h-full w-full">
{inputs.some( {inputs.some(
(input) => input.id === selectedViewField.id (input) => input.id === selectedViewField.id
) ? ( ) ? (
@ -308,28 +328,30 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
<div className="absolute bottom-8 right-8"></div> <div className="absolute bottom-8 right-8"></div>
)} )}
</div> </div>
{!haveChat && (
<div className="flex w-full justify-end pt-6">
<Button
variant={"outline"}
className="flex gap-2 px-3"
onClick={() => sendMessage(1)}
>
<IconComponent
name={isBuilding ? "Loader2" : "Play"}
className={cn(
"h-4 w-4",
isBuilding
? "animate-spin"
: "fill-current text-medium-indigo"
)}
/>
Run Flow
</Button>
</div>
)}
</div> </div>
</BaseModal.Content> </BaseModal.Content>
<BaseModal.Footer>
{!haveChat && (
<div className="flex w-full justify-end pt-6">
<Button
variant={"outline"}
className="flex gap-2 px-3"
onClick={() => sendMessage(1)}
>
<IconComponent
name={isBuilding ? "Loader2" : "Play"}
className={cn(
"h-4 w-4",
isBuilding
? "animate-spin"
: "fill-current text-medium-indigo"
)}
/>
Run Flow
</Button>
</div>
)}
</BaseModal.Footer>
</BaseModal> </BaseModal>
); );
} }

View file

@ -14,9 +14,8 @@ export default function CodeAreaComponent({
setNodeClass, setNodeClass,
id = "", id = "",
readonly = false, readonly = false,
openModal, open,
selected, setOpen,
setOpenModal,
}: CodeAreaComponentType) { }: CodeAreaComponentType) {
const [myValue, setMyValue] = useState( const [myValue, setMyValue] = useState(
typeof value == "string" ? value : JSON.stringify(value) typeof value == "string" ? value : JSON.stringify(value)
@ -35,9 +34,8 @@ export default function CodeAreaComponent({
return ( return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}> <div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<CodeAreaModal <CodeAreaModal
selected={selected} open={open}
openModal={openModal} setOpen={setOpen}
setOpenModal={setOpenModal}
readonly={readonly} readonly={readonly}
dynamic={dynamic} dynamic={dynamic}
value={myValue} value={myValue}

View file

@ -126,7 +126,7 @@ export default function CodeTabsComponent({
<Tabs <Tabs
value={activeTab} value={activeTab}
className={ className={
"api-modal-tabs " + "api-modal-tabs m-0 inset-0 " +
(isMessage ? "dark " : "") + (isMessage ? "dark " : "") +
(dark && isMessage ? "bg-background" : "") (dark && isMessage ? "bg-background" : "")
} }
@ -166,7 +166,7 @@ export default function CodeTabsComponent({
) : ( ) : (
<IconComponent name="Clipboard" className="h-4 w-4" /> <IconComponent name="Clipboard" className="h-4 w-4" />
)} )}
{isCopied ? "Copied!" : "Copy code"} {isCopied ? "Copied!" : "Copy Code"}
</button> </button>
<button <button
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300" className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
@ -193,9 +193,9 @@ export default function CodeTabsComponent({
></div> ></div>
)} )}
<SyntaxHighlighter <SyntaxHighlighter
className="mt-0 h-full w-full overflow-auto custom-scroll" language={tab.language}
language={tab.mode}
style={oneDark} style={oneDark}
className="mt-0 h-full overflow-auto custom-scroll rounded-sm text-left"
> >
{tab.code} {tab.code}
</SyntaxHighlighter> </SyntaxHighlighter>

View file

@ -5,6 +5,7 @@ import { classNames } from "../../utils/utils";
import IconComponent from "../genericIconComponent"; import IconComponent from "../genericIconComponent";
export default function Dropdown({ export default function Dropdown({
isLoading,
value, value,
options, options,
onSelect, onSelect,
@ -129,11 +130,17 @@ export default function Dropdown({
</> </>
) : ( ) : (
<> <>
<div> {(!isLoading && (
<span className="text-sm italic"> <div>
No parameters are available for display. <span className="text-sm italic">
</span> No parameters are available for display.
</div> </span>
</div>
)) || (
<div>
<span className="text-sm italic">Loading...</span>
</div>
)}
</> </>
)} )}
</> </>

View file

@ -9,7 +9,7 @@ import {
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Node } from "reactflow"; import { Node } from "reactflow";
import { savedHover } from "../../../../constants/constants"; import { SAVED_HOVER } from "../../../../constants/constants";
import FlowSettingsModal from "../../../../modals/flowSettingsModal"; import FlowSettingsModal from "../../../../modals/flowSettingsModal";
import useAlertStore from "../../../../stores/alertStore"; import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore"; import useFlowStore from "../../../../stores/flowStore";
@ -138,7 +138,7 @@ export const MenuBar = ({
</div> </div>
<ShadTooltip <ShadTooltip
content={ content={
savedHover + SAVED_HOVER +
new Date(currentFlow.updated_at ?? "").toLocaleString("en-US", { new Date(currentFlow.updated_at ?? "").toLocaleString("en-US", {
hour: "numeric", hour: "numeric",
minute: "numeric", minute: "numeric",

View file

@ -91,6 +91,7 @@ export default function InputComponent({
handleKeyDown(e, value, ""); handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur(); if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}} }}
data-testid={editNode ? id + "-edit" : id}
/> />
)} )}
{password && ( {password && (

View file

@ -45,6 +45,7 @@ export default function IntComponent({
onChange={(event) => { onChange={(event) => {
onChange(event.target.value); onChange(event.target.value);
}} }}
data-testid={id}
/> />
</div> </div>
); );

View file

@ -2,8 +2,8 @@ import { useEffect, useState } from "react";
import IconComponent from "../../../components/genericIconComponent"; import IconComponent from "../../../components/genericIconComponent";
import { Textarea } from "../../../components/ui/textarea"; import { Textarea } from "../../../components/ui/textarea";
import { import {
chatInputPlaceholder, CHAT_INPUT_PLACEHOLDER,
chatInputPlaceholderSend, CHAT_INPUT_PLACEHOLDER_SEND,
} from "../../../constants/constants"; } from "../../../constants/constants";
import useFlowsManagerStore from "../../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../../stores/flowsManagerStore";
import { chatInputType } from "../../../types/components"; import { chatInputType } from "../../../types/components";
@ -84,7 +84,7 @@ export default function ChatInput({
"form-modal-lockchat" "form-modal-lockchat"
)} )}
placeholder={ placeholder={
noInput ? chatInputPlaceholder : chatInputPlaceholderSend noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
} }
/> />
<div className="form-modal-send-icon-position"> <div className="form-modal-send-icon-position">

View file

@ -50,7 +50,7 @@ export function CodeBlock({ language, value }: Props): JSX.Element {
<div className="flex items-center"> <div className="flex items-center">
<button className="code-block-modal-button" onClick={copyToClipboard}> <button className="code-block-modal-button" onClick={copyToClipboard}>
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />} {isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
{isCopied ? "Copied!" : "Copy code"} {isCopied ? "Copied!" : "Copy Code"}
</button> </button>
<button className="code-block-modal-button" onClick={downloadAsFile}> <button className="code-block-modal-button" onClick={downloadAsFile}>
<IconDownload size={18} /> <IconDownload size={18} />

View file

@ -108,7 +108,7 @@ export default function ChatMessage({
chat.isSend ? "" : " " chat.isSend ? "" : " "
)} )}
> >
<div className={classNames("form-modal-chatbot-icon ")}> <div className={classNames("form-modal-chatbot-icon")}>
{!chat.isSend ? ( {!chat.isSend ? (
<div className="form-modal-chat-image"> <div className="form-modal-chat-image">
<div className="form-modal-chat-bot-icon "> <div className="form-modal-chat-bot-icon ">
@ -134,7 +134,7 @@ export default function ChatMessage({
)} )}
</div> </div>
{!chat.isSend ? ( {!chat.isSend ? (
<div className="form-modal-chat-text-position"> <div className="form-modal-chat-text-position flex-grow min-w-96">
<div className="form-modal-chat-text"> <div className="form-modal-chat-text">
{hidden && chat.thought && chat.thought !== "" && ( {hidden && chat.thought && chat.thought !== "" && (
<div <div
@ -155,9 +155,9 @@ export default function ChatMessage({
/> />
)} )}
{chat.thought && chat.thought !== "" && !hidden && <br></br>} {chat.thought && chat.thought !== "" && !hidden && <br></br>}
<div className="w-full"> <div className="w-full flex flex-col">
<div className="w-full dark:text-white"> <div className="w-full flex flex-col dark:text-white">
<div className="w-full"> <div className="w-full flex flex-col">
{useMemo( {useMemo(
() => () =>
chatMessage === "" && lockChat ? ( chatMessage === "" && lockChat ? (
@ -169,7 +169,7 @@ export default function ChatMessage({
<Markdown <Markdown
remarkPlugins={[remarkGfm, remarkMath]} remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax]} rehypePlugins={[rehypeMathjax]}
className="markdown prose min-w-full text-primary word-break-break-word className="markdown flex flex-col prose text-primary word-break-break-word
dark:prose-invert" dark:prose-invert"
components={{ components={{
pre({ node, ...props }) { pre({ node, ...props }) {

View file

@ -2,8 +2,8 @@ import { useEffect, useRef, useState } from "react";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import { NOCHATOUTPUT_NOTICE_ALERT } from "../../constants/alerts_constants"; import { NOCHATOUTPUT_NOTICE_ALERT } from "../../constants/alerts_constants";
import { import {
chatFirstInitialText, CHAT_FIRST_INITIAL_TEXT,
chatSecondInitialText, CHAT_SECOND_INITIAL_TEXT,
} from "../../constants/constants"; } from "../../constants/constants";
import { deleteFlowPool } from "../../controllers/API"; import { deleteFlowPool } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
@ -182,14 +182,14 @@ export default function NewChatView({
<br /> <br />
<div className="langflow-chat-desc"> <div className="langflow-chat-desc">
<span className="langflow-chat-desc-span"> <span className="langflow-chat-desc-span">
{chatFirstInitialText}{" "} {CHAT_FIRST_INITIAL_TEXT}{" "}
<span> <span>
<IconComponent <IconComponent
name="MessageSquare" name="MessageSquare"
className="mx-1 inline h-5 w-5 animate-bounce " className="mx-1 inline h-5 w-5 animate-bounce "
/> />
</span>{" "} </span>{" "}
{chatSecondInitialText} {CHAT_SECOND_INITIAL_TEXT}
</span> </span>
</div> </div>
</div> </div>

View file

@ -1,5 +1,5 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { editTextModalTitle } from "../../constants/constants"; import { EDIT_TEXT_MODAL_TITLE } from "../../constants/constants";
import { TypeModal } from "../../constants/enums"; import { TypeModal } from "../../constants/enums";
import GenericModal from "../../modals/genericModal"; import GenericModal from "../../modals/genericModal";
import { TextAreaComponentType } from "../../types/components"; import { TextAreaComponentType } from "../../types/components";
@ -38,7 +38,7 @@ export default function TextAreaComponent({
<GenericModal <GenericModal
type={TypeModal.TEXT} type={TypeModal.TEXT}
buttonText="Finish Editing" buttonText="Finish Editing"
modalTitle={editTextModalTitle} modalTitle={EDIT_TEXT_MODAL_TITLE}
value={value} value={value}
setValue={(value: string) => { setValue={(value: string) => {
onChange(value); onChange(value);

View file

@ -7,6 +7,7 @@ export default function ToggleShadComponent({
disabled, disabled,
size, size,
id = "", id = "",
editNode = false,
}: ToggleComponentType): JSX.Element { }: ToggleComponentType): JSX.Element {
let scaleX, scaleY; let scaleX, scaleY;
switch (size) { switch (size) {
@ -31,6 +32,7 @@ export default function ToggleShadComponent({
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}> <div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
<Switch <Switch
id={id} id={id}
data-testid={id}
style={{ style={{
transform: `scaleX(${scaleX}) scaleY(${scaleY})`, transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
}} }}

View file

@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full", "flex flex-col fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
className className
)} )}
{...props} {...props}

View file

@ -0,0 +1,51 @@
import IconComponent from "../../components/genericIconComponent";
import { NodeDataType } from "../../types/flow";
import { cn } from "../../utils/utils";
function RefreshButton({
isLoading,
disabled,
name,
data,
handleUpdateValues,
className,
id,
}: {
isLoading: boolean;
disabled: boolean;
name: string;
data: NodeDataType;
className?: string;
handleUpdateValues: (name: string, data: NodeDataType) => void;
id: string;
}) {
const handleClick = async () => {
if (disabled) return;
handleUpdateValues(name, data);
};
const classNames = cn(
className,
disabled ? "cursor-not-allowed" : "cursor-pointer"
);
// icon class name should take into account the disabled state and the loading state
const disabledIconTextClass = disabled ? "text-muted-foreground" : "";
const iconClassName = cn(
"h-4 w-4",
isLoading ? "animate-spin" : "animate-wiggle",
disabledIconTextClass
);
return (
<button className={classNames} onClick={handleClick} id={id}>
<IconComponent
name={isLoading ? "Loader2" : "RefreshCcw"}
className={iconClassName}
id={id + "-icon"}
/>
</button>
);
}
export { RefreshButton };

View file

@ -56,3 +56,6 @@ export const USER_ADD_SUCCESS_ALERT = "Success! New user added!";
export const DEL_KEY_SUCCESS_ALERT = "Success! Key deleted!"; export const DEL_KEY_SUCCESS_ALERT = "Success! Key deleted!";
export const FLOW_BUILD_SUCCESS_ALERT = `Flow built successfully`; export const FLOW_BUILD_SUCCESS_ALERT = `Flow built successfully`;
export const SAVE_SUCCESS_ALERT = "Changes saved successfully!"; export const SAVE_SUCCESS_ALERT = "Changes saved successfully!";
// Generic Node

View file

@ -25,10 +25,18 @@ export const INVALID_CHARACTERS = [
/** /**
* regex to highlight the variables in the text * regex to highlight the variables in the text
* @constant * @constant regexHighlight
* @type {RegExp}
* @default
* @example
* {{variable}} or {variable}
* @returns {RegExp}
* @description
* This regex is used to highlight the variables in the text.
* It matches the variables in the text that are between {{}} or {}.
*/ */
export const regexHighlight = /\{([^}]+)\}/g; export const regexHighlight = /\{\{(.*?)\}\}|\{([^{}]+)\}/g;
export const specialCharsRegex = /[!@#$%^&*()\-_=+[\]{}|;:'",.<>/?\\`´]/; export const specialCharsRegex = /[!@#$%^&*()\-_=+[\]{}|;:'",.<>/?\\`´]/;
export const programmingLanguages: languageMap = { export const programmingLanguages: languageMap = {
@ -683,38 +691,39 @@ export const priorityFields = new Set(["code", "template"]);
export const INPUT_TYPES = new Set(["ChatInput", "TextInput"]); export const INPUT_TYPES = new Set(["ChatInput", "TextInput"]);
export const OUTPUT_TYPES = new Set(["ChatOutput", "TextOutput"]); export const OUTPUT_TYPES = new Set(["ChatOutput", "TextOutput"]);
export const chatFirstInitialText = export const CHAT_FIRST_INITIAL_TEXT =
"Start a conversation and click the agent's thoughts"; "Start a conversation and click the agent's thoughts";
export const chatSecondInitialText = "to inspect the chaining process."; export const CHAT_SECOND_INITIAL_TEXT = "to inspect the chaining process.";
export const zeroNotifications = "No new notifications"; export const ZERO_NOTIFICATIONS = "No new notifications";
export const successBuild = "Built sucessfully ✨"; export const SUCCESS_BUILD = "Built sucessfully ✨";
export const alertSaveWApi = export const ALERT_SAVE_WITH_API =
"Caution: Uncheck this box only removes API keys from fields specifically designated for API keys."; "Caution: Uncheck this box only removes API keys from fields specifically designated for API keys.";
export const saveWApiCheckbox = "Save with my API keys"; export const SAVE_WITH_API_CHECKBOX = "Save with my API keys";
export const editTextModalTitle = "Edit Text"; export const EDIT_TEXT_MODAL_TITLE = "Edit Text";
export const editTextPlaceholder = "Type message here."; export const EDIT_TEXT_PLACEHOLDER = "Type message here.";
export const inputHandleHover = "Avaliable input components:"; export const INPUT_HANDLER_HOVER = "Avaliable input components:";
export const outputHandleHover = "Avaliable output components:"; export const OUTPUT_HANDLER_HOVER = "Avaliable output components:";
export const textInputModalTitle = "Text Inputs"; export const TEXT_INPUT_MODAL_TITLE = "Text Inputs";
export const outputsModalTitle = "Text Outputs"; export const OUTPUTS_MODAL_TITLE = "Text Outputs";
export const langflowChatTitle = "Langflow Chat"; export const LANGFLOW_CHAT_TITLE = "Langflow Chat";
export const chatInputPlaceholder = export const CHAT_INPUT_PLACEHOLDER =
"No chat input variables found. Click to run your flow."; "No chat input variables found. Click to run your flow.";
export const chatInputPlaceholderSend = "Send a message..."; export const CHAT_INPUT_PLACEHOLDER_SEND = "Send a message...";
export const editCodeTitle = "Edit Code"; export const EDIT_CODE_TITLE = "Edit Code";
export const myCollectionDesc = export const MY_COLLECTION_DESC =
"Manage your personal projects. Download or upload your collection."; "Manage your personal projects. Download and upload entire collections.";
export const storeDesc = "Search flows and components from the community."; export const STORE_DESC = "Explore community-shared flows and components.";
export const storeTitle = "Langflow Store"; export const STORE_TITLE = "Langflow Store";
export const noApi = "You don't have an API key. "; export const NO_API_KEY = "You don't have an API key. ";
export const insertApi = "Insert your Langflow API key."; export const INSERT_API_KEY = "Insert your Langflow API key.";
export const invalidApi = "Your API key is not valid. "; export const INVALID_API_KEY = "Your API key is not valid. ";
export const createApi = `Don’t have an API key? Sign up at`; export const CREATE_API_KEY = `Don’t have an API key? Sign up at`;
export const statusBuild = "Build to validate status."; export const STATUS_BUILD = "Build to validate status.";
export const statusBuilding = "Building..."; export const STATUS_BUILDING = "Building...";
export const savedHover = "Last saved at "; export const SAVED_HOVER = "Last saved at ";
export const RUN_TIMESTAMP_PREFIX = "Last Run: ";

View file

@ -856,13 +856,16 @@ export async function requestLogout() {
export async function getVerticesOrder( export async function getVerticesOrder(
flowId: string, flowId: string,
nodeId?: string | null startNodeId?: string | null,
stopNodeId?: string | null
): Promise<AxiosResponse<VerticesOrderTypeAPI>> { ): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
// nodeId is optional and is a query parameter // nodeId is optional and is a query parameter
// if nodeId is not provided, the API will return all vertices // if nodeId is not provided, the API will return all vertices
const config = {}; const config = {};
if (nodeId) { if (stopNodeId) {
config["params"] = { component_id: nodeId }; config["params"] = { stop_component_id: stopNodeId };
} else if (startNodeId) {
config["params"] = { start_component_id: startNodeId };
} }
return await api.get(`${BASE_URL_API}build/${flowId}/vertices`, config); return await api.get(`${BASE_URL_API}build/${flowId}/vertices`, config);
} }
@ -872,6 +875,7 @@ export async function postBuildVertex(
vertexId: string, vertexId: string,
input_value: string input_value: string
): Promise<AxiosResponse<VertexBuildTypeAPI>> { ): Promise<AxiosResponse<VertexBuildTypeAPI>> {
// input_value is optional and is a query parameter
return await api.post( return await api.post(
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`, `${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
input_value ? { inputs: { input_value: input_value } } : undefined input_value ? { inputs: { input_value: input_value } } : undefined

View file

@ -89,6 +89,7 @@ function ConfirmationModal({
setModalOpen(false); setModalOpen(false);
onConfirm(index, data); onConfirm(index, data);
}} }}
data-testid="replace-button"
> >
{confirmationText} {confirmationText}
</Button> </Button>

View file

@ -35,9 +35,9 @@ export default function DeleteConfirmationModal({
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<span> <span>
Are you sure you want to delete this {description ?? "component"}? Confirm deletion of {description ?? "component"}?
<br></br> <br></br>
This action cannot be undone. Note: This action is irreversible.
</span> </span>
<DialogFooter> <DialogFooter>
<DialogClose> <DialogClose>

View file

@ -165,7 +165,8 @@ const EditNodeModal = forwardRef(
) )
) ?? false; ) ?? false;
return ( return (
<TableRow key={index} className="h-10"> <TableRow key={index} className={"h-10 " + ((templateParam==="code" && myData.node?.template[templateParam].type==="code") || (templateParam.includes("code") && myData.node?.template[templateParam].proxy) ? " hidden " : "")
}>
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3"> <TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
<ShadTooltip <ShadTooltip
content={ content={
@ -242,7 +243,11 @@ const EditNodeModal = forwardRef(
/> />
) : ( ) : (
<InputComponent <InputComponent
id={"input-" + index} id={
"input-" +
myData.node.template[templateParam]
.name
}
editNode={true} editNode={true}
disabled={disabled} disabled={disabled}
password={ password={
@ -338,7 +343,10 @@ const EditNodeModal = forwardRef(
<div className="ml-auto"> <div className="ml-auto">
{" "} {" "}
<ToggleShadComponent <ToggleShadComponent
id={"toggle-edit-" + index} id={
"toggle-edit-" +
myData.node.template[templateParam].name
}
disabled={disabled} disabled={disabled}
enabled={ enabled={
myData.node.template[templateParam] myData.node.template[templateParam]
@ -351,6 +359,7 @@ const EditNodeModal = forwardRef(
); );
}} }}
size="small" size="small"
editNode={true}
/> />
</div> </div>
) : myData.node?.template[templateParam] ) : myData.node?.template[templateParam]
@ -391,14 +400,20 @@ const EditNodeModal = forwardRef(
myData.node.template[templateParam] myData.node.template[templateParam]
.value ?? "Choose an option" .value ?? "Choose an option"
} }
id={"dropdown-edit-" + index} id={
"dropdown-edit-" +
myData.node.template[templateParam].name
}
></Dropdown> ></Dropdown>
</div> </div>
) : myData.node?.template[templateParam] ) : myData.node?.template[templateParam]
.type === "int" ? ( .type === "int" ? (
<div className="mx-auto"> <div className="mx-auto">
<IntComponent <IntComponent
id={"edit-int-input-" + index} id={
"edit-int-input-" +
myData.node.template[templateParam].name
}
disabled={disabled} disabled={disabled}
editNode={true} editNode={true}
value={ value={
@ -493,7 +508,10 @@ const EditNodeModal = forwardRef(
onChange={(value: string | string[]) => { onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam); handleOnNewValue(value, templateParam);
}} }}
id={"code-area-edit" + index} id={
"code-area-edit" +
myData.node.template[templateParam].name
}
/> />
</div> </div>
) : myData.node?.template[templateParam] ) : myData.node?.template[templateParam]
@ -519,6 +537,7 @@ const EditNodeModal = forwardRef(
}} }}
disabled={disabled} disabled={disabled}
size="small" size="small"
editNode={true}
/> />
</div> </div>
</TableCell> </TableCell>

View file

@ -8,10 +8,10 @@ import {
API_SUCCESS_ALERT, API_SUCCESS_ALERT,
} from "../../constants/alerts_constants"; } from "../../constants/alerts_constants";
import { import {
createApi, CREATE_API_KEY,
insertApi, INSERT_API_KEY,
invalidApi, INVALID_API_KEY,
noApi, NO_API_KEY,
} from "../../constants/constants"; } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { addApiKeyStore } from "../../controllers/API"; import { addApiKeyStore } from "../../controllers/API";
@ -68,8 +68,11 @@ export default function StoreApiKeyModal({
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger> <BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header <BaseModal.Header
description={ description={
(hasApiKey && !validApiKey ? invalidApi : !hasApiKey ? noApi : "") + (hasApiKey && !validApiKey
insertApi ? INVALID_API_KEY
: !hasApiKey
? NO_API_KEY
: "") + INSERT_API_KEY
} }
> >
<span className="pr-2">API Key</span> <span className="pr-2">API Key</span>
@ -104,7 +107,7 @@ export default function StoreApiKeyModal({
</div> </div>
<div className="flex items-end justify-between"> <div className="flex items-end justify-between">
<span className="pr-1 text-xs text-muted-foreground"> <span className="pr-1 text-xs text-muted-foreground">
{createApi}{" "} {CREATE_API_KEY}{" "}
<a <a
className="text-high-indigo underline" className="text-high-indigo underline"
href="https://langflow.store/" href="https://langflow.store/"

View file

@ -126,7 +126,7 @@ function BaseModal({
minWidth = "min-w-[60vw]"; minWidth = "min-w-[60vw]";
break; break;
case "large": case "large":
minWidth = "min-w-[80vw]"; minWidth = "min-w-[85vw]";
height = "h-[80vh]"; height = "h-[80vh]";
break; break;
case "large-thin": case "large-thin":

View file

@ -18,7 +18,7 @@ import {
} from "../../constants/alerts_constants"; } from "../../constants/alerts_constants";
import { import {
CODE_PROMPT_DIALOG_SUBTITLE, CODE_PROMPT_DIALOG_SUBTITLE,
editCodeTitle, EDIT_CODE_TITLE,
} from "../../constants/constants"; } from "../../constants/constants";
import { postCustomComponent, postValidateCode } from "../../controllers/API"; import { postCustomComponent, postValidateCode } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
@ -35,11 +35,14 @@ export default function CodeAreaModal({
children, children,
dynamic, dynamic,
readonly = false, readonly = false,
openModal, open: myOpen,
selected, setOpen: mySetOpen,
setOpenModal,
}: codeAreaModalPropsType): JSX.Element { }: codeAreaModalPropsType): JSX.Element {
const [code, setCode] = useState(value); const [code, setCode] = useState(value);
const [open, setOpen] =
mySetOpen !== undefined && myOpen !== undefined
? [myOpen, mySetOpen]
: useState(false);
const dark = useDarkStore((state) => state.dark); const dark = useDarkStore((state) => state.dark);
const unselectAll = useFlowStore((state) => state.unselectAll); const unselectAll = useFlowStore((state) => state.unselectAll);
const [height, setHeight] = useState<string | null>(null); const [height, setHeight] = useState<string | null>(null);
@ -48,7 +51,6 @@ export default function CodeAreaModal({
const [error, setError] = useState<{ const [error, setError] = useState<{
detail: { error: string | undefined; traceback: string | undefined }; detail: { error: string | undefined; traceback: string | undefined };
} | null>(null); } | null>(null);
const [open, setOpen] = useState(false);
const nodes = useFlowStore((state) => state.nodes); const nodes = useFlowStore((state) => state.nodes);
useEffect(() => { useEffect(() => {
@ -59,10 +61,6 @@ export default function CodeAreaModal({
} }
}, []); }, []);
useEffect(() => {
if (openModal) setOpen(true);
}, [openModal]);
function processNonDynamicField() { function processNonDynamicField() {
postValidateCode(code) postValidateCode(code)
.then((apiReturn) => { .then((apiReturn) => {
@ -154,7 +152,7 @@ export default function CodeAreaModal({
<BaseModal open={open} setOpen={setOpen}> <BaseModal open={open} setOpen={setOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger> <BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={CODE_PROMPT_DIALOG_SUBTITLE}> <BaseModal.Header description={CODE_PROMPT_DIALOG_SUBTITLE}>
<span className="pr-2"> {editCodeTitle} </span> <span className="pr-2"> {EDIT_CODE_TITLE} </span>
<IconComponent <IconComponent
name="prompts" name="prompts"
className="h-6 w-6 pl-1 text-primary " className="h-6 w-6 pl-1 text-primary "

View file

@ -5,9 +5,9 @@ import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox"; import { Checkbox } from "../../components/ui/checkbox";
import { API_WARNING_NOTICE_ALERT } from "../../constants/alerts_constants"; import { API_WARNING_NOTICE_ALERT } from "../../constants/alerts_constants";
import { import {
ALERT_SAVE_WITH_API,
EXPORT_DIALOG_SUBTITLE, EXPORT_DIALOG_SUBTITLE,
alertSaveWApi, SAVE_WITH_API_CHECKBOX,
saveWApiCheckbox,
} from "../../constants/constants"; } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import { useDarkStore } from "../../stores/darkStore"; import { useDarkStore } from "../../stores/darkStore";
@ -56,10 +56,12 @@ const ExportModal = forwardRef(
}} }}
/> />
<label htmlFor="terms" className="export-modal-save-api text-sm "> <label htmlFor="terms" className="export-modal-save-api text-sm ">
{saveWApiCheckbox} {SAVE_WITH_API_CHECKBOX}
</label> </label>
</div> </div>
<span className=" text-xs text-destructive ">{alertSaveWApi}</span> <span className=" text-xs text-destructive ">
{ALERT_SAVE_WITH_API}
</span>
</BaseModal.Content> </BaseModal.Content>
<BaseModal.Footer> <BaseModal.Footer>

View file

@ -2,8 +2,8 @@ import { useEffect } from "react";
import IconComponent from "../../../components/genericIconComponent"; import IconComponent from "../../../components/genericIconComponent";
import { Textarea } from "../../../components/ui/textarea"; import { Textarea } from "../../../components/ui/textarea";
import { import {
chatInputPlaceholder, CHAT_INPUT_PLACEHOLDER,
chatInputPlaceholderSend, CHAT_INPUT_PLACEHOLDER_SEND,
} from "../../../constants/constants"; } from "../../../constants/constants";
import { chatInputType } from "../../../types/components"; import { chatInputType } from "../../../types/components";
import { classNames } from "../../../utils/utils"; import { classNames } from "../../../utils/utils";
@ -55,7 +55,7 @@ export default function ChatInput({
? "Thinking..." ? "Thinking..."
: typeof chatValue === "object" && : typeof chatValue === "object" &&
Object.keys(chatValue)?.length === 0 Object.keys(chatValue)?.length === 0
? chatInputPlaceholder ? CHAT_INPUT_PLACEHOLDER
: chatValue : chatValue
} }
onChange={(event): void => { onChange={(event): void => {
@ -70,7 +70,9 @@ export default function ChatInput({
"form-modal-lockchat" "form-modal-lockchat"
)} )}
placeholder={noInput ? chatInputPlaceholder : chatInputPlaceholderSend} placeholder={
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
}
/> />
<div className="form-modal-send-icon-position"> <div className="form-modal-send-icon-position">
<button <button

View file

@ -50,7 +50,7 @@ export function CodeBlock({ language, value }: Props): JSX.Element {
<div className="flex items-center"> <div className="flex items-center">
<button className="code-block-modal-button" onClick={copyToClipboard}> <button className="code-block-modal-button" onClick={copyToClipboard}>
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />} {isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
{isCopied ? "Copied!" : "Copy code"} {isCopied ? "Copied!" : "Copy Code"}
</button> </button>
<button className="code-block-modal-button" onClick={downloadAsFile}> <button className="code-block-modal-button" onClick={downloadAsFile}>
<IconDownload size={18} /> <IconDownload size={18} />

View file

@ -26,10 +26,10 @@ import {
MSG_ERROR_ALERT, MSG_ERROR_ALERT,
} from "../../constants/alerts_constants"; } from "../../constants/alerts_constants";
import { import {
CHAT_FIRST_INITIAL_TEXT,
CHAT_FORM_DIALOG_SUBTITLE, CHAT_FORM_DIALOG_SUBTITLE,
chatFirstInitialText, CHAT_SECOND_INITIAL_TEXT,
chatSecondInitialText, LANGFLOW_CHAT_TITLE,
langflowChatTitle,
} from "../../constants/constants"; } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { getBuildStatus } from "../../controllers/API"; import { getBuildStatus } from "../../controllers/API";
@ -594,20 +594,20 @@ export default function FormModal({
<span> <span>
👋{" "} 👋{" "}
<span className="langflow-chat-span"> <span className="langflow-chat-span">
{langflowChatTitle} {LANGFLOW_CHAT_TITLE}
</span> </span>
</span> </span>
<br /> <br />
<div className="langflow-chat-desc"> <div className="langflow-chat-desc">
<span className="langflow-chat-desc-span"> <span className="langflow-chat-desc-span">
{chatFirstInitialText}{" "} {CHAT_FIRST_INITIAL_TEXT}{" "}
<span> <span>
<IconComponent <IconComponent
name="MessageSquare" name="MessageSquare"
className="mx-1 inline h-5 w-5 animate-bounce " className="mx-1 inline h-5 w-5 animate-bounce "
/> />
</span>{" "} </span>{" "}
{chatSecondInitialText} {CHAT_SECOND_INITIAL_TEXT}
</span> </span>
</div> </div>
</div> </div>

View file

@ -12,11 +12,11 @@ import {
TEMP_NOTICE_ALERT, TEMP_NOTICE_ALERT,
} from "../../constants/alerts_constants"; } from "../../constants/alerts_constants";
import { import {
EDIT_TEXT_PLACEHOLDER,
INVALID_CHARACTERS, INVALID_CHARACTERS,
MAX_WORDS_HIGHLIGHT, MAX_WORDS_HIGHLIGHT,
PROMPT_DIALOG_SUBTITLE, PROMPT_DIALOG_SUBTITLE,
TEXT_DIALOG_SUBTITLE, TEXT_DIALOG_SUBTITLE,
editTextPlaceholder,
regexHighlight, regexHighlight,
} from "../../constants/constants"; } from "../../constants/constants";
import { TypeModal } from "../../constants/enums"; import { TypeModal } from "../../constants/enums";
@ -97,13 +97,23 @@ export default function GenericModal({
useEffect(() => { useEffect(() => {
setInputValue(value); setInputValue(value);
}, [value, modalOpen]); }, [value, modalOpen]);
const coloredContent = (inputValue || "") const coloredContent = (inputValue || "")
.replace(/</g, "&lt;") .replace(/</g, "&lt;")
.replace(/>/g, "&gt;") .replace(/>/g, "&gt;")
.replace(regexHighlight, varHighlightHTML({ name: "$1" })) .replace(regexHighlight, (match, p1, p2) => {
.replace(/\n/g, "<br />"); // Decide which group was matched. If p1 is not undefined, do nothing
// we don't want to change the text. If p2 is not undefined, then we
// have a variable, so we should highlight it.
// ! This will not work with multiline or indented json yet
if (p1 !== undefined) {
return match;
} else if (p2 !== undefined) {
return varHighlightHTML({ name: p2 });
}
return match;
})
.replace(/\n/g, "<br />");
function getClassByNumberLength(): string { function getClassByNumberLength(): string {
let sumOfCaracteres: number = 0; let sumOfCaracteres: number = 0;
wordsHighlight.forEach((element) => { wordsHighlight.forEach((element) => {
@ -159,7 +169,7 @@ export default function GenericModal({
setIsEdit(true); setIsEdit(true);
return setErrorData({ return setErrorData({
title: PROMPT_ERROR_ALERT, title: PROMPT_ERROR_ALERT,
list: [error.toString()], list: [error.response.data.detail ?? ""],
}); });
}); });
} }
@ -217,7 +227,7 @@ export default function GenericModal({
setInputValue(event.target.value); setInputValue(event.target.value);
checkVariables(event.target.value); checkVariables(event.target.value);
}} }}
placeholder={editTextPlaceholder} placeholder={EDIT_TEXT_PLACEHOLDER}
onKeyDown={(e) => { onKeyDown={(e) => {
handleKeyDown(e, inputValue, ""); handleKeyDown(e, inputValue, "");
}} }}
@ -239,7 +249,7 @@ export default function GenericModal({
onChange={(event) => { onChange={(event) => {
setInputValue(event.target.value); setInputValue(event.target.value);
}} }}
placeholder={editTextPlaceholder} placeholder={EDIT_TEXT_PLACEHOLDER}
onKeyDown={(e) => { onKeyDown={(e) => {
handleKeyDown(e, value, ""); handleKeyDown(e, value, "");
}} }}

View file

@ -48,7 +48,7 @@ export default function ShareModal({
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const [internalOpen, internalSetOpen] = useState(children ? false : true); const [internalOpen, internalSetOpen] = useState(children ? false : true);
const [openConfirmationModal, setOpenConfirmationModal] = useState(false); const [openConfirmationModal, setOpenConfirmationModal] = useState(false);
const nameComponent = is_component ? "component" : "flow"; const nameComponent = is_component ? "component" : "workflow";
const [tags, setTags] = useState<{ id: string; name: string }[]>([]); const [tags, setTags] = useState<{ id: string; name: string }[]>([]);
const [loadingTags, setLoadingTags] = useState<boolean>(false); const [loadingTags, setLoadingTags] = useState<boolean>(false);
@ -179,7 +179,7 @@ export default function ShareModal({
</span> </span>
<br></br> <br></br>
<span className=" text-xs text-destructive "> <span className=" text-xs text-destructive ">
Warning: This action cannot be undone. Note: This action is irreversible.
</span> </span>
</ConfirmationModal.Content> </ConfirmationModal.Content>
</ConfirmationModal> </ConfirmationModal>
@ -204,7 +204,7 @@ export default function ShareModal({
{children ? children : <></>} {children ? children : <></>}
</BaseModal.Trigger> </BaseModal.Trigger>
<BaseModal.Header <BaseModal.Header
description={`Share your ${nameComponent} to the Langflow Store.`} description={`Publish ${is_component ? "your component" : "workflow"} to the Langflow Store.`}
> >
<span className="pr-2">Share</span> <span className="pr-2">Share</span>
<IconComponent <IconComponent
@ -235,12 +235,11 @@ export default function ShareModal({
}} }}
/> />
<label htmlFor="public" className="export-modal-save-api text-sm "> <label htmlFor="public" className="export-modal-save-api text-sm ">
Make {nameComponent} public Set {nameComponent} status to public
</label> </label>
</div> </div>
<span className=" text-xs text-destructive "> <span className=" text-xs text-destructive ">
<b>Warning:</b> API keys in designated fields are removed when <b>Attention:</b> API keys in specified fields are automatically removed upon sharing.
sharing.
</span> </span>
</BaseModal.Content> </BaseModal.Content>

View file

@ -31,6 +31,7 @@ import {
getNodeId, getNodeId,
isValidConnection, isValidConnection,
reconnectEdges, reconnectEdges,
scapeJSONParse,
validateSelection, validateSelection,
} from "../../../../utils/reactflowUtils"; } from "../../../../utils/reactflowUtils";
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils"; import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
@ -320,6 +321,8 @@ export default function Page({
(oldEdge: Edge, newConnection: Connection) => { (oldEdge: Edge, newConnection: Connection) => {
if (isValidConnection(newConnection, nodes, edges)) { if (isValidConnection(newConnection, nodes, edges)) {
edgeUpdateSuccessful.current = true; edgeUpdateSuccessful.current = true;
oldEdge.data.targetHandle = scapeJSONParse(newConnection.targetHandle!);
oldEdge.data.sourceHandle = scapeJSONParse(newConnection.sourceHandle!);
setEdges((els) => updateEdge(oldEdge, newConnection, els)); setEdges((els) => updateEdge(oldEdge, newConnection, els));
} }
}, },

View file

@ -315,25 +315,6 @@ export default function NodeToolbarComponent({
}} }}
data-testid="code-button-modal" data-testid="code-button-modal"
> >
<div className="hidden">
<CodeAreaComponent
openModal={openModal}
setOpenModal={setOpenModal}
readonly={
data.node?.flow && data.node.template[name].dynamic
? true
: false
}
dynamic={data.node?.template[name].dynamic ?? false}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={false}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"code-input-node-toolbar-" + name}
selected={selected}
/>
</div>
<IconComponent name="TerminalSquare" className="h-4 w-4" /> <IconComponent name="TerminalSquare" className="h-4 w-4" />
</button> </button>
</ShadTooltip> </ShadTooltip>
@ -382,7 +363,7 @@ export default function NodeToolbarComponent({
</button> </button>
</ShadTooltip> </ShadTooltip>
<ShadTooltip content="Pin" side="top"> <ShadTooltip content="Freeze" side="top">
<button <button
className={classNames( 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" "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"
@ -402,10 +383,11 @@ export default function NodeToolbarComponent({
}} }}
> >
<IconComponent <IconComponent
name="Pin" name="Snowflake"
className={cn( className={cn(
"h-4 w-4 transition-all", "h-4 w-4 transition-all",
pinned ? "animate-wiggle fill-current" : "" // TODO UPDATE THIS COLOR TO BE A VARIABLE
pinned ? "animate-wiggle text-ice" : ""
)} )}
/> />
</button> </button>
@ -499,7 +481,7 @@ export default function NodeToolbarComponent({
value={"Share"} value={"Share"}
disabled={!hasApiKey || !validApiKey} disabled={!hasApiKey || !validApiKey}
> >
<div className="flex" data-testid="save-button-modal"> <div className="flex" data-testid="share-button-modal">
<IconComponent <IconComponent
name="Share3" name="Share3"
className="relative top-0.5 -m-1 mr-1 h-6 w-6" className="relative top-0.5 -m-1 mr-1 h-6 w-6"
@ -633,6 +615,26 @@ export default function NodeToolbarComponent({
is_component={true} is_component={true}
component={flowComponent!} component={flowComponent!}
/> />
{hasCode && (
<div className="hidden">
<CodeAreaComponent
open={openModal}
setOpen={setOpenModal}
readonly={
data.node?.flow && data.node.template[name].dynamic
? true
: false
}
dynamic={data.node?.template[name].dynamic ?? false}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={false}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"code-input-node-toolbar-" + name}
/>
</div>
)}
</span> </span>
</div> </div>
</> </>

View file

@ -27,7 +27,7 @@ export default function ComponentsComponent({
const flows = useFlowsManagerStore((state) => state.flows); const flows = useFlowsManagerStore((state) => state.flows);
const setSuccessData = useAlertStore((state) => state.setSuccessData); const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(20);
const [pageIndex, setPageIndex] = useState(1); const [pageIndex, setPageIndex] = useState(1);
const [loadingScreen, setLoadingScreen] = useState(true); const [loadingScreen, setLoadingScreen] = useState(true);
@ -96,7 +96,7 @@ export default function ComponentsComponent({
function resetFilter() { function resetFilter() {
setPageIndex(1); setPageIndex(1);
setPageSize(10); setPageSize(20);
} }
return ( return (

View file

@ -8,8 +8,8 @@ import SidebarNav from "../../components/sidebarComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { CONSOLE_ERROR_MSG } from "../../constants/alerts_constants"; import { CONSOLE_ERROR_MSG } from "../../constants/alerts_constants";
import { import {
MY_COLLECTION_DESC,
USER_PROJECTS_HEADER, USER_PROJECTS_HEADER,
myCollectionDesc,
} from "../../constants/constants"; } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore";
@ -75,7 +75,7 @@ export default function HomePage(): JSX.Element {
return ( return (
<PageLayout <PageLayout
title={USER_PROJECTS_HEADER} title={USER_PROJECTS_HEADER}
description={myCollectionDesc} description={MY_COLLECTION_DESC}
button={ button={
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button

View file

@ -26,7 +26,7 @@ import {
INVALID_API_ERROR_ALERT, INVALID_API_ERROR_ALERT,
NOAPI_ERROR_ALERT, NOAPI_ERROR_ALERT,
} from "../../constants/alerts_constants"; } from "../../constants/alerts_constants";
import { storeDesc, storeTitle } from "../../constants/constants"; import { STORE_DESC, STORE_TITLE } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { getStoreComponents, getStoreTags } from "../../controllers/API"; import { getStoreComponents, getStoreTags } from "../../controllers/API";
import StoreApiKeyModal from "../../modals/StoreApiKeyModal"; import StoreApiKeyModal from "../../modals/StoreApiKeyModal";
@ -174,8 +174,8 @@ export default function StorePage(): JSX.Element {
return ( return (
<PageLayout <PageLayout
betaIcon betaIcon
title={storeTitle} title={STORE_TITLE}
description={storeDesc} description={STORE_DESC}
button={ button={
<> <>
{StoreApiKeyModal && ( {StoreApiKeyModal && (

View file

@ -187,10 +187,12 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
typeof change === "function" typeof change === "function"
? change(get().nodes.find((node) => node.id === id)!) ? change(get().nodes.find((node) => node.id === id)!)
: change; : change;
get().setNodes((oldNodes) => get().setNodes((oldNodes) =>
oldNodes.map((node) => { oldNodes.map((node) => {
if (node.id === id) { if (node.id === id) {
if((node.data as NodeDataType).node?.pinned){
(newChange.data as NodeDataType).node!.pinned = false;
}
return newChange; return newChange;
} }
return node; return node;
@ -223,12 +225,10 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
selection.nodes.some((node) => node.data.type === "ChatInput") && selection.nodes.some((node) => node.data.type === "ChatInput") &&
checkChatInput(get().nodes) checkChatInput(get().nodes)
) { ) {
useAlertStore useAlertStore.getState().setErrorData({
.getState() title: "Error pasting components",
.setErrorData({ list: ["You can only have one ChatInput component in the flow"],
title: "Error pasting components", });
list: ["You can only have one ChatInput component in the flow"],
});
return; return;
} }
let minimumX = Infinity; let minimumX = Infinity;
@ -418,10 +418,12 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
}); });
}, },
buildFlow: async ({ buildFlow: async ({
nodeId, startNodeId,
stopNodeId,
input_value, input_value,
}: { }: {
nodeId?: string; startNodeId?: string;
stopNodeId?: string;
input_value?: string; input_value?: string;
}) => { }) => {
get().setIsBuilding(true); get().setIsBuilding(true);
@ -446,25 +448,52 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
function handleBuildUpdate( function handleBuildUpdate(
vertexBuildData: VertexBuildTypeAPI, vertexBuildData: VertexBuildTypeAPI,
status: BuildStatus, status: BuildStatus,
buildId: string runId: string
) { ) {
if (vertexBuildData && vertexBuildData.inactive_vertices) { if (vertexBuildData && vertexBuildData.inactive_vertices) {
get().removeFromVerticesBuild(vertexBuildData.inactive_vertices); get().removeFromVerticesBuild(vertexBuildData.inactive_vertices);
} }
if (vertexBuildData.next_vertices_ids) {
// next_vertices_ids is a list of vertices that are going to be built next
// verticesLayers is a list of list of vertices ids, where each list is a layer of vertices
// we want to add a new layer (next_vertices_ids) to the list of layers (verticesLayers)
// and the values of next_vertices_ids to the list of vertices ids (verticesIds)
const newLayers = [
...get().verticesBuild!.verticesLayers,
vertexBuildData.next_vertices_ids,
];
const newIds = [
...get().verticesBuild!.verticesIds,
...vertexBuildData.next_vertices_ids,
];
get().updateVerticesBuild({
verticesIds: newIds,
verticesLayers: newLayers,
runId: runId,
});
get().updateBuildStatus(
vertexBuildData.next_vertices_ids,
BuildStatus.TO_BUILD
);
}
get().addDataToFlowPool( get().addDataToFlowPool(
{ ...vertexBuildData, buildId }, { ...vertexBuildData, buildId: runId },
vertexBuildData.id vertexBuildData.id
); );
useFlowStore.getState().updateBuildStatus([vertexBuildData.id], status); useFlowStore.getState().updateBuildStatus([vertexBuildData.id], status);
} }
await buildVertices({ await buildVertices({
input_value, input_value,
flowId: currentFlow!.id, flowId: currentFlow!.id,
nodeId, startNodeId,
stopNodeId,
onGetOrderSuccess: () => { onGetOrderSuccess: () => {
setNoticeData({ title: "Running components" }); setNoticeData({ title: "Running components" });
}, },
onBuildComplete: () => { onBuildComplete: () => {
const nodeId = startNodeId || stopNodeId;
if (nodeId) { if (nodeId) {
setSuccessData({ setSuccessData({
title: `${ title: `${
@ -481,12 +510,14 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
onBuildError: (title, list, idList) => { onBuildError: (title, list, idList) => {
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILT); useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILT);
setErrorData({ list, title }); setErrorData({ list, title });
get().setIsBuilding(false);
}, },
onBuildStart: (idList) => { onBuildStart: (idList) => {
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING); useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING);
}, },
validateNodes: validateSubgraph, validateNodes: validateSubgraph,
}); });
get().setIsBuilding(false);
get().revertBuiltStatusFromBuilding(); get().revertBuiltStatusFromBuilding();
}, },
getFlow: () => { getFlow: () => {
@ -499,11 +530,11 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
updateVerticesBuild: ( updateVerticesBuild: (
vertices: { vertices: {
verticesIds: string[]; verticesIds: string[];
verticesOrder: string[][];
verticesLayers: string[][]; verticesLayers: string[][];
runId: string; runId: string;
} | null } | null
) => { ) => {
console.log("updateVerticesBuild", vertices);
set({ verticesBuild: vertices }); set({ verticesBuild: vertices });
}, },
verticesBuild: null, verticesBuild: null,
@ -520,17 +551,25 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
}); });
}, },
updateBuildStatus: (nodeIdList: string[], status: BuildStatus) => { updateBuildStatus: (nodeIdList: string[], status: BuildStatus) => {
console.log("updateBuildStatus", nodeIdList, status);
const newFlowBuildStatus = { ...get().flowBuildStatus }; const newFlowBuildStatus = { ...get().flowBuildStatus };
nodeIdList.forEach((id) => { nodeIdList.forEach((id) => {
newFlowBuildStatus[id] = status; newFlowBuildStatus[id] = {
status,
};
if (status == BuildStatus.BUILT) {
const timestamp_string = new Date(Date.now()).toLocaleString();
newFlowBuildStatus[id].timestamp = timestamp_string;
}
console.log("updateBuildStatus", newFlowBuildStatus);
}); });
set({ flowBuildStatus: newFlowBuildStatus }); set({ flowBuildStatus: newFlowBuildStatus });
}, },
revertBuiltStatusFromBuilding: () => { revertBuiltStatusFromBuilding: () => {
const newFlowBuildStatus = { ...get().flowBuildStatus }; const newFlowBuildStatus = { ...get().flowBuildStatus };
Object.keys(newFlowBuildStatus).forEach((id) => { Object.keys(newFlowBuildStatus).forEach((id) => {
if (newFlowBuildStatus[id] === BuildStatus.BUILDING) { if (newFlowBuildStatus[id].status === BuildStatus.BUILDING) {
newFlowBuildStatus[id] = BuildStatus.BUILT; newFlowBuildStatus[id].status = BuildStatus.BUILT;
} }
}); });
}, },

View file

@ -845,13 +845,13 @@
} }
.api-modal-tabs { .api-modal-tabs {
@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; @apply flex h-full flex-col overflow-hidden rounded-md border bg-muted text-center;
} }
.api-modal-tablist-div { .api-modal-tablist-div {
@apply flex items-center justify-between px-2 py-2; @apply flex items-center justify-between px-2 py-2;
} }
.api-modal-tabs-content { .api-modal-tabs-content {
@apply -mt-1 h-full w-full px-4 pb-4; @apply -mt-1 h-full w-full px-4 pb-4;
} }
.api-modal-accordion-display { .api-modal-accordion-display {
@apply mt-2 flex h-full w-full; @apply mt-2 flex h-full w-full;
@ -902,7 +902,7 @@
@apply flex-max-width px-2 py-6 pl-4 pr-9; @apply flex-max-width px-2 py-6 pl-4 pr-9;
} }
.form-modal-chatbot-icon { .form-modal-chatbot-icon {
@apply mb-3 ml-3 mr-6 mt-1; @apply flex flex-col mb-3 ml-3 mr-6 mt-1;
} }
.form-modal-chat-image { .form-modal-chat-image {
@apply flex flex-col items-center gap-1; @apply flex flex-col items-center gap-1;

View file

@ -27,6 +27,7 @@
--radius: 0.5rem; --radius: 0.5rem;
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */ --ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--round-btn-shadow: #00000063; --round-btn-shadow: #00000063;
--ice: #31a3cc;
--error-background: #fef2f2; --error-background: #fef2f2;
--error-foreground: #991b1b; --error-foreground: #991b1b;
@ -67,6 +68,7 @@
.dark { .dark {
--background: 224 35% 7.5%; /* hsl(224 40% 10%) */ --background: 224 35% 7.5%; /* hsl(224 40% 10%) */
--foreground: 213 31% 80%; /* hsl(213 31% 91%) */ --foreground: 213 31% 80%; /* hsl(213 31% 91%) */
--ice: #60A5FA;
--muted: 223 27% 11%; /* hsl(223 27% 11%) */ --muted: 223 27% 11%; /* hsl(223 27% 11%) */
--muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */ --muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */

View file

@ -54,6 +54,7 @@ export type TemplateVariableType = {
input_types?: Array<string>; input_types?: Array<string>;
display_name?: string; display_name?: string;
name?: string; name?: string;
refresh?: boolean;
[key: string]: any; [key: string]: any;
}; };
export type sendAllProps = { export type sendAllProps = {
@ -134,13 +135,15 @@ export type Component = {
}; };
export type VerticesOrderTypeAPI = { export type VerticesOrderTypeAPI = {
ids: Array<Array<string>>; ids: Array<string>;
run_id: string; run_id: string;
}; };
export type VertexBuildTypeAPI = { export type VertexBuildTypeAPI = {
id: string; id: string;
next_vertices_ids: Array<string>;
inactive_vertices: Array<string> | null; inactive_vertices: Array<string> | null;
run_id: string;
valid: boolean; valid: boolean;
params: string; params: string;
data: VertexDataTypeAPI; data: VertexDataTypeAPI;

View file

@ -27,8 +27,10 @@ export type ToggleComponentType = {
disabled: boolean | undefined; disabled: boolean | undefined;
size: "small" | "medium" | "large"; size: "small" | "medium" | "large";
id?: string; id?: string;
editNode?: boolean;
}; };
export type DropDownComponentType = { export type DropDownComponentType = {
isLoading?: boolean;
value: string; value: string;
options: string[]; options: string[];
onSelect: (value: string) => void; onSelect: (value: string) => void;
@ -113,8 +115,8 @@ export type CodeAreaComponentType = {
dynamic?: boolean; dynamic?: boolean;
id?: string; id?: string;
readonly?: boolean; readonly?: boolean;
openModal?: boolean; open?: boolean;
selected?: boolean; setOpen?: (open: boolean) => void;
}; };
export type FileComponentType = { export type FileComponentType = {
@ -521,8 +523,8 @@ export type codeAreaModalPropsType = {
children: ReactNode; children: ReactNode;
dynamic?: boolean; dynamic?: boolean;
readonly?: boolean; readonly?: boolean;
openModal?: boolean; open?: boolean;
selected?: boolean; setOpen?: (open: boolean) => void;
}; };
export type chatMessagePropsType = { export type chatMessagePropsType = {

View file

@ -41,8 +41,8 @@ export type FlowPoolType = {
export type FlowStoreType = { export type FlowStoreType = {
flowPool: FlowPoolType; flowPool: FlowPoolType;
inputs: Array<{ type: string; id: string }>; inputs: Array<{ type: string; id: string; displayName: string }>;
outputs: Array<{ type: string; id: string }>; outputs: Array<{ type: string; id: string; displayName: string }>;
hasIO: boolean; hasIO: boolean;
setFlowPool: (flowPool: FlowPoolType) => void; setFlowPool: (flowPool: FlowPoolType) => void;
addDataToFlowPool: (data: FlowPoolObjectType, nodeId: string) => void; addDataToFlowPool: (data: FlowPoolObjectType, nodeId: string) => void;
@ -90,10 +90,13 @@ export type FlowStoreType = {
onConnect: (connection: Connection) => void; onConnect: (connection: Connection) => void;
unselectAll: () => void; unselectAll: () => void;
buildFlow: ({ buildFlow: ({
nodeId, startNodeId,
stopNodeId,
input_value, input_value,
}: { }: {
nodeId?: string; nodeId?: string;
startNodeId?: string;
stopNodeId?: string;
input_value?: string; input_value?: string;
}) => Promise<void>; }) => Promise<void>;
getFlow: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport }; getFlow: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
@ -101,7 +104,6 @@ export type FlowStoreType = {
vertices: { vertices: {
verticesIds: string[]; verticesIds: string[];
verticesLayers: string[][]; verticesLayers: string[][];
verticesOrder: string[][];
runId: string; runId: string;
} | null } | null
) => void; ) => void;
@ -109,12 +111,13 @@ export type FlowStoreType = {
verticesBuild: { verticesBuild: {
verticesIds: string[]; verticesIds: string[];
verticesLayers: string[][]; verticesLayers: string[][];
verticesOrder: string[][];
runId: string; runId: string;
} | null; } | null;
updateBuildStatus: (nodeId: string[], status: BuildStatus) => void; updateBuildStatus: (nodeId: string[], status: BuildStatus) => void;
revertBuiltStatusFromBuilding: () => void; revertBuiltStatusFromBuilding: () => void;
flowBuildStatus: { [key: string]: BuildStatus }; flowBuildStatus: {
[key: string]: { status: BuildStatus; timestamp?: string };
};
updateFlowPool: ( updateFlowPool: (
nodeId: string, nodeId: string,
data: FlowPoolObjectType | ChatOutputType | chatInputType, data: FlowPoolObjectType | ChatOutputType | chatInputType,

View file

@ -8,7 +8,8 @@ import { VertexBuildTypeAPI } from "../types/api";
type BuildVerticesParams = { type BuildVerticesParams = {
flowId: string; // Assuming FlowType is the type for your flow flowId: string; // Assuming FlowType is the type for your flow
input_value?: any; // Replace any with the actual type if it's not any input_value?: any; // Replace any with the actual type if it's not any
nodeId?: string | null; // Assuming nodeId is of type string, and it's optional startNodeId?: string | null; // Assuming nodeId is of type string, and it's optional
stopNodeId?: string | null; // Assuming nodeId is of type string, and it's optional
onGetOrderSuccess?: () => void; onGetOrderSuccess?: () => void;
onBuildUpdate?: ( onBuildUpdate?: (
data: VertexBuildTypeAPI, data: VertexBuildTypeAPI,
@ -31,6 +32,8 @@ function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
id: vertexId, id: vertexId,
data: inactiveData, data: inactiveData,
params: "Inactive", params: "Inactive",
run_id: "",
next_vertices_ids: [],
inactive_vertices: null, inactive_vertices: null,
valid: false, valid: false,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@ -41,18 +44,18 @@ function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
export async function updateVerticesOrder( export async function updateVerticesOrder(
flowId: string, flowId: string,
nodeId: string | null startNodeId?: string | null,
stopNodeId?: string | null
): Promise<{ ): Promise<{
verticesLayers: string[][]; verticesLayers: string[][];
verticesIds: string[]; verticesIds: string[];
verticesOrder: string[][];
runId: string; runId: string;
}> { }> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const setErrorData = useAlertStore.getState().setErrorData; const setErrorData = useAlertStore.getState().setErrorData;
let orderResponse; let orderResponse;
try { try {
orderResponse = await getVerticesOrder(flowId, nodeId); orderResponse = await getVerticesOrder(flowId, startNodeId, stopNodeId);
} catch (error: any) { } catch (error: any) {
console.log(error); console.log(error);
setErrorData({ setErrorData({
@ -62,43 +65,40 @@ export async function updateVerticesOrder(
useFlowStore.getState().setIsBuilding(false); useFlowStore.getState().setIsBuilding(false);
throw new Error("Invalid nodes"); throw new Error("Invalid nodes");
} }
let verticesOrder: Array<Array<string>> = orderResponse.data.ids; let verticesLayers: Array<Array<string>> = [orderResponse.data.ids];
const runId = orderResponse.data.run_id; const runId = orderResponse.data.run_id;
let verticesLayers: Array<Array<string>> = []; // if (nodeId) {
// for (let i = 0; i < verticesOrder.length; i += 1) {
if (nodeId) { // const innerArray = verticesOrder[i];
for (let i = 0; i < verticesOrder.length; i += 1) { // const idIndex = innerArray.indexOf(nodeId);
const innerArray = verticesOrder[i]; // if (idIndex !== -1) {
const idIndex = innerArray.indexOf(nodeId); // // If there's a nodeId, we want to run just that component and not the entire layer
if (idIndex !== -1) { // // because a layer contains dependencies for the next layer
// If there's a nodeId, we want to run just that component and not the entire layer // // and we are stopping at the layer that contains the nodeId
// because a layer contains dependencies for the next layer // verticesLayers.push([innerArray[idIndex]]);
// and we are stopping at the layer that contains the nodeId // break; // Stop searching after finding the first occurrence
verticesLayers.push([innerArray[idIndex]]); // }
break; // Stop searching after finding the first occurrence // // If the targetId is not found, include the entire inner array
} // verticesLayers.push(innerArray);
// If the targetId is not found, include the entire inner array // }
verticesLayers.push(innerArray); // } else {
} // verticesLayers = verticesOrder;
} else { // }
verticesLayers = verticesOrder; const verticesIds = orderResponse.data.ids;
}
const verticesIds = verticesLayers.flat();
useFlowStore.getState().updateVerticesBuild({ useFlowStore.getState().updateVerticesBuild({
verticesLayers, verticesLayers,
verticesIds, verticesIds,
verticesOrder,
runId, runId,
}); });
resolve({ verticesLayers, verticesIds, verticesOrder, runId }); resolve({ verticesLayers, verticesIds, runId });
}); });
} }
export async function buildVertices({ export async function buildVertices({
flowId, flowId,
input_value, input_value,
nodeId = null, startNodeId,
stopNodeId,
onGetOrderSuccess, onGetOrderSuccess,
onBuildUpdate, onBuildUpdate,
onBuildComplete, onBuildComplete,
@ -107,12 +107,17 @@ export async function buildVertices({
validateNodes, validateNodes,
}: BuildVerticesParams) { }: BuildVerticesParams) {
let verticesBuild = useFlowStore.getState().verticesBuild; let verticesBuild = useFlowStore.getState().verticesBuild;
if (!verticesBuild || nodeId) { // if startNodeId and stopNodeId are provided
verticesBuild = await updateVerticesOrder(flowId, nodeId); // something is wrong
if (startNodeId && stopNodeId) {
return;
} }
if (!verticesBuild || startNodeId || stopNodeId) {
verticesBuild = await updateVerticesOrder(flowId, startNodeId, stopNodeId);
}
const verticesIds = verticesBuild?.verticesIds!; const verticesIds = verticesBuild?.verticesIds!;
const verticesLayers = verticesBuild?.verticesLayers!; const verticesLayers = verticesBuild?.verticesLayers!;
const verticesOrder = verticesBuild?.verticesOrder!;
const runId = verticesBuild?.runId!; const runId = verticesBuild?.runId!;
let stop = false; let stop = false;
@ -120,7 +125,8 @@ export async function buildVertices({
if (validateNodes) { if (validateNodes) {
try { try {
validateNodes(verticesOrder.flatMap((id) => id)); const nodes = useFlowStore.getState().nodes;
validateNodes(nodes.map((node) => node.id));
} catch (e) { } catch (e) {
return; return;
} }
@ -128,49 +134,83 @@ export async function buildVertices({
useFlowStore.getState().updateBuildStatus(verticesIds, BuildStatus.TO_BUILD); useFlowStore.getState().updateBuildStatus(verticesIds, BuildStatus.TO_BUILD);
useFlowStore.getState().setIsBuilding(true); useFlowStore.getState().setIsBuilding(true);
let currentLayerIndex = 0; // Start with the first layer
// Set each vertex state to building // Set each vertex state to building
const buildResults: Array<boolean> = []; const buildResults: Array<boolean> = [];
for (const layer of verticesLayers) {
if (onBuildStart) onBuildStart(layer); // Build each layer
for (const id of layer) { while (
// Check if id is in the list of inactive nodes currentLayerIndex <
if (!verticesIds.includes(id) && onBuildUpdate) { (useFlowStore.getState().verticesBuild?.verticesLayers! || []).length
// If it is, skip building and set the state to inactive ) {
onBuildUpdate(getInactiveVertexData(id), BuildStatus.INACTIVE, runId); // Get the current layer
buildResults.push(false); const currentLayer =
continue; useFlowStore.getState().verticesBuild?.verticesLayers![currentLayerIndex];
} // If there are no more layers, we are done
await buildVertex({ if (!currentLayer) {
flowId, if (onBuildComplete) {
id, const allNodesValid = buildResults.every((result) => result);
input_value, onBuildComplete(allNodesValid);
onBuildUpdate: (data: VertexBuildTypeAPI, status: BuildStatus) => { useFlowStore.getState().setIsBuilding(false);
if (onBuildUpdate) onBuildUpdate(data, status, runId);
},
onBuildError,
verticesIds,
buildResults,
stopBuild: () => {
stop = true;
},
});
if (stop) {
break;
} }
return;
} }
// If there is a callback for the start of the build, call it
if (onBuildStart) onBuildStart(currentLayer);
// Build each vertex in the current layer
await Promise.all(
currentLayer.map(async (vertexId) => {
// Check if id is in the list of inactive nodes
if (
!useFlowStore
.getState()
.verticesBuild?.verticesIds.includes(vertexId) &&
onBuildUpdate
) {
// If it is, skip building and set the state to inactive
onBuildUpdate(
getInactiveVertexData(vertexId),
BuildStatus.INACTIVE,
runId
);
buildResults.push(false);
return;
}
// Build the vertex
await buildVertex({
flowId,
id: vertexId,
input_value,
onBuildUpdate: (data: VertexBuildTypeAPI, status: BuildStatus) => {
if (onBuildUpdate) onBuildUpdate(data, status, runId);
},
onBuildError,
verticesIds,
buildResults,
stopBuild: () => {
stop = true;
},
});
if (stop) {
return;
}
})
);
// Once the current layer is built, move to the next layer
currentLayerIndex += 1;
if (stop) { if (stop) {
break; break;
} }
}
if (onBuildComplete) { if (onBuildComplete) {
const allNodesValid = buildResults.every((result) => result); const allNodesValid = buildResults.every((result) => result);
onBuildComplete(allNodesValid); onBuildComplete(allNodesValid);
useFlowStore.getState().setIsBuilding(false); useFlowStore.getState().setIsBuilding(false);
}
} }
} }
async function buildVertex({ async function buildVertex({
flowId, flowId,
id, id,
@ -192,6 +232,7 @@ async function buildVertex({
}) { }) {
try { try {
const buildRes = await postBuildVertex(flowId, id, input_value); const buildRes = await postBuildVertex(flowId, id, input_value);
const buildData: VertexBuildTypeAPI = buildRes.data; const buildData: VertexBuildTypeAPI = buildRes.data;
if (onBuildUpdate) { if (onBuildUpdate) {
if (!buildData.valid) { if (!buildData.valid) {

View file

@ -12,8 +12,8 @@ import {
INPUT_TYPES, INPUT_TYPES,
LANGFLOW_SUPPORTED_TYPES, LANGFLOW_SUPPORTED_TYPES,
OUTPUT_TYPES, OUTPUT_TYPES,
SUCCESS_BUILD,
specialCharsRegex, specialCharsRegex,
successBuild,
} from "../constants/constants"; } from "../constants/constants";
import { downloadFlowsFromDatabase } from "../controllers/API"; import { downloadFlowsFromDatabase } from "../controllers/API";
import { import {
@ -1092,7 +1092,7 @@ export function getGroupStatus(
flow: FlowType, flow: FlowType,
ssData: { [key: string]: { valid: boolean; params: string } } ssData: { [key: string]: { valid: boolean; params: string } }
) { ) {
let status = { valid: true, params: successBuild }; let status = { valid: true, params: SUCCESS_BUILD };
const { nodes } = flow.data!; const { nodes } = flow.data!;
const ids = nodes.map((n: NodeType) => n.data.id); const ids = nodes.map((n: NodeType) => n.data.id);
ids.forEach((id) => { ids.forEach((id) => {

View file

@ -25,15 +25,23 @@ export function getTagsIds(
} }
export function getInputsAndOutputs(nodes: Node[]) { export function getInputsAndOutputs(nodes: Node[]) {
let inputs: { type: string; id: string }[] = []; let inputs: { type: string; id: string; displayName: string }[] = [];
let outputs: { type: string; id: string }[] = []; let outputs: { type: string; id: string; displayName: string }[] = [];
nodes.forEach((node) => { nodes.forEach((node) => {
const nodeData: NodeDataType = node.data as NodeDataType; const nodeData: NodeDataType = node.data as NodeDataType;
if (isOutputNode(nodeData)) { if (isOutputNode(nodeData)) {
outputs.push({ type: nodeData.type, id: nodeData.id }); outputs.push({
type: nodeData.type,
id: nodeData.id,
displayName: nodeData.node?.display_name ?? nodeData.id,
});
} }
if (isInputNode(nodeData)) { if (isInputNode(nodeData)) {
inputs.push({ type: nodeData.type, id: nodeData.id }); inputs.push({
type: nodeData.type,
id: nodeData.id,
displayName: nodeData.node?.display_name ?? nodeData.id,
});
} }
}); });
return { inputs, outputs }; return { inputs, outputs };

Some files were not shown because too many files have changed in this diff Show more