Add LLM cache (#541)

✨ feat(main.py): call setup_llm_caching function on app startup
The `setup_llm_caching` function is added to `utils.py` to set up LLM
caching. The function is then called on app startup in `main.py` using
the `app.on_event("startup")` method. This improves the performance of
the application by caching LLM objects.
This commit is contained in:
Gustavo Schaedler 2023-06-25 21:00:34 +01:00 • committed by GitHub
commit 3287f45137
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 4706 additions and 2082 deletions

1119
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "langflow" name = "langflow"
version = "0.1.7" version = "0.2.0"
description = "A Python package with a built-in web application" description = "A Python package with a built-in web application"
authors = ["Logspace <contact@logspace.ai>"] authors = ["Logspace <contact@logspace.ai>"]
maintainers = [ maintainers = [
@ -23,14 +23,14 @@ langflow = "langflow.__main__:main"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.9,<3.12" python = ">=3.9,<3.12"
fastapi = "^0.97.0" fastapi = "^0.98.0"
uvicorn = "^0.22.0" uvicorn = "^0.22.0"
beautifulsoup4 = "^4.11.2" beautifulsoup4 = "^4.11.2"
google-search-results = "^2.4.1" google-search-results = "^2.4.1"
google-api-python-client = "^2.79.0" google-api-python-client = "^2.79.0"
typer = "^0.9.0" typer = "^0.9.0"
gunicorn = "^20.1.0" gunicorn = "^20.1.0"
langchain = "^0.0.208" langchain = "^0.0.211"
openai = "^0.27.8" openai = "^0.27.8"
types-pyyaml = "^6.0.12.8" types-pyyaml = "^6.0.12.8"
pandas = "^1.5.3" pandas = "^1.5.3"
@ -51,12 +51,13 @@ tiktoken = "~0.4.0"
wikipedia = "^1.4.0" wikipedia = "^1.4.0"
langchain-serve = { version = ">0.0.39", optional = true } langchain-serve = { version = ">0.0.39", optional = true }
qdrant-client = "^1.2.0" qdrant-client = "^1.2.0"
websockets = "^11.0.3" websockets = "^10.3"
weaviate-client = "^3.21.0" weaviate-client = "^3.21.0"
jina = "3.15.2" jina = "3.15.2"
sentence-transformers = "^2.2.2" sentence-transformers = "^2.2.2"
ctransformers = "^0.2.2" ctransformers = "^0.2.2"
cohere = "^4.6.0" cohere = "^4.6.0"
python-multipart = "^0.0.6"
sqlmodel = "^0.0.8" sqlmodel = "^0.0.8"
faiss-cpu = "^1.7.4" faiss-cpu = "^1.7.4"
anthropic = "^0.2.10" anthropic = "^0.2.10"
@ -64,6 +65,11 @@ orjson = "^3.9.1"
multiprocess = "^0.70.14" multiprocess = "^0.70.14"
cachetools = "^5.3.1" cachetools = "^5.3.1"
types-cachetools = "^5.3.0.5" types-cachetools = "^5.3.0.5"
appdirs = "^1.4.4"
pinecone-client = "^2.2.2"
supabase = "^1.0.3"
pymongo = "^4.4.0"
certifi = "^2023.5.7"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
@ -78,6 +84,7 @@ requests = "^2.28.0"
pytest-cov = "^4.0.0" pytest-cov = "^4.0.0"
pandas-stubs = "^2.0.0.230412" pandas-stubs = "^2.0.0.230412"
types-pillow = "^9.5.0.2" types-pillow = "^9.5.0.2"
types-appdirs = "^1.4.3.5"
[tool.poetry.extras] [tool.poetry.extras]

View file

@ -1,4 +1,3 @@
import json
from fastapi import ( from fastapi import (
APIRouter, APIRouter,
HTTPException, HTTPException,
@ -7,7 +6,7 @@ from fastapi import (
status, status,
) )
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from langflow.api.v1.schemas import BuiltResponse, InitResponse from langflow.api.v1.schemas import BuiltResponse, InitResponse, StreamData
from langflow.chat.manager import ChatManager from langflow.chat.manager import ChatManager
from langflow.graph.graph.base import Graph from langflow.graph.graph.base import Graph
@ -71,30 +70,37 @@ async def stream_build(flow_id: str):
"""Stream the build process based on stored flow data.""" """Stream the build process based on stored flow data."""
async def event_stream(flow_id): async def event_stream(flow_id):
final_response = json.dumps({"end_of_stream": True}) final_response = {"end_of_stream": True}
try: try:
if flow_id not in flow_data_store: if flow_id not in flow_data_store:
error_message = "Invalid session ID" error_message = "Invalid session ID"
yield f"data: {json.dumps({'error': error_message})}\n\n" yield str(StreamData(event="error", data={"error": error_message}))
return return
graph_data = flow_data_store[flow_id].get("data") graph_data = flow_data_store[flow_id].get("data")
if not graph_data: if not graph_data:
error_message = "No data provided" error_message = "No data provided"
yield f"data: {json.dumps({'error': error_message})}\n\n" yield str(StreamData(event="error", data={"error": error_message}))
return return
logger.debug("Building langchain object") logger.debug("Building langchain object")
graph = Graph.from_payload(graph_data) try:
# Some error could happen when building the graph
graph = Graph.from_payload(graph_data)
except Exception as exc:
logger.exception(exc)
error_message = str(exc)
yield str(StreamData(event="error", data={"error": error_message}))
return
number_of_nodes = len(graph.nodes) number_of_nodes = len(graph.nodes)
for i, vertex in enumerate(graph.generator_build(), 1): for i, vertex in enumerate(graph.generator_build(), 1):
try: try:
log_dict = { log_dict = {
"log": f"Building node {vertex.vertex_type}", "log": f"Building node {vertex.vertex_type}",
"progress": round(i / number_of_nodes, 2),
} }
yield f"data: {json.dumps(log_dict)}\n\n" yield str(StreamData(event="log", data=log_dict))
vertex.build() vertex.build()
params = vertex._built_object_repr() params = vertex._built_object_repr()
valid = True valid = True
@ -105,21 +111,21 @@ async def stream_build(flow_id: str):
params = str(exc) params = str(exc)
valid = False valid = False
response = json.dumps( response = {
{ "valid": valid,
"valid": valid, "params": params,
"params": params, "id": vertex.id,
"id": vertex.id, "progress": round(i / number_of_nodes, 2),
} }
)
yield f"data: {response}\n\n" yield str(StreamData(event="message", data=response))
chat_manager.set_cache(flow_id, graph.build()) chat_manager.set_cache(flow_id, graph.build())
except Exception as exc: except Exception as exc:
logger.error("Error while building the flow: %s", exc) logger.error("Error while building the flow: %s", exc)
yield f"error: {json.dumps({'error': str(exc)})}\n\n" yield str(StreamData(event="error", data={"error": str(exc)}))
finally: finally:
yield f"data: {final_response}\n\n" yield str(StreamData(event="message", data=final_response))
try: try:
return StreamingResponse(event_stream(flow_id), media_type="text/event-stream") return StreamingResponse(event_stream(flow_id), media_type="text/event-stream")

View file

@ -1,12 +1,14 @@
from typing import Optional
from langflow.cache.utils import save_uploaded_file
from langflow.database.models.flow import Flow from langflow.database.models.flow import Flow
from langflow.processing.process import process_graph_cached, process_tweaks from langflow.processing.process import process_graph_cached, process_tweaks
from langflow.utils.logger import logger from langflow.utils.logger import logger
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, UploadFile
from langflow.api.v1.schemas import ( from langflow.api.v1.schemas import (
PredictRequest, ProcessResponse,
PredictResponse, UploadFileResponse,
) )
from langflow.interface.types import build_langchain_types_dict from langflow.interface.types import build_langchain_types_dict
@ -22,14 +24,17 @@ def get_all():
return build_langchain_types_dict() return build_langchain_types_dict()
@router.post("/predict/{flow_id}", response_model=PredictResponse) # For backwards compatibility we will keep the old endpoint
async def predict_flow( @router.post("/predict/{flow_id}", response_model=ProcessResponse)
predict_request: PredictRequest, @router.post("/process/{flow_id}", response_model=ProcessResponse)
async def process_flow(
flow_id: str, flow_id: str,
inputs: Optional[dict] = None,
tweaks: Optional[dict] = None,
session: Session = Depends(get_session), session: Session = Depends(get_session),
): ):
""" """
Endpoint to process a message using the flow passed in the bearer token. Endpoint to process an input with a given flow_id.
""" """
try: try:
@ -40,15 +45,14 @@ async def predict_flow(
if flow.data is None: if flow.data is None:
raise ValueError(f"Flow {flow_id} has no data") raise ValueError(f"Flow {flow_id} has no data")
graph_data = flow.data graph_data = flow.data
if predict_request.tweaks: if tweaks:
try: try:
graph_data = process_tweaks(graph_data, predict_request.tweaks) graph_data = process_tweaks(graph_data, tweaks)
except Exception as exc: except Exception as exc:
logger.error(f"Error processing tweaks: {exc}") logger.error(f"Error processing tweaks: {exc}")
response = process_graph_cached(graph_data, predict_request.message) response = process_graph_cached(graph_data, inputs)
return PredictResponse( return ProcessResponse(
result=response.get("result", ""), result=response,
intermediate_steps=response.get("thought", ""),
) )
except Exception as e: except Exception as e:
# Log stack trace # Log stack trace
@ -56,6 +60,21 @@ async def predict_flow(
raise HTTPException(status_code=500, detail=str(e)) from e raise HTTPException(status_code=500, detail=str(e)) from e
@router.post("/upload/{flow_id}", response_model=UploadFileResponse, status_code=201)
async def create_upload_file(file: UploadFile, flow_id: str):
# Cache file
try:
file_path = save_uploaded_file(file.file, folder_name=flow_id)
return UploadFileResponse(
flowId=flow_id,
file_path=file_path,
)
except Exception as exc:
logger.error(f"Error saving file: {exc}")
raise HTTPException(status_code=500, detail=str(exc)) from exc
# get endpoint to return version of langflow # get endpoint to return version of langflow
@router.get("/version") @router.get("/version")
def get_version(): def get_version():

View file

@ -1,6 +1,8 @@
from pathlib import Path
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from langflow.database.models.flow import FlowCreate, FlowRead from langflow.database.models.flow import FlowCreate, FlowRead
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
import json
class GraphData(BaseModel): class GraphData(BaseModel):
@ -19,34 +21,22 @@ class ExportedFlow(BaseModel):
data: GraphData data: GraphData
class PredictRequest(BaseModel): class InputRequest(BaseModel):
"""Predict request schema.""" input: dict
message: str
class TweaksRequest(BaseModel):
tweaks: Optional[Dict[str, Dict[str, str]]] = Field(default_factory=dict) tweaks: Optional[Dict[str, Dict[str, str]]] = Field(default_factory=dict)
class Config:
schema_extra = { class UpdateTemplateRequest(BaseModel):
"example": { template: dict
"message": "Hello, how are you?",
"tweaks": {
"dndnode_986363f0-4677-4035-9f38-74b94af5dd78": {
"name": "A tool name",
"description": "A tool description",
},
"dndnode_986363f0-4677-4035-9f38-74b94af57378": {
"template": "A {template}",
},
},
}
}
class PredictResponse(BaseModel): class ProcessResponse(BaseModel):
"""Predict response schema.""" """Process response schema."""
result: str result: dict
intermediate_steps: str = ""
class ChatMessage(BaseModel): class ChatMessage(BaseModel):
@ -101,3 +91,18 @@ class InitResponse(BaseModel):
class BuiltResponse(BaseModel): class BuiltResponse(BaseModel):
built: bool built: bool
class UploadFileResponse(BaseModel):
"""Upload file response schema."""
flowId: str
file_path: Path
class StreamData(BaseModel):
event: str
data: dict
def __str__(self) -> str:
return f"event: {self.event}\ndata: {json.dumps(self.data)}\n\n"

View file

@ -1,5 +1,3 @@
import json
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from langflow.api.v1.base import ( from langflow.api.v1.base import (
@ -9,8 +7,6 @@ from langflow.api.v1.base import (
PromptValidationResponse, PromptValidationResponse,
validate_prompt, validate_prompt,
) )
from langflow.graph.vertex.types import VectorStoreVertex
from langflow.graph import Graph
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.validate import validate_code from langflow.utils.validate import validate_code
@ -37,21 +33,3 @@ def post_validate_prompt(prompt: Prompt):
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
raise HTTPException(status_code=500, detail=str(e)) from e raise HTTPException(status_code=500, detail=str(e)) from e
# validate node
@router.post("/node/{node_id}", status_code=200)
def post_validate_node(node_id: str, data: dict):
try:
# build graph
graph = Graph.from_payload(data)
# validate node
node = graph.get_node(node_id)
if node is None:
raise ValueError(f"Node {node_id} not found")
if not isinstance(node, VectorStoreVertex):
node.build()
return json.dumps({"valid": True, "params": str(node._built_object_repr())})
except Exception as e:
logger.exception(e)
return json.dumps({"valid": False, "params": str(e)})

View file

@ -62,9 +62,6 @@ class BaseCache(abc.ABC):
Args: Args:
key: The key of the item to retrieve. 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 @abc.abstractmethod

View file

@ -8,15 +8,17 @@ import tempfile
from collections import OrderedDict from collections import OrderedDict
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
from appdirs import user_cache_dir
CACHE: Dict[str, Any] = {} CACHE: Dict[str, Any] = {}
CACHE_DIR = user_cache_dir("langflow", "langflow")
def create_cache_folder(func): def create_cache_folder(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
# Get the destination folder # Get the destination folder
cache_path = Path(tempfile.gettempdir()) / PREFIX cache_path = Path(CACHE_DIR) / PREFIX
# Create the destination folder if it doesn't exist # Create the destination folder if it doesn't exist
os.makedirs(cache_path, exist_ok=True) os.makedirs(cache_path, exist_ok=True)
@ -118,7 +120,7 @@ def save_binary_file(content: str, file_name: str, accepted_types: list[str]) ->
raise ValueError(f"File {file_name} is not accepted") raise ValueError(f"File {file_name} is not accepted")
# Get the destination folder # Get the destination folder
cache_path = Path(tempfile.gettempdir()) / PREFIX cache_path = Path(CACHE_DIR) / PREFIX
if not content: if not content:
raise ValueError("Please, reload the file in the loader.") raise ValueError("Please, reload the file in the loader.")
data = content.split(",")[1] data = content.split(",")[1]
@ -132,3 +134,46 @@ def save_binary_file(content: str, file_name: str, accepted_types: list[str]) ->
file.write(decoded_bytes) file.write(decoded_bytes)
return file_path return file_path
@create_cache_folder
def save_uploaded_file(file, folder_name):
"""
Save an uploaded file to the specified folder with a hash of its content as the file name.
Args:
file: The uploaded file object.
folder_name: The name of the folder to save the file in.
Returns:
The path to the saved file.
"""
cache_path = Path(CACHE_DIR)
folder_path = cache_path / folder_name
# Create the folder if it doesn't exist
if not folder_path.exists():
folder_path.mkdir()
# Create a hash of the file content
sha256_hash = hashlib.sha256()
# Reset the file cursor to the beginning of the file
file.seek(0)
# Iterate over the uploaded file in small chunks to conserve memory
while chunk := file.read(8192): # Read 8KB at a time (adjust as needed)
sha256_hash.update(chunk)
# Use the hex digest of the hash as the file name
hex_dig = sha256_hash.hexdigest()
file_name = hex_dig
# Reset the file cursor to the beginning of the file
file.seek(0)
# Save the file with the hash as its name
file_path = folder_path / file_name
with open(file_path, "wb") as new_file:
while chunk := file.read(8192):
new_file.write(chunk)
return file_path

View file

@ -144,6 +144,8 @@ class ChatManager:
if isinstance(msg, FileResponse): if isinstance(msg, FileResponse):
if msg.data_type == "image": if msg.data_type == "image":
# Base64 encode the image # Base64 encode the image
if isinstance(msg.data, str):
continue
msg.data = pil_to_base64(msg.data) msg.data = pil_to_base64(msg.data)
file_responses.append(msg) file_responses.append(msg)
if msg.type == "start": if msg.type == "start":

View file

@ -49,6 +49,7 @@ documentloaders:
- SlackDirectoryLoader - SlackDirectoryLoader
- NotionDirectoryLoader - NotionDirectoryLoader
- DirectoryLoader - DirectoryLoader
- GitLoader
embeddings: embeddings:
- OpenAIEmbeddings - OpenAIEmbeddings
- HuggingFaceEmbeddings - HuggingFaceEmbeddings
@ -58,7 +59,7 @@ llms:
# - AzureOpenAI # - AzureOpenAI
# - AzureChatOpenAI # - AzureChatOpenAI
- ChatOpenAI - ChatOpenAI
- LlamaCpp - LlamaCpp
- CTransformers - CTransformers
- Cohere - Cohere
- Anthropic - Anthropic
@ -130,6 +131,9 @@ vectorstores:
- Qdrant - Qdrant
- Weaviate - Weaviate
- FAISS - FAISS
- Pinecone
- SupabaseVectorStore
- MongoDBAtlasVectorSearch
wrappers: wrappers:
- RequestsWrapper - RequestsWrapper
# - ChatPromptTemplate # - ChatPromptTemplate

View file

@ -146,7 +146,7 @@ class Graph:
def generator_build(self) -> Generator: def generator_build(self) -> Generator:
"""Builds each vertex in the graph and yields it.""" """Builds each vertex in the graph and yields it."""
sorted_vertices = self.topological_sort() sorted_vertices = self.topological_sort()
logger.info("Sorted vertices: %s", sorted_vertices) logger.debug("Sorted vertices: %s", sorted_vertices)
yield from sorted_vertices yield from sorted_vertices
def get_node_neighbors(self, node: Vertex) -> Dict[Vertex, int]: def get_node_neighbors(self, node: Vertex) -> Dict[Vertex, int]:

View file

@ -30,9 +30,6 @@ from langflow.interface.wrappers.base import wrapper_creator
from typing import Dict, Type from typing import Dict, Type
DIRECT_TYPES = ["str", "bool", "code", "int", "float", "Any", "prompt"]
VERTEX_TYPE_MAP: Dict[str, Type[Vertex]] = { VERTEX_TYPE_MAP: Dict[str, Type[Vertex]] = {
**{t: PromptVertex for t in prompt_creator.to_list()}, **{t: PromptVertex for t in prompt_creator.to_list()},
**{t: AgentVertex for t in agent_creator.to_list()}, **{t: AgentVertex for t in agent_creator.to_list()},

View file

@ -1,6 +1,5 @@
from langflow.cache import utils as cache_utils from langflow.utils.constants import DIRECT_TYPES
from langflow.graph.vertex.constants import DIRECT_TYPES from langflow.interface.initialize import loading
from langflow.interface import loading
from langflow.interface.listing import ALL_TYPES_DICT from langflow.interface.listing import ALL_TYPES_DICT
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import sync_to_async from langflow.utils.util import sync_to_async
@ -90,12 +89,7 @@ class Vertex:
# Load the type in value.get('suffixes') using # Load the type in value.get('suffixes') using
# what is inside value.get('content') # what is inside value.get('content')
# value.get('value') is the file name # value.get('value') is the file name
file_name = value.get("value") file_path = value.get("file_path")
content = value.get("content")
type_to_load = value.get("suffixes")
file_path = cache_utils.save_binary_file(
content=content, file_name=file_name, accepted_types=type_to_load
)
params[key] = file_path params[key] = file_path

View file

@ -1 +1 @@
DIRECT_TYPES = ["str", "bool", "code", "int", "float", "Any", "prompt"]

View file

@ -106,9 +106,6 @@ class VectorStoreVertex(Vertex):
def __init__(self, data: Dict): def __init__(self, data: Dict):
super().__init__(data, base_type="vectorstores") super().__init__(data, base_type="vectorstores")
def _built_object_repr(self):
return "Vector stores can take time to build. It will build on the first query."
class MemoryVertex(Vertex): class MemoryVertex(Vertex):
def __init__(self, data: Dict): def __init__(self, data: Dict):

View file

@ -68,7 +68,7 @@ class LangChainTypeCreator(BaseModel, ABC):
value=value.get("value", None), value=value.get("value", None),
suffixes=value.get("suffixes", []), suffixes=value.get("suffixes", []),
file_types=value.get("fileTypes", []), file_types=value.get("fileTypes", []),
content=value.get("content", None), file_path=value.get("file_path", None),
) )
for key, value in signature["template"].items() for key, value in signature["template"].items()
if key != "_type" if key != "_type"

View file

@ -1,31 +1,21 @@
import json import json
from typing import Any, Callable, Dict, Optional from typing import Any, Callable, Dict, Sequence
from langchain.agents import ZeroShotAgent from langchain.agents import ZeroShotAgent
from langchain.agents import agent as agent_module from langchain.agents import agent as agent_module
from langchain.agents.agent import AgentExecutor from langchain.agents.agent import AgentExecutor
from langchain.agents.agent_toolkits.base import BaseToolkit from langchain.agents.agent_toolkits.base import BaseToolkit
from langchain.agents.load_tools import ( from langchain.agents.tools import BaseTool
_BASE_TOOLS, from langflow.interface.initialize.vector_store import vecstore_initializer
_EXTRA_LLM_TOOLS,
_EXTRA_OPTIONAL_TOOLS,
_LLM_TOOLS,
)
from langchain.agents.loading import load_agent_from_config
from langchain.agents.tools import Tool
from langchain.base_language import BaseLanguageModel
from langchain.callbacks.base import BaseCallbackManager
from langchain.chains.loading import load_chain_from_config
from langchain.llms.loading import load_llm_from_config
from pydantic import ValidationError from pydantic import ValidationError
from langflow.interface.custom_lists import CUSTOM_NODES from langflow.interface.custom_lists import CUSTOM_NODES
from langflow.interface.importing.utils import get_function, import_by_type from langflow.interface.importing.utils import get_function, import_by_type
from langflow.interface.toolkits.base import toolkits_creator from langflow.interface.toolkits.base import toolkits_creator
from langflow.interface.chains.base import chain_creator from langflow.interface.chains.base import chain_creator
from langflow.interface.types import get_type_list
from langflow.interface.utils import load_file_into_dict from langflow.interface.utils import load_file_into_dict
from langflow.utils import util, validate from langflow.utils import validate
def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any: def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any:
@ -153,24 +143,48 @@ def instantiate_embedding(class_object, params):
def instantiate_vectorstore(class_object, params): def instantiate_vectorstore(class_object, params):
if len(params.get("documents", [])) == 0: search_kwargs = params.pop("search_kwargs", {})
raise ValueError( if initializer := vecstore_initializer.get(class_object.__name__):
"The source you provided did not load correctly or was empty." vecstore = initializer(class_object, params)
"This may cause an error in the vectorstore." else:
) if "texts" in params:
# Chroma requires all metadata values to not be None params["documents"] = params.pop("texts")
if class_object.__name__ == "Chroma": vecstore = class_object.from_documents(**params)
for doc in params["documents"]:
if doc.metadata is None: # ! This might not work. Need to test
doc.metadata = {} if search_kwargs and hasattr(vecstore, "as_retriever"):
for key, value in doc.metadata.items(): vecstore = vecstore.as_retriever(search_kwargs=search_kwargs)
if value is None:
doc.metadata[key] = "" return vecstore
return class_object.from_documents(**params)
def instantiate_documentloader(class_object, params): def instantiate_documentloader(class_object, params):
return class_object(**params).load() if "file_filter" in params:
# file_filter will be a string but we need a function
# that will be used to filter the files using file_filter
# like lambda x: x.endswith(".txt") but as we don't know
# anything besides the string, we will simply check if the string is
# in x and if it is, we will return True
file_filter = params.pop("file_filter", None)
extensions = file_filter.split(",")
params["file_filter"] = lambda x: any(
extension.strip() in x for extension in extensions
)
metadata = params.pop("metadata", None)
docs = class_object(**params).load()
if metadata:
if isinstance(metadata, str):
try:
metadata = json.loads(metadata)
except json.JSONDecodeError as exc:
raise ValueError(
"The metadata you provided is not a valid JSON string."
) from exc
for doc in docs:
doc.metadata = metadata
return docs
def instantiate_textsplitter(class_object, params): def instantiate_textsplitter(class_object, params):
@ -207,48 +221,14 @@ def replace_zero_shot_prompt_with_prompt_template(nodes):
return nodes return nodes
def load_langchain_type_from_config(config: Dict[str, Any]):
"""Load langchain type from config"""
# Get type list
type_list = get_type_list()
if config["_type"] in type_list["agents"]:
config = util.update_verbose(config, new_value=False)
return load_agent_executor_from_config(config, verbose=True)
elif config["_type"] in type_list["chains"]:
config = util.update_verbose(config, new_value=False)
return load_chain_from_config(config, verbose=True)
elif config["_type"] in type_list["llms"]:
config = util.update_verbose(config, new_value=True)
return load_llm_from_config(config)
else:
raise ValueError("Type should be either agent, chain or llm")
def load_agent_executor_from_config(
config: dict,
llm: Optional[BaseLanguageModel] = None,
tools: Optional[list[Tool]] = None,
callback_manager: Optional[BaseCallbackManager] = None,
**kwargs: Any,
):
tools = load_tools_from_config(config["allowed_tools"])
config["allowed_tools"] = [tool.name for tool in tools] if tools else []
agent_obj = load_agent_from_config(config, llm, tools, **kwargs)
return AgentExecutor.from_agent_and_tools(
agent=agent_obj,
tools=tools,
callback_manager=callback_manager,
**kwargs,
)
def load_agent_executor(agent_class: type[agent_module.Agent], params, **kwargs): def load_agent_executor(agent_class: type[agent_module.Agent], params, **kwargs):
"""Load agent executor from agent class, tools and chain""" """Load agent executor from agent class, tools and chain"""
allowed_tools = params.get("allowed_tools", []) allowed_tools: Sequence[BaseTool] = params.get("allowed_tools", [])
llm_chain = params["llm_chain"] llm_chain = params["llm_chain"]
# if allowed_tools is not a list or set, make it a list # if allowed_tools is not a list or set, make it a list
if not isinstance(allowed_tools, (list, set)): if not isinstance(allowed_tools, (list, set)) and isinstance(
allowed_tools, BaseTool
):
allowed_tools = [allowed_tools] allowed_tools = [allowed_tools]
tool_names = [tool.name for tool in allowed_tools] tool_names = [tool.name for tool in allowed_tools]
# Agent class requires an output_parser but Agent classes # Agent class requires an output_parser but Agent classes
@ -267,46 +247,6 @@ def load_toolkits_executor(node_type: str, toolkit: BaseToolkit, params: dict):
return create_function(llm=llm, toolkit=toolkit) return create_function(llm=llm, toolkit=toolkit)
def load_tools_from_config(tool_list: list[dict]) -> list:
"""Load tools based on a config list.
Args:
config: config list.
Returns:
List of tools.
"""
tools = []
for tool in tool_list:
tool_type = tool.pop("_type")
llm_config = tool.pop("llm", None)
llm = load_llm_from_config(llm_config) if llm_config else None
kwargs = tool
if tool_type in _BASE_TOOLS:
tools.append(_BASE_TOOLS[tool_type]())
elif tool_type in _LLM_TOOLS:
if llm is None:
raise ValueError(f"Tool {tool_type} requires an LLM to be provided")
tools.append(_LLM_TOOLS[tool_type](llm))
elif tool_type in _EXTRA_LLM_TOOLS:
if llm is None:
raise ValueError(f"Tool {tool_type} requires an LLM to be provided")
_get_llm_tool_func, extra_keys = _EXTRA_LLM_TOOLS[tool_type]
if missing_keys := set(extra_keys).difference(kwargs):
raise ValueError(
f"Tool {tool_type} requires some parameters that were not "
f"provided: {missing_keys}"
)
tools.append(_get_llm_tool_func(llm=llm, **kwargs))
elif tool_type in _EXTRA_OPTIONAL_TOOLS:
_get_tool_func, extra_keys = _EXTRA_OPTIONAL_TOOLS[tool_type]
kwargs = {k: value for k, value in kwargs.items() if value}
tools.append(_get_tool_func(**kwargs))
else:
raise ValueError(f"Got unknown tool {tool_type}")
return tools
def build_prompt_template(prompt, tools): def build_prompt_template(prompt, tools):
"""Build PromptTemplate from ZeroShotPrompt""" """Build PromptTemplate from ZeroShotPrompt"""
prefix = prompt["node"]["template"]["prefix"]["value"] prefix = prompt["node"]["template"]["prefix"]["value"]

View file

@ -0,0 +1,223 @@
import json
from typing import Any, Callable, Dict, Type
from langchain.vectorstores import (
Pinecone,
Qdrant,
Chroma,
FAISS,
Weaviate,
SupabaseVectorStore,
MongoDBAtlasVectorSearch,
)
import os
def docs_in_params(params: dict) -> bool:
"""Check if params has documents OR texts and one of them is not an empty list,
If any of them is not an empty list, return True, else return False"""
return ("documents" in params and params["documents"]) or (
"texts" in params and params["texts"]
)
def initialize_mongodb(class_object: Type[MongoDBAtlasVectorSearch], params: dict):
"""Initialize mongodb and return the class object"""
MONGODB_ATLAS_CLUSTER_URI = params.pop("mongodb_atlas_cluster_uri")
if not MONGODB_ATLAS_CLUSTER_URI:
raise ValueError("Mongodb atlas cluster uri must be provided in the params")
from pymongo import MongoClient
import certifi
client: MongoClient = MongoClient(
MONGODB_ATLAS_CLUSTER_URI, tlsCAFile=certifi.where()
)
db_name = params.pop("db_name", None)
collection_name = params.pop("collection_name", None)
if not db_name or not collection_name:
raise ValueError("db_name and collection_name must be provided in the params")
index_name = params.pop("index_name", None)
if not index_name:
raise ValueError("index_name must be provided in the params")
collection = client[db_name][collection_name]
if not docs_in_params(params):
# __init__ requires collection, embedding and index_name
init_args = {
"collection": collection,
"index_name": index_name,
"embedding": params.get("embedding"),
}
return class_object(**init_args)
if "texts" in params:
params["documents"] = params.pop("texts")
params["collection"] = collection
params["index_name"] = index_name
return class_object.from_documents(**params)
def initialize_supabase(class_object: Type[SupabaseVectorStore], params: dict):
"""Initialize supabase and return the class object"""
from supabase.client import Client, create_client
if "supabase_url" not in params or "supabase_service_key" not in params:
raise ValueError("Supabase url and service key must be provided in the params")
if "texts" in params:
params["documents"] = params.pop("texts")
client_kwargs = {
"supabase_url": params.pop("supabase_url"),
"supabase_key": params.pop("supabase_service_key"),
}
supabase: Client = create_client(**client_kwargs)
if not docs_in_params(params):
params.pop("documents", None)
params.pop("texts", None)
return class_object(client=supabase, **params)
# If there are docs in the params, create a new index
return class_object.from_documents(client=supabase, **params)
def initialize_weaviate(class_object: Type[Weaviate], params: dict):
"""Initialize weaviate and return the class object"""
if not docs_in_params(params):
import weaviate # type: ignore
client_kwargs_json = params.get("client_kwargs", "{}")
client_kwargs = json.loads(client_kwargs_json)
client_params = {
"url": params.get("weaviate_url"),
}
client_params.update(client_kwargs)
weaviate_client = weaviate.Client(**client_params)
new_params = {
"client": weaviate_client,
"index_name": params.get("index_name"),
"text_key": params.get("text_key"),
}
return class_object(**new_params)
# If there are docs in the params, create a new index
if "texts" in params:
params["documents"] = params.pop("texts")
return class_object.from_documents(**params)
def initialize_faiss(class_object: Type[FAISS], params: dict):
"""Initialize faiss and return the class object"""
if not docs_in_params(params):
return class_object.load_local
save_local = params.get("save_local")
faiss_index = class_object(**params)
if save_local:
faiss_index.save_local(folder_path=save_local)
return faiss_index
def initialize_pinecone(class_object: Type[Pinecone], params: dict):
"""Initialize pinecone and return the class object"""
import pinecone # type: ignore
pinecone_api_key = params.get("pinecone_api_key")
pinecone_env = params.get("pinecone_env")
if pinecone_api_key is None or pinecone_env is None:
if os.getenv("PINECONE_API_KEY") is not None:
pinecone_api_key = os.getenv("PINECONE_API_KEY")
if os.getenv("PINECONE_ENV") is not None:
pinecone_env = os.getenv("PINECONE_ENV")
if pinecone_api_key is None or pinecone_env is None:
raise ValueError(
"Pinecone API key and environment must be provided in the params"
)
# initialize pinecone
pinecone.init(
api_key=pinecone_api_key, # find at app.pinecone.io
environment=pinecone_env, # next to api key in console
)
# If there are no docs in the params, return an existing index
# but first remove any texts or docs keys from the params
if not docs_in_params(params):
existing_index_params = {
"embedding": params.pop("embedding"),
}
if "index_name" in params:
existing_index_params["index_name"] = params.pop("index_name")
if "namespace" in params:
existing_index_params["namespace"] = params.pop("namespace")
return class_object.from_existing_index(**existing_index_params)
# If there are docs in the params, create a new index
if "texts" in params:
params["documents"] = params.pop("texts")
return class_object.from_documents(**params)
def initialize_chroma(class_object: Type[Chroma], params: dict):
"""Initialize a ChromaDB object from the params"""
persist = params.pop("persist", False)
if not docs_in_params(params):
params.pop("documents", None)
params.pop("texts", None)
params["embedding_function"] = params.pop("embedding")
chromadb = class_object(**params)
else:
if "texts" in params:
params["documents"] = params.pop("texts")
for doc in params["documents"]:
if doc.metadata is None:
doc.metadata = {}
for key, value in doc.metadata.items():
if value is None:
doc.metadata[key] = ""
chromadb = class_object.from_documents(**params)
if persist:
chromadb.persist()
return chromadb
def initialize_qdrant(class_object: Type[Qdrant], params: dict):
if not docs_in_params(params):
if "location" not in params and "api_key" not in params:
raise ValueError("Location and API key must be provided in the params")
from qdrant_client import QdrantClient
client_params = {
"location": params.pop("location"),
"api_key": params.pop("api_key"),
}
lc_params = {
"collection_name": params.pop("collection_name"),
"embeddings": params.pop("embedding"),
}
client = QdrantClient(**client_params)
return class_object(client=client, **lc_params)
return class_object.from_documents(**params)
vecstore_initializer: Dict[str, Callable[[Type[Any], dict], Any]] = {
"Pinecone": initialize_pinecone,
"Chroma": initialize_chroma,
"Qdrant": initialize_qdrant,
"Weaviate": initialize_weaviate,
"FAISS": initialize_faiss,
"SupabaseVectorStore": initialize_supabase,
"MongoDBAtlasVectorSearch": initialize_mongodb,
}

View file

@ -7,6 +7,7 @@ import re
import yaml import yaml
from langchain.base_language import BaseLanguageModel from langchain.base_language import BaseLanguageModel
from PIL.Image import Image from PIL.Image import Image
from langflow.utils.logger import logger
def load_file_into_dict(file_path: str) -> dict: def load_file_into_dict(file_path: str) -> dict:
@ -58,3 +59,12 @@ def try_setting_streaming_options(langchain_object, websocket):
def extract_input_variables_from_prompt(prompt: str) -> list[str]: def extract_input_variables_from_prompt(prompt: str) -> list[str]:
"""Extract input variables from prompt.""" """Extract input variables from prompt."""
return re.findall(r"{(.*?)}", prompt) return re.findall(r"{(.*?)}", prompt)
def setup_llm_caching():
"""Setup LLM caching."""
import langchain
from langchain.cache import SQLiteCache
logger.debug("Setting up LLM caching")
langchain.llm_cache = SQLiteCache()

View file

@ -3,6 +3,7 @@ from fastapi.middleware.cors import CORSMiddleware
from langflow.api import router from langflow.api import router
from langflow.database.base import create_db_and_tables from langflow.database.base import create_db_and_tables
from langflow.interface.utils import setup_llm_caching
def create_app(): def create_app():
@ -28,6 +29,7 @@ def create_app():
app.include_router(router) app.include_router(router)
app.on_event("startup")(create_db_and_tables) app.on_event("startup")(create_db_and_tables)
app.on_event("startup")(setup_llm_caching)
return app return app

View file

@ -1,5 +1,3 @@
import contextlib
import io
from pathlib import Path from pathlib import Path
from langchain.schema import AgentAction from langchain.schema import AgentAction
import json import json
@ -10,7 +8,8 @@ from langflow.interface.run import (
) )
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.graph import Graph from langflow.graph import Graph
from langchain.chains.base import Chain
from langchain.vectorstores.base import VectorStore
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
@ -55,69 +54,42 @@ def format_actions(actions: List[Tuple[AgentAction, str]]) -> str:
return "\n".join(output) return "\n".join(output)
def get_result_and_thought(langchain_object, message: str): def get_result_and_thought(langchain_object: Any, inputs: dict):
"""Get result and thought from extracted json""" """Get result and thought from extracted json"""
try: try:
if hasattr(langchain_object, "verbose"): if hasattr(langchain_object, "verbose"):
langchain_object.verbose = True langchain_object.verbose = True
chat_input = None
memory_key = ""
if hasattr(langchain_object, "memory") and langchain_object.memory is not None:
memory_key = langchain_object.memory.memory_key
if hasattr(langchain_object, "input_keys"):
for key in langchain_object.input_keys:
if key not in [memory_key, "chat_history"]:
chat_input = {key: message}
else:
chat_input = message # type: ignore
if hasattr(langchain_object, "return_intermediate_steps"): if hasattr(langchain_object, "return_intermediate_steps"):
# https://github.com/hwchase17/langchain/issues/2068 langchain_object.return_intermediate_steps = True
# Deactivating until we have a frontend solution
# to display intermediate steps
langchain_object.return_intermediate_steps = False
fix_memory_inputs(langchain_object) fix_memory_inputs(langchain_object)
with io.StringIO() as output_buffer, contextlib.redirect_stdout(output_buffer): try:
try: output = langchain_object(inputs, return_only_outputs=True)
# if hasattr(langchain_object, "acall"): except ValueError as exc:
# output = await langchain_object.acall(chat_input) # make the error message more informative
# else: logger.debug(f"Error: {str(exc)}")
output = langchain_object(chat_input) output = langchain_object.run(inputs)
except ValueError as exc:
# make the error message more informative
logger.debug(f"Error: {str(exc)}")
output = langchain_object.run(chat_input)
intermediate_steps = (
output.get("intermediate_steps", []) if isinstance(output, dict) else []
)
result = (
output.get(langchain_object.output_keys[0])
if isinstance(output, dict)
else output
)
if intermediate_steps:
thought = format_actions(intermediate_steps)
else:
thought = output_buffer.getvalue()
except Exception as exc: except Exception as exc:
raise ValueError(f"Error: {str(exc)}") from exc raise ValueError(f"Error: {str(exc)}") from exc
return result, thought return output
def process_graph_cached(data_graph: Dict[str, Any], message: str): def get_input_str_if_only_one_input(inputs: dict) -> Optional[str]:
"""Get input string if only one input is provided"""
return list(inputs.values())[0] if len(inputs) == 1 else None
def process_graph_cached(data_graph: Dict[str, Any], inputs: Optional[dict] = None):
""" """
Process graph by extracting input variables and replacing ZeroShotPrompt Process graph by extracting input variables and replacing ZeroShotPrompt
with PromptTemplate,then run the graph and return the result and thought. with PromptTemplate,then run the graph and return the result and thought.
""" """
# Load langchain object # Load langchain object
langchain_object = build_langchain_object_with_caching(data_graph) langchain_object = build_langchain_object_with_caching(data_graph)
logger.debug("Loaded langchain object") logger.debug("Loaded LangChain object")
if langchain_object is None: if langchain_object is None:
# Raise user facing error # Raise user facing error
@ -126,10 +98,16 @@ def process_graph_cached(data_graph: Dict[str, Any], message: str):
) )
# Generate result and thought # Generate result and thought
logger.debug("Generating result and thought") if isinstance(langchain_object, Chain):
result, thought = get_result_and_thought(langchain_object, message) if inputs is None:
logger.debug("Generated result and thought") raise ValueError("Inputs must be provided for a Chain")
return {"result": str(result), "thought": thought.strip()} logger.debug("Generating result and thought")
result = get_result_and_thought(langchain_object, inputs)
logger.debug("Generated result and thought")
elif isinstance(langchain_object, VectorStore):
class_name = langchain_object.__class__.__name__
result = {"message": f"Processed {class_name} successfully"}
return result
def load_flow_from_json( def load_flow_from_json(
@ -206,7 +184,8 @@ def apply_tweaks(node: Dict[str, Any], node_tweaks: Dict[str, Any]) -> None:
for tweak_name, tweak_value in node_tweaks.items(): for tweak_name, tweak_value in node_tweaks.items():
if tweak_name and tweak_value and tweak_name in template_data: if tweak_name and tweak_value and tweak_name in template_data:
template_data[tweak_name]["value"] = tweak_value key = tweak_name if tweak_name == "file_path" else "value"
template_data[tweak_name][key] = tweak_value
def process_tweaks( def process_tweaks(

View file

@ -15,7 +15,7 @@ class TemplateFieldCreator(BaseModel, ABC):
suffixes: list[str] = [] suffixes: list[str] = []
fileTypes: list[str] = [] fileTypes: list[str] = []
file_types: list[str] = [] file_types: list[str] = []
content: Union[str, None] = None file_path: Union[str, None] = None
password: bool = False password: bool = False
options: list[str] = [] options: list[str] = []
name: str = "" name: str = ""
@ -35,7 +35,7 @@ class TemplateFieldCreator(BaseModel, ABC):
result["fileTypes"] = result.pop("file_types") result["fileTypes"] = result.pop("file_types")
if self.field_type == "file": if self.field_type == "file":
result["content"] = self.content result["file_path"] = self.file_path
return result return result

View file

@ -13,12 +13,37 @@ class ChainFrontendNode(FrontendNode):
self.template.add_field( self.template.add_field(
TemplateField( TemplateField(
field_type="BaseChatMemory", field_type="BaseChatMemory",
required=False, required=True,
show=True, show=True,
name="memory", name="memory",
advanced=False, advanced=False,
) )
) )
# add return_source_documents
self.template.add_field(
TemplateField(
field_type="bool",
required=False,
show=True,
name="return_source_documents",
advanced=False,
value=True,
display_name="Return source documents",
)
)
self.template.add_field(
TemplateField(
field_type="str",
required=True,
is_list=True,
show=True,
multiline=False,
options=QA_CHAIN_TYPES,
value=QA_CHAIN_TYPES[0],
name="chain_type",
advanced=False,
)
)
@staticmethod @staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None: def format_field(field: TemplateField, name: Optional[str] = None) -> None:
@ -47,7 +72,7 @@ class ChainFrontendNode(FrontendNode):
field.show = True field.show = True
field.advanced = False field.advanced = False
if field.name == "memory": if field.name == "memory":
field.required = False # field.required = False
field.show = True field.show = True
field.advanced = False field.advanced = False
if field.name == "verbose": if field.name == "verbose":
@ -59,6 +84,12 @@ class ChainFrontendNode(FrontendNode):
field.show = True field.show = True
field.advanced = False field.advanced = False
if field.name == "return_source_documents":
field.required = False
field.show = True
field.advanced = True
field.value = True
class SeriesCharacterChainNode(FrontendNode): class SeriesCharacterChainNode(FrontendNode):
name: str = "SeriesCharacterChain" name: str = "SeriesCharacterChain"

View file

@ -1,8 +1,9 @@
from typing import Optional
from langflow.template.field.base import TemplateField from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode from langflow.template.frontend_node.base import FrontendNode
def build_template( def build_file_field(
suffixes: list, fileTypes: list, name: str = "file_path" suffixes: list, fileTypes: list, name: str = "file_path"
) -> TemplateField: ) -> TemplateField:
"""Build a template field for a document loader.""" """Build a template field for a document loader."""
@ -19,32 +20,35 @@ def build_template(
class DocumentLoaderFrontNode(FrontendNode): class DocumentLoaderFrontNode(FrontendNode):
file_path_templates = { file_path_templates = {
"AirbyteJSONLoader": build_template(suffixes=[".json"], fileTypes=["json"]), "AirbyteJSONLoader": build_file_field(suffixes=[".json"], fileTypes=["json"]),
"CoNLLULoader": build_template(suffixes=[".csv"], fileTypes=["csv"]), "CoNLLULoader": build_file_field(suffixes=[".csv"], fileTypes=["csv"]),
"CSVLoader": build_template(suffixes=[".csv"], fileTypes=["csv"]), "CSVLoader": build_file_field(suffixes=[".csv"], fileTypes=["csv"]),
"UnstructuredEmailLoader": build_template(suffixes=[".eml"], fileTypes=["eml"]), "UnstructuredEmailLoader": build_file_field(
"EverNoteLoader": build_template(suffixes=[".xml"], fileTypes=["xml"]), suffixes=[".eml"], fileTypes=["eml"]
"FacebookChatLoader": build_template(suffixes=[".json"], fileTypes=["json"]), ),
"GutenbergLoader": build_template(suffixes=[".txt"], fileTypes=["txt"]), "SlackDirectoryLoader": build_file_field(suffixes=[".zip"], fileTypes=["zip"]),
"BSHTMLLoader": build_template(suffixes=[".html"], fileTypes=["html"]), "EverNoteLoader": build_file_field(suffixes=[".xml"], fileTypes=["xml"]),
"UnstructuredHTMLLoader": build_template( "FacebookChatLoader": build_file_field(suffixes=[".json"], fileTypes=["json"]),
"GutenbergLoader": build_file_field(suffixes=[".txt"], fileTypes=["txt"]),
"BSHTMLLoader": build_file_field(suffixes=[".html"], fileTypes=["html"]),
"UnstructuredHTMLLoader": build_file_field(
suffixes=[".html"], fileTypes=["html"] suffixes=[".html"], fileTypes=["html"]
), ),
"UnstructuredImageLoader": build_template( "UnstructuredImageLoader": build_file_field(
suffixes=[".jpg", ".jpeg", ".png", ".gif", ".bmp"], suffixes=[".jpg", ".jpeg", ".png", ".gif", ".bmp"],
fileTypes=["jpg", "jpeg", "png", "gif", "bmp"], fileTypes=["jpg", "jpeg", "png", "gif", "bmp"],
), ),
"UnstructuredMarkdownLoader": build_template( "UnstructuredMarkdownLoader": build_file_field(
suffixes=[".md"], fileTypes=["md"] suffixes=[".md"], fileTypes=["md"]
), ),
"PyPDFLoader": build_template(suffixes=[".pdf"], fileTypes=["pdf"]), "PyPDFLoader": build_file_field(suffixes=[".pdf"], fileTypes=["pdf"]),
"UnstructuredPowerPointLoader": build_template( "UnstructuredPowerPointLoader": build_file_field(
suffixes=[".pptx", ".ppt"], fileTypes=["pptx", "ppt"] suffixes=[".pptx", ".ppt"], fileTypes=["pptx", "ppt"]
), ),
"SRTLoader": build_template(suffixes=[".srt"], fileTypes=["srt"]), "SRTLoader": build_file_field(suffixes=[".srt"], fileTypes=["srt"]),
"TelegramChatLoader": build_template(suffixes=[".json"], fileTypes=["json"]), "TelegramChatLoader": build_file_field(suffixes=[".json"], fileTypes=["json"]),
"TextLoader": build_template(suffixes=[".txt"], fileTypes=["txt"]), "TextLoader": build_file_field(suffixes=[".txt"], fileTypes=["txt"]),
"UnstructuredWordDocumentLoader": build_template( "UnstructuredWordDocumentLoader": build_file_field(
suffixes=[".docx", ".doc"], fileTypes=["docx", "doc"] suffixes=[".docx", ".doc"], fileTypes=["docx", "doc"]
), ),
} }
@ -52,7 +56,54 @@ class DocumentLoaderFrontNode(FrontendNode):
def add_extra_fields(self) -> None: def add_extra_fields(self) -> None:
name = None name = None
display_name = "Web Page" display_name = "Web Page"
if self.template.type_name in self.file_path_templates: if self.template.type_name in {"GitLoader"}:
# Add fields repo_path, clone_url, branch and file_filter
self.template.add_field(
TemplateField(
field_type="str",
required=True,
show=True,
name="repo_path",
value="",
display_name="Path to repository",
advanced=False,
)
)
self.template.add_field(
TemplateField(
field_type="str",
required=False,
show=True,
name="clone_url",
value="",
display_name="Clone URL",
advanced=False,
)
)
self.template.add_field(
TemplateField(
field_type="str",
required=True,
show=True,
name="branch",
value="",
display_name="Branch",
advanced=False,
)
)
self.template.add_field(
TemplateField(
field_type="str",
required=False,
show=True,
name="file_filter",
value="",
display_name="File extensions (comma-separated)",
advanced=False,
)
)
elif self.template.type_name in self.file_path_templates:
self.template.add_field(self.file_path_templates[self.template.type_name]) self.template.add_field(self.file_path_templates[self.template.type_name])
elif self.template.type_name in { elif self.template.type_name in {
"WebBaseLoader", "WebBaseLoader",
@ -65,7 +116,11 @@ class DocumentLoaderFrontNode(FrontendNode):
name = "web_path" name = "web_path"
elif self.template.type_name in {"GitbookLoader"}: elif self.template.type_name in {"GitbookLoader"}:
name = "web_page" name = "web_page"
elif self.template.type_name in {"DirectoryLoader", "ReadTheDocsLoader"}: elif self.template.type_name in {
"DirectoryLoader",
"ReadTheDocsLoader",
"NotionDirectoryLoader",
}:
name = "path" name = "path"
display_name = "Local directory" display_name = "Local directory"
if name: if name:
@ -90,3 +145,23 @@ class DocumentLoaderFrontNode(FrontendNode):
display_name="glob", display_name="glob",
) )
) )
# add a metadata field of type dict
self.template.add_field(
TemplateField(
field_type="code",
required=True,
show=True,
name="metadata",
value="{}",
display_name="Metadata",
multiline=False,
)
)
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
if field.name == "metadata":
field.show = True
field.advanced = False
field.show = True

View file

@ -18,6 +18,27 @@ class MemoryFrontendNode(FrontendNode):
value=False, value=False,
) )
) )
# add input_key and output_key str fields
self.template.add_field(
TemplateField(
field_type="str",
required=False,
show=True,
name="input_key",
advanced=True,
value="",
)
)
self.template.add_field(
TemplateField(
field_type="str",
required=False,
show=True,
name="output_key",
advanced=True,
value="",
)
)
@staticmethod @staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None: def format_field(field: TemplateField, name: Optional[str] = None) -> None:
@ -36,3 +57,10 @@ class MemoryFrontendNode(FrontendNode):
field.required = False field.required = False
field.show = True field.show = True
field.advanced = False field.advanced = False
if field.name in ["input_key", "output_key"]:
field.required = False
field.show = True
field.advanced = False
field.value = ""
if field.name == "memory_key":
field.value = "chat_history"

View file

@ -96,6 +96,16 @@ class PythonFunctionToolNode(FrontendNode):
name="code", name="code",
advanced=False, advanced=False,
), ),
TemplateField(
field_type="bool",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=False,
value=False,
name="return_direct",
),
], ],
) )
description: str = "Python function to be executed." description: str = "Python function to be executed."

