Merge zustand/io/migration into codeShortcut
This commit is contained in:
commit
2760bd5428
122 changed files with 2731 additions and 2020 deletions
|
|
@ -56,6 +56,13 @@ LANGFLOW_REMOVE_API_KEYS=
|
||||||
# LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600)
|
# LANGFLOW_REDIS_CACHE_EXPIRE (default: 3600)
|
||||||
LANGFLOW_CACHE_TYPE=
|
LANGFLOW_CACHE_TYPE=
|
||||||
|
|
||||||
|
|
||||||
|
# Set AUTO_LOGIN to false if you want to disable auto login
|
||||||
|
# and use the login form to login. LANGFLOW_SUPERUSER and LANGFLOW_SUPERUSER_PASSWORD
|
||||||
|
# must be set if AUTO_LOGIN is set to false
|
||||||
|
# Values: true, false
|
||||||
|
LANGFLOW_AUTO_LOGIN=
|
||||||
|
|
||||||
# Superuser username
|
# Superuser username
|
||||||
# Example: LANGFLOW_SUPERUSER=admin
|
# Example: LANGFLOW_SUPERUSER=admin
|
||||||
LANGFLOW_SUPERUSER=
|
LANGFLOW_SUPERUSER=
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,6 @@ INVALID_CHARACTERS = {
|
||||||
")",
|
")",
|
||||||
"[",
|
"[",
|
||||||
"]",
|
"]",
|
||||||
"{",
|
|
||||||
"}",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
INVALID_NAMES = {
|
INVALID_NAMES = {
|
||||||
|
|
@ -88,73 +86,110 @@ def validate_prompt(template: str):
|
||||||
# Check if there are invalid characters in the input_variables
|
# Check if there are invalid characters in the input_variables
|
||||||
input_variables = check_input_variables(input_variables)
|
input_variables = check_input_variables(input_variables)
|
||||||
if any(var in INVALID_NAMES for var in input_variables):
|
if any(var in INVALID_NAMES for var in input_variables):
|
||||||
raise ValueError(f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. ")
|
raise ValueError(
|
||||||
|
f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. "
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
PromptTemplate(template=template, input_variables=input_variables)
|
PromptTemplate(template=template, input_variables=input_variables)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise ValueError(str(exc)) from exc
|
raise ValueError(f"Invalid prompt: {exc}") from exc
|
||||||
|
|
||||||
return input_variables
|
return input_variables
|
||||||
|
|
||||||
|
|
||||||
def check_input_variables(input_variables: list):
|
def is_json_like(var):
|
||||||
|
if var.startswith("{{") and var.endswith("}}"):
|
||||||
|
# If it is a double brance variable
|
||||||
|
# we don't want to validate any of its content
|
||||||
|
return True
|
||||||
|
# the above doesn't work on all cases because the json string can be multiline
|
||||||
|
# or indented which can add \n or spaces at the start or end of the string
|
||||||
|
# test_case_3 new_var == '\n{{\n "test": "hello",\n "text": "world"\n}}\n'
|
||||||
|
# what we can do is to remove the \n and spaces from the start and end of the string
|
||||||
|
# and then check if the string starts with {{ and ends with }}
|
||||||
|
var = var.strip()
|
||||||
|
var = var.replace("\n", "")
|
||||||
|
var = var.replace(" ", "")
|
||||||
|
# Now it should be a valid json string
|
||||||
|
return var.startswith("{{") and var.endswith("}}")
|
||||||
|
|
||||||
|
|
||||||
|
def fix_variable(var, invalid_chars, wrong_variables):
|
||||||
|
if not var:
|
||||||
|
return var, invalid_chars, wrong_variables
|
||||||
|
new_var = var
|
||||||
|
|
||||||
|
# Handle variables starting with a number
|
||||||
|
if var[0].isdigit():
|
||||||
|
invalid_chars.append(var[0])
|
||||||
|
new_var, invalid_chars, wrong_variables = fix_variable(
|
||||||
|
var[1:], invalid_chars, wrong_variables
|
||||||
|
)
|
||||||
|
|
||||||
|
# Temporarily replace {{ and }} to avoid treating them as invalid
|
||||||
|
new_var = new_var.replace("{{", "ᴛᴇᴍᴘᴏᴘᴇɴ").replace("}}", "ᴛᴇᴍᴘᴄʟᴏsᴇ")
|
||||||
|
|
||||||
|
# Remove invalid characters
|
||||||
|
for char in new_var:
|
||||||
|
if char in INVALID_CHARACTERS:
|
||||||
|
invalid_chars.append(char)
|
||||||
|
new_var = new_var.replace(char, "")
|
||||||
|
if var not in wrong_variables: # Avoid duplicating entries
|
||||||
|
wrong_variables.append(var)
|
||||||
|
|
||||||
|
# Restore {{ and }}
|
||||||
|
new_var = new_var.replace("ᴛᴇᴍᴘᴏᴘᴇɴ", "{{").replace("ᴛᴇᴍᴘᴄʟᴏsᴇ", "}}")
|
||||||
|
|
||||||
|
return new_var, invalid_chars, wrong_variables
|
||||||
|
|
||||||
|
|
||||||
|
def check_variable(var, invalid_chars, wrong_variables, empty_variables):
|
||||||
|
if any(char in invalid_chars for char in var):
|
||||||
|
wrong_variables.append(var)
|
||||||
|
elif var == "":
|
||||||
|
empty_variables.append(var)
|
||||||
|
return wrong_variables, empty_variables
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_errors(
|
||||||
|
input_variables, fixed_variables, wrong_variables, empty_variables
|
||||||
|
):
|
||||||
|
if any(var for var in input_variables if var not in fixed_variables):
|
||||||
|
error_message = (
|
||||||
|
f"Error: Input variables contain invalid characters or formats. \n"
|
||||||
|
f"Invalid variables: {', '.join(wrong_variables)}.\n"
|
||||||
|
f"Empty variables: {', '.join(empty_variables)}. \n"
|
||||||
|
f"Fixed variables: {', '.join(fixed_variables)}."
|
||||||
|
)
|
||||||
|
raise ValueError(error_message)
|
||||||
|
|
||||||
|
|
||||||
|
def check_input_variables(input_variables):
|
||||||
invalid_chars = []
|
invalid_chars = []
|
||||||
fixed_variables = []
|
fixed_variables = []
|
||||||
wrong_variables = []
|
wrong_variables = []
|
||||||
empty_variables = []
|
empty_variables = []
|
||||||
for variable in input_variables:
|
variables_to_check = []
|
||||||
new_var = variable
|
|
||||||
|
|
||||||
# if variable is empty, then we should add that to the wrong variables
|
for var in input_variables:
|
||||||
if not variable:
|
# First, let's check if the variable is a JSON string
|
||||||
empty_variables.append(variable)
|
# because if it is, it won't be considered a variable
|
||||||
|
# and we don't need to validate it
|
||||||
|
if is_json_like(var):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# if variable starts with a number we should add that to the invalid chars
|
new_var, wrong_variables, empty_variables = fix_variable(
|
||||||
# and wrong variables
|
var, invalid_chars, wrong_variables
|
||||||
if variable[0].isdigit():
|
|
||||||
invalid_chars.append(variable[0])
|
|
||||||
new_var = new_var.replace(variable[0], "")
|
|
||||||
wrong_variables.append(variable)
|
|
||||||
else:
|
|
||||||
for char in INVALID_CHARACTERS:
|
|
||||||
if char in variable:
|
|
||||||
invalid_chars.append(char)
|
|
||||||
new_var = new_var.replace(char, "")
|
|
||||||
wrong_variables.append(variable)
|
|
||||||
fixed_variables.append(new_var)
|
|
||||||
# If any of the input_variables is not in the fixed_variables, then it means that
|
|
||||||
# there are invalid characters in the input_variables
|
|
||||||
|
|
||||||
if any(var not in fixed_variables for var in input_variables):
|
|
||||||
error_message = build_error_message(
|
|
||||||
input_variables,
|
|
||||||
invalid_chars,
|
|
||||||
wrong_variables,
|
|
||||||
fixed_variables,
|
|
||||||
empty_variables,
|
|
||||||
)
|
)
|
||||||
raise ValueError(error_message)
|
wrong_variables, empty_variables = check_variable(
|
||||||
return input_variables
|
var, INVALID_CHARACTERS, wrong_variables, empty_variables
|
||||||
|
)
|
||||||
|
fixed_variables.append(new_var)
|
||||||
|
variables_to_check.append(var)
|
||||||
|
|
||||||
|
check_for_errors(
|
||||||
|
variables_to_check, fixed_variables, wrong_variables, empty_variables
|
||||||
|
)
|
||||||
|
|
||||||
def build_error_message(input_variables, invalid_chars, wrong_variables, fixed_variables, empty_variables):
|
return fixed_variables
|
||||||
input_variables_str = ", ".join([f"'{var}'" for var in input_variables])
|
|
||||||
error_string = f"Invalid input variables: {input_variables_str}. "
|
|
||||||
|
|
||||||
if wrong_variables and invalid_chars:
|
|
||||||
# fix the wrong variables replacing invalid chars and find them in the fixed variables
|
|
||||||
error_string_vars = "You can fix them by replacing the invalid characters: "
|
|
||||||
wvars = wrong_variables.copy()
|
|
||||||
for i, wrong_var in enumerate(wvars):
|
|
||||||
for char in invalid_chars:
|
|
||||||
wrong_var = wrong_var.replace(char, "")
|
|
||||||
if wrong_var in fixed_variables:
|
|
||||||
error_string_vars += f"'{wrong_variables[i]}' -> '{wrong_var}'"
|
|
||||||
error_string += error_string_vars
|
|
||||||
elif empty_variables:
|
|
||||||
error_string += f" There are {len(empty_variables)} empty variable{'s' if len(empty_variables) > 1 else ''}."
|
|
||||||
elif len(set(fixed_variables)) != len(fixed_variables):
|
|
||||||
error_string += "There are duplicate variables."
|
|
||||||
return error_string
|
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,10 @@ from langflow.services.auth.utils import get_current_active_user
|
||||||
from langflow.services.chat.service import ChatService
|
from langflow.services.chat.service import ChatService
|
||||||
from langflow.services.deps import get_chat_service, get_session, get_session_service
|
from langflow.services.deps import get_chat_service, get_session, get_session_service
|
||||||
from langflow.services.monitor.utils import log_vertex_build
|
from langflow.services.monitor.utils import log_vertex_build
|
||||||
from langflow.services.session.service import SessionService
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from langflow.graph.vertex.types import ChatVertex
|
from langflow.graph.vertex.types import ChatVertex
|
||||||
|
from langflow.services.session.service import SessionService
|
||||||
|
|
||||||
router = APIRouter(tags=["Chat"])
|
router = APIRouter(tags=["Chat"])
|
||||||
|
|
||||||
|
|
@ -49,7 +49,8 @@ async def try_running_celery_task(vertex, user_id):
|
||||||
@router.get("/build/{flow_id}/vertices", response_model=VerticesOrderResponse)
|
@router.get("/build/{flow_id}/vertices", response_model=VerticesOrderResponse)
|
||||||
async def get_vertices(
|
async def get_vertices(
|
||||||
flow_id: str,
|
flow_id: str,
|
||||||
component_id: Optional[str] = None,
|
stop_component_id: Optional[str] = None,
|
||||||
|
start_component_id: Optional[str] = None,
|
||||||
chat_service: "ChatService" = Depends(get_chat_service),
|
chat_service: "ChatService" = Depends(get_chat_service),
|
||||||
session=Depends(get_session),
|
session=Depends(get_session),
|
||||||
):
|
):
|
||||||
|
|
@ -60,9 +61,9 @@ async def get_vertices(
|
||||||
if cache := chat_service.get_cache(flow_id):
|
if cache := chat_service.get_cache(flow_id):
|
||||||
graph = cache.get("result")
|
graph = cache.get("result")
|
||||||
graph = build_and_cache_graph(flow_id, session, chat_service, graph)
|
graph = build_and_cache_graph(flow_id, session, chat_service, graph)
|
||||||
if component_id:
|
if stop_component_id or start_component_id:
|
||||||
try:
|
try:
|
||||||
vertices = graph.sort_vertices(component_id)
|
vertices = graph.sort_vertices(stop_component_id, start_component_id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(exc)
|
logger.error(exc)
|
||||||
vertices = graph.sort_vertices()
|
vertices = graph.sort_vertices()
|
||||||
|
|
@ -94,6 +95,7 @@ async def build_vertex(
|
||||||
"""Build a vertex instead of the entire graph."""
|
"""Build a vertex instead of the entire graph."""
|
||||||
{"inputs": {"input_value": "some value"}}
|
{"inputs": {"input_value": "some value"}}
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
|
next_vertices_ids = []
|
||||||
try:
|
try:
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
cache = chat_service.get_cache(flow_id)
|
cache = chat_service.get_cache(flow_id)
|
||||||
|
|
@ -119,6 +121,10 @@ async def build_vertex(
|
||||||
artifacts = vertex.artifacts
|
artifacts = vertex.artifacts
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"No result found for vertex {vertex_id}")
|
raise ValueError(f"No result found for vertex {vertex_id}")
|
||||||
|
next_vertices_ids = vertex.successors_ids
|
||||||
|
next_vertices_ids = [
|
||||||
|
v for v in next_vertices_ids if graph.should_run_vertex(v)
|
||||||
|
]
|
||||||
|
|
||||||
result_data_response = ResultDataResponse(**result_dict.model_dump())
|
result_data_response = ResultDataResponse(**result_dict.model_dump())
|
||||||
|
|
||||||
|
|
@ -155,7 +161,15 @@ async def build_vertex(
|
||||||
graph.reset_inactive_vertices()
|
graph.reset_inactive_vertices()
|
||||||
chat_service.set_cache(flow_id, graph)
|
chat_service.set_cache(flow_id, graph)
|
||||||
|
|
||||||
|
# graph.stop_vertex tells us if the user asked
|
||||||
|
# to stop the build of the graph at a certain vertex
|
||||||
|
# if it is in next_vertices_ids, we need to remove other
|
||||||
|
# vertices from next_vertices_ids
|
||||||
|
if graph.stop_vertex and graph.stop_vertex in next_vertices_ids:
|
||||||
|
next_vertices_ids = [graph.stop_vertex]
|
||||||
|
|
||||||
build_response = VertexBuildResponse(
|
build_response = VertexBuildResponse(
|
||||||
|
next_vertices_ids=next_vertices_ids,
|
||||||
inactive_vertices=inactive_vertices,
|
inactive_vertices=inactive_vertices,
|
||||||
valid=valid,
|
valid=valid,
|
||||||
params=params,
|
params=params,
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,15 @@ def get_all(
|
||||||
|
|
||||||
logger.debug("Building langchain types dict")
|
logger.debug("Building langchain types dict")
|
||||||
try:
|
try:
|
||||||
return get_all_types_dict(settings_service)
|
all_types_dict = get_all_types_dict(settings_service)
|
||||||
|
return all_types_dict
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
@router.post("/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True)
|
@router.post(
|
||||||
|
"/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True
|
||||||
|
)
|
||||||
async def run_flow_with_caching(
|
async def run_flow_with_caching(
|
||||||
session: Annotated[Session, Depends(get_session)],
|
session: Annotated[Session, Depends(get_session)],
|
||||||
flow_id: str,
|
flow_id: str,
|
||||||
|
|
@ -67,7 +70,9 @@ async def run_flow_with_caching(
|
||||||
input_values_dict = {}
|
input_values_dict = {}
|
||||||
|
|
||||||
if session_id:
|
if session_id:
|
||||||
session_data = await session_service.load_session(session_id, flow_id=flow_id)
|
session_data = await session_service.load_session(
|
||||||
|
session_id, flow_id=flow_id
|
||||||
|
)
|
||||||
graph, artifacts = session_data if session_data else (None, None)
|
graph, artifacts = session_data if session_data else (None, None)
|
||||||
task_result: Any = None
|
task_result: Any = None
|
||||||
if not graph:
|
if not graph:
|
||||||
|
|
@ -85,7 +90,11 @@ async def run_flow_with_caching(
|
||||||
else:
|
else:
|
||||||
# Get the flow that matches the flow_id and belongs to the user
|
# Get the flow that matches the flow_id and belongs to the user
|
||||||
# flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first()
|
# flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first()
|
||||||
flow = session.exec(select(Flow).where(Flow.id == flow_id).where(Flow.user_id == api_key_user.id)).first()
|
flow = session.exec(
|
||||||
|
select(Flow)
|
||||||
|
.where(Flow.id == flow_id)
|
||||||
|
.where(Flow.user_id == api_key_user.id)
|
||||||
|
).first()
|
||||||
if flow is None:
|
if flow is None:
|
||||||
raise ValueError(f"Flow {flow_id} not found")
|
raise ValueError(f"Flow {flow_id} not found")
|
||||||
|
|
||||||
|
|
@ -108,12 +117,18 @@ async def run_flow_with_caching(
|
||||||
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
|
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
|
||||||
if "badly formed hexadecimal UUID string" in str(exc):
|
if "badly formed hexadecimal UUID string" in str(exc):
|
||||||
# This means the Flow ID is not a valid UUID which means it can't find the flow
|
# This means the Flow ID is not a valid UUID which means it can't find the flow
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
||||||
|
) from exc
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
if f"Flow {flow_id} not found" in str(exc):
|
if f"Flow {flow_id} not found" in str(exc):
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
||||||
|
) from exc
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
|
@ -142,7 +157,8 @@ async def process(
|
||||||
"""
|
"""
|
||||||
# Raise a depreciation warning
|
# Raise a depreciation warning
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"The /process endpoint is deprecated and will be removed in a future version. " "Please use /run instead."
|
"The /process endpoint is deprecated and will be removed in a future version. "
|
||||||
|
"Please use /run instead."
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
|
@ -214,12 +230,16 @@ async def custom_component(
|
||||||
|
|
||||||
built_frontend_node = build_custom_component_template(component, user_id=user.id)
|
built_frontend_node = build_custom_component_template(component, user_id=user.id)
|
||||||
|
|
||||||
built_frontend_node = update_frontend_node_with_template_values(built_frontend_node, raw_code.frontend_node)
|
built_frontend_node = update_frontend_node_with_template_values(
|
||||||
|
built_frontend_node, raw_code.frontend_node
|
||||||
|
)
|
||||||
return built_frontend_node
|
return built_frontend_node
|
||||||
|
|
||||||
|
|
||||||
@router.post("/custom_component/reload", status_code=HTTPStatus.OK)
|
@router.post("/custom_component/reload", status_code=HTTPStatus.OK)
|
||||||
async def reload_custom_component(path: str, user: User = Depends(get_current_active_user)):
|
async def reload_custom_component(
|
||||||
|
path: str, user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
from langflow.interface.custom.utils import build_custom_component_template
|
from langflow.interface.custom.utils import build_custom_component_template
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -241,6 +261,8 @@ async def custom_component_update(
|
||||||
):
|
):
|
||||||
component = CustomComponent(code=raw_code.code)
|
component = CustomComponent(code=raw_code.code)
|
||||||
|
|
||||||
component_node = build_custom_component_template(component, user_id=user.id, update_field=raw_code.field)
|
component_node = build_custom_component_template(
|
||||||
|
component, user_id=user.id, update_field=raw_code.field
|
||||||
|
)
|
||||||
# Update the field
|
# Update the field
|
||||||
return component_node
|
return component_node
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,9 @@ class StreamData(BaseModel):
|
||||||
data: dict
|
data: dict
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n"
|
return (
|
||||||
|
f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomComponentCode(BaseModel):
|
class CustomComponentCode(BaseModel):
|
||||||
|
|
@ -214,7 +216,7 @@ class ApiKeyCreateRequest(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class VerticesOrderResponse(BaseModel):
|
class VerticesOrderResponse(BaseModel):
|
||||||
ids: List[List[str]]
|
ids: List[str]
|
||||||
run_id: UUID
|
run_id: UUID
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -227,9 +229,10 @@ class ResultDataResponse(BaseModel):
|
||||||
|
|
||||||
class VertexBuildResponse(BaseModel):
|
class VertexBuildResponse(BaseModel):
|
||||||
id: Optional[str] = None
|
id: Optional[str] = None
|
||||||
|
next_vertices_ids: Optional[List[str]] = None
|
||||||
inactive_vertices: Optional[List[str]] = None
|
inactive_vertices: Optional[List[str]] = None
|
||||||
valid: bool
|
valid: bool
|
||||||
params: Optional[str]
|
params: Optional[Any] = Field(default_factory=dict)
|
||||||
"""JSON string of the params."""
|
"""JSON string of the params."""
|
||||||
data: ResultDataResponse
|
data: ResultDataResponse
|
||||||
"""Mapping of vertex ids to result dict containing the param name and result value."""
|
"""Mapping of vertex ids to result dict containing the param name and result value."""
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,12 @@ class ConversationChainComponent(CustomComponent):
|
||||||
chain = ConversationChain(llm=llm)
|
chain = ConversationChain(llm=llm)
|
||||||
else:
|
else:
|
||||||
chain = ConversationChain(llm=llm, memory=memory)
|
chain = ConversationChain(llm=llm, memory=memory)
|
||||||
result = chain.invoke({chain.input_key: input_value})
|
result = chain.invoke(inputs)
|
||||||
# result is an AIMessage which is a subclass of BaseMessage
|
|
||||||
# We need to check if it is a string or a BaseMessage
|
|
||||||
result_str: Text = ""
|
|
||||||
if hasattr(result, "content") and isinstance(result.content, str):
|
if hasattr(result, "content") and isinstance(result.content, str):
|
||||||
result_str = result.content
|
result = result.content
|
||||||
elif isinstance(result, str):
|
elif isinstance(result, str):
|
||||||
result_str = result
|
result = result
|
||||||
else:
|
else:
|
||||||
# is dict
|
result = result.get("response")
|
||||||
result_str = Text(result.get("response"))
|
self.status = result
|
||||||
self.status = result_str
|
return result
|
||||||
return result_str
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from langchain_core.documents import Document
|
from typing import List
|
||||||
|
|
||||||
from langflow import CustomComponent
|
from langflow import CustomComponent
|
||||||
|
from langflow.schema import Record
|
||||||
from langflow.utils.constants import LOADERS_INFO
|
from langflow.utils.constants import LOADERS_INFO
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -10,7 +11,9 @@ class FileLoaderComponent(CustomComponent):
|
||||||
beta = True
|
beta = True
|
||||||
|
|
||||||
def build_config(self):
|
def build_config(self):
|
||||||
loader_options = ["Automatic"] + [loader_info["name"] for loader_info in LOADERS_INFO]
|
loader_options = ["Automatic"] + [
|
||||||
|
loader_info["name"] for loader_info in LOADERS_INFO
|
||||||
|
]
|
||||||
|
|
||||||
file_types = []
|
file_types = []
|
||||||
suffixes = []
|
suffixes = []
|
||||||
|
|
@ -74,7 +77,7 @@ class FileLoaderComponent(CustomComponent):
|
||||||
"code": {"show": False},
|
"code": {"show": False},
|
||||||
}
|
}
|
||||||
|
|
||||||
def build(self, file_path: str, loader: str) -> Document:
|
def build(self, file_path: str, loader: str) -> List[Record]:
|
||||||
file_type = file_path.split(".")[-1]
|
file_type = file_path.split(".")[-1]
|
||||||
|
|
||||||
# Map the loader to the correct loader class
|
# Map the loader to the correct loader class
|
||||||
|
|
@ -102,7 +105,9 @@ class FileLoaderComponent(CustomComponent):
|
||||||
if isinstance(selected_loader_info, dict):
|
if isinstance(selected_loader_info, dict):
|
||||||
loader_import: str = selected_loader_info["import"]
|
loader_import: str = selected_loader_info["import"]
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Loader info for {loader} is not a dict\nLoader info:\n{selected_loader_info}")
|
raise ValueError(
|
||||||
|
f"Loader info for {loader} is not a dict\nLoader info:\n{selected_loader_info}"
|
||||||
|
)
|
||||||
module_name, class_name = loader_import.rsplit(".", 1)
|
module_name, class_name = loader_import.rsplit(".", 1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -110,7 +115,10 @@ class FileLoaderComponent(CustomComponent):
|
||||||
loader_module = __import__(module_name, fromlist=[class_name])
|
loader_module = __import__(module_name, fromlist=[class_name])
|
||||||
loader_instance = getattr(loader_module, class_name)
|
loader_instance = getattr(loader_module, class_name)
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
raise ValueError(f"Loader {loader} could not be imported\nLoader info:\n{selected_loader_info}") from e
|
raise ValueError(
|
||||||
|
f"Loader {loader} could not be imported\nLoader info:\n{selected_loader_info}"
|
||||||
|
) from e
|
||||||
|
|
||||||
result = loader_instance(file_path=file_path)
|
result = loader_instance(file_path=file_path)
|
||||||
return result.load()
|
docs = result.load()
|
||||||
|
return self.to_records(docs)
|
||||||
|
|
|
||||||
|
|
@ -70,11 +70,17 @@ class GatherRecordsComponent(CustomComponent):
|
||||||
|
|
||||||
glob = "**/*" if recursive else "*"
|
glob = "**/*" if recursive else "*"
|
||||||
paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob)
|
paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob)
|
||||||
file_paths = [Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)]
|
file_paths = [
|
||||||
|
Text(p)
|
||||||
|
for p in paths
|
||||||
|
if p.is_file() and match_types(p) and is_not_hidden(p)
|
||||||
|
]
|
||||||
|
|
||||||
return file_paths
|
return file_paths
|
||||||
|
|
||||||
def parse_file_to_record(self, file_path: str, silent_errors: bool) -> Optional[Record]:
|
def parse_file_to_record(
|
||||||
|
self, file_path: str, silent_errors: bool
|
||||||
|
) -> Optional[Record]:
|
||||||
# Use the partition function to load the file
|
# Use the partition function to load the file
|
||||||
from unstructured.partition.auto import partition # type: ignore
|
from unstructured.partition.auto import partition # type: ignore
|
||||||
|
|
||||||
|
|
@ -100,9 +106,14 @@ class GatherRecordsComponent(CustomComponent):
|
||||||
use_multithreading: bool,
|
use_multithreading: bool,
|
||||||
) -> List[Optional[Record]]:
|
) -> List[Optional[Record]]:
|
||||||
if use_multithreading:
|
if use_multithreading:
|
||||||
records = self.parallel_load_records(file_paths, silent_errors, max_concurrency)
|
records = self.parallel_load_records(
|
||||||
|
file_paths, silent_errors, max_concurrency
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
records = [self.parse_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
records = [
|
||||||
|
self.parse_file_to_record(file_path, silent_errors)
|
||||||
|
for file_path in file_paths
|
||||||
|
]
|
||||||
records = list(filter(None, records))
|
records = list(filter(None, records))
|
||||||
return records
|
return records
|
||||||
|
|
||||||
|
|
@ -131,13 +142,20 @@ class GatherRecordsComponent(CustomComponent):
|
||||||
if types is None:
|
if types is None:
|
||||||
types = []
|
types = []
|
||||||
resolved_path = self.resolve_path(path)
|
resolved_path = self.resolve_path(path)
|
||||||
file_paths = self.retrieve_file_paths(resolved_path, types, load_hidden, recursive, depth)
|
file_paths = self.retrieve_file_paths(
|
||||||
|
resolved_path, types, load_hidden, recursive, depth
|
||||||
|
)
|
||||||
loaded_records = []
|
loaded_records = []
|
||||||
|
|
||||||
if use_multithreading:
|
if use_multithreading:
|
||||||
loaded_records = self.parallel_load_records(file_paths, silent_errors, max_concurrency)
|
loaded_records = self.parallel_load_records(
|
||||||
|
file_paths, silent_errors, max_concurrency
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
loaded_records = [self.parse_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
loaded_records = [
|
||||||
|
self.parse_file_to_record(file_path, silent_errors)
|
||||||
|
for file_path in file_paths
|
||||||
|
]
|
||||||
loaded_records = list(filter(None, loaded_records))
|
loaded_records = list(filter(None, loaded_records))
|
||||||
self.status = loaded_records
|
self.status = loaded_records
|
||||||
return loaded_records
|
return loaded_records
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ class ChatOutput(ChatComponent):
|
||||||
|
|
||||||
def build(
|
def build(
|
||||||
self,
|
self,
|
||||||
sender: Optional[str] = "User",
|
sender: Optional[str] = "Machine",
|
||||||
sender_name: Optional[str] = "User",
|
sender_name: Optional[str] = "AI",
|
||||||
input_value: Optional[str] = None,
|
input_value: Optional[str] = None,
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
return_record: Optional[bool] = False,
|
return_record: Optional[bool] = False,
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,12 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from langflow import CustomComponent
|
from langflow.components.io.base.text import TextComponent
|
||||||
from langflow.field_typing import Text
|
from langflow.field_typing import Text
|
||||||
|
|
||||||
|
|
||||||
class TextInput(CustomComponent):
|
class TextInput(TextComponent):
|
||||||
display_name = "Text Input"
|
display_name = "Text Input"
|
||||||
description = "Used to pass text input to the next component."
|
description = "Used to pass text input to the next component."
|
||||||
|
|
||||||
field_config = {
|
|
||||||
"input_value": {"display_name": "Value", "multiline": True},
|
|
||||||
}
|
|
||||||
|
|
||||||
def build(self, input_value: Optional[str] = "") -> Text:
|
def build(self, input_value: Optional[str] = "") -> Text:
|
||||||
self.status = input_value
|
return super().build(input_value=input_value)
|
||||||
if not input_value:
|
|
||||||
input_value = ""
|
|
||||||
return input_value
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from langflow import CustomComponent
|
from langflow.components.io.base.text import TextComponent
|
||||||
from langflow.field_typing import Text
|
from langflow.field_typing import Text
|
||||||
|
|
||||||
|
|
||||||
class TextOutput(CustomComponent):
|
class TextOutput(TextComponent):
|
||||||
display_name = "Text Output"
|
display_name = "Text Output"
|
||||||
description = "Used to pass text output to the next component."
|
description = "Used to pass text output to the next component."
|
||||||
|
|
||||||
field_config = {
|
field_config = {
|
||||||
"value": {"display_name": "Value"},
|
"input_value": {"display_name": "Value"},
|
||||||
}
|
}
|
||||||
|
|
||||||
def build(self, value: Optional[str] = "") -> Text:
|
def build(self, input_value: Optional[Text] = "") -> Text:
|
||||||
self.status = value
|
return super().build(input_value=input_value)
|
||||||
if not value:
|
|
||||||
value = ""
|
|
||||||
return value
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,9 @@ class ChatComponent(CustomComponent):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if not session_id or not sender or not sender_name:
|
if not session_id or not sender or not sender_name:
|
||||||
raise ValueError("All of session_id, sender, and sender_name must be provided.")
|
raise ValueError(
|
||||||
|
"All of session_id, sender, and sender_name must be provided."
|
||||||
|
)
|
||||||
if isinstance(message, Record):
|
if isinstance(message, Record):
|
||||||
record = message
|
record = message
|
||||||
record.data.update(
|
record.data.update(
|
||||||
|
|
|
||||||
19
src/backend/langflow/components/io/base/text.py
Normal file
19
src/backend/langflow/components/io/base/text.py
Normal 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
|
||||||
|
|
@ -23,7 +23,7 @@ class PromptComponent(CustomComponent):
|
||||||
prompt_template = PromptTemplate.from_template(Text(template))
|
prompt_template = PromptTemplate.from_template(Text(template))
|
||||||
|
|
||||||
attributes_to_check = ["text", "page_content"]
|
attributes_to_check = ["text", "page_content"]
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.copy().items():
|
||||||
for attribute in attributes_to_check:
|
for attribute in attributes_to_check:
|
||||||
if hasattr(value, attribute):
|
if hasattr(value, attribute):
|
||||||
kwargs[key] = getattr(value, attribute)
|
kwargs[key] = getattr(value, attribute)
|
||||||
|
|
|
||||||
19
src/backend/langflow/components/utilities/ListFlows.py
Normal file
19
src/backend/langflow/components/utilities/ListFlows.py
Normal 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
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from langflow import CustomComponent
|
from langflow import CustomComponent
|
||||||
from langflow.field_typing import Text
|
from langflow.field_typing import Text
|
||||||
|
from langflow.helpers.record import records_to_text
|
||||||
from langflow.schema import Record
|
from langflow.schema import Record
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -27,7 +28,6 @@ class RecordsAsTextComponent(CustomComponent):
|
||||||
if isinstance(records, Record):
|
if isinstance(records, Record):
|
||||||
records = [records]
|
records = [records]
|
||||||
|
|
||||||
formated_records = [template.format(text=record.text, data=record.data, **record.data) for record in records]
|
result_string = records_to_text(template, records)
|
||||||
result_string = "\n".join(formated_records)
|
|
||||||
self.status = result_string
|
self.status = result_string
|
||||||
return result_string
|
return result_string
|
||||||
|
|
|
||||||
58
src/backend/langflow/components/utilities/RunFlow.py
Normal file
58
src/backend/langflow/components/utilities/RunFlow.py
Normal 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
|
||||||
38
src/backend/langflow/components/utilities/SharedState.py
Normal file
38
src/backend/langflow/components/utilities/SharedState.py
Normal 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
|
||||||
|
|
@ -93,7 +93,8 @@ class ChromaSearchComponent(LCVectorStoreComponent):
|
||||||
|
|
||||||
if chroma_server_host is not None:
|
if chroma_server_host is not None:
|
||||||
chroma_settings = chromadb.config.Settings(
|
chroma_settings = chromadb.config.Settings(
|
||||||
chroma_server_cors_allow_origins=chroma_server_cors_allow_origins or None,
|
chroma_server_cors_allow_origins=chroma_server_cors_allow_origins
|
||||||
|
or None,
|
||||||
chroma_server_host=chroma_server_host,
|
chroma_server_host=chroma_server_host,
|
||||||
chroma_server_port=chroma_server_port or None,
|
chroma_server_port=chroma_server_port or None,
|
||||||
chroma_server_grpc_port=chroma_server_grpc_port or None,
|
chroma_server_grpc_port=chroma_server_grpc_port or None,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,9 @@ class FAISSSearchComponent(LCVectorStoreComponent):
|
||||||
if not folder_path:
|
if not folder_path:
|
||||||
raise ValueError("Folder path is required to save the FAISS index.")
|
raise ValueError("Folder path is required to save the FAISS index.")
|
||||||
path = self.resolve_path(folder_path)
|
path = self.resolve_path(folder_path)
|
||||||
vector_store = FAISS.load_local(folder_path=Text(path), embeddings=embedding, index_name=index_name)
|
vector_store = FAISS.load_local(
|
||||||
|
folder_path=Text(path), embeddings=embedding, index_name=index_name
|
||||||
|
)
|
||||||
if not vector_store:
|
if not vector_store:
|
||||||
raise ValueError("Failed to load the FAISS index.")
|
raise ValueError("Failed to load the FAISS index.")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@ class SupabaseSearchComponent(LCVectorStoreComponent):
|
||||||
supabase_url: str = "",
|
supabase_url: str = "",
|
||||||
table_name: str = "",
|
table_name: str = "",
|
||||||
) -> List[Record]:
|
) -> List[Record]:
|
||||||
supabase: Client = create_client(supabase_url, supabase_key=supabase_service_key)
|
supabase: Client = create_client(
|
||||||
|
supabase_url, supabase_key=supabase_service_key
|
||||||
|
)
|
||||||
vector_store = SupabaseVectorStore(
|
vector_store = SupabaseVectorStore(
|
||||||
client=supabase,
|
client=supabase,
|
||||||
embedding=embedding,
|
embedding=embedding,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ from langflow.schema import Record
|
||||||
class VectaraSearchComponent(VectaraComponent, LCVectorStoreComponent):
|
class VectaraSearchComponent(VectaraComponent, LCVectorStoreComponent):
|
||||||
display_name: str = "Vectara Search"
|
display_name: str = "Vectara Search"
|
||||||
description: str = "Search a Vectara Vector Store for similar documents."
|
description: str = "Search a Vectara Vector Store for similar documents."
|
||||||
documentation = "https://python.langchain.com/docs/integrations/vectorstores/vectara"
|
documentation = (
|
||||||
|
"https://python.langchain.com/docs/integrations/vectorstores/vectara"
|
||||||
|
)
|
||||||
beta = True
|
beta = True
|
||||||
icon = "Vectara"
|
icon = "Vectara"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ from langflow.schema import Record
|
||||||
class WeaviateSearchVectorStore(WeaviateVectorStoreComponent, LCVectorStoreComponent):
|
class WeaviateSearchVectorStore(WeaviateVectorStoreComponent, LCVectorStoreComponent):
|
||||||
display_name: str = "Weaviate Search"
|
display_name: str = "Weaviate Search"
|
||||||
description: str = "Search a Weaviate Vector Store for similar documents."
|
description: str = "Search a Weaviate Vector Store for similar documents."
|
||||||
documentation = "https://python.langchain.com/docs/integrations/vectorstores/weaviate"
|
documentation = (
|
||||||
|
"https://python.langchain.com/docs/integrations/vectorstores/weaviate"
|
||||||
|
)
|
||||||
beta = True
|
beta = True
|
||||||
icon = "Weaviate"
|
icon = "Weaviate"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ from langchain_core.vectorstores import VectorStore
|
||||||
|
|
||||||
from langflow import CustomComponent
|
from langflow import CustomComponent
|
||||||
from langflow.field_typing import Text
|
from langflow.field_typing import Text
|
||||||
from langflow.schema import Record, docs_to_records
|
from langflow.helpers.record import docs_to_records
|
||||||
|
from langflow.schema import Record
|
||||||
|
|
||||||
|
|
||||||
class LCVectorStoreComponent(CustomComponent):
|
class LCVectorStoreComponent(CustomComponent):
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ class PGVectorSearchComponent(PGVectorComponent, LCVectorStoreComponent):
|
||||||
|
|
||||||
display_name: str = "PGVector Search"
|
display_name: str = "PGVector Search"
|
||||||
description: str = "Search a PGVector Store for similar documents."
|
description: str = "Search a PGVector Store for similar documents."
|
||||||
documentation = "https://python.langchain.com/docs/integrations/vectorstores/pgvector"
|
documentation = (
|
||||||
|
"https://python.langchain.com/docs/integrations/vectorstores/pgvector"
|
||||||
|
)
|
||||||
|
|
||||||
def build_config(self):
|
def build_config(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
from typing import TYPE_CHECKING, Any, List, Optional
|
from typing import TYPE_CHECKING, Any, List, Optional
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
from langflow.graph.edge.utils import build_clean_params
|
from langflow.graph.edge.utils import build_clean_params
|
||||||
from langflow.graph.schema import INPUT_FIELD_NAME
|
from langflow.graph.schema import INPUT_FIELD_NAME
|
||||||
from langflow.services.deps import get_monitor_service
|
from langflow.services.deps import get_monitor_service
|
||||||
from langflow.services.monitor.utils import log_message
|
from langflow.services.monitor.utils import log_message
|
||||||
|
from loguru import logger
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from langflow.graph.vertex.base import Vertex
|
from langflow.graph.vertex.base import Vertex
|
||||||
|
|
||||||
|
|
||||||
class SourceHandle(BaseModel):
|
class SourceHandle(BaseModel):
|
||||||
baseClasses: List[str] = Field(..., description="List of base classes for the source handle.")
|
baseClasses: List[str] = Field(
|
||||||
|
..., description="List of base classes for the source handle."
|
||||||
|
)
|
||||||
dataType: str = Field(..., description="Data type for the source handle.")
|
dataType: str = Field(..., description="Data type for the source handle.")
|
||||||
id: str = Field(..., description="Unique identifier for the source handle.")
|
id: str = Field(..., description="Unique identifier for the source handle.")
|
||||||
|
|
||||||
|
|
@ -21,7 +22,9 @@ class SourceHandle(BaseModel):
|
||||||
class TargetHandle(BaseModel):
|
class TargetHandle(BaseModel):
|
||||||
fieldName: str = Field(..., description="Field name for the target handle.")
|
fieldName: str = Field(..., description="Field name for the target handle.")
|
||||||
id: str = Field(..., description="Unique identifier for the target handle.")
|
id: str = Field(..., description="Unique identifier for the target handle.")
|
||||||
inputTypes: Optional[List[str]] = Field(None, description="List of input types for the target handle.")
|
inputTypes: Optional[List[str]] = Field(
|
||||||
|
None, description="List of input types for the target handle."
|
||||||
|
)
|
||||||
type: str = Field(..., description="Type of the target handle.")
|
type: str = Field(..., description="Type of the target handle.")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -50,16 +53,24 @@ class Edge:
|
||||||
|
|
||||||
def validate_handles(self, source, target) -> None:
|
def validate_handles(self, source, target) -> None:
|
||||||
if self.target_handle.inputTypes is None:
|
if self.target_handle.inputTypes is None:
|
||||||
self.valid_handles = self.target_handle.type in self.source_handle.baseClasses
|
self.valid_handles = (
|
||||||
|
self.target_handle.type in self.source_handle.baseClasses
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.valid_handles = (
|
self.valid_handles = (
|
||||||
any(baseClass in self.target_handle.inputTypes for baseClass in self.source_handle.baseClasses)
|
any(
|
||||||
|
baseClass in self.target_handle.inputTypes
|
||||||
|
for baseClass in self.source_handle.baseClasses
|
||||||
|
)
|
||||||
or self.target_handle.type in self.source_handle.baseClasses
|
or self.target_handle.type in self.source_handle.baseClasses
|
||||||
)
|
)
|
||||||
if not self.valid_handles:
|
if not self.valid_handles:
|
||||||
logger.debug(self.source_handle)
|
logger.debug(self.source_handle)
|
||||||
logger.debug(self.target_handle)
|
logger.debug(self.target_handle)
|
||||||
raise ValueError(f"Edge between {source.vertex_type} and {target.vertex_type} " f"has invalid handles")
|
raise ValueError(
|
||||||
|
f"Edge between {source.vertex_type} and {target.vertex_type} "
|
||||||
|
f"has invalid handles"
|
||||||
|
)
|
||||||
|
|
||||||
def __setstate__(self, state):
|
def __setstate__(self, state):
|
||||||
self.source_id = state["source_id"]
|
self.source_id = state["source_id"]
|
||||||
|
|
@ -76,7 +87,11 @@ class Edge:
|
||||||
# Both lists contain strings and sometimes a string contains the value we are
|
# Both lists contain strings and sometimes a string contains the value we are
|
||||||
# looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"]
|
# looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"]
|
||||||
# so we need to check if any of the strings in source_types is in target_reqs
|
# so we need to check if any of the strings in source_types is in target_reqs
|
||||||
self.valid = any(output in target_req for output in self.source_types for target_req in self.target_reqs)
|
self.valid = any(
|
||||||
|
output in target_req
|
||||||
|
for output in self.source_types
|
||||||
|
for target_req in self.target_reqs
|
||||||
|
)
|
||||||
# Get what type of input the target node is expecting
|
# Get what type of input the target node is expecting
|
||||||
|
|
||||||
self.matched_type = next(
|
self.matched_type = next(
|
||||||
|
|
@ -87,7 +102,10 @@ class Edge:
|
||||||
if no_matched_type:
|
if no_matched_type:
|
||||||
logger.debug(self.source_types)
|
logger.debug(self.source_types)
|
||||||
logger.debug(self.target_reqs)
|
logger.debug(self.target_reqs)
|
||||||
raise ValueError(f"Edge between {source.vertex_type} and {target.vertex_type} " f"has no matched type")
|
raise ValueError(
|
||||||
|
f"Edge between {source.vertex_type} and {target.vertex_type} "
|
||||||
|
f"has no matched type"
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
|
|
@ -98,8 +116,12 @@ class Edge:
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return hash(self.__repr__())
|
return hash(self.__repr__())
|
||||||
|
|
||||||
def __eq__(self, __value: object) -> bool:
|
def __eq__(self, __o: object) -> bool:
|
||||||
return self.__repr__() == __value.__repr__() if isinstance(__value, Edge) else False
|
# Create a better way to compare edges
|
||||||
|
return (
|
||||||
|
self._source_handle == __o._source_handle
|
||||||
|
and self._target_handle == __o._target_handle
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ContractEdge(Edge):
|
class ContractEdge(Edge):
|
||||||
|
|
@ -156,7 +178,9 @@ class ContractEdge(Edge):
|
||||||
return f"{self.source_id} -[{self.target_param}]-> {self.target_id}"
|
return f"{self.source_id} -[{self.target_param}]-> {self.target_id}"
|
||||||
|
|
||||||
|
|
||||||
def log_transaction(edge: ContractEdge, source: "Vertex", target: "Vertex", status, error=None):
|
def log_transaction(
|
||||||
|
edge: ContractEdge, source: "Vertex", target: "Vertex", status, error=None
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
monitor_service = get_monitor_service()
|
monitor_service = get_monitor_service()
|
||||||
clean_params = build_clean_params(target)
|
clean_params = build_clean_params(target)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
|
from itertools import chain
|
||||||
from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Type, Union
|
from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Type, Union
|
||||||
|
|
||||||
from langchain.chains.base import Chain
|
from langchain.chains.base import Chain
|
||||||
|
|
@ -54,6 +55,10 @@ class Graph:
|
||||||
|
|
||||||
self._vertices = self._graph_data["nodes"]
|
self._vertices = self._graph_data["nodes"]
|
||||||
self._edges = self._graph_data["edges"]
|
self._edges = self._graph_data["edges"]
|
||||||
|
self.vertices_layers = []
|
||||||
|
self.vertices_to_run = set()
|
||||||
|
self.stop_vertex = None
|
||||||
|
|
||||||
self.inactive_vertices: set = set()
|
self.inactive_vertices: set = set()
|
||||||
self.edges: List[ContractEdge] = []
|
self.edges: List[ContractEdge] = []
|
||||||
self.vertices: List[Vertex] = []
|
self.vertices: List[Vertex] = []
|
||||||
|
|
@ -104,7 +109,12 @@ class Graph:
|
||||||
vertex = self.get_vertex(vertex_id)
|
vertex = self.get_vertex(vertex_id)
|
||||||
if vertex is None:
|
if vertex is None:
|
||||||
raise ValueError(f"Vertex {vertex_id} not found")
|
raise ValueError(f"Vertex {vertex_id} not found")
|
||||||
if not stream and hasattr(vertex, "consume_async_generator"):
|
|
||||||
|
if (
|
||||||
|
not vertex.result
|
||||||
|
and not stream
|
||||||
|
and hasattr(vertex, "consume_async_generator")
|
||||||
|
):
|
||||||
await vertex.consume_async_generator()
|
await vertex.consume_async_generator()
|
||||||
outputs.append(vertex.result)
|
outputs.append(vertex.result)
|
||||||
return outputs
|
return outputs
|
||||||
|
|
@ -126,6 +136,14 @@ class Graph:
|
||||||
outputs.extend(run_outputs)
|
outputs.extend(run_outputs)
|
||||||
return outputs
|
return outputs
|
||||||
|
|
||||||
|
# vertices_layers is a list of lists ordered by the order the vertices
|
||||||
|
# should be built.
|
||||||
|
# We need to create a new method that will take the vertices_layers
|
||||||
|
# and return the next vertex to be built.
|
||||||
|
def next_vertex_to_build(self):
|
||||||
|
"""Returns the next vertex to be built."""
|
||||||
|
yield from chain.from_iterable(self.vertices_layers)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self):
|
def metadata(self):
|
||||||
return {
|
return {
|
||||||
|
|
@ -236,7 +254,7 @@ class Graph:
|
||||||
self.edges = new_edges
|
self.edges = new_edges
|
||||||
|
|
||||||
def vertex_data_is_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool:
|
def vertex_data_is_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool:
|
||||||
data_is_equivalent = vertex.__repr__() == other_vertex.__repr__()
|
data_is_equivalent = vertex == other_vertex
|
||||||
if not data_is_equivalent:
|
if not data_is_equivalent:
|
||||||
return False
|
return False
|
||||||
return self.vertex_edges_are_identical(vertex, other_vertex)
|
return self.vertex_edges_are_identical(vertex, other_vertex)
|
||||||
|
|
@ -262,40 +280,64 @@ class Graph:
|
||||||
# Find vertices that are in self but not in other (removed vertices)
|
# Find vertices that are in self but not in other (removed vertices)
|
||||||
removed_vertex_ids = existing_vertex_ids - other_vertex_ids
|
removed_vertex_ids = existing_vertex_ids - other_vertex_ids
|
||||||
|
|
||||||
# Update existing vertices that have changed
|
# Remove vertices that are not in the other graph
|
||||||
for vertex_id in existing_vertex_ids.intersection(other_vertex_ids):
|
|
||||||
self_vertex = self.get_vertex(vertex_id)
|
|
||||||
other_vertex = other.get_vertex(vertex_id)
|
|
||||||
if not self.vertex_data_is_identical(self_vertex, other_vertex):
|
|
||||||
self_vertex._data = other_vertex._data
|
|
||||||
self_vertex._parse_data()
|
|
||||||
# Now we update the edges of the vertex
|
|
||||||
self.update_edges_from_vertex(self_vertex, other_vertex)
|
|
||||||
self_vertex.params = {}
|
|
||||||
self_vertex._build_params()
|
|
||||||
self_vertex.graph = self
|
|
||||||
# If the vertex is pinned, we don't want
|
|
||||||
# to reset the results nor the _built attribute
|
|
||||||
if not self_vertex.pinned:
|
|
||||||
self_vertex._built = False
|
|
||||||
self_vertex.result = None
|
|
||||||
self_vertex.artifacts = {}
|
|
||||||
self_vertex.set_top_level(self.top_level_vertices)
|
|
||||||
self.reset_all_edges_of_vertex(self_vertex)
|
|
||||||
|
|
||||||
# Remove vertices
|
|
||||||
for vertex_id in removed_vertex_ids:
|
for vertex_id in removed_vertex_ids:
|
||||||
self.remove_vertex(vertex_id)
|
self.remove_vertex(vertex_id)
|
||||||
|
|
||||||
|
# The order here matters because adding the vertex is required
|
||||||
|
# if any of them have edges that point to any of the new vertices
|
||||||
|
# By adding them first, them adding the edges we ensure that the
|
||||||
|
# edges have valid vertices to point to
|
||||||
|
|
||||||
# Add new vertices
|
# Add new vertices
|
||||||
for vertex_id in new_vertex_ids:
|
for vertex_id in new_vertex_ids:
|
||||||
new_vertex = other.get_vertex(vertex_id)
|
new_vertex = other.get_vertex(vertex_id)
|
||||||
self._add_vertex(new_vertex)
|
self._add_vertex(new_vertex)
|
||||||
|
|
||||||
|
# Now update the edges
|
||||||
|
for vertex_id in new_vertex_ids:
|
||||||
|
new_vertex = other.get_vertex(vertex_id)
|
||||||
|
self._update_edges(new_vertex)
|
||||||
|
# Graph is set at the end because the edges come from the graph
|
||||||
|
# and the other graph is where the new edges and vertices come from
|
||||||
|
new_vertex.graph = self
|
||||||
|
|
||||||
|
# Update existing vertices that have changed
|
||||||
|
for vertex_id in existing_vertex_ids.intersection(other_vertex_ids):
|
||||||
|
self_vertex = self.get_vertex(vertex_id)
|
||||||
|
other_vertex = other.get_vertex(vertex_id)
|
||||||
|
# If the vertices are not identical, update the vertex
|
||||||
|
if not self.vertex_data_is_identical(self_vertex, other_vertex):
|
||||||
|
self.update_vertex_from_another(self_vertex, other_vertex)
|
||||||
|
|
||||||
self.build_graph_maps()
|
self.build_graph_maps()
|
||||||
self.increment_update_count()
|
self.increment_update_count()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def update_vertex_from_another(self, vertex: Vertex, other_vertex: Vertex) -> None:
|
||||||
|
"""
|
||||||
|
Updates a vertex from another vertex.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vertex (Vertex): The vertex to be updated.
|
||||||
|
other_vertex (Vertex): The vertex to update from.
|
||||||
|
"""
|
||||||
|
vertex._data = other_vertex._data
|
||||||
|
vertex._parse_data()
|
||||||
|
# Now we update the edges of the vertex
|
||||||
|
self.update_edges_from_vertex(vertex, other_vertex)
|
||||||
|
vertex.params = {}
|
||||||
|
vertex._build_params()
|
||||||
|
vertex.graph = self
|
||||||
|
# If the vertex is pinned, we don't want
|
||||||
|
# to reset the results nor the _built attribute
|
||||||
|
if not vertex.pinned:
|
||||||
|
vertex._built = False
|
||||||
|
vertex.result = None
|
||||||
|
vertex.artifacts = {}
|
||||||
|
vertex.set_top_level(self.top_level_vertices)
|
||||||
|
self.reset_all_edges_of_vertex(vertex)
|
||||||
|
|
||||||
def reset_all_edges_of_vertex(self, vertex: Vertex) -> None:
|
def reset_all_edges_of_vertex(self, vertex: Vertex) -> None:
|
||||||
"""Resets all the edges of a vertex."""
|
"""Resets all the edges of a vertex."""
|
||||||
for edge in vertex.edges:
|
for edge in vertex.edges:
|
||||||
|
|
@ -306,12 +348,24 @@ class Graph:
|
||||||
_vertex._build_params()
|
_vertex._build_params()
|
||||||
|
|
||||||
def _add_vertex(self, vertex: Vertex) -> None:
|
def _add_vertex(self, vertex: Vertex) -> None:
|
||||||
"""Adds a new vertex to the graph."""
|
"""Adds a vertex to the graph."""
|
||||||
self.vertices.append(vertex)
|
self.vertices.append(vertex)
|
||||||
self.vertex_map[vertex.id] = vertex
|
self.vertex_map[vertex.id] = vertex
|
||||||
|
|
||||||
|
def add_vertex(self, vertex: Vertex) -> None:
|
||||||
|
"""Adds a new vertex to the graph."""
|
||||||
|
self._add_vertex(vertex)
|
||||||
|
self._update_edges(vertex)
|
||||||
|
|
||||||
|
def _update_edges(self, vertex: Vertex) -> None:
|
||||||
|
"""Updates the edges of a vertex."""
|
||||||
# Vertex has edges, so we need to update the edges
|
# Vertex has edges, so we need to update the edges
|
||||||
for edge in vertex.edges:
|
for edge in vertex.edges:
|
||||||
if edge.source_id in self.vertex_map and edge.target_id in self.vertex_map:
|
if (
|
||||||
|
edge not in self.edges
|
||||||
|
and edge.source_id in self.vertex_map
|
||||||
|
and edge.target_id in self.vertex_map
|
||||||
|
):
|
||||||
self.edges.append(edge)
|
self.edges.append(edge)
|
||||||
|
|
||||||
def _build_graph(self) -> None:
|
def _build_graph(self) -> None:
|
||||||
|
|
@ -409,13 +463,19 @@ class Graph:
|
||||||
async def process(self) -> "Graph":
|
async def process(self) -> "Graph":
|
||||||
"""Processes the graph with vertices in each layer run in parallel."""
|
"""Processes the graph with vertices in each layer run in parallel."""
|
||||||
vertices_layers = self.sorted_vertices_layers
|
vertices_layers = self.sorted_vertices_layers
|
||||||
|
vertex_task_run_count = {}
|
||||||
for layer_index, layer in enumerate(vertices_layers):
|
for layer_index, layer in enumerate(vertices_layers):
|
||||||
tasks = []
|
tasks = []
|
||||||
for vertex_id in layer:
|
for vertex_id in layer:
|
||||||
vertex = self.get_vertex(vertex_id)
|
vertex = self.get_vertex(vertex_id)
|
||||||
task = asyncio.create_task(vertex.build(), name=f"layer-{layer_index}-vertex-{vertex_id}")
|
task = asyncio.create_task(
|
||||||
|
vertex.build(),
|
||||||
|
name=f"{vertex.display_name} Run {vertex_task_run_count.get(vertex_id, 0)}",
|
||||||
|
)
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
|
vertex_task_run_count[vertex_id] = (
|
||||||
|
vertex_task_run_count.get(vertex_id, 0) + 1
|
||||||
|
)
|
||||||
logger.debug(f"Running layer {layer_index} with {len(tasks)} tasks")
|
logger.debug(f"Running layer {layer_index} with {len(tasks)} tasks")
|
||||||
await self._execute_tasks(tasks)
|
await self._execute_tasks(tasks)
|
||||||
logger.debug("Graph processing complete")
|
logger.debug("Graph processing complete")
|
||||||
|
|
@ -434,6 +494,10 @@ class Graph:
|
||||||
# coroutine has not attribute get_name
|
# coroutine has not attribute get_name
|
||||||
task_name = tasks[i].get_name()
|
task_name = tasks[i].get_name()
|
||||||
logger.error(f"Task {task_name} failed with exception: {e}")
|
logger.error(f"Task {task_name} failed with exception: {e}")
|
||||||
|
# Cancel all remaining tasks
|
||||||
|
for t in tasks[i:]:
|
||||||
|
t.cancel()
|
||||||
|
raise e
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def topological_sort(self) -> List[Vertex]:
|
def topological_sort(self) -> List[Vertex]:
|
||||||
|
|
@ -573,22 +637,54 @@ class Graph:
|
||||||
edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges])
|
edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges])
|
||||||
return f"Graph:\nNodes: {vertex_ids}\nConnections:\n{edges_repr}"
|
return f"Graph:\nNodes: {vertex_ids}\nConnections:\n{edges_repr}"
|
||||||
|
|
||||||
def sort_up_to_vertex(self, vertex_id: str) -> List[Vertex]:
|
def sort_up_to_vertex(self, vertex_id: str, is_start: bool = False) -> List[Vertex]:
|
||||||
"""Cuts the graph up to a given vertex and sorts the resulting subgraph."""
|
"""Cuts the graph up to a given vertex and sorts the resulting subgraph."""
|
||||||
# Initial setup
|
# Initial setup
|
||||||
visited = set() # To keep track of visited vertices
|
visited = set() # To keep track of visited vertices
|
||||||
|
excluded = set() # To keep track of vertices that should be excluded
|
||||||
stack = [vertex_id] # Use a list as a stack for DFS
|
stack = [vertex_id] # Use a list as a stack for DFS
|
||||||
|
|
||||||
|
def get_successors(vertex, recursive=True):
|
||||||
|
# Recursively get the successors of the current vertex
|
||||||
|
successors = vertex.successors
|
||||||
|
if not successors:
|
||||||
|
return []
|
||||||
|
successors_result = []
|
||||||
|
for successor in successors:
|
||||||
|
# Just return a list of successors
|
||||||
|
if recursive:
|
||||||
|
next_successors = get_successors(successor)
|
||||||
|
successors_result.extend(next_successors)
|
||||||
|
successors_result.append(successor)
|
||||||
|
return successors_result
|
||||||
|
|
||||||
# DFS to collect all vertices that can reach the specified vertex
|
# DFS to collect all vertices that can reach the specified vertex
|
||||||
while stack:
|
while stack:
|
||||||
current_id = stack.pop()
|
current_id = stack.pop()
|
||||||
if current_id not in visited:
|
if current_id not in visited and current_id not in excluded:
|
||||||
visited.add(current_id)
|
visited.add(current_id)
|
||||||
current_vertex = self.get_vertex(current_id)
|
current_vertex = self.get_vertex(current_id)
|
||||||
# Assuming get_predecessors is a method that returns all vertices with edges to current_vertex
|
# Assuming get_predecessors is a method that returns all vertices with edges to current_vertex
|
||||||
for predecessor in current_vertex.predecessors:
|
for predecessor in current_vertex.predecessors:
|
||||||
stack.append(predecessor.id)
|
stack.append(predecessor.id)
|
||||||
|
|
||||||
|
if current_id == vertex_id:
|
||||||
|
# We should add to visited all the vertices that are successors of the current vertex
|
||||||
|
# and their successors and so on
|
||||||
|
# if the vertex is a start, it means we are starting from the beginning
|
||||||
|
# and getting successors
|
||||||
|
for successor in current_vertex.successors:
|
||||||
|
if is_start:
|
||||||
|
stack.append(successor.id)
|
||||||
|
else:
|
||||||
|
excluded.add(successor.id)
|
||||||
|
all_successors = get_successors(successor)
|
||||||
|
for successor in all_successors:
|
||||||
|
if is_start:
|
||||||
|
stack.append(successor.id)
|
||||||
|
else:
|
||||||
|
excluded.add(successor.id)
|
||||||
|
|
||||||
# Filter the original graph's vertices and edges to keep only those in `visited`
|
# Filter the original graph's vertices and edges to keep only those in `visited`
|
||||||
vertices_to_keep = [self.get_vertex(vid) for vid in visited]
|
vertices_to_keep = [self.get_vertex(vid) for vid in visited]
|
||||||
|
|
||||||
|
|
@ -679,19 +775,41 @@ class Graph:
|
||||||
|
|
||||||
return vertices_layers
|
return vertices_layers
|
||||||
|
|
||||||
def sort_vertices(self, component_id: Optional[str] = None) -> List[List[str]]:
|
def sort_vertices(
|
||||||
|
self,
|
||||||
|
stop_component_id: Optional[str] = None,
|
||||||
|
start_component_id: Optional[str] = None,
|
||||||
|
) -> List[str]:
|
||||||
"""Sorts the vertices in the graph."""
|
"""Sorts the vertices in the graph."""
|
||||||
self.mark_all_vertices("ACTIVE")
|
self.mark_all_vertices("ACTIVE")
|
||||||
if component_id:
|
if stop_component_id:
|
||||||
vertices = self.sort_up_to_vertex(component_id)
|
self.stop_vertex = stop_component_id
|
||||||
|
vertices = self.sort_up_to_vertex(stop_component_id)
|
||||||
|
elif start_component_id:
|
||||||
|
vertices = self.sort_up_to_vertex(start_component_id, is_start=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
vertices = self.vertices
|
vertices = self.vertices
|
||||||
vertices_layers = self.layered_topological_sort(vertices)
|
vertices_layers = self.layered_topological_sort(vertices)
|
||||||
vertices_layers = self.sort_by_avg_build_time(vertices_layers)
|
vertices_layers = self.sort_by_avg_build_time(vertices_layers)
|
||||||
vertices_layers = self.sort_chat_inputs_first(vertices_layers)
|
# vertices_layers = self.sort_chat_inputs_first(vertices_layers)
|
||||||
self.increment_run_count()
|
self.increment_run_count()
|
||||||
self._sorted_vertices_layers = vertices_layers
|
self._sorted_vertices_layers = vertices_layers
|
||||||
return vertices_layers
|
first_layer = vertices_layers[0]
|
||||||
|
# save the only the rest
|
||||||
|
self.vertices_layers = vertices_layers[1:]
|
||||||
|
self.vertices_to_run = {
|
||||||
|
vertex for vertex in chain.from_iterable(vertices_layers)
|
||||||
|
}
|
||||||
|
# Return just the first layer
|
||||||
|
return first_layer
|
||||||
|
|
||||||
|
def should_run_vertex(self, vertex_id: str) -> bool:
|
||||||
|
"""Returns whether a component should be run."""
|
||||||
|
should_run = vertex_id in self.vertices_to_run
|
||||||
|
if should_run:
|
||||||
|
self.vertices_to_run.remove(vertex_id)
|
||||||
|
return should_run
|
||||||
|
|
||||||
def sort_interface_components_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
|
def sort_interface_components_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
|
||||||
"""Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first."""
|
"""Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first."""
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,10 @@ class VertexTypesDict(LazyLoadDictBase):
|
||||||
**{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()},
|
**{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()},
|
||||||
**{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()},
|
**{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()},
|
||||||
**{t: types.OutputParserVertex for t in output_parser_creator.to_list()},
|
**{t: types.OutputParserVertex for t in output_parser_creator.to_list()},
|
||||||
**{t: types.CustomComponentVertex for t in custom_component_creator.to_list()},
|
**{
|
||||||
|
t: types.CustomComponentVertex
|
||||||
|
for t in custom_component_creator.to_list()
|
||||||
|
},
|
||||||
**{t: types.RetrieverVertex for t in retriever_creator.to_list()},
|
**{t: types.RetrieverVertex for t in retriever_creator.to_list()},
|
||||||
**{t: types.ChatVertex for t in CHAT_COMPONENTS},
|
**{t: types.ChatVertex for t in CHAT_COMPONENTS},
|
||||||
**{t: types.RoutingVertex for t in ROUTING_COMPONENTS},
|
**{t: types.RoutingVertex for t in ROUTING_COMPONENTS},
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ from typing import Any, Optional
|
||||||
from pydantic import BaseModel, Field, field_serializer
|
from pydantic import BaseModel, Field, field_serializer
|
||||||
|
|
||||||
from langflow.graph.utils import serialize_field
|
from langflow.graph.utils import serialize_field
|
||||||
from langflow.utils.schemas import ContainsEnumMeta
|
from langflow.utils.schemas import ChatOutputResponse, ContainsEnumMeta
|
||||||
|
|
||||||
|
|
||||||
class ResultData(BaseModel):
|
class ResultData(BaseModel):
|
||||||
results: Optional[Any] = Field(default_factory=dict)
|
results: Optional[Any] = Field(default_factory=dict)
|
||||||
artifacts: Optional[Any] = Field(default_factory=dict)
|
artifacts: Optional[Any] = Field(default_factory=dict)
|
||||||
|
messages: Optional[list[ChatOutputResponse]] = Field(default_factory=list)
|
||||||
timedelta: Optional[float] = None
|
timedelta: Optional[float] = None
|
||||||
duration: Optional[str] = None
|
duration: Optional[str] = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,17 @@ import ast
|
||||||
import inspect
|
import inspect
|
||||||
import types
|
import types
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Dict, List, Optional
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
AsyncIterator,
|
||||||
|
Callable,
|
||||||
|
Coroutine,
|
||||||
|
Dict,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
)
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|
@ -18,6 +28,7 @@ from langflow.interface.initialize import loading
|
||||||
from langflow.interface.listing import lazy_load_dict
|
from langflow.interface.listing import lazy_load_dict
|
||||||
from langflow.services.deps import get_storage_service
|
from langflow.services.deps import get_storage_service
|
||||||
from langflow.utils.constants import DIRECT_TYPES
|
from langflow.utils.constants import DIRECT_TYPES
|
||||||
|
from langflow.utils.schemas import ChatOutputResponse
|
||||||
from langflow.utils.util import sync_to_async
|
from langflow.utils.util import sync_to_async
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -120,6 +131,8 @@ class Vertex:
|
||||||
if not isinstance(result, (dict, str)) and hasattr(result, "content"):
|
if not isinstance(result, (dict, str)) and hasattr(result, "content"):
|
||||||
return result.content
|
return result.content
|
||||||
return result
|
return result
|
||||||
|
if isinstance(self._built_object, str):
|
||||||
|
self._built_result = self._built_object
|
||||||
|
|
||||||
if isinstance(self._built_result, UnbuiltResult):
|
if isinstance(self._built_result, UnbuiltResult):
|
||||||
return {}
|
return {}
|
||||||
|
|
@ -140,6 +153,10 @@ class Vertex:
|
||||||
def successors(self) -> List["Vertex"]:
|
def successors(self) -> List["Vertex"]:
|
||||||
return self.graph.get_successors(self)
|
return self.graph.get_successors(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def successors_ids(self) -> List[str]:
|
||||||
|
return self.graph.successor_map.get(self.id, [])
|
||||||
|
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
return {
|
return {
|
||||||
"_data": self._data,
|
"_data": self._data,
|
||||||
|
|
@ -360,15 +377,43 @@ class Vertex:
|
||||||
|
|
||||||
self._built = True
|
self._built = True
|
||||||
|
|
||||||
|
def extract_messages_from_artifacts(self, artifacts: Dict[str, Any]) -> List[str]:
|
||||||
|
"""
|
||||||
|
Extracts messages from the artifacts.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
artifacts (Dict[str, Any]): The artifacts to extract messages from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: The extracted messages.
|
||||||
|
"""
|
||||||
|
messages = []
|
||||||
|
for key, artifact in artifacts.items():
|
||||||
|
if not isinstance(artifact, dict):
|
||||||
|
continue
|
||||||
|
if "message" in artifact:
|
||||||
|
chat_output_response = ChatOutputResponse(
|
||||||
|
message=artifact["message"],
|
||||||
|
sender=artifact.get("sender"),
|
||||||
|
sender_name=artifact.get("sender_name"),
|
||||||
|
session_id=artifact.get("session_id"),
|
||||||
|
component_id=self.id,
|
||||||
|
)
|
||||||
|
messages.append(chat_output_response.model_dump(exclude_none=True))
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
def _finalize_build(self):
|
def _finalize_build(self):
|
||||||
result_dict = self.get_built_result()
|
result_dict = self.get_built_result()
|
||||||
# We need to set the artifacts to pass information
|
# We need to set the artifacts to pass information
|
||||||
# to the frontend
|
# to the frontend
|
||||||
self.set_artifacts()
|
self.set_artifacts()
|
||||||
artifacts = self.artifacts
|
artifacts = self.artifacts
|
||||||
|
messages = self.extract_messages_from_artifacts(artifacts)
|
||||||
result_dict = ResultData(
|
result_dict = ResultData(
|
||||||
results=result_dict,
|
results=result_dict,
|
||||||
artifacts=artifacts,
|
artifacts=artifacts,
|
||||||
|
messages=messages,
|
||||||
)
|
)
|
||||||
self.set_result(result_dict)
|
self.set_result(result_dict)
|
||||||
|
|
||||||
|
|
@ -463,12 +508,21 @@ class Vertex:
|
||||||
self.params[key] = []
|
self.params[key] = []
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
built = await node.get_result(requester=self, user_id=user_id)
|
built = await node.get_result(requester=self, user_id=user_id)
|
||||||
|
# Weird check to see if the params[key] is a list
|
||||||
|
# because sometimes it is a Record and breaks the code
|
||||||
|
if not isinstance(self.params[key], list):
|
||||||
|
self.params[key] = [self.params[key]]
|
||||||
|
|
||||||
if isinstance(built, list):
|
if isinstance(built, list):
|
||||||
if key not in self.params:
|
|
||||||
self.params[key] = []
|
|
||||||
self.params[key].extend(built)
|
self.params[key].extend(built)
|
||||||
else:
|
else:
|
||||||
self.params[key].append(built)
|
try:
|
||||||
|
self.params[key].append(built)
|
||||||
|
except AttributeError as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise ValueError(
|
||||||
|
f"Error building node {self.display_name}: {str(e)}"
|
||||||
|
) from e
|
||||||
|
|
||||||
def _handle_func(self, key, result):
|
def _handle_func(self, key, result):
|
||||||
"""
|
"""
|
||||||
|
|
@ -536,6 +590,11 @@ class Vertex:
|
||||||
message += " Make sure your build method returns a component."
|
message += " Make sure your build method returns a component."
|
||||||
|
|
||||||
logger.warning(message)
|
logger.warning(message)
|
||||||
|
elif isinstance(self._built_object, (Iterator, AsyncIterator)):
|
||||||
|
if self.display_name in ["Text Output"]:
|
||||||
|
raise ValueError(
|
||||||
|
f"You are trying to stream to a {self.display_name}. Try using a Chat Output instead."
|
||||||
|
)
|
||||||
|
|
||||||
def _reset(self, params_update: Optional[Dict[str, Any]] = None):
|
def _reset(self, params_update: Optional[Dict[str, Any]] = None):
|
||||||
self._built = False
|
self._built = False
|
||||||
|
|
@ -545,6 +604,9 @@ class Vertex:
|
||||||
self.steps_ran = []
|
self.steps_ran = []
|
||||||
self._build_params()
|
self._build_params()
|
||||||
|
|
||||||
|
def _is_chat_input(self):
|
||||||
|
return False
|
||||||
|
|
||||||
def build_inactive(self):
|
def build_inactive(self):
|
||||||
# Just set the results to None
|
# Just set the results to None
|
||||||
self._built = True
|
self._built = True
|
||||||
|
|
@ -567,7 +629,7 @@ class Vertex:
|
||||||
return self.get_requester_result(requester)
|
return self.get_requester_result(requester)
|
||||||
self._reset()
|
self._reset()
|
||||||
|
|
||||||
if self.is_input and inputs is not None:
|
if self._is_chat_input() and inputs is not None:
|
||||||
self.update_raw_params(inputs)
|
self.update_raw_params(inputs)
|
||||||
|
|
||||||
# Run steps
|
# Run steps
|
||||||
|
|
@ -603,7 +665,15 @@ class Vertex:
|
||||||
|
|
||||||
def __eq__(self, __o: object) -> bool:
|
def __eq__(self, __o: object) -> bool:
|
||||||
try:
|
try:
|
||||||
return self.id == __o.id if isinstance(__o, Vertex) else False
|
if not isinstance(__o, Vertex):
|
||||||
|
return False
|
||||||
|
# We should create a more robust comparison
|
||||||
|
# for the Vertex class
|
||||||
|
ids_are_equal = self.id == __o.id
|
||||||
|
# self._data is a dict and we need to compare them
|
||||||
|
# to check if they are equal
|
||||||
|
data_are_equal = self.data == __o.data
|
||||||
|
return ids_are_equal and data_are_equal
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import ast
|
import ast
|
||||||
import json
|
import json
|
||||||
from typing import AsyncIterator, Callable, Dict, Iterator, List, Optional, Union
|
from typing import (AsyncIterator, Callable, Dict, Iterator, List, Optional,
|
||||||
|
Union)
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from langchain_core.messages import AIMessage
|
from langchain_core.messages import AIMessage
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from langflow.graph.schema import INPUT_FIELD_NAME
|
from langflow.graph.schema import INPUT_FIELD_NAME, InterfaceComponentTypes
|
||||||
from langflow.graph.utils import UnbuiltObject, flatten_list, serialize_field
|
from langflow.graph.utils import UnbuiltObject, flatten_list, serialize_field
|
||||||
from langflow.graph.vertex.base import StatefulVertex, StatelessVertex
|
from langflow.graph.vertex.base import StatefulVertex, StatelessVertex
|
||||||
from langflow.interface.utils import extract_input_variables_from_prompt
|
from langflow.interface.utils import extract_input_variables_from_prompt
|
||||||
|
|
@ -123,12 +124,14 @@ class DocumentLoaderVertex(StatefulVertex):
|
||||||
# show how many documents are in the list?
|
# show how many documents are in the list?
|
||||||
|
|
||||||
if not isinstance(self._built_object, UnbuiltObject):
|
if not isinstance(self._built_object, UnbuiltObject):
|
||||||
avg_length = sum(len(doc.page_content) for doc in self._built_object if hasattr(doc, "page_content")) / len(
|
avg_length = sum(
|
||||||
self._built_object
|
len(record.text)
|
||||||
)
|
for record in self._built_object
|
||||||
return f"""{self.display_name}({len(self._built_object)} documents)
|
if hasattr(record, "text")
|
||||||
\nAvg. Document Length (characters): {int(avg_length)}
|
) / len(self._built_object)
|
||||||
Documents: {self._built_object[:3]}..."""
|
return f"""{self.display_name}({len(self._built_object)} records)
|
||||||
|
\nAvg. Record Length (characters): {int(avg_length)}
|
||||||
|
Records: {self._built_object[:3]}..."""
|
||||||
return f"{self.vertex_type}()"
|
return f"{self.vertex_type}()"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -198,7 +201,9 @@ class TextSplitterVertex(StatefulVertex):
|
||||||
# show how many documents are in the list?
|
# show how many documents are in the list?
|
||||||
|
|
||||||
if not isinstance(self._built_object, UnbuiltObject):
|
if not isinstance(self._built_object, UnbuiltObject):
|
||||||
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(self._built_object)
|
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(
|
||||||
|
self._built_object
|
||||||
|
)
|
||||||
return f"""{self.vertex_type}({len(self._built_object)} documents)
|
return f"""{self.vertex_type}({len(self._built_object)} documents)
|
||||||
\nAvg. Document Length (characters): {int(avg_length)}
|
\nAvg. Document Length (characters): {int(avg_length)}
|
||||||
\nDocuments: {self._built_object[:3]}..."""
|
\nDocuments: {self._built_object[:3]}..."""
|
||||||
|
|
@ -245,18 +250,27 @@ class PromptVertex(StatelessVertex):
|
||||||
user_id = kwargs.get("user_id", None)
|
user_id = kwargs.get("user_id", None)
|
||||||
tools = kwargs.get("tools", [])
|
tools = kwargs.get("tools", [])
|
||||||
if not self._built or force:
|
if not self._built or force:
|
||||||
if "input_variables" not in self.params or self.params["input_variables"] is None:
|
if (
|
||||||
|
"input_variables" not in self.params
|
||||||
|
or self.params["input_variables"] is None
|
||||||
|
):
|
||||||
self.params["input_variables"] = []
|
self.params["input_variables"] = []
|
||||||
# Check if it is a ZeroShotPrompt and needs a tool
|
# Check if it is a ZeroShotPrompt and needs a tool
|
||||||
if "ShotPrompt" in self.vertex_type:
|
if "ShotPrompt" in self.vertex_type:
|
||||||
tools = [tool_node.build(user_id=user_id) for tool_node in tools] if tools is not None else []
|
tools = (
|
||||||
|
[tool_node.build(user_id=user_id) for tool_node in tools]
|
||||||
|
if tools is not None
|
||||||
|
else []
|
||||||
|
)
|
||||||
# flatten the list of tools if it is a list of lists
|
# flatten the list of tools if it is a list of lists
|
||||||
# first check if it is a list
|
# first check if it is a list
|
||||||
if tools and isinstance(tools, list) and isinstance(tools[0], list):
|
if tools and isinstance(tools, list) and isinstance(tools[0], list):
|
||||||
tools = flatten_list(tools)
|
tools = flatten_list(tools)
|
||||||
self.params["tools"] = tools
|
self.params["tools"] = tools
|
||||||
prompt_params = [
|
prompt_params = [
|
||||||
key for key, value in self.params.items() if isinstance(value, str) and key != "format_instructions"
|
key
|
||||||
|
for key, value in self.params.items()
|
||||||
|
if isinstance(value, str) and key != "format_instructions"
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
prompt_params = ["template"]
|
prompt_params = ["template"]
|
||||||
|
|
@ -266,14 +280,20 @@ class PromptVertex(StatelessVertex):
|
||||||
prompt_text = self.params[param]
|
prompt_text = self.params[param]
|
||||||
variables = extract_input_variables_from_prompt(prompt_text)
|
variables = extract_input_variables_from_prompt(prompt_text)
|
||||||
self.params["input_variables"].extend(variables)
|
self.params["input_variables"].extend(variables)
|
||||||
self.params["input_variables"] = list(set(self.params["input_variables"]))
|
self.params["input_variables"] = list(
|
||||||
|
set(self.params["input_variables"])
|
||||||
|
)
|
||||||
elif isinstance(self.params, dict):
|
elif isinstance(self.params, dict):
|
||||||
self.params.pop("input_variables", None)
|
self.params.pop("input_variables", None)
|
||||||
|
|
||||||
await self._build(user_id=user_id)
|
await self._build(user_id=user_id)
|
||||||
|
|
||||||
def _built_object_repr(self):
|
def _built_object_repr(self):
|
||||||
if not self.artifacts or self._built_object is None or not hasattr(self._built_object, "format"):
|
if (
|
||||||
|
not self.artifacts
|
||||||
|
or self._built_object is None
|
||||||
|
or not hasattr(self._built_object, "format")
|
||||||
|
):
|
||||||
return super()._built_object_repr()
|
return super()._built_object_repr()
|
||||||
elif isinstance(self._built_object, UnbuiltObject):
|
elif isinstance(self._built_object, UnbuiltObject):
|
||||||
return super()._built_object_repr()
|
return super()._built_object_repr()
|
||||||
|
|
@ -285,7 +305,9 @@ class PromptVertex(StatelessVertex):
|
||||||
# so the prompt format doesn't break
|
# so the prompt format doesn't break
|
||||||
artifacts.pop("handle_keys", None)
|
artifacts.pop("handle_keys", None)
|
||||||
try:
|
try:
|
||||||
if not hasattr(self._built_object, "template") and hasattr(self._built_object, "prompt"):
|
if not hasattr(self._built_object, "template") and hasattr(
|
||||||
|
self._built_object, "prompt"
|
||||||
|
):
|
||||||
template = self._built_object.prompt.template
|
template = self._built_object.prompt.template
|
||||||
else:
|
else:
|
||||||
template = self._built_object.template
|
template = self._built_object.template
|
||||||
|
|
@ -293,7 +315,11 @@ class PromptVertex(StatelessVertex):
|
||||||
if value:
|
if value:
|
||||||
replace_key = "{" + key + "}"
|
replace_key = "{" + key + "}"
|
||||||
template = template.replace(replace_key, value)
|
template = template.replace(replace_key, value)
|
||||||
return template if isinstance(template, str) else f"{self.vertex_type}({template})"
|
return (
|
||||||
|
template
|
||||||
|
if isinstance(template, str)
|
||||||
|
else f"{self.vertex_type}({template})"
|
||||||
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return str(self._built_object)
|
return str(self._built_object)
|
||||||
|
|
||||||
|
|
@ -430,18 +456,30 @@ class ChatVertex(StatelessVertex):
|
||||||
async for _ in self.stream():
|
async for _ in self.stream():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _is_chat_input(self):
|
||||||
|
return self.vertex_type == InterfaceComponentTypes.ChatInput and self.is_input
|
||||||
|
|
||||||
|
|
||||||
class RoutingVertex(StatelessVertex):
|
class RoutingVertex(StatelessVertex):
|
||||||
def __init__(self, data: Dict, graph):
|
def __init__(self, data: Dict, graph):
|
||||||
super().__init__(data, graph=graph, base_type="custom_components")
|
super().__init__(data, graph=graph, base_type="custom_components")
|
||||||
self.use_result = True
|
self.use_result = True
|
||||||
self.steps = [self._build, self._run]
|
self.steps = [self._build]
|
||||||
|
|
||||||
def _built_object_repr(self):
|
def _built_object_repr(self):
|
||||||
if self.artifacts and "repr" in self.artifacts:
|
if self.artifacts and "repr" in self.artifacts:
|
||||||
return self.artifacts["repr"] or super()._built_object_repr()
|
return self.artifacts["repr"] or super()._built_object_repr()
|
||||||
return super()._built_object_repr()
|
return super()._built_object_repr()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def successors_ids(self):
|
||||||
|
if isinstance(self._built_object, bool):
|
||||||
|
ids = super().successors_ids
|
||||||
|
if self._built_object:
|
||||||
|
return ids
|
||||||
|
return []
|
||||||
|
raise ValueError("RoutingVertex should return a boolean value.")
|
||||||
|
|
||||||
def _run(self, *args, **kwargs):
|
def _run(self, *args, **kwargs):
|
||||||
if self._built_object:
|
if self._built_object:
|
||||||
condition = self._built_object.get("condition")
|
condition = self._built_object.get("condition")
|
||||||
|
|
|
||||||
3
src/backend/langflow/helpers/__init__.py
Normal file
3
src/backend/langflow/helpers/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .record import docs_to_records, records_to_text
|
||||||
|
|
||||||
|
__all__ = ["docs_to_records", "records_to_text"]
|
||||||
37
src/backend/langflow/helpers/record.py
Normal file
37
src/backend/langflow/helpers/record.py
Normal 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)
|
||||||
|
|
@ -21,7 +21,9 @@ class ComponentFunctionEntrypointNameNullError(HTTPException):
|
||||||
|
|
||||||
class Component:
|
class Component:
|
||||||
ERROR_CODE_NULL: ClassVar[str] = "Python code must be provided."
|
ERROR_CODE_NULL: ClassVar[str] = "Python code must be provided."
|
||||||
ERROR_FUNCTION_ENTRYPOINT_NAME_NULL: ClassVar[str] = "The name of the entrypoint function must be provided."
|
ERROR_FUNCTION_ENTRYPOINT_NAME_NULL: ClassVar[str] = (
|
||||||
|
"The name of the entrypoint function must be provided."
|
||||||
|
)
|
||||||
|
|
||||||
code: Optional[str] = None
|
code: Optional[str] = None
|
||||||
_function_entrypoint_name: str = "build"
|
_function_entrypoint_name: str = "build"
|
||||||
|
|
@ -39,7 +41,8 @@ class Component:
|
||||||
def __setattr__(self, key, value):
|
def __setattr__(self, key, value):
|
||||||
if key == "_user_id" and hasattr(self, "_user_id"):
|
if key == "_user_id" and hasattr(self, "_user_id"):
|
||||||
warnings.warn("user_id is immutable and cannot be changed.")
|
warnings.warn("user_id is immutable and cannot be changed.")
|
||||||
super().__setattr__(key, value)
|
else:
|
||||||
|
super().__setattr__(key, value)
|
||||||
|
|
||||||
@cachedmethod(cache=operator.attrgetter("cache"))
|
@cachedmethod(cache=operator.attrgetter("cache"))
|
||||||
def get_code_tree(self, code: str):
|
def get_code_tree(self, code: str):
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import yaml
|
||||||
from cachetools import TTLCache, cachedmethod
|
from cachetools import TTLCache, cachedmethod
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from langchain_core.documents import Document
|
from langchain_core.documents import Document
|
||||||
|
from pydantic import BaseModel
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
|
|
||||||
from langflow.interface.custom.code_parser.utils import (
|
from langflow.interface.custom.code_parser.utils import (
|
||||||
|
|
@ -73,6 +74,7 @@ class CustomComponent(Component):
|
||||||
user_id: Optional[Union[UUID, str]] = None
|
user_id: Optional[Union[UUID, str]] = None
|
||||||
status: Optional[Any] = None
|
status: Optional[Any] = None
|
||||||
"""The status of the component. This is displayed on the frontend. Defaults to None."""
|
"""The status of the component. This is displayed on the frontend. Defaults to None."""
|
||||||
|
_flows_records: Optional[List[Record]] = None
|
||||||
|
|
||||||
_tree: Optional[dict] = None
|
_tree: Optional[dict] = None
|
||||||
|
|
||||||
|
|
@ -110,7 +112,7 @@ class CustomComponent(Component):
|
||||||
return yaml.dump(self.repr_value)
|
return yaml.dump(self.repr_value)
|
||||||
if isinstance(self.repr_value, str):
|
if isinstance(self.repr_value, str):
|
||||||
return self.repr_value
|
return self.repr_value
|
||||||
return str(self.repr_value)
|
return self.repr_value
|
||||||
|
|
||||||
def build_config(self):
|
def build_config(self):
|
||||||
return self.field_config
|
return self.field_config
|
||||||
|
|
@ -119,34 +121,57 @@ class CustomComponent(Component):
|
||||||
def tree(self):
|
def tree(self):
|
||||||
return self.get_code_tree(self.code or "")
|
return self.get_code_tree(self.code or "")
|
||||||
|
|
||||||
def to_records(self, data: Any, text_key: str = "text", data_key: str = "data") -> List[Record]:
|
def to_records(
|
||||||
|
self, data: Any, text_key: str = "text", data_key: str = "data"
|
||||||
|
) -> List[Record]:
|
||||||
"""
|
"""
|
||||||
Convert data into a list of records.
|
Converts input data into a list of Record objects.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data (Any): The input data to be converted.
|
data (Any): The input data to be converted. It can be a single item or a sequence of items.
|
||||||
text_key (str, optional): The key to extract the text from a dictionary item. Defaults to "text".
|
If the input data is a Langchain Document, text_key and data_key are ignored.
|
||||||
data_key (str, optional): The key to extract the data from a dictionary item. Defaults to "data".
|
|
||||||
|
text_key (str, optional): The key to access the text value in each item. Defaults to "text".
|
||||||
|
data_key (str, optional): The key to access the data value in each item. Defaults to "data".
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[dict]: A list of records, where each record is a dictionary with 'text' and 'data' keys.
|
List[Record]: A list of Record objects.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the input data is not of a valid type or if the specified keys are not found in the data.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
records = []
|
records = []
|
||||||
if not isinstance(data, Sequence):
|
if not isinstance(data, Sequence):
|
||||||
data = [data]
|
data = [data]
|
||||||
for item in data:
|
for item in data:
|
||||||
if isinstance(item, str):
|
if isinstance(item, Document):
|
||||||
records.append(Record(text=item))
|
item = {"text": item.page_content, "data": item.metadata}
|
||||||
|
elif isinstance(item, BaseModel):
|
||||||
|
model_dump = item.model_dump()
|
||||||
|
if text_key not in model_dump:
|
||||||
|
raise ValueError(f"Key '{text_key}' not found in BaseModel item.")
|
||||||
|
if data_key not in model_dump:
|
||||||
|
raise ValueError(f"Key '{data_key}' not found in BaseModel item.")
|
||||||
|
item = {"text": model_dump[text_key], "data": model_dump[data_key]}
|
||||||
|
elif isinstance(item, str):
|
||||||
|
item = {"text": item, "data": {}}
|
||||||
elif isinstance(item, dict):
|
elif isinstance(item, dict):
|
||||||
records.append(Record(text=item.get(text_key), data=item.get(data_key)))
|
if text_key not in item:
|
||||||
elif isinstance(item, Document):
|
raise ValueError(f"Key '{text_key}' not found in dictionary item.")
|
||||||
records.append(Record(text=item.page_content, data=item.metadata))
|
if data_key not in item:
|
||||||
|
raise ValueError(f"Key '{data_key}' not found in dictionary item.")
|
||||||
|
item = {"text": item[text_key], "data": item[data_key]}
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Invalid data type: {type(item)}")
|
raise ValueError(f"Invalid data type: {type(item)}")
|
||||||
|
|
||||||
|
records.append(Record(**item))
|
||||||
|
|
||||||
return records
|
return records
|
||||||
|
|
||||||
def create_references_from_records(self, records: List[Record], include_data: bool = False) -> str:
|
def create_references_from_records(
|
||||||
|
self, records: List[Record], include_data: bool = False
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Create references from a list of records.
|
Create references from a list of records.
|
||||||
|
|
||||||
|
|
@ -181,7 +206,8 @@ class CustomComponent(Component):
|
||||||
detail={
|
detail={
|
||||||
"error": "Type hint Error",
|
"error": "Type hint Error",
|
||||||
"traceback": (
|
"traceback": (
|
||||||
"Prompt type is not supported in the build method." " Try using PromptTemplate instead."
|
"Prompt type is not supported in the build method."
|
||||||
|
" Try using PromptTemplate instead."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -195,14 +221,20 @@ class CustomComponent(Component):
|
||||||
if not self.code:
|
if not self.code:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
component_classes = [cls for cls in self.tree["classes"] if self.code_class_base_inheritance in cls["bases"]]
|
component_classes = [
|
||||||
|
cls
|
||||||
|
for cls in self.tree["classes"]
|
||||||
|
if self.code_class_base_inheritance in cls["bases"]
|
||||||
|
]
|
||||||
if not component_classes:
|
if not component_classes:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Assume the first Component class is the one we're interested in
|
# Assume the first Component class is the one we're interested in
|
||||||
component_class = component_classes[0]
|
component_class = component_classes[0]
|
||||||
build_methods = [
|
build_methods = [
|
||||||
method for method in component_class["methods"] if method["name"] == self.function_entrypoint_name
|
method
|
||||||
|
for method in component_class["methods"]
|
||||||
|
if method["name"] == self.function_entrypoint_name
|
||||||
]
|
]
|
||||||
|
|
||||||
return build_methods[0] if build_methods else {}
|
return build_methods[0] if build_methods else {}
|
||||||
|
|
@ -259,7 +291,9 @@ class CustomComponent(Component):
|
||||||
# Retrieve and decrypt the credential by name for the current user
|
# Retrieve and decrypt the credential by name for the current user
|
||||||
db_service = get_db_service()
|
db_service = get_db_service()
|
||||||
with session_getter(db_service) as session:
|
with session_getter(db_service) as session:
|
||||||
return credential_service.get_credential(user_id=self._user_id or "", name=name, session=session)
|
return credential_service.get_credential(
|
||||||
|
user_id=self._user_id or "", name=name, session=session
|
||||||
|
)
|
||||||
|
|
||||||
return get_credential
|
return get_credential
|
||||||
|
|
||||||
|
|
@ -269,7 +303,9 @@ class CustomComponent(Component):
|
||||||
credential_service = get_credential_service()
|
credential_service = get_credential_service()
|
||||||
db_service = get_db_service()
|
db_service = get_db_service()
|
||||||
with session_getter(db_service) as session:
|
with session_getter(db_service) as session:
|
||||||
return credential_service.list_credentials(user_id=self._user_id, session=session)
|
return credential_service.list_credentials(
|
||||||
|
user_id=self._user_id, session=session
|
||||||
|
)
|
||||||
|
|
||||||
def index(self, value: int = 0):
|
def index(self, value: int = 0):
|
||||||
"""Returns a function that returns the value at the given index in the iterable."""
|
"""Returns a function that returns the value at the given index in the iterable."""
|
||||||
|
|
@ -299,24 +335,51 @@ class CustomComponent(Component):
|
||||||
async def run_flow(
|
async def run_flow(
|
||||||
self,
|
self,
|
||||||
input_value: Union[str, list[str]],
|
input_value: Union[str, list[str]],
|
||||||
flow_id: str,
|
flow_id: Optional[str] = None,
|
||||||
|
flow_name: Optional[str] = None,
|
||||||
tweaks: Optional[dict] = None,
|
tweaks: Optional[dict] = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
|
if not flow_id and not flow_name:
|
||||||
|
raise ValueError("Flow ID or Flow Name is required")
|
||||||
|
if not self._flows_records:
|
||||||
|
self.list_flows()
|
||||||
|
if not flow_id and self._flows_records:
|
||||||
|
flow_ids = [
|
||||||
|
flow.data["id"]
|
||||||
|
for flow in self._flows_records
|
||||||
|
if flow.data["name"] == flow_name
|
||||||
|
]
|
||||||
|
if not flow_ids:
|
||||||
|
raise ValueError(f"Flow {flow_name} not found")
|
||||||
|
elif len(flow_ids) > 1:
|
||||||
|
raise ValueError(f"Multiple flows found with the name {flow_name}")
|
||||||
|
flow_id = flow_ids[0]
|
||||||
|
|
||||||
|
if not flow_id:
|
||||||
|
raise ValueError(f"Flow {flow_name} not found")
|
||||||
|
|
||||||
graph = await self.load_flow(flow_id, tweaks)
|
graph = await self.load_flow(flow_id, tweaks)
|
||||||
input_value_dict = {"input_value": input_value}
|
input_value_dict = {"input_value": input_value}
|
||||||
return await graph.run(input_value_dict, stream=False)
|
return await graph.run(input_value_dict, stream=False)
|
||||||
|
|
||||||
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]:
|
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Record]:
|
||||||
if not self._user_id:
|
if not self._user_id:
|
||||||
raise ValueError("Session is invalid")
|
raise ValueError("Session is invalid")
|
||||||
try:
|
try:
|
||||||
get_session = get_session or session_getter
|
get_session = get_session or session_getter
|
||||||
db_service = get_db_service()
|
db_service = get_db_service()
|
||||||
with get_session(db_service) as session:
|
with get_session(db_service) as session:
|
||||||
flows = session.exec(select(Flow).where(Flow.user_id == self._user_id)).all()
|
flows = session.exec(
|
||||||
return flows
|
select(Flow)
|
||||||
|
.where(Flow.user_id == self._user_id)
|
||||||
|
.where(Flow.is_component == False)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
flows_records = [flow.to_record() for flow in flows]
|
||||||
|
self._flows_records = flows_records
|
||||||
|
return flows_records
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError("Session is invalid") from e
|
raise ValueError(f"Error listing flows: {e}")
|
||||||
|
|
||||||
def build(self, *args: Any, **kwargs: Any) -> Any:
|
def build(self, *args: Any, **kwargs: Any) -> Any:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
|
||||||
|
|
@ -80,9 +80,13 @@ class DirectoryReader:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error while loading component: {e}")
|
logger.error(f"Error while loading component: {e}")
|
||||||
continue
|
continue
|
||||||
items.append({"name": menu["name"], "path": menu["path"], "components": components})
|
items.append(
|
||||||
|
{"name": menu["name"], "path": menu["path"], "components": components}
|
||||||
|
)
|
||||||
filtered = [menu for menu in items if menu["components"]]
|
filtered = [menu for menu in items if menu["components"]]
|
||||||
logger.debug(f'Filtered components {"with errors" if with_errors else ""}: {len(filtered)}')
|
logger.debug(
|
||||||
|
f'Filtered components {"with errors" if with_errors else ""}: {len(filtered)}'
|
||||||
|
)
|
||||||
return {"menu": filtered}
|
return {"menu": filtered}
|
||||||
|
|
||||||
def validate_code(self, file_content):
|
def validate_code(self, file_content):
|
||||||
|
|
@ -115,7 +119,9 @@ class DirectoryReader:
|
||||||
Walk through the directory path and return a list of all .py files.
|
Walk through the directory path and return a list of all .py files.
|
||||||
"""
|
"""
|
||||||
if not (safe_path := self.get_safe_path()):
|
if not (safe_path := self.get_safe_path()):
|
||||||
raise CustomComponentPathValueError(f"The path needs to start with '{self.base_path}'.")
|
raise CustomComponentPathValueError(
|
||||||
|
f"The path needs to start with '{self.base_path}'."
|
||||||
|
)
|
||||||
|
|
||||||
file_list = []
|
file_list = []
|
||||||
safe_path_obj = Path(safe_path)
|
safe_path_obj = Path(safe_path)
|
||||||
|
|
@ -125,7 +131,11 @@ class DirectoryReader:
|
||||||
# any folders below [folder] will be ignored
|
# any folders below [folder] will be ignored
|
||||||
# basically the parent folder of the file should be a
|
# basically the parent folder of the file should be a
|
||||||
# folder in the safe_path
|
# folder in the safe_path
|
||||||
if file_path.is_file() and file_path.parent.parent == safe_path_obj and not file_path.name.startswith("__"):
|
if (
|
||||||
|
file_path.is_file()
|
||||||
|
and file_path.parent.parent == safe_path_obj
|
||||||
|
and not file_path.name.startswith("__")
|
||||||
|
):
|
||||||
file_list.append(str(file_path))
|
file_list.append(str(file_path))
|
||||||
return file_list
|
return file_list
|
||||||
|
|
||||||
|
|
@ -163,7 +173,9 @@ class DirectoryReader:
|
||||||
for node in ast.walk(module):
|
for node in ast.walk(module):
|
||||||
if isinstance(node, ast.FunctionDef):
|
if isinstance(node, ast.FunctionDef):
|
||||||
for arg in node.args.args:
|
for arg in node.args.args:
|
||||||
if self._is_type_hint_in_arg_annotation(arg.annotation, type_hint_name):
|
if self._is_type_hint_in_arg_annotation(
|
||||||
|
arg.annotation, type_hint_name
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
except SyntaxError:
|
except SyntaxError:
|
||||||
# Returns False if the code is not valid Python
|
# Returns False if the code is not valid Python
|
||||||
|
|
@ -181,14 +193,16 @@ class DirectoryReader:
|
||||||
and annotation.value.id == type_hint_name
|
and annotation.value.id == type_hint_name
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_type_hint_used_but_not_imported(self, type_hint_name: str, code: str) -> bool:
|
def is_type_hint_used_but_not_imported(
|
||||||
|
self, type_hint_name: str, code: str
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if a type hint is used but not imported in the given code.
|
Check if a type hint is used but not imported in the given code.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return self._is_type_hint_used_in_args(type_hint_name, code) and not self._is_type_hint_imported(
|
return self._is_type_hint_used_in_args(
|
||||||
type_hint_name, code
|
type_hint_name, code
|
||||||
)
|
) and not self._is_type_hint_imported(type_hint_name, code)
|
||||||
except SyntaxError:
|
except SyntaxError:
|
||||||
# Returns True if there's something wrong with the code
|
# Returns True if there's something wrong with the code
|
||||||
# TODO : Find a better way to handle this
|
# TODO : Find a better way to handle this
|
||||||
|
|
@ -209,9 +223,9 @@ class DirectoryReader:
|
||||||
return False, "Syntax error"
|
return False, "Syntax error"
|
||||||
elif not self.validate_build(file_content):
|
elif not self.validate_build(file_content):
|
||||||
return False, "Missing build function"
|
return False, "Missing build function"
|
||||||
elif self._is_type_hint_used_in_args("Optional", file_content) and not self._is_type_hint_imported(
|
elif self._is_type_hint_used_in_args(
|
||||||
"Optional", file_content
|
"Optional", file_content
|
||||||
):
|
) and not self._is_type_hint_imported("Optional", file_content):
|
||||||
return (
|
return (
|
||||||
False,
|
False,
|
||||||
"Type hint 'Optional' is used but not imported in the code.",
|
"Type hint 'Optional' is used but not imported in the code.",
|
||||||
|
|
@ -227,14 +241,18 @@ class DirectoryReader:
|
||||||
from the .py files in the directory.
|
from the .py files in the directory.
|
||||||
"""
|
"""
|
||||||
response = {"menu": []}
|
response = {"menu": []}
|
||||||
logger.debug("-------------------- Building component menu list --------------------")
|
logger.debug(
|
||||||
|
"-------------------- Building component menu list --------------------"
|
||||||
|
)
|
||||||
|
|
||||||
for file_path in file_paths:
|
for file_path in file_paths:
|
||||||
menu_name = os.path.basename(os.path.dirname(file_path))
|
menu_name = os.path.basename(os.path.dirname(file_path))
|
||||||
filename = os.path.basename(file_path)
|
filename = os.path.basename(file_path)
|
||||||
validation_result, result_content = self.process_file(file_path)
|
validation_result, result_content = self.process_file(file_path)
|
||||||
if not validation_result:
|
if not validation_result:
|
||||||
logger.error(f"Error while processing file {file_path}: {result_content}")
|
logger.error(
|
||||||
|
f"Error while processing file {file_path}: {result_content}"
|
||||||
|
)
|
||||||
|
|
||||||
menu_result = self.find_menu(response, menu_name) or {
|
menu_result = self.find_menu(response, menu_name) or {
|
||||||
"name": menu_name,
|
"name": menu_name,
|
||||||
|
|
@ -247,7 +265,9 @@ class DirectoryReader:
|
||||||
|
|
||||||
# first check if it's already CamelCase
|
# first check if it's already CamelCase
|
||||||
if "_" in component_name:
|
if "_" in component_name:
|
||||||
component_name_camelcase = " ".join(word.title() for word in component_name.split("_"))
|
component_name_camelcase = " ".join(
|
||||||
|
word.title() for word in component_name.split("_")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
component_name_camelcase = component_name
|
component_name_camelcase = component_name
|
||||||
|
|
||||||
|
|
@ -255,7 +275,9 @@ class DirectoryReader:
|
||||||
try:
|
try:
|
||||||
output_types = self.get_output_types_from_code(result_content)
|
output_types = self.get_output_types_from_code(result_content)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception(f"Error while getting output types from code: {str(exc)}")
|
logger.exception(
|
||||||
|
f"Error while getting output types from code: {str(exc)}"
|
||||||
|
)
|
||||||
output_types = [component_name_camelcase]
|
output_types = [component_name_camelcase]
|
||||||
else:
|
else:
|
||||||
output_types = [component_name_camelcase]
|
output_types = [component_name_camelcase]
|
||||||
|
|
@ -271,7 +293,9 @@ class DirectoryReader:
|
||||||
|
|
||||||
if menu_result not in response["menu"]:
|
if menu_result not in response["menu"]:
|
||||||
response["menu"].append(menu_result)
|
response["menu"].append(menu_result)
|
||||||
logger.debug("-------------------- Component menu list built --------------------")
|
logger.debug(
|
||||||
|
"-------------------- Component menu list built --------------------"
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -281,5 +305,6 @@ class DirectoryReader:
|
||||||
"""
|
"""
|
||||||
custom_component = CustomComponent(code=code)
|
custom_component = CustomComponent(code=code)
|
||||||
types_list = custom_component.get_function_entrypoint_return_type
|
types_list = custom_component.get_function_entrypoint_return_type
|
||||||
|
|
||||||
# Get the name of types classes
|
# Get the name of types classes
|
||||||
return [type_.__name__ for type_ in types_list if hasattr(type_, "__name__")]
|
return [type_.__name__ for type_ in types_list if hasattr(type_, "__name__")]
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,8 @@ def add_base_classes(frontend_node: CustomComponentFrontendNode, return_types: L
|
||||||
)
|
)
|
||||||
|
|
||||||
base_classes = get_base_classes(return_type_instance)
|
base_classes = get_base_classes(return_type_instance)
|
||||||
|
if return_type_instance == str:
|
||||||
|
base_classes.append("Text")
|
||||||
|
|
||||||
for base_class in base_classes:
|
for base_class in base_classes:
|
||||||
frontend_node.add_base_class(base_class)
|
frontend_node.add_base_class(base_class)
|
||||||
|
|
@ -232,10 +234,12 @@ def run_build_config(
|
||||||
# Allow user to build TemplateField as well
|
# Allow user to build TemplateField as well
|
||||||
# as a dict with the same keys as TemplateField
|
# as a dict with the same keys as TemplateField
|
||||||
field_dict = get_field_dict(field)
|
field_dict = get_field_dict(field)
|
||||||
|
# This has to be done to set refresh if options or value are callable
|
||||||
|
update_field_dict(field_dict)
|
||||||
if update_field is not None and field_name != update_field:
|
if update_field is not None and field_name != update_field:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
update_field_dict(field_dict)
|
update_field_dict(field_dict, call=True)
|
||||||
build_config[field_name] = field_dict
|
build_config[field_name] = field_dict
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(f"Error while getting build_config: {str(exc)}")
|
logger.error(f"Error while getting build_config: {str(exc)}")
|
||||||
|
|
@ -330,11 +334,13 @@ def create_component_template(component):
|
||||||
"""Create a template for a component."""
|
"""Create a template for a component."""
|
||||||
component_code = component["code"]
|
component_code = component["code"]
|
||||||
component_output_types = component["output_types"]
|
component_output_types = component["output_types"]
|
||||||
|
# remove
|
||||||
|
|
||||||
component_extractor = CustomComponent(code=component_code)
|
component_extractor = CustomComponent(code=component_code)
|
||||||
|
|
||||||
component_template = build_custom_component_template(component_extractor)
|
component_template = build_custom_component_template(component_extractor)
|
||||||
component_template["output_types"] = component_output_types
|
if not component_template["output_types"] and component_output_types:
|
||||||
|
component_template["output_types"] = component_output_types
|
||||||
|
|
||||||
return component_template
|
return component_template
|
||||||
|
|
||||||
|
|
@ -364,15 +370,17 @@ def build_custom_components(settings_service):
|
||||||
return custom_components_from_file
|
return custom_components_from_file
|
||||||
|
|
||||||
|
|
||||||
def update_field_dict(field_dict):
|
def update_field_dict(field_dict, call=False):
|
||||||
"""Update the field dictionary by calling options() or value() if they are callable"""
|
"""Update the field dictionary by calling options() or value() if they are callable"""
|
||||||
if "options" in field_dict and callable(field_dict["options"]):
|
if "options" in field_dict and callable(field_dict["options"]):
|
||||||
field_dict["options"] = field_dict["options"]()
|
if call:
|
||||||
|
field_dict["options"] = field_dict["options"]()
|
||||||
# Also update the "refresh" key
|
# Also update the "refresh" key
|
||||||
field_dict["refresh"] = True
|
field_dict["refresh"] = True
|
||||||
|
|
||||||
if "value" in field_dict and callable(field_dict["value"]):
|
if "value" in field_dict and callable(field_dict["value"]):
|
||||||
field_dict["value"] = field_dict["value"]()
|
if call:
|
||||||
|
field_dict["value"] = field_dict["value"]()
|
||||||
field_dict["refresh"] = True
|
field_dict["refresh"] = True
|
||||||
|
|
||||||
# Let's check if "range_spec" is a RangeSpec object
|
# Let's check if "range_spec" is a RangeSpec object
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from io import BytesIO
|
|
||||||
import re
|
import re
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from langchain.base_language import BaseLanguageModel
|
from langchain.base_language import BaseLanguageModel
|
||||||
from PIL.Image import Image
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from PIL.Image import Image
|
||||||
|
|
||||||
from langflow.services.chat.config import ChatConfig
|
from langflow.services.chat.config import ChatConfig
|
||||||
from langflow.services.deps import get_settings_service
|
from langflow.services.deps import get_settings_service
|
||||||
|
|
||||||
|
|
@ -43,7 +43,9 @@ def try_setting_streaming_options(langchain_object):
|
||||||
llm = None
|
llm = None
|
||||||
if hasattr(langchain_object, "llm"):
|
if hasattr(langchain_object, "llm"):
|
||||||
llm = langchain_object.llm
|
llm = langchain_object.llm
|
||||||
elif hasattr(langchain_object, "llm_chain") and hasattr(langchain_object.llm_chain, "llm"):
|
elif hasattr(langchain_object, "llm_chain") and hasattr(
|
||||||
|
langchain_object.llm_chain, "llm"
|
||||||
|
):
|
||||||
llm = langchain_object.llm_chain.llm
|
llm = langchain_object.llm_chain.llm
|
||||||
|
|
||||||
if isinstance(llm, BaseLanguageModel):
|
if isinstance(llm, BaseLanguageModel):
|
||||||
|
|
@ -56,8 +58,37 @@ def try_setting_streaming_options(langchain_object):
|
||||||
|
|
||||||
|
|
||||||
def extract_input_variables_from_prompt(prompt: str) -> list[str]:
|
def extract_input_variables_from_prompt(prompt: str) -> list[str]:
|
||||||
"""Extract input variables from prompt."""
|
variables = []
|
||||||
return re.findall(r"{(.*?)}", prompt)
|
remaining_text = prompt
|
||||||
|
|
||||||
|
# Pattern to match single {var} and double {{var}} braces.
|
||||||
|
pattern = r"\{\{(.*?)\}\}|\{([^{}]+)\}"
|
||||||
|
|
||||||
|
while True:
|
||||||
|
match = re.search(pattern, remaining_text)
|
||||||
|
if not match:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Extract the variable name from either the single or double brace match
|
||||||
|
if match.group(1): # Match found in double braces
|
||||||
|
variable_name = (
|
||||||
|
"{{" + match.group(1) + "}}"
|
||||||
|
) # Re-add single braces for JSON strings
|
||||||
|
else: # Match found in single braces
|
||||||
|
variable_name = match.group(2)
|
||||||
|
if variable_name is not None:
|
||||||
|
# This means there is a match
|
||||||
|
# but there is nothing inside the braces
|
||||||
|
variables.append(variable_name)
|
||||||
|
|
||||||
|
# Remove the matched text from the remaining_text
|
||||||
|
start, end = match.span()
|
||||||
|
remaining_text = remaining_text[:start] + remaining_text[end:]
|
||||||
|
|
||||||
|
# Proceed to the next match until no more matches are found
|
||||||
|
# No need to compare remaining "{}" instances because we are re-adding braces for JSON compatibility
|
||||||
|
|
||||||
|
return variables
|
||||||
|
|
||||||
|
|
||||||
def setup_llm_caching():
|
def setup_llm_caching():
|
||||||
|
|
@ -73,11 +104,14 @@ def setup_llm_caching():
|
||||||
|
|
||||||
def set_langchain_cache(settings):
|
def set_langchain_cache(settings):
|
||||||
from langchain.globals import set_llm_cache
|
from langchain.globals import set_llm_cache
|
||||||
|
|
||||||
from langflow.interface.importing.utils import import_class
|
from langflow.interface.importing.utils import import_class
|
||||||
|
|
||||||
if cache_type := os.getenv("LANGFLOW_LANGCHAIN_CACHE"):
|
if cache_type := os.getenv("LANGFLOW_LANGCHAIN_CACHE"):
|
||||||
try:
|
try:
|
||||||
cache_class = import_class(f"langchain.cache.{cache_type or settings.LANGCHAIN_CACHE}")
|
cache_class = import_class(
|
||||||
|
f"langchain.cache.{cache_type or settings.LANGCHAIN_CACHE}"
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(f"Setting up LLM caching with {cache_class.__name__}")
|
logger.debug(f"Setting up LLM caching with {cache_class.__name__}")
|
||||||
set_llm_cache(cache_class())
|
set_llm_cache(cache_class())
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from langflow.schema import Record
|
from langflow.schema import Record
|
||||||
from langflow.services.deps import get_monitor_service
|
from langflow.services.deps import get_monitor_service
|
||||||
from langflow.services.monitor.schema import MessageModel
|
from langflow.services.monitor.schema import MessageModel
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
|
|
||||||
def get_messages(
|
def get_messages(
|
||||||
|
|
|
||||||
3
src/backend/langflow/schema/__init__.py
Normal file
3
src/backend/langflow/schema/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .schema import Record
|
||||||
|
|
||||||
|
__all__ = ["Record"]
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
from langchain_core.documents import Document
|
from langchain_core.documents import Document
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
@ -13,7 +13,7 @@ class Record(BaseModel):
|
||||||
data (dict, optional): Additional data associated with the record.
|
data (dict, optional): Additional data associated with the record.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
text: str
|
text: Optional[str] = ""
|
||||||
data: dict = {}
|
data: dict = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -52,19 +52,6 @@ class Record(BaseModel):
|
||||||
Returns the text of the record.
|
Returns the text of the record.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The text of the record.
|
str: The text and data of the record.
|
||||||
"""
|
"""
|
||||||
return self.text
|
return self.model_dump_json(indent=2)
|
||||||
|
|
||||||
|
|
||||||
def docs_to_records(documents: list[Document]) -> list[Record]:
|
|
||||||
"""
|
|
||||||
Converts a list of Documents to a list of Records.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
documents (list[Document]): The list of Documents to convert.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[Record]: The converted list of Records.
|
|
||||||
"""
|
|
||||||
return [Record.from_document(document) for document in documents]
|
|
||||||
|
|
@ -7,6 +7,8 @@ from uuid import UUID, uuid4
|
||||||
from pydantic import field_serializer, field_validator
|
from pydantic import field_serializer, field_validator
|
||||||
from sqlmodel import JSON, Column, Field, Relationship, SQLModel
|
from sqlmodel import JSON, Column, Field, Relationship, SQLModel
|
||||||
|
|
||||||
|
from langflow.schema.schema import Record
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from langflow.services.database.models.user import User
|
from langflow.services.database.models.user import User
|
||||||
|
|
||||||
|
|
@ -16,7 +18,9 @@ class FlowBase(SQLModel):
|
||||||
description: Optional[str] = Field(index=True, nullable=True, default=None)
|
description: Optional[str] = Field(index=True, nullable=True, default=None)
|
||||||
data: Optional[Dict] = Field(default=None, nullable=True)
|
data: Optional[Dict] = Field(default=None, nullable=True)
|
||||||
is_component: Optional[bool] = Field(default=False, nullable=True)
|
is_component: Optional[bool] = Field(default=False, nullable=True)
|
||||||
updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow, nullable=True)
|
updated_at: Optional[datetime] = Field(
|
||||||
|
default_factory=datetime.utcnow, nullable=True
|
||||||
|
)
|
||||||
folder: Optional[str] = Field(default=None, nullable=True)
|
folder: Optional[str] = Field(default=None, nullable=True)
|
||||||
|
|
||||||
@field_validator("data")
|
@field_validator("data")
|
||||||
|
|
@ -57,6 +61,18 @@ class Flow(FlowBase, table=True):
|
||||||
user_id: UUID = Field(index=True, foreign_key="user.id", nullable=True)
|
user_id: UUID = Field(index=True, foreign_key="user.id", nullable=True)
|
||||||
user: "User" = Relationship(back_populates="flows")
|
user: "User" = Relationship(back_populates="flows")
|
||||||
|
|
||||||
|
def to_record(self):
|
||||||
|
serialized = self.model_dump()
|
||||||
|
data = {
|
||||||
|
"id": serialized.pop("id"),
|
||||||
|
"data": serialized.pop("data"),
|
||||||
|
"name": serialized.pop("name"),
|
||||||
|
"description": serialized.pop("description"),
|
||||||
|
"updated_at": serialized.pop("updated_at"),
|
||||||
|
}
|
||||||
|
record = Record(text=data.get("name"), data=data)
|
||||||
|
return record
|
||||||
|
|
||||||
|
|
||||||
class FlowCreate(FlowBase):
|
class FlowCreate(FlowBase):
|
||||||
user_id: Optional[UUID] = None
|
user_id: Optional[UUID] = None
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,10 @@ class DatabaseService(Service):
|
||||||
def _create_engine(self) -> "Engine":
|
def _create_engine(self) -> "Engine":
|
||||||
"""Create the engine for the database."""
|
"""Create the engine for the database."""
|
||||||
settings_service = get_settings_service()
|
settings_service = get_settings_service()
|
||||||
if settings_service.settings.DATABASE_URL and settings_service.settings.DATABASE_URL.startswith("sqlite"):
|
if (
|
||||||
|
settings_service.settings.DATABASE_URL
|
||||||
|
and settings_service.settings.DATABASE_URL.startswith("sqlite")
|
||||||
|
):
|
||||||
connect_args = {"check_same_thread": False}
|
connect_args = {"check_same_thread": False}
|
||||||
else:
|
else:
|
||||||
connect_args = {}
|
connect_args = {}
|
||||||
|
|
@ -48,7 +51,9 @@ class DatabaseService(Service):
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
if exc_type is not None: # If an exception has been raised
|
if exc_type is not None: # If an exception has been raised
|
||||||
logger.error(f"Session rollback because of exception: {exc_type.__name__} {exc_value}")
|
logger.error(
|
||||||
|
f"Session rollback because of exception: {exc_type.__name__} {exc_value}"
|
||||||
|
)
|
||||||
self._session.rollback()
|
self._session.rollback()
|
||||||
else:
|
else:
|
||||||
self._session.commit()
|
self._session.commit()
|
||||||
|
|
@ -65,7 +70,9 @@ class DatabaseService(Service):
|
||||||
settings_service = get_settings_service()
|
settings_service = get_settings_service()
|
||||||
if settings_service.auth_settings.AUTO_LOGIN:
|
if settings_service.auth_settings.AUTO_LOGIN:
|
||||||
with Session(self.engine) as session:
|
with Session(self.engine) as session:
|
||||||
flows = session.exec(select(models.Flow).where(models.Flow.user_id is None)).all()
|
flows = session.exec(
|
||||||
|
select(models.Flow).where(models.Flow.user_id is None)
|
||||||
|
).all()
|
||||||
if flows:
|
if flows:
|
||||||
logger.debug("Migrating flows to default superuser")
|
logger.debug("Migrating flows to default superuser")
|
||||||
username = settings_service.auth_settings.SUPERUSER
|
username = settings_service.auth_settings.SUPERUSER
|
||||||
|
|
@ -95,7 +102,9 @@ class DatabaseService(Service):
|
||||||
expected_columns = list(model.model_fields.keys())
|
expected_columns = list(model.model_fields.keys())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
available_columns = [col["name"] for col in inspector.get_columns(table)]
|
available_columns = [
|
||||||
|
col["name"] for col in inspector.get_columns(table)
|
||||||
|
]
|
||||||
except sa.exc.NoSuchTableError:
|
except sa.exc.NoSuchTableError:
|
||||||
logger.error(f"Missing table: {table}")
|
logger.error(f"Missing table: {table}")
|
||||||
return False
|
return False
|
||||||
|
|
@ -152,14 +161,16 @@ class DatabaseService(Service):
|
||||||
try:
|
try:
|
||||||
command.check(alembic_cfg)
|
command.check(alembic_cfg)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if isinstance(exc, (util.exc.CommandError, util.exc.AutogenerateDiffsDetected)):
|
if isinstance(
|
||||||
|
exc, (util.exc.CommandError, util.exc.AutogenerateDiffsDetected)
|
||||||
|
):
|
||||||
command.upgrade(alembic_cfg, "head")
|
command.upgrade(alembic_cfg, "head")
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
command.check(alembic_cfg)
|
command.check(alembic_cfg)
|
||||||
except util.exc.AutogenerateDiffsDetected as e:
|
except util.exc.AutogenerateDiffsDetected as e:
|
||||||
logger.error("AutogenerateDiffsDetected: {exc}")
|
logger.error(f"AutogenerateDiffsDetected: {exc}")
|
||||||
if not fix:
|
if not fix:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Something went wrong running migrations. Please, run `langflow migration --fix`"
|
"Something went wrong running migrations. Please, run `langflow migration --fix`"
|
||||||
|
|
@ -188,7 +199,10 @@ class DatabaseService(Service):
|
||||||
# We will check that all models are in the database
|
# We will check that all models are in the database
|
||||||
# and that the database is up to date with all columns
|
# and that the database is up to date with all columns
|
||||||
sql_models = [models.Flow, models.User, models.ApiKey]
|
sql_models = [models.Flow, models.User, models.ApiKey]
|
||||||
return [TableResults(sql_model.__tablename__, self.check_table(sql_model)) for sql_model in sql_models]
|
return [
|
||||||
|
TableResults(sql_model.__tablename__, self.check_table(sql_model))
|
||||||
|
for sql_model in sql_models
|
||||||
|
]
|
||||||
|
|
||||||
def check_table(self, model):
|
def check_table(self, model):
|
||||||
results = []
|
results = []
|
||||||
|
|
@ -197,7 +211,9 @@ class DatabaseService(Service):
|
||||||
expected_columns = list(model.__fields__.keys())
|
expected_columns = list(model.__fields__.keys())
|
||||||
available_columns = []
|
available_columns = []
|
||||||
try:
|
try:
|
||||||
available_columns = [col["name"] for col in inspector.get_columns(table_name)]
|
available_columns = [
|
||||||
|
col["name"] for col in inspector.get_columns(table_name)
|
||||||
|
]
|
||||||
results.append(Result(name=table_name, type="table", success=True))
|
results.append(Result(name=table_name, type="table", success=True))
|
||||||
except sa.exc.NoSuchTableError:
|
except sa.exc.NoSuchTableError:
|
||||||
logger.error(f"Missing table: {table_name}")
|
logger.error(f"Missing table: {table_name}")
|
||||||
|
|
@ -228,7 +244,9 @@ class DatabaseService(Service):
|
||||||
try:
|
try:
|
||||||
table.create(self.engine, checkfirst=True)
|
table.create(self.engine, checkfirst=True)
|
||||||
except OperationalError as oe:
|
except OperationalError as oe:
|
||||||
logger.warning(f"Table {table} already exists, skipping. Exception: {oe}")
|
logger.warning(
|
||||||
|
f"Table {table} already exists, skipping. Exception: {oe}"
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(f"Error creating table {table}: {exc}")
|
logger.error(f"Error creating table {table}: {exc}")
|
||||||
raise RuntimeError(f"Error creating table {table}") from exc
|
raise RuntimeError(f"Error creating table {table}") from exc
|
||||||
|
|
@ -240,7 +258,9 @@ class DatabaseService(Service):
|
||||||
if table not in table_names:
|
if table not in table_names:
|
||||||
logger.error("Something went wrong creating the database and tables.")
|
logger.error("Something went wrong creating the database and tables.")
|
||||||
logger.error("Please check your database settings.")
|
logger.error("Please check your database settings.")
|
||||||
raise RuntimeError("Something went wrong creating the database and tables.")
|
raise RuntimeError(
|
||||||
|
"Something went wrong creating the database and tables."
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug("Database and tables created successfully")
|
logger.debug("Database and tables created successfully")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
class TransactionModel(BaseModel):
|
class TransactionModel(BaseModel):
|
||||||
id: Optional[int] = Field(default=None, alias="id")
|
id: Optional[int] = Field(default=None, alias="id")
|
||||||
timestamp: Optional[datetime] = Field(default_factory=datetime.now, alias="timestamp")
|
timestamp: Optional[datetime] = Field(
|
||||||
|
default_factory=datetime.now, alias="timestamp"
|
||||||
|
)
|
||||||
source: str
|
source: str
|
||||||
target: str
|
target: str
|
||||||
target_args: dict
|
target_args: dict
|
||||||
|
|
@ -51,8 +53,12 @@ class MessageModel(BaseModel):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_record(cls, record: "Record"):
|
def from_record(cls, record: "Record"):
|
||||||
# first check if the record has all the required fields
|
# first check if the record has all the required fields
|
||||||
if not record.data or ("sender" not in record.data and "sender_name" not in record.data):
|
if not record.data or (
|
||||||
raise ValueError("The record does not have the required fields 'sender' and 'sender_name' in the data.")
|
"sender" not in record.data and "sender_name" not in record.data
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"The record does not have the required fields 'sender' and 'sender_name' in the data."
|
||||||
|
)
|
||||||
return cls(
|
return cls(
|
||||||
sender=record.data["sender"],
|
sender=record.data["sender"],
|
||||||
sender_name=record.data["sender_name"],
|
sender_name=record.data["sender_name"],
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ class ChatOutputResponse(BaseModel):
|
||||||
message: Union[str, List[Union[str, Dict]]]
|
message: Union[str, List[Union[str, Dict]]]
|
||||||
sender: Optional[str] = "Machine"
|
sender: Optional[str] = "Machine"
|
||||||
sender_name: Optional[str] = "AI"
|
sender_name: Optional[str] = "AI"
|
||||||
|
session_id: Optional[str] = None
|
||||||
stream_url: Optional[str] = None
|
stream_url: Optional[str] = None
|
||||||
|
component_id: Optional[str] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_message(
|
def from_message(
|
||||||
|
|
|
||||||
4
src/frontend/.gitignore
vendored
4
src/frontend/.gitignore
vendored
|
|
@ -24,3 +24,7 @@ yarn-error.log*
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/*/
|
/playwright-report/*/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
27
src/frontend/package-lock.json
generated
27
src/frontend/package-lock.json
generated
|
|
@ -48,6 +48,7 @@
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.331.0",
|
"lucide-react": "^0.331.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"playwright": "^1.42.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-ace": "^10.1.0",
|
"react-ace": "^10.1.0",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
|
|
@ -77,7 +78,7 @@
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.41.2",
|
"@playwright/test": "^1.42.0",
|
||||||
"@swc/cli": "^0.1.62",
|
"@swc/cli": "^0.1.62",
|
||||||
"@swc/core": "^1.3.80",
|
"@swc/core": "^1.3.80",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
|
@ -1492,12 +1493,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.41.2",
|
"version": "1.42.0",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.0.tgz",
|
||||||
"integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==",
|
"integrity": "sha512-2k1HzC28Fs+HiwbJOQDUwrWMttqSLUVdjCqitBOjdCD0svWOMQUVqrXX6iFD7POps6xXAojsX/dGBpKnjZctLA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.41.2"
|
"playwright": "1.42.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -8719,12 +8720,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.41.2",
|
"version": "1.42.0",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.0.tgz",
|
||||||
"integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==",
|
"integrity": "sha512-Ko7YRUgj5xBHbntrgt4EIw/nE//XBHOKVKnBjO1KuZkmkhlbgyggTe5s9hjqQ1LpN+Xg+kHsQyt5Pa0Bw5XpvQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.41.2"
|
"playwright-core": "1.42.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -8737,10 +8737,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.41.2",
|
"version": "1.42.0",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.0.tgz",
|
||||||
"integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==",
|
"integrity": "sha512-0HD9y8qEVlcbsAjdpBaFjmaTHf+1FeIddy8VJLeiqwhcNqGCBe4Wp2e8knpqiYbzxtxarxiXyNDw2cG8sCaNMQ==",
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.331.0",
|
"lucide-react": "^0.331.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"playwright": "^1.42.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-ace": "^10.1.0",
|
"react-ace": "^10.1.0",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
|
|
@ -99,7 +100,7 @@
|
||||||
},
|
},
|
||||||
"proxy": "http://127.0.0.1:7860",
|
"proxy": "http://127.0.0.1:7860",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.41.2",
|
"@playwright/test": "^1.42.0",
|
||||||
"@swc/cli": "^0.1.62",
|
"@swc/cli": "^0.1.62",
|
||||||
"@swc/core": "^1.3.80",
|
"@swc/core": "^1.3.80",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,6 @@
|
||||||
<li>
|
<li>
|
||||||
<a href="./e2e/index.html">e2e report</a>
|
<a href="./e2e/index.html">e2e report</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href="./onlyFront/index.html">frontEnd Only report</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,16 @@ terminate_process_by_port() {
|
||||||
echo "Process terminated."
|
echo "Process terminated."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete_temp() {
|
||||||
|
cd ../../
|
||||||
|
echo "Deleting temp database"
|
||||||
|
rm temp
|
||||||
|
echo "Temp database deleted."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Trap signals to ensure cleanup on script termination
|
# Trap signals to ensure cleanup on script termination
|
||||||
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000' EXIT
|
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000; delete_temp' EXIT
|
||||||
|
|
||||||
# install playwright if there is not installed yet
|
# install playwright if there is not installed yet
|
||||||
npx playwright install
|
npx playwright install
|
||||||
|
|
@ -42,26 +50,16 @@ make frontend &
|
||||||
# Give some time for the frontend to start (adjust sleep duration as needed)
|
# Give some time for the frontend to start (adjust sleep duration as needed)
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
||||||
# Navigate to the test directory
|
#install backend
|
||||||
cd src/frontend
|
poetry install --extras deploy
|
||||||
|
|
||||||
# Run frontend only Playwright tests with or without UI based on the --ui flag
|
|
||||||
if [ "$ui" = true ]; then
|
|
||||||
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --ui --project=chromium
|
|
||||||
else
|
|
||||||
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --project=chromium
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Navigate back to the project root directory
|
|
||||||
cd ../../
|
|
||||||
|
|
||||||
# Start the backend using 'make backend' in the background
|
# Start the backend using 'make backend' in the background
|
||||||
make backend &
|
LANGFLOW_DATABASE_URL=sqlite:///./temp LANGFLOW_AUTO_LOGIN=True poetry run langflow run --backend-only --port 7860 --host 0.0.0.0 --no-open-browser &
|
||||||
|
|
||||||
# Give some time for the backend to start (adjust sleep duration as needed)
|
# Give some time for the backend to start (adjust sleep duration as needed)
|
||||||
sleep 25
|
sleep 25
|
||||||
|
|
||||||
# Navigate back to the test directory
|
# Navigate to the test directory
|
||||||
cd src/frontend
|
cd src/frontend
|
||||||
|
|
||||||
# Run Playwright tests with or without UI based on the --ui flag
|
# Run Playwright tests with or without UI based on the --ui flag
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,12 @@ import PromptAreaComponent from "../../../../components/promptComponent";
|
||||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||||
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||||
import { Button } from "../../../../components/ui/button";
|
import { Button } from "../../../../components/ui/button";
|
||||||
|
import { RefreshButton } from "../../../../components/ui/refreshButton";
|
||||||
import {
|
import {
|
||||||
|
INPUT_HANDLER_HOVER,
|
||||||
LANGFLOW_SUPPORTED_TYPES,
|
LANGFLOW_SUPPORTED_TYPES,
|
||||||
|
OUTPUT_HANDLER_HOVER,
|
||||||
TOOLTIP_EMPTY,
|
TOOLTIP_EMPTY,
|
||||||
inputHandleHover,
|
|
||||||
outputHandleHover,
|
|
||||||
} from "../../../../constants/constants";
|
} from "../../../../constants/constants";
|
||||||
import { postCustomComponentUpdate } from "../../../../controllers/API";
|
import { postCustomComponentUpdate } from "../../../../controllers/API";
|
||||||
import useAlertStore from "../../../../stores/alertStore";
|
import useAlertStore from "../../../../stores/alertStore";
|
||||||
|
|
@ -68,7 +69,7 @@ export default function ParameterComponent({
|
||||||
const nodes = useFlowStore((state) => state.nodes);
|
const nodes = useFlowStore((state) => state.nodes);
|
||||||
const edges = useFlowStore((state) => state.edges);
|
const edges = useFlowStore((state) => state.edges);
|
||||||
const setNode = useFlowStore((state) => state.setNode);
|
const setNode = useFlowStore((state) => state.setNode);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const flow = currentFlow?.data?.nodes ?? null;
|
const flow = currentFlow?.data?.nodes ?? null;
|
||||||
|
|
||||||
const groupedEdge = useRef(null);
|
const groupedEdge = useRef(null);
|
||||||
|
|
@ -85,7 +86,12 @@ export default function ParameterComponent({
|
||||||
|
|
||||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||||
|
|
||||||
const handleUpdateValues = async (name: string, data: NodeDataType) => {
|
const handleUpdateValues = async (
|
||||||
|
name: string,
|
||||||
|
data: NodeDataType,
|
||||||
|
delayAnimation: boolean = true
|
||||||
|
) => {
|
||||||
|
setIsLoading(true);
|
||||||
const code = data.node?.template["code"]?.value;
|
const code = data.node?.template["code"]?.value;
|
||||||
if (!code) {
|
if (!code) {
|
||||||
console.error("Code not found in the template");
|
console.error("Code not found in the template");
|
||||||
|
|
@ -95,13 +101,48 @@ export default function ParameterComponent({
|
||||||
try {
|
try {
|
||||||
const res = await postCustomComponentUpdate(code, name);
|
const res = await postCustomComponentUpdate(code, name);
|
||||||
if (res.status === 200 && data.node?.template) {
|
if (res.status === 200 && data.node?.template) {
|
||||||
data.node!.template[name] = res.data.template[name];
|
setNode(data.id, (oldNode) => {
|
||||||
|
let newNode = cloneDeep(oldNode);
|
||||||
|
|
||||||
|
newNode.data = {
|
||||||
|
...newNode.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
newNode.data.node.template[name] = res.data.template[name];
|
||||||
|
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setErrorData(err as { title: string; list?: Array<string> });
|
setErrorData(err as { title: string; list?: Array<string> });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderTooltips();
|
||||||
|
if (delayAnimation) {
|
||||||
|
try {
|
||||||
|
// Wait for at least 500 milliseconds
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
// Continue with the request
|
||||||
|
// If the request takes longer than 500 milliseconds, it will not wait an additional 500 milliseconds
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error occurred while waiting for refresh:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
} else setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function fetchData() {
|
||||||
|
if (
|
||||||
|
data.node?.template[name]?.refresh &&
|
||||||
|
Object.keys(data.node?.template[name]?.options ?? {}).length === 0
|
||||||
|
) {
|
||||||
|
handleUpdateValues(name, data, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
const handleOnNewValue = (
|
const handleOnNewValue = (
|
||||||
newValue: string | string[] | boolean | Object[]
|
newValue: string | string[] | boolean | Object[]
|
||||||
): void => {
|
): void => {
|
||||||
|
|
@ -182,7 +223,7 @@ export default function ParameterComponent({
|
||||||
return (
|
return (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
{index === 0 && (
|
{index === 0 && (
|
||||||
<span>{left ? inputHandleHover : outputHandleHover}</span>
|
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
|
|
@ -212,15 +253,15 @@ export default function ParameterComponent({
|
||||||
{item.display_name === "" ? "" : " - "}
|
{item.display_name === "" ? "" : " - "}
|
||||||
{item.display_name.split(", ").length > 2
|
{item.display_name.split(", ").length > 2
|
||||||
? item.display_name.split(", ").map((el, index) => (
|
? item.display_name.split(", ").map((el, index) => (
|
||||||
<React.Fragment key={el + index}>
|
<React.Fragment key={el + name}>
|
||||||
<span>
|
<span>
|
||||||
{index ===
|
{index ===
|
||||||
item.display_name.split(", ").length - 1
|
item.display_name.split(", ").length - 1
|
||||||
? el
|
? el
|
||||||
: (el += `, `)}
|
: (el += `, `)}
|
||||||
</span>
|
</span>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))
|
))
|
||||||
: item.display_name}
|
: item.display_name}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -229,14 +270,14 @@ export default function ParameterComponent({
|
||||||
{item.type === "" ? "" : " - "}
|
{item.type === "" ? "" : " - "}
|
||||||
{item.type.split(", ").length > 2
|
{item.type.split(", ").length > 2
|
||||||
? item.type.split(", ").map((el, index) => (
|
? item.type.split(", ").map((el, index) => (
|
||||||
<React.Fragment key={el + index}>
|
<React.Fragment key={el + name}>
|
||||||
<span>
|
<span>
|
||||||
{index === item.type.split(", ").length - 1
|
{index === item.type.split(", ").length - 1
|
||||||
? el
|
? el
|
||||||
: (el += `, `)}
|
: (el += `, `)}
|
||||||
</span>
|
</span>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))
|
))
|
||||||
: item.type}
|
: item.type}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -304,24 +345,31 @@ export default function ParameterComponent({
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={
|
className={
|
||||||
"relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" +
|
"relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" +
|
||||||
(name === "code" ? " hidden " : "")
|
((name === "code" && type === "code") ||
|
||||||
|
(name.includes("code") && proxy)
|
||||||
|
? " hidden "
|
||||||
|
: "")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
"w-full truncate text-sm" +
|
"w-full truncate text-sm" +
|
||||||
(left ? "" : " text-end") +
|
(left ? "" : " gap-2 flex justify-end items-center") +
|
||||||
(info !== "" ? " flex items-center" : "")
|
(info !== "" ? " flex items-center" : "")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{!left && data.node?.pinned &&
|
||||||
|
<div>
|
||||||
|
<IconComponent className="w-5 h-5 text-ice" name={"Snowflake"} />
|
||||||
|
</div>}
|
||||||
{proxy ? (
|
{proxy ? (
|
||||||
<ShadTooltip content={<span>{proxy.id}</span>}>
|
<ShadTooltip content={<span>{proxy.id}</span>}>
|
||||||
<span>{title}</span>
|
<span className={!left && data.node?.pinned?" text-ice":""}>{title}</span>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
) : (
|
) : (
|
||||||
title
|
<span className={!left && data.node?.pinned?" text-ice":""}>{title}</span>
|
||||||
)}
|
)}
|
||||||
<span className={(info === "" ? "" : "ml-1 ") + " text-status-red"}>
|
<span className={(info === "" ? "" : "ml-1 ") + " text-status-red"}>
|
||||||
{required ? " *" : ""}
|
{required ? " *" : ""}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -383,20 +431,35 @@ export default function ParameterComponent({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{left === true &&
|
{left === true &&
|
||||||
type === "str" &&
|
type === "str" &&
|
||||||
!data.node?.template[name].options ? (
|
!data.node?.template[name].options ? (
|
||||||
<div className="mt-2 w-full">
|
<div className="mt-2 w-full">
|
||||||
{data.node?.template[name].list ? (
|
{data.node?.template[name].list ? (
|
||||||
<InputListComponent
|
<div className="w-5/6 flex-grow">
|
||||||
disabled={disabled}
|
<InputListComponent
|
||||||
value={
|
disabled={disabled}
|
||||||
!data.node.template[name].value ||
|
value={
|
||||||
data.node.template[name].value === ""
|
!data.node.template[name].value ||
|
||||||
? [""]
|
data.node.template[name].value === ""
|
||||||
: data.node.template[name].value
|
? [""]
|
||||||
}
|
: data.node.template[name].value
|
||||||
onChange={handleOnNewValue}
|
}
|
||||||
/>
|
onChange={handleOnNewValue}
|
||||||
|
/>
|
||||||
|
{data.node?.template[name].refresh && (
|
||||||
|
<div className="w-1/6">
|
||||||
|
<RefreshButton
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={disabled}
|
||||||
|
name={name}
|
||||||
|
data={data}
|
||||||
|
className="extra-side-bar-buttons ml-2 mt-1"
|
||||||
|
handleUpdateValues={handleUpdateValues}
|
||||||
|
id={"refresh-button-" + name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : data.node?.template[name].multiline ? (
|
) : data.node?.template[name].multiline ? (
|
||||||
<TextAreaComponent
|
<TextAreaComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
@ -409,7 +472,7 @@ export default function ParameterComponent({
|
||||||
<div className="mt-2 flex w-full items-center">
|
<div className="mt-2 flex w-full items-center">
|
||||||
<div className="w-5/6 flex-grow">
|
<div className="w-5/6 flex-grow">
|
||||||
<InputComponent
|
<InputComponent
|
||||||
id={"input-" + index}
|
id={"input-" + name}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
password={data.node?.template[name].password ?? false}
|
password={data.node?.template[name].password ?? false}
|
||||||
value={data.node?.template[name].value ?? ""}
|
value={data.node?.template[name].value ?? ""}
|
||||||
|
|
@ -417,14 +480,17 @@ export default function ParameterComponent({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{data.node?.template[name].refresh && (
|
{data.node?.template[name].refresh && (
|
||||||
<button
|
<div className="w-1/6">
|
||||||
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
|
<RefreshButton
|
||||||
onClick={() => {
|
isLoading={isLoading}
|
||||||
handleUpdateValues(name, data);
|
disabled={disabled}
|
||||||
}}
|
name={name}
|
||||||
>
|
data={data}
|
||||||
<IconComponent name="RefreshCcw" />
|
className="extra-side-bar-buttons ml-2 mt-1"
|
||||||
</button>
|
handleUpdateValues={handleUpdateValues}
|
||||||
|
id={"refresh-button-" + name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -432,11 +498,12 @@ export default function ParameterComponent({
|
||||||
) : left === true && type === "bool" ? (
|
) : left === true && type === "bool" ? (
|
||||||
<div className="mt-2 w-full">
|
<div className="mt-2 w-full">
|
||||||
<ToggleShadComponent
|
<ToggleShadComponent
|
||||||
id={"toggle-" + index}
|
id={"toggle-" + name}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
enabled={data.node?.template[name].value ?? false}
|
enabled={data.node?.template[name].value ?? false}
|
||||||
setEnabled={handleOnNewValue}
|
setEnabled={handleOnNewValue}
|
||||||
size="large"
|
size="large"
|
||||||
|
editNode={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : left === true && type === "float" ? (
|
) : left === true && type === "float" ? (
|
||||||
|
|
@ -450,26 +517,31 @@ export default function ParameterComponent({
|
||||||
</div>
|
</div>
|
||||||
) : left === true &&
|
) : left === true &&
|
||||||
type === "str" &&
|
type === "str" &&
|
||||||
data.node?.template[name].options ? (
|
(data.node?.template[name].options ||
|
||||||
|
data.node?.template[name]?.refresh) ? (
|
||||||
// TODO: Improve CSS
|
// TODO: Improve CSS
|
||||||
<div className="mt-2 flex w-full items-center">
|
<div className="mt-2 flex w-full items-center">
|
||||||
<div className="w-5/6 flex-grow">
|
<div className="w-5/6 flex-grow">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
isLoading={isLoading}
|
||||||
options={data.node.template[name].options}
|
options={data.node.template[name].options}
|
||||||
onSelect={handleOnNewValue}
|
onSelect={handleOnNewValue}
|
||||||
value={data.node.template[name].value ?? "Choose an option"}
|
value={data.node.template[name].value ?? "Choose an option"}
|
||||||
id={"dropdown-" + index}
|
id={"dropdown-" + name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{data.node?.template[name].refresh && (
|
{data.node?.template[name].refresh && (
|
||||||
<button
|
<div className="w-1/6">
|
||||||
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
|
<RefreshButton
|
||||||
onClick={() => {
|
isLoading={isLoading}
|
||||||
handleUpdateValues(name, data);
|
disabled={disabled}
|
||||||
}}
|
name={name}
|
||||||
>
|
data={data}
|
||||||
<IconComponent name="RefreshCcw" />
|
className="extra-side-bar-buttons ml-2 mt-1"
|
||||||
</button>
|
handleUpdateValues={handleUpdateValues}
|
||||||
|
id={"refresh-button-" + name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : left === true && type === "code" ? (
|
) : left === true && type === "code" ? (
|
||||||
|
|
@ -486,7 +558,7 @@ export default function ParameterComponent({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={data.node?.template[name].value ?? ""}
|
value={data.node?.template[name].value ?? ""}
|
||||||
onChange={handleOnNewValue}
|
onChange={handleOnNewValue}
|
||||||
id={"code-input-" + index}
|
id={"code-input-" + name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : left === true && type === "file" ? (
|
) : left === true && type === "file" ? (
|
||||||
|
|
@ -507,7 +579,7 @@ export default function ParameterComponent({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={data.node?.template[name].value ?? ""}
|
value={data.node?.template[name].value ?? ""}
|
||||||
onChange={handleOnNewValue}
|
onChange={handleOnNewValue}
|
||||||
id={"int-input-" + index}
|
id={"int-input-" + name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : left === true && type === "prompt" ? (
|
) : left === true && type === "prompt" ? (
|
||||||
|
|
@ -520,8 +592,8 @@ export default function ParameterComponent({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={data.node?.template[name].value ?? ""}
|
value={data.node?.template[name].value ?? ""}
|
||||||
onChange={handleOnNewValue}
|
onChange={handleOnNewValue}
|
||||||
id={"prompt-input-" + index}
|
id={"prompt-input-" + name}
|
||||||
data-testid={"prompt-input-" + index}
|
data-testid={"prompt-input-" + name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : left === true && type === "NestedDict" ? (
|
) : left === true && type === "NestedDict" ? (
|
||||||
|
|
@ -531,10 +603,10 @@ export default function ParameterComponent({
|
||||||
editNode={false}
|
editNode={false}
|
||||||
value={
|
value={
|
||||||
!data.node!.template[name].value ||
|
!data.node!.template[name].value ||
|
||||||
data.node!.template[name].value?.toString() === "{}"
|
data.node!.template[name].value?.toString() === "{}"
|
||||||
? {
|
? {
|
||||||
yourkey: "value",
|
yourkey: "value",
|
||||||
}
|
}
|
||||||
: data.node!.template[name].value
|
: data.node!.template[name].value
|
||||||
}
|
}
|
||||||
onChange={handleOnNewValue}
|
onChange={handleOnNewValue}
|
||||||
|
|
@ -548,7 +620,7 @@ export default function ParameterComponent({
|
||||||
editNode={false}
|
editNode={false}
|
||||||
value={
|
value={
|
||||||
data.node!.template[name].value?.length === 0 ||
|
data.node!.template[name].value?.length === 0 ||
|
||||||
!data.node!.template[name].value
|
!data.node!.template[name].value
|
||||||
? [{ "": "" }]
|
? [{ "": "" }]
|
||||||
: convertObjToArray(data.node!.template[name].value)
|
: convertObjToArray(data.node!.template[name].value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ import Loading from "../../components/ui/loading";
|
||||||
import { Textarea } from "../../components/ui/textarea";
|
import { Textarea } from "../../components/ui/textarea";
|
||||||
import Xmark from "../../components/ui/xmark";
|
import Xmark from "../../components/ui/xmark";
|
||||||
import {
|
import {
|
||||||
|
RUN_TIMESTAMP_PREFIX,
|
||||||
|
STATUS_BUILD,
|
||||||
|
STATUS_BUILDING,
|
||||||
priorityFields,
|
priorityFields,
|
||||||
statusBuild,
|
|
||||||
statusBuilding,
|
|
||||||
} from "../../constants/constants";
|
} from "../../constants/constants";
|
||||||
import { BuildStatus } from "../../constants/enums";
|
import { BuildStatus } from "../../constants/enums";
|
||||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||||
|
|
@ -49,11 +50,18 @@ export default function GenericNode({
|
||||||
const [nodeDescription, setNodeDescription] = useState(
|
const [nodeDescription, setNodeDescription] = useState(
|
||||||
data.node?.description!
|
data.node?.description!
|
||||||
);
|
);
|
||||||
const buildStatus = useFlowStore((state) => state.flowBuildStatus[data.id]);
|
const buildStatus = useFlowStore(
|
||||||
|
(state) => state.flowBuildStatus[data.id]?.status
|
||||||
|
);
|
||||||
|
const lastRunTime = useFlowStore(
|
||||||
|
(state) => state.flowBuildStatus[data.id]?.timestamp
|
||||||
|
);
|
||||||
const [validationStatus, setValidationStatus] =
|
const [validationStatus, setValidationStatus] =
|
||||||
useState<validationStatusType | null>(null);
|
useState<validationStatusType | null>(null);
|
||||||
const [handles, setHandles] = useState<number>(0);
|
const [handles, setHandles] = useState<number>(0);
|
||||||
|
|
||||||
|
const [validationString, setValidationString] = useState<string>("");
|
||||||
|
|
||||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||||
|
|
||||||
function countHandles(): void {
|
function countHandles(): void {
|
||||||
|
|
@ -100,7 +108,7 @@ export default function GenericNode({
|
||||||
if (duration === undefined) {
|
if (duration === undefined) {
|
||||||
return "";
|
return "";
|
||||||
} else {
|
} else {
|
||||||
return `Duration: ${duration}`;
|
return `${duration}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const durationString = getDurationString(validationStatus?.data.duration);
|
const durationString = getDurationString(validationStatus?.data.duration);
|
||||||
|
|
@ -126,6 +134,18 @@ export default function GenericNode({
|
||||||
}
|
}
|
||||||
}, [flowPool[data.id], data.id]);
|
}, [flowPool[data.id], data.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (validationStatus?.params) {
|
||||||
|
// if it is not a string turn it into a string
|
||||||
|
let newValidationString = validationStatus.params;
|
||||||
|
if (typeof newValidationString !== "string") {
|
||||||
|
newValidationString = JSON.stringify(validationStatus.params);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValidationString(newValidationString);
|
||||||
|
}
|
||||||
|
}, [validationStatus, validationStatus?.params]);
|
||||||
|
|
||||||
const showNode = data.showNode ?? true;
|
const showNode = data.showNode ?? true;
|
||||||
|
|
||||||
const nameEditable = true;
|
const nameEditable = true;
|
||||||
|
|
@ -332,8 +352,8 @@ export default function GenericNode({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="group flex items-center gap-2.5">
|
<div className="group flex items-start gap-1.5">
|
||||||
<ShadTooltip content={data.id}>
|
<ShadTooltip content={data.node?.display_name}>
|
||||||
<div
|
<div
|
||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
if (nameEditable) {
|
if (nameEditable) {
|
||||||
|
|
@ -359,8 +379,8 @@ export default function GenericNode({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="Pencil"
|
name="PencilLine"
|
||||||
className="hidden h-4 w-4 animate-pulse text-status-blue group-hover:block"
|
className="hidden h-3 w-3 text-status-blue group-hover:block"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -390,13 +410,34 @@ export default function GenericNode({
|
||||||
})}
|
})}
|
||||||
data={data}
|
data={data}
|
||||||
color={
|
color={
|
||||||
nodeColors[
|
data.node?.template[templateField].input_types &&
|
||||||
types[data.node?.template[templateField].type!]
|
data.node?.template[templateField].input_types!
|
||||||
] ??
|
.length > 0
|
||||||
nodeColors[
|
? nodeColors[
|
||||||
data.node?.template[templateField].type!
|
data.node?.template[templateField]
|
||||||
] ??
|
.input_types![
|
||||||
nodeColors.unknown
|
data.node?.template[templateField]
|
||||||
|
.input_types!.length - 1
|
||||||
|
]
|
||||||
|
] ??
|
||||||
|
nodeColors[
|
||||||
|
types[
|
||||||
|
data.node?.template[templateField]
|
||||||
|
.input_types![
|
||||||
|
data.node?.template[templateField]
|
||||||
|
.input_types!.length - 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
: nodeColors[
|
||||||
|
data.node?.template[templateField].type!
|
||||||
|
] ??
|
||||||
|
nodeColors[
|
||||||
|
types[
|
||||||
|
data.node?.template[templateField].type!
|
||||||
|
]
|
||||||
|
] ??
|
||||||
|
nodeColors.unknown
|
||||||
}
|
}
|
||||||
title={getFieldTitle(
|
title={getFieldTitle(
|
||||||
data.node?.template!,
|
data.node?.template!,
|
||||||
|
|
@ -458,38 +499,55 @@ export default function GenericNode({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showNode && (
|
{showNode && (
|
||||||
<Button
|
<Button variant="secondary" className={"group h-9 px-1.5"}>
|
||||||
variant="secondary"
|
|
||||||
className={"group h-9 px-1.5"}
|
|
||||||
onClick={() => {
|
|
||||||
if (buildStatus === BuildStatus.BUILDING || isBuilding)
|
|
||||||
return;
|
|
||||||
setValidationStatus(null);
|
|
||||||
buildFlow({ nodeId: data.id });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<ShadTooltip
|
<ShadTooltip
|
||||||
content={
|
content={
|
||||||
buildStatus === BuildStatus.BUILDING ? (
|
buildStatus === BuildStatus.BUILDING ? (
|
||||||
<span> {statusBuilding} </span>
|
<span> {STATUS_BUILDING} </span>
|
||||||
) : !validationStatus ? (
|
) : !validationStatus ? (
|
||||||
<span className="flex">{statusBuild}</span>
|
<span className="flex">{STATUS_BUILD}</span>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-96 overflow-auto">
|
<div className="max-h-100">
|
||||||
{typeof validationStatus.params === "string"
|
<div>
|
||||||
? `${durationString}\n${validationStatus.params}`
|
{lastRunTime && (
|
||||||
.split("\n")
|
<div className="justify-left flex text-muted-foreground">
|
||||||
.map((line, index) => (
|
<div>{RUN_TIMESTAMP_PREFIX}</div>
|
||||||
<div key={index}>{line}</div>
|
<div className="ml-1 text-status-blue">
|
||||||
))
|
{lastRunTime}
|
||||||
: durationString}
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="justify-left flex text-muted-foreground">
|
||||||
|
<div>Duration:</div>
|
||||||
|
<div className="ml-1 text-status-blue">
|
||||||
|
{validationStatus?.data.duration}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<span className="flex justify-center text-muted-foreground ">
|
||||||
|
Output
|
||||||
|
</span>
|
||||||
|
<div className="max-h-96 overflow-auto custom-scroll">
|
||||||
|
{validationString.split("\n").map((line, index) => (
|
||||||
|
<div key={index}>{line}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
>
|
>
|
||||||
<div className="generic-node-status-position flex items-center justify-center">
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
if (buildStatus === BuildStatus.BUILDING || isBuilding)
|
||||||
|
return;
|
||||||
|
setValidationStatus(null);
|
||||||
|
buildFlow({ stopNodeId: data.id });
|
||||||
|
}}
|
||||||
|
className="generic-node-status-position flex items-center justify-center"
|
||||||
|
>
|
||||||
{renderIconStatus(buildStatus, validationStatus)}
|
{renderIconStatus(buildStatus, validationStatus)}
|
||||||
</div>
|
</div>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
|
|
@ -607,13 +665,18 @@ export default function GenericNode({
|
||||||
data.node?.template[templateField].input_types!
|
data.node?.template[templateField].input_types!
|
||||||
.length > 0
|
.length > 0
|
||||||
? nodeColors[
|
? nodeColors[
|
||||||
data.node?.template[templateField]
|
data.node?.template[templateField].input_types![
|
||||||
.input_types![0]
|
data.node?.template[templateField]
|
||||||
|
.input_types!.length - 1
|
||||||
|
]
|
||||||
] ??
|
] ??
|
||||||
nodeColors[
|
nodeColors[
|
||||||
types[
|
types[
|
||||||
data.node?.template[templateField]
|
data.node?.template[templateField]
|
||||||
.input_types![0]
|
.input_types![
|
||||||
|
data.node?.template[templateField]
|
||||||
|
.input_types!.length - 1
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
: nodeColors[
|
: nodeColors[
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "../../components/ui/popover";
|
} from "../../components/ui/popover";
|
||||||
import { zeroNotifications } from "../../constants/constants";
|
import { ZERO_NOTIFICATIONS } from "../../constants/constants";
|
||||||
import useAlertStore from "../../stores/alertStore";
|
import useAlertStore from "../../stores/alertStore";
|
||||||
import { AlertDropdownType } from "../../types/alerts";
|
import { AlertDropdownType } from "../../types/alerts";
|
||||||
import SingleAlert from "./components/singleAlertComponent";
|
import SingleAlert from "./components/singleAlertComponent";
|
||||||
|
|
@ -70,7 +70,7 @@ export default function AlertDropdown({
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center pb-16 text-ring">
|
<div className="flex h-full w-full items-center justify-center pb-16 text-ring">
|
||||||
{zeroNotifications}
|
{ZERO_NOTIFICATIONS}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,10 @@ export default function AccordionComponent({
|
||||||
>
|
>
|
||||||
{trigger}
|
{trigger}
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="AccordionContent flex flex-col">
|
<AccordionContent>
|
||||||
{children}
|
<div className="AccordionContent flex flex-col">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
CHAT_FORM_DIALOG_SUBTITLE,
|
CHAT_FORM_DIALOG_SUBTITLE,
|
||||||
outputsModalTitle,
|
OUTPUTS_MODAL_TITLE,
|
||||||
textInputModalTitle,
|
TEXT_INPUT_MODAL_TITLE,
|
||||||
} from "../../constants/constants";
|
} from "../../constants/constants";
|
||||||
import BaseModal from "../../modals/baseModal";
|
import BaseModal from "../../modals/baseModal";
|
||||||
import useFlowStore from "../../stores/flowStore";
|
import useFlowStore from "../../stores/flowStore";
|
||||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||||
|
import { NodeType } from "../../types/flow";
|
||||||
import { updateVerticesOrder } from "../../utils/buildUtils";
|
import { updateVerticesOrder } from "../../utils/buildUtils";
|
||||||
import { cn } from "../../utils/utils";
|
import { cn } from "../../utils/utils";
|
||||||
import AccordionComponent from "../AccordionComponent";
|
import AccordionComponent from "../AccordionComponent";
|
||||||
import IOInputField from "../IOInputField";
|
import IOInputField from "../IOInputField";
|
||||||
import IOOutputView from "../IOOutputView";
|
import IOOutputView from "../IOOutputView";
|
||||||
|
import ShadTooltip from "../ShadTooltipComponent";
|
||||||
import IconComponent from "../genericIconComponent";
|
import IconComponent from "../genericIconComponent";
|
||||||
import NewChatView from "../newChatView";
|
import NewChatView from "../newChatView";
|
||||||
import { Badge } from "../ui/badge";
|
import { Badge } from "../ui/badge";
|
||||||
|
|
@ -50,6 +52,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
|
||||||
const [chatValue, setChatValue] = useState("");
|
const [chatValue, setChatValue] = useState("");
|
||||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||||
|
const setNode = useFlowStore((state) => state.setNode);
|
||||||
|
|
||||||
async function updateVertices() {
|
async function updateVertices() {
|
||||||
return updateVerticesOrder(currentFlow!.id, null);
|
return updateVerticesOrder(currentFlow!.id, null);
|
||||||
|
|
@ -67,12 +70,22 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
|
||||||
setLockChat(true);
|
setLockChat(true);
|
||||||
setChatValue("");
|
setChatValue("");
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
await buildFlow({ input_value: chatValue }).catch((err) => {
|
await buildFlow({
|
||||||
|
input_value: chatValue,
|
||||||
|
startNodeId: chatInput?.id,
|
||||||
|
}).catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setLockChat(false);
|
setLockChat(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setLockChat(false);
|
setLockChat(false);
|
||||||
|
if (chatInput) {
|
||||||
|
setNode(chatInput.id, (node: NodeType) => {
|
||||||
|
const newNode = { ...node };
|
||||||
|
newNode.data.node!.template["input_value"].value = chatValue;
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -134,7 +147,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
|
||||||
>
|
>
|
||||||
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
|
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
|
||||||
<IconComponent className="h-4 w-4" name={"Type"} />
|
<IconComponent className="h-4 w-4" name={"Type"} />
|
||||||
{textInputModalTitle}
|
{TEXT_INPUT_MODAL_TITLE}
|
||||||
</div>
|
</div>
|
||||||
{nodes
|
{nodes
|
||||||
.filter((node) =>
|
.filter((node) =>
|
||||||
|
|
@ -195,7 +208,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
|
||||||
>
|
>
|
||||||
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
|
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
|
||||||
<IconComponent className="h-4 w-4" name={"FileType2"} />
|
<IconComponent className="h-4 w-4" name={"FileType2"} />
|
||||||
{outputsModalTitle}
|
{OUTPUTS_MODAL_TITLE}
|
||||||
</div>
|
</div>
|
||||||
{nodes
|
{nodes
|
||||||
.filter((node) =>
|
.filter((node) =>
|
||||||
|
|
@ -213,9 +226,16 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
|
||||||
<AccordionComponent
|
<AccordionComponent
|
||||||
trigger={
|
trigger={
|
||||||
<div className="file-component-badge-div">
|
<div className="file-component-badge-div">
|
||||||
<Badge variant="gray" size="md">
|
<ShadTooltip
|
||||||
{output.id}
|
content={output.id}
|
||||||
</Badge>
|
styleClasses="z-50"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Badge variant="gray" size="md">
|
||||||
|
{node.data.node.display_name}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</ShadTooltip>
|
||||||
{haveChat && (
|
{haveChat && (
|
||||||
<div
|
<div
|
||||||
className="-mb-1 pr-4"
|
className="-mb-1 pr-4"
|
||||||
|
|
@ -255,7 +275,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{haveChat ? (
|
{haveChat ? (
|
||||||
<div className="flex h-full w-full">
|
<div className="flex h-full min-w-96 flex-grow">
|
||||||
{selectedViewField && (
|
{selectedViewField && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -272,7 +292,7 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
|
||||||
</button>
|
</button>
|
||||||
{selectedViewField.type}
|
{selectedViewField.type}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full">
|
<div className="h-full w-full">
|
||||||
{inputs.some(
|
{inputs.some(
|
||||||
(input) => input.id === selectedViewField.id
|
(input) => input.id === selectedViewField.id
|
||||||
) ? (
|
) ? (
|
||||||
|
|
@ -308,28 +328,30 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
|
||||||
<div className="absolute bottom-8 right-8"></div>
|
<div className="absolute bottom-8 right-8"></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!haveChat && (
|
|
||||||
<div className="flex w-full justify-end pt-6">
|
|
||||||
<Button
|
|
||||||
variant={"outline"}
|
|
||||||
className="flex gap-2 px-3"
|
|
||||||
onClick={() => sendMessage(1)}
|
|
||||||
>
|
|
||||||
<IconComponent
|
|
||||||
name={isBuilding ? "Loader2" : "Play"}
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-4",
|
|
||||||
isBuilding
|
|
||||||
? "animate-spin"
|
|
||||||
: "fill-current text-medium-indigo"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
Run Flow
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</BaseModal.Content>
|
</BaseModal.Content>
|
||||||
|
<BaseModal.Footer>
|
||||||
|
{!haveChat && (
|
||||||
|
<div className="flex w-full justify-end pt-6">
|
||||||
|
<Button
|
||||||
|
variant={"outline"}
|
||||||
|
className="flex gap-2 px-3"
|
||||||
|
onClick={() => sendMessage(1)}
|
||||||
|
>
|
||||||
|
<IconComponent
|
||||||
|
name={isBuilding ? "Loader2" : "Play"}
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4",
|
||||||
|
isBuilding
|
||||||
|
? "animate-spin"
|
||||||
|
: "fill-current text-medium-indigo"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
Run Flow
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</BaseModal.Footer>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,8 @@ export default function CodeAreaComponent({
|
||||||
setNodeClass,
|
setNodeClass,
|
||||||
id = "",
|
id = "",
|
||||||
readonly = false,
|
readonly = false,
|
||||||
openModal,
|
open,
|
||||||
selected,
|
setOpen,
|
||||||
setOpenModal,
|
|
||||||
}: CodeAreaComponentType) {
|
}: CodeAreaComponentType) {
|
||||||
const [myValue, setMyValue] = useState(
|
const [myValue, setMyValue] = useState(
|
||||||
typeof value == "string" ? value : JSON.stringify(value)
|
typeof value == "string" ? value : JSON.stringify(value)
|
||||||
|
|
@ -35,9 +34,8 @@ export default function CodeAreaComponent({
|
||||||
return (
|
return (
|
||||||
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
||||||
<CodeAreaModal
|
<CodeAreaModal
|
||||||
selected={selected}
|
open={open}
|
||||||
openModal={openModal}
|
setOpen={setOpen}
|
||||||
setOpenModal={setOpenModal}
|
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
dynamic={dynamic}
|
dynamic={dynamic}
|
||||||
value={myValue}
|
value={myValue}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ export default function CodeTabsComponent({
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
className={
|
className={
|
||||||
"api-modal-tabs " +
|
"api-modal-tabs m-0 inset-0 " +
|
||||||
(isMessage ? "dark " : "") +
|
(isMessage ? "dark " : "") +
|
||||||
(dark && isMessage ? "bg-background" : "")
|
(dark && isMessage ? "bg-background" : "")
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +166,7 @@ export default function CodeTabsComponent({
|
||||||
) : (
|
) : (
|
||||||
<IconComponent name="Clipboard" className="h-4 w-4" />
|
<IconComponent name="Clipboard" className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
{isCopied ? "Copied!" : "Copy code"}
|
{isCopied ? "Copied!" : "Copy Code"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
||||||
|
|
@ -193,9 +193,9 @@ export default function CodeTabsComponent({
|
||||||
></div>
|
></div>
|
||||||
)}
|
)}
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
className="mt-0 h-full w-full overflow-auto custom-scroll"
|
language={tab.language}
|
||||||
language={tab.mode}
|
|
||||||
style={oneDark}
|
style={oneDark}
|
||||||
|
className="mt-0 h-full overflow-auto custom-scroll rounded-sm text-left"
|
||||||
>
|
>
|
||||||
{tab.code}
|
{tab.code}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { classNames } from "../../utils/utils";
|
||||||
import IconComponent from "../genericIconComponent";
|
import IconComponent from "../genericIconComponent";
|
||||||
|
|
||||||
export default function Dropdown({
|
export default function Dropdown({
|
||||||
|
isLoading,
|
||||||
value,
|
value,
|
||||||
options,
|
options,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
|
@ -129,11 +130,17 @@ export default function Dropdown({
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div>
|
{(!isLoading && (
|
||||||
<span className="text-sm italic">
|
<div>
|
||||||
No parameters are available for display.
|
<span className="text-sm italic">
|
||||||
</span>
|
No parameters are available for display.
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
)) || (
|
||||||
|
<div>
|
||||||
|
<span className="text-sm italic">Loading...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Node } from "reactflow";
|
import { Node } from "reactflow";
|
||||||
import { savedHover } from "../../../../constants/constants";
|
import { SAVED_HOVER } from "../../../../constants/constants";
|
||||||
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
|
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
|
||||||
import useAlertStore from "../../../../stores/alertStore";
|
import useAlertStore from "../../../../stores/alertStore";
|
||||||
import useFlowStore from "../../../../stores/flowStore";
|
import useFlowStore from "../../../../stores/flowStore";
|
||||||
|
|
@ -138,7 +138,7 @@ export const MenuBar = ({
|
||||||
</div>
|
</div>
|
||||||
<ShadTooltip
|
<ShadTooltip
|
||||||
content={
|
content={
|
||||||
savedHover +
|
SAVED_HOVER +
|
||||||
new Date(currentFlow.updated_at ?? "").toLocaleString("en-US", {
|
new Date(currentFlow.updated_at ?? "").toLocaleString("en-US", {
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "numeric",
|
minute: "numeric",
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ export default function InputComponent({
|
||||||
handleKeyDown(e, value, "");
|
handleKeyDown(e, value, "");
|
||||||
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||||
}}
|
}}
|
||||||
|
data-testid={editNode ? id + "-edit" : id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{password && (
|
{password && (
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ export default function IntComponent({
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
onChange(event.target.value);
|
onChange(event.target.value);
|
||||||
}}
|
}}
|
||||||
|
data-testid={id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { useEffect, useState } from "react";
|
||||||
import IconComponent from "../../../components/genericIconComponent";
|
import IconComponent from "../../../components/genericIconComponent";
|
||||||
import { Textarea } from "../../../components/ui/textarea";
|
import { Textarea } from "../../../components/ui/textarea";
|
||||||
import {
|
import {
|
||||||
chatInputPlaceholder,
|
CHAT_INPUT_PLACEHOLDER,
|
||||||
chatInputPlaceholderSend,
|
CHAT_INPUT_PLACEHOLDER_SEND,
|
||||||
} from "../../../constants/constants";
|
} from "../../../constants/constants";
|
||||||
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
|
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
|
||||||
import { chatInputType } from "../../../types/components";
|
import { chatInputType } from "../../../types/components";
|
||||||
|
|
@ -84,7 +84,7 @@ export default function ChatInput({
|
||||||
"form-modal-lockchat"
|
"form-modal-lockchat"
|
||||||
)}
|
)}
|
||||||
placeholder={
|
placeholder={
|
||||||
noInput ? chatInputPlaceholder : chatInputPlaceholderSend
|
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="form-modal-send-icon-position">
|
<div className="form-modal-send-icon-position">
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export function CodeBlock({ language, value }: Props): JSX.Element {
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button className="code-block-modal-button" onClick={copyToClipboard}>
|
<button className="code-block-modal-button" onClick={copyToClipboard}>
|
||||||
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
|
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
|
||||||
{isCopied ? "Copied!" : "Copy code"}
|
{isCopied ? "Copied!" : "Copy Code"}
|
||||||
</button>
|
</button>
|
||||||
<button className="code-block-modal-button" onClick={downloadAsFile}>
|
<button className="code-block-modal-button" onClick={downloadAsFile}>
|
||||||
<IconDownload size={18} />
|
<IconDownload size={18} />
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ export default function ChatMessage({
|
||||||
chat.isSend ? "" : " "
|
chat.isSend ? "" : " "
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={classNames("form-modal-chatbot-icon ")}>
|
<div className={classNames("form-modal-chatbot-icon")}>
|
||||||
{!chat.isSend ? (
|
{!chat.isSend ? (
|
||||||
<div className="form-modal-chat-image">
|
<div className="form-modal-chat-image">
|
||||||
<div className="form-modal-chat-bot-icon ">
|
<div className="form-modal-chat-bot-icon ">
|
||||||
|
|
@ -134,7 +134,7 @@ export default function ChatMessage({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!chat.isSend ? (
|
{!chat.isSend ? (
|
||||||
<div className="form-modal-chat-text-position">
|
<div className="form-modal-chat-text-position flex-grow min-w-96">
|
||||||
<div className="form-modal-chat-text">
|
<div className="form-modal-chat-text">
|
||||||
{hidden && chat.thought && chat.thought !== "" && (
|
{hidden && chat.thought && chat.thought !== "" && (
|
||||||
<div
|
<div
|
||||||
|
|
@ -155,9 +155,9 @@ export default function ChatMessage({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
|
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
|
||||||
<div className="w-full">
|
<div className="w-full flex flex-col">
|
||||||
<div className="w-full dark:text-white">
|
<div className="w-full flex flex-col dark:text-white">
|
||||||
<div className="w-full">
|
<div className="w-full flex flex-col">
|
||||||
{useMemo(
|
{useMemo(
|
||||||
() =>
|
() =>
|
||||||
chatMessage === "" && lockChat ? (
|
chatMessage === "" && lockChat ? (
|
||||||
|
|
@ -169,7 +169,7 @@ export default function ChatMessage({
|
||||||
<Markdown
|
<Markdown
|
||||||
remarkPlugins={[remarkGfm, remarkMath]}
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
rehypePlugins={[rehypeMathjax]}
|
rehypePlugins={[rehypeMathjax]}
|
||||||
className="markdown prose min-w-full text-primary word-break-break-word
|
className="markdown flex flex-col prose text-primary word-break-break-word
|
||||||
dark:prose-invert"
|
dark:prose-invert"
|
||||||
components={{
|
components={{
|
||||||
pre({ node, ...props }) {
|
pre({ node, ...props }) {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { useEffect, useRef, useState } from "react";
|
||||||
import IconComponent from "../../components/genericIconComponent";
|
import IconComponent from "../../components/genericIconComponent";
|
||||||
import { NOCHATOUTPUT_NOTICE_ALERT } from "../../constants/alerts_constants";
|
import { NOCHATOUTPUT_NOTICE_ALERT } from "../../constants/alerts_constants";
|
||||||
import {
|
import {
|
||||||
chatFirstInitialText,
|
CHAT_FIRST_INITIAL_TEXT,
|
||||||
chatSecondInitialText,
|
CHAT_SECOND_INITIAL_TEXT,
|
||||||
} from "../../constants/constants";
|
} from "../../constants/constants";
|
||||||
import { deleteFlowPool } from "../../controllers/API";
|
import { deleteFlowPool } from "../../controllers/API";
|
||||||
import useAlertStore from "../../stores/alertStore";
|
import useAlertStore from "../../stores/alertStore";
|
||||||
|
|
@ -182,14 +182,14 @@ export default function NewChatView({
|
||||||
<br />
|
<br />
|
||||||
<div className="langflow-chat-desc">
|
<div className="langflow-chat-desc">
|
||||||
<span className="langflow-chat-desc-span">
|
<span className="langflow-chat-desc-span">
|
||||||
{chatFirstInitialText}{" "}
|
{CHAT_FIRST_INITIAL_TEXT}{" "}
|
||||||
<span>
|
<span>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="MessageSquare"
|
name="MessageSquare"
|
||||||
className="mx-1 inline h-5 w-5 animate-bounce "
|
className="mx-1 inline h-5 w-5 animate-bounce "
|
||||||
/>
|
/>
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
{chatSecondInitialText}
|
{CHAT_SECOND_INITIAL_TEXT}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { editTextModalTitle } from "../../constants/constants";
|
import { EDIT_TEXT_MODAL_TITLE } from "../../constants/constants";
|
||||||
import { TypeModal } from "../../constants/enums";
|
import { TypeModal } from "../../constants/enums";
|
||||||
import GenericModal from "../../modals/genericModal";
|
import GenericModal from "../../modals/genericModal";
|
||||||
import { TextAreaComponentType } from "../../types/components";
|
import { TextAreaComponentType } from "../../types/components";
|
||||||
|
|
@ -38,7 +38,7 @@ export default function TextAreaComponent({
|
||||||
<GenericModal
|
<GenericModal
|
||||||
type={TypeModal.TEXT}
|
type={TypeModal.TEXT}
|
||||||
buttonText="Finish Editing"
|
buttonText="Finish Editing"
|
||||||
modalTitle={editTextModalTitle}
|
modalTitle={EDIT_TEXT_MODAL_TITLE}
|
||||||
value={value}
|
value={value}
|
||||||
setValue={(value: string) => {
|
setValue={(value: string) => {
|
||||||
onChange(value);
|
onChange(value);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export default function ToggleShadComponent({
|
||||||
disabled,
|
disabled,
|
||||||
size,
|
size,
|
||||||
id = "",
|
id = "",
|
||||||
|
editNode = false,
|
||||||
}: ToggleComponentType): JSX.Element {
|
}: ToggleComponentType): JSX.Element {
|
||||||
let scaleX, scaleY;
|
let scaleX, scaleY;
|
||||||
switch (size) {
|
switch (size) {
|
||||||
|
|
@ -31,6 +32,7 @@ export default function ToggleShadComponent({
|
||||||
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
|
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
|
||||||
<Switch
|
<Switch
|
||||||
id={id}
|
id={id}
|
||||||
|
data-testid={id}
|
||||||
style={{
|
style={{
|
||||||
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
|
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
|
"flex flex-col fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
51
src/frontend/src/components/ui/refreshButton.tsx
Normal file
51
src/frontend/src/components/ui/refreshButton.tsx
Normal 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 };
|
||||||
|
|
@ -56,3 +56,6 @@ export const USER_ADD_SUCCESS_ALERT = "Success! New user added!";
|
||||||
export const DEL_KEY_SUCCESS_ALERT = "Success! Key deleted!";
|
export const DEL_KEY_SUCCESS_ALERT = "Success! Key deleted!";
|
||||||
export const FLOW_BUILD_SUCCESS_ALERT = `Flow built successfully`;
|
export const FLOW_BUILD_SUCCESS_ALERT = `Flow built successfully`;
|
||||||
export const SAVE_SUCCESS_ALERT = "Changes saved successfully!";
|
export const SAVE_SUCCESS_ALERT = "Changes saved successfully!";
|
||||||
|
|
||||||
|
// Generic Node
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,18 @@ export const INVALID_CHARACTERS = [
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* regex to highlight the variables in the text
|
* regex to highlight the variables in the text
|
||||||
* @constant
|
* @constant regexHighlight
|
||||||
|
* @type {RegExp}
|
||||||
|
* @default
|
||||||
|
* @example
|
||||||
|
* {{variable}} or {variable}
|
||||||
|
* @returns {RegExp}
|
||||||
|
* @description
|
||||||
|
* This regex is used to highlight the variables in the text.
|
||||||
|
* It matches the variables in the text that are between {{}} or {}.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const regexHighlight = /\{([^}]+)\}/g;
|
export const regexHighlight = /\{\{(.*?)\}\}|\{([^{}]+)\}/g;
|
||||||
export const specialCharsRegex = /[!@#$%^&*()\-_=+[\]{}|;:'",.<>/?\\`´]/;
|
export const specialCharsRegex = /[!@#$%^&*()\-_=+[\]{}|;:'",.<>/?\\`´]/;
|
||||||
|
|
||||||
export const programmingLanguages: languageMap = {
|
export const programmingLanguages: languageMap = {
|
||||||
|
|
@ -683,38 +691,39 @@ export const priorityFields = new Set(["code", "template"]);
|
||||||
export const INPUT_TYPES = new Set(["ChatInput", "TextInput"]);
|
export const INPUT_TYPES = new Set(["ChatInput", "TextInput"]);
|
||||||
export const OUTPUT_TYPES = new Set(["ChatOutput", "TextOutput"]);
|
export const OUTPUT_TYPES = new Set(["ChatOutput", "TextOutput"]);
|
||||||
|
|
||||||
export const chatFirstInitialText =
|
export const CHAT_FIRST_INITIAL_TEXT =
|
||||||
"Start a conversation and click the agent's thoughts";
|
"Start a conversation and click the agent's thoughts";
|
||||||
|
|
||||||
export const chatSecondInitialText = "to inspect the chaining process.";
|
export const CHAT_SECOND_INITIAL_TEXT = "to inspect the chaining process.";
|
||||||
|
|
||||||
export const zeroNotifications = "No new notifications";
|
export const ZERO_NOTIFICATIONS = "No new notifications";
|
||||||
|
|
||||||
export const successBuild = "Built sucessfully ✨";
|
export const SUCCESS_BUILD = "Built sucessfully ✨";
|
||||||
|
|
||||||
export const alertSaveWApi =
|
export const ALERT_SAVE_WITH_API =
|
||||||
"Caution: Uncheck this box only removes API keys from fields specifically designated for API keys.";
|
"Caution: Uncheck this box only removes API keys from fields specifically designated for API keys.";
|
||||||
|
|
||||||
export const saveWApiCheckbox = "Save with my API keys";
|
export const SAVE_WITH_API_CHECKBOX = "Save with my API keys";
|
||||||
export const editTextModalTitle = "Edit Text";
|
export const EDIT_TEXT_MODAL_TITLE = "Edit Text";
|
||||||
export const editTextPlaceholder = "Type message here.";
|
export const EDIT_TEXT_PLACEHOLDER = "Type message here.";
|
||||||
export const inputHandleHover = "Avaliable input components:";
|
export const INPUT_HANDLER_HOVER = "Avaliable input components:";
|
||||||
export const outputHandleHover = "Avaliable output components:";
|
export const OUTPUT_HANDLER_HOVER = "Avaliable output components:";
|
||||||
export const textInputModalTitle = "Text Inputs";
|
export const TEXT_INPUT_MODAL_TITLE = "Text Inputs";
|
||||||
export const outputsModalTitle = "Text Outputs";
|
export const OUTPUTS_MODAL_TITLE = "Text Outputs";
|
||||||
export const langflowChatTitle = "Langflow Chat";
|
export const LANGFLOW_CHAT_TITLE = "Langflow Chat";
|
||||||
export const chatInputPlaceholder =
|
export const CHAT_INPUT_PLACEHOLDER =
|
||||||
"No chat input variables found. Click to run your flow.";
|
"No chat input variables found. Click to run your flow.";
|
||||||
export const chatInputPlaceholderSend = "Send a message...";
|
export const CHAT_INPUT_PLACEHOLDER_SEND = "Send a message...";
|
||||||
export const editCodeTitle = "Edit Code";
|
export const EDIT_CODE_TITLE = "Edit Code";
|
||||||
export const myCollectionDesc =
|
export const MY_COLLECTION_DESC =
|
||||||
"Manage your personal projects. Download or upload your collection.";
|
"Manage your personal projects. Download and upload entire collections.";
|
||||||
export const storeDesc = "Search flows and components from the community.";
|
export const STORE_DESC = "Explore community-shared flows and components.";
|
||||||
export const storeTitle = "Langflow Store";
|
export const STORE_TITLE = "Langflow Store";
|
||||||
export const noApi = "You don't have an API key. ";
|
export const NO_API_KEY = "You don't have an API key. ";
|
||||||
export const insertApi = "Insert your Langflow API key.";
|
export const INSERT_API_KEY = "Insert your Langflow API key.";
|
||||||
export const invalidApi = "Your API key is not valid. ";
|
export const INVALID_API_KEY = "Your API key is not valid. ";
|
||||||
export const createApi = `Don’t have an API key? Sign up at`;
|
export const CREATE_API_KEY = `Don’t have an API key? Sign up at`;
|
||||||
export const statusBuild = "Build to validate status.";
|
export const STATUS_BUILD = "Build to validate status.";
|
||||||
export const statusBuilding = "Building...";
|
export const STATUS_BUILDING = "Building...";
|
||||||
export const savedHover = "Last saved at ";
|
export const SAVED_HOVER = "Last saved at ";
|
||||||
|
export const RUN_TIMESTAMP_PREFIX = "Last Run: ";
|
||||||
|
|
|
||||||
|
|
@ -856,13 +856,16 @@ export async function requestLogout() {
|
||||||
|
|
||||||
export async function getVerticesOrder(
|
export async function getVerticesOrder(
|
||||||
flowId: string,
|
flowId: string,
|
||||||
nodeId?: string | null
|
startNodeId?: string | null,
|
||||||
|
stopNodeId?: string | null
|
||||||
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
|
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
|
||||||
// nodeId is optional and is a query parameter
|
// nodeId is optional and is a query parameter
|
||||||
// if nodeId is not provided, the API will return all vertices
|
// if nodeId is not provided, the API will return all vertices
|
||||||
const config = {};
|
const config = {};
|
||||||
if (nodeId) {
|
if (stopNodeId) {
|
||||||
config["params"] = { component_id: nodeId };
|
config["params"] = { stop_component_id: stopNodeId };
|
||||||
|
} else if (startNodeId) {
|
||||||
|
config["params"] = { start_component_id: startNodeId };
|
||||||
}
|
}
|
||||||
return await api.get(`${BASE_URL_API}build/${flowId}/vertices`, config);
|
return await api.get(`${BASE_URL_API}build/${flowId}/vertices`, config);
|
||||||
}
|
}
|
||||||
|
|
@ -872,6 +875,7 @@ export async function postBuildVertex(
|
||||||
vertexId: string,
|
vertexId: string,
|
||||||
input_value: string
|
input_value: string
|
||||||
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
|
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
|
||||||
|
// input_value is optional and is a query parameter
|
||||||
return await api.post(
|
return await api.post(
|
||||||
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
|
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
|
||||||
input_value ? { inputs: { input_value: input_value } } : undefined
|
input_value ? { inputs: { input_value: input_value } } : undefined
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ function ConfirmationModal({
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
onConfirm(index, data);
|
onConfirm(index, data);
|
||||||
}}
|
}}
|
||||||
|
data-testid="replace-button"
|
||||||
>
|
>
|
||||||
{confirmationText}
|
{confirmationText}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ export default function DeleteConfirmationModal({
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<span>
|
<span>
|
||||||
Are you sure you want to delete this {description ?? "component"}?
|
Confirm deletion of {description ?? "component"}?
|
||||||
<br></br>
|
<br></br>
|
||||||
This action cannot be undone.
|
Note: This action is irreversible.
|
||||||
</span>
|
</span>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose>
|
<DialogClose>
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,8 @@ const EditNodeModal = forwardRef(
|
||||||
)
|
)
|
||||||
) ?? false;
|
) ?? false;
|
||||||
return (
|
return (
|
||||||
<TableRow key={index} className="h-10">
|
<TableRow key={index} className={"h-10 " + ((templateParam==="code" && myData.node?.template[templateParam].type==="code") || (templateParam.includes("code") && myData.node?.template[templateParam].proxy) ? " hidden " : "")
|
||||||
|
}>
|
||||||
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
|
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
|
||||||
<ShadTooltip
|
<ShadTooltip
|
||||||
content={
|
content={
|
||||||
|
|
@ -242,7 +243,11 @@ const EditNodeModal = forwardRef(
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<InputComponent
|
<InputComponent
|
||||||
id={"input-" + index}
|
id={
|
||||||
|
"input-" +
|
||||||
|
myData.node.template[templateParam]
|
||||||
|
.name
|
||||||
|
}
|
||||||
editNode={true}
|
editNode={true}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
password={
|
password={
|
||||||
|
|
@ -338,7 +343,10 @@ const EditNodeModal = forwardRef(
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
{" "}
|
{" "}
|
||||||
<ToggleShadComponent
|
<ToggleShadComponent
|
||||||
id={"toggle-edit-" + index}
|
id={
|
||||||
|
"toggle-edit-" +
|
||||||
|
myData.node.template[templateParam].name
|
||||||
|
}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
enabled={
|
enabled={
|
||||||
myData.node.template[templateParam]
|
myData.node.template[templateParam]
|
||||||
|
|
@ -351,6 +359,7 @@ const EditNodeModal = forwardRef(
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
|
editNode={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : myData.node?.template[templateParam]
|
) : myData.node?.template[templateParam]
|
||||||
|
|
@ -391,14 +400,20 @@ const EditNodeModal = forwardRef(
|
||||||
myData.node.template[templateParam]
|
myData.node.template[templateParam]
|
||||||
.value ?? "Choose an option"
|
.value ?? "Choose an option"
|
||||||
}
|
}
|
||||||
id={"dropdown-edit-" + index}
|
id={
|
||||||
|
"dropdown-edit-" +
|
||||||
|
myData.node.template[templateParam].name
|
||||||
|
}
|
||||||
></Dropdown>
|
></Dropdown>
|
||||||
</div>
|
</div>
|
||||||
) : myData.node?.template[templateParam]
|
) : myData.node?.template[templateParam]
|
||||||
.type === "int" ? (
|
.type === "int" ? (
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<IntComponent
|
<IntComponent
|
||||||
id={"edit-int-input-" + index}
|
id={
|
||||||
|
"edit-int-input-" +
|
||||||
|
myData.node.template[templateParam].name
|
||||||
|
}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
editNode={true}
|
editNode={true}
|
||||||
value={
|
value={
|
||||||
|
|
@ -493,7 +508,10 @@ const EditNodeModal = forwardRef(
|
||||||
onChange={(value: string | string[]) => {
|
onChange={(value: string | string[]) => {
|
||||||
handleOnNewValue(value, templateParam);
|
handleOnNewValue(value, templateParam);
|
||||||
}}
|
}}
|
||||||
id={"code-area-edit" + index}
|
id={
|
||||||
|
"code-area-edit" +
|
||||||
|
myData.node.template[templateParam].name
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : myData.node?.template[templateParam]
|
) : myData.node?.template[templateParam]
|
||||||
|
|
@ -519,6 +537,7 @@ const EditNodeModal = forwardRef(
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
size="small"
|
size="small"
|
||||||
|
editNode={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import {
|
||||||
API_SUCCESS_ALERT,
|
API_SUCCESS_ALERT,
|
||||||
} from "../../constants/alerts_constants";
|
} from "../../constants/alerts_constants";
|
||||||
import {
|
import {
|
||||||
createApi,
|
CREATE_API_KEY,
|
||||||
insertApi,
|
INSERT_API_KEY,
|
||||||
invalidApi,
|
INVALID_API_KEY,
|
||||||
noApi,
|
NO_API_KEY,
|
||||||
} from "../../constants/constants";
|
} from "../../constants/constants";
|
||||||
import { AuthContext } from "../../contexts/authContext";
|
import { AuthContext } from "../../contexts/authContext";
|
||||||
import { addApiKeyStore } from "../../controllers/API";
|
import { addApiKeyStore } from "../../controllers/API";
|
||||||
|
|
@ -68,8 +68,11 @@ export default function StoreApiKeyModal({
|
||||||
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
|
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
|
||||||
<BaseModal.Header
|
<BaseModal.Header
|
||||||
description={
|
description={
|
||||||
(hasApiKey && !validApiKey ? invalidApi : !hasApiKey ? noApi : "") +
|
(hasApiKey && !validApiKey
|
||||||
insertApi
|
? INVALID_API_KEY
|
||||||
|
: !hasApiKey
|
||||||
|
? NO_API_KEY
|
||||||
|
: "") + INSERT_API_KEY
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="pr-2">API Key</span>
|
<span className="pr-2">API Key</span>
|
||||||
|
|
@ -104,7 +107,7 @@ export default function StoreApiKeyModal({
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end justify-between">
|
<div className="flex items-end justify-between">
|
||||||
<span className="pr-1 text-xs text-muted-foreground">
|
<span className="pr-1 text-xs text-muted-foreground">
|
||||||
{createApi}{" "}
|
{CREATE_API_KEY}{" "}
|
||||||
<a
|
<a
|
||||||
className="text-high-indigo underline"
|
className="text-high-indigo underline"
|
||||||
href="https://langflow.store/"
|
href="https://langflow.store/"
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ function BaseModal({
|
||||||
minWidth = "min-w-[60vw]";
|
minWidth = "min-w-[60vw]";
|
||||||
break;
|
break;
|
||||||
case "large":
|
case "large":
|
||||||
minWidth = "min-w-[80vw]";
|
minWidth = "min-w-[85vw]";
|
||||||
height = "h-[80vh]";
|
height = "h-[80vh]";
|
||||||
break;
|
break;
|
||||||
case "large-thin":
|
case "large-thin":
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import {
|
||||||
} from "../../constants/alerts_constants";
|
} from "../../constants/alerts_constants";
|
||||||
import {
|
import {
|
||||||
CODE_PROMPT_DIALOG_SUBTITLE,
|
CODE_PROMPT_DIALOG_SUBTITLE,
|
||||||
editCodeTitle,
|
EDIT_CODE_TITLE,
|
||||||
} from "../../constants/constants";
|
} from "../../constants/constants";
|
||||||
import { postCustomComponent, postValidateCode } from "../../controllers/API";
|
import { postCustomComponent, postValidateCode } from "../../controllers/API";
|
||||||
import useAlertStore from "../../stores/alertStore";
|
import useAlertStore from "../../stores/alertStore";
|
||||||
|
|
@ -35,11 +35,14 @@ export default function CodeAreaModal({
|
||||||
children,
|
children,
|
||||||
dynamic,
|
dynamic,
|
||||||
readonly = false,
|
readonly = false,
|
||||||
openModal,
|
open: myOpen,
|
||||||
selected,
|
setOpen: mySetOpen,
|
||||||
setOpenModal,
|
|
||||||
}: codeAreaModalPropsType): JSX.Element {
|
}: codeAreaModalPropsType): JSX.Element {
|
||||||
const [code, setCode] = useState(value);
|
const [code, setCode] = useState(value);
|
||||||
|
const [open, setOpen] =
|
||||||
|
mySetOpen !== undefined && myOpen !== undefined
|
||||||
|
? [myOpen, mySetOpen]
|
||||||
|
: useState(false);
|
||||||
const dark = useDarkStore((state) => state.dark);
|
const dark = useDarkStore((state) => state.dark);
|
||||||
const unselectAll = useFlowStore((state) => state.unselectAll);
|
const unselectAll = useFlowStore((state) => state.unselectAll);
|
||||||
const [height, setHeight] = useState<string | null>(null);
|
const [height, setHeight] = useState<string | null>(null);
|
||||||
|
|
@ -48,7 +51,6 @@ export default function CodeAreaModal({
|
||||||
const [error, setError] = useState<{
|
const [error, setError] = useState<{
|
||||||
detail: { error: string | undefined; traceback: string | undefined };
|
detail: { error: string | undefined; traceback: string | undefined };
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const nodes = useFlowStore((state) => state.nodes);
|
const nodes = useFlowStore((state) => state.nodes);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -59,10 +61,6 @@ export default function CodeAreaModal({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (openModal) setOpen(true);
|
|
||||||
}, [openModal]);
|
|
||||||
|
|
||||||
function processNonDynamicField() {
|
function processNonDynamicField() {
|
||||||
postValidateCode(code)
|
postValidateCode(code)
|
||||||
.then((apiReturn) => {
|
.then((apiReturn) => {
|
||||||
|
|
@ -154,7 +152,7 @@ export default function CodeAreaModal({
|
||||||
<BaseModal open={open} setOpen={setOpen}>
|
<BaseModal open={open} setOpen={setOpen}>
|
||||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||||
<BaseModal.Header description={CODE_PROMPT_DIALOG_SUBTITLE}>
|
<BaseModal.Header description={CODE_PROMPT_DIALOG_SUBTITLE}>
|
||||||
<span className="pr-2"> {editCodeTitle} </span>
|
<span className="pr-2"> {EDIT_CODE_TITLE} </span>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="prompts"
|
name="prompts"
|
||||||
className="h-6 w-6 pl-1 text-primary "
|
className="h-6 w-6 pl-1 text-primary "
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import { Button } from "../../components/ui/button";
|
||||||
import { Checkbox } from "../../components/ui/checkbox";
|
import { Checkbox } from "../../components/ui/checkbox";
|
||||||
import { API_WARNING_NOTICE_ALERT } from "../../constants/alerts_constants";
|
import { API_WARNING_NOTICE_ALERT } from "../../constants/alerts_constants";
|
||||||
import {
|
import {
|
||||||
|
ALERT_SAVE_WITH_API,
|
||||||
EXPORT_DIALOG_SUBTITLE,
|
EXPORT_DIALOG_SUBTITLE,
|
||||||
alertSaveWApi,
|
SAVE_WITH_API_CHECKBOX,
|
||||||
saveWApiCheckbox,
|
|
||||||
} from "../../constants/constants";
|
} from "../../constants/constants";
|
||||||
import useAlertStore from "../../stores/alertStore";
|
import useAlertStore from "../../stores/alertStore";
|
||||||
import { useDarkStore } from "../../stores/darkStore";
|
import { useDarkStore } from "../../stores/darkStore";
|
||||||
|
|
@ -56,10 +56,12 @@ const ExportModal = forwardRef(
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="terms" className="export-modal-save-api text-sm ">
|
<label htmlFor="terms" className="export-modal-save-api text-sm ">
|
||||||
{saveWApiCheckbox}
|
{SAVE_WITH_API_CHECKBOX}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<span className=" text-xs text-destructive ">{alertSaveWApi}</span>
|
<span className=" text-xs text-destructive ">
|
||||||
|
{ALERT_SAVE_WITH_API}
|
||||||
|
</span>
|
||||||
</BaseModal.Content>
|
</BaseModal.Content>
|
||||||
|
|
||||||
<BaseModal.Footer>
|
<BaseModal.Footer>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { useEffect } from "react";
|
||||||
import IconComponent from "../../../components/genericIconComponent";
|
import IconComponent from "../../../components/genericIconComponent";
|
||||||
import { Textarea } from "../../../components/ui/textarea";
|
import { Textarea } from "../../../components/ui/textarea";
|
||||||
import {
|
import {
|
||||||
chatInputPlaceholder,
|
CHAT_INPUT_PLACEHOLDER,
|
||||||
chatInputPlaceholderSend,
|
CHAT_INPUT_PLACEHOLDER_SEND,
|
||||||
} from "../../../constants/constants";
|
} from "../../../constants/constants";
|
||||||
import { chatInputType } from "../../../types/components";
|
import { chatInputType } from "../../../types/components";
|
||||||
import { classNames } from "../../../utils/utils";
|
import { classNames } from "../../../utils/utils";
|
||||||
|
|
@ -55,7 +55,7 @@ export default function ChatInput({
|
||||||
? "Thinking..."
|
? "Thinking..."
|
||||||
: typeof chatValue === "object" &&
|
: typeof chatValue === "object" &&
|
||||||
Object.keys(chatValue)?.length === 0
|
Object.keys(chatValue)?.length === 0
|
||||||
? chatInputPlaceholder
|
? CHAT_INPUT_PLACEHOLDER
|
||||||
: chatValue
|
: chatValue
|
||||||
}
|
}
|
||||||
onChange={(event): void => {
|
onChange={(event): void => {
|
||||||
|
|
@ -70,7 +70,9 @@ export default function ChatInput({
|
||||||
|
|
||||||
"form-modal-lockchat"
|
"form-modal-lockchat"
|
||||||
)}
|
)}
|
||||||
placeholder={noInput ? chatInputPlaceholder : chatInputPlaceholderSend}
|
placeholder={
|
||||||
|
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="form-modal-send-icon-position">
|
<div className="form-modal-send-icon-position">
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export function CodeBlock({ language, value }: Props): JSX.Element {
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button className="code-block-modal-button" onClick={copyToClipboard}>
|
<button className="code-block-modal-button" onClick={copyToClipboard}>
|
||||||
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
|
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
|
||||||
{isCopied ? "Copied!" : "Copy code"}
|
{isCopied ? "Copied!" : "Copy Code"}
|
||||||
</button>
|
</button>
|
||||||
<button className="code-block-modal-button" onClick={downloadAsFile}>
|
<button className="code-block-modal-button" onClick={downloadAsFile}>
|
||||||
<IconDownload size={18} />
|
<IconDownload size={18} />
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,10 @@ import {
|
||||||
MSG_ERROR_ALERT,
|
MSG_ERROR_ALERT,
|
||||||
} from "../../constants/alerts_constants";
|
} from "../../constants/alerts_constants";
|
||||||
import {
|
import {
|
||||||
|
CHAT_FIRST_INITIAL_TEXT,
|
||||||
CHAT_FORM_DIALOG_SUBTITLE,
|
CHAT_FORM_DIALOG_SUBTITLE,
|
||||||
chatFirstInitialText,
|
CHAT_SECOND_INITIAL_TEXT,
|
||||||
chatSecondInitialText,
|
LANGFLOW_CHAT_TITLE,
|
||||||
langflowChatTitle,
|
|
||||||
} from "../../constants/constants";
|
} from "../../constants/constants";
|
||||||
import { AuthContext } from "../../contexts/authContext";
|
import { AuthContext } from "../../contexts/authContext";
|
||||||
import { getBuildStatus } from "../../controllers/API";
|
import { getBuildStatus } from "../../controllers/API";
|
||||||
|
|
@ -594,20 +594,20 @@ export default function FormModal({
|
||||||
<span>
|
<span>
|
||||||
👋{" "}
|
👋{" "}
|
||||||
<span className="langflow-chat-span">
|
<span className="langflow-chat-span">
|
||||||
{langflowChatTitle}
|
{LANGFLOW_CHAT_TITLE}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<div className="langflow-chat-desc">
|
<div className="langflow-chat-desc">
|
||||||
<span className="langflow-chat-desc-span">
|
<span className="langflow-chat-desc-span">
|
||||||
{chatFirstInitialText}{" "}
|
{CHAT_FIRST_INITIAL_TEXT}{" "}
|
||||||
<span>
|
<span>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="MessageSquare"
|
name="MessageSquare"
|
||||||
className="mx-1 inline h-5 w-5 animate-bounce "
|
className="mx-1 inline h-5 w-5 animate-bounce "
|
||||||
/>
|
/>
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
{chatSecondInitialText}
|
{CHAT_SECOND_INITIAL_TEXT}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ import {
|
||||||
TEMP_NOTICE_ALERT,
|
TEMP_NOTICE_ALERT,
|
||||||
} from "../../constants/alerts_constants";
|
} from "../../constants/alerts_constants";
|
||||||
import {
|
import {
|
||||||
|
EDIT_TEXT_PLACEHOLDER,
|
||||||
INVALID_CHARACTERS,
|
INVALID_CHARACTERS,
|
||||||
MAX_WORDS_HIGHLIGHT,
|
MAX_WORDS_HIGHLIGHT,
|
||||||
PROMPT_DIALOG_SUBTITLE,
|
PROMPT_DIALOG_SUBTITLE,
|
||||||
TEXT_DIALOG_SUBTITLE,
|
TEXT_DIALOG_SUBTITLE,
|
||||||
editTextPlaceholder,
|
|
||||||
regexHighlight,
|
regexHighlight,
|
||||||
} from "../../constants/constants";
|
} from "../../constants/constants";
|
||||||
import { TypeModal } from "../../constants/enums";
|
import { TypeModal } from "../../constants/enums";
|
||||||
|
|
@ -97,13 +97,23 @@ export default function GenericModal({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInputValue(value);
|
setInputValue(value);
|
||||||
}, [value, modalOpen]);
|
}, [value, modalOpen]);
|
||||||
|
|
||||||
const coloredContent = (inputValue || "")
|
const coloredContent = (inputValue || "")
|
||||||
.replace(/</g, "<")
|
.replace(/</g, "<")
|
||||||
.replace(/>/g, ">")
|
.replace(/>/g, ">")
|
||||||
.replace(regexHighlight, varHighlightHTML({ name: "$1" }))
|
.replace(regexHighlight, (match, p1, p2) => {
|
||||||
.replace(/\n/g, "<br />");
|
// Decide which group was matched. If p1 is not undefined, do nothing
|
||||||
|
// we don't want to change the text. If p2 is not undefined, then we
|
||||||
|
// have a variable, so we should highlight it.
|
||||||
|
// ! This will not work with multiline or indented json yet
|
||||||
|
if (p1 !== undefined) {
|
||||||
|
return match;
|
||||||
|
} else if (p2 !== undefined) {
|
||||||
|
return varHighlightHTML({ name: p2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return match;
|
||||||
|
})
|
||||||
|
.replace(/\n/g, "<br />");
|
||||||
function getClassByNumberLength(): string {
|
function getClassByNumberLength(): string {
|
||||||
let sumOfCaracteres: number = 0;
|
let sumOfCaracteres: number = 0;
|
||||||
wordsHighlight.forEach((element) => {
|
wordsHighlight.forEach((element) => {
|
||||||
|
|
@ -159,7 +169,7 @@ export default function GenericModal({
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
return setErrorData({
|
return setErrorData({
|
||||||
title: PROMPT_ERROR_ALERT,
|
title: PROMPT_ERROR_ALERT,
|
||||||
list: [error.toString()],
|
list: [error.response.data.detail ?? ""],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +227,7 @@ export default function GenericModal({
|
||||||
setInputValue(event.target.value);
|
setInputValue(event.target.value);
|
||||||
checkVariables(event.target.value);
|
checkVariables(event.target.value);
|
||||||
}}
|
}}
|
||||||
placeholder={editTextPlaceholder}
|
placeholder={EDIT_TEXT_PLACEHOLDER}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
handleKeyDown(e, inputValue, "");
|
handleKeyDown(e, inputValue, "");
|
||||||
}}
|
}}
|
||||||
|
|
@ -239,7 +249,7 @@ export default function GenericModal({
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setInputValue(event.target.value);
|
setInputValue(event.target.value);
|
||||||
}}
|
}}
|
||||||
placeholder={editTextPlaceholder}
|
placeholder={EDIT_TEXT_PLACEHOLDER}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
handleKeyDown(e, value, "");
|
handleKeyDown(e, value, "");
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export default function ShareModal({
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
const [internalOpen, internalSetOpen] = useState(children ? false : true);
|
const [internalOpen, internalSetOpen] = useState(children ? false : true);
|
||||||
const [openConfirmationModal, setOpenConfirmationModal] = useState(false);
|
const [openConfirmationModal, setOpenConfirmationModal] = useState(false);
|
||||||
const nameComponent = is_component ? "component" : "flow";
|
const nameComponent = is_component ? "component" : "workflow";
|
||||||
|
|
||||||
const [tags, setTags] = useState<{ id: string; name: string }[]>([]);
|
const [tags, setTags] = useState<{ id: string; name: string }[]>([]);
|
||||||
const [loadingTags, setLoadingTags] = useState<boolean>(false);
|
const [loadingTags, setLoadingTags] = useState<boolean>(false);
|
||||||
|
|
@ -179,7 +179,7 @@ export default function ShareModal({
|
||||||
</span>
|
</span>
|
||||||
<br></br>
|
<br></br>
|
||||||
<span className=" text-xs text-destructive ">
|
<span className=" text-xs text-destructive ">
|
||||||
Warning: This action cannot be undone.
|
Note: This action is irreversible.
|
||||||
</span>
|
</span>
|
||||||
</ConfirmationModal.Content>
|
</ConfirmationModal.Content>
|
||||||
</ConfirmationModal>
|
</ConfirmationModal>
|
||||||
|
|
@ -204,7 +204,7 @@ export default function ShareModal({
|
||||||
{children ? children : <></>}
|
{children ? children : <></>}
|
||||||
</BaseModal.Trigger>
|
</BaseModal.Trigger>
|
||||||
<BaseModal.Header
|
<BaseModal.Header
|
||||||
description={`Share your ${nameComponent} to the Langflow Store.`}
|
description={`Publish ${is_component ? "your component" : "workflow"} to the Langflow Store.`}
|
||||||
>
|
>
|
||||||
<span className="pr-2">Share</span>
|
<span className="pr-2">Share</span>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
|
|
@ -235,12 +235,11 @@ export default function ShareModal({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="public" className="export-modal-save-api text-sm ">
|
<label htmlFor="public" className="export-modal-save-api text-sm ">
|
||||||
Make {nameComponent} public
|
Set {nameComponent} status to public
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<span className=" text-xs text-destructive ">
|
<span className=" text-xs text-destructive ">
|
||||||
<b>Warning:</b> API keys in designated fields are removed when
|
<b>Attention:</b> API keys in specified fields are automatically removed upon sharing.
|
||||||
sharing.
|
|
||||||
</span>
|
</span>
|
||||||
</BaseModal.Content>
|
</BaseModal.Content>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import {
|
||||||
getNodeId,
|
getNodeId,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
reconnectEdges,
|
reconnectEdges,
|
||||||
|
scapeJSONParse,
|
||||||
validateSelection,
|
validateSelection,
|
||||||
} from "../../../../utils/reactflowUtils";
|
} from "../../../../utils/reactflowUtils";
|
||||||
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
|
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
|
||||||
|
|
@ -320,6 +321,8 @@ export default function Page({
|
||||||
(oldEdge: Edge, newConnection: Connection) => {
|
(oldEdge: Edge, newConnection: Connection) => {
|
||||||
if (isValidConnection(newConnection, nodes, edges)) {
|
if (isValidConnection(newConnection, nodes, edges)) {
|
||||||
edgeUpdateSuccessful.current = true;
|
edgeUpdateSuccessful.current = true;
|
||||||
|
oldEdge.data.targetHandle = scapeJSONParse(newConnection.targetHandle!);
|
||||||
|
oldEdge.data.sourceHandle = scapeJSONParse(newConnection.sourceHandle!);
|
||||||
setEdges((els) => updateEdge(oldEdge, newConnection, els));
|
setEdges((els) => updateEdge(oldEdge, newConnection, els));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -315,25 +315,6 @@ export default function NodeToolbarComponent({
|
||||||
}}
|
}}
|
||||||
data-testid="code-button-modal"
|
data-testid="code-button-modal"
|
||||||
>
|
>
|
||||||
<div className="hidden">
|
|
||||||
<CodeAreaComponent
|
|
||||||
openModal={openModal}
|
|
||||||
setOpenModal={setOpenModal}
|
|
||||||
readonly={
|
|
||||||
data.node?.flow && data.node.template[name].dynamic
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
dynamic={data.node?.template[name].dynamic ?? false}
|
|
||||||
setNodeClass={handleNodeClass}
|
|
||||||
nodeClass={data.node}
|
|
||||||
disabled={false}
|
|
||||||
value={data.node?.template[name].value ?? ""}
|
|
||||||
onChange={handleOnNewValue}
|
|
||||||
id={"code-input-node-toolbar-" + name}
|
|
||||||
selected={selected}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<IconComponent name="TerminalSquare" className="h-4 w-4" />
|
<IconComponent name="TerminalSquare" className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
|
|
@ -382,7 +363,7 @@ export default function NodeToolbarComponent({
|
||||||
</button>
|
</button>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
|
|
||||||
<ShadTooltip content="Pin" side="top">
|
<ShadTooltip content="Freeze" side="top">
|
||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
|
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
|
||||||
|
|
@ -402,10 +383,11 @@ export default function NodeToolbarComponent({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="Pin"
|
name="Snowflake"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-4 w-4 transition-all",
|
"h-4 w-4 transition-all",
|
||||||
pinned ? "animate-wiggle fill-current" : ""
|
// TODO UPDATE THIS COLOR TO BE A VARIABLE
|
||||||
|
pinned ? "animate-wiggle text-ice" : ""
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -499,7 +481,7 @@ export default function NodeToolbarComponent({
|
||||||
value={"Share"}
|
value={"Share"}
|
||||||
disabled={!hasApiKey || !validApiKey}
|
disabled={!hasApiKey || !validApiKey}
|
||||||
>
|
>
|
||||||
<div className="flex" data-testid="save-button-modal">
|
<div className="flex" data-testid="share-button-modal">
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="Share3"
|
name="Share3"
|
||||||
className="relative top-0.5 -m-1 mr-1 h-6 w-6"
|
className="relative top-0.5 -m-1 mr-1 h-6 w-6"
|
||||||
|
|
@ -633,6 +615,26 @@ export default function NodeToolbarComponent({
|
||||||
is_component={true}
|
is_component={true}
|
||||||
component={flowComponent!}
|
component={flowComponent!}
|
||||||
/>
|
/>
|
||||||
|
{hasCode && (
|
||||||
|
<div className="hidden">
|
||||||
|
<CodeAreaComponent
|
||||||
|
open={openModal}
|
||||||
|
setOpen={setOpenModal}
|
||||||
|
readonly={
|
||||||
|
data.node?.flow && data.node.template[name].dynamic
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
dynamic={data.node?.template[name].dynamic ?? false}
|
||||||
|
setNodeClass={handleNodeClass}
|
||||||
|
nodeClass={data.node}
|
||||||
|
disabled={false}
|
||||||
|
value={data.node?.template[name].value ?? ""}
|
||||||
|
onChange={handleOnNewValue}
|
||||||
|
id={"code-input-node-toolbar-" + name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export default function ComponentsComponent({
|
||||||
const flows = useFlowsManagerStore((state) => state.flows);
|
const flows = useFlowsManagerStore((state) => state.flows);
|
||||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(20);
|
||||||
const [pageIndex, setPageIndex] = useState(1);
|
const [pageIndex, setPageIndex] = useState(1);
|
||||||
const [loadingScreen, setLoadingScreen] = useState(true);
|
const [loadingScreen, setLoadingScreen] = useState(true);
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@ export default function ComponentsComponent({
|
||||||
|
|
||||||
function resetFilter() {
|
function resetFilter() {
|
||||||
setPageIndex(1);
|
setPageIndex(1);
|
||||||
setPageSize(10);
|
setPageSize(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import SidebarNav from "../../components/sidebarComponent";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import { CONSOLE_ERROR_MSG } from "../../constants/alerts_constants";
|
import { CONSOLE_ERROR_MSG } from "../../constants/alerts_constants";
|
||||||
import {
|
import {
|
||||||
|
MY_COLLECTION_DESC,
|
||||||
USER_PROJECTS_HEADER,
|
USER_PROJECTS_HEADER,
|
||||||
myCollectionDesc,
|
|
||||||
} from "../../constants/constants";
|
} from "../../constants/constants";
|
||||||
import useAlertStore from "../../stores/alertStore";
|
import useAlertStore from "../../stores/alertStore";
|
||||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||||
|
|
@ -75,7 +75,7 @@ export default function HomePage(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout
|
||||||
title={USER_PROJECTS_HEADER}
|
title={USER_PROJECTS_HEADER}
|
||||||
description={myCollectionDesc}
|
description={MY_COLLECTION_DESC}
|
||||||
button={
|
button={
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import {
|
||||||
INVALID_API_ERROR_ALERT,
|
INVALID_API_ERROR_ALERT,
|
||||||
NOAPI_ERROR_ALERT,
|
NOAPI_ERROR_ALERT,
|
||||||
} from "../../constants/alerts_constants";
|
} from "../../constants/alerts_constants";
|
||||||
import { storeDesc, storeTitle } from "../../constants/constants";
|
import { STORE_DESC, STORE_TITLE } from "../../constants/constants";
|
||||||
import { AuthContext } from "../../contexts/authContext";
|
import { AuthContext } from "../../contexts/authContext";
|
||||||
import { getStoreComponents, getStoreTags } from "../../controllers/API";
|
import { getStoreComponents, getStoreTags } from "../../controllers/API";
|
||||||
import StoreApiKeyModal from "../../modals/StoreApiKeyModal";
|
import StoreApiKeyModal from "../../modals/StoreApiKeyModal";
|
||||||
|
|
@ -174,8 +174,8 @@ export default function StorePage(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<PageLayout
|
<PageLayout
|
||||||
betaIcon
|
betaIcon
|
||||||
title={storeTitle}
|
title={STORE_TITLE}
|
||||||
description={storeDesc}
|
description={STORE_DESC}
|
||||||
button={
|
button={
|
||||||
<>
|
<>
|
||||||
{StoreApiKeyModal && (
|
{StoreApiKeyModal && (
|
||||||
|
|
|
||||||
|
|
@ -187,10 +187,12 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
typeof change === "function"
|
typeof change === "function"
|
||||||
? change(get().nodes.find((node) => node.id === id)!)
|
? change(get().nodes.find((node) => node.id === id)!)
|
||||||
: change;
|
: change;
|
||||||
|
|
||||||
get().setNodes((oldNodes) =>
|
get().setNodes((oldNodes) =>
|
||||||
oldNodes.map((node) => {
|
oldNodes.map((node) => {
|
||||||
if (node.id === id) {
|
if (node.id === id) {
|
||||||
|
if((node.data as NodeDataType).node?.pinned){
|
||||||
|
(newChange.data as NodeDataType).node!.pinned = false;
|
||||||
|
}
|
||||||
return newChange;
|
return newChange;
|
||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
|
|
@ -223,12 +225,10 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
selection.nodes.some((node) => node.data.type === "ChatInput") &&
|
selection.nodes.some((node) => node.data.type === "ChatInput") &&
|
||||||
checkChatInput(get().nodes)
|
checkChatInput(get().nodes)
|
||||||
) {
|
) {
|
||||||
useAlertStore
|
useAlertStore.getState().setErrorData({
|
||||||
.getState()
|
title: "Error pasting components",
|
||||||
.setErrorData({
|
list: ["You can only have one ChatInput component in the flow"],
|
||||||
title: "Error pasting components",
|
});
|
||||||
list: ["You can only have one ChatInput component in the flow"],
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let minimumX = Infinity;
|
let minimumX = Infinity;
|
||||||
|
|
@ -418,10 +418,12 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
buildFlow: async ({
|
buildFlow: async ({
|
||||||
nodeId,
|
startNodeId,
|
||||||
|
stopNodeId,
|
||||||
input_value,
|
input_value,
|
||||||
}: {
|
}: {
|
||||||
nodeId?: string;
|
startNodeId?: string;
|
||||||
|
stopNodeId?: string;
|
||||||
input_value?: string;
|
input_value?: string;
|
||||||
}) => {
|
}) => {
|
||||||
get().setIsBuilding(true);
|
get().setIsBuilding(true);
|
||||||
|
|
@ -446,25 +448,52 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
function handleBuildUpdate(
|
function handleBuildUpdate(
|
||||||
vertexBuildData: VertexBuildTypeAPI,
|
vertexBuildData: VertexBuildTypeAPI,
|
||||||
status: BuildStatus,
|
status: BuildStatus,
|
||||||
buildId: string
|
runId: string
|
||||||
) {
|
) {
|
||||||
if (vertexBuildData && vertexBuildData.inactive_vertices) {
|
if (vertexBuildData && vertexBuildData.inactive_vertices) {
|
||||||
get().removeFromVerticesBuild(vertexBuildData.inactive_vertices);
|
get().removeFromVerticesBuild(vertexBuildData.inactive_vertices);
|
||||||
}
|
}
|
||||||
|
if (vertexBuildData.next_vertices_ids) {
|
||||||
|
// next_vertices_ids is a list of vertices that are going to be built next
|
||||||
|
// verticesLayers is a list of list of vertices ids, where each list is a layer of vertices
|
||||||
|
// we want to add a new layer (next_vertices_ids) to the list of layers (verticesLayers)
|
||||||
|
// and the values of next_vertices_ids to the list of vertices ids (verticesIds)
|
||||||
|
const newLayers = [
|
||||||
|
...get().verticesBuild!.verticesLayers,
|
||||||
|
vertexBuildData.next_vertices_ids,
|
||||||
|
];
|
||||||
|
const newIds = [
|
||||||
|
...get().verticesBuild!.verticesIds,
|
||||||
|
...vertexBuildData.next_vertices_ids,
|
||||||
|
];
|
||||||
|
get().updateVerticesBuild({
|
||||||
|
verticesIds: newIds,
|
||||||
|
verticesLayers: newLayers,
|
||||||
|
runId: runId,
|
||||||
|
});
|
||||||
|
get().updateBuildStatus(
|
||||||
|
vertexBuildData.next_vertices_ids,
|
||||||
|
BuildStatus.TO_BUILD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get().addDataToFlowPool(
|
get().addDataToFlowPool(
|
||||||
{ ...vertexBuildData, buildId },
|
{ ...vertexBuildData, buildId: runId },
|
||||||
vertexBuildData.id
|
vertexBuildData.id
|
||||||
);
|
);
|
||||||
|
|
||||||
useFlowStore.getState().updateBuildStatus([vertexBuildData.id], status);
|
useFlowStore.getState().updateBuildStatus([vertexBuildData.id], status);
|
||||||
}
|
}
|
||||||
await buildVertices({
|
await buildVertices({
|
||||||
input_value,
|
input_value,
|
||||||
flowId: currentFlow!.id,
|
flowId: currentFlow!.id,
|
||||||
nodeId,
|
startNodeId,
|
||||||
|
stopNodeId,
|
||||||
onGetOrderSuccess: () => {
|
onGetOrderSuccess: () => {
|
||||||
setNoticeData({ title: "Running components" });
|
setNoticeData({ title: "Running components" });
|
||||||
},
|
},
|
||||||
onBuildComplete: () => {
|
onBuildComplete: () => {
|
||||||
|
const nodeId = startNodeId || stopNodeId;
|
||||||
if (nodeId) {
|
if (nodeId) {
|
||||||
setSuccessData({
|
setSuccessData({
|
||||||
title: `${
|
title: `${
|
||||||
|
|
@ -481,12 +510,14 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
onBuildError: (title, list, idList) => {
|
onBuildError: (title, list, idList) => {
|
||||||
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILT);
|
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILT);
|
||||||
setErrorData({ list, title });
|
setErrorData({ list, title });
|
||||||
|
get().setIsBuilding(false);
|
||||||
},
|
},
|
||||||
onBuildStart: (idList) => {
|
onBuildStart: (idList) => {
|
||||||
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING);
|
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING);
|
||||||
},
|
},
|
||||||
validateNodes: validateSubgraph,
|
validateNodes: validateSubgraph,
|
||||||
});
|
});
|
||||||
|
get().setIsBuilding(false);
|
||||||
get().revertBuiltStatusFromBuilding();
|
get().revertBuiltStatusFromBuilding();
|
||||||
},
|
},
|
||||||
getFlow: () => {
|
getFlow: () => {
|
||||||
|
|
@ -499,11 +530,11 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
updateVerticesBuild: (
|
updateVerticesBuild: (
|
||||||
vertices: {
|
vertices: {
|
||||||
verticesIds: string[];
|
verticesIds: string[];
|
||||||
verticesOrder: string[][];
|
|
||||||
verticesLayers: string[][];
|
verticesLayers: string[][];
|
||||||
runId: string;
|
runId: string;
|
||||||
} | null
|
} | null
|
||||||
) => {
|
) => {
|
||||||
|
console.log("updateVerticesBuild", vertices);
|
||||||
set({ verticesBuild: vertices });
|
set({ verticesBuild: vertices });
|
||||||
},
|
},
|
||||||
verticesBuild: null,
|
verticesBuild: null,
|
||||||
|
|
@ -520,17 +551,25 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
updateBuildStatus: (nodeIdList: string[], status: BuildStatus) => {
|
updateBuildStatus: (nodeIdList: string[], status: BuildStatus) => {
|
||||||
|
console.log("updateBuildStatus", nodeIdList, status);
|
||||||
const newFlowBuildStatus = { ...get().flowBuildStatus };
|
const newFlowBuildStatus = { ...get().flowBuildStatus };
|
||||||
nodeIdList.forEach((id) => {
|
nodeIdList.forEach((id) => {
|
||||||
newFlowBuildStatus[id] = status;
|
newFlowBuildStatus[id] = {
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
if (status == BuildStatus.BUILT) {
|
||||||
|
const timestamp_string = new Date(Date.now()).toLocaleString();
|
||||||
|
newFlowBuildStatus[id].timestamp = timestamp_string;
|
||||||
|
}
|
||||||
|
console.log("updateBuildStatus", newFlowBuildStatus);
|
||||||
});
|
});
|
||||||
set({ flowBuildStatus: newFlowBuildStatus });
|
set({ flowBuildStatus: newFlowBuildStatus });
|
||||||
},
|
},
|
||||||
revertBuiltStatusFromBuilding: () => {
|
revertBuiltStatusFromBuilding: () => {
|
||||||
const newFlowBuildStatus = { ...get().flowBuildStatus };
|
const newFlowBuildStatus = { ...get().flowBuildStatus };
|
||||||
Object.keys(newFlowBuildStatus).forEach((id) => {
|
Object.keys(newFlowBuildStatus).forEach((id) => {
|
||||||
if (newFlowBuildStatus[id] === BuildStatus.BUILDING) {
|
if (newFlowBuildStatus[id].status === BuildStatus.BUILDING) {
|
||||||
newFlowBuildStatus[id] = BuildStatus.BUILT;
|
newFlowBuildStatus[id].status = BuildStatus.BUILT;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -845,13 +845,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.api-modal-tabs {
|
.api-modal-tabs {
|
||||||
@apply flex h-full max-w-full flex-col overflow-hidden rounded-md border bg-muted text-center sm:w-[75vw] md:w-[75vw] lg:w-[75vw] xl:w-[76vw] 2xl:w-full;
|
@apply flex h-full flex-col overflow-hidden rounded-md border bg-muted text-center;
|
||||||
}
|
}
|
||||||
.api-modal-tablist-div {
|
.api-modal-tablist-div {
|
||||||
@apply flex items-center justify-between px-2 py-2;
|
@apply flex items-center justify-between px-2 py-2;
|
||||||
}
|
}
|
||||||
.api-modal-tabs-content {
|
.api-modal-tabs-content {
|
||||||
@apply -mt-1 h-full w-full px-4 pb-4;
|
@apply -mt-1 h-full w-full px-4 pb-4;
|
||||||
}
|
}
|
||||||
.api-modal-accordion-display {
|
.api-modal-accordion-display {
|
||||||
@apply mt-2 flex h-full w-full;
|
@apply mt-2 flex h-full w-full;
|
||||||
|
|
@ -902,7 +902,7 @@
|
||||||
@apply flex-max-width px-2 py-6 pl-4 pr-9;
|
@apply flex-max-width px-2 py-6 pl-4 pr-9;
|
||||||
}
|
}
|
||||||
.form-modal-chatbot-icon {
|
.form-modal-chatbot-icon {
|
||||||
@apply mb-3 ml-3 mr-6 mt-1;
|
@apply flex flex-col mb-3 ml-3 mr-6 mt-1;
|
||||||
}
|
}
|
||||||
.form-modal-chat-image {
|
.form-modal-chat-image {
|
||||||
@apply flex flex-col items-center gap-1;
|
@apply flex flex-col items-center gap-1;
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
|
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
|
||||||
--round-btn-shadow: #00000063;
|
--round-btn-shadow: #00000063;
|
||||||
|
--ice: #31a3cc;
|
||||||
|
|
||||||
--error-background: #fef2f2;
|
--error-background: #fef2f2;
|
||||||
--error-foreground: #991b1b;
|
--error-foreground: #991b1b;
|
||||||
|
|
@ -67,6 +68,7 @@
|
||||||
.dark {
|
.dark {
|
||||||
--background: 224 35% 7.5%; /* hsl(224 40% 10%) */
|
--background: 224 35% 7.5%; /* hsl(224 40% 10%) */
|
||||||
--foreground: 213 31% 80%; /* hsl(213 31% 91%) */
|
--foreground: 213 31% 80%; /* hsl(213 31% 91%) */
|
||||||
|
--ice: #60A5FA;
|
||||||
|
|
||||||
--muted: 223 27% 11%; /* hsl(223 27% 11%) */
|
--muted: 223 27% 11%; /* hsl(223 27% 11%) */
|
||||||
--muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */
|
--muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ export type TemplateVariableType = {
|
||||||
input_types?: Array<string>;
|
input_types?: Array<string>;
|
||||||
display_name?: string;
|
display_name?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
refresh?: boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
export type sendAllProps = {
|
export type sendAllProps = {
|
||||||
|
|
@ -134,13 +135,15 @@ export type Component = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VerticesOrderTypeAPI = {
|
export type VerticesOrderTypeAPI = {
|
||||||
ids: Array<Array<string>>;
|
ids: Array<string>;
|
||||||
run_id: string;
|
run_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VertexBuildTypeAPI = {
|
export type VertexBuildTypeAPI = {
|
||||||
id: string;
|
id: string;
|
||||||
|
next_vertices_ids: Array<string>;
|
||||||
inactive_vertices: Array<string> | null;
|
inactive_vertices: Array<string> | null;
|
||||||
|
run_id: string;
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
params: string;
|
params: string;
|
||||||
data: VertexDataTypeAPI;
|
data: VertexDataTypeAPI;
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,10 @@ export type ToggleComponentType = {
|
||||||
disabled: boolean | undefined;
|
disabled: boolean | undefined;
|
||||||
size: "small" | "medium" | "large";
|
size: "small" | "medium" | "large";
|
||||||
id?: string;
|
id?: string;
|
||||||
|
editNode?: boolean;
|
||||||
};
|
};
|
||||||
export type DropDownComponentType = {
|
export type DropDownComponentType = {
|
||||||
|
isLoading?: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
options: string[];
|
options: string[];
|
||||||
onSelect: (value: string) => void;
|
onSelect: (value: string) => void;
|
||||||
|
|
@ -113,8 +115,8 @@ export type CodeAreaComponentType = {
|
||||||
dynamic?: boolean;
|
dynamic?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
openModal?: boolean;
|
open?: boolean;
|
||||||
selected?: boolean;
|
setOpen?: (open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FileComponentType = {
|
export type FileComponentType = {
|
||||||
|
|
@ -521,8 +523,8 @@ export type codeAreaModalPropsType = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
dynamic?: boolean;
|
dynamic?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
openModal?: boolean;
|
open?: boolean;
|
||||||
selected?: boolean;
|
setOpen?: (open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type chatMessagePropsType = {
|
export type chatMessagePropsType = {
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ export type FlowPoolType = {
|
||||||
|
|
||||||
export type FlowStoreType = {
|
export type FlowStoreType = {
|
||||||
flowPool: FlowPoolType;
|
flowPool: FlowPoolType;
|
||||||
inputs: Array<{ type: string; id: string }>;
|
inputs: Array<{ type: string; id: string; displayName: string }>;
|
||||||
outputs: Array<{ type: string; id: string }>;
|
outputs: Array<{ type: string; id: string; displayName: string }>;
|
||||||
hasIO: boolean;
|
hasIO: boolean;
|
||||||
setFlowPool: (flowPool: FlowPoolType) => void;
|
setFlowPool: (flowPool: FlowPoolType) => void;
|
||||||
addDataToFlowPool: (data: FlowPoolObjectType, nodeId: string) => void;
|
addDataToFlowPool: (data: FlowPoolObjectType, nodeId: string) => void;
|
||||||
|
|
@ -90,10 +90,13 @@ export type FlowStoreType = {
|
||||||
onConnect: (connection: Connection) => void;
|
onConnect: (connection: Connection) => void;
|
||||||
unselectAll: () => void;
|
unselectAll: () => void;
|
||||||
buildFlow: ({
|
buildFlow: ({
|
||||||
nodeId,
|
startNodeId,
|
||||||
|
stopNodeId,
|
||||||
input_value,
|
input_value,
|
||||||
}: {
|
}: {
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
|
startNodeId?: string;
|
||||||
|
stopNodeId?: string;
|
||||||
input_value?: string;
|
input_value?: string;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
getFlow: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
|
getFlow: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
|
||||||
|
|
@ -101,7 +104,6 @@ export type FlowStoreType = {
|
||||||
vertices: {
|
vertices: {
|
||||||
verticesIds: string[];
|
verticesIds: string[];
|
||||||
verticesLayers: string[][];
|
verticesLayers: string[][];
|
||||||
verticesOrder: string[][];
|
|
||||||
runId: string;
|
runId: string;
|
||||||
} | null
|
} | null
|
||||||
) => void;
|
) => void;
|
||||||
|
|
@ -109,12 +111,13 @@ export type FlowStoreType = {
|
||||||
verticesBuild: {
|
verticesBuild: {
|
||||||
verticesIds: string[];
|
verticesIds: string[];
|
||||||
verticesLayers: string[][];
|
verticesLayers: string[][];
|
||||||
verticesOrder: string[][];
|
|
||||||
runId: string;
|
runId: string;
|
||||||
} | null;
|
} | null;
|
||||||
updateBuildStatus: (nodeId: string[], status: BuildStatus) => void;
|
updateBuildStatus: (nodeId: string[], status: BuildStatus) => void;
|
||||||
revertBuiltStatusFromBuilding: () => void;
|
revertBuiltStatusFromBuilding: () => void;
|
||||||
flowBuildStatus: { [key: string]: BuildStatus };
|
flowBuildStatus: {
|
||||||
|
[key: string]: { status: BuildStatus; timestamp?: string };
|
||||||
|
};
|
||||||
updateFlowPool: (
|
updateFlowPool: (
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
data: FlowPoolObjectType | ChatOutputType | chatInputType,
|
data: FlowPoolObjectType | ChatOutputType | chatInputType,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ import { VertexBuildTypeAPI } from "../types/api";
|
||||||
type BuildVerticesParams = {
|
type BuildVerticesParams = {
|
||||||
flowId: string; // Assuming FlowType is the type for your flow
|
flowId: string; // Assuming FlowType is the type for your flow
|
||||||
input_value?: any; // Replace any with the actual type if it's not any
|
input_value?: any; // Replace any with the actual type if it's not any
|
||||||
nodeId?: string | null; // Assuming nodeId is of type string, and it's optional
|
startNodeId?: string | null; // Assuming nodeId is of type string, and it's optional
|
||||||
|
stopNodeId?: string | null; // Assuming nodeId is of type string, and it's optional
|
||||||
onGetOrderSuccess?: () => void;
|
onGetOrderSuccess?: () => void;
|
||||||
onBuildUpdate?: (
|
onBuildUpdate?: (
|
||||||
data: VertexBuildTypeAPI,
|
data: VertexBuildTypeAPI,
|
||||||
|
|
@ -31,6 +32,8 @@ function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
|
||||||
id: vertexId,
|
id: vertexId,
|
||||||
data: inactiveData,
|
data: inactiveData,
|
||||||
params: "Inactive",
|
params: "Inactive",
|
||||||
|
run_id: "",
|
||||||
|
next_vertices_ids: [],
|
||||||
inactive_vertices: null,
|
inactive_vertices: null,
|
||||||
valid: false,
|
valid: false,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|
@ -41,18 +44,18 @@ function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
|
||||||
|
|
||||||
export async function updateVerticesOrder(
|
export async function updateVerticesOrder(
|
||||||
flowId: string,
|
flowId: string,
|
||||||
nodeId: string | null
|
startNodeId?: string | null,
|
||||||
|
stopNodeId?: string | null
|
||||||
): Promise<{
|
): Promise<{
|
||||||
verticesLayers: string[][];
|
verticesLayers: string[][];
|
||||||
verticesIds: string[];
|
verticesIds: string[];
|
||||||
verticesOrder: string[][];
|
|
||||||
runId: string;
|
runId: string;
|
||||||
}> {
|
}> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const setErrorData = useAlertStore.getState().setErrorData;
|
const setErrorData = useAlertStore.getState().setErrorData;
|
||||||
let orderResponse;
|
let orderResponse;
|
||||||
try {
|
try {
|
||||||
orderResponse = await getVerticesOrder(flowId, nodeId);
|
orderResponse = await getVerticesOrder(flowId, startNodeId, stopNodeId);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
setErrorData({
|
setErrorData({
|
||||||
|
|
@ -62,43 +65,40 @@ export async function updateVerticesOrder(
|
||||||
useFlowStore.getState().setIsBuilding(false);
|
useFlowStore.getState().setIsBuilding(false);
|
||||||
throw new Error("Invalid nodes");
|
throw new Error("Invalid nodes");
|
||||||
}
|
}
|
||||||
let verticesOrder: Array<Array<string>> = orderResponse.data.ids;
|
let verticesLayers: Array<Array<string>> = [orderResponse.data.ids];
|
||||||
const runId = orderResponse.data.run_id;
|
const runId = orderResponse.data.run_id;
|
||||||
let verticesLayers: Array<Array<string>> = [];
|
// if (nodeId) {
|
||||||
|
// for (let i = 0; i < verticesOrder.length; i += 1) {
|
||||||
if (nodeId) {
|
// const innerArray = verticesOrder[i];
|
||||||
for (let i = 0; i < verticesOrder.length; i += 1) {
|
// const idIndex = innerArray.indexOf(nodeId);
|
||||||
const innerArray = verticesOrder[i];
|
// if (idIndex !== -1) {
|
||||||
const idIndex = innerArray.indexOf(nodeId);
|
// // If there's a nodeId, we want to run just that component and not the entire layer
|
||||||
if (idIndex !== -1) {
|
// // because a layer contains dependencies for the next layer
|
||||||
// If there's a nodeId, we want to run just that component and not the entire layer
|
// // and we are stopping at the layer that contains the nodeId
|
||||||
// because a layer contains dependencies for the next layer
|
// verticesLayers.push([innerArray[idIndex]]);
|
||||||
// and we are stopping at the layer that contains the nodeId
|
// break; // Stop searching after finding the first occurrence
|
||||||
verticesLayers.push([innerArray[idIndex]]);
|
// }
|
||||||
break; // Stop searching after finding the first occurrence
|
// // If the targetId is not found, include the entire inner array
|
||||||
}
|
// verticesLayers.push(innerArray);
|
||||||
// If the targetId is not found, include the entire inner array
|
// }
|
||||||
verticesLayers.push(innerArray);
|
// } else {
|
||||||
}
|
// verticesLayers = verticesOrder;
|
||||||
} else {
|
// }
|
||||||
verticesLayers = verticesOrder;
|
const verticesIds = orderResponse.data.ids;
|
||||||
}
|
|
||||||
|
|
||||||
const verticesIds = verticesLayers.flat();
|
|
||||||
useFlowStore.getState().updateVerticesBuild({
|
useFlowStore.getState().updateVerticesBuild({
|
||||||
verticesLayers,
|
verticesLayers,
|
||||||
verticesIds,
|
verticesIds,
|
||||||
verticesOrder,
|
|
||||||
runId,
|
runId,
|
||||||
});
|
});
|
||||||
resolve({ verticesLayers, verticesIds, verticesOrder, runId });
|
resolve({ verticesLayers, verticesIds, runId });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildVertices({
|
export async function buildVertices({
|
||||||
flowId,
|
flowId,
|
||||||
input_value,
|
input_value,
|
||||||
nodeId = null,
|
startNodeId,
|
||||||
|
stopNodeId,
|
||||||
onGetOrderSuccess,
|
onGetOrderSuccess,
|
||||||
onBuildUpdate,
|
onBuildUpdate,
|
||||||
onBuildComplete,
|
onBuildComplete,
|
||||||
|
|
@ -107,12 +107,17 @@ export async function buildVertices({
|
||||||
validateNodes,
|
validateNodes,
|
||||||
}: BuildVerticesParams) {
|
}: BuildVerticesParams) {
|
||||||
let verticesBuild = useFlowStore.getState().verticesBuild;
|
let verticesBuild = useFlowStore.getState().verticesBuild;
|
||||||
if (!verticesBuild || nodeId) {
|
// if startNodeId and stopNodeId are provided
|
||||||
verticesBuild = await updateVerticesOrder(flowId, nodeId);
|
// something is wrong
|
||||||
|
if (startNodeId && stopNodeId) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (!verticesBuild || startNodeId || stopNodeId) {
|
||||||
|
verticesBuild = await updateVerticesOrder(flowId, startNodeId, stopNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
const verticesIds = verticesBuild?.verticesIds!;
|
const verticesIds = verticesBuild?.verticesIds!;
|
||||||
const verticesLayers = verticesBuild?.verticesLayers!;
|
const verticesLayers = verticesBuild?.verticesLayers!;
|
||||||
const verticesOrder = verticesBuild?.verticesOrder!;
|
|
||||||
const runId = verticesBuild?.runId!;
|
const runId = verticesBuild?.runId!;
|
||||||
let stop = false;
|
let stop = false;
|
||||||
|
|
||||||
|
|
@ -120,7 +125,8 @@ export async function buildVertices({
|
||||||
|
|
||||||
if (validateNodes) {
|
if (validateNodes) {
|
||||||
try {
|
try {
|
||||||
validateNodes(verticesOrder.flatMap((id) => id));
|
const nodes = useFlowStore.getState().nodes;
|
||||||
|
validateNodes(nodes.map((node) => node.id));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -128,49 +134,83 @@ export async function buildVertices({
|
||||||
|
|
||||||
useFlowStore.getState().updateBuildStatus(verticesIds, BuildStatus.TO_BUILD);
|
useFlowStore.getState().updateBuildStatus(verticesIds, BuildStatus.TO_BUILD);
|
||||||
useFlowStore.getState().setIsBuilding(true);
|
useFlowStore.getState().setIsBuilding(true);
|
||||||
|
let currentLayerIndex = 0; // Start with the first layer
|
||||||
// Set each vertex state to building
|
// Set each vertex state to building
|
||||||
const buildResults: Array<boolean> = [];
|
const buildResults: Array<boolean> = [];
|
||||||
for (const layer of verticesLayers) {
|
|
||||||
if (onBuildStart) onBuildStart(layer);
|
// Build each layer
|
||||||
for (const id of layer) {
|
while (
|
||||||
// Check if id is in the list of inactive nodes
|
currentLayerIndex <
|
||||||
if (!verticesIds.includes(id) && onBuildUpdate) {
|
(useFlowStore.getState().verticesBuild?.verticesLayers! || []).length
|
||||||
// If it is, skip building and set the state to inactive
|
) {
|
||||||
onBuildUpdate(getInactiveVertexData(id), BuildStatus.INACTIVE, runId);
|
// Get the current layer
|
||||||
buildResults.push(false);
|
const currentLayer =
|
||||||
continue;
|
useFlowStore.getState().verticesBuild?.verticesLayers![currentLayerIndex];
|
||||||
}
|
// If there are no more layers, we are done
|
||||||
await buildVertex({
|
if (!currentLayer) {
|
||||||
flowId,
|
if (onBuildComplete) {
|
||||||
id,
|
const allNodesValid = buildResults.every((result) => result);
|
||||||
input_value,
|
onBuildComplete(allNodesValid);
|
||||||
onBuildUpdate: (data: VertexBuildTypeAPI, status: BuildStatus) => {
|
useFlowStore.getState().setIsBuilding(false);
|
||||||
if (onBuildUpdate) onBuildUpdate(data, status, runId);
|
|
||||||
},
|
|
||||||
onBuildError,
|
|
||||||
verticesIds,
|
|
||||||
buildResults,
|
|
||||||
stopBuild: () => {
|
|
||||||
stop = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (stop) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
// If there is a callback for the start of the build, call it
|
||||||
|
if (onBuildStart) onBuildStart(currentLayer);
|
||||||
|
// Build each vertex in the current layer
|
||||||
|
await Promise.all(
|
||||||
|
currentLayer.map(async (vertexId) => {
|
||||||
|
// Check if id is in the list of inactive nodes
|
||||||
|
if (
|
||||||
|
!useFlowStore
|
||||||
|
.getState()
|
||||||
|
.verticesBuild?.verticesIds.includes(vertexId) &&
|
||||||
|
onBuildUpdate
|
||||||
|
) {
|
||||||
|
// If it is, skip building and set the state to inactive
|
||||||
|
onBuildUpdate(
|
||||||
|
getInactiveVertexData(vertexId),
|
||||||
|
BuildStatus.INACTIVE,
|
||||||
|
runId
|
||||||
|
);
|
||||||
|
buildResults.push(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the vertex
|
||||||
|
await buildVertex({
|
||||||
|
flowId,
|
||||||
|
id: vertexId,
|
||||||
|
input_value,
|
||||||
|
onBuildUpdate: (data: VertexBuildTypeAPI, status: BuildStatus) => {
|
||||||
|
if (onBuildUpdate) onBuildUpdate(data, status, runId);
|
||||||
|
},
|
||||||
|
onBuildError,
|
||||||
|
verticesIds,
|
||||||
|
buildResults,
|
||||||
|
stopBuild: () => {
|
||||||
|
stop = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (stop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// Once the current layer is built, move to the next layer
|
||||||
|
currentLayerIndex += 1;
|
||||||
|
|
||||||
if (stop) {
|
if (stop) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (onBuildComplete) {
|
if (onBuildComplete) {
|
||||||
const allNodesValid = buildResults.every((result) => result);
|
const allNodesValid = buildResults.every((result) => result);
|
||||||
onBuildComplete(allNodesValid);
|
onBuildComplete(allNodesValid);
|
||||||
useFlowStore.getState().setIsBuilding(false);
|
useFlowStore.getState().setIsBuilding(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildVertex({
|
async function buildVertex({
|
||||||
flowId,
|
flowId,
|
||||||
id,
|
id,
|
||||||
|
|
@ -192,6 +232,7 @@ async function buildVertex({
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const buildRes = await postBuildVertex(flowId, id, input_value);
|
const buildRes = await postBuildVertex(flowId, id, input_value);
|
||||||
|
|
||||||
const buildData: VertexBuildTypeAPI = buildRes.data;
|
const buildData: VertexBuildTypeAPI = buildRes.data;
|
||||||
if (onBuildUpdate) {
|
if (onBuildUpdate) {
|
||||||
if (!buildData.valid) {
|
if (!buildData.valid) {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ import {
|
||||||
INPUT_TYPES,
|
INPUT_TYPES,
|
||||||
LANGFLOW_SUPPORTED_TYPES,
|
LANGFLOW_SUPPORTED_TYPES,
|
||||||
OUTPUT_TYPES,
|
OUTPUT_TYPES,
|
||||||
|
SUCCESS_BUILD,
|
||||||
specialCharsRegex,
|
specialCharsRegex,
|
||||||
successBuild,
|
|
||||||
} from "../constants/constants";
|
} from "../constants/constants";
|
||||||
import { downloadFlowsFromDatabase } from "../controllers/API";
|
import { downloadFlowsFromDatabase } from "../controllers/API";
|
||||||
import {
|
import {
|
||||||
|
|
@ -1092,7 +1092,7 @@ export function getGroupStatus(
|
||||||
flow: FlowType,
|
flow: FlowType,
|
||||||
ssData: { [key: string]: { valid: boolean; params: string } }
|
ssData: { [key: string]: { valid: boolean; params: string } }
|
||||||
) {
|
) {
|
||||||
let status = { valid: true, params: successBuild };
|
let status = { valid: true, params: SUCCESS_BUILD };
|
||||||
const { nodes } = flow.data!;
|
const { nodes } = flow.data!;
|
||||||
const ids = nodes.map((n: NodeType) => n.data.id);
|
const ids = nodes.map((n: NodeType) => n.data.id);
|
||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,23 @@ export function getTagsIds(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInputsAndOutputs(nodes: Node[]) {
|
export function getInputsAndOutputs(nodes: Node[]) {
|
||||||
let inputs: { type: string; id: string }[] = [];
|
let inputs: { type: string; id: string; displayName: string }[] = [];
|
||||||
let outputs: { type: string; id: string }[] = [];
|
let outputs: { type: string; id: string; displayName: string }[] = [];
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
const nodeData: NodeDataType = node.data as NodeDataType;
|
const nodeData: NodeDataType = node.data as NodeDataType;
|
||||||
if (isOutputNode(nodeData)) {
|
if (isOutputNode(nodeData)) {
|
||||||
outputs.push({ type: nodeData.type, id: nodeData.id });
|
outputs.push({
|
||||||
|
type: nodeData.type,
|
||||||
|
id: nodeData.id,
|
||||||
|
displayName: nodeData.node?.display_name ?? nodeData.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (isInputNode(nodeData)) {
|
if (isInputNode(nodeData)) {
|
||||||
inputs.push({ type: nodeData.type, id: nodeData.id });
|
inputs.push({
|
||||||
|
type: nodeData.type,
|
||||||
|
id: nodeData.id,
|
||||||
|
displayName: nodeData.node?.display_name ?? nodeData.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { inputs, outputs };
|
return { inputs, outputs };
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue