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_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
# Example: LANGFLOW_SUPERUSER=admin
LANGFLOW_SUPERUSER=

View file

@ -68,8 +68,6 @@ INVALID_CHARACTERS = {
")",
"[",
"]",
"{",
"}",
}
INVALID_NAMES = {
@ -88,73 +86,110 @@ def validate_prompt(template: str):
# Check if there are invalid characters in the input_variables
input_variables = check_input_variables(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:
PromptTemplate(template=template, input_variables=input_variables)
except Exception as exc:
raise ValueError(str(exc)) from exc
raise ValueError(f"Invalid prompt: {exc}") from exc
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 = []
fixed_variables = []
wrong_variables = []
empty_variables = []
for variable in input_variables:
new_var = variable
variables_to_check = []
# if variable is empty, then we should add that to the wrong variables
if not variable:
empty_variables.append(variable)
for var in input_variables:
# First, let's check if the variable is a JSON string
# 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
# if variable starts with a number we should add that to the invalid chars
# and 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,
new_var, wrong_variables, empty_variables = fix_variable(
var, invalid_chars, wrong_variables
)
raise ValueError(error_message)
return input_variables
wrong_variables, empty_variables = check_variable(
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):
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
return fixed_variables

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.deps import get_chat_service, get_session, get_session_service
from langflow.services.monitor.utils import log_vertex_build
from langflow.services.session.service import SessionService
if TYPE_CHECKING:
from langflow.graph.vertex.types import ChatVertex
from langflow.services.session.service import SessionService
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)
async def get_vertices(
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),
session=Depends(get_session),
):
@ -60,9 +61,9 @@ async def get_vertices(
if cache := chat_service.get_cache(flow_id):
graph = cache.get("result")
graph = build_and_cache_graph(flow_id, session, chat_service, graph)
if component_id:
if stop_component_id or start_component_id:
try:
vertices = graph.sort_vertices(component_id)
vertices = graph.sort_vertices(stop_component_id, start_component_id)
except Exception as exc:
logger.error(exc)
vertices = graph.sort_vertices()
@ -94,6 +95,7 @@ async def build_vertex(
"""Build a vertex instead of the entire graph."""
{"inputs": {"input_value": "some value"}}
start_time = time.perf_counter()
next_vertices_ids = []
try:
start_time = time.perf_counter()
cache = chat_service.get_cache(flow_id)
@ -119,6 +121,10 @@ async def build_vertex(
artifacts = vertex.artifacts
else:
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())
@ -155,7 +161,15 @@ async def build_vertex(
graph.reset_inactive_vertices()
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(
next_vertices_ids=next_vertices_ids,
inactive_vertices=inactive_vertices,
valid=valid,
params=params,

View file

@ -44,12 +44,15 @@ def get_all(
logger.debug("Building langchain types dict")
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:
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(
session: Annotated[Session, Depends(get_session)],
flow_id: str,
@ -67,7 +70,9 @@ async def run_flow_with_caching(
input_values_dict = {}
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)
task_result: Any = None
if not graph:
@ -85,7 +90,11 @@ async def run_flow_with_caching(
else:
# 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.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:
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')
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
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:
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:
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(
@ -142,7 +157,8 @@ async def process(
"""
# Raise a depreciation 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(
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 = 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
@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
try:
@ -241,6 +261,8 @@ async def custom_component_update(
):
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
return component_node

View file

@ -158,7 +158,9 @@ class StreamData(BaseModel):
data: dict
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):
@ -214,7 +216,7 @@ class ApiKeyCreateRequest(BaseModel):
class VerticesOrderResponse(BaseModel):
ids: List[List[str]]
ids: List[str]
run_id: UUID
@ -227,9 +229,10 @@ class ResultDataResponse(BaseModel):
class VertexBuildResponse(BaseModel):
id: Optional[str] = None
next_vertices_ids: Optional[List[str]] = None
inactive_vertices: Optional[List[str]] = None
valid: bool
params: Optional[str]
params: Optional[Any] = Field(default_factory=dict)
"""JSON string of the params."""
data: ResultDataResponse
"""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)
else:
chain = ConversationChain(llm=llm, memory=memory)
result = chain.invoke({chain.input_key: input_value})
# 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 = ""
result = chain.invoke(inputs)
if hasattr(result, "content") and isinstance(result.content, str):
result_str = result.content
result = result.content
elif isinstance(result, str):
result_str = result
result = result
else:
# is dict
result_str = Text(result.get("response"))
self.status = result_str
return result_str
result = result.get("response")
self.status = result
return result

View file

@ -1,6 +1,7 @@
from langchain_core.documents import Document
from typing import List
from langflow import CustomComponent
from langflow.schema import Record
from langflow.utils.constants import LOADERS_INFO
@ -10,7 +11,9 @@ class FileLoaderComponent(CustomComponent):
beta = True
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 = []
suffixes = []
@ -74,7 +77,7 @@ class FileLoaderComponent(CustomComponent):
"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]
# Map the loader to the correct loader class
@ -102,7 +105,9 @@ class FileLoaderComponent(CustomComponent):
if isinstance(selected_loader_info, dict):
loader_import: str = selected_loader_info["import"]
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)
try:
@ -110,7 +115,10 @@ class FileLoaderComponent(CustomComponent):
loader_module = __import__(module_name, fromlist=[class_name])
loader_instance = getattr(loader_module, class_name)
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)
return result.load()
docs = result.load()
return self.to_records(docs)

View file

@ -70,11 +70,17 @@ class GatherRecordsComponent(CustomComponent):
glob = "**/*" if recursive else "*"
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
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
from unstructured.partition.auto import partition # type: ignore
@ -100,9 +106,14 @@ class GatherRecordsComponent(CustomComponent):
use_multithreading: bool,
) -> List[Optional[Record]]:
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:
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))
return records
@ -131,13 +142,20 @@ class GatherRecordsComponent(CustomComponent):
if types is None:
types = []
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 = []
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:
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))
self.status = loaded_records
return loaded_records

View file

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

View file

@ -1,19 +1,12 @@
from typing import Optional
from langflow import CustomComponent
from langflow.components.io.base.text import TextComponent
from langflow.field_typing import Text
class TextInput(CustomComponent):
class TextInput(TextComponent):
display_name = "Text Input"
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:
self.status = input_value
if not input_value:
input_value = ""
return input_value
return super().build(input_value=input_value)

View file

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

View file

@ -45,7 +45,9 @@ class ChatComponent(CustomComponent):
return []
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):
record = message
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))
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:
if hasattr(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.field_typing import Text
from langflow.helpers.record import records_to_text
from langflow.schema import Record
@ -27,7 +28,6 @@ class RecordsAsTextComponent(CustomComponent):
if isinstance(records, Record):
records = [records]
formated_records = [template.format(text=record.text, data=record.data, **record.data) for record in records]
result_string = "\n".join(formated_records)
result_string = records_to_text(template, records)
self.status = 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:
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_port=chroma_server_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:
raise ValueError("Folder path is required to save the FAISS index.")
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:
raise ValueError("Failed to load the FAISS index.")

View file

@ -38,7 +38,9 @@ class SupabaseSearchComponent(LCVectorStoreComponent):
supabase_url: str = "",
table_name: str = "",
) -> 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(
client=supabase,
embedding=embedding,

View file

@ -11,7 +11,9 @@ from langflow.schema import Record
class VectaraSearchComponent(VectaraComponent, LCVectorStoreComponent):
display_name: str = "Vectara Search"
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
icon = "Vectara"

View file

@ -11,7 +11,9 @@ from langflow.schema import Record
class WeaviateSearchVectorStore(WeaviateVectorStoreComponent, LCVectorStoreComponent):
display_name: str = "Weaviate Search"
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
icon = "Weaviate"

View file

@ -6,7 +6,8 @@ from langchain_core.vectorstores import VectorStore
from langflow import CustomComponent
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):