View file

@ -1,4 +1,4 @@
from typing import Optional from typing import List, Optional
from langflow.template.field.base import TemplateField from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode from langflow.template.frontend_node.base import FrontendNode
@ -6,6 +6,19 @@ from langflow.template.frontend_node.base import FrontendNode
class VectorStoreFrontendNode(FrontendNode): class VectorStoreFrontendNode(FrontendNode):
def add_extra_fields(self) -> None: def add_extra_fields(self) -> None:
extra_fields: List[TemplateField] = []
# Add search_kwargs field
extra_field = TemplateField(
name="search_kwargs",
field_type="code",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
value="{}",
)
extra_fields.append(extra_field)
if self.template.type_name == "Weaviate": if self.template.type_name == "Weaviate":
extra_field = TemplateField( extra_field = TemplateField(
name="weaviate_url", name="weaviate_url",
@ -17,8 +30,174 @@ class VectorStoreFrontendNode(FrontendNode):
multiline=False, multiline=False,
value="http://localhost:8080", value="http://localhost:8080",
) )
# Add client_kwargs field
extra_field2 = TemplateField(
name="client_kwargs",
field_type="code",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
value="{}",
)
extra_fields.extend((extra_field, extra_field2))
self.template.add_field(extra_field) elif self.template.type_name == "Chroma":
# New bool field for persist parameter
extra_field = TemplateField(
name="persist",
field_type="bool",
required=False,
show=True,
advanced=False,
value=True,
display_name="Persist",
)
extra_fields.append(extra_field)
elif self.template.type_name == "Pinecone":
# add pinecone_api_key and pinecone_env
extra_field = TemplateField(
name="pinecone_api_key",
field_type="str",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
value="",
)
extra_field2 = TemplateField(
name="pinecone_env",
field_type="str",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
value="",
)
extra_fields.extend((extra_field, extra_field2))
elif self.template.type_name == "FAISS":
extra_field = TemplateField(
name="folder_path",
field_type="str",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
display_name="Local Path",
value="",
)
extra_field2 = TemplateField(
name="index_name",
field_type="str",
required=False,
show=True,
advanced=False,
value="",
display_name="Index Name",
)
extra_fields.extend((extra_field, extra_field2))
elif self.template.type_name == "SupabaseVectorStore":
self.display_name = "Supabase"
# Add table_name and query_name
extra_field = TemplateField(
name="table_name",
field_type="str",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
value="",
)
extra_field2 = TemplateField(
name="query_name",
field_type="str",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
value="",
)
# Add supabase_url and supabase_service_key
extra_field3 = TemplateField(
name="supabase_url",
field_type="str",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
value="",
)
extra_field4 = TemplateField(
name="supabase_service_key",
field_type="str",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
value="",
)
extra_fields.extend((extra_field, extra_field2, extra_field3, extra_field4))
elif self.template.type_name == "MongoDBAtlasVectorSearch":
self.display_name = "MongoDB Atlas"
extra_field = TemplateField(
name="mongodb_atlas_cluster_uri",
field_type="str",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
display_name="MongoDB Atlas Cluster URI",
value="",
)
extra_field2 = TemplateField(
name="collection_name",
field_type="str",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
display_name="Collection Name",
value="",
)
extra_field3 = TemplateField(
name="db_name",
field_type="str",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
display_name="Database Name",
value="",
)
extra_field4 = TemplateField(
name="index_name",
field_type="str",
required=False,
placeholder="",
show=True,
advanced=True,
multiline=False,
display_name="Index Name",
value="",
)
extra_fields.extend((extra_field, extra_field2, extra_field3, extra_field4))
if extra_fields:
for field in extra_fields:
self.template.add_field(field)
def add_extra_base_classes(self) -> None: def add_extra_base_classes(self) -> None:
self.base_classes.append("BaseRetriever") self.base_classes.append("BaseRetriever")
@ -27,7 +206,25 @@ class VectorStoreFrontendNode(FrontendNode):
def format_field(field: TemplateField, name: Optional[str] = None) -> None: def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name) FrontendNode.format_field(field, name)
# Define common field attributes # Define common field attributes
basic_fields = ["work_dir", "collection_name", "api_key", "location"] basic_fields = [
"work_dir",
"collection_name",
"api_key",
"location",
"persist_directory",
"persist",
"weaviate_url",
"index_name",
"namespace",
"folder_path",
"table_name",
"query_name",
"supabase_url",
"supabase_service_key",
"mongodb_atlas_cluster_uri",
"collection_name",
"db_name",
]
advanced_fields = [ advanced_fields = [
"n_dim", "n_dim",
"key", "key",
@ -43,14 +240,21 @@ class VectorStoreFrontendNode(FrontendNode):
"https", "https",
"prefer_grpc", "prefer_grpc",
"grpc_port", "grpc_port",
"pinecone_api_key",
"pinecone_env",
"client_kwargs",
"search_kwargs",
] ]
# Check and set field attributes # Check and set field attributes
if field.name == "texts": if field.name == "texts":
# if field.name is "texts" it has to be replaced
# when instantiating the vectorstores
field.name = "documents" field.name = "documents"
field.field_type = "TextSplitter" field.field_type = "TextSplitter"
field.display_name = "Text Splitter" field.display_name = "Documents"
field.required = True field.required = False
field.show = True field.show = True
field.advanced = False field.advanced = False
@ -78,5 +282,6 @@ class VectorStoreFrontendNode(FrontendNode):
field.advanced = True field.advanced = True
if "key" in field.name: if "key" in field.name:
field.password = False field.password = False
# TODO: Weaviate requires weaviate_url to be passed as it is not part of
# the class or from_texts method. We need the add_extra_fields to fix this elif field.name == "text_key":
field.show = False

