merge fix

This commit is contained in:
cristhianzl 2024-03-13 11:12:42 -03:00
commit 34f5a1300a
73 changed files with 2165 additions and 1531 deletions

View file

@ -197,7 +197,7 @@ def format_elapsed_time(elapsed_time: float) -> str:
return f"{minutes} {minutes_unit}, {seconds} {seconds_unit}"
def build_and_cache_graph(
async def build_and_cache_graph(
flow_id: str,
session: Session,
chat_service: "ChatService",
@ -212,7 +212,7 @@ def build_and_cache_graph(
graph = other_graph
else:
graph = graph.update(other_graph)
chat_service.set_cache(flow_id, graph)
await chat_service.set_cache(flow_id, graph)
return graph

View file

@ -58,9 +58,9 @@ async def get_vertices(
try:
# First, we need to check if the flow_id is in the cache
graph = None
if cache := chat_service.get_cache(flow_id):
if cache := await chat_service.get_cache(flow_id):
graph = cache.get("result")
graph = build_and_cache_graph(flow_id, session, chat_service, graph)
graph = await build_and_cache_graph(flow_id, session, chat_service, graph)
if stop_component_id or start_component_id:
try:
vertices = graph.sort_vertices(stop_component_id, start_component_id)
@ -98,11 +98,11 @@ async def build_vertex(
next_vertices_ids = []
try:
start_time = time.perf_counter()
cache = chat_service.get_cache(flow_id)
cache = await chat_service.get_cache(flow_id)
if not cache:
# If there's no cache
logger.warning(f"No cache found for {flow_id}. Building graph starting at {vertex_id}")
graph = build_and_cache_graph(flow_id=flow_id, session=next(get_session()), chat_service=chat_service)
graph = await build_and_cache_graph(flow_id=flow_id, session=next(get_session()), chat_service=chat_service)
else:
graph = cache.get("result")
result_data_response = ResultDataResponse(results={})
@ -121,8 +121,11 @@ async def build_vertex(
artifacts = vertex.artifacts
else:
raise ValueError(f"No result found for vertex {vertex_id}")
next_vertices_ids = vertex.successors_ids
next_vertices_ids = [v for v in next_vertices_ids if graph.should_run_vertex(v)]
async with chat_service._cache_locks[flow_id] as lock:
graph.remove_from_predecessors(vertex_id)
next_vertices_ids = vertex.successors_ids
next_vertices_ids = [v for v in next_vertices_ids if graph.should_run_vertex(v)]
await chat_service.set_cache(flow_id=flow_id, data=graph, lock=lock)
result_data_response = ResultDataResponse(**result_dict.model_dump())
@ -134,7 +137,7 @@ async def build_vertex(
artifacts = {}
# If there's an error building the vertex
# we need to clear the cache
chat_service.clear_cache(flow_id)
await chat_service.clear_cache(flow_id)
# Log the vertex build
if not vertex.will_stream:
@ -157,7 +160,7 @@ async def build_vertex(
inactivated_vertices = list(graph.inactivated_vertices)
graph.reset_inactivated_vertices()
graph.reset_activated_vertices()
chat_service.set_cache(flow_id, graph)
await 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

View file

@ -1,5 +1,5 @@
from http import HTTPStatus
from typing import Annotated, Any, List, Optional, Union
from typing import Annotated, List, Optional, Union
import sqlalchemy as sa
from fastapi import APIRouter, Body, Depends, HTTPException, UploadFile, status
@ -8,14 +8,16 @@ from sqlmodel import Session, select
from langflow.api.utils import update_frontend_node_with_template_values
from langflow.api.v1.schemas import (
CustomComponentCode,
CustomComponentRequest,
InputValueRequest,
ProcessResponse,
RunResponse,
TaskStatusResponse,
Tweaks,
UpdateCustomComponentRequest,
UploadFileResponse,
)
from langflow.graph.schema import RunOutputs
from langflow.interface.custom.custom_component import CustomComponent
from langflow.interface.custom.directory_reader import DirectoryReader
from langflow.interface.custom.utils import build_custom_component_template
@ -24,12 +26,7 @@ from langflow.services.auth.utils import api_key_security, get_current_active_us
from langflow.services.cache.utils import save_uploaded_file
from langflow.services.database.models.flow import Flow
from langflow.services.database.models.user.model import User
from langflow.services.deps import (
get_session,
get_session_service,
get_settings_service,
get_task_service,
)
from langflow.services.deps import get_session, get_session_service, get_settings_service, get_task_service
from langflow.services.session.service import SessionService
from langflow.services.task.service import TaskService
@ -56,8 +53,8 @@ def get_all(
async def run_flow_with_caching(
session: Annotated[Session, Depends(get_session)],
flow_id: str,
inputs: Optional[List[InputValueRequest]] = None,
outputs: Optional[List[str]] = None,
inputs: Optional[List[InputValueRequest]] = [],
outputs: Optional[List[str]] = [],
tweaks: Annotated[Optional[Tweaks], Body(embed=True)] = None, # noqa: F821
stream: Annotated[bool, Body(embed=True)] = False, # noqa: F821
session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
@ -102,25 +99,20 @@ async def run_flow_with_caching(
This endpoint facilitates complex flow executions with customized inputs, outputs, and configurations, catering to diverse application requirements.
"""
try:
if inputs is not None:
input_values: list[dict[str, Union[str, list[str]]]] = [_input.model_dump() for _input in inputs]
else:
input_values = [{}]
if outputs is None:
outputs = []
if session_id:
session_data = await session_service.load_session(session_id, flow_id=flow_id)
graph, artifacts = session_data if session_data else (None, None)
task_result: Any = None
task_result: List[RunOutputs] = []
if not graph:
raise ValueError("Graph not found in the session")
task_result, session_id = await run_graph(
graph=graph,
flow_id=flow_id,
session_id=session_id,
inputs=input_values,
inputs=inputs,
outputs=outputs,
artifacts=artifacts,
session_service=session_service,
@ -142,7 +134,7 @@ async def run_flow_with_caching(
graph=graph_data,
flow_id=flow_id,
session_id=session_id,
inputs=input_values,
inputs=inputs,
outputs=outputs,
artifacts={},
session_service=session_service,
@ -253,12 +245,12 @@ def get_version():
@router.post("/custom_component", status_code=HTTPStatus.OK)
async def custom_component(
raw_code: CustomComponentCode,
raw_code: CustomComponentRequest,
user: User = Depends(get_current_active_user),
):
component = CustomComponent(code=raw_code.code)
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)
return built_frontend_node
@ -275,23 +267,46 @@ async def reload_custom_component(path: str, user: User = Depends(get_current_ac
raise ValueError(content)
extractor = CustomComponent(code=content)
return build_custom_component_template(extractor, user_id=user.id)
frontend_node, _ = build_custom_component_template(extractor, user_id=user.id)
return frontend_node
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc))
@router.post("/custom_component/update", status_code=HTTPStatus.OK)
async def custom_component_update(
raw_code: CustomComponentCode,
code_request: UpdateCustomComponentRequest,
user: User = Depends(get_current_active_user),
):
component = CustomComponent(code=raw_code.code)
"""
Update a custom component with the provided code request.
component_node = build_custom_component_template(
component,
user_id=user.id,
update_field=raw_code.field,
update_field_value=raw_code.field_value,
)
# Update the field
return component_node
This endpoint generates the CustomComponentFrontendNode normally but then runs the `update_build_config` method
on the latest version of the template. This ensures that every time it runs, it has the latest version of the template.
Args:
code_request (CustomComponentRequest): The code request containing the updated code for the custom component.
user (User, optional): The user making the request. Defaults to the current active user.
Returns:
dict: The updated custom component node.
"""
try:
component = CustomComponent(code=code_request.code)
component_node, cc_instance = build_custom_component_template(
component,
user_id=user.id,
)
updated_build_config = cc_instance.update_build_config(
build_config=code_request.get_template(),
field_value=code_request.field_value,
field_name=code_request.field,
)
component_node["template"] = updated_build_config
return component_node
except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc

View file

@ -211,7 +211,5 @@ async def download_file(
current_user: User = Depends(get_current_active_user),
):
"""Download all flows as a file."""
flows = read_flows(
current_user=current_user, session=session, settings_service=settings_service
)
flows = read_flows(current_user=current_user, session=session, settings_service=settings_service)
return FlowListRead(flows=flows)

View file

@ -1,5 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import Session
from langflow.api.v1.schemas import Token
from langflow.services.auth.utils import (
authenticate_user,
@ -8,7 +10,6 @@ from langflow.services.auth.utils import (
create_user_tokens,
)
from langflow.services.deps import get_session, get_settings_service
from sqlmodel import Session
router = APIRouter(tags=["Login"])
@ -40,7 +41,7 @@ async def login_to_get_access_token(
httponly=auth_settings.REFRESH_HTTPONLY,
samesite=auth_settings.REFRESH_SAME_SITE,
secure=auth_settings.REFRESH_SECURE,
expires=auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES*60,
expires=auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES * 60,
)
response.set_cookie(
"access_token_lf",
@ -48,7 +49,7 @@ async def login_to_get_access_token(
httponly=auth_settings.ACCESS_HTTPONLY,
samesite=auth_settings.ACCESS_SAME_SITE,
secure=auth_settings.ACCESS_SECURE,
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES*60,
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
)
return tokens
else:
@ -74,7 +75,7 @@ async def auto_login(
httponly=auth_settings.ACCESS_HTTPONLY,
samesite=auth_settings.ACCESS_SAME_SITE,
secure=auth_settings.ACCESS_SECURE,
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES*60,
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
)
return tokens
@ -98,10 +99,10 @@ async def refresh_token(request: Request, response: Response, settings_service=D
response.set_cookie(
"refresh_token_lf",
tokens["refresh_token"],
httponly=auth_settings.REFRESH_TOKEN_HTTPONLY,
httponly=auth_settings.REFRESH_HTTPONLY,
samesite=auth_settings.REFRESH_SAME_SITE,
secure=auth_settings.REFRESH_SECURE,
expires=auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES*60,
expires=auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES * 60,
)
response.set_cookie(
"access_token_lf",
@ -109,7 +110,7 @@ async def refresh_token(request: Request, response: Response, settings_service=D
httponly=auth_settings.ACCESS_HTTPONLY,
samesite=auth_settings.ACCESS_SAME_SITE,
secure=auth_settings.ACCESS_SECURE,
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES*60,
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
)
return tokens
else:

View file

@ -4,8 +4,17 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from uuid import UUID
from pydantic import BaseModel, Field, RootModel, field_validator, model_serializer
from pydantic import (
BaseModel,
ConfigDict,
Field,
RootModel,
field_validator,
model_serializer,
)
from langflow.graph.schema import RunOutputs
from langflow.schema import dotdict
from langflow.services.database.models.api_key.model import ApiKeyRead
from langflow.services.database.models.base import orjson_dumps
from langflow.services.database.models.flow import FlowCreate, FlowRead
@ -49,7 +58,7 @@ class ProcessResponse(BaseModel):
class RunResponse(BaseModel):
"""Run response schema."""
outputs: Optional[List[Any]] = None
outputs: Optional[List[RunOutputs]] = []
session_id: Optional[str] = None
@model_serializer(mode="wrap")
@ -161,13 +170,21 @@ class StreamData(BaseModel):
return f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n"
class CustomComponentCode(BaseModel):
class CustomComponentRequest(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
code: str
field: Optional[str] = None
field_value: Optional[Any] = None
frontend_node: Optional[dict] = None
class UpdateCustomComponentRequest(CustomComponentRequest):
field: str
field_value: Optional[Union[str, int, float, bool, dict, list]] = None
template: dict
def get_template(self):
return dotdict(self.template)
class CustomComponentResponseError(BaseModel):
detail: str
traceback: str
@ -244,7 +261,7 @@ class VerticesBuiltResponse(BaseModel):
class InputValueRequest(BaseModel):
components: Optional[List[str]] = None
components: Optional[List[str]] = []
input_value: Optional[str] = None
# add an example

View file

@ -21,6 +21,7 @@ TEXT_FILE_TYPES = [
"xml",
"html",
"htm",
"pdf",
]
@ -54,9 +55,7 @@ def retrieve_file_paths(
glob = "**/*" if recursive else "*"
paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob)
file_paths = [
Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)
]
file_paths = [Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)]
return file_paths
@ -85,19 +84,28 @@ def read_text_file(file_path: str) -> str:
return f.read()
def parse_pdf_to_text(file_path: str) -> str:
from pypdf import PdfReader # type: ignore
with open(file_path, "rb") as f:
reader = PdfReader(f)
return "\n\n".join([page.extract_text() for page in reader.pages])
def parse_text_file_to_record(file_path: str, silent_errors: bool) -> Optional[Record]:
try:
text = read_text_file(file_path)
if file_path.endswith(".pdf"):
text = parse_pdf_to_text(file_path)
else:
text = read_text_file(file_path)
# if file is json, yaml, or xml, we can parse it
if file_path.endswith(".json"):
text = json.loads(text)
elif file_path.endswith(".yaml") or file_path.endswith(".yml"):
text = yaml.safe_load(text)
elif file_path.endswith(".xml"):
text = ET.fromstring(text)
xml_element = ET.fromstring(text)
text = ET.tostring(xml_element, encoding="unicode")
except Exception as e:
if not silent_errors:
raise ValueError(f"Error loading file {file_path}: {e}") from e
@ -116,10 +124,7 @@ def get_elements(
if use_multithreading:
records = parallel_load_records(file_paths, silent_errors, max_concurrency)
else:
records = [
partition_file_to_record(file_path, silent_errors)
for file_path in file_paths
]
records = [partition_file_to_record(file_path, silent_errors) for file_path in file_paths]
records = list(filter(None, records))
return records

View file

@ -26,6 +26,7 @@ class ChatComponent(CustomComponent):
"session_id": {
"display_name": "Session ID",
"info": "If provided, the message will be stored in the memory.",
"advanced": True,
},
"return_record": {
"display_name": "Return Record",

View file

@ -0,0 +1,70 @@
# from typing import Dict, List
# import dspy
# from langflow import CustomComponent
# from langflow.field_typing import Text
# class ReActAgentComponent(CustomComponent):
# display_name = "ReAct Agent"
# description = "A component to create a ReAct Agent."
# icon = "user-secret"
# def build_config(self):
# return {
# "input_value": {
# "display_name": "Input",
# "input_types": ["Text"],
# "info": "The input value for the ReAct Agent.",
# },
# "instructions": {
# "display_name": "Instructions",
# "info": "The Prompt.",
# },
# "inputs": {
# "display_name": "Inputs",
# "info": "The Name and Description of the Input Fields.",
# },
# "outputs": {
# "display_name": "Outputs",
# "info": "The Name and Description of the Output Fields.",
# },
# }
# def build(
# self,
# input_value: List[dict],
# instructions: Text,
# inputs: List[dict],
# outputs: List[Dict],
# ) -> Text:
# # inputs is a list of dictionaries where the key is the name of the input
# # and the value is the description of the input
# input_fields = (
# {}
# ) # dict[str, FieldInfo] InputField and OutputField are subclasses of pydantic.Field
# for input_dict in inputs:
# for name, description in input_dict.items():
# prefix = name if ":" in name else f"{name}:"
# input_fields[name] = dspy.InputField(
# prefix=prefix, description=description
# )
# output_fields = {} # dict[str, FieldInfo]
# for output_dict in outputs:
# for name, description in output_dict.items():
# prefix = name if ":" in name else f"{name}:"
# output_fields[name] = dspy.OutputField(
# prefix=prefix, description=description
# )
# signature = dspy.make_signature(inputs, instructions=instructions)
# agent = dspy.ReAct(
# signature=signature,
# )
# inputs_dict = {}
# for input_dict in input_value:
# inputs_dict.update(input_dict)
# result = agent(inputs_dict)

View file

@ -56,9 +56,7 @@ class APIRequest(CustomComponent):
data = body if body else None
payload = json.dumps(data)
try:
response = await client.request(
method, url, headers=headers, content=payload, timeout=timeout
)
response = await client.request(method, url, headers=headers, content=payload, timeout=timeout)
try:
result = response.json()
except Exception:
@ -108,13 +106,10 @@ class APIRequest(CustomComponent):
bodies = [body.data]
if len(urls) != len(bodies):
# add bodies with None
bodies += [None] * (len(urls) - len(bodies))
bodies += [None] * (len(urls) - len(bodies)) # type: ignore
async with httpx.AsyncClient() as client:
results = await asyncio.gather(
*[
self.make_request(client, method, u, headers, rec, timeout)
for u, rec in zip(urls, bodies)
]
*[self.make_request(client, method, u, headers, rec, timeout) for u, rec in zip(urls, bodies)]
)
self.status = results
return results

View file

@ -53,20 +53,14 @@ class DirectoryComponent(CustomComponent):
silent_errors: bool = False,
use_multithreading: bool = True,
) -> List[Optional[Record]]:
resolved_path = self.resolve_path(path)
file_paths = retrieve_file_paths(resolved_path, load_hidden, recursive, depth)
loaded_records = []
if use_multithreading:
loaded_records = parallel_load_records(
file_paths, silent_errors, max_concurrency
)
loaded_records = parallel_load_records(file_paths, silent_errors, max_concurrency)
else:
loaded_records = [
parse_text_file_to_record(file_path, silent_errors)
for file_path in file_paths
]
loaded_records = [parse_text_file_to_record(file_path, silent_errors) for file_path in file_paths]
loaded_records = list(filter(None, loaded_records))
self.status = loaded_records
return loaded_records

View file

@ -15,7 +15,7 @@ class URLComponent(CustomComponent):
"urls": {"display_name": "URL"},
}
async def build(
def build(
self,
urls: list[str],
) -> list[Record]:

View file

@ -21,9 +21,7 @@ class ExtractKeyFromRecordComponent(CustomComponent):
},
}
def build(
self, record: Record, keys: list[str], silent_error: bool = True
) -> Record:
def build(self, record: Record, keys: list[str], silent_error: bool = True) -> Record:
"""
Extracts the keys from a record.

View file

@ -3,12 +3,10 @@ from .ExtractDataFromRecord import ExtractKeyFromRecordComponent
from .GetNotified import GetNotifiedComponent
from .ListFlows import ListFlowsComponent
from .MergeRecords import MergeRecordsComponent
from .MessageHistory import MessageHistoryComponent
from .Notify import NotifyComponent
from .RunFlow import RunFlowComponent
from .RunnableExecutor import RunnableExecComponent
from .SQLExecutor import SQLExecutorComponent
from .TextToRecord import TextToRecordComponent
__all__ = [
"ClearMessageHistoryComponent",

View file

@ -15,6 +15,7 @@ class UpdateRecordComponent(CustomComponent):
"new_data": {
"display_name": "New Data",
"info": "The new data to update the record with.",
"input_types": ["Text"],
},
}

View file

@ -1,13 +1,19 @@
from .CustomComponent import Component
from .DocumentToRecord import DocumentToRecordComponent
from .IDGenerator import UUIDGeneratorComponent
from .MessageHistory import MessageHistoryComponent
from .PythonFunction import PythonFunctionComponent
from .RecordsAsText import RecordsAsTextComponent
from .TextToRecord import TextToRecordComponent
from .UpdateRecord import UpdateRecordComponent
__all__ = [
"Component",
"UpdateRecordComponent",
"DocumentToRecordComponent",
"UUIDGeneratorComponent",
"PythonFunctionComponent",
"RecordsAsTextComponent",
"TextToRecordComponent",
"MessageHistoryComponent",
]

View file

@ -24,9 +24,7 @@ class PromptComponent(CustomComponent):
prompt_template = PromptTemplate.from_template(Text(template))
kwargs = dict_values_to_string(kwargs)
kwargs = {
k: "\n".join(v) if isinstance(v, list) else v for k, v in kwargs.items()
}
kwargs = {k: "\n".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}
try:
formated_prompt = prompt_template.format(**kwargs)
except Exception as exc:

View file

@ -1,4 +1,5 @@
from langchain_community.chat_models.cohere import ChatCohere
from pydantic.v1 import SecretStr
from langflow.components.models.base.model import LCModelComponent
from langflow.field_typing import Text
@ -44,8 +45,9 @@ class CohereComponent(LCModelComponent):
temperature: float = 0.75,
stream: bool = False,
) -> Text:
api_key = SecretStr(cohere_api_key)
output = ChatCohere( # type: ignore
cohere_api_key=cohere_api_key,
cohere_api_key=api_key,
temperature=temperature,
)
return self.get_result(output=output, stream=stream, input_value=input_value)

View file

@ -122,7 +122,9 @@ class ContractEdge(Edge):
return
if not source._built:
await source.build()
# The system should be read-only, so we should not be building vertices
# that are not already built.
raise ValueError(f"Source vertex {source.id} is not built.")
if self.matched_type == "Text":
self.result = source._built_result
@ -132,7 +134,7 @@ class ContractEdge(Edge):
target.params[self.target_param] = self.result
self.is_fulfilled = True
async def get_result(self, source: "Vertex", target: "Vertex"):
async def get_result_from_source(self, source: "Vertex", target: "Vertex"):
# Fulfill the contract if it has not been fulfilled.
if not self.is_fulfilled:
await self.honor(source, target)

View file

@ -9,7 +9,7 @@ from langflow.graph.edge.base import ContractEdge
from langflow.graph.graph.constants import lazy_load_vertex_dict
from langflow.graph.graph.state_manager import GraphStateManager
from langflow.graph.graph.utils import process_flow
from langflow.graph.schema import INPUT_FIELD_NAME, InterfaceComponentTypes
from langflow.graph.schema import INPUT_FIELD_NAME, InterfaceComponentTypes, RunOutputs
from langflow.graph.vertex.base import Vertex
from langflow.graph.vertex.types import (
ChatVertex,
@ -162,15 +162,16 @@ class Graph:
if vertex is None:
raise ValueError(f"Vertex {vertex_id} not found")
vertex.update_raw_params({"session_id": session_id})
# Process the graph
try:
await self.process()
self.increment_run_count()
except Exception as exc:
logger.exception(exc)
raise ValueError(f"Error running graph: {exc}") from exc
# Get the outputs
vertex_outputs = []
for vertex_id in self._is_output_vertices:
vertex = self.get_vertex(vertex_id)
for vertex in self.vertices:
if vertex is None:
raise ValueError(f"Vertex {vertex_id} not found")
@ -178,17 +179,18 @@ class Graph:
await vertex.consume_async_generator()
if not outputs or (vertex.display_name in outputs or vertex.id in outputs):
vertex_outputs.append(vertex.result)
return vertex_outputs
async def run(
self,
inputs: list[Dict[str, Union[str, list[str]]]],
inputs: list[Dict[str, str]],
inputs_components: Optional[list[list[str]]] = None,
outputs: Optional[list[str]] = None,
session_id: Optional[str] = None,
stream: bool = False,
) -> List[List[Optional["ResultData"]]]:
) -> List[RunOutputs]:
"""Runs the graph with the given inputs."""
# inputs is {"message": "Hello, world!"}
# we need to go through self.inputs and update the self._raw_params
# of the vertices that are inputs
@ -196,30 +198,24 @@ class Graph:
vertex_outputs = []
if not isinstance(inputs, list):
inputs = [inputs]
for input_dict in inputs:
components: Union[str, list[str]] = input_dict.get("components", [])
if not isinstance(components, list):
components = [components]
if INPUT_FIELD_NAME not in input_dict:
input_value = ""
else:
_input_value = input_dict[INPUT_FIELD_NAME]
if isinstance(_input_value, str):
input_value = _input_value
else:
raise ValueError(f"Invalid input value: {input_value}. Expected string")
for run_inputs, components in zip(inputs, inputs_components or []):
if components and not isinstance(components, list):
raise ValueError(f"Invalid components value: {components}. Expected list")
elif components is None:
components = []
if not isinstance(run_inputs.get(INPUT_FIELD_NAME, ""), str):
raise ValueError(f"Invalid input value: {run_inputs.get(INPUT_FIELD_NAME)}. Expected string")
run_outputs = await self._run(
inputs={INPUT_FIELD_NAME: input_value},
inputs=run_inputs,
input_components=components,
outputs=outputs or [],
stream=stream,
session_id=session_id or "",
)
logger.debug(f"Run outputs: {run_outputs}")
vertex_outputs.append(run_outputs)
run_output_object = RunOutputs(inputs=run_inputs, outputs=run_outputs)
logger.debug(f"Run outputs: {run_output_object}")
vertex_outputs.append(run_output_object)
return vertex_outputs
# vertices_layers is a list of lists ordered by the order the vertices
@ -240,6 +236,7 @@ class Graph:
def build_graph_maps(self):
self.predecessor_map, self.successor_map = self.build_adjacency_maps()
self.in_degree_map = self.build_in_degree()
self.parent_child_map = self.build_parent_child_map()
@ -295,6 +292,15 @@ class Graph:
successor_map[edge.source_id].append(edge.target_id)
return predecessor_map, successor_map
def build_run_map(self):
run_map = defaultdict(list)
# The run map gets the predecessor_map and maps the info like this:
# {vertex_id: every id that contains the vertex_id in the predecessor_map}
for vertex_id, predecessors in self.predecessor_map.items():
for predecessor in predecessors:
run_map[predecessor].append(vertex_id)
return run_map
@classmethod
def from_payload(cls, payload: Dict, flow_id: Optional[str] = None) -> "Graph":
"""
@ -939,16 +945,37 @@ class Graph:
# save the only the rest
self.vertices_layers = vertices_layers[1:]
self.vertices_to_run = {vertex_id for vertex_id in chain.from_iterable(vertices_layers)}
self.run_map, self.run_predecessors = (
self.build_run_map(),
self.predecessor_map.copy(),
)
# Return just the first layer
return first_layer
def vertex_has_no_more_predecessors(self, vertex_id: str) -> bool:
"""Returns whether a vertex has no more predecessors."""
return not self.run_predecessors.get(vertex_id)
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
# the self.run_map is a map of vertex_id to a list of predecessors
# each time a vertex is run, we remove it from the list of predecessors
# if a vertex has no more predecessors, it should be run
should_run = vertex_id in self.vertices_to_run and self.vertex_has_no_more_predecessors(vertex_id)
if should_run:
self.vertices_to_run.remove(vertex_id)
# remove the vertex from the run_map
self.remove_from_predecessors(vertex_id)
return should_run
def remove_from_predecessors(self, vertex_id: str):
predecessors = self.run_map.get(vertex_id, [])
for predecessor in predecessors:
if vertex_id in self.run_predecessors[predecessor]:
self.run_predecessors[predecessor].remove(vertex_id)
def sort_interface_components_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
"""Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first."""

View file

@ -1,5 +1,5 @@
from enum import Enum
from typing import Any, Optional
from typing import Any, List, Optional
from pydantic import BaseModel, Field, field_serializer
@ -13,6 +13,8 @@ class ResultData(BaseModel):
messages: Optional[list[ChatOutputResponse]] = Field(default_factory=list)
timedelta: Optional[float] = None
duration: Optional[str] = None
component_display_name: Optional[str] = None
component_id: Optional[str] = None
@field_serializer("results")
def serialize_results(self, value):
@ -48,3 +50,8 @@ OUTPUT_COMPONENTS = [
]
INPUT_FIELD_NAME = "input_value"
class RunOutputs(BaseModel):
inputs: dict = Field(default_factory=dict)
outputs: List[Optional[ResultData]] = Field(default_factory=list)

View file

@ -1,4 +1,5 @@
import ast
import asyncio
import inspect
import types
from enum import Enum
@ -7,7 +8,6 @@ from typing import (
Any,
AsyncIterator,
Callable,
Coroutine,
Dict,
Iterator,
List,
@ -56,17 +56,13 @@ class Vertex:
) -> None:
# is_external means that the Vertex send or receives data from
# an external source (e.g the chat)
self._lock = asyncio.Lock()
self.will_stream = False
self.updated_raw_params = False
self.id: str = data["id"]
self.is_state = False
self.is_input = any(
input_component_name in self.id for input_component_name in INPUT_COMPONENTS
)
self.is_output = any(
output_component_name in self.id
for output_component_name in OUTPUT_COMPONENTS
)
self.is_input = any(input_component_name in self.id for input_component_name in INPUT_COMPONENTS)
self.is_output = any(output_component_name in self.id for output_component_name in OUTPUT_COMPONENTS)
self.has_session_id = None
self._custom_component = None
self.has_external_input = False
@ -106,17 +102,11 @@ class Vertex:
def set_state(self, state: str):
self.state = VertexStates[state]
if (
self.state == VertexStates.INACTIVE
and self.graph.in_degree_map[self.id] < 2
):
if self.state == VertexStates.INACTIVE and self.graph.in_degree_map[self.id] < 2:
# If the vertex is inactive and has only one in degree
# it means that it is not a merge point in the graph
self.graph.inactivated_vertices.add(self.id)
elif (
self.state == VertexStates.ACTIVE
and self.id in self.graph.inactivated_vertices
):
elif self.state == VertexStates.ACTIVE and self.id in self.graph.inactivated_vertices:
self.graph.inactivated_vertices.remove(self.id)
@property
@ -133,9 +123,7 @@ class Vertex:
# If the Vertex.type is a power component
# then we need to return the built object
# instead of the result dict
if self.is_interface_component and not isinstance(
self._built_object, UnbuiltObject
):
if self.is_interface_component and not isinstance(self._built_object, UnbuiltObject):
result = self._built_object
# if it is not a dict or a string and hasattr model_dump then
# return the model_dump
@ -147,11 +135,7 @@ class Vertex:
if isinstance(self._built_result, UnbuiltResult):
return {}
return (
self._built_result
if isinstance(self._built_result, dict)
else {"result": self._built_result}
)
return self._built_result if isinstance(self._built_result, dict) else {"result": self._built_result}
def set_artifacts(self) -> None:
pass
@ -188,6 +172,7 @@ class Vertex:
}
def __setstate__(self, state):
self._lock = asyncio.Lock()
self._data = state["_data"]
self.params = state["params"]
self.base_type = state["base_type"]
@ -225,31 +210,19 @@ class Vertex:
self.selected_output_type = self.data["node"].get("selected_output_type")
self.is_input = self.data["node"].get("is_input") or self.is_input
self.is_output = self.data["node"].get("is_output") or self.is_output
template_dicts = {
key: value
for key, value in self.data["node"]["template"].items()
if isinstance(value, dict)
}
template_dicts = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)}
self.has_session_id = "session_id" in template_dicts
self.required_inputs = [
template_dicts[key]["type"]
for key, value in template_dicts.items()
if value["required"]
template_dicts[key]["type"] for key, value in template_dicts.items() if value["required"]
]
self.optional_inputs = [
template_dicts[key]["type"]
for key, value in template_dicts.items()
if not value["required"]
template_dicts[key]["type"] for key, value in template_dicts.items() if not value["required"]
]
# Add the template_dicts[key]["input_types"] to the optional_inputs
self.optional_inputs.extend(
[
input_type
for value in template_dicts.values()
for input_type in value.get("input_types", [])
]
[input_type for value in template_dicts.values() for input_type in value.get("input_types", [])]
)
template_dict = self.data["node"]["template"]
@ -296,11 +269,7 @@ class Vertex:
self.updated_raw_params = False
return
template_dict = {
key: value
for key, value in self.data["node"]["template"].items()
if isinstance(value, dict)
}
template_dict = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)}
params = {}
for edge in self.edges:
@ -321,10 +290,7 @@ class Vertex:
# we don't know the key of the dict but we need to set the value
# to the vertex that is the source of the edge
param_dict = template_dict[param_key]["value"]
params[param_key] = {
key: self.graph.get_vertex(edge.source_id)
for key in param_dict.keys()
}
params[param_key] = {key: self.graph.get_vertex(edge.source_id) for key in param_dict.keys()}
else:
params[param_key] = self.graph.get_vertex(edge.source_id)
@ -360,11 +326,7 @@ class Vertex:
# list of dicts, so we need to convert it to a dict
# before passing it to the build method
if isinstance(val, list):
params[key] = {
k: v
for item in value.get("value", [])
for k, v in item.items()
}
params[key] = {k: v for item in value.get("value", []) for k, v in item.items()}
elif isinstance(val, dict):
params[key] = val
elif value.get("type") == "int" and val is not None:
@ -423,7 +385,7 @@ class Vertex:
Initiate the build process.
"""
logger.debug(f"Building {self.display_name}")
await self._build_each_node_in_params_dict(user_id)
await self._build_each_vertex_in_params_dict(user_id)
await self._get_and_instantiate_class(user_id)
self._validate_built_object()
@ -466,6 +428,8 @@ class Vertex:
results=result_dict,
artifacts=artifacts,
messages=messages,
component_display_name=self.display_name,
component_id=self.id,
)
self.set_result(result_dict)
@ -489,116 +453,126 @@ class Vertex:
if isinstance(self._built_object, str):
self._built_result = self._built_object
result = await generate_result(
self._built_object, inputs, self.has_external_output, session_id
)
result = await generate_result(self._built_object, inputs, self.has_external_output, session_id)
self._built_result = result
async def _build_each_node_in_params_dict(self, user_id=None):
async def _build_each_vertex_in_params_dict(self, user_id=None):
"""
Iterates over each node in the params dictionary and builds it.
Iterates over each vertex in the params dictionary and builds it.
"""
for key, value in self._raw_params.items():
if self._is_node(value):
if self._is_vertex(value):
if value == self:
del self.params[key]
continue
await self._build_node_and_update_params(key, value, user_id)
elif isinstance(value, list) and self._is_list_of_nodes(value):
await self._build_list_of_nodes_and_update_params(key, value, user_id)
await self._build_vertex_and_update_params(
key,
value,
)
elif isinstance(value, list) and self._is_list_of_vertices(value):
await self._build_list_of_vertices_and_update_params(key, value)
elif isinstance(value, dict):
await self._build_dict_and_update_params(key, value, user_id)
await self._build_dict_and_update_params(
key,
value,
)
elif key not in self.params or self.updated_raw_params:
self.params[key] = value
async def _build_dict_and_update_params(
self, key, nodes_dict: Dict[str, "Vertex"], user_id=None
self,
key,
vertices_dict: Dict[str, "Vertex"],
):
"""
Iterates over a dictionary of nodes, builds each and updates the params dictionary.
Iterates over a dictionary of vertices, builds each and updates the params dictionary.
"""
for sub_key, value in nodes_dict.items():
if not self._is_node(value):
for sub_key, value in vertices_dict.items():
if not self._is_vertex(value):
self.params[key][sub_key] = value
else:
built = await value.get_result(requester=self, user_id=user_id)
self.params[key][sub_key] = built
result = await value.get_result()
self.params[key][sub_key] = result
def _is_node(self, value):
def _is_vertex(self, value):
"""
Checks if the provided value is an instance of Vertex.
"""
return isinstance(value, Vertex)
def _is_list_of_nodes(self, value):
def _is_list_of_vertices(self, value):
"""
Checks if the provided value is a list of Vertex instances.
"""
return all(self._is_node(node) for node in value)
return all(self._is_vertex(vertex) for vertex in value)
async def get_result(
self, requester: Optional["Vertex"] = None, user_id=None, timeout=None
self,
) -> Any:
# PLEASE REVIEW THIS IF STATEMENT
# Check if the Vertex was built already
if self._built:
return self._built_object if not self.use_result else self._built_result
if self.is_task and self.task_id is not None:
task = self.get_task()
result = task.get(timeout=timeout)
if isinstance(result, Coroutine):
result = await result
if result is not None: # If result is ready
self._update_built_object_and_artifacts(result)
return self._built_object
else:
# Handle the case when the result is not ready (retry, throw exception, etc.)
pass
# If there's no task_id, build the vertex locally
await self.build(requester=requester, user_id=user_id)
return self._built_object
async def _build_node_and_update_params(self, key, node: "Vertex", user_id=None):
"""
Builds a given node and updates the params dictionary accordingly.
Retrieves the result of the vertex.
This is a read-only method so it raises an error if the vertex has not been built yet.
Returns:
The result of the vertex.
"""
async with self._lock:
return await self._get_result()
async def _get_result(self) -> Any:
"""
Retrieves the result of the built component.
If the component has not been built yet, a ValueError is raised.
Returns:
The built result if use_result is True, else the built object.
"""
if not self._built:
raise ValueError(f"Component {self.display_name} has not been built yet")
return self._built_result if self.use_result else self._built_object
async def _build_vertex_and_update_params(self, key, vertex: "Vertex"):
"""
Builds a given vertex and updates the params dictionary accordingly.
"""
result = await node.get_result(requester=self, user_id=user_id)
result = await vertex.get_result()
self._handle_func(key, result)
if isinstance(result, list):
self._extend_params_list_with_result(key, result)
self.params[key] = result
async def _build_list_of_nodes_and_update_params(
self, key, nodes: List["Vertex"], user_id=None
async def _build_list_of_vertices_and_update_params(
self,
key,
vertices: List["Vertex"],
):
"""
Iterates over a list of nodes, builds each and updates the params dictionary.
Iterates over a list of vertices, builds each and updates the params dictionary.
"""
self.params[key] = []
for node in nodes:
built = await node.get_result(requester=self, user_id=user_id)
for vertex in vertices:
result = await vertex.get_result()
# 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):
self.params[key].extend(built)
if isinstance(result, list):
self.params[key].extend(result)
else:
try:
if self.params[key] == built:
if self.params[key] == result:
continue
self.params[key].append(built)
self.params[key].append(result)
except AttributeError as e:
logger.exception(e)
raise ValueError(
f"Params {key} ({self.params[key]}) is not a list and cannot be extended with {built}"
f"Error building node {self.display_name}: {str(e)}"
f"Params {key} ({self.params[key]}) is not a list and cannot be extended with {result}"
f"Error building vertex {self.display_name}: {str(e)}"
) from e
def _handle_func(self, key, result):
@ -628,12 +602,9 @@ class Vertex:
Gets the class from a dictionary and instantiates it with the params.
"""
if self.base_type is None:
raise ValueError(f"Base type for node {self.display_name} not found")
raise ValueError(f"Base type for vertex {self.display_name} not found")
try:
result = await loading.instantiate_class(
node_type=self.vertex_type,
base_type=self.base_type,
params=self.params,
user_id=user_id,
vertex=self,
)
@ -641,9 +612,7 @@ class Vertex:
except Exception as exc:
logger.exception(exc)
raise ValueError(
f"Error building node {self.display_name}: {str(exc)}"
) from exc
raise ValueError(f"Error building vertex {self.display_name}: {str(exc)}") from exc
def _update_built_object_and_artifacts(self, result):
"""
@ -671,9 +640,7 @@ class Vertex:
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."
)
raise ValueError(f"You are trying to stream to a {self.display_name}. Try using a Chat Output instead.")
def _reset(self, params_update: Optional[Dict[str, Any]] = None):
self._built = False
@ -699,33 +666,34 @@ class Vertex:
requester: Optional["Vertex"] = None,
**kwargs,
) -> Any:
if self.state == VertexStates.INACTIVE:
# If the vertex is inactive, return None
self.build_inactive()
return
async with self._lock:
if self.state == VertexStates.INACTIVE:
# If the vertex is inactive, return None
self.build_inactive()
return
if self.frozen and self._built:
return self.get_requester_result(requester)
elif self._built and requester is not None:
# This means that the vertex has already been built
# and we are just getting the result for the requester
return await self.get_requester_result(requester)
self._reset()
if self.frozen and self._built:
return self.get_requester_result(requester)
elif self._built and requester is not None:
# This means that the vertex has already been built
# and we are just getting the result for the requester
return await self.get_requester_result(requester)
self._reset()
if self._is_chat_input() and inputs:
inputs = {"input_value": inputs.get(INPUT_FIELD_NAME, "")}
self.update_raw_params(inputs, overwrite=True)
if self._is_chat_input() and inputs:
inputs = {"input_value": inputs.get(INPUT_FIELD_NAME, "")}
self.update_raw_params(inputs, overwrite=True)
# Run steps
for step in self.steps:
if step not in self.steps_ran:
if inspect.iscoroutinefunction(step):
await step(user_id=user_id, **kwargs)
else:
step(user_id=user_id, **kwargs)
self.steps_ran.append(step)
# Run steps
for step in self.steps:
if step not in self.steps_ran:
if inspect.iscoroutinefunction(step):
await step(user_id=user_id, **kwargs)
else:
step(user_id=user_id, **kwargs)
self.steps_ran.append(step)
self._finalize_build()
self._finalize_build()
return await self.get_requester_result(requester)
@ -736,14 +704,12 @@ class Vertex:
return self._built_object
# Get the requester edge
requester_edge = next(
(edge for edge in self.edges if edge.target_id == requester.id), None
)
requester_edge = next((edge for edge in self.edges if edge.target_id == requester.id), None)
# Return the result of the requester edge
return (
None
if requester_edge is None
else await requester_edge.get_result(source=self, target=requester)
else await requester_edge.get_result_from_source(source=self, target=requester)
)
def add_edge(self, edge: "ContractEdge") -> None:
@ -751,9 +717,7 @@ class Vertex:
self.edges.append(edge)
def __repr__(self) -> str:
return (
f"Vertex(display_name={self.display_name}, id={self.id}, data={self.data})"
)
return f"Vertex(display_name={self.display_name}, id={self.id}, data={self.data})"
def __eq__(self, __o: object) -> bool:
try:
@ -774,8 +738,4 @@ class Vertex:
def _built_object_repr(self):
# Add a message with an emoji, stars for sucess,
return (
"Built sucessfully ✨"
if self._built_object is not None
else "Failed to build 😵‍💫"
)
return "Built sucessfully ✨" if self._built_object is not None else "Failed to build 😵‍💫"

View file

@ -124,11 +124,9 @@ class DocumentLoaderVertex(Vertex):
# show how many documents are in the list?
if not isinstance(self._built_object, UnbuiltObject):
avg_length = sum(
len(record.text)
for record in self._built_object
if hasattr(record, "text")
) / len(self._built_object)
avg_length = sum(len(record.text) for record in self._built_object if hasattr(record, "text")) / len(
self._built_object
)
return f"""{self.display_name}({len(self._built_object)} records)
\nAvg. Record Length (characters): {int(avg_length)}
Records: {self._built_object[:3]}..."""
@ -201,9 +199,7 @@ class TextSplitterVertex(Vertex):
# show how many documents are in the list?
if not isinstance(self._built_object, UnbuiltObject):
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(
self._built_object
)
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(self._built_object)
return f"""{self.vertex_type}({len(self._built_object)} documents)
\nAvg. Document Length (characters): {int(avg_length)}
\nDocuments: {self._built_object[:3]}..."""
@ -250,27 +246,18 @@ class PromptVertex(Vertex):
user_id = kwargs.get("user_id", None)
tools = kwargs.get("tools", [])
if not self._built or force:
if (
"input_variables" not in self.params
or self.params["input_variables"] is None
):
if "input_variables" not in self.params or self.params["input_variables"] is None:
self.params["input_variables"] = []
# Check if it is a ZeroShotPrompt and needs a tool
if "ShotPrompt" in self.vertex_type:
tools = (
[tool_node.build(user_id=user_id) for tool_node in tools]
if tools is not None
else []
)
tools = [tool_node.build(user_id=user_id) for tool_node in tools] if tools is not None else []
# flatten the list of tools if it is a list of lists
# first check if it is a list
if tools and isinstance(tools, list) and isinstance(tools[0], list):
tools = flatten_list(tools)
self.params["tools"] = tools
prompt_params = [
key
for key, value in self.params.items()
if isinstance(value, str) and key != "format_instructions"
key for key, value in self.params.items() if isinstance(value, str) and key != "format_instructions"
]
else:
prompt_params = ["template"]
@ -280,20 +267,14 @@ class PromptVertex(Vertex):
prompt_text = self.params[param]
variables = extract_input_variables_from_prompt(prompt_text)
self.params["input_variables"].extend(variables)
self.params["input_variables"] = list(
set(self.params["input_variables"])
)
self.params["input_variables"] = list(set(self.params["input_variables"]))
elif isinstance(self.params, dict):
self.params.pop("input_variables", None)
await self._build(user_id=user_id)
def _built_object_repr(self):
if (
not self.artifacts
or self._built_object is None
or not hasattr(self._built_object, "format")
):
if not self.artifacts or self._built_object is None or not hasattr(self._built_object, "format"):
return super()._built_object_repr()
elif isinstance(self._built_object, UnbuiltObject):
return super()._built_object_repr()
@ -305,9 +286,7 @@ class PromptVertex(Vertex):
# so the prompt format doesn't break
artifacts.pop("handle_keys", None)
try:
if not hasattr(self._built_object, "template") and hasattr(
self._built_object, "prompt"
):
if not hasattr(self._built_object, "template") and hasattr(self._built_object, "prompt"):
template = self._built_object.prompt.template
else:
template = self._built_object.template
@ -315,11 +294,7 @@ class PromptVertex(Vertex):
if value:
replace_key = "{" + key + "}"
template = template.replace(replace_key, value)
return (
template
if isinstance(template, str)
else f"{self.vertex_type}({template})"
)
return template if isinstance(template, str) else f"{self.vertex_type}({template})"
except KeyError:
return str(self._built_object)
@ -354,14 +329,8 @@ class ChatVertex(Vertex):
return f"Task {self.task_id} is not running"
if self.artifacts:
# dump as a yaml string
artifacts = {
k.title().replace("_", " "): v
for k, v in self.artifacts.items()
if v is not None
}
yaml_str = yaml.dump(
artifacts, default_flow_style=False, allow_unicode=True
)
artifacts = {k.title().replace("_", " "): v for k, v in self.artifacts.items() if v is not None}
yaml_str = yaml.dump(artifacts, default_flow_style=False, allow_unicode=True)
return yaml_str
return super()._built_object_repr()

View file

@ -366,9 +366,10 @@ class CustomComponent(Component):
if not flow_id:
raise ValueError(f"Flow {flow_name} not found")
if isinstance(input_value, str):
input_value = [input_value]
graph = await self.load_flow(flow_id, tweaks)
input_value_dict = [{"input_value": input_value}]
input_value_dict = [{"input_value": input_val} for input_val in input_value]
return await graph.run(input_value_dict, stream=False)
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Record]:

View file

@ -3,7 +3,7 @@ import contextlib
import re
import traceback
import warnings
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional, Tuple, Union
from uuid import UUID
from fastapi import HTTPException
@ -155,6 +155,9 @@ def add_new_custom_field(
field_value = field_config.pop("value", field_value)
field_advanced = field_config.pop("advanced", False)
if field_type == "Dict":
field_type = "dict"
if field_type == "bool" and field_value is None:
field_value = False
@ -240,14 +243,12 @@ def get_field_dict(field: Union[TemplateField, dict]):
def run_build_config(
custom_component: CustomComponent,
user_id: Optional[Union[str, UUID]] = None,
update_field=None,
update_field_value=None,
):
) -> Tuple[dict, CustomComponent]:
"""Build the field configuration for a custom component"""
try:
if custom_component.code is None:
return {}
raise ValueError("Code is None")
elif isinstance(custom_component.code, str):
custom_class = eval_custom_component_code(custom_component.code)
else:
@ -271,38 +272,6 @@ def run_build_config(
# as a dict with the same keys as TemplateField
field_dict = get_field_dict(field)
build_config[field_name] = field_dict
# This has to be done to set refresh if options or value are callable
if update_field is not None and field_name != update_field:
build_config = update_field_dict(
custom_component_instance=custom_instance,
field_dict=field_dict,
build_config=build_config,
call=False,
)
continue
try:
build_config = update_field_dict(
custom_component_instance=custom_instance,
field_dict=field_dict,
build_config=build_config,
update_field=update_field,
update_field_value=update_field_value,
call=True,
)
build_config[field_name] = field_dict
except Exception as exc:
logger.error(f"Error while getting build_config: {str(exc)}")
if isinstance(exc, UpdateBuildConfigError):
message = str(exc)
else:
message = f"Error while getting build_config: {str(exc)}"
raise HTTPException(
status_code=400,
detail={
"error": message,
"traceback": traceback.format_exc(),
},
) from exc
return build_config, custom_instance
@ -355,9 +324,7 @@ def add_code_field(frontend_node: CustomComponentFrontendNode, raw_code, field_c
def build_custom_component_template(
custom_component: CustomComponent,
user_id: Optional[Union[str, UUID]] = None,
update_field: Optional[str] = None,
update_field_value: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
) -> Tuple[Dict[str, Any], CustomComponent]:
"""Build a custom component template for the langchain"""
try:
frontend_node = build_frontend_node(custom_component.template_config)
@ -365,8 +332,6 @@ def build_custom_component_template(
field_config, custom_instance = run_build_config(
custom_component,
user_id=user_id,
update_field=update_field,
update_field_value=update_field_value,
)
entrypoint_args = custom_component.get_function_entrypoint_args
@ -380,7 +345,7 @@ def build_custom_component_template(
reorder_fields(frontend_node, custom_instance._get_field_order())
return frontend_node.to_dict(add_name=False)
return frontend_node.to_dict(add_name=False), custom_instance
except Exception as exc:
if isinstance(exc, HTTPException):
raise exc
@ -400,7 +365,7 @@ def create_component_template(component):
component_extractor = CustomComponent(code=component_code)
component_template = build_custom_component_template(component_extractor)
component_template, _ = build_custom_component_template(component_extractor)
if not component_template["output_types"] and component_output_types:
component_template["output_types"] = component_output_types

View file

@ -1,6 +1,6 @@
import inspect
import json
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Sequence, Type
from typing import TYPE_CHECKING, Any, Callable, Dict, Sequence, Type
import orjson
from langchain.agents import agent as agent_module
@ -40,27 +40,29 @@ if TYPE_CHECKING:
async def instantiate_class(
node_type: str,
base_type: str,
params: Dict,
vertex: "Vertex",
user_id=None,
vertex: Optional["Vertex"] = None,
) -> Any:
"""Instantiate class from module type and key, and params"""
vertex_type = vertex.vertex_type
base_type = vertex.base_type
params = vertex.params
params = convert_params_to_sets(params)
params = convert_kwargs(params)
if node_type in CUSTOM_NODES:
if custom_node := CUSTOM_NODES.get(node_type):
if vertex_type in CUSTOM_NODES:
if custom_node := CUSTOM_NODES.get(vertex_type):
if hasattr(custom_node, "initialize"):
return custom_node.initialize(**params)
return custom_node(**params)
logger.debug(f"Instantiating {node_type} of type {base_type}")
class_object = import_by_type(_type=base_type, name=node_type)
logger.debug(f"Instantiating {vertex_type} of type {base_type}")
if not base_type:
raise ValueError("No base type provided for vertex")
class_object = import_by_type(_type=base_type, name=vertex_type)
return await instantiate_based_on_type(
class_object=class_object,
base_type=base_type,
node_type=node_type,
node_type=vertex_type,
params=params,
user_id=user_id,
vertex=vertex,

View file

@ -17,9 +17,7 @@ CUSTOM_TOOLS = {
"PythonFunctionTool": PythonFunctionTool,
}
OTHER_TOOLS = {
tool: import_class(f"langchain_community.tools.{tool}") for tool in tools.__all__
}
OTHER_TOOLS = {tool: import_class(f"langchain_community.tools.{tool}") for tool in tools.__all__}
ALL_TOOLS_NAMES = {
**_BASE_TOOLS,

View file

@ -11,13 +11,14 @@ from loguru import logger
from pydantic import BaseModel
from langflow.graph.graph.base import Graph
from langflow.graph.schema import INPUT_FIELD_NAME, RunOutputs
from langflow.graph.vertex.base import Vertex
from langflow.interface.custom.custom_component import CustomComponent
from langflow.interface.run import get_memory_key, update_memory_keys
from langflow.services.session.service import SessionService
if TYPE_CHECKING:
from langflow.api.v1.schemas import Tweaks
from langflow.api.v1.schemas import InputValueRequest, Tweaks
def fix_memory_inputs(langchain_object):
@ -200,31 +201,43 @@ async def run_graph(
flow_id: str,
stream: bool,
session_id: Optional[str] = None,
inputs: Optional[list[dict[str, Union[List[str], str]]]] = None,
inputs: Optional[List["InputValueRequest"]] = None,
outputs: Optional[List[str]] = None,
artifacts: Optional[Dict[str, Any]] = None,
session_service: Optional[SessionService] = None,
):
) -> tuple[List[RunOutputs], str]:
"""Run the graph and generate the result"""
inputs = inputs or []
if isinstance(graph, dict):
graph_data = graph
graph = Graph.from_payload(graph, flow_id=flow_id)
else:
graph_data = graph._graph_data
if session_id is None and session_service is not None:
session_id = session_service.generate_key(session_id=flow_id, data_graph=graph_data)
if inputs is None:
inputs = [{}]
session_id_str = session_service.generate_key(session_id=flow_id, data_graph=graph_data)
elif session_id is not None:
session_id_str = session_id
else:
raise ValueError("session_id or session_service must be provided")
components = []
inputs_list = []
for input_value_request in inputs:
if input_value_request.input_value is None:
logger.warning("InputValueRequest input_value cannot be None, defaulting to an empty string.")
input_value_request.input_value = ""
components.append(input_value_request.components or [])
inputs_list.append({INPUT_FIELD_NAME: input_value_request.input_value})
run_outputs = await graph.run(
inputs,
inputs_list,
components,
outputs or [],
stream=stream,
session_id=session_id or "",
session_id=session_id_str or "",
)
if session_id and session_service:
session_service.update_session(session_id, (graph, artifacts))
return run_outputs, session_id
if session_id_str and session_service:
session_service.update_session(session_id_str, (graph, artifacts))
return run_outputs, session_id_str
def validate_input(
@ -272,6 +285,9 @@ def process_tweaks(graph_data: Dict[str, Any], tweaks: Union["Tweaks", Dict[str,
:raises ValueError: If the input is not in the expected format.
"""
if not isinstance(tweaks, dict):
tweaks = tweaks.model_dump()
nodes = validate_input(graph_data, tweaks)
nodes_map = {node.get("id"): node for node in nodes}

View file

@ -16,7 +16,7 @@ class Record(BaseModel):
_default_value: str = ""
@model_validator(mode="before")
def validate_data(values):
def validate_data(cls, values):
if not values.get("data"):
values["data"] = {}
# Any other keyword should be added to the data dictionary
@ -73,9 +73,7 @@ class Record(BaseModel):
return self.data.get(key, self._default_value)
except KeyError:
# Fallback to default behavior to raise AttributeError for undefined attributes
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{key}'"
)
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{key}'")
def __setattr__(self, key, value):
"""

View file

@ -1,9 +1,17 @@
from . import factory, service
from langflow.services.cache.service import InMemoryCache
from langflow.services.cache.service import (
AsyncInMemoryCache,
BaseCacheService,
RedisCache,
ThreadingInMemoryCache,
)
from . import factory, service
__all__ = [
"factory",
"service",
"InMemoryCache",
"ThreadingInMemoryCache",
"AsyncInMemoryCache",
"BaseCacheService",
"RedisCache",
]

View file

@ -1,4 +1,7 @@
import abc
import asyncio
import threading
from typing import Optional
from langflow.services.base import Service
@ -11,7 +14,7 @@ class BaseCacheService(Service):
name = "cache_service"
@abc.abstractmethod
def get(self, key):
def get(self, key, lock: Optional[threading.Lock] = None):
"""
Retrieve an item from the cache.
@ -23,7 +26,7 @@ class BaseCacheService(Service):
"""
@abc.abstractmethod
def set(self, key, value):
def set(self, key, value, lock: Optional[threading.Lock] = None):
"""
Add an item to the cache.
@ -33,7 +36,7 @@ class BaseCacheService(Service):
"""
@abc.abstractmethod
def upsert(self, key, value):
def upsert(self, key, value, lock: Optional[threading.Lock] = None):
"""
Add an item to the cache if it doesn't exist, or update it if it does.
@ -43,7 +46,7 @@ class BaseCacheService(Service):
"""
@abc.abstractmethod
def delete(self, key):
def delete(self, key, lock: Optional[threading.Lock] = None):
"""
Remove an item from the cache.
@ -52,7 +55,7 @@ class BaseCacheService(Service):
"""
@abc.abstractmethod
def clear(self):
def clear(self, lock: Optional[threading.Lock] = None):
"""
Clear all items from the cache.
"""
@ -96,3 +99,70 @@ class BaseCacheService(Service):
Args:
key: The key of the item to remove.
"""
class AsyncBaseCacheService(Service):
"""
Abstract base class for a async cache.
"""
name = "cache_service"
@abc.abstractmethod
async def get(self, key, lock: Optional[asyncio.Lock] = None):
"""
Retrieve an item from the cache.
Args:
key: The key of the item to retrieve.
Returns:
The value associated with the key, or None if the key is not found.
"""
@abc.abstractmethod
async def set(self, key, value, lock: Optional[asyncio.Lock] = None):
"""
Add an item to the cache.
Args:
key: The key of the item.
value: The value to cache.
"""
@abc.abstractmethod
async def upsert(self, key, value, lock: Optional[asyncio.Lock] = None):
"""
Add an item to the cache if it doesn't exist, or update it if it does.
Args:
key: The key of the item.
value: The value to cache.
"""
@abc.abstractmethod
async def delete(self, key, lock: Optional[asyncio.Lock] = None):
"""
Remove an item from the cache.
Args:
key: The key of the item to remove.
"""
@abc.abstractmethod
async def clear(self, lock: Optional[asyncio.Lock] = None):
"""
Clear all items from the cache.
"""
@abc.abstractmethod
def __contains__(self, key):
"""
Check if the key is in the cache.
Args:
key: The key of the item to check.
Returns:
True if the key is in the cache, False otherwise.
"""

View file

@ -1,6 +1,11 @@
from typing import TYPE_CHECKING
from langflow.services.cache.service import BaseCacheService, InMemoryCache, RedisCache
from langflow.services.cache.service import (
AsyncInMemoryCache,
BaseCacheService,
RedisCache,
ThreadingInMemoryCache,
)
from langflow.services.factory import ServiceFactory
from langflow.utils.logger import logger
@ -29,7 +34,9 @@ class CacheServiceFactory(ServiceFactory):
logger.debug("Redis cache is connected")
return redis_cache
logger.warning("Redis cache is not connected, falling back to in-memory cache")
return InMemoryCache()
return ThreadingInMemoryCache()
elif settings_service.settings.CACHE_TYPE == "memory":
return InMemoryCache()
return ThreadingInMemoryCache()
elif settings_service.settings.CACHE_TYPE == "async":
return AsyncInMemoryCache()

View file

@ -1,16 +1,17 @@
import asyncio
import pickle
import threading
import time
from collections import OrderedDict
from typing import Optional
from loguru import logger
from langflow.services.base import Service
from langflow.services.cache.base import BaseCacheService
from langflow.services.cache.base import AsyncBaseCacheService, BaseCacheService
class InMemoryCache(BaseCacheService, Service):
class ThreadingInMemoryCache(BaseCacheService, Service):
"""
A simple in-memory cache using an OrderedDict.
@ -49,7 +50,7 @@ class InMemoryCache(BaseCacheService, Service):
self.max_size = max_size
self.expiration_time = expiration_time
def get(self, key):
def get(self, key, lock: Optional[threading.Lock] = None):
"""
Retrieve an item from the cache.
@ -59,7 +60,7 @@ class InMemoryCache(BaseCacheService, Service):
Returns:
The value associated with the key, or None if the key is not found or the item has expired.
"""
with self._lock:
with lock or self._lock:
return self._get_without_lock(key)
def _get_without_lock(self, key):
@ -80,7 +81,7 @@ class InMemoryCache(BaseCacheService, Service):
self.delete(key)
return None
def set(self, key, value, pickle=False):
def set(self, key, value, lock: Optional[threading.Lock] = None):
"""
Add an item to the cache.
@ -90,7 +91,7 @@ class InMemoryCache(BaseCacheService, Service):
key: The key of the item.
value: The value to cache.
"""
with self._lock:
with lock or self._lock:
if key in self._cache:
# Remove existing key before re-inserting to update order
self.delete(key)
@ -98,12 +99,10 @@ class InMemoryCache(BaseCacheService, Service):
# Remove least recently used item
self._cache.popitem(last=False)
# pickle locally to mimic Redis
if pickle:
value = pickle.dumps(value)
self._cache[key] = {"value": value, "time": time.time()}
def upsert(self, key, value):
def upsert(self, key, value, lock: Optional[threading.Lock] = None):
"""
Inserts or updates a value in the cache.
If the existing value and the new value are both dictionaries, they are merged.
@ -112,7 +111,7 @@ class InMemoryCache(BaseCacheService, Service):
key: The key of the item.
value: The value to insert or update.
"""
with self._lock:
with lock or self._lock:
existing_value = self._get_without_lock(key)
if existing_value is not None and isinstance(existing_value, dict) and isinstance(value, dict):
existing_value.update(value)
@ -120,7 +119,7 @@ class InMemoryCache(BaseCacheService, Service):
self.set(key, value)
def get_or_set(self, key, value):
def get_or_set(self, key, value, lock: Optional[threading.Lock] = None):
"""
Retrieve an item from the cache. If the item does not exist,
set it with the provided value.
@ -132,27 +131,27 @@ class InMemoryCache(BaseCacheService, Service):
Returns:
The cached value associated with the key.
"""
with self._lock:
with lock or self._lock:
if key in self._cache:
return self.get(key)
self.set(key, value)
return value
def delete(self, key):
def delete(self, key, lock: Optional[threading.Lock] = None):
"""
Remove an item from the cache.
Args:
key: The key of the item to remove.
"""
with self._lock:
with lock or self._lock:
self._cache.pop(key, None)
def clear(self):
def clear(self, lock: Optional[threading.Lock] = None):
"""
Clear all items from the cache.
"""
with self._lock:
with lock or self._lock:
self._cache.clear()
def __contains__(self, key):
@ -323,3 +322,85 @@ class RedisCache(BaseCacheService, Service):
def __repr__(self):
"""Return a string representation of the RedisCache instance."""
return f"RedisCache(expiration_time={self.expiration_time})"
class AsyncInMemoryCache(AsyncBaseCacheService, Service):
def __init__(self, max_size=None, expiration_time=3600):
self.cache = OrderedDict()
self.lock = asyncio.Lock()
self.max_size = max_size
self.expiration_time = expiration_time
async def get(self, key, lock: Optional[asyncio.Lock] = None):
if not lock:
async with self.lock:
return await self._get(key)
else:
return await self._get(key)
async def _get(self, key):
item = self.cache.get(key, None)
if item and (time.time() - item["time"] < self.expiration_time):
self.cache.move_to_end(key)
return pickle.loads(item["value"]) if isinstance(item["value"], bytes) else item["value"]
if item:
await self.delete(key)
return None
async def set(self, key, value, lock: Optional[asyncio.Lock] = None):
if not lock:
async with self.lock:
await self._set(
key,
value,
)
else:
await self._set(
key,
value,
)
async def _set(self, key, value):
if self.max_size and len(self.cache) >= self.max_size:
self.cache.popitem(last=False)
self.cache[key] = {"value": value, "time": time.time()}
self.cache.move_to_end(key)
async def delete(self, key, lock: Optional[asyncio.Lock] = None):
if not lock:
async with self.lock:
await self._delete(key)
else:
await self._delete(key)
async def _delete(self, key):
if key in self.cache:
del self.cache[key]
async def clear(self, lock: Optional[asyncio.Lock] = None):
if not lock:
async with self.lock:
await self._clear()
else:
await self._clear()
async def _clear(self):
self.cache.clear()
async def upsert(self, key, value, lock: Optional[asyncio.Lock] = None):
if not lock:
async with self.lock:
await self._upsert(key, value)
else:
await self._upsert(key, value)
async def _upsert(self, key, value):
existing_value = await self.get(key)
if existing_value is not None and isinstance(existing_value, dict) and isinstance(value, dict):
existing_value.update(value)
value = existing_value
await self.set(key, value)
def __contains__(self, key):
return key in self.cache

View file

@ -1,4 +1,6 @@
from typing import Any
import asyncio
from collections import defaultdict
from typing import Any, Optional
from langflow.services.base import Service
from langflow.services.deps import get_cache_service
@ -8,30 +10,30 @@ class ChatService(Service):
name = "chat_service"
def __init__(self):
self._cache_locks = defaultdict(asyncio.Lock)
self.cache_service = get_cache_service()
def set_cache(self, client_id: str, data: Any) -> bool:
async def set_cache(self, flow_id: str, data: Any, lock: Optional[asyncio.Lock] = None) -> bool:
"""
Set the cache for a client.
"""
# client_id is the flow id but that already exists in the cache
# so we need to change it to something else
result_dict = {
"result": data,
"type": type(data),
}
self.cache_service.upsert(client_id, result_dict)
return client_id in self.cache_service
await self.cache_service.upsert(flow_id, result_dict, lock=lock or self._cache_locks[flow_id])
return flow_id in self.cache_service
def get_cache(self, client_id: str) -> Any:
async def get_cache(self, flow_id: str, lock: Optional[asyncio.Lock] = None) -> Any:
"""
Get the cache for a client.
"""
return self.cache_service.get(client_id)
return await self.cache_service.get(flow_id, lock=lock or self._cache_locks[flow_id])
def clear_cache(self, client_id: str):
async def clear_cache(self, flow_id: str, lock: Optional[asyncio.Lock] = None):
"""
Clear the cache for a client.
"""
self.cache_service.delete(client_id)
self.cache_service.delete(flow_id, lock=lock or self._cache_locks[flow_id])

View file

@ -1,4 +1,3 @@
import datetime
import secrets
from pathlib import Path
from typing import Optional
@ -37,7 +36,7 @@ class AuthSettings(BaseSettings):
NEW_USER_IS_ACTIVE: bool = False
SUPERUSER: str = DEFAULT_SUPERUSER
SUPERUSER_PASSWORD: str = DEFAULT_SUPERUSER_PASSWORD
REFRESH_SAME_SITE: str = "none"
"""The SameSite attribute of the refresh token cookie."""
REFRESH_SECURE: bool = True

View file

@ -38,7 +38,7 @@ class Settings(BaseSettings):
DEV: bool = False
DATABASE_URL: Optional[str] = None
CACHE_TYPE: str = "memory"
CACHE_TYPE: str = "async"
REMOVE_API_KEYS: bool = False
COMPONENTS_PATH: List[str] = []
LANGCHAIN_CACHE: str = "InMemoryCache"

View file

@ -20,12 +20,8 @@ def remove_ansi_escape_codes(text):
return re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", text)
def build_template_from_function(
name: str, type_to_loader_dict: Dict, add_function: bool = False
):
classes = [
item.__annotations__["return"].__name__ for item in type_to_loader_dict.values()
]
def build_template_from_function(name: str, type_to_loader_dict: Dict, add_function: bool = False):
classes = [item.__annotations__["return"].__name__ for item in type_to_loader_dict.values()]
# Raise error if name is not in chains
if name not in classes:
@ -46,10 +42,8 @@ def build_template_from_function(
for name_, value_ in value.__repr_args__():
if name_ == "default_factory":
try:
variables[class_field_items]["default"] = (
get_default_factory(
module=_class.__base__.__module__, function=value_
)
variables[class_field_items]["default"] = get_default_factory(
module=_class.__base__.__module__, function=value_
)
except Exception:
variables[class_field_items]["default"] = None
@ -57,9 +51,7 @@ def build_template_from_function(
variables[class_field_items][name_] = value_
variables[class_field_items]["placeholder"] = (
docs.params[class_field_items]
if class_field_items in docs.params
else ""
docs.params[class_field_items] if class_field_items in docs.params else ""
)
# Adding function to base classes to allow
# the output to be a function
@ -74,9 +66,7 @@ def build_template_from_function(
}
def build_template_from_class(
name: str, type_to_cls_dict: Dict, add_function: bool = False
):
def build_template_from_class(name: str, type_to_cls_dict: Dict, add_function: bool = False):
classes = [item.__name__ for item in type_to_cls_dict.values()]
# Raise error if name is not in chains
@ -100,11 +90,9 @@ def build_template_from_class(
for name_, value_ in value.__repr_args__():
if name_ == "default_factory":
try:
variables[class_field_items]["default"] = (
get_default_factory(
module=_class.__base__.__module__,
function=value_,
)
variables[class_field_items]["default"] = get_default_factory(
module=_class.__base__.__module__,
function=value_,
)
except Exception:
variables[class_field_items]["default"] = None
@ -112,9 +100,7 @@ def build_template_from_class(
variables[class_field_items][name_] = value_
variables[class_field_items]["placeholder"] = (
docs.params[class_field_items]
if class_field_items in docs.params
else ""
docs.params[class_field_items] if class_field_items in docs.params else ""
)
base_classes = get_base_classes(_class)
# Adding function to base classes to allow
@ -146,9 +132,7 @@ def build_template_from_method(
# Check if the method exists in this class
if not hasattr(_class, method_name):
raise ValueError(
f"Method {method_name} not found in class {class_name}"
)
raise ValueError(f"Method {method_name} not found in class {class_name}")
# Get the method
method = getattr(_class, method_name)
@ -167,14 +151,8 @@ def build_template_from_method(
"_type": _type,
**{
name: {
"default": (
param.default if param.default != param.empty else None
),
"type": (
param.annotation
if param.annotation != param.empty
else None
),
"default": (param.default if param.default != param.empty else None),
"type": (param.annotation if param.annotation != param.empty else None),
"required": param.default == param.empty,
}
for name, param in params.items()
@ -261,9 +239,7 @@ def sync_to_async(func):
return async_wrapper
def format_dict(
dictionary: Dict[str, Any], class_name: Optional[str] = None
) -> Dict[str, Any]:
def format_dict(dictionary: Dict[str, Any], class_name: Optional[str] = None) -> Dict[str, Any]:
"""
Formats a dictionary by removing certain keys and modifying the
values of other keys.
@ -349,9 +325,7 @@ def check_list_type(_type: str, value: Dict[str, Any]) -> str:
The modified type string.
"""
if any(list_type in _type for list_type in ["List", "Sequence", "Set"]):
_type = (
_type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1]
)
_type = _type.replace("List[", "").replace("Sequence[", "").replace("Set[", "")[:-1]
value["list"] = True
else:
value["list"] = False
@ -454,9 +428,7 @@ def set_headers_value(value: Dict[str, Any]) -> None:
value["value"] = """{"Authorization": "Bearer <token>"}"""
def add_options_to_field(
value: Dict[str, Any], class_name: Optional[str], key: str
) -> None:
def add_options_to_field(value: Dict[str, Any], class_name: Optional[str], key: str) -> None:
"""
Adds options to the field based on the class name and key.
"""

View file

@ -102,35 +102,46 @@ export default function App() {
)}
</ErrorBoundary>
<div></div>
<div className="app-div z-40">
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "error" ? (
<ErrorAlert
key={alert.id}
title={alert.title}
list={alert.list}
id={alert.id}
removeAlert={removeAlert}
/>
) : alert.type === "notice" ? (
<NoticeAlert
key={alert.id}
title={alert.title}
link={alert.link}
id={alert.id}
removeAlert={removeAlert}
/>
) : (
<SuccessAlert
key={alert.id}
title={alert.title}
id={alert.id}
removeAlert={removeAlert}
/>
)}
</div>
))}
<div className="app-div">
<div className="flex flex-col-reverse" style={{ zIndex: 999 }}>
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "error" && (
<ErrorAlert
key={alert.id}
title={alert.title}
list={alert.list}
id={alert.id}
removeAlert={removeAlert}
/>
)}
</div>
))}
</div>
<div className="z-40 flex flex-col-reverse">
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "notice" ? (
<NoticeAlert
key={alert.id}
title={alert.title}
link={alert.link}
id={alert.id}
removeAlert={removeAlert}
/>
) : (
alert.type === "success" && (
<SuccessAlert
key={alert.id}
title={alert.title}
id={alert.id}
removeAlert={removeAlert}
/>
)
)}
</div>
))}
</div>
</div>
</div>
);

View file

@ -1,5 +1,6 @@
import { cloneDeep } from "lodash";
import { useCallback, useEffect, useState } from "react";
import { NodeToolbar } from "reactflow";
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent";
import IconComponent from "../../components/genericIconComponent";
import InputComponent from "../../components/inputComponent";
@ -9,6 +10,7 @@ import Loading from "../../components/ui/loading";
import { Textarea } from "../../components/ui/textarea";
import Xmark from "../../components/ui/xmark";
import {
NATIVE_CATEGORIES,
RUN_TIMESTAMP_PREFIX,
STATUS_BUILD,
STATUS_BUILDING,
@ -20,6 +22,7 @@ import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useTypesStore } from "../../stores/typesStore";
import { APIClassType } from "../../types/api";
import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow";
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
@ -39,10 +42,12 @@ export default function GenericNode({
yPos: number;
}): JSX.Element {
const types = useTypesStore((state) => state.types);
const templates = useTypesStore((state) => state.templates);
const deleteNode = useFlowStore((state) => state.deleteNode);
const flowPool = useFlowStore((state) => state.flowPool);
const buildFlow = useFlowStore((state) => state.buildFlow);
const setNode = useFlowStore((state) => state.setNode);
const updateNodeInternals = useUpdateNodeInternals();
const setErrorData = useAlertStore((state) => state.setErrorData);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
const [inputName, setInputName] = useState(false);
@ -51,6 +56,7 @@ export default function GenericNode({
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
);
const [isOutdated, setIsOutdated] = useState(false);
const buildStatus = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.status
);
@ -65,6 +71,52 @@ export default function GenericNode({
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
useEffect(() => {
// This one should run only once
// first check if data.type in NATIVE_CATEGORIES
// if not return
if (
!NATIVE_CATEGORIES.includes(types[data.type]) ||
!data.node?.template?.code?.value
)
return;
const thisNodeTemplate = templates[data.type].template;
// if the template does not have a code key
// return
if (!thisNodeTemplate.code) return;
const currentCode = thisNodeTemplate.code?.value;
const thisNodesCode = data.node!.template?.code?.value;
if (currentCode !== thisNodesCode) {
setIsOutdated(true);
} else {
setIsOutdated(false);
}
// template.code can be undefined
}, [data.node?.template?.code?.value]);
const updateNodeCode = useCallback(
(newNodeClass: APIClassType, code: string, name: string) => {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
node: newNodeClass,
description: newNodeClass.description ?? data.node!.description,
display_name: newNodeClass.display_name ?? data.node!.display_name,
};
newNode.data.node.template[name].value = code;
setIsOutdated(false);
return newNode;
});
updateNodeInternals(data.id);
},
[data.id, data.node, setNode, setIsOutdated]
);
if (!data.node!.template) {
setErrorData({
title: `Error in component ${data.node!.display_name}`,
@ -305,6 +357,8 @@ export default function GenericNode({
showNode={showNode}
openAdvancedModal={false}
onCloseAdvancedModal={() => {}}
updateNodeCode={updateNodeCode}
isOutdated={isOutdated}
selected={selected}
></NodeToolbarComponent>
</NodeToolbar>

View file

@ -55,8 +55,6 @@ export default function CodeTabsComponent({
const dark = useDarkStore((state) => state.dark);
const unselectAll = useFlowStore((state) => state.unselectAll);
const setNodes = useFlowStore((state) => state.setNodes);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => {

View file

@ -16,7 +16,6 @@ import FlowSettingsModal from "../../../../modals/flowSettingsModal";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useStoreStore } from "../../../../stores/storeStore";
import { cn } from "../../../../utils/utils";
import ShadTooltip from "../../../ShadTooltipComponent";
import IconComponent from "../../../genericIconComponent";
@ -34,10 +33,8 @@ export const MenuBar = ({
const redo = useFlowsManagerStore((state) => state.redo);
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
const [openSettings, setOpenSettings] = useState(false);
const n = useFlowStore((state) => state.nodes);
const nodes = useFlowStore((state) => state.nodes);
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const hasApiKey = useStoreStore((state) => state.hasApiKey);
const validApiKey = useStoreStore((state) => state.validApiKey);
const navigate = useNavigate();
const isBuilding = useFlowStore((state) => state.isBuilding);
@ -46,7 +43,6 @@ export const MenuBar = ({
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
// saveFlowStyleInDataBase();
} catch (err) {
setErrorData(err as { title: string; list?: Array<string> });
}
@ -65,7 +61,7 @@ export const MenuBar = ({
<div className="round-button-div">
<button
onClick={() => {
removeFunction(n);
removeFunction(nodes);
navigate(-1);
}}
>

View file

@ -33,7 +33,7 @@ export default function Header(): JSX.Element {
const removeFlow = useFlowsManagerStore((store) => store.removeFlow);
const hasStore = useStoreStore((state) => state.hasStore);
const { id } = useParams();
const n = useFlowStore((state) => state.nodes);
const nodes = useFlowStore((state) => state.nodes);
const dark = useDarkStore((state) => state.dark);
const setDark = useDarkStore((state) => state.setDark);
@ -57,7 +57,7 @@ export default function Header(): JSX.Element {
return (
<div className="header-arrangement">
<div className="header-start-display lg:w-[30%]">
<Link to="/" onClick={() => checkForChanges(n)}>
<Link to="/" onClick={() => checkForChanges(nodes)}>
<span className="ml-4 text-2xl">⛓️</span>
</Link>
<MenuBar removeFunction={checkForChanges} />
@ -74,7 +74,7 @@ export default function Header(): JSX.Element {
}
size="sm"
onClick={() => {
checkForChanges(n);
checkForChanges(nodes);
}}
>
<IconComponent name="Home" className="h-4 w-4" />
@ -100,7 +100,7 @@ export default function Header(): JSX.Element {
variant={location.pathname === "/store" ? "primary" : "secondary"}
size="sm"
onClick={() => {
checkForChanges(n);
checkForChanges(nodes);
}}
>
<IconComponent name="Store" className="h-4 w-4" />

View file

@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 flex w-full max-w-lg translate-x-[-50%] translate-y-[-50%] flex-col 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",
"fixed z-50 flex w-full max-w-lg flex-col 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%] sm:rounded-lg md:w-full",
className
)}
{...props}

View file

@ -28,6 +28,9 @@ export default function RenameLabel(props) {
}
}
resizeInput();
return () => {
if(isRename) document.removeEventListener("keydown", () => {});
};
}, [isRename]);
const inputRef = useRef<HTMLInputElement | null>(null);

View file

@ -40,7 +40,7 @@ export const COMPONENTS_ERROR_ALERT = "Error getting components.";
// NOTICE
export const NOCHATOUTPUT_NOTICE_ALERT =
"There is no ChatOutput node in the flow.";
"There is no ChatOutput Component in the flow.";
export const API_WARNING_NOTICE_ALERT =
"Warning: Critical data, JSON file may include API keys.";
export const COPIED_NOTICE_ALERT = "API Key copied!";

View file

@ -740,6 +740,16 @@ export const PRIORITY_SIDEBAR_ORDER = [
"helpers",
"experimental",
];
export const NATIVE_CATEGORIES = [
"inputs",
"outputs",
"prompts",
"data",
"prompt",
"models",
"helpers",
"experimental",
];
/*
Data ingestion
Basic Prompting

View file

@ -1,7 +1,6 @@
import axios, { AxiosError, AxiosInstance } from "axios";
import { useContext, useEffect } from "react";
import { Cookies } from "react-cookie";
import { useNavigate } from "react-router-dom";
import { renewAccessToken } from ".";
import { BuildStatus } from "../../constants/enums";
import { AuthContext } from "../../contexts/authContext";
@ -17,26 +16,28 @@ function ApiInterceptor() {
const setErrorData = useAlertStore((state) => state.setErrorData);
let { accessToken, login, logout, authenticationErrorCount, autoLogin } =
useContext(AuthContext);
const navigate = useNavigate();
const cookies = new Cookies();
useEffect(() => {
const interceptor = api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (error.response?.status === 401) {
const accessToken = cookies.get("access_token_lf");
if (error.response?.status === 403 || error.response?.status === 401) {
if (!autoLogin) {
const stillRefresh = checkErrorCount();
if (!stillRefresh) {
return Promise.reject(error);
}
const acceptedRequest = await tryToRenewAccessToken(error);
if (accessToken && !autoLogin) {
checkErrorCount();
await tryToRenewAccessToken(error);
const accessToken = cookies.get("access_token_lf");
if (!accessToken && error?.config?.url?.includes("login")) {
return Promise.reject(error);
}
return acceptedRequest;
}
if (!accessToken && error?.config?.url?.includes("login")) {
return Promise.reject(error);
}
return logout();
}
await clearBuildVerticesState(error);
return Promise.reject(error);
@ -98,11 +99,15 @@ function ApiInterceptor() {
if (authenticationErrorCount > 3) {
authenticationErrorCount = 0;
logout();
return false;
}
return true;
}
async function tryToRenewAccessToken(error: AxiosError) {
try {
if (window.location.pathname.includes("/login")) return;
const res = await renewAccessToken();
if (res?.data?.access_token && res?.data?.refresh_token) {
login(res?.data?.access_token);
@ -116,7 +121,9 @@ function ApiInterceptor() {
return response;
}
} catch (error) {
clearBuildVerticesState(error);
logout();
return Promise.reject("Authentication error");
}
}

View file

@ -4,6 +4,7 @@ import { BASE_URL_API } from "../../constants/constants";
import { api } from "../../controllers/API/api";
import {
APIObjectType,
APITemplateType,
Component,
LoginType,
Users,
@ -369,11 +370,13 @@ export async function postCustomComponent(
export async function postCustomComponentUpdate(
code: string,
template: APITemplateType,
field: string,
field_value: any
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, {
code,
template,
field,
field_value,
});

View file

@ -9,7 +9,7 @@ const SvgBotMessageSquare = (props) => (
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-bot-message-square"
className="lucide lucide-bot-message-square"
{...props}
>
<path d="M12 6V2H8" />

View file

@ -23,7 +23,6 @@ import {
import { postCustomComponent, postValidateCode } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import { CodeErrorDataTypeAPI } from "../../types/api";
import { codeAreaModalPropsType } from "../../types/components";
import BaseModal from "../baseModal";
@ -45,14 +44,12 @@ export default function CodeAreaModal({
? [myOpen, mySetOpen]
: useState(false);
const dark = useDarkStore((state) => state.dark);
const unselectAll = useFlowStore((state) => state.unselectAll);
const [height, setHeight] = useState<string | null>(null);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const [error, setError] = useState<{
detail: CodeErrorDataTypeAPI;
} | null>(null);
const nodes = useFlowStore((state) => state.nodes);
useEffect(() => {
// if nodeClass.template has more fields other than code and dynamic is true

View file

@ -250,10 +250,20 @@ export default function ExtraSidebar(): JSX.Element {
setSearch(event.target.value);
}}
/>
<div className="search-icon">
<div
className="search-icon "
onClick={() => {
if (search) {
setFilterData(data);
setSearch("");
}
}}
>
<IconComponent
name="Search"
className={"h-5 w-5 stroke-[1.5] text-primary"}
name={search ? "X" : "Search"}
className={`h-5 w-5 stroke-[1.5] text-primary ${
search ? "cursor-pointer" : "cursor-default"
}`}
aria-hidden="true"
/>
</div>

View file

@ -10,6 +10,7 @@ import {
SelectItem,
SelectTrigger,
} from "../../../../components/ui/select-custom";
import { postCustomComponent } from "../../../../controllers/API";
import ConfirmationModal from "../../../../modals/ConfirmationModal";
import EditNodeModal from "../../../../modals/EditNodeModal";
import ShareModal from "../../../../modals/shareModal";
@ -18,6 +19,7 @@ import { useDarkStore } from "../../../../stores/darkStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useStoreStore } from "../../../../stores/storeStore";
import { useTypesStore } from "../../../../stores/typesStore";
import { APIClassType } from "../../../../types/api";
import { nodeToolbarPropsType } from "../../../../types/components";
import { FlowType } from "../../../../types/flow";
@ -28,6 +30,7 @@ import {
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { classNames, cn } from "../../../../utils/utils";
import ToolbarSelectItem from "./toolbarSelectItem";
export default function NodeToolbarComponent({
data,
@ -38,8 +41,10 @@ export default function NodeToolbarComponent({
showNode,
name = "code",
selected,
updateNodeCode,
setShowState,
onCloseAdvancedModal,
isOutdated,
}: nodeToolbarPropsType): JSX.Element {
const nodeLength = Object.keys(data.node!.template).filter(
(templateField) =>
@ -56,7 +61,7 @@ export default function NodeToolbarComponent({
data.node.template[templateField].type === "dict" ||
data.node.template[templateField].type === "NestedDict")
).length;
const templates = useTypesStore((state) => state.templates);
const hasStore = useStoreStore((state) => state.hasStore);
const hasApiKey = useStoreStore((state) => state.hasApiKey);
const validApiKey = useStoreStore((state) => state.validApiKey);
@ -70,6 +75,7 @@ export default function NodeToolbarComponent({
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const setNodes = useFlowStore((state) => state.setNodes);
const setEdges = useFlowStore((state) => state.setEdges);
const unselectAll = useFlowStore((state) => state.unselectAll);
const saveComponent = useFlowsManagerStore((state) => state.saveComponent);
@ -79,7 +85,6 @@ export default function NodeToolbarComponent({
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
const [showconfirmShare, setShowconfirmShare] = useState(false);
const [showOverrideModal, setShowOverrideModal] = useState(false);
const [flowComponent, setFlowComponent] = useState<FlowType>();
const openInNewTab = (url) => {
@ -170,6 +175,37 @@ export default function NodeToolbarComponent({
paneY: nodes.find((node) => node.id === data.id)?.position.y,
}
);
break;
case "update":
takeSnapshot();
// to update we must get the code from the templates in useTypesStore
const thisNodeTemplate = templates[data.type].template;
// if the template does not have a code key
// return
if (!thisNodeTemplate.code) return;
const currentCode = thisNodeTemplate.code.value;
if (data.node) {
postCustomComponent(currentCode, data.node)
.then((apiReturn) => {
const { data } = apiReturn;
if (data && updateNodeCode) {
updateNodeCode(data, currentCode, "code");
}
})
.catch((err) => {
console.log(err);
});
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...data,
};
newNode.data.node.template.code.value = currentCode;
return newNode;
});
}
break;
}
};
@ -411,93 +447,59 @@ export default function NodeToolbarComponent({
<SelectContent>
{nodeLength > 0 && (
<SelectItem value={nodeLength === 0 ? "disabled" : "advanced"}>
<div className="flex" data-testid="edit-button-modal">
<IconComponent
name="Settings2"
className="relative top-0.5 mr-2 h-4 w-4 "
/>{" "}
<span className="">Edit</span>{" "}
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
) : (
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">
Ctrl +{" "}
</span>
)}
<span className="absolute right-2 top-[0.46em]">E</span>
</div>
<ToolbarSelectItem
keyboardKey="E"
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={false}
value={"Edit"}
icon={"Settings2"}
/>
</SelectItem>
)}
<SelectItem value={"duplicate"}>
<div className="flex" data-testid="duplicate-button-modal">
<IconComponent
name="Copy"
className="relative top-0.5 mr-2 h-4 w-4"
/>
Duplicate
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
) : (
<span className="absolute right-[1.20rem] top-[0.40em] stroke-2">
Ctrl +{" "}
</span>
)}
<span className="absolute right-2 top-[0.4em]">D</span>
</div>{" "}
<ToolbarSelectItem
keyboardKey="D"
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={false}
value={"Duplicate"}
icon={"Copy"}
/>
</SelectItem>
<SelectItem value={"copy"}>
<div className="flex">
<IconComponent
name="Clipboard"
className="relative top-0.5 mr-2 h-4 w-4 "
/>{" "}
<span className="">Copy</span>{" "}
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
) : (
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">
Ctrl +{" "}
</span>
)}
<span className="absolute right-2 top-[0.4em]">C</span>
</div>
<ToolbarSelectItem
keyboardKey="C"
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={false}
value={"Copy"}
icon={"Clipboard"}
/>
</SelectItem>
{isOutdated && (
<SelectItem value={"update"}>
<ToolbarSelectItem
keyboardKey="U"
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={false}
value={"Update"}
icon={"Code"}
/>
</SelectItem>
)}
{hasStore && (
<SelectItem
value={"Share"}
disabled={!hasApiKey || !validApiKey}
>
<div className="flex" data-testid="share-button-modal">
<IconComponent
name="Share3"
className="relative top-0.5 -m-1 mr-1 h-6 w-6"
/>{" "}
Share{" "}
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
) : (
<span className="absolute right-[2.10rem] top-[0.43em] stroke-2">
Ctrl
</span>
)}
<IconComponent
name="ArrowBigUp"
className="absolute right-[1.09rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
<span className="absolute right-2 top-[0.45em]">S</span>
</div>{" "}
<ToolbarSelectItem
keyboardKey="S"
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={true}
value={"Share"}
icon={"Share3"}
styleObj={{
iconClasses: "relative top-0.5 -m-1 mr-1 h-6 w-6",
}}
/>
</SelectItem>
)}
{!hasStore && (
@ -515,28 +517,13 @@ export default function NodeToolbarComponent({
value={"documentation"}
disabled={data.node?.documentation === ""}
>
<div className="flex">
<IconComponent
name="FileText"
className="relative top-0.5 mr-2 h-4 w-4 "
/>{" "}
<span className="">Docs</span>{" "}
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
) : (
<span className="absolute right-[2.10rem] top-[0.43em] stroke-2">
Ctrl
</span>
)}
<IconComponent
name="ArrowBigUp"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
></IconComponent>
<span className="absolute right-2 top-[0.43em]">D</span>
</div>
<ToolbarSelectItem
keyboardKey="D"
isMac={navigator.userAgent.toUpperCase().includes("MAC")}
shift={true}
value={"Docs"}
icon={"FileText"}
/>
</SelectItem>
{isMinimal && (
<SelectItem value={"show"}>

View file

@ -0,0 +1,44 @@
import { useEffect } from "react";
import ForwardedIconComponent from "../../../../../components/genericIconComponent";
import { toolbarSelectItemProps } from "../../../../../types/components";
export default function ToolbarSelectItem({
shift,
isMac,
keyboardKey,
value,
icon,
styleObj,
}: toolbarSelectItemProps) {
return (
<div className="flex">
<ForwardedIconComponent
name={icon}
className={`relative top-0.5 mr-2 h-4 w-4 ${styleObj?.iconClasses}`}
/>{" "}
<span className={styleObj?.valueClasses}>{value}</span>{" "}
{isMac ? (
<ForwardedIconComponent
name="Command"
className={`absolute right-[${shift ? "2rem" : "1.15rem"}] top-[0.65em] h-3.5 w-3.5 stroke-2 ${styleObj?.commandClasses}`}
></ForwardedIconComponent>
) : (
<span className={`absolute right-[${shift ? "2.10rem" : "1.15rem"}] top-[0.43em] stroke-2 ${styleObj?.ctrlClasses}`}>
{shift ? (
"Ctrl"
) : (
"Ctrl +"
)}
</span>
)}
{shift && (
<ForwardedIconComponent
name="ArrowBigUp"
className={`absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2 ${styleObj?.shiftClasses}`}
/>
)}
<span className={`absolute right-2 top-[0.43em] ${styleObj?.keyClasses}`}>{keyboardKey}</span>
</div>
);
}

View file

@ -147,7 +147,7 @@ export default function StorePage(): JSX.Element {
}
})
.catch((err) => {
if (err.response.status === 403 || err.response.status === 401) {
if (err.response?.status === 403 || err.response?.status === 401) {
setValidApiKey(false);
} else {
setSearchData([]);

View file

@ -584,7 +584,7 @@
}
.app-div {
@apply fixed bottom-5 left-5 flex flex-col-reverse;
@apply absolute bottom-5 left-5 flex flex-col-reverse;
}
.chat-input-modal-txtarea {

View file

@ -491,7 +491,13 @@ export type nodeToolbarPropsType = {
openAdvancedModal?: boolean;
onCloseAdvancedModal?: (close: boolean) => void;
selected: boolean;
updateNodeCode?: (
newNodeClass: APIClassType,
code: string,
name: string
) => void;
setShowState: (show: boolean | SetStateAction<boolean>) => void;
isOutdated?: boolean;
};
export type parsedDataType = {
@ -682,3 +688,19 @@ export type IOFileInputProps = {
field: TemplateVariableType;
updateValue: (e: any, type: string) => void;
};
export type toolbarSelectItemProps = {
isMac: boolean;
shift: boolean;
keyboardKey: string;
value: string;
icon: string;
styleObj?: {
iconClasses?: string;
commandClasses?: string;
shiftClasses?: string;
ctrlClasses?: string;
keyClasses?: string;
valueClasses?: string;
}
};

View file

@ -9,9 +9,15 @@ export const handleUpdateValues = async (name: string, data: NodeDataType) => {
console.error("Code not found in the template");
return;
}
const template = data.node?.template;
if (!template) {
console.error("No template found in the node.");
return;
}
try {
let newTemplate = await postCustomComponentUpdate(
code,
template,
name,
data.node?.template[name]?.value
)

View file

@ -264,7 +264,7 @@ export function buildTweaks(flow: FlowType) {
export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
if (!node.data?.node?.template || !Object.keys(node.data.node.template)) {
return [
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
"We've noticed a potential issue with a Component in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
];
}

View file

@ -278,6 +278,7 @@ export const nodeNames: { [char: string]: string } = {
};
export const nodeIconsLucide: iconsType = {
X: X,
Notify: Bell,
ListFlows: Group,
ClearMessageHistory: FileClock,

View file

@ -386,7 +386,7 @@ export function getCurlCode(
-H 'Content-Type: application/json'\\${
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
}
-d '{"inputs": ${inputs}, "tweaks": ${
-d '{"inputs": [${inputs}], "tweaks": ${
tweak && tweak.length > 0
? buildTweakObject(tweak)
: JSON.stringify(tweaks, null, 2)