View file

@ -15,7 +15,9 @@ class PGVectorSearchComponent(PGVectorComponent, LCVectorStoreComponent):
display_name: str = "PGVector Search"
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):
"""

View file

@ -1,19 +1,20 @@
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.schema import INPUT_FIELD_NAME
from langflow.services.deps import get_monitor_service
from langflow.services.monitor.utils import log_message
from loguru import logger
from pydantic import BaseModel, Field
if TYPE_CHECKING:
from langflow.graph.vertex.base import Vertex
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.")
id: str = Field(..., description="Unique identifier for the source handle.")
@ -21,7 +22,9 @@ class SourceHandle(BaseModel):
class TargetHandle(BaseModel):
fieldName: str = Field(..., description="Field name 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.")
@ -50,16 +53,24 @@ class Edge:
def validate_handles(self, source, target) -> 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:
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
)
if not self.valid_handles:
logger.debug(self.source_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):
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
# 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
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
self.matched_type = next(
@ -87,7 +102,10 @@ class Edge:
if no_matched_type:
logger.debug(self.source_types)
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:
return (
@ -98,8 +116,12 @@ class Edge:
def __hash__(self) -> int:
return hash(self.__repr__())
def __eq__(self, __value: object) -> bool:
return self.__repr__() == __value.__repr__() if isinstance(__value, Edge) else False
def __eq__(self, __o: object) -> bool:
# Create a better way to compare edges
return (
self._source_handle == __o._source_handle
and self._target_handle == __o._target_handle
)
class ContractEdge(Edge):
@ -156,7 +178,9 @@ class ContractEdge(Edge):
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:
monitor_service = get_monitor_service()
clean_params = build_clean_params(target)

View file

@ -1,5 +1,6 @@
import asyncio
from collections import defaultdict, deque
from itertools import chain
from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Type, Union
from langchain.chains.base import Chain
@ -54,6 +55,10 @@ class Graph:
self._vertices = self._graph_data["nodes"]
self._edges = self._graph_data["edges"]
self.vertices_layers = []
self.vertices_to_run = set()
self.stop_vertex = None
self.inactive_vertices: set = set()
self.edges: List[ContractEdge] = []
self.vertices: List[Vertex] = []
@ -104,7 +109,12 @@ class Graph:
vertex = self.get_vertex(vertex_id)
if vertex is None:
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()
outputs.append(vertex.result)
return outputs
@ -126,6 +136,14 @@ class Graph:
outputs.extend(run_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
def metadata(self):
return {
@ -236,7 +254,7 @@ class Graph:
self.edges = new_edges
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:
return False
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)
removed_vertex_ids = existing_vertex_ids - other_vertex_ids
# 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 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
# Remove vertices that are not in the other graph
for vertex_id in removed_vertex_ids:
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
for vertex_id in new_vertex_ids:
new_vertex = other.get_vertex(vertex_id)
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.increment_update_count()
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:
"""Resets all the edges of a vertex."""
for edge in vertex.edges:
@ -306,12 +348,24 @@ class Graph:
_vertex._build_params()
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.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
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)
def _build_graph(self) -> None:
@ -409,13 +463,19 @@ class Graph:
async def process(self) -> "Graph":
"""Processes the graph with vertices in each layer run in parallel."""
vertices_layers = self.sorted_vertices_layers
vertex_task_run_count = {}
for layer_index, layer in enumerate(vertices_layers):
tasks = []
for vertex_id in layer:
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)
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")
await self._execute_tasks(tasks)
logger.debug("Graph processing complete")
@ -434,6 +494,10 @@ class Graph:
# coroutine has not attribute get_name
task_name = tasks[i].get_name()
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
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])
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."""
# Initial setup
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
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
while stack:
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)
current_vertex = self.get_vertex(current_id)
# Assuming get_predecessors is a method that returns all vertices with edges to current_vertex
for predecessor in current_vertex.predecessors:
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`
vertices_to_keep = [self.get_vertex(vid) for vid in visited]
@ -679,19 +775,41 @@ class Graph:
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."""
self.mark_all_vertices("ACTIVE")
if component_id:
vertices = self.sort_up_to_vertex(component_id)
if stop_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:
vertices = self.vertices
vertices_layers = self.layered_topological_sort(vertices)
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._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]]:
"""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.TextSplitterVertex for t in textsplitter_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.ChatVertex for t in CHAT_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 langflow.graph.utils import serialize_field
from langflow.utils.schemas import ContainsEnumMeta
from langflow.utils.schemas import ChatOutputResponse, ContainsEnumMeta
class ResultData(BaseModel):
results: 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
duration: Optional[str] = None

View file

@ -2,7 +2,17 @@ import ast
import inspect
import types
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
@ -18,6 +28,7 @@ from langflow.interface.initialize import loading
from langflow.interface.listing import lazy_load_dict
from langflow.services.deps import get_storage_service
from langflow.utils.constants import DIRECT_TYPES
from langflow.utils.schemas import ChatOutputResponse
from langflow.utils.util import sync_to_async
if TYPE_CHECKING:
@ -120,6 +131,8 @@ class Vertex:
if not isinstance(result, (dict, str)) and hasattr(result, "content"):
return result.content
return result
if isinstance(self._built_object, str):
self._built_result = self._built_object
if isinstance(self._built_result, UnbuiltResult):
return {}
@ -140,6 +153,10 @@ class Vertex:
def successors(self) -> List["Vertex"]:
return self.graph.get_successors(self)
@property
def successors_ids(self) -> List[str]:
return self.graph.successor_map.get(self.id, [])
def __getstate__(self):
return {
"_data": self._data,
@ -360,15 +377,43 @@ class Vertex:
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):
result_dict = self.get_built_result()
# We need to set the artifacts to pass information
# to the frontend
self.set_artifacts()
artifacts = self.artifacts
messages = self.extract_messages_from_artifacts(artifacts)
result_dict = ResultData(
results=result_dict,
artifacts=artifacts,
messages=messages,
)
self.set_result(result_dict)
@ -463,12 +508,21 @@ class Vertex:
self.params[key] = []
for node in nodes:
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 key not in self.params:
self.params[key] = []
self.params[key].extend(built)
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):
"""
@ -536,6 +590,11 @@ class Vertex:
message += " Make sure your build method returns a component."
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):
self._built = False
@ -545,6 +604,9 @@ class Vertex:
self.steps_ran = []
self._build_params()
def _is_chat_input(self):
return False
def build_inactive(self):
# Just set the results to None
self._built = True
@ -567,7 +629,7 @@ class Vertex:
return self.get_requester_result(requester)
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)
# Run steps
@ -603,7 +665,15 @@ class Vertex:
def __eq__(self, __o: object) -> bool:
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:
return False

View file

@ -1,12 +1,13 @@
import ast
import json
from typing import AsyncIterator, Callable, Dict, Iterator, List, Optional, Union
from typing import (AsyncIterator, Callable, Dict, Iterator, List, Optional,
Union)
import yaml
from langchain_core.messages import AIMessage
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.vertex.base import StatefulVertex, StatelessVertex
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?
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(
self._built_object
)
return f"""{self.display_name}({len(self._built_object)} documents)
\nAvg. Document Length (characters): {int(avg_length)}
Documents: {self._built_object[:3]}..."""
avg_length = sum(
len(record.text)
for record in self._built_object
if hasattr(record, "text")
) / len(self._built_object)
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}()"
@ -198,7 +201,9 @@ class TextSplitterVertex(StatefulVertex):
# show how many documents are in the list?
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)
\nAvg. Document Length (characters): {int(avg_length)}
\nDocuments: {self._built_object[:3]}..."""
@ -245,18 +250,27 @@ class PromptVertex(StatelessVertex):
user_id = kwargs.get("user_id", None)
tools = kwargs.get("tools", [])
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"] = []
# Check if it is a ZeroShotPrompt and needs a tool
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
# first check if it is a list
if tools and isinstance(tools, list) and isinstance(tools[0], list):
tools = flatten_list(tools)
self.params["tools"] = tools
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:
prompt_params = ["template"]
@ -266,14 +280,20 @@ class PromptVertex(StatelessVertex):
prompt_text = self.params[param]
variables = extract_input_variables_from_prompt(prompt_text)
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):
self.params.pop("input_variables", None)
await self._build(user_id=user_id)
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()
elif isinstance(self._built_object, UnbuiltObject):
return super()._built_object_repr()
@ -285,7 +305,9 @@ class PromptVertex(StatelessVertex):
# so the prompt format doesn't break
artifacts.pop("handle_keys", None)
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
else:
template = self._built_object.template
@ -293,7 +315,11 @@ class PromptVertex(StatelessVertex):
if value:
replace_key = "{" + key + "}"
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:
return str(self._built_object)
@ -430,18 +456,30 @@ class ChatVertex(StatelessVertex):
async for _ in self.stream():
pass
def _is_chat_input(self):
return self.vertex_type == InterfaceComponentTypes.ChatInput and self.is_input
class RoutingVertex(StatelessVertex):
def __init__(self, data: Dict, graph):
super().__init__(data, graph=graph, base_type="custom_components")
self.use_result = True
self.steps = [self._build, self._run]
self.steps = [self._build]
def _built_object_repr(self):
if self.artifacts and "repr" in self.artifacts:
return self.artifacts["repr"] or super()._built_object_repr()
return super()._built_object_repr()
@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):
if self._built_object:
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:
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
_function_entrypoint_name: str = "build"
@ -39,7 +41,8 @@ class Component:
def __setattr__(self, key, value):
if key == "_user_id" and hasattr(self, "_user_id"):
warnings.warn("user_id is immutable and cannot be changed.")
super().__setattr__(key, value)
else:
super().__setattr__(key, value)
@cachedmethod(cache=operator.attrgetter("cache"))
def get_code_tree(self, code: str):

View file

@ -16,6 +16,7 @@ import yaml
from cachetools import TTLCache, cachedmethod
from fastapi import HTTPException
from langchain_core.documents import Document
from pydantic import BaseModel
from sqlmodel import select
from langflow.interface.custom.code_parser.utils import (
@ -73,6 +74,7 @@ class CustomComponent(Component):
user_id: Optional[Union[UUID, str]] = None
status: Optional[Any] = 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
@ -110,7 +112,7 @@ class CustomComponent(Component):
return yaml.dump(self.repr_value)
if isinstance(self.repr_value, str):
return self.repr_value
return str(self.repr_value)
return self.repr_value
def build_config(self):
return self.field_config
@ -119,34 +121,57 @@ class CustomComponent(Component):
def tree(self):
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:
data (Any): The input data to be converted.
text_key (str, optional): The key to extract the text from a dictionary item. Defaults to "text".
data_key (str, optional): The key to extract the data from a dictionary item. Defaults to "data".
data (Any): The input data to be converted. It can be a single item or a sequence of items.
If the input data is a Langchain Document, text_key and data_key are ignored.
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:
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 = []
if not isinstance(data, Sequence):
data = [data]
for item in data:
if isinstance(item, str):
records.append(Record(text=item))
if isinstance(item, Document):
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):
records.append(Record(text=item.get(text_key), data=item.get(data_key)))
elif isinstance(item, Document):
records.append(Record(text=item.page_content, data=item.metadata))
if text_key not in item:
raise ValueError(f"Key '{text_key}' not found in dictionary item.")
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:
raise ValueError(f"Invalid data type: {type(item)}")
records.append(Record(**item))
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.
@ -181,7 +206,8 @@ class CustomComponent(Component):
detail={
"error": "Type hint Error",
"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:
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:
return {}
# Assume the first Component class is the one we're interested in
component_class = component_classes[0]
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 {}
@ -259,7 +291,9 @@ class CustomComponent(Component):
# Retrieve and decrypt the credential by name for the current user
db_service = get_db_service()
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
@ -269,7 +303,9 @@ class CustomComponent(Component):
credential_service = get_credential_service()
db_service = get_db_service()
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):
"""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(
self,
input_value: Union[str, list[str]],
flow_id: str,
flow_id: Optional[str] = None,
flow_name: Optional[str] = None,
tweaks: Optional[dict] = None,
) -> 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)
input_value_dict = {"input_value": input_value}
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:
raise ValueError("Session is invalid")
try:
get_session = get_session or session_getter
db_service = get_db_service()
with get_session(db_service) as session:
flows = session.exec(select(Flow).where(Flow.user_id == self._user_id)).all()
return flows
flows = session.exec(
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:
raise ValueError("Session is invalid") from e
raise ValueError(f"Error listing flows: {e}")
def build(self, *args: Any, **kwargs: Any) -> Any:
raise NotImplementedError

View file

@ -80,9 +80,13 @@ class DirectoryReader:
except Exception as e:
logger.error(f"Error while loading component: {e}")
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"]]
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}
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.
"""
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 = []
safe_path_obj = Path(safe_path)
@ -125,7 +131,11 @@ class DirectoryReader:
# any folders below [folder] will be ignored
# basically the parent folder of the file should be a
# 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))
return file_list
@ -163,7 +173,9 @@ class DirectoryReader:
for node in ast.walk(module):
if isinstance(node, ast.FunctionDef):
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
except SyntaxError:
# Returns False if the code is not valid Python
@ -181,14 +193,16 @@ class DirectoryReader:
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.
"""
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
)
) and not self._is_type_hint_imported(type_hint_name, code)
except SyntaxError:
# Returns True if there's something wrong with the code
# TODO : Find a better way to handle this
@ -209,9 +223,9 @@ class DirectoryReader:
return False, "Syntax error"
elif not self.validate_build(file_content):
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
):
) and not self._is_type_hint_imported("Optional", file_content):
return (
False,
"Type hint 'Optional' is used but not imported in the code.",
@ -227,14 +241,18 @@ class DirectoryReader:
from the .py files in the directory.
"""
response = {"menu": []}
logger.debug("-------------------- Building component menu list --------------------")
logger.debug(
"-------------------- Building component menu list --------------------"
)
for file_path in file_paths:
menu_name = os.path.basename(os.path.dirname(file_path))
filename = os.path.basename(file_path)
validation_result, result_content = self.process_file(file_path)
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 {
"name": menu_name,
@ -247,7 +265,9 @@ class DirectoryReader:
# first check if it's already CamelCase
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:
component_name_camelcase = component_name
@ -255,7 +275,9 @@ class DirectoryReader:
try:
output_types = self.get_output_types_from_code(result_content)
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]
else:
output_types = [component_name_camelcase]
@ -271,7 +293,9 @@ class DirectoryReader:
if menu_result not in response["menu"]:
response["menu"].append(menu_result)
logger.debug("-------------------- Component menu list built --------------------")
logger.debug(
"-------------------- Component menu list built --------------------"
)
return response
@staticmethod
@ -281,5 +305,6 @@ class DirectoryReader:
"""
custom_component = CustomComponent(code=code)
types_list = custom_component.get_function_entrypoint_return_type
# Get the name of types classes
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)
if return_type_instance == str:
base_classes.append("Text")
for base_class in base_classes:
frontend_node.add_base_class(base_class)
@ -232,10 +234,12 @@ def run_build_config(
# Allow user to build TemplateField as well
# as a dict with the same keys as TemplateField
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:
continue
try:
update_field_dict(field_dict)
update_field_dict(field_dict, call=True)
build_config[field_name] = field_dict
except Exception as 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."""
component_code = component["code"]
component_output_types = component["output_types"]
# remove
component_extractor = CustomComponent(code=component_code)
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
@ -364,15 +370,17 @@ def build_custom_components(settings_service):
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"""
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
field_dict["refresh"] = True
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
# Let's check if "range_spec" is a RangeSpec object

View file

@ -1,14 +1,14 @@
import base64
import json
import os
from io import BytesIO
import re
from io import BytesIO
import yaml
from langchain.base_language import BaseLanguageModel
from PIL.Image import Image
from loguru import logger
from PIL.Image import Image
from langflow.services.chat.config import ChatConfig
from langflow.services.deps import get_settings_service
@ -43,7 +43,9 @@ def try_setting_streaming_options(langchain_object):
llm = None
if hasattr(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
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]:
"""Extract input variables from prompt."""
return re.findall(r"{(.*?)}", prompt)
variables = []
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():
@ -73,11 +104,14 @@ def setup_llm_caching():
def set_langchain_cache(settings):
from langchain.globals import set_llm_cache
from langflow.interface.importing.utils import import_class
if cache_type := os.getenv("LANGFLOW_LANGCHAIN_CACHE"):
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__}")
set_llm_cache(cache_class())

View file

@ -1,9 +1,10 @@
from typing import Optional, Union
from loguru import logger
from langflow.schema import Record
from langflow.services.deps import get_monitor_service
from langflow.services.monitor.schema import MessageModel
from loguru import logger
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 pydantic import BaseModel
@ -13,7 +13,7 @@ class Record(BaseModel):
data (dict, optional): Additional data associated with the record.
"""
text: str
text: Optional[str] = ""
data: dict = {}
@classmethod
@ -52,19 +52,6 @@ class Record(BaseModel):
Returns the text of the record.
Returns:
str: The text of the record.
str: The text and data of the record.
"""
return self.text
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]
return self.model_dump_json(indent=2)

View file

@ -7,6 +7,8 @@ from uuid import UUID, uuid4
from pydantic import field_serializer, field_validator
from sqlmodel import JSON, Column, Field, Relationship, SQLModel
from langflow.schema.schema import Record
if TYPE_CHECKING:
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)
data: Optional[Dict] = Field(default=None, 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)
@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: "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):
user_id: Optional[UUID] = None

View file

@ -36,7 +36,10 @@ class DatabaseService(Service):
def _create_engine(self) -> "Engine":
"""Create the engine for the database."""
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}
else:
connect_args = {}
@ -48,7 +51,9 @@ class DatabaseService(Service):
def __exit__(self, exc_type, exc_value, traceback):
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()
else:
self._session.commit()
@ -65,7 +70,9 @@ class DatabaseService(Service):
settings_service = get_settings_service()
if settings_service.auth_settings.AUTO_LOGIN:
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:
logger.debug("Migrating flows to default superuser")
username = settings_service.auth_settings.SUPERUSER
@ -95,7 +102,9 @@ class DatabaseService(Service):
expected_columns = list(model.model_fields.keys())
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:
logger.error(f"Missing table: {table}")
return False
@ -152,14 +161,16 @@ class DatabaseService(Service):
try:
command.check(alembic_cfg)
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")
time.sleep(3)
try:
command.check(alembic_cfg)
except util.exc.AutogenerateDiffsDetected as e:
logger.error("AutogenerateDiffsDetected: {exc}")
logger.error(f"AutogenerateDiffsDetected: {exc}")
if not fix:
raise RuntimeError(
"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
# and that the database is up to date with all columns
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):
results = []
@ -197,7 +211,9 @@ class DatabaseService(Service):
expected_columns = list(model.__fields__.keys())
available_columns = []
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))
except sa.exc.NoSuchTableError:
logger.error(f"Missing table: {table_name}")
@ -228,7 +244,9 @@ class DatabaseService(Service):
try:
table.create(self.engine, checkfirst=True)
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:
logger.error(f"Error creating table {table}: {exc}")
raise RuntimeError(f"Error creating table {table}") from exc
@ -240,7 +258,9 @@ class DatabaseService(Service):
if table not in table_names:
logger.error("Something went wrong creating the database and tables.")
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")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,8 +27,16 @@ terminate_process_by_port() {
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 '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
npx playwright install
@ -42,26 +50,16 @@ make frontend &
# Give some time for the frontend to start (adjust sleep duration as needed)
sleep 10
# Navigate to the test directory
cd src/frontend
# 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 ../../
#install backend
poetry install --extras deploy
# 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)
sleep 25
# Navigate back to the test directory
# Navigate to the test directory
cd src/frontend
# 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 ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button";
import { RefreshButton } from "../../../../components/ui/refreshButton";
import {
INPUT_HANDLER_HOVER,
LANGFLOW_SUPPORTED_TYPES,
OUTPUT_HANDLER_HOVER,
TOOLTIP_EMPTY,
inputHandleHover,
outputHandleHover,
} from "../../../../constants/constants";
import { postCustomComponentUpdate } from "../../../../controllers/API";
import useAlertStore from "../../../../stores/alertStore";
@ -68,7 +69,7 @@ export default function ParameterComponent({
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode);
const [isLoading, setIsLoading] = useState(false);
const flow = currentFlow?.data?.nodes ?? null;
const groupedEdge = useRef(null);
@ -85,7 +86,12 @@ export default function ParameterComponent({
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;
if (!code) {
console.error("Code not found in the template");
@ -95,13 +101,48 @@ export default function ParameterComponent({
try {
const res = await postCustomComponentUpdate(code, name);
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) {
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 = (
newValue: string | string[] | boolean | Object[]
): void => {
@ -182,7 +223,7 @@ export default function ParameterComponent({
return (
<div key={index}>
{index === 0 && (
<span>{left ? inputHandleHover : outputHandleHover}</span>
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
)}
<span
key={index}
@ -212,15 +253,15 @@ export default function ParameterComponent({
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index ===
<React.Fragment key={el + name}>
<span>
{index ===
item.display_name.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.display_name}
</span>
) : (
@ -229,14 +270,14 @@ export default function ParameterComponent({
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
<React.Fragment key={el + name}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
)}
@ -304,24 +345,31 @@ export default function ParameterComponent({
ref={ref}
className={
"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
className={
"w-full truncate text-sm" +
(left ? "" : " text-end") +
(left ? "" : " gap-2 flex justify-end items-center") +
(info !== "" ? " flex items-center" : "")
}
>
{!left && data.node?.pinned &&
<div>
<IconComponent className="w-5 h-5 text-ice" name={"Snowflake"} />
</div>}
{proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
<span>{title}</span>
<span className={!left && data.node?.pinned?" text-ice":""}>{title}</span>
</ShadTooltip>
) : (
title
)}
<span className={!left && data.node?.pinned?" text-ice":""}>{title}</span>
)}
<span className={(info === "" ? "" : "ml-1 ") + " text-status-red"}>
{required ? " *" : ""}
</span>
@ -383,20 +431,35 @@ export default function ParameterComponent({
)}
{left === true &&
type === "str" &&
!data.node?.template[name].options ? (
type === "str" &&
!data.node?.template[name].options ? (
<div className="mt-2 w-full">
{data.node?.template[name].list ? (
<InputListComponent
disabled={disabled}
value={
!data.node.template[name].value ||
data.node.template[name].value === ""
? [""]
: data.node.template[name].value
}
onChange={handleOnNewValue}
/>
<div className="w-5/6 flex-grow">
<InputListComponent
disabled={disabled}
value={
!data.node.template[name].value ||
data.node.template[name].value === ""
? [""]
: data.node.template[name].value
}
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 ? (
<TextAreaComponent
disabled={disabled}
@ -409,7 +472,7 @@ export default function ParameterComponent({
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<InputComponent
id={"input-" + index}
id={"input-" + name}
disabled={disabled}
password={data.node?.template[name].password ?? false}
value={data.node?.template[name].value ?? ""}
@ -417,14 +480,17 @@ export default function ParameterComponent({
/>
</div>
{data.node?.template[name].refresh && (
<button
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
onClick={() => {
handleUpdateValues(name, data);
}}
>
<IconComponent name="RefreshCcw" />
</button>
<div 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>
)}
@ -432,11 +498,12 @@ export default function ParameterComponent({
) : left === true && type === "bool" ? (
<div className="mt-2 w-full">
<ToggleShadComponent
id={"toggle-" + index}
id={"toggle-" + name}
disabled={disabled}
enabled={data.node?.template[name].value ?? false}
setEnabled={handleOnNewValue}
size="large"
editNode={false}
/>
</div>
) : left === true && type === "float" ? (
@ -450,26 +517,31 @@ export default function ParameterComponent({
</div>
) : left === true &&
type === "str" &&
data.node?.template[name].options ? (
(data.node?.template[name].options ||
data.node?.template[name]?.refresh) ? (
// TODO: Improve CSS
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<Dropdown
isLoading={isLoading}
options={data.node.template[name].options}
onSelect={handleOnNewValue}
value={data.node.template[name].value ?? "Choose an option"}
id={"dropdown-" + index}
id={"dropdown-" + name}
/>
</div>
{data.node?.template[name].refresh && (
<button
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
onClick={() => {
handleUpdateValues(name, data);
}}
>
<IconComponent name="RefreshCcw" />
</button>
<div 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>
) : left === true && type === "code" ? (
@ -486,7 +558,7 @@ export default function ParameterComponent({
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"code-input-" + index}
id={"code-input-" + name}
/>
</div>
) : left === true && type === "file" ? (
@ -507,7 +579,7 @@ export default function ParameterComponent({
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"int-input-" + index}
id={"int-input-" + name}
/>
</div>
) : left === true && type === "prompt" ? (
@ -520,8 +592,8 @@ export default function ParameterComponent({
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"prompt-input-" + index}
data-testid={"prompt-input-" + index}
id={"prompt-input-" + name}
data-testid={"prompt-input-" + name}
/>
</div>
) : left === true && type === "NestedDict" ? (
@ -531,10 +603,10 @@ export default function ParameterComponent({
editNode={false}
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
}
onChange={handleOnNewValue}
@ -548,7 +620,7 @@ export default function ParameterComponent({
editNode={false}
value={
data.node!.template[name].value?.length === 0 ||
!data.node!.template[name].value
!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 Xmark from "../../components/ui/xmark";
import {
RUN_TIMESTAMP_PREFIX,
STATUS_BUILD,
STATUS_BUILDING,
priorityFields,
statusBuild,
statusBuilding,
} from "../../constants/constants";
import { BuildStatus } from "../../constants/enums";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
@ -49,11 +50,18 @@ export default function GenericNode({
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
);
const buildStatus = useFlowStore((state) => state.flowBuildStatus[data.id]);
const buildStatus = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.status
);
const lastRunTime = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.timestamp
);
const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null);
const [handles, setHandles] = useState<number>(0);
const [validationString, setValidationString] = useState<string>("");
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
function countHandles(): void {
@ -100,7 +108,7 @@ export default function GenericNode({
if (duration === undefined) {
return "";
} else {
return `Duration: ${duration}`;
return `${duration}`;
}
};
const durationString = getDurationString(validationStatus?.data.duration);
@ -126,6 +134,18 @@ export default function GenericNode({
}
}, [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 nameEditable = true;
@ -332,8 +352,8 @@ export default function GenericNode({
/>
</div>
) : (
<div className="group flex items-center gap-2.5">
<ShadTooltip content={data.id}>
<div className="group flex items-start gap-1.5">
<ShadTooltip content={data.node?.display_name}>
<div
onDoubleClick={(event) => {
if (nameEditable) {
@ -359,8 +379,8 @@ export default function GenericNode({
}}
>
<IconComponent
name="Pencil"
className="hidden h-4 w-4 animate-pulse text-status-blue group-hover:block"
name="PencilLine"
className="hidden h-3 w-3 text-status-blue group-hover:block"
/>
</div>
)}
@ -390,13 +410,34 @@ export default function GenericNode({
})}
data={data}
color={
nodeColors[
types[data.node?.template[templateField].type!]
] ??
nodeColors[
data.node?.template[templateField].type!
] ??
nodeColors.unknown
data.node?.template[templateField].input_types &&
data.node?.template[templateField].input_types!
.length > 0
? nodeColors[
data.node?.template[templateField]
.input_types![
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(
data.node?.template!,
@ -458,38 +499,55 @@ export default function GenericNode({
)}
</div>
{showNode && (
<Button
variant="secondary"
className={"group h-9 px-1.5"}
onClick={() => {
if (buildStatus === BuildStatus.BUILDING || isBuilding)
return;
setValidationStatus(null);
buildFlow({ nodeId: data.id });
}}
>
<Button variant="secondary" className={"group h-9 px-1.5"}>
<div>
<ShadTooltip
content={
buildStatus === BuildStatus.BUILDING ? (
<span> {statusBuilding} </span>
<span> {STATUS_BUILDING} </span>
) : !validationStatus ? (
<span className="flex">{statusBuild}</span>
<span className="flex">{STATUS_BUILD}</span>
) : (
<div className="max-h-96 overflow-auto">
{typeof validationStatus.params === "string"
? `${durationString}\n${validationStatus.params}`
.split("\n")
.map((line, index) => (
<div key={index}>{line}</div>
))
: durationString}
<div className="max-h-100">
<div>
{lastRunTime && (
<div className="justify-left flex text-muted-foreground">
<div>{RUN_TIMESTAMP_PREFIX}</div>
<div className="ml-1 text-status-blue">
{lastRunTime}
</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>
)
}
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)}
</div>
</ShadTooltip>
@ -607,13 +665,18 @@ export default function GenericNode({
data.node?.template[templateField].input_types!
.length > 0
? nodeColors[
data.node?.template[templateField]
.input_types![0]
data.node?.template[templateField].input_types![
data.node?.template[templateField]
.input_types!.length - 1
]
] ??
nodeColors[
types[
data.node?.template[templateField]
.input_types![0]
.input_types![
data.node?.template[templateField]
.input_types!.length - 1
]
]
]
: nodeColors[

View file

@ -6,7 +6,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "../../components/ui/popover";
import { zeroNotifications } from "../../constants/constants";
import { ZERO_NOTIFICATIONS } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore";
import { AlertDropdownType } from "../../types/alerts";
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">
{zeroNotifications}
{ZERO_NOTIFICATIONS}
</div>
)}
</div>

View file

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

View file

@ -1,17 +1,19 @@
import { useEffect, useState } from "react";
import {
CHAT_FORM_DIALOG_SUBTITLE,
outputsModalTitle,
textInputModalTitle,
OUTPUTS_MODAL_TITLE,
TEXT_INPUT_MODAL_TITLE,
} from "../../constants/constants";
import BaseModal from "../../modals/baseModal";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { NodeType } from "../../types/flow";
import { updateVerticesOrder } from "../../utils/buildUtils";
import { cn } from "../../utils/utils";
import AccordionComponent from "../AccordionComponent";
import IOInputField from "../IOInputField";
import IOOutputView from "../IOOutputView";
import ShadTooltip from "../ShadTooltipComponent";
import IconComponent from "../genericIconComponent";
import NewChatView from "../newChatView";
import { Badge } from "../ui/badge";
@ -50,6 +52,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
const [chatValue, setChatValue] = useState("");
const isBuilding = useFlowStore((state) => state.isBuilding);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setNode = useFlowStore((state) => state.setNode);
async function updateVertices() {
return updateVerticesOrder(currentFlow!.id, null);
@ -67,12 +70,22 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
setLockChat(true);
setChatValue("");
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);
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(() => {
@ -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">
<IconComponent className="h-4 w-4" name={"Type"} />
{textInputModalTitle}
{TEXT_INPUT_MODAL_TITLE}
</div>
{nodes
.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">
<IconComponent className="h-4 w-4" name={"FileType2"} />
{outputsModalTitle}
{OUTPUTS_MODAL_TITLE}
</div>
{nodes
.filter((node) =>
@ -213,9 +226,16 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
<AccordionComponent
trigger={
<div className="file-component-badge-div">
<Badge variant="gray" size="md">
{output.id}
</Badge>
<ShadTooltip
content={output.id}
styleClasses="z-50"
>
<div>
<Badge variant="gray" size="md">
{node.data.node.display_name}
</Badge>
</div>
</ShadTooltip>
{haveChat && (
<div
className="-mb-1 pr-4"
@ -255,7 +275,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
)}
{haveChat ? (
<div className="flex h-full w-full">
<div className="flex h-full min-w-96 flex-grow">
{selectedViewField && (
<div
className={cn(
@ -272,7 +292,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
</button>
{selectedViewField.type}
</div>
<div className="h-full">
<div className="h-full w-full">
{inputs.some(
(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>
{!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>
</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>
);
}

View file

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

View file

@ -126,7 +126,7 @@ export default function CodeTabsComponent({
<Tabs
value={activeTab}
className={
"api-modal-tabs " +
"api-modal-tabs m-0 inset-0 " +
(isMessage ? "dark " : "") +
(dark && isMessage ? "bg-background" : "")
}
@ -166,7 +166,7 @@ export default function CodeTabsComponent({
) : (
<IconComponent name="Clipboard" className="h-4 w-4" />
)}
{isCopied ? "Copied!" : "Copy code"}
{isCopied ? "Copied!" : "Copy Code"}
</button>
<button
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>
)}
<SyntaxHighlighter
className="mt-0 h-full w-full overflow-auto custom-scroll"
language={tab.mode}
language={tab.language}
style={oneDark}
className="mt-0 h-full overflow-auto custom-scroll rounded-sm text-left"
>
{tab.code}
</SyntaxHighlighter>

View file

@ -5,6 +5,7 @@ import { classNames } from "../../utils/utils";
import IconComponent from "../genericIconComponent";
export default function Dropdown({
isLoading,
value,
options,
onSelect,
@ -129,11 +130,17 @@ export default function Dropdown({
</>
) : (
<>
<div>
<span className="text-sm italic">
No parameters are available for display.
</span>
</div>
{(!isLoading && (
<div>
<span className="text-sm italic">
No parameters are available for display.
</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 { Node } from "reactflow";
import { savedHover } from "../../../../constants/constants";
import { SAVED_HOVER } from "../../../../constants/constants";
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
@ -138,7 +138,7 @@ export const MenuBar = ({
</div>
<ShadTooltip
content={
savedHover +
SAVED_HOVER +
new Date(currentFlow.updated_at ?? "").toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",

View file

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

View file

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

View file

@ -2,8 +2,8 @@ import { useEffect, useState } from "react";
import IconComponent from "../../../components/genericIconComponent";
import { Textarea } from "../../../components/ui/textarea";
import {
chatInputPlaceholder,
chatInputPlaceholderSend,
CHAT_INPUT_PLACEHOLDER,
CHAT_INPUT_PLACEHOLDER_SEND,
} from "../../../constants/constants";
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
import { chatInputType } from "../../../types/components";
@ -84,7 +84,7 @@ export default function ChatInput({
"form-modal-lockchat"
)}
placeholder={
noInput ? chatInputPlaceholder : chatInputPlaceholderSend
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
}
/>
<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">
<button className="code-block-modal-button" onClick={copyToClipboard}>
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
{isCopied ? "Copied!" : "Copy code"}
{isCopied ? "Copied!" : "Copy Code"}
</button>
<button className="code-block-modal-button" onClick={downloadAsFile}>
<IconDownload size={18} />

View file

@ -108,7 +108,7 @@ export default function ChatMessage({
chat.isSend ? "" : " "
)}
>
<div className={classNames("form-modal-chatbot-icon ")}>
<div className={classNames("form-modal-chatbot-icon")}>
{!chat.isSend ? (
<div className="form-modal-chat-image">
<div className="form-modal-chat-bot-icon ">
@ -134,7 +134,7 @@ export default function ChatMessage({
)}
</div>
{!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">
{hidden && chat.thought && chat.thought !== "" && (
<div
@ -155,9 +155,9 @@ export default function ChatMessage({
/>
)}
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
<div className="w-full">
<div className="w-full dark:text-white">
<div className="w-full">
<div className="w-full flex flex-col">
<div className="w-full flex flex-col dark:text-white">
<div className="w-full flex flex-col">
{useMemo(
() =>
chatMessage === "" && lockChat ? (
@ -169,7 +169,7 @@ export default function ChatMessage({
<Markdown
remarkPlugins={[remarkGfm, remarkMath]}
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"
components={{
pre({ node, ...props }) {

View file

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

View file

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

View file

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

View file

@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
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
)}
{...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 FLOW_BUILD_SUCCESS_ALERT = `Flow built 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
* @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 programmingLanguages: languageMap = {
@ -683,38 +691,39 @@ export const priorityFields = new Set(["code", "template"]);
export const INPUT_TYPES = new Set(["ChatInput", "TextInput"]);
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";
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.";
export const saveWApiCheckbox = "Save with my API keys";
export const editTextModalTitle = "Edit Text";
export const editTextPlaceholder = "Type message here.";
export const inputHandleHover = "Avaliable input components:";
export const outputHandleHover = "Avaliable output components:";
export const textInputModalTitle = "Text Inputs";
export const outputsModalTitle = "Text Outputs";
export const langflowChatTitle = "Langflow Chat";
export const chatInputPlaceholder =
export const SAVE_WITH_API_CHECKBOX = "Save with my API keys";
export const EDIT_TEXT_MODAL_TITLE = "Edit Text";
export const EDIT_TEXT_PLACEHOLDER = "Type message here.";
export const INPUT_HANDLER_HOVER = "Avaliable input components:";
export const OUTPUT_HANDLER_HOVER = "Avaliable output components:";
export const TEXT_INPUT_MODAL_TITLE = "Text Inputs";
export const OUTPUTS_MODAL_TITLE = "Text Outputs";
export const LANGFLOW_CHAT_TITLE = "Langflow Chat";
export const CHAT_INPUT_PLACEHOLDER =
"No chat input variables found. Click to run your flow.";
export const chatInputPlaceholderSend = "Send a message...";
export const editCodeTitle = "Edit Code";
export const myCollectionDesc =
"Manage your personal projects. Download or upload your collection.";
export const storeDesc = "Search flows and components from the community.";
export const storeTitle = "Langflow Store";
export const noApi = "You don't have an API key. ";
export const insertApi = "Insert your Langflow API key.";
export const invalidApi = "Your API key is not valid. ";
export const createApi = `Dont have an API key? Sign up at`;
export const statusBuild = "Build to validate status.";
export const statusBuilding = "Building...";
export const savedHover = "Last saved at ";
export const CHAT_INPUT_PLACEHOLDER_SEND = "Send a message...";
export const EDIT_CODE_TITLE = "Edit Code";
export const MY_COLLECTION_DESC =
"Manage your personal projects. Download and upload entire collections.";
export const STORE_DESC = "Explore community-shared flows and components.";
export const STORE_TITLE = "Langflow Store";
export const NO_API_KEY = "You don't have an API key. ";
export const INSERT_API_KEY = "Insert your Langflow API key.";
export const INVALID_API_KEY = "Your API key is not valid. ";
export const CREATE_API_KEY = `Dont have an API key? Sign up at`;
export const STATUS_BUILD = "Build to validate status.";
export const STATUS_BUILDING = "Building...";
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(
flowId: string,
nodeId?: string | null
startNodeId?: string | null,
stopNodeId?: string | null
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
// nodeId is optional and is a query parameter
// if nodeId is not provided, the API will return all vertices
const config = {};
if (nodeId) {
config["params"] = { component_id: nodeId };
if (stopNodeId) {
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);
}
@ -872,6 +875,7 @@ export async function postBuildVertex(
vertexId: string,
input_value: string
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
// input_value is optional and is a query parameter
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
input_value ? { inputs: { input_value: input_value } } : undefined

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,7 +18,7 @@ import {
} from "../../constants/alerts_constants";
import {
CODE_PROMPT_DIALOG_SUBTITLE,
editCodeTitle,
EDIT_CODE_TITLE,
} from "../../constants/constants";
import { postCustomComponent, postValidateCode } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
@ -35,11 +35,14 @@ export default function CodeAreaModal({
children,
dynamic,
readonly = false,
openModal,
selected,
setOpenModal,
open: myOpen,
setOpen: mySetOpen,
}: codeAreaModalPropsType): JSX.Element {
const [code, setCode] = useState(value);
const [open, setOpen] =
mySetOpen !== undefined && myOpen !== undefined
? [myOpen, mySetOpen]
: useState(false);
const dark = useDarkStore((state) => state.dark);
const unselectAll = useFlowStore((state) => state.unselectAll);
const [height, setHeight] = useState<string | null>(null);
@ -48,7 +51,6 @@ export default function CodeAreaModal({
const [error, setError] = useState<{
detail: { error: string | undefined; traceback: string | undefined };
} | null>(null);
const [open, setOpen] = useState(false);
const nodes = useFlowStore((state) => state.nodes);
useEffect(() => {
@ -59,10 +61,6 @@ export default function CodeAreaModal({
}
}, []);
useEffect(() => {
if (openModal) setOpen(true);
}, [openModal]);
function processNonDynamicField() {
postValidateCode(code)
.then((apiReturn) => {
@ -154,7 +152,7 @@ export default function CodeAreaModal({
<BaseModal open={open} setOpen={setOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={CODE_PROMPT_DIALOG_SUBTITLE}>
<span className="pr-2"> {editCodeTitle} </span>
<span className="pr-2"> {EDIT_CODE_TITLE} </span>
<IconComponent
name="prompts"
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 { API_WARNING_NOTICE_ALERT } from "../../constants/alerts_constants";
import {
ALERT_SAVE_WITH_API,
EXPORT_DIALOG_SUBTITLE,
alertSaveWApi,
saveWApiCheckbox,
SAVE_WITH_API_CHECKBOX,
} from "../../constants/constants";
import useAlertStore from "../../stores/alertStore";
import { useDarkStore } from "../../stores/darkStore";
@ -56,10 +56,12 @@ const ExportModal = forwardRef(
}}
/>
<label htmlFor="terms" className="export-modal-save-api text-sm ">
{saveWApiCheckbox}
{SAVE_WITH_API_CHECKBOX}
</label>
</div>
<span className=" text-xs text-destructive ">{alertSaveWApi}</span>
<span className=" text-xs text-destructive ">
{ALERT_SAVE_WITH_API}
</span>
</BaseModal.Content>
<BaseModal.Footer>

View file

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

View file

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

View file

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

View file

@ -12,11 +12,11 @@ import {
TEMP_NOTICE_ALERT,
} from "../../constants/alerts_constants";
import {
EDIT_TEXT_PLACEHOLDER,
INVALID_CHARACTERS,
MAX_WORDS_HIGHLIGHT,
PROMPT_DIALOG_SUBTITLE,
TEXT_DIALOG_SUBTITLE,
editTextPlaceholder,
regexHighlight,
} from "../../constants/constants";
import { TypeModal } from "../../constants/enums";
@ -97,13 +97,23 @@ export default function GenericModal({
useEffect(() => {
setInputValue(value);
}, [value, modalOpen]);
const coloredContent = (inputValue || "")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(regexHighlight, varHighlightHTML({ name: "$1" }))
.replace(/\n/g, "<br />");
.replace(regexHighlight, (match, p1, p2) => {
// 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 {
let sumOfCaracteres: number = 0;
wordsHighlight.forEach((element) => {
@ -159,7 +169,7 @@ export default function GenericModal({
setIsEdit(true);
return setErrorData({
title: PROMPT_ERROR_ALERT,
list: [error.toString()],
list: [error.response.data.detail ?? ""],
});
});
}
@ -217,7 +227,7 @@ export default function GenericModal({
setInputValue(event.target.value);
checkVariables(event.target.value);
}}
placeholder={editTextPlaceholder}
placeholder={EDIT_TEXT_PLACEHOLDER}
onKeyDown={(e) => {
handleKeyDown(e, inputValue, "");
}}
@ -239,7 +249,7 @@ export default function GenericModal({
onChange={(event) => {
setInputValue(event.target.value);
}}
placeholder={editTextPlaceholder}
placeholder={EDIT_TEXT_PLACEHOLDER}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
}}

View file

@ -48,7 +48,7 @@ export default function ShareModal({
const setErrorData = useAlertStore((state) => state.setErrorData);
const [internalOpen, internalSetOpen] = useState(children ? false : true);
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 [loadingTags, setLoadingTags] = useState<boolean>(false);
@ -179,7 +179,7 @@ export default function ShareModal({
</span>
<br></br>
<span className=" text-xs text-destructive ">
Warning: This action cannot be undone.
Note: This action is irreversible.
</span>
</ConfirmationModal.Content>
</ConfirmationModal>
@ -204,7 +204,7 @@ export default function ShareModal({
{children ? children : <></>}
</BaseModal.Trigger>
<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>
<IconComponent
@ -235,12 +235,11 @@ export default function ShareModal({
}}
/>
<label htmlFor="public" className="export-modal-save-api text-sm ">
Make {nameComponent} public
Set {nameComponent} status to public
</label>
</div>
<span className=" text-xs text-destructive ">
<b>Warning:</b> API keys in designated fields are removed when
sharing.
<b>Attention:</b> API keys in specified fields are automatically removed upon sharing.
</span>
</BaseModal.Content>

View file

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

View file

@ -315,25 +315,6 @@ export default function NodeToolbarComponent({
}}
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" />
</button>
</ShadTooltip>
@ -382,7 +363,7 @@ export default function NodeToolbarComponent({
</button>
</ShadTooltip>
<ShadTooltip content="Pin" side="top">
<ShadTooltip content="Freeze" side="top">
<button
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
@ -402,10 +383,11 @@ export default function NodeToolbarComponent({
}}
>
<IconComponent
name="Pin"
name="Snowflake"
className={cn(
"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>
@ -499,7 +481,7 @@ export default function NodeToolbarComponent({
value={"Share"}
disabled={!hasApiKey || !validApiKey}
>
<div className="flex" data-testid="save-button-modal">
<div className="flex" data-testid="share-button-modal">
<IconComponent
name="Share3"
className="relative top-0.5 -m-1 mr-1 h-6 w-6"
@ -633,6 +615,26 @@ export default function NodeToolbarComponent({
is_component={true}
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>
</div>
</>

View file

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

View file

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

View file

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

View file

@ -187,10 +187,12 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
typeof change === "function"
? change(get().nodes.find((node) => node.id === id)!)
: change;
get().setNodes((oldNodes) =>
oldNodes.map((node) => {
if (node.id === id) {
if((node.data as NodeDataType).node?.pinned){
(newChange.data as NodeDataType).node!.pinned = false;
}
return newChange;
}
return node;
@ -223,12 +225,10 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
selection.nodes.some((node) => node.data.type === "ChatInput") &&
checkChatInput(get().nodes)
) {
useAlertStore
.getState()
.setErrorData({
title: "Error pasting components",
list: ["You can only have one ChatInput component in the flow"],
});
useAlertStore.getState().setErrorData({
title: "Error pasting components",
list: ["You can only have one ChatInput component in the flow"],
});
return;
}
let minimumX = Infinity;
@ -418,10 +418,12 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
});
},
buildFlow: async ({
nodeId,
startNodeId,
stopNodeId,
input_value,
}: {
nodeId?: string;
startNodeId?: string;
stopNodeId?: string;
input_value?: string;
}) => {
get().setIsBuilding(true);
@ -446,25 +448,52 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
function handleBuildUpdate(
vertexBuildData: VertexBuildTypeAPI,
status: BuildStatus,
buildId: string
runId: string
) {
if (vertexBuildData && 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(
{ ...vertexBuildData, buildId },
{ ...vertexBuildData, buildId: runId },
vertexBuildData.id
);
useFlowStore.getState().updateBuildStatus([vertexBuildData.id], status);
}
await buildVertices({
input_value,
flowId: currentFlow!.id,
nodeId,
startNodeId,
stopNodeId,
onGetOrderSuccess: () => {
setNoticeData({ title: "Running components" });
},
onBuildComplete: () => {
const nodeId = startNodeId || stopNodeId;
if (nodeId) {
setSuccessData({
title: `${
@ -481,12 +510,14 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
onBuildError: (title, list, idList) => {
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILT);
setErrorData({ list, title });
get().setIsBuilding(false);
},
onBuildStart: (idList) => {
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING);
},
validateNodes: validateSubgraph,
});
get().setIsBuilding(false);
get().revertBuiltStatusFromBuilding();
},
getFlow: () => {
@ -499,11 +530,11 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
updateVerticesBuild: (
vertices: {
verticesIds: string[];
verticesOrder: string[][];
verticesLayers: string[][];
runId: string;
} | null
) => {
console.log("updateVerticesBuild", vertices);
set({ verticesBuild: vertices });
},
verticesBuild: null,
@ -520,17 +551,25 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
});
},
updateBuildStatus: (nodeIdList: string[], status: BuildStatus) => {
console.log("updateBuildStatus", nodeIdList, status);
const newFlowBuildStatus = { ...get().flowBuildStatus };
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 });
},
revertBuiltStatusFromBuilding: () => {
const newFlowBuildStatus = { ...get().flowBuildStatus };
Object.keys(newFlowBuildStatus).forEach((id) => {
if (newFlowBuildStatus[id] === BuildStatus.BUILDING) {
newFlowBuildStatus[id] = BuildStatus.BUILT;
if (newFlowBuildStatus[id].status === BuildStatus.BUILDING) {
newFlowBuildStatus[id].status = BuildStatus.BUILT;
}
});
},

View file

@ -845,13 +845,13 @@
}
.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 {
@apply flex items-center justify-between px-2 py-2;
}
.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 {
@apply mt-2 flex h-full w-full;
@ -902,7 +902,7 @@
@apply flex-max-width px-2 py-6 pl-4 pr-9;
}
.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 {
@apply flex flex-col items-center gap-1;

View file

@ -27,6 +27,7 @@
--radius: 0.5rem;
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
--round-btn-shadow: #00000063;
--ice: #31a3cc;
--error-background: #fef2f2;
--error-foreground: #991b1b;
@ -67,6 +68,7 @@
.dark {
--background: 224 35% 7.5%; /* hsl(224 40% 10%) */
--foreground: 213 31% 80%; /* hsl(213 31% 91%) */
--ice: #60A5FA;
--muted: 223 27% 11%; /* hsl(223 27% 11%) */
--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>;
display_name?: string;
name?: string;
refresh?: boolean;
[key: string]: any;
};
export type sendAllProps = {
@ -134,13 +135,15 @@ export type Component = {
};
export type VerticesOrderTypeAPI = {
ids: Array<Array<string>>;
ids: Array<string>;
run_id: string;
};
export type VertexBuildTypeAPI = {
id: string;
next_vertices_ids: Array<string>;
inactive_vertices: Array<string> | null;
run_id: string;
valid: boolean;
params: string;
data: VertexDataTypeAPI;

View file

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

View file

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

View file

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

View file

@ -12,8 +12,8 @@ import {
INPUT_TYPES,
LANGFLOW_SUPPORTED_TYPES,
OUTPUT_TYPES,
SUCCESS_BUILD,
specialCharsRegex,
successBuild,
} from "../constants/constants";
import { downloadFlowsFromDatabase } from "../controllers/API";
import {
@ -1092,7 +1092,7 @@ export function getGroupStatus(
flow: FlowType,
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 ids = nodes.map((n: NodeType) => n.data.id);
ids.forEach((id) => {

View file

@ -25,15 +25,23 @@ export function getTagsIds(
}
export function getInputsAndOutputs(nodes: Node[]) {
let inputs: { type: string; id: string }[] = [];
let outputs: { type: string; id: string }[] = [];
let inputs: { type: string; id: string; displayName: string }[] = [];
let outputs: { type: string; id: string; displayName: string }[] = [];
nodes.forEach((node) => {
const nodeData: NodeDataType = node.data as NodeDataType;
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)) {
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 };

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