View file

@ -3,6 +3,7 @@ from typing import Callable, Optional, Union
from pydantic import BaseModel from pydantic import BaseModel
from langflow.template.field.base import TemplateField from langflow.template.field.base import TemplateField
from langflow.utils.constants import DIRECT_TYPES
class Template(BaseModel): class Template(BaseModel):
@ -18,8 +19,15 @@ class Template(BaseModel):
for field in self.fields: for field in self.fields:
format_field_func(field, name) format_field_func(field, name)
def sort_fields(self):
# first sort alphabetically
# then sort fields so that fields that have .field_type in DIRECT_TYPES are first
self.fields.sort(key=lambda x: x.name)
self.fields.sort(key=lambda x: x.field_type in DIRECT_TYPES, reverse=False)
def to_dict(self, format_field_func=None): def to_dict(self, format_field_func=None):
self.process_fields(self.type_name, format_field_func) self.process_fields(self.type_name, format_field_func)
self.sort_fields()
result = {field.name: field.to_dict() for field in self.fields} result = {field.name: field.to_dict() for field in self.fields}
result["_type"] = self.type_name # type: ignore result["_type"] = self.type_name # type: ignore
return result return result

View file

@ -36,3 +36,4 @@ def python_function(text: str) -> str:
\"\"\"This is a default python function that returns the input text\"\"\" \"\"\"This is a default python function that returns the input text\"\"\"
return text return text
""" """
DIRECT_TYPES = ["str", "bool", "code", "int", "float", "Any", "prompt"]

View file

@ -299,12 +299,15 @@ def format_dict(d, name: Optional[str] = None):
if name == "OpenAI" and key == "model_name": if name == "OpenAI" and key == "model_name":
value["options"] = constants.OPENAI_MODELS value["options"] = constants.OPENAI_MODELS
value["list"] = True value["list"] = True
value["value"] = constants.OPENAI_MODELS[0]
elif name == "ChatOpenAI" and key == "model_name": elif name == "ChatOpenAI" and key == "model_name":
value["options"] = constants.CHAT_OPENAI_MODELS value["options"] = constants.CHAT_OPENAI_MODELS
value["list"] = True value["list"] = True
value["value"] = constants.CHAT_OPENAI_MODELS[0]
elif (name == "Anthropic" or name == "ChatAnthropic") and key == "model_name": elif (name == "Anthropic" or name == "ChatAnthropic") and key == "model_name":
value["options"] = constants.ANTHROPIC_MODELS value["options"] = constants.ANTHROPIC_MODELS
value["list"] = True value["list"] = True
value["value"] = constants.ANTHROPIC_MODELS[0]
return d return d

File diff suppressed because it is too large Load diff

View file

@ -8,15 +8,16 @@
"@headlessui/react": "^1.7.10", "@headlessui/react": "^1.7.10",
"@heroicons/react": "^2.0.15", "@heroicons/react": "^2.0.15",
"@mui/material": "^5.11.9", "@mui/material": "^5.11.9",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.6", "@radix-ui/react-tooltip": "^1.0.6",
"@tabler/icons-react": "^2.18.0", "@tabler/icons-react": "^2.18.0",
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
@ -98,6 +99,7 @@
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"@vitejs/plugin-react-swc": "^3.0.0", "@vitejs/plugin-react-swc": "^3.0.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"daisyui": "^3.1.1",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"typescript": "^5.0.2", "typescript": "^5.0.2",

View file

@ -1,8 +1,10 @@
import { Handle, Position, useUpdateNodeInternals } from "reactflow"; import { Handle, Position, useUpdateNodeInternals } from "reactflow";
import { import {
classNames, classNames,
getRandomKeyByssmm,
groupByFamily, groupByFamily,
isValidConnection, isValidConnection,
nodeIconsLucide,
} from "../../../../utils"; } from "../../../../utils";
import { useContext, useEffect, useRef, useState } from "react"; import { useContext, useEffect, useRef, useState } from "react";
import InputComponent from "../../../../components/inputComponent"; import InputComponent from "../../../../components/inputComponent";
@ -17,7 +19,7 @@ import InputFileComponent from "../../../../components/inputFileComponent";
import { TabsContext } from "../../../../contexts/tabsContext"; import { TabsContext } from "../../../../contexts/tabsContext";
import IntComponent from "../../../../components/intComponent"; import IntComponent from "../../../../components/intComponent";
import PromptAreaComponent from "../../../../components/promptComponent"; import PromptAreaComponent from "../../../../components/promptComponent";
import { nodeNames, nodeIcons } from "../../../../utils"; import { nodeNames } from "../../../../utils";
import React from "react"; import React from "react";
import { nodeColors } from "../../../../utils"; import { nodeColors } from "../../../../utils";
import ShadTooltip from "../../../../components/ShadTooltipComponent"; import ShadTooltip from "../../../../components/ShadTooltipComponent";
@ -40,7 +42,7 @@ export default function ParameterComponent({
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();
const [position, setPosition] = useState(0); const [position, setPosition] = useState(0);
const { closePopUp } = useContext(PopUpContext); const { closePopUp } = useContext(PopUpContext);
const { setTabsState, tabId } = useContext(TabsContext); const { setTabsState, tabId, save } = useContext(TabsContext);
useEffect(() => { useEffect(() => {
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) { if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
@ -82,18 +84,18 @@ export default function ParameterComponent({
refHtml.current = groupedObj.map((item, i) => ( refHtml.current = groupedObj.map((item, i) => (
<span <span
key={i} key={getRandomKeyByssmm()}
className={classNames( className={classNames(
i > 0 ? "items-center flex mt-3" : "items-center flex" i > 0 ? "items-center flex mt-3" : "items-center flex"
)} )}
> >
<div <div
className="h-5 w-5" className="h-6 w-6"
style={{ style={{
color: nodeColors[item.family], color: nodeColors[item.family],
}} }}
> >
{React.createElement(nodeIcons[item.family])} {React.createElement(nodeIconsLucide[item.family])}
</div> </div>
<span className="ps-2 text-gray-950"> <span className="ps-2 text-gray-950">
{nodeNames[item.family] ?? ""}{" "} {nodeNames[item.family] ?? ""}{" "}
@ -102,14 +104,14 @@ export default function ParameterComponent({
-&nbsp; -&nbsp;
{item.type.split(", ").length > 2 {item.type.split(", ").length > 2
? item.type.split(", ").map((el, i) => ( ? item.type.split(", ").map((el, i) => (
<> <React.Fragment key={el + i}>
<span key={i}> <span>
{i == item.type.split(", ").length - 1 {i === item.type.split(", ").length - 1
? el ? el
: (el += `, `)} : (el += `, `)}
</span> </span>
{i % 2 == 0 && i > 0 && <br></br>} {i % 2 === 0 && i > 0 && <br />}
</> </React.Fragment>
)) ))
: item.type} : item.type}
</span> </span>
@ -239,7 +241,8 @@ export default function ParameterComponent({
fileTypes={data.node.template[name].fileTypes} fileTypes={data.node.template[name].fileTypes}
suffixes={data.node.template[name].suffixes} suffixes={data.node.template[name].suffixes}
onFileChange={(t: string) => { onFileChange={(t: string) => {
data.node.template[name].content = t; data.node.template[name].file_path = t;
save();
}} }}
></InputFileComponent> ></InputFileComponent>
) : left === true && type === "int" ? ( ) : left === true && type === "int" ? (

View file

@ -1,7 +1,21 @@
import { classNames, nodeColors, nodeIcons, toTitleCase } from "../../utils"; import {
classNames,
nodeColors,
nodeIconsLucide,
toTitleCase,
} from "../../utils";
import ParameterComponent from "./components/parameterComponent"; import ParameterComponent from "./components/parameterComponent";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import { useContext, useState, useEffect, useRef } from "react"; import {
useContext,
useState,
useEffect,
useRef,
ForwardRefExoticComponent,
ComponentType,
SVGProps,
ReactNode,
} from "react";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
@ -12,6 +26,7 @@ import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarCom
import ShadTooltip from "../../components/ShadTooltipComponent"; import ShadTooltip from "../../components/ShadTooltipComponent";
import { useSSE } from "../../contexts/SSEContext"; import { useSSE } from "../../contexts/SSEContext";
import { ReactElement } from "react-markdown/lib/react-markdown";
export default function GenericNode({ export default function GenericNode({
data, data,
@ -25,8 +40,9 @@ export default function GenericNode({
const { types, deleteNode } = useContext(typesContext); const { types, deleteNode } = useContext(typesContext);
const { closePopUp, openPopUp } = useContext(PopUpContext); const { closePopUp, openPopUp } = useContext(PopUpContext);
// any to avoid type conflict
const Icon = nodeIcons[data.type] || nodeIcons[types[data.type]]; const Icon: any =
nodeIconsLucide[data.type] || nodeIconsLucide[types[data.type]];
const [validationStatus, setValidationStatus] = useState(null); const [validationStatus, setValidationStatus] = useState(null);
// State for outline color // State for outline color
const { sseData, isBuilding } = useSSE(); const { sseData, isBuilding } = useSSE();
@ -88,8 +104,13 @@ export default function GenericNode({
}} }}
/> />
<div className="ml-2 truncate"> <div className="ml-2 truncate">
<ShadTooltip delayDuration={1500} content={data.type}> <ShadTooltip
<div className="ml-2 truncate text-gray-800">{data.type}</div> delayDuration={1500}
content={data.node.display_name}
>
<div className="ml-2 truncate text-gray-800">
{data.node.display_name}
</div>
</ShadTooltip> </ShadTooltip>
</div> </div>
</div> </div>

View file

@ -1,13 +1,8 @@
import {
XCircleIcon,
XMarkIcon,
InformationCircleIcon,
CheckCircleIcon,
} from "@heroicons/react/24/outline";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
import { useState } from "react"; import { useState } from "react";
import { SingleAlertComponentType } from "../../../../types/alerts"; import { SingleAlertComponentType } from "../../../../types/alerts";
import { X, CheckCircle2, Info, XCircle } from "lucide-react";
export default function SingleAlert({ export default function SingleAlert({
dropItem, dropItem,
@ -34,7 +29,7 @@ export default function SingleAlert({
key={dropItem.id} key={dropItem.id}
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<XCircleIcon <XCircle
className="h-5 w-5 text-red-400 dark:text-red-50" className="h-5 w-5 text-red-400 dark:text-red-50"
aria-hidden="true" aria-hidden="true"
/> />
@ -70,7 +65,7 @@ export default function SingleAlert({
className="inline-flex rounded-md bg-red-50 dark:bg-transparent p-1.5 text-red-500 dark:text-red-50" className="inline-flex rounded-md bg-red-50 dark:bg-transparent p-1.5 text-red-500 dark:text-red-50"
> >
<span className="sr-only">Dismiss</span> <span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" /> <X className="h-5 w-5" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
@ -81,7 +76,7 @@ export default function SingleAlert({
key={dropItem.id} key={dropItem.id}
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<InformationCircleIcon <Info
className="h-5 w-5 text-blue-400 dark:text-blue-50" className="h-5 w-5 text-blue-400 dark:text-blue-50"
aria-hidden="true" aria-hidden="true"
/> />
@ -116,7 +111,7 @@ export default function SingleAlert({
className="inline-flex rounded-md bg-blue-50 dark:bg-transparent p-1.5 text-blue-500 dark:text-blue-50" className="inline-flex rounded-md bg-blue-50 dark:bg-transparent p-1.5 text-blue-500 dark:text-blue-50"
> >
<span className="sr-only">Dismiss</span> <span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" /> <X className="h-5 w-5" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
@ -127,7 +122,7 @@ export default function SingleAlert({
key={dropItem.id} key={dropItem.id}
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<CheckCircleIcon <CheckCircle2
className="h-5 w-5 text-green-400 dark:text-green-50" className="h-5 w-5 text-green-400 dark:text-green-50"
aria-hidden="true" aria-hidden="true"
/> />
@ -150,7 +145,7 @@ export default function SingleAlert({
className="inline-flex rounded-md bg-green-50 dark:bg-transparent p-1.5 text-green-500 dark:text-green-50" className="inline-flex rounded-md bg-green-50 dark:bg-transparent p-1.5 text-green-500 dark:text-green-50"
> >
<span className="sr-only">Dismiss</span> <span className="sr-only">Dismiss</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" /> <X className="h-5 w-5" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,11 +1,11 @@
import { useContext, useEffect, useRef } from "react"; import { useContext, useEffect, useRef } from "react";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { XMarkIcon } from "@heroicons/react/24/solid";
import { TrashIcon } from "@heroicons/react/24/outline";
import SingleAlert from "./components/singleAlertComponent"; import SingleAlert from "./components/singleAlertComponent";
import { AlertDropdownType } from "../../types/alerts"; import { AlertDropdownType } from "../../types/alerts";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import { useOnClickOutside } from "../hooks/useOnClickOutside"; import { useOnClickOutside } from "../hooks/useOnClickOutside";
import { X, Trash2 } from "lucide-react";
export default function AlertDropdown({}: AlertDropdownType) { export default function AlertDropdown({}: AlertDropdownType) {
const { closePopUp } = useContext(PopUpContext); const { closePopUp } = useContext(PopUpContext);
const componentRef = useRef<HTMLDivElement>(null); const componentRef = useRef<HTMLDivElement>(null);
@ -36,13 +36,13 @@ export default function AlertDropdown({}: AlertDropdownType) {
setTimeout(clearNotificationList, 100); setTimeout(clearNotificationList, 100);
}} }}
> >
<TrashIcon className="w-[1.1rem] h-[1.1rem]" /> <Trash2 className="w-[1.1rem] h-[1.1rem]" />
</button> </button>
<button <button
className="text-gray-800 hover:text-red-500 dark:text-gray-200 dark:hover:text-red-500" className="text-gray-800 hover:text-red-500 dark:text-gray-200 dark:hover:text-red-500"
onClick={closePopUp} onClick={closePopUp}
> >
<XMarkIcon className="h-5 w-5" /> <X className="h-5 w-5" />
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,7 +1,7 @@
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
import { XCircleIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ErrorAlertType } from "../../types/alerts"; import { ErrorAlertType } from "../../types/alerts";
import { XCircle } from "lucide-react";
export default function ErrorAlert({ export default function ErrorAlert({
title, title,
@ -43,7 +43,7 @@ export default function ErrorAlert({
> >
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<XCircleIcon <XCircle
className="h-5 w-5 text-red-400 dark:text-red-50" className="h-5 w-5 text-red-400 dark:text-red-50"
aria-hidden="true" aria-hidden="true"
/> />

View file

@ -1,8 +1,8 @@
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
import { InformationCircleIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { NoticeAlertType } from "../../types/alerts"; import { NoticeAlertType } from "../../types/alerts";
import { Info } from "lucide-react";
export default function NoticeAlert({ export default function NoticeAlert({
title, title,
@ -40,7 +40,7 @@ export default function NoticeAlert({
> >
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<InformationCircleIcon <Info
className="h-5 w-5 text-blue-400 dark:text-blue-50" className="h-5 w-5 text-blue-400 dark:text-blue-50"
aria-hidden="true" aria-hidden="true"
/> />

View file

@ -1,7 +1,7 @@
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
import { CheckCircleIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { SuccessAlertType } from "../../types/alerts"; import { SuccessAlertType } from "../../types/alerts";
import { CheckCircle2 } from "lucide-react";
export default function SuccessAlert({ export default function SuccessAlert({
title, title,
@ -38,7 +38,7 @@ export default function SuccessAlert({
> >
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<CheckCircleIcon <CheckCircle2
className="h-5 w-5 text-green-400 dark:text-green-50" className="h-5 w-5 text-green-400 dark:text-green-50"
aria-hidden="true" aria-hidden="true"
/> />

View file

@ -1,5 +1,4 @@
import { Disclosure } from "@headlessui/react"; import { Disclosure } from "@headlessui/react";
import { ChevronLeftIcon } from "@heroicons/react/24/outline";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { classNames } from "../../utils"; import { classNames } from "../../utils";

View file

@ -0,0 +1,6 @@
import { useContext, useEffect, useRef, useState } from "react";
import { RadialProgressType } from "../../types/components";
export default function LoadingSpinner({}) {
return <></>;
}

View file

@ -0,0 +1,21 @@
import { ReactElement, useContext, useEffect, useRef, useState } from "react";
import { ProgressBarType } from "../../types/components";
import { Progress } from "../../components/ui/progress";
import { progressContext } from "../../contexts/ProgressContext";
import { setInterval } from "timers/promises";
export default function ProgressBarComponent({
value,
children,
}: ProgressBarType) {
const ref = useRef(0);
const reff = useRef();
const { progress } = useContext(progressContext);
useEffect(() => {
ref.current = progress * 100;
console.log(progress);
}, [progress]);
return <Progress className="h-2.5" value={ref.current} />;
}

View file

@ -0,0 +1,19 @@
import { useContext, useEffect, useRef, useState } from "react";
import { RadialProgressType } from "../../types/components";
export default function RadialProgressComponent({
value,
color,
}: RadialProgressType) {
const style = {
"--value": value * 100,
"--size": "1.5rem",
"--thickness": "2px",
} as React.CSSProperties;
return (
<div className={"radial-progress " + color} style={style}>
<strong className="text-[8px]">{Math.trunc(value * 100)}%</strong>
</div>
);
}

View file

@ -1,4 +1,4 @@
import { useContext } from "react"; import { useContext, useState } from "react";
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
import { Zap } from "lucide-react"; import { Zap } from "lucide-react";
import { validateNodes } from "../../../utils"; import { validateNodes } from "../../../utils";
@ -9,6 +9,8 @@ import { typesContext } from "../../../contexts/typesContext";
import { alertContext } from "../../../contexts/alertContext"; import { alertContext } from "../../../contexts/alertContext";
import { postBuildInit } from "../../../controllers/API"; import { postBuildInit } from "../../../controllers/API";
import RadialProgressComponent from "../../RadialProgress";
export default function BuildTrigger({ export default function BuildTrigger({
open, open,
flow, flow,
@ -20,9 +22,12 @@ export default function BuildTrigger({
setIsBuilt: any; setIsBuilt: any;
isBuilt: boolean; isBuilt: boolean;
}) { }) {
const { updateSSEData, isBuilding, setIsBuilding } = useSSE(); const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
const { reactFlowInstance } = useContext(typesContext); const { reactFlowInstance } = useContext(typesContext);
const { setErrorData, setSuccessData } = useContext(alertContext); const { setErrorData, setSuccessData } = useContext(alertContext);
const [isIconTouched, setIsIconTouched] = useState(false);
const eventClick = isBuilding ? "pointer-events-none" : "";
const [progress, setProgress] = useState(0);
async function handleBuild(flow: FlowType) { async function handleBuild(flow: FlowType) {
try { try {
@ -58,12 +63,10 @@ export default function BuildTrigger({
setIsBuilding(false); setIsBuilding(false);
} }
} }
async function streamNodeData(flow: FlowType) { async function streamNodeData(flow: FlowType) {
// Step 1: Make a POST request to send the flow data and receive a unique session ID // Step 1: Make a POST request to send the flow data and receive a unique session ID
const response = await postBuildInit(flow); const response = await postBuildInit(flow);
const { flowId } = response.data; const { flowId } = response.data;
// Step 2: Use the session ID to establish an SSE connection using EventSource // Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults = []; let validationResults = [];
let finished = false; let finished = false;
@ -83,19 +86,23 @@ export default function BuildTrigger({
return; return;
} else if (parsedData.log) { } else if (parsedData.log) {
// If the event is a log, log it // If the event is a log, log it
// TODO: implement the progress
setSuccessData({ title: parsedData.log }); setSuccessData({ title: parsedData.log });
setSuccessData({ title: parsedData.progress });
} else { } else {
// Otherwise, process the data // Otherwise, process the data
const isValid = processStreamResult(parsedData); const isValid = processStreamResult(parsedData);
setProgress(parsedData.progress);
validationResults.push(isValid); validationResults.push(isValid);
} }
}; };
eventSource.onerror = (error) => { eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error); console.error("EventSource failed:", error);
eventSource.close(); eventSource.close();
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
setIsBuilding(false);
}
}; };
// Step 3: Wait for the stream to finish // Step 3: Wait for the stream to finish
while (!finished) { while (!finished) {
@ -129,6 +136,14 @@ export default function BuildTrigger({
} }
} }
const handleMouseEnter = () => {
setIsIconTouched(true);
};
const handleMouseLeave = () => {
setIsIconTouched(false);
};
return ( return (
<Transition <Transition
show={!open} show={!open}
@ -142,17 +157,23 @@ export default function BuildTrigger({
> >
<div className={`fixed right-4` + (isBuilt ? " bottom-20" : " bottom-4")}> <div className={`fixed right-4` + (isBuilt ? " bottom-20" : " bottom-4")}>
<div <div
className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032] className={`${eventClick} flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032] bg-[#E2E7EE] dark:border-gray-600 cursor-pointer`}
bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
onClick={() => { onClick={() => {
handleBuild(flow); handleBuild(flow);
}} }}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
> >
<button> <button>
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
{isBuilding ? ( {isBuilding && progress < 1 ? (
// Render your loading animation here when isBuilding is true // Render your loading animation here when isBuilding is true
<Loading strokeWidth={1.5} style={{ color: "white" }} /> <RadialProgressComponent
color={"text-orange-400"}
value={progress}
></RadialProgressComponent>
) : isBuilding ? (
<Loading strokeWidth={1.5} style={{ color: "#fb923c" }} />
) : ( ) : (
<Zap className="sh-6 w-6 fill-orange-400 stroke-1 stroke-orange-400" /> <Zap className="sh-6 w-6 fill-orange-400 stroke-1 stroke-orange-400" />
)} )}

View file

@ -1,13 +1,9 @@
import {
ChatBubbleLeftEllipsisIcon,
ChatBubbleOvalLeftEllipsisIcon,
PlusSmallIcon,
} from "@heroicons/react/24/outline";
import { useState } from "react"; import { useState } from "react";
import { ChatMessageType } from "../../../types/chat"; import { ChatMessageType } from "../../../types/chat";
import { nodeColors } from "../../../utils"; import { nodeColors } from "../../../utils";
import Convert from "ansi-to-html"; import Convert from "ansi-to-html";
const convert = new Convert({ newline: true }); const convert = new Convert({ newline: true });
import { MessageCircle } from "lucide-react";
export default function ChatMessage({ chat }: { chat: ChatMessageType }) { export default function ChatMessage({ chat }: { chat: ChatMessageType }) {
const [hidden, setHidden] = useState(true); const [hidden, setHidden] = useState(true);
@ -24,7 +20,7 @@ export default function ChatMessage({ chat }: { chat: ChatMessageType }) {
onClick={() => setHidden((prev) => !prev)} onClick={() => setHidden((prev) => !prev)}
className="absolute top-2 right-2 cursor-pointer" className="absolute top-2 right-2 cursor-pointer"
> >
<ChatBubbleOvalLeftEllipsisIcon className="w-5 h-5 animate-bounce" /> <MessageCircle className="w-5 h-5 animate-bounce" />
</div> </div>
)} )}
{chat.thought && chat.thought !== "" && !hidden && ( {chat.thought && chat.thought !== "" && !hidden && (

View file

@ -1,10 +1,10 @@
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import CodeAreaModal from "../../modals/codeAreaModal"; import CodeAreaModal from "../../modals/codeAreaModal";
import TextAreaModal from "../../modals/textAreaModal"; import TextAreaModal from "../../modals/textAreaModal";
import { TextAreaComponentType } from "../../types/components"; import { TextAreaComponentType } from "../../types/components";
import { INPUT_STYLE } from "../../constants"; import { INPUT_STYLE } from "../../constants";
import { ExternalLink } from "lucide-react";
export default function CodeAreaComponent({ export default function CodeAreaComponent({
value, value,
@ -69,7 +69,7 @@ export default function CodeAreaComponent({
}} }}
> >
{!editNode && ( {!editNode && (
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-ring dark:text-gray-300 ml-3" /> <ExternalLink className="w-6 h-6 hover:text-ring dark:text-gray-300 ml-3" />
)} )}
</button> </button>
</div> </div>

View file

@ -1,9 +1,9 @@
import { Listbox, Transition } from "@headlessui/react"; import { Listbox, Transition } from "@headlessui/react";
import { ChevronUpDownIcon, CheckIcon } from "@heroicons/react/24/outline"; import { Fragment, useEffect, useState } from "react";
import { Fragment, useState } from "react";
import { DropDownComponentType } from "../../types/components"; import { DropDownComponentType } from "../../types/components";
import { classNames } from "../../utils"; import { classNames } from "../../utils";
import { INPUT_STYLE } from "../../constants"; import { INPUT_STYLE } from "../../constants";
import { ChevronsUpDown, Check } from "lucide-react";
export default function Dropdown({ export default function Dropdown({
value, value,
@ -15,6 +15,9 @@ export default function Dropdown({
let [internalValue, setInternalValue] = useState( let [internalValue, setInternalValue] = useState(
value === "" || !value ? "Choose an option" : value value === "" || !value ? "Choose an option" : value
); );
useEffect(() => {
setInternalValue(value === "" || !value ? "Choose an option" : value);
}, [value]);
return ( return (
<> <>
@ -43,7 +46,7 @@ export default function Dropdown({
"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2" "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
} }
> >
<ChevronUpDownIcon <ChevronsUpDown
className="h-5 w-5 text-gray-400" className="h-5 w-5 text-gray-400"
aria-hidden="true" aria-hidden="true"
/> />
@ -97,7 +100,7 @@ export default function Dropdown({
"absolute inset-y-0 right-0 flex items-center pr-4" "absolute inset-y-0 right-0 flex items-center pr-4"
)} )}
> >
<CheckIcon <Check
className={ className={
active active
? "h-5 w-5 dark:text-black text-black" ? "h-5 w-5 dark:text-black text-black"

View file

@ -53,7 +53,7 @@ export const MenuBar = ({ flows, tabId }) => {
</Link> </Link>
<div className="flex items-center font-medium text-sm rounded-md py-1 px-1.5 gap-0.5"> <div className="flex items-center font-medium text-sm rounded-md py-1 px-1.5 gap-0.5">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger asChild>
<Button <Button
className="gap-2 flex items-center max-w-[200px]" className="gap-2 flex items-center max-w-[200px]"
variant="primary" variant="primary"
@ -69,6 +69,7 @@ export const MenuBar = ({ flows, tabId }) => {
onClick={() => { onClick={() => {
handleAddFlow(); handleAddFlow();
}} }}
className="cursor-pointer"
> >
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
New New
@ -77,6 +78,7 @@ export const MenuBar = ({ flows, tabId }) => {
onClick={() => { onClick={() => {
openPopUp(<FlowSettingsModal />); openPopUp(<FlowSettingsModal />);
}} }}
className="cursor-pointer"
> >
<Settings2 className="w-4 h-4 mr-2 dark:text-gray-300" /> <Settings2 className="w-4 h-4 mr-2 dark:text-gray-300" />
Settings Settings
@ -85,6 +87,7 @@ export const MenuBar = ({ flows, tabId }) => {
onClick={() => { onClick={() => {
undo(); undo();
}} }}
className="cursor-pointer"
> >
<Undo className="w-4 h-4 mr-2 dark:text-gray-300" /> <Undo className="w-4 h-4 mr-2 dark:text-gray-300" />
Undo Undo
@ -93,6 +96,7 @@ export const MenuBar = ({ flows, tabId }) => {
onClick={() => { onClick={() => {
redo(); redo();
}} }}
className="cursor-pointer"
> >
<Redo className="w-4 h-4 mr-2 dark:text-gray-300" /> <Redo className="w-4 h-4 mr-2 dark:text-gray-300" />
Redo Redo

View file

@ -13,6 +13,7 @@ import { Link, useLocation, useParams } from "react-router-dom";
import { USER_PROJECTS_HEADER } from "../../constants"; import { USER_PROJECTS_HEADER } from "../../constants";
import { getRepoStars } from "../../controllers/API"; import { getRepoStars } from "../../controllers/API";
import { Separator } from "../ui/separator"; import { Separator } from "../ui/separator";
import { Bell } from "lucide-react";
export default function Header() { export default function Header() {
const { flows, addFlow, tabId } = useContext(TabsContext); const { flows, addFlow, tabId } = useContext(TabsContext);
@ -98,7 +99,7 @@ export default function Header() {
> >
<FaDiscord className="h-5 w-5" /> <FaDiscord className="h-5 w-5" />
</a> </a>
<Separator orientation="vertical" /> {/* <Separator orientation="vertical" />
<button <button
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200" className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200"
onClick={() => { onClick={() => {
@ -110,7 +111,7 @@ export default function Header() {
) : ( ) : (
<MoonIcon className="h-5 w-5" /> <MoonIcon className="h-5 w-5" />
)} )}
</button> </button> */}
<button <button
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200 relative" className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200 relative"
onClick={(event: React.MouseEvent<HTMLElement>) => { onClick={(event: React.MouseEvent<HTMLElement>) => {
@ -134,7 +135,7 @@ export default function Header() {
{notificationCenter && ( {notificationCenter && (
<div className="absolute w-1.5 h-1.5 rounded-full bg-destructive right-[3px]"></div> <div className="absolute w-1.5 h-1.5 rounded-full bg-destructive right-[3px]"></div>
)} )}
<BellIcon className="h-5 w-5" aria-hidden="true" /> <Bell className="h-5 w-5" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,8 +1,10 @@
import { DocumentMagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { FileComponentType } from "../../types/components"; import { FileComponentType } from "../../types/components";
import { TabsContext } from "../../contexts/tabsContext";
import { INPUT_STYLE } from "../../constants"; import { INPUT_STYLE } from "../../constants";
import { FileSearch2 } from "lucide-react";
import { uploadFile } from "../../controllers/API";
export default function InputFileComponent({ export default function InputFileComponent({
value, value,
@ -14,7 +16,9 @@ export default function InputFileComponent({
editNode = false, editNode = false,
}: FileComponentType) { }: FileComponentType) {
const [myValue, setMyValue] = useState(value); const [myValue, setMyValue] = useState(value);
const [loading, setLoading] = useState(false);
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const { tabId } = useContext(TabsContext);
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
setMyValue(""); setMyValue("");
@ -23,12 +27,6 @@ export default function InputFileComponent({
} }
}, [disabled, onChange]); }, [disabled, onChange]);
function attachFile(fileReadEvent: ProgressEvent<FileReader>) {
fileReadEvent.preventDefault();
const file = fileReadEvent.target.result;
onFileChange(file as string);
}
function checkFileType(fileName: string): boolean { function checkFileType(fileName: string): boolean {
for (let index = 0; index < suffixes.length; index++) { for (let index = 0; index < suffixes.length; index++) {
if (fileName.endsWith(suffixes[index])) { if (fileName.endsWith(suffixes[index])) {
@ -43,27 +41,54 @@ export default function InputFileComponent({
}, [value]); }, [value]);
const handleButtonClick = () => { const handleButtonClick = () => {
// Create a file input element
const input = document.createElement("input"); const input = document.createElement("input");
input.type = "file"; input.type = "file";
input.accept = suffixes.join(","); input.accept = suffixes.join(",");
input.style.display = "none"; input.style.display = "none"; // Hidden from view
input.multiple = false; input.multiple = false; // Allow only one file selection
input.onchange = (e: Event) => { input.onchange = (e: Event) => {
setLoading(true);
// Get the selected file
const file = (e.target as HTMLInputElement).files?.[0]; const file = (e.target as HTMLInputElement).files?.[0];
const fileData = new FileReader();
fileData.onload = attachFile; // Check if the file type is correct
if (file && checkFileType(file.name)) { if (file && checkFileType(file.name)) {
fileData.readAsDataURL(file); // Upload the file
setMyValue(file.name); uploadFile(file, tabId)
onChange(file.name); .then((res) => res.data)
.then((data) => {
console.log("File uploaded successfully");
// Get the file name from the response
const { file_path } = data;
console.log("File name:", file_path);
// Update the state and callback with the name of the file
// sets the value to the user
setMyValue(file.name);
onChange(file.name);
// sets the value that goes to the backend
onFileChange(file_path);
setLoading(false);
})
.catch(() => {
console.error("Error occurred while uploading file");
setLoading(false);
});
} else { } else {
// Show an error if the file type is not allowed
setErrorData({ setErrorData({
title: title:
"Please select a valid file. Only files this files are allowed:", "Please select a valid file. Only these file types are allowed:",
list: fileTypes, list: fileTypes,
}); });
setLoading(false);
} }
}; };
// Trigger the file selection dialog
input.click(); input.click();
}; };
@ -73,7 +98,7 @@ export default function InputFileComponent({
disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full" disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full"
} }
> >
<div className="w-full flex items-center gap-3"> <div className="w-full flex items-center gap-2">
<span <span
onClick={handleButtonClick} onClick={handleButtonClick}
className={ className={
@ -88,8 +113,11 @@ export default function InputFileComponent({
{myValue !== "" ? myValue : "No file"} {myValue !== "" ? myValue : "No file"}
</span> </span>
<button onClick={handleButtonClick}> <button onClick={handleButtonClick}>
{!editNode && ( {!editNode && !loading && (
<DocumentMagnifyingGlassIcon className="w-8 h-8 hover:text-ring" /> <FileSearch2 className="w-6 h-6 hover:text-ring" />
)}
{!editNode && loading && (
<span className="loading loading-spinner loading-sm pl-3 h-8 pointer-events-none"></span>
)} )}
</button> </button>
</div> </div>

View file

@ -1,10 +1,11 @@
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { InputListComponentType } from "../../types/components"; import { InputListComponentType } from "../../types/components";
import { TabsContext } from "../../contexts/tabsContext"; import { TabsContext } from "../../contexts/tabsContext";
import _ from "lodash"; import _ from "lodash";
import { INPUT_STYLE } from "../../constants"; import { INPUT_STYLE } from "../../constants";
import { X, Plus } from "lucide-react";
export default function InputListComponent({ export default function InputListComponent({
value, value,
onChange, onChange,
@ -59,7 +60,7 @@ export default function InputListComponent({
onChange(inputList); onChange(inputList);
}} }}
> >
<PlusIcon className={"w-4 h-4 hover:text-ring"} /> <Plus className={"w-4 h-4 hover:text-ring"} />
</button> </button>
) : ( ) : (
<button <button
@ -72,7 +73,7 @@ export default function InputListComponent({
onChange(inputList); onChange(inputList);
}} }}
> >
<XMarkIcon className="w-4 h-4 hover:text-red-600" /> <X className="w-4 h-4 hover:text-red-600" />
</button> </button>
)} )}
</div> </div>

View file

@ -1,10 +1,10 @@
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import { TextAreaComponentType } from "../../types/components"; import { TextAreaComponentType } from "../../types/components";
import GenericModal from "../../modals/genericModal"; import GenericModal from "../../modals/genericModal";
import { TypeModal } from "../../utils"; import { TypeModal } from "../../utils";
import { INPUT_STYLE } from "../../constants"; import { INPUT_STYLE } from "../../constants";
import { ExternalLink } from "lucide-react";
export default function PromptAreaComponent({ export default function PromptAreaComponent({
value, value,
@ -74,7 +74,7 @@ export default function PromptAreaComponent({
}} }}
> >
{!editNode && ( {!editNode && (
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-ring dark:text-gray-300" /> <ExternalLink className="w-6 h-6 hover:text-ring dark:text-gray-300" />
)} )}
</button> </button>
</div> </div>

View file

@ -1,10 +1,11 @@
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import { TextAreaComponentType } from "../../types/components"; import { TextAreaComponentType } from "../../types/components";
import GenericModal from "../../modals/genericModal"; import GenericModal from "../../modals/genericModal";
import { TypeModal } from "../../utils"; import { TypeModal } from "../../utils";
import { INPUT_STYLE } from "../../constants"; import { INPUT_STYLE } from "../../constants";
import { ExternalLink } from "lucide-react";
export default function TextAreaComponent({ export default function TextAreaComponent({
value, value,
onChange, onChange,
@ -76,7 +77,7 @@ export default function TextAreaComponent({
}} }}
> >
{!editNode && ( {!editNode && (
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-ring dark:text-gray-300" /> <ExternalLink className="w-6 h-6 hover:text-ring dark:text-gray-300" />
)} )}
</button> </button>
</div> </div>

View file

@ -47,7 +47,7 @@ const CardDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<p <div
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}

View file

@ -0,0 +1,27 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "../../utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };

View file

@ -66,7 +66,7 @@ export const getPythonApiCode = (flow: FlowType): string => {
BASE_API_URL = "${window.location.protocol}//${ BASE_API_URL = "${window.location.protocol}//${
window.location.host window.location.host
}/api/v1/predict" }/api/v1/process"
FLOW_ID = "${flowId}" FLOW_ID = "${flowId}"
# You can tweak the flow by adding a tweaks dictionary # You can tweak the flow by adding a tweaks dictionary
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}} # e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
@ -83,7 +83,7 @@ def run_flow(message: str, flow_id: str, tweaks: dict = None) -> dict:
""" """
api_url = f"{BASE_API_URL}/{flow_id}" api_url = f"{BASE_API_URL}/{flow_id}"
payload = {"message": message} payload = {"inputs": {"input": message}}
if tweaks: if tweaks:
payload["tweaks"] = tweaks payload["tweaks"] = tweaks
@ -106,9 +106,9 @@ export const getCurlCode = (flow: FlowType): string => {
return `curl -X POST \\ return `curl -X POST \\
${window.location.protocol}//${ ${window.location.protocol}//${
window.location.host window.location.host
}/api/v1/predict/${flowId} \\ }/api/v1/process/${flowId} \\
-H 'Content-Type: application/json' \\ -H 'Content-Type: application/json' \\
-d '{"message": "Your message", "tweaks": ${JSON.stringify( -d '{"inputs": {"input": message}, "tweaks": ${JSON.stringify(
tweaks, tweaks,
null, null,
2 2

View file

@ -16,7 +16,7 @@ import {
} from "../utils"; } from "../utils";
import { alertContext } from "./alertContext"; import { alertContext } from "./alertContext";
import { typesContext } from "./typesContext"; import { typesContext } from "./typesContext";
import { APITemplateType } from "../types/api"; import { APIClassType, APITemplateType } from "../types/api";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { addEdge } from "reactflow"; import { addEdge } from "reactflow";
import { import {
@ -192,20 +192,26 @@ export function TabsProvider({ children }: { children: ReactNode }) {
} }
function processFlowEdges(flow) { function processFlowEdges(flow) {
if(!flow.data || !flow.data.edges) return;
flow.data.edges.forEach((edge) => { flow.data.edges.forEach((edge) => {
edge.className = ""; edge.className = "";
edge.style = { stroke: "#555555" }; edge.style = { stroke: "#555555" };
}); });
} }
function updateDisplay_name(node:NodeType,template:APIClassType) {
node.data.node.display_name = template["display_name"]?template["display_name"]:node.data.type;
}
function processFlowNodes(flow) { function processFlowNodes(flow) {
flow.data.nodes.forEach((node) => { if(!flow.data || !flow.data.nodes) return;
flow.data.nodes.forEach((node:NodeType) => {
const template = templates[node.data.type]; const template = templates[node.data.type];
if (!template) { if (!template) {
setErrorData({ title: `Unknown node type: ${node.data.type}` }); setErrorData({ title: `Unknown node type: ${node.data.type}` });
return; return;
} }
if (Object.keys(template["template"]).length > 0) { if (Object.keys(template["template"]).length > 0) {
updateDisplay_name(node,template);
updateNodeBaseClasses(node, template); updateNodeBaseClasses(node, template);
updateNodeEdges(flow, node, template); updateNodeEdges(flow, node, template);
updateNodeDescription(node, template); updateNodeDescription(node, template);
@ -214,11 +220,11 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}); });
} }
function updateNodeBaseClasses(node, template) { function updateNodeBaseClasses(node:NodeType,template:APIClassType) {
node.data.node.base_classes = template["base_classes"]; node.data.node.base_classes = template["base_classes"];
} }
function updateNodeEdges(flow, node, template) { function updateNodeEdges(flow:FlowType, node:NodeType,template:APIClassType) {
flow.data.edges.forEach((edge) => { flow.data.edges.forEach((edge) => {
if (edge.source === node.id) { if (edge.source === node.id) {
edge.sourceHandle = edge.sourceHandle edge.sourceHandle = edge.sourceHandle
@ -230,11 +236,11 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}); });
} }
function updateNodeDescription(node, template) { function updateNodeDescription(node:NodeType,template:APIClassType) {
node.data.node.description = template["description"]; node.data.node.description = template["description"];
} }
function updateNodeTemplate(node, template) { function updateNodeTemplate(node:NodeType,template:APIClassType) {
node.data.node.template = updateTemplate( node.data.node.template = updateTemplate(
template["template"] as unknown as APITemplateType, template["template"] as unknown as APITemplateType,
node.data.node.template as APITemplateType node.data.node.template as APITemplateType
@ -463,6 +469,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// Create a new flow with a default name if no flow is provided. // Create a new flow with a default name if no flow is provided.
const newFlow = createNewFlow(flowData, flow); const newFlow = createNewFlow(flowData, flow);
processFlowEdges(newFlow);
processFlowNodes(newFlow);
try { try {
const { id } = await saveFlowToDatabase(newFlow); const { id } = await saveFlowToDatabase(newFlow);

View file

@ -3,6 +3,7 @@ import {
PromptTypeAPI, PromptTypeAPI,
errorsTypeAPI, errorsTypeAPI,
InitTypeAPI, InitTypeAPI,
UploadFileTypeAPI,
} from "./../../types/api/index"; } from "./../../types/api/index";
import { APIObjectType, sendAllProps } from "../../types/api/index"; import { APIObjectType, sendAllProps } from "../../types/api/index";
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse } from "axios";
@ -48,13 +49,6 @@ export async function postValidateCode(
return await axios.post("/api/v1/validate/code", { code }); return await axios.post("/api/v1/validate/code", { code });
} }
export async function postValidateNode(
nodeId: string,
data: any
): Promise<AxiosResponse<string>> {
return await axios.post(`/api/v1/validate/node/${nodeId}`, { data });
}
/** /**
* Checks the prompt for the code block by sending it to an API endpoint. * Checks the prompt for the code block by sending it to an API endpoint.
* *
@ -319,3 +313,21 @@ export async function postBuildInit(
): Promise<AxiosResponse<InitTypeAPI>> { ): Promise<AxiosResponse<InitTypeAPI>> {
return await axios.post(`/api/v1/build/init`, flow); return await axios.post(`/api/v1/build/init`, flow);
} }
// fetch(`/upload/${id}`, {
// method: "POST",
// body: formData,
// });
/**
* Uploads a file to the server.
* @param {File} file - The file to upload.
* @param {string} id - The ID of the flow to upload the file to.
*/
export async function uploadFile(
file: File,
id: string
): Promise<AxiosResponse<UploadFileTypeAPI>> {
const formData = new FormData();
formData.append("file", file);
return await axios.post(`/api/v1/upload/${id}`, formData);
}

View file

@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import { ReactComponent as MongoDBSVG } from "./mongodb-icon.svg";
export const MongoDBIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <MongoDBSVG ref={ref} {...props} />;
});

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 32 32"><path d="M15.9.087l.854 1.604c.192.296.4.558.645.802.715.715 1.394 1.464 2.004 2.266 1.447 1.9 2.423 4.01 3.12 6.292.418 1.394.645 2.824.662 4.27.07 4.323-1.412 8.035-4.4 11.12-.488.488-1.01.94-1.57 1.342-.296 0-.436-.227-.558-.436-.227-.383-.366-.82-.436-1.255-.105-.523-.174-1.046-.14-1.586v-.244C16.057 24.21 15.796.21 15.9.087z" fill="#599636"/><path d="M15.9.034c-.035-.07-.07-.017-.105.017.017.35-.105.662-.296.96-.21.296-.488.523-.767.767-1.55 1.342-2.77 2.963-3.747 4.776-1.3 2.44-1.97 5.055-2.16 7.808-.087.993.314 4.497.627 5.508.854 2.684 2.388 4.933 4.375 6.885.488.47 1.01.906 1.55 1.325.157 0 .174-.14.21-.244a4.78 4.78 0 0 0 .157-.68l.35-2.614L15.9.034z" fill="#6cac48"/><path d="M16.754 28.845c.035-.4.227-.732.436-1.063-.21-.087-.366-.26-.488-.453-.105-.174-.192-.383-.26-.575-.244-.732-.296-1.5-.366-2.248v-.453c-.087.07-.105.662-.105.75a17.37 17.37 0 0 1-.314 2.353c-.052.314-.087.627-.28.906 0 .035 0 .07.017.122.314.924.4 1.865.453 2.824v.35c0 .418-.017.33.33.47.14.052.296.07.436.174.105 0 .122-.087.122-.157l-.052-.575v-1.604c-.017-.28.035-.558.07-.82z" fill="#c2bfbf"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import { ReactComponent as PineconeSVG } from "./pinecone_logo.svg";
export const PineconeIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <PineconeSVG ref={ref} {...props} />;
});

View file

@ -0,0 +1,21 @@
<svg width="32" height="35" viewBox="0 0 32 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.8555 34.2962C14.9325 34.2962 15.8055 33.4451 15.8055 32.3954C15.8055 31.3456 14.9325 30.4946 13.8555 30.4946C12.7786 30.4946 11.9055 31.3456 11.9055 32.3954C11.9055 33.4451 12.7786 34.2962 13.8555 34.2962Z" fill="black"/>
<path d="M18.4138 7.19675L19.2512 2.66005" stroke="black" stroke-width="2.11786" stroke-linecap="square"/>
<path d="M22.2656 5.5855L19.3466 2.11099L15.3748 4.37292" stroke="black" stroke-width="2.11786" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M14.9202 26.5528L15.7337 22.0169" stroke="black" stroke-width="2.11786" stroke-linecap="square"/>
<path d="M18.7729 24.9304L15.83 21.4671L11.8701 23.741" stroke="black" stroke-width="2.11786" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M16.6077 17.1996L17.4212 12.6633" stroke="black" stroke-width="2.11786" stroke-linecap="square"/>
<path d="M20.4587 15.58L17.5277 12.128L13.5679 14.3904" stroke="black" stroke-width="2.11786" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M8.32871 26.1554L4.75171 28.5815" stroke="black" stroke-width="2.01017" stroke-linecap="square"/>
<path d="M8.54383 30.0865L4.3208 28.8738L4.63185 24.5944" stroke="black" stroke-width="2.01017" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M21.3213 28.4299L23.8096 31.9282" stroke="black" stroke-width="2.01017" stroke-linecap="square"/>
<path d="M19.718 32.045L24.1085 32.3365L25.3527 28.2438" stroke="black" stroke-width="2.01017" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M25.3999 21.3291L29.7784 22.0996" stroke="black" stroke-width="2.05804" stroke-linecap="square"/>
<path d="M26.9072 25.072L30.3048 22.1919L28.1634 18.3557" stroke="black" stroke-width="2.05804" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M24.1196 12.8615L28.0197 10.763" stroke="black" stroke-width="2.05804" stroke-linecap="square"/>
<path d="M24.3357 8.83965L28.4869 10.5188L27.7093 14.8216" stroke="black" stroke-width="2.05804" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M6.91639 18.1572L2.52588 17.4101" stroke="black" stroke-width="2.05804" stroke-linecap="square"/>
<path d="M4.17731 21.1645L2 17.328L5.36167 14.436" stroke="black" stroke-width="2.05804" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M11.0799 10.6129L8.14893 7.34769" stroke="black" stroke-width="2.05804" stroke-linecap="square"/>
<path d="M12.2897 6.77496L7.80349 6.96156L7.01392 11.2649" stroke="black" stroke-width="2.05804" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import { ReactComponent as SupabaseSvg } from "./supabase-icon.svg";
export const SupabaseIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <SupabaseSvg ref={ref} {...props} />;
});

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64"
height="64"
viewBox="0 0 64 64"
version="1.1"
id="svg20"
sodipodi:docname="supabase-icon.svg"
style="fill:none"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata24">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1687"
inkscape:window-height="849"
id="namedview22"
showgrid="false"
inkscape:zoom="2.0884956"
inkscape:cx="54.5"
inkscape:cy="56.5"
inkscape:window-x="70"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg20" />
<path
d="m 37.41219,62.936701 c -1.634985,2.05896 -4.950068,0.93085 -4.989463,-1.69817 L 31.846665,22.786035 h 25.855406 c 4.683108,0 7.294967,5.409033 4.382927,9.07673 z"
id="path2"
style="fill:url(#paint0_linear);stroke-width:0.57177335"
inkscape:connector-curvature="0" />
<path
d="m 37.41219,62.936701 c -1.634985,2.05896 -4.950068,0.93085 -4.989463,-1.69817 L 31.846665,22.786035 h 25.855406 c 4.683108,0 7.294967,5.409033 4.382927,9.07673 z"
id="path4"
style="fill:url(#paint1_linear);fill-opacity:0.2;stroke-width:0.57177335"
inkscape:connector-curvature="0" />
<path
d="m 26.89694,1.0634102 c 1.634986,-2.05918508 4.950125,-0.93090008 4.989521,1.698149 L 32.138899,41.214003 H 6.607076 c -4.6832501,0 -7.29518376,-5.409032 -4.3830007,-9.07673 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#3ecf8e;stroke-width:0.57177335" />
<defs
id="defs18">
<linearGradient
id="paint0_linear"
x1="53.973801"
y1="54.973999"
x2="94.163498"
y2="71.829498"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.57177306,0,0,0.57177334,0.98590077,-0.12074988)">
<stop
stop-color="#249361"
id="stop8" />
<stop
offset="1"
stop-color="#3ECF8E"
id="stop10" />
</linearGradient>
<linearGradient
id="paint1_linear"
x1="36.1558"
y1="30.577999"
x2="54.484402"
y2="65.080597"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.57177306,0,0,0.57177334,0.98590077,-0.12074988)">
<stop
id="stop13" />
<stop
offset="1"
stop-opacity="0"
id="stop15" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -1,4 +1,3 @@
import { CodeBracketSquareIcon } from "@heroicons/react/24/outline";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import "ace-builds/src-noconflict/mode-python"; import "ace-builds/src-noconflict/mode-python";
@ -26,7 +25,7 @@ import {
TabsList, TabsList,
TabsTrigger, TabsTrigger,
} from "../../components/ui/tabs"; } from "../../components/ui/tabs";
import { Check, Clipboard } from "lucide-react"; import { Check, Clipboard, Code2 } from "lucide-react";
export default function ApiModal({ flow }: { flow: FlowType }) { export default function ApiModal({ flow }: { flow: FlowType }) {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
@ -88,7 +87,7 @@ export default function ApiModal({ flow }: { flow: FlowType }) {
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center"> <DialogTitle className="flex items-center">
<span className="pr-2">Code</span> <span className="pr-2">Code</span>
<CodeBracketSquareIcon <Code2
className="h-6 w-6 text-gray-800 pl-1 dark:text-white" className="h-6 w-6 text-gray-800 pl-1 dark:text-white"
aria-hidden="true" aria-hidden="true"
/> />
@ -104,7 +103,7 @@ export default function ApiModal({ flow }: { flow: FlowType }) {
<div className="flex items-center justify-between px-2"> <div className="flex items-center justify-between px-2">
<TabsList> <TabsList>
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (
<TabsTrigger value={index.toString()}>{tab.name}</TabsTrigger> <TabsTrigger key={index} value={index.toString()}>{tab.name}</TabsTrigger>
))} ))}
</TabsList> </TabsList>
<div className="float-right"> <div className="float-right">

View file

@ -12,7 +12,6 @@ import {
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../components/ui/table";
import ToggleShadComponent from "../../components/toggleShadComponent"; import ToggleShadComponent from "../../components/toggleShadComponent";
import { VariableIcon } from "@heroicons/react/24/outline";
import InputListComponent from "../../components/inputListComponent"; import InputListComponent from "../../components/inputListComponent";
import TextAreaComponent from "../../components/textAreaComponent"; import TextAreaComponent from "../../components/textAreaComponent";
import InputComponent from "../../components/inputComponent"; import InputComponent from "../../components/inputComponent";
@ -33,6 +32,7 @@ import {
} from "../../components/ui/dialog"; } from "../../components/ui/dialog";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Badge } from "../../components/ui/badge"; import { Badge } from "../../components/ui/badge";
import { Variable } from "lucide-react";
export default function EditNodeModal({ data }: { data: NodeDataType }) { export default function EditNodeModal({ data }: { data: NodeDataType }) {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
@ -79,8 +79,8 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
} }
return ( return (
<Dialog open={true} onOpenChange={setModalOpen}> <Dialog open={true} onOpenChange={setModalOpen} >
<DialogTrigger></DialogTrigger> <DialogTrigger asChild></DialogTrigger>
<DialogContent className="lg:max-w-[700px] "> <DialogContent className="lg:max-w-[700px] ">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center"> <DialogTitle className="flex items-center">
@ -90,9 +90,7 @@ export default function EditNodeModal({ data }: { data: NodeDataType }) {
<DialogDescription> <DialogDescription>
{data.node?.description} {data.node?.description}
<div className="flex pt-4"> <div className="flex pt-4">
<VariableIcon className="w-5 h-5 pe-1 text-gray-700 stroke-2 dark:text-slate-200"> <Variable className="w-5 h-5 pe-1 text-gray-700 stroke-2 dark:text-slate-200"></Variable>
&nbsp;
</VariableIcon>
<span className="text-sm font-semibold text-gray-800 dark:text-white"> <span className="text-sm font-semibold text-gray-800 dark:text-white">
Parameters Parameters
</span> </span>

View file

@ -101,6 +101,7 @@ export default function ModalField({
data.node.template[name].value = t; data.node.template[name].value = t;
setEnabled(t); setEnabled(t);
}} }}
size="small"
/> />
</div> </div>
) : type === "float" ? ( ) : type === "float" ? (

View file

@ -1,5 +1,4 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { Fragment, useContext, useRef, useState } from "react"; import { Fragment, useContext, useRef, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
@ -13,6 +12,7 @@ import {
} from "../../utils"; } from "../../utils";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import ModalField from "./components/ModalField"; import ModalField from "./components/ModalField";
import { X } from "lucide-react";
export default function NodeModal({ data }: { data: NodeDataType }) { export default function NodeModal({ data }: { data: NodeDataType }) {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
@ -27,7 +27,8 @@ export default function NodeModal({ data }: { data: NodeDataType }) {
}, 300); }, 300);
} }
} }
const Icon = nodeIcons[types[data.type]]; // any to avoid type conflict
const Icon: any = nodeIcons[types[data.type]];
return ( return (
<Transition.Root show={open} appear={true} as={Fragment}> <Transition.Root show={open} appear={true} as={Fragment}>
<Dialog <Dialog
@ -69,7 +70,7 @@ export default function NodeModal({ data }: { data: NodeDataType }) {
}} }}
> >
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" /> <X className="h-6 w-6" aria-hidden="true" />
</button> </button>
</div> </div>
<div className="h-full w-full flex flex-col justify-center items-center"> <div className="h-full w-full flex flex-col justify-center items-center">

View file

@ -1,8 +1,9 @@
import { LockClosedIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
import { classNames } from "../../../utils"; import { classNames } from "../../../utils";
import { useContext, useEffect, useRef, useState } from "react"; import { useContext, useEffect, useRef, useState } from "react";
import { TabsContext } from "../../../contexts/tabsContext"; import { TabsContext } from "../../../contexts/tabsContext";
import { INPUT_STYLE } from "../../../constants"; import { INPUT_STYLE } from "../../../constants";
import { Lock, Send } from "lucide-react";
export default function ChatInput({ export default function ChatInput({
lockChat, lockChat,
chatValue, chatValue,
@ -60,12 +61,12 @@ export default function ChatInput({
<div className="absolute bottom-0.5 right-3"> <div className="absolute bottom-0.5 right-3">
<button disabled={lockChat} onClick={() => sendMessage()}> <button disabled={lockChat} onClick={() => sendMessage()}>
{lockChat ? ( {lockChat ? (
<LockClosedIcon <Lock
className="h-5 w-5 text-gray-500 dark:hover:text-gray-300 animate-pulse" className="h-5 w-5 text-gray-500 dark:hover:text-gray-300 animate-pulse"
aria-hidden="true" aria-hidden="true"
/> />
) : ( ) : (
<PaperAirplaneIcon <Send
className="h-5 w-5 text-gray-500 hover:text-gray-600 dark:hover:text-gray-300" className="h-5 w-5 text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
aria-hidden="true" aria-hidden="true"
/> />

View file

@ -1,10 +1,8 @@
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { ChatMessageType } from "../../../types/chat"; import { ChatMessageType } from "../../../types/chat";
import { classNames } from "../../../utils"; import { classNames } from "../../../utils";
import AiIcon from "../../../assets/Gooey Ring-5s-271px.svg"; import AiIcon from "../../../assets/Gooey Ring-5s-271px.svg";
import AiIconStill from "../../../assets/froze-flow.png"; import AiIconStill from "../../../assets/froze-flow.png";
import { UserIcon } from "@heroicons/react/24/solid";
import FileCard from "../fileComponent"; import FileCard from "../fileComponent";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import rehypeMathjax from "rehype-mathjax"; import rehypeMathjax from "rehype-mathjax";
@ -12,6 +10,7 @@ import remarkGfm from "remark-gfm";
import remarkMath from "remark-math"; import remarkMath from "remark-math";
import { CodeBlock } from "./codeBlock"; import { CodeBlock } from "./codeBlock";
import Convert from "ansi-to-html"; import Convert from "ansi-to-html";
import { User2, MessageCircle } from "lucide-react";
export default function ChatMessage({ export default function ChatMessage({
chat, chat,
@ -62,7 +61,7 @@ export default function ChatMessage({
</div> </div>
)} )}
{chat.isSend && ( {chat.isSend && (
<UserIcon className="w-6 h-6 -mb-1 text-gray-800 dark:text-gray-200" /> <User2 className="w-6 h-6 -mb-1 text-gray-800 dark:text-gray-200" />
)} )}
</div> </div>
{!chat.isSend ? ( {!chat.isSend ? (
@ -73,7 +72,7 @@ export default function ChatMessage({
onClick={() => setHidden((prev) => !prev)} onClick={() => setHidden((prev) => !prev)}
className="absolute -top-1 -left-2 cursor-pointer" className="absolute -top-1 -left-2 cursor-pointer"
> >
<ChatBubbleOvalLeftEllipsisIcon className="w-5 h-5 animate-bounce dark:text-white" /> <MessageCircle className="w-5 h-5 animate-bounce dark:text-white" />
</div> </div>
)} )}
{chat.thought && chat.thought !== "" && !hidden && ( {chat.thought && chat.thought !== "" && !hidden && (
@ -152,7 +151,7 @@ export default function ChatMessage({
</div> </div>
) : ( ) : (
<div className="w-full flex items-center"> <div className="w-full flex items-center">
<div className="text-start inline-block px-3 text-sm text-gray-600 dark:text-white"> <div className="text-start inline-block px-3 text-gray-600 dark:text-white">
<span <span
className="text-gray-600 dark:text-gray-200" className="text-gray-600 dark:text-gray-200"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{

View file

@ -1,6 +1,6 @@
import { CloudArrowDownIcon, DocumentIcon } from "@heroicons/react/24/outline";
import * as base64js from "base64-js"; import * as base64js from "base64-js";
import { useState } from "react"; import { useState } from "react";
import { DownloadCloud, File } from "lucide-react";
export default function FileCard({ fileName, content, fileType }) { export default function FileCard({ fileName, content, fileType }) {
const handleDownload = () => { const handleDownload = () => {
@ -43,7 +43,7 @@ export default function FileCard({ fileName, content, fileType }) {
className="text-gray-500 py-1 px-2 dark:bg-gray-700 dark:text-gray-300" className="text-gray-500 py-1 px-2 dark:bg-gray-700 dark:text-gray-300"
onClick={handleDownload} onClick={handleDownload}
> >
<CloudArrowDownIcon className="hover:scale-110 w-5 h-5 text-current"></CloudArrowDownIcon> <DownloadCloud className="hover:scale-110 w-5 h-5 text-current" />
</button> </button>
</div> </div>
)} )}
@ -65,14 +65,14 @@ export default function FileCard({ fileName, content, fileType }) {
className="w-8 h-8" className="w-8 h-8"
/> />
) : ( ) : (
<DocumentIcon className="w-8 h-8" /> <File className="w-8 h-8" />
)} )}
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
{" "} {" "}
<div className="truncate text-sm text-current">{fileName}</div> <div className="truncate text-sm text-current">{fileName}</div>
<div className="truncate text-xs text-gray-500">{fileType}</div> <div className="truncate text-xs text-gray-500">{fileType}</div>
</div> </div>
<CloudArrowDownIcon className="w-6 h-6 text-current ml-auto" /> <DownloadCloud className="w-6 h-6 text-current ml-auto" />
</div> </div>
</button> </button>
); );

View file

@ -1,13 +1,11 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
import { Fragment, useContext, useEffect, useRef, useState } from "react"; import { Fragment, useContext, useEffect, useRef, useState } from "react";
import { FlowType } from "../../types/flow"; import { FlowType } from "../../types/flow";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { validateNodes } from "../../utils"; import { validateNodes } from "../../utils";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import ChatMessage from "./chatMessage"; import ChatMessage from "./chatMessage";
import { Eraser } from "lucide-react"; import { X, MessagesSquare, Eraser } from "lucide-react";
import { X } from "lucide-react";
import { sendAllProps } from "../../types/api"; import { sendAllProps } from "../../types/api";
import { ChatMessageType } from "../../types/chat"; import { ChatMessageType } from "../../types/chat";
import ChatInput from "./chatInput"; import ChatInput from "./chatInput";
@ -387,7 +385,7 @@ export default function ChatModal({
<span className="text-base text-gray-500"> <span className="text-base text-gray-500">
Start a conversation and click the agent’s thoughts{" "} Start a conversation and click the agent’s thoughts{" "}
<span> <span>
<ChatBubbleOvalLeftEllipsisIcon className="w-6 h-6 inline animate-bounce " /> <MessagesSquare className="w-5 h-5 inline animate-bounce mx-1 " />
</span>{" "} </span>{" "}
to inspect the chaining process. to inspect the chaining process.
</span> </span>

View file

@ -1,4 +1,3 @@
import { XMarkIcon, CommandLineIcon } from "@heroicons/react/24/outline";
import { Fragment, useContext, useRef, useState } from "react"; import { Fragment, useContext, useRef, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import AceEditor from "react-ace"; import AceEditor from "react-ace";
@ -22,6 +21,7 @@ import {
} from "../../components/ui/dialog"; } from "../../components/ui/dialog";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants"; import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants";
import { TerminalSquare } from "lucide-react";
export default function CodeAreaModal({ export default function CodeAreaModal({
value, value,
@ -51,7 +51,7 @@ export default function CodeAreaModal({
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center"> <DialogTitle className="flex items-center">
<span className="pr-2">Edit Code</span> <span className="pr-2">Edit Code</span>
<CommandLineIcon <TerminalSquare
className="h-6 w-6 text-gray-800 pl-1 dark:text-white" className="h-6 w-6 text-gray-800 pl-1 dark:text-white"
aria-hidden="true" aria-hidden="true"
/> />

View file

@ -1,4 +1,3 @@
import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
import { useContext, useRef, useState } from "react"; import { useContext, useRef, useState } from "react";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";

View file

@ -1,4 +1,3 @@
import { XMarkIcon, DocumentTextIcon } from "@heroicons/react/24/outline";
import { Fragment, useContext, useRef, useState } from "react"; import { Fragment, useContext, useRef, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import { darkContext } from "../../contexts/darkContext"; import { darkContext } from "../../contexts/darkContext";
@ -17,6 +16,7 @@ import {
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Textarea } from "../../components/ui/textarea"; import { Textarea } from "../../components/ui/textarea";
import { PROMPT_DIALOG_SUBTITLE, TEXT_DIALOG_SUBTITLE } from "../../constants"; import { PROMPT_DIALOG_SUBTITLE, TEXT_DIALOG_SUBTITLE } from "../../constants";
import { FileText } from "lucide-react";
export default function GenericModal({ export default function GenericModal({
value, value,
@ -54,7 +54,7 @@ export default function GenericModal({
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center"> <DialogTitle className="flex items-center">
<span className="pr-2">{myModalTitle}</span> <span className="pr-2">{myModalTitle}</span>
<DocumentTextIcon <FileText
className="h-6 w-6 text-gray-800 pl-1 dark:text-white" className="h-6 w-6 text-gray-800 pl-1 dark:text-white"
aria-hidden="true" aria-hidden="true"
/> />

View file

@ -1,7 +1,5 @@
import React, { ReactNode, useEffect, useRef, useState } from "react"; import React, { ReactNode } from "react";
import { DocumentDuplicateIcon } from "@heroicons/react/solid";
import { classNames } from "../../../utils"; import { classNames } from "../../../utils";
import Tooltip from "../../../components/TooltipComponent";
export default function ButtonBox({ export default function ButtonBox({
onClick, onClick,

View file

@ -1,6 +1,6 @@
import { ChevronRightIcon } from "@heroicons/react/24/solid";
import { Disclosure } from "@headlessui/react"; import { Disclosure } from "@headlessui/react";
import { DisclosureComponentType } from "../../../../types/components"; import { DisclosureComponentType } from "../../../../types/components";
import { ChevronRight } from "lucide-react";
export default function DisclosureComponent({ export default function DisclosureComponent({
button: { title, Icon, buttons = [] }, button: { title, Icon, buttons = [] },
@ -14,7 +14,7 @@ export default function DisclosureComponent({
<div> <div>
<Disclosure.Button className="select-none bg-muted dark:bg-gray-700/60 dark:border-y-gray-600 w-full flex justify-between items-center -mt-px px-3 py-2 border-y border-y-gray-200"> <Disclosure.Button className="select-none bg-muted dark:bg-gray-700/60 dark:border-y-gray-600 w-full flex justify-between items-center -mt-px px-3 py-2 border-y border-y-gray-200">
<div className="flex gap-4"> <div className="flex gap-4">
<Icon className="w-6 text-gray-800 dark:text-white/80" /> <Icon size={22} className="text-gray-800 dark:text-white/80" />
<span className="flex items-center text-sm text-gray-800 dark:text-white/80 font-medium"> <span className="flex items-center text-sm text-gray-800 dark:text-white/80 font-medium">
{title} {title}
</span> </span>
@ -26,7 +26,7 @@ export default function DisclosureComponent({
</button> </button>
))} ))}
<div> <div>
<ChevronRightIcon <ChevronRight
className={`${ className={`${
open || openDisc ? "rotate-90 transform" : "" open || openDisc ? "rotate-90 transform" : ""
} h-4 w-4 text-gray-800 dark:text-white`} } h-4 w-4 text-gray-800 dark:text-white`}

View file

@ -398,6 +398,8 @@ export default function Page({ flow }: { flow: FlowType }) {
zoomOnDoubleClick={!disableCopyPaste} zoomOnDoubleClick={!disableCopyPaste}
selectNodesOnDrag={false} selectNodesOnDrag={false}
className="theme-attribution" className="theme-attribution"
minZoom={0.01}
maxZoom={8}
> >
<Background className="dark:bg-gray-900" /> <Background className="dark:bg-gray-900" />
<Controls className="[&>button]:text-black [&>button]:dark:bg-gray-800 hover:[&>button]:dark:bg-gray-700 [&>button]:dark:text-gray-400 [&>button]:dark:fill-gray-400 [&>button]:dark:border-gray-600"></Controls> <Controls className="[&>button]:text-black [&>button]:dark:bg-gray-800 hover:[&>button]:dark:bg-gray-700 [&>button]:dark:text-gray-400 [&>button]:dark:fill-gray-400 [&>button]:dark:border-gray-600"></Controls>

View file

@ -1,25 +1,23 @@
import { Bars2Icon } from "@heroicons/react/24/outline";
import DisclosureComponent from "../DisclosureComponent"; import DisclosureComponent from "../DisclosureComponent";
import { import {
classNames, classNames,
nodeColors, nodeColors,
nodeIcons, nodeIconsLucide,
nodeNames, nodeNames,
} from "../../../../utils"; } from "../../../../utils";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { typesContext } from "../../../../contexts/typesContext"; import { typesContext } from "../../../../contexts/typesContext";
import { APIClassType, APIObjectType } from "../../../../types/api"; import { APIClassType, APIObjectType } from "../../../../types/api";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import ShadTooltip from "../../../../components/ShadTooltipComponent"; import ShadTooltip from "../../../../components/ShadTooltipComponent";
import { Code2, FileDown, FileUp, Save } from "lucide-react"; import { Code2, FileDown, FileUp, Save, Search } from "lucide-react";
import { PopUpContext } from "../../../../contexts/popUpContext"; import { PopUpContext } from "../../../../contexts/popUpContext";
import ExportModal from "../../../../modals/exportModal"; import ExportModal from "../../../../modals/exportModal";
import ApiModal from "../../../../modals/ApiModal"; import ApiModal from "../../../../modals/ApiModal";
import { TabsContext } from "../../../../contexts/tabsContext"; import { TabsContext } from "../../../../contexts/tabsContext";
import { alertContext } from "../../../../contexts/alertContext"; import { alertContext } from "../../../../contexts/alertContext";
import { updateFlowInDatabase } from "../../../../controllers/API";
import { INPUT_STYLE } from "../../../../constants"; import { INPUT_STYLE } from "../../../../constants";
import { Separator } from "../../../../components/ui/separator"; import { Separator } from "../../../../components/ui/separator";
import { Menu } from "lucide-react";
export default function ExtraSidebar() { export default function ExtraSidebar() {
const { data } = useContext(typesContext); const { data } = useContext(typesContext);
@ -135,7 +133,7 @@ export default function ExtraSidebar() {
}} }}
/> />
<div className="absolute inset-y-0 right-0 flex py-1.5 pr-3 items-center"> <div className="absolute inset-y-0 right-0 flex py-1.5 pr-3 items-center">
<MagnifyingGlassIcon className="h-5 w-5 dark:text-white"></MagnifyingGlassIcon> <Search size={20} color="#000000" />
</div> </div>
</div> </div>
@ -146,10 +144,10 @@ export default function ExtraSidebar() {
Object.keys(dataFilter[d]).length > 0 ? ( Object.keys(dataFilter[d]).length > 0 ? (
<DisclosureComponent <DisclosureComponent
openDisc={search.length == 0 ? false : true} openDisc={search.length == 0 ? false : true}
key={i} key={nodeNames[d]}
button={{ button={{
title: nodeNames[d] ?? nodeNames.unknown, title: nodeNames[d] ?? nodeNames.unknown,
Icon: nodeIcons[d] ?? nodeIcons.unknown, Icon: nodeIconsLucide[d] ?? nodeIconsLucide.unknown,
}} }}
> >
<div className="p-2 flex flex-col gap-2"> <div className="p-2 flex flex-col gap-2">
@ -157,9 +155,10 @@ export default function ExtraSidebar() {
.sort() .sort()
.map((t: string, k) => ( .map((t: string, k) => (
<ShadTooltip <ShadTooltip
content={t} content={data[d][t].display_name}
delayDuration={1500} delayDuration={1500}
side="right" side="right"
key={data[d][t].display_name}
> >
<div key={k} data-tooltip-id={t}> <div key={k} data-tooltip-id={t}>
<div <div
@ -185,9 +184,9 @@ export default function ExtraSidebar() {
> >
<div className="flex w-full justify-between text-sm px-3 py-1 bg-white dark:bg-gray-800 items-center border-dashed border-gray-400 dark:border-gray-600 border-l-0 rounded-md rounded-l-none border"> <div className="flex w-full justify-between text-sm px-3 py-1 bg-white dark:bg-gray-800 items-center border-dashed border-gray-400 dark:border-gray-600 border-l-0 rounded-md rounded-l-none border">
<span className="text-black dark:text-white w-full pr-1 truncate text-xs"> <span className="text-black dark:text-white w-full pr-1 truncate text-xs">
{t} {data[d][t].display_name}
</span> </span>
<Bars2Icon className="w-4 h-6 text-gray-400 dark:text-gray-600" /> <Menu className="w-4 h-6 text-gray-400 dark:text-gray-600" />
</div> </div>
</div> </div>
</div> </div>

View file

@ -11,6 +11,7 @@ export type APIClassType = {
base_classes: Array<string>; base_classes: Array<string>;
description: string; description: string;
template: APITemplateType; template: APITemplateType;
display_name: string;
[key: string]: Array<string> | string | APITemplateType; [key: string]: Array<string> | string | APITemplateType;
}; };
export type TemplateVariableType = { export type TemplateVariableType = {
@ -46,3 +47,8 @@ export type BuildStatusTypeAPI = {
export type InitTypeAPI = { export type InitTypeAPI = {
flowId: string; flowId: string;
}; };
export type UploadFileTypeAPI = {
file_path: string;
flowId: string;
};

View file

@ -1,4 +1,10 @@
import { ForwardRefExoticComponent, ReactElement, ReactNode } from "react"; import {
ComponentType,
ForwardRefExoticComponent,
ReactElement,
ReactNode,
SVGProps,
} from "react";
import { NodeDataType } from "../flow/index"; import { NodeDataType } from "../flow/index";
import { typesContextType } from "../typesContext"; import { typesContextType } from "../typesContext";
export type InputComponentType = { export type InputComponentType = {
@ -65,7 +71,7 @@ export type DisclosureComponentType = {
openDisc: boolean; openDisc: boolean;
button: { button: {
title: string; title: string;
Icon: ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>; Icon: any;
buttons?: { buttons?: {
Icon: ReactElement; Icon: ReactElement;
title: string; title: string;
@ -98,3 +104,14 @@ export type TooltipComponentType = {
| "top-start" | "top-start"
| "top"; | "top";
}; };
export type ProgressBarType = {
children?: ReactElement;
value?: number;
max?: number;
};
export type RadialProgressType = {
value?: number;
color?: string;
};

View file

@ -1,5 +1,3 @@
import { HomeIcon } from "@heroicons/react/24/outline";
export type sidebarNavigationItemType = { export type sidebarNavigationItemType = {
name: string; name: string;
href: string; href: string;

View file

@ -1,7 +1,8 @@
import { ReactFlowInstance } from "reactflow"; import { ReactFlowInstance } from "reactflow";
import { APIClassType } from "../api";
const types: { [char: string]: string } = {}; const types: { [char: string]: string } = {};
const template: { [char: string]: string } = {}; const template: { [char: string]: APIClassType } = {};
const data: { [char: string]: string } = {}; const data: { [char: string]: string } = {};
export type typesContextType = { export type typesContextType = {

View file

@ -39,9 +39,33 @@ import { OpenAiIcon } from "./icons/OpenAi";
import { QDrantIcon } from "./icons/QDrant"; import { QDrantIcon } from "./icons/QDrant";
import { SearxIcon } from "./icons/Searx"; import { SearxIcon } from "./icons/Searx";
import { SlackIcon } from "./icons/Slack"; import { SlackIcon } from "./icons/Slack";
import { PineconeIcon } from "./icons/Pinecone";
import clsx, { ClassValue } from "clsx"; import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { ADJECTIVES, DESCRIPTIONS, NOUNS } from "./constants"; import { ADJECTIVES, DESCRIPTIONS, NOUNS } from "./constants";
import { ComponentType, SVGProps } from "react";
import {
Boxes,
Cpu,
Fingerprint,
Gift,
Hammer,
HelpCircle,
Laptop2,
Layers,
LayoutDashboard,
Lightbulb,
Link,
MessageCircle,
Paperclip,
Rocket,
Scissors,
TerminalSquare,
Wand2,
Wrench,
} from "lucide-react";
import { SupabaseIcon } from "./icons/supabase";
import { MongoDBIcon } from "./icons/MongoDB";
export function classNames(...classes: Array<string>) { export function classNames(...classes: Array<string>) {
return classes.filter(Boolean).join(" "); return classes.filter(Boolean).join(" ");
@ -170,6 +194,9 @@ export const nodeIcons: {
ChatOpenAI: OpenAiIcon, ChatOpenAI: OpenAiIcon,
OpenAI: OpenAiIcon, OpenAI: OpenAiIcon,
OpenAIEmbeddings: OpenAiIcon, OpenAIEmbeddings: OpenAiIcon,
Pinecone: PineconeIcon,
SupabaseVectorStore: SupabaseIcon,
MongoDBAtlasVectorSearch: MongoDBIcon,
// UnstructuredPowerPointLoader: PowerPointIcon, // word and powerpoint have differente styles // UnstructuredPowerPointLoader: PowerPointIcon, // word and powerpoint have differente styles
Qdrant: QDrantIcon, Qdrant: QDrantIcon,
// ReadTheDocsLoader: ReadTheDocsIcon, // does not work // ReadTheDocsLoader: ReadTheDocsIcon, // does not work
@ -198,6 +225,142 @@ export const nodeIcons: {
unknown: QuestionMarkCircleIcon, unknown: QuestionMarkCircleIcon,
}; };
export const nodeIconsLucide: {
[char: string]: React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>;
} = {
Chroma: ChromaIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
AirbyteJSONLoader: AirbyteIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
Anthropic: AnthropicIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
ChatAnthropic: AnthropicIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
BingSearchAPIWrapper: BingIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
BingSearchRun: BingIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
Cohere: CohereIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
CohereEmbeddings: CohereIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
EverNoteLoader: EvernoteIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
FacebookChatLoader: FBIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
GitbookLoader: GitBookIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
GoogleSearchAPIWrapper: GoogleIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
GoogleSearchResults: GoogleIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
GoogleSearchRun: GoogleIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
HNLoader: HackerNewsIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
HuggingFaceHub: HugginFaceIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
HuggingFaceEmbeddings: HugginFaceIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
IFixitLoader: IFixIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
Meta: MetaIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
Midjorney: MidjorneyIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
NotionDirectoryLoader: NotionIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
ChatOpenAI: OpenAiIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
OpenAI: OpenAiIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
OpenAIEmbeddings: OpenAiIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
Qdrant: QDrantIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
Searx: SearxIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
SlackDirectoryLoader: SlackIcon as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
agents: Rocket as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
chains: Link as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
memories: Cpu as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
llms: Lightbulb as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
prompts: TerminalSquare as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
tools: Wrench as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
advanced: Laptop2 as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
chat: MessageCircle as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
embeddings: Fingerprint as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
documentloaders: Paperclip as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
vectorstores: Layers as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
toolkits: Hammer as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
textsplitters: Scissors as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
wrappers: Gift as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
utilities: Wand2 as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
unknown: HelpCircle as React.ForwardRefExoticComponent<
ComponentType<SVGProps<SVGSVGElement>>
>,
};
export const gradients = [ export const gradients = [
"bg-gradient-to-br from-gray-800 via-rose-700 to-violet-900", "bg-gradient-to-br from-gray-800 via-rose-700 to-violet-900",
"bg-gradient-to-br from-green-200 via-green-300 to-blue-500", "bg-gradient-to-br from-green-200 via-green-300 to-blue-500",
@ -403,9 +566,7 @@ export function toFirstUpperCase(str: string) {
} }
export function snakeToSpaces(str: string) { export function snakeToSpaces(str: string) {
let result = str.split("_").join(" "); return str.split("_").join(" ");
return result;
} }
export function toNormalCase(str: string) { export function toNormalCase(str: string) {
@ -449,10 +610,7 @@ export function roundNumber(x: number, decimals: number) {
export function getConnectedNodes(edge: Edge, nodes: Array<Node>): Array<Node> { export function getConnectedNodes(edge: Edge, nodes: Array<Node>): Array<Node> {
const sourceId = edge.source; const sourceId = edge.source;
const targetId = edge.target; const targetId = edge.target;
const connectedNodes = nodes.filter( return nodes.filter((node) => node.id === targetId || node.id === sourceId);
(node) => node.id === targetId || node.id === sourceId
);
return connectedNodes;
} }
export function isValidConnection( export function isValidConnection(
@ -699,7 +857,7 @@ export function groupByFamily(data, baseClasses) {
return foundIndex === index; return foundIndex === index;
}); });
let groupedObj = groupedBy.reduce((result, item) => { return groupedBy.reduce((result, item) => {
const existingGroup = result.find((group) => group.family === item.family); const existingGroup = result.find((group) => group.family === item.family);
if (existingGroup) { if (existingGroup) {
@ -710,8 +868,6 @@ export function groupByFamily(data, baseClasses) {
return result; return result;
}, []); }, []);
return groupedObj;
} }
export function buildTweaks(flow) { export function buildTweaks(flow) {
@ -752,9 +908,7 @@ export function validateNode(
) )
? [ ? [
`${type} is missing ${ `${type} is missing ${
template.display_name template.display_name || toNormalCase(template[t].name)
? template.display_name
: toNormalCase(template[t].name)
}.`, }.`,
] ]
: [] : []
@ -812,3 +966,10 @@ export function getRandomName(
// Return title case final name // Return title case final name
return toTitleCase(final_name); return toTitleCase(final_name);
} }
export function getRandomKeyByssmm(): string {
const now = new Date();
const seconds = String(now.getSeconds()).padStart(2, "0");
const milliseconds = String(now.getMilliseconds()).padStart(3, "0");
return seconds + milliseconds;
}

View file

@ -241,5 +241,6 @@ module.exports = {
}); });
}), }),
require("@tailwindcss/typography"), require("@tailwindcss/typography"),
require("daisyui"),
], ],
}; };

File diff suppressed because one or more lines are too long

View file

@ -97,7 +97,7 @@ def test_csv_agent(client: TestClient):
"name": "path", "name": "path",
"type": "file", "type": "file",
"list": False, "list": False,
"content": None, "file_path": None,
"advanced": False, "advanced": False,
} }
assert template["llm"] == { assert template["llm"] == {
@ -137,6 +137,7 @@ def test_initialize_agent(client: TestClient):
"self-ask-with-search", "self-ask-with-search",
"conversational-react-description", "conversational-react-description",
"openai-functions", "openai-functions",
"openai-multi-functions",
], ],
"name": "agent", "name": "agent",
"type": "str", "type": "str",

View file

@ -34,7 +34,7 @@ def test_template_field_defaults(sample_template_field: TemplateField):
assert sample_template_field.value is None assert sample_template_field.value is None
assert sample_template_field.suffixes == [] assert sample_template_field.suffixes == []
assert sample_template_field.file_types == [] assert sample_template_field.file_types == []
assert sample_template_field.content is None assert sample_template_field.file_path is None
assert sample_template_field.password is False assert sample_template_field.password is False
assert sample_template_field.name == "test_field" assert sample_template_field.name == "test_field"

View file

@ -1,3 +1,5 @@
import os
from pathlib import Path
from typing import Type, Union from typing import Type, Union
from langflow.graph.edge.base import Edge from langflow.graph.edge.base import Edge
from langflow.graph.vertex.base import Vertex from langflow.graph.vertex.base import Vertex
@ -236,6 +238,7 @@ def test_build_params(basic_graph):
root = get_root_node(basic_graph) root = get_root_node(basic_graph)
# Root node is a TimeTravelGuideChain # Root node is a TimeTravelGuideChain
# which requires an llm and memory # which requires an llm and memory
assert root is not None
assert isinstance(root.params, dict) assert isinstance(root.params, dict)
assert "llm" in root.params assert "llm" in root.params
assert "memory" in root.params assert "memory" in root.params
@ -269,7 +272,6 @@ def test_tool_node_build(complex_graph):
assert tool_node is not None assert tool_node is not None
built_object = tool_node.build() built_object = tool_node.build()
assert built_object is not None assert built_object is not None
# Add any further assertions specific to the ToolNode's build() method
def test_chain_node_build(complex_graph): def test_chain_node_build(complex_graph):
@ -277,7 +279,6 @@ def test_chain_node_build(complex_graph):
assert chain_node is not None assert chain_node is not None
built_object = chain_node.build() built_object = chain_node.build()
assert built_object is not None assert built_object is not None
# Add any further assertions specific to the ChainNode's build() method
def test_prompt_node_build(complex_graph): def test_prompt_node_build(complex_graph):
@ -285,7 +286,6 @@ def test_prompt_node_build(complex_graph):
assert prompt_node is not None assert prompt_node is not None
built_object = prompt_node.build() built_object = prompt_node.build()
assert built_object is not None assert built_object is not None
# Add any further assertions specific to the PromptNode's build() method
def test_llm_node_build(basic_graph): def test_llm_node_build(basic_graph):
@ -293,23 +293,36 @@ def test_llm_node_build(basic_graph):
assert llm_node is not None assert llm_node is not None
built_object = llm_node.build() built_object = llm_node.build()
assert built_object is not None assert built_object is not None
# Add any further assertions specific to the LLMNode's build() method
def test_toolkit_node_build(openapi_graph): def test_toolkit_node_build(openapi_graph):
# Write a file to the disk
file_path = "api-with-examples.yaml"
with open(file_path, "w") as f:
f.write("openapi: 3.0.0")
toolkit_node = get_node_by_type(openapi_graph, ToolkitVertex) toolkit_node = get_node_by_type(openapi_graph, ToolkitVertex)
assert toolkit_node is not None assert toolkit_node is not None
built_object = toolkit_node.build() built_object = toolkit_node.build()
assert built_object is not None assert built_object is not None
# Add any further assertions specific to the ToolkitNode's build() method # Remove the file
os.remove(file_path)
assert not Path(file_path).exists()
def test_file_tool_node_build(openapi_graph): def test_file_tool_node_build(openapi_graph):
file_path = "api-with-examples.yaml"
with open(file_path, "w") as f:
f.write("openapi: 3.0.0")
assert Path(file_path).exists()
file_tool_node = get_node_by_type(openapi_graph, FileToolVertex) file_tool_node = get_node_by_type(openapi_graph, FileToolVertex)
assert file_tool_node is not None assert file_tool_node is not None
built_object = file_tool_node.build() built_object = file_tool_node.build()
assert built_object is not None assert built_object is not None
# Add any further assertions specific to the FileToolNode's build() method # Remove the file
os.remove(file_path)
assert not Path(file_path).exists()
def test_wrapper_node_build(openapi_graph): def test_wrapper_node_build(openapi_graph):
@ -317,7 +330,6 @@ def test_wrapper_node_build(openapi_graph):
assert wrapper_node is not None assert wrapper_node is not None
built_object = wrapper_node.build() built_object = wrapper_node.build()
assert built_object is not None assert built_object is not None
# Add any further assertions specific to the WrapperNode's build() method
def test_get_result_and_thought(basic_graph): def test_get_result_and_thought(basic_graph):
@ -325,7 +337,7 @@ def test_get_result_and_thought(basic_graph):
responses = [ responses = [
"Final Answer: I am a response", "Final Answer: I am a response",
] ]
message = "Hello" message = {"input": "Hello"}
# Find the node that is an LLMNode and change the # Find the node that is an LLMNode and change the
# _built_object to a FakeListLLM # _built_object to a FakeListLLM
llm_node = get_node_by_type(basic_graph, LLMVertex) llm_node = get_node_by_type(basic_graph, LLMVertex)
@ -338,8 +350,5 @@ def test_get_result_and_thought(basic_graph):
# now build again and check if FakeListLLM was used # now build again and check if FakeListLLM was used
# Get the result and thought # Get the result and thought
result, thought = get_result_and_thought(langchain_object, message) result = get_result_and_thought(langchain_object, message)
# The result should be a str assert isinstance(result, dict)
assert isinstance(result, str)
# The thought should be a Thought
assert isinstance(thought, str)

View file

@ -369,7 +369,7 @@ def test_chat_open_ai(client: TestClient):
"placeholder": "", "placeholder": "",
"show": True, "show": True,
"multiline": False, "multiline": False,
"value": "gpt-3.5-turbo", "value": "gpt-3.5-turbo-0613",
"password": False, "password": False,
"options": [ "options": [
"gpt-3.5-turbo-0613", "gpt-3.5-turbo-0613",

View file

@ -59,6 +59,7 @@ def test_build_template_from_function():
# Test with valid name # Test with valid name
result = build_template_from_function("ExampleClass1", type_to_loader_dict) result = build_template_from_function("ExampleClass1", type_to_loader_dict)
assert result is not None
assert "template" in result assert "template" in result
assert "description" in result assert "description" in result
assert "base_classes" in result assert "base_classes" in result
@ -67,6 +68,7 @@ def test_build_template_from_function():
result_with_function = build_template_from_function( result_with_function = build_template_from_function(
"ExampleClass1", type_to_loader_dict, add_function=True "ExampleClass1", type_to_loader_dict, add_function=True
) )
assert result_with_function is not None
assert "function" in result_with_function["base_classes"] assert "function" in result_with_function["base_classes"]
# Test with invalid name # Test with invalid name
@ -80,6 +82,7 @@ def test_build_template_from_class():
# Test valid input # Test valid input
result = build_template_from_class("Child", type_to_cls_dict) result = build_template_from_class("Child", type_to_cls_dict)
assert result is not None
assert "template" in result assert "template" in result
assert "description" in result assert "description" in result
assert "base_classes" in result assert "base_classes" in result
@ -222,6 +225,7 @@ def test_format_dict():
"password": False, "password": False,
"multiline": False, "multiline": False,
"options": OPENAI_MODELS, "options": OPENAI_MODELS,
"value": "text-davinci-003",
}, },
} }
expected_output_openai_chat = { expected_output_openai_chat = {
@ -233,6 +237,7 @@ def test_format_dict():
"password": False, "password": False,
"multiline": False, "multiline": False,
"options": CHAT_OPENAI_MODELS, "options": CHAT_OPENAI_MODELS,
"value": "gpt-3.5-turbo-0613",
}, },
} }
assert format_dict(input_dict, "OpenAI") == expected_output_openai assert format_dict(input_dict, "OpenAI") == expected_output_openai