Merge branch 'zustand/io/migration' into cz/fixTestsIo
This commit is contained in:
commit
b6f27173e0
142 changed files with 2701 additions and 1587 deletions
|
|
@ -223,7 +223,7 @@ def build_and_cache_graph(
|
|||
flow: Flow = session.get(Flow, flow_id)
|
||||
if not flow or not flow.data:
|
||||
raise ValueError("Invalid flow ID")
|
||||
other_graph = Graph.from_payload(flow.data)
|
||||
other_graph = Graph.from_payload(flow.data, flow_id)
|
||||
if graph is None:
|
||||
graph = other_graph
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ from fastapi import (
|
|||
WebSocketException,
|
||||
status,
|
||||
)
|
||||
from fastapi.responses import StreamingResponse
|
||||
from loguru import logger
|
||||
from sqlmodel import Session
|
||||
|
||||
from langflow.api.utils import build_and_cache_graph, format_elapsed_time
|
||||
from langflow.api.v1.schemas import (
|
||||
ResultData,
|
||||
StreamData,
|
||||
VertexBuildResponse,
|
||||
VerticesOrderResponse,
|
||||
)
|
||||
|
|
@ -161,7 +163,7 @@ async def build_vertex(
|
|||
artifacts = vertex.artifacts
|
||||
else:
|
||||
raise ValueError(f"No result found for vertex {vertex_id}")
|
||||
chat_service.set_cache(flow_id, graph)
|
||||
|
||||
except Exception as exc:
|
||||
params = str(exc)
|
||||
valid = False
|
||||
|
|
@ -191,6 +193,7 @@ async def build_vertex(
|
|||
if graph.inactive_vertices:
|
||||
inactive_vertices = list(graph.inactive_vertices)
|
||||
graph.reset_inactive_vertices()
|
||||
chat_service.set_cache(flow_id, graph)
|
||||
|
||||
return VertexBuildResponse(
|
||||
inactive_vertices=inactive_vertices,
|
||||
|
|
@ -203,3 +206,51 @@ async def build_vertex(
|
|||
logger.error(f"Error building vertex: {exc}")
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
# Now onto an endpoint that is an SSE endpoint
|
||||
# it will receive a component_id and a flow_id
|
||||
#
|
||||
@router.get("/build/{flow_id}/{vertex_id}/stream", response_class=StreamingResponse)
|
||||
async def build_vertex_stream(
|
||||
flow_id: str,
|
||||
vertex_id: str,
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
):
|
||||
"""Build a vertex instead of the entire graph."""
|
||||
try:
|
||||
|
||||
async def stream_vertex():
|
||||
try:
|
||||
cache = chat_service.get_cache(flow_id)
|
||||
if not cache:
|
||||
# If there's no cache
|
||||
raise ValueError(f"No cache found for {flow_id}.")
|
||||
else:
|
||||
graph = cache.get("result")
|
||||
|
||||
vertex = graph.get_vertex(vertex_id)
|
||||
if not vertex.pinned or not vertex._built:
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"message": "Building vertex"},
|
||||
)
|
||||
yield str(stream_data)
|
||||
|
||||
async for chunk in vertex.stream():
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"chunk": chunk},
|
||||
)
|
||||
yield str(stream_data)
|
||||
else:
|
||||
raise ValueError(f"No result found for vertex {vertex_id}")
|
||||
|
||||
except Exception as exc:
|
||||
yield str(StreamData(event="error", data={"error": str(exc)}))
|
||||
|
||||
yield str(StreamData(event="close", data={"message": "Stream closed"}))
|
||||
|
||||
return StreamingResponse(stream_vertex(), media_type="text/event-stream")
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail="Error building vertex") from exc
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
from typing import Callable, Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
from langchain.chains import LLMChain
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, BasePromptTemplate, Chain, Text
|
||||
from langflow.field_typing import (
|
||||
BaseLanguageModel,
|
||||
BaseMemory,
|
||||
BasePromptTemplate,
|
||||
Text,
|
||||
)
|
||||
|
||||
|
||||
class LLMChainComponent(CustomComponent):
|
||||
|
|
@ -22,5 +28,10 @@ class LLMChainComponent(CustomComponent):
|
|||
prompt: BasePromptTemplate,
|
||||
llm: BaseLanguageModel,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
) -> Union[Chain, Callable, Text]:
|
||||
return LLMChain(prompt=prompt, llm=llm, memory=memory)
|
||||
) -> Text:
|
||||
runnable = LLMChain(prompt=prompt, llm=llm, memory=memory)
|
||||
result_dict = runnable.invoke({})
|
||||
output_key = runnable.output_key
|
||||
result = result_dict[output_key]
|
||||
self.status = result
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
from langchain.llms.base import BaseLLM
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class PromptRunner(CustomComponent):
|
||||
display_name: str = "Prompt Runner"
|
||||
description: str = "Run a Chain with the given PromptTemplate"
|
||||
beta: bool = True
|
||||
field_config = {
|
||||
"llm": {"display_name": "LLM"},
|
||||
"prompt": {
|
||||
"display_name": "Prompt Template",
|
||||
"info": "Make sure the prompt has all variables filled.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {}) -> Text:
|
||||
chain = prompt | llm
|
||||
# The input is an empty dict because the prompt is already filled
|
||||
result_message: BaseMessage = chain.invoke(input=inputs)
|
||||
if hasattr(result_message, "content"):
|
||||
result: str = result_message.content
|
||||
elif isinstance(result_message, str):
|
||||
result = result_message
|
||||
else:
|
||||
result = str(result_message)
|
||||
self.repr_value = result
|
||||
return result
|
||||
|
|
@ -3,6 +3,7 @@ from typing import Callable, Optional, Union
|
|||
from langchain.chains.combine_documents.base import BaseCombineDocumentsChain
|
||||
from langchain.chains.retrieval_qa.base import BaseRetrievalQA, RetrievalQA
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseMemory, BaseRetriever, Text
|
||||
|
||||
|
|
@ -47,4 +48,11 @@ class RetrievalQAComponent(CustomComponent):
|
|||
result = result.content if hasattr(result, "content") else result
|
||||
# Result is a dict with keys "query", "result" and "source_documents"
|
||||
# for now we just return the result
|
||||
return result.get("result")
|
||||
records = self.to_records(result.get("source_documents"))
|
||||
references_str = ""
|
||||
if return_source_documents:
|
||||
references_str = self.create_references_from_records(records)
|
||||
result_str = result.get("result")
|
||||
final_result = "\n".join([result_str, references_str])
|
||||
self.status = final_result
|
||||
return final_result
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.chains import RetrievalQAWithSourcesChain
|
||||
from langchain.chains.qa_with_sources.base import BaseQAWithSourcesChain
|
||||
from langchain.chains.combine_documents.base import BaseCombineDocumentsChain
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, BaseRetriever
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, BaseRetriever, Text
|
||||
|
||||
|
||||
class RetrievalQAWithSourcesChainComponent(CustomComponent):
|
||||
|
|
@ -18,25 +17,42 @@ class RetrievalQAWithSourcesChainComponent(CustomComponent):
|
|||
"chain_type": {
|
||||
"display_name": "Chain Type",
|
||||
"options": ["stuff", "map_reduce", "map_rerank", "refine"],
|
||||
"info": "The type of chain to use to combined Documents.",
|
||||
},
|
||||
"memory": {"display_name": "Memory"},
|
||||
"return_source_documents": {"display_name": "Return Source Documents"},
|
||||
"retriever": {"display_name": "Retriever"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
inputs: str,
|
||||
retriever: BaseRetriever,
|
||||
llm: BaseLanguageModel,
|
||||
combine_documents_chain: BaseCombineDocumentsChain,
|
||||
chain_type: str,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
return_source_documents: Optional[bool] = True,
|
||||
) -> BaseQAWithSourcesChain:
|
||||
return RetrievalQAWithSourcesChain.from_chain_type(
|
||||
) -> Text:
|
||||
runnable = RetrievalQAWithSourcesChain.from_chain_type(
|
||||
llm=llm,
|
||||
chain_type=chain_type,
|
||||
combine_documents_chain=combine_documents_chain,
|
||||
memory=memory,
|
||||
return_source_documents=return_source_documents,
|
||||
retriever=retriever,
|
||||
)
|
||||
if isinstance(inputs, Document):
|
||||
inputs = inputs.page_content
|
||||
self.status = runnable
|
||||
input_key = runnable.input_keys[0]
|
||||
result = runnable.invoke({input_key: inputs})
|
||||
result = result.content if hasattr(result, "content") else result
|
||||
# Result is a dict with keys "query", "result" and "source_documents"
|
||||
# for now we just return the result
|
||||
records = self.to_records(result.get("source_documents"))
|
||||
references_str = ""
|
||||
if return_source_documents:
|
||||
references_str = self.create_references_from_records(records)
|
||||
result_str = result.get("answer")
|
||||
final_result = "\n".join([result_str, references_str])
|
||||
self.status = final_result
|
||||
return final_result
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
from langflow import CustomComponent
|
||||
from typing import Callable, Union
|
||||
from langflow.field_typing import BasePromptTemplate, BaseLanguageModel, Chain
|
||||
from langchain_community.utilities.sql_database import SQLDatabase
|
||||
from langchain_experimental.sql.base import SQLDatabaseChain
|
||||
|
||||
|
||||
class SQLDatabaseChainComponent(CustomComponent):
|
||||
display_name = "SQLDatabaseChain"
|
||||
description = ""
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"db": {"display_name": "Database"},
|
||||
"llm": {"display_name": "LLM"},
|
||||
"prompt": {"display_name": "Prompt"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
db: SQLDatabase,
|
||||
llm: BaseLanguageModel,
|
||||
prompt: BasePromptTemplate,
|
||||
) -> Union[Chain, Callable, SQLDatabaseChain]:
|
||||
return SQLDatabaseChain.from_llm(llm=llm, db=db, prompt=prompt)
|
||||
53
src/backend/langflow/components/chains/SQLGenerator.py
Normal file
53
src/backend/langflow/components/chains/SQLGenerator.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.chains import create_sql_query_chain
|
||||
from langchain_community.utilities.sql_database import SQLDatabase
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, Text
|
||||
|
||||
|
||||
class SQLGeneratorComponent(CustomComponent):
|
||||
display_name = "Natural Language to SQL"
|
||||
description = "Generate SQL from natural language."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"db": {"display_name": "Database"},
|
||||
"llm": {"display_name": "LLM"},
|
||||
"prompt": {
|
||||
"display_name": "Prompt",
|
||||
"info": "The prompt must contain `{question}`.",
|
||||
},
|
||||
"top_k": {
|
||||
"display_name": "Top K",
|
||||
"info": "The number of results per select statement to return. If 0, no limit.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
inputs: Text,
|
||||
db: SQLDatabase,
|
||||
llm: BaseLanguageModel,
|
||||
top_k: int = 5,
|
||||
prompt: Optional[PromptTemplate] = None,
|
||||
) -> Text:
|
||||
if top_k > 0:
|
||||
kwargs = {
|
||||
"k": top_k,
|
||||
}
|
||||
if not prompt:
|
||||
sql_query_chain = create_sql_query_chain(llm=llm, db=db, **kwargs)
|
||||
else:
|
||||
template = prompt.template if hasattr(prompt, "template") else prompt
|
||||
# Check if {question} is in the prompt
|
||||
if "{question}" not in template or "question" not in template.input_variables:
|
||||
raise ValueError("Prompt must contain `{question}` to be used with Natural Language to SQL.")
|
||||
sql_query_chain = create_sql_query_chain(llm=llm, db=db, prompt=prompt, **kwargs)
|
||||
query_writer = sql_query_chain | {"query": lambda x: x.replace("SQLQuery:", "").strip()}
|
||||
response = query_writer.invoke({"question": inputs})
|
||||
query = response.get("query")
|
||||
self.status = query
|
||||
return query
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
from typing import Any, Dict, List
|
||||
|
||||
from langchain.docstore.document import Document
|
||||
from langchain.document_loaders.directory import DirectoryLoader
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class DirectoryLoaderComponent(CustomComponent):
|
||||
display_name = "DirectoryLoader"
|
||||
description = "Load from a directory."
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"glob": {"display_name": "Glob Pattern", "value": "**/*.txt"},
|
||||
"load_hidden": {"display_name": "Load Hidden Files", "value": False, "advanced": True},
|
||||
"max_concurrency": {"display_name": "Max Concurrency", "value": 10, "advanced": True},
|
||||
"metadata": {"display_name": "Metadata", "value": {}},
|
||||
"path": {"display_name": "Local Directory"},
|
||||
"recursive": {"display_name": "Recursive", "value": True, "advanced": True},
|
||||
"silent_errors": {"display_name": "Silent Errors", "value": False, "advanced": True},
|
||||
"use_multithreading": {"display_name": "Use Multithreading", "value": True, "advanced": True},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
glob: str,
|
||||
path: str,
|
||||
max_concurrency: int = 2,
|
||||
load_hidden: bool = False,
|
||||
recursive: bool = True,
|
||||
silent_errors: bool = False,
|
||||
use_multithreading: bool = True,
|
||||
) -> List[Document]:
|
||||
return DirectoryLoader(
|
||||
glob=glob,
|
||||
path=path,
|
||||
load_hidden=load_hidden,
|
||||
max_concurrency=max_concurrency,
|
||||
recursive=recursive,
|
||||
silent_errors=silent_errors,
|
||||
use_multithreading=use_multithreading,
|
||||
).load()
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from langchain_core.documents import Document
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.utils.constants import LOADERS_INFO
|
||||
|
||||
|
|
@ -38,6 +39,7 @@ class FileLoaderComponent(CustomComponent):
|
|||
"srt",
|
||||
"eml",
|
||||
"md",
|
||||
"mdx",
|
||||
"pptx",
|
||||
"docx",
|
||||
],
|
||||
|
|
@ -55,6 +57,7 @@ class FileLoaderComponent(CustomComponent):
|
|||
".srt",
|
||||
".eml",
|
||||
".md",
|
||||
".mdx",
|
||||
".pptx",
|
||||
".docx",
|
||||
],
|
||||
|
|
@ -74,7 +77,7 @@ class FileLoaderComponent(CustomComponent):
|
|||
def build(self, file_path: str, loader: str) -> Document:
|
||||
file_type = file_path.split(".")[-1]
|
||||
|
||||
# Mapeie o nome do loader selecionado para suas informações
|
||||
# Map the loader to the correct loader class
|
||||
selected_loader_info = None
|
||||
for loader_info in LOADERS_INFO:
|
||||
if loader_info["name"] == loader:
|
||||
|
|
@ -85,7 +88,7 @@ class FileLoaderComponent(CustomComponent):
|
|||
raise ValueError(f"Loader {loader} not found in the loader info list")
|
||||
|
||||
if loader == "Automatic":
|
||||
# Determine o loader automaticamente com base na extensão do arquivo
|
||||
# Determine the loader based on the file type
|
||||
default_loader_info = None
|
||||
for info in LOADERS_INFO:
|
||||
if "defaultFor" in info and file_type in info["defaultFor"]:
|
||||
|
|
@ -103,7 +106,7 @@ class FileLoaderComponent(CustomComponent):
|
|||
module_name, class_name = loader_import.rsplit(".", 1)
|
||||
|
||||
try:
|
||||
# Importe o loader dinamicamente
|
||||
# Import the loader class
|
||||
loader_module = __import__(module_name, fromlist=[class_name])
|
||||
loader_instance = getattr(loader_module, class_name)
|
||||
except ImportError as e:
|
||||
|
|
|
|||
129
src/backend/langflow/components/documentloaders/GatherRecords.py
Normal file
129
src/backend/langflow/components/documentloaders/GatherRecords.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
from concurrent import futures
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class GatherRecordsComponent(CustomComponent):
|
||||
display_name = "Gather Records"
|
||||
description = "Gather records from a directory."
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"load_hidden": {
|
||||
"display_name": "Load Hidden Files",
|
||||
"value": False,
|
||||
"advanced": True,
|
||||
},
|
||||
"max_concurrency": {
|
||||
"display_name": "Max Concurrency",
|
||||
"value": 10,
|
||||
"advanced": True,
|
||||
},
|
||||
"path": {"display_name": "Local Directory"},
|
||||
"recursive": {"display_name": "Recursive", "value": True, "advanced": True},
|
||||
"use_multithreading": {
|
||||
"display_name": "Use Multithreading",
|
||||
"value": True,
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def is_hidden(self, path: Path) -> bool:
|
||||
return path.name.startswith(".")
|
||||
|
||||
def retrieve_file_paths(
|
||||
self,
|
||||
path: str,
|
||||
types: List[str],
|
||||
load_hidden: bool,
|
||||
recursive: bool,
|
||||
depth: int,
|
||||
) -> List[str]:
|
||||
path_obj = Path(path)
|
||||
if not path_obj.exists() or not path_obj.is_dir():
|
||||
raise ValueError(f"Path {path} must exist and be a directory.")
|
||||
|
||||
def match_types(p: Path) -> bool:
|
||||
return any(p.suffix == f".{t}" for t in types) if types else True
|
||||
|
||||
def is_not_hidden(p: Path) -> bool:
|
||||
return not self.is_hidden(p) or load_hidden
|
||||
|
||||
def walk_level(directory: Path, max_depth: int):
|
||||
directory = directory.resolve()
|
||||
prefix_length = len(directory.parts)
|
||||
for p in directory.rglob("*" if recursive else "[!.]*"):
|
||||
if len(p.parts) - prefix_length <= max_depth:
|
||||
yield p
|
||||
|
||||
glob = "**/*" if recursive else "*"
|
||||
paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob)
|
||||
file_paths = [str(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)]
|
||||
|
||||
return file_paths
|
||||
|
||||
def parse_file_to_record(self, file_path: str, silent_errors: bool) -> Record:
|
||||
# Use the partition function to load the file
|
||||
from unstructured.partition.auto import partition
|
||||
|
||||
try:
|
||||
elements = partition(file_path)
|
||||
except Exception as e:
|
||||
if not silent_errors:
|
||||
raise ValueError(f"Error loading file {file_path}: {e}") from e
|
||||
return None
|
||||
|
||||
# Create a Record
|
||||
text = "\n\n".join([str(el) for el in elements])
|
||||
metadata = elements.metadata if hasattr(elements, "metadata") else {}
|
||||
metadata["file_path"] = file_path
|
||||
record = Record(text=text, data=metadata)
|
||||
return record
|
||||
|
||||
def get_elements(
|
||||
self,
|
||||
file_paths: List[str],
|
||||
silent_errors: bool,
|
||||
max_concurrency: int,
|
||||
use_multithreading: bool,
|
||||
) -> List[Record]:
|
||||
if use_multithreading:
|
||||
records = self.parallel_load_records(file_paths, silent_errors, max_concurrency)
|
||||
else:
|
||||
records = [self.parse_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
||||
records = list(filter(None, records))
|
||||
return records
|
||||
|
||||
def parallel_load_records(self, file_paths: List[str], silent_errors: bool, max_concurrency: int) -> List[Record]:
|
||||
with futures.ThreadPoolExecutor(max_workers=max_concurrency) as executor:
|
||||
loaded_files = executor.map(
|
||||
lambda file_path: self.parse_file_to_record(file_path, silent_errors),
|
||||
file_paths,
|
||||
)
|
||||
return loaded_files
|
||||
|
||||
def build(
|
||||
self,
|
||||
path: str,
|
||||
types: List[str] = None,
|
||||
depth: int = 0,
|
||||
max_concurrency: int = 2,
|
||||
load_hidden: bool = False,
|
||||
recursive: bool = True,
|
||||
silent_errors: bool = False,
|
||||
use_multithreading: bool = True,
|
||||
) -> List[Record]:
|
||||
resolved_path = self.resolve_path(path)
|
||||
file_paths = self.retrieve_file_paths(resolved_path, types, load_hidden, recursive, depth)
|
||||
loaded_records = []
|
||||
|
||||
if use_multithreading:
|
||||
loaded_records = self.parallel_load_records(file_paths, silent_errors, max_concurrency)
|
||||
else:
|
||||
loaded_records = [self.parse_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
||||
loaded_records = list(filter(None, loaded_records))
|
||||
self.status = loaded_records
|
||||
return loaded_records
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.embeddings import BedrockEmbeddings
|
||||
from langchain.embeddings.base import Embeddings
|
||||
from langchain_community.embeddings import BedrockEmbeddings
|
||||
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain.embeddings import VertexAIEmbeddings
|
||||
from langchain_community.embeddings import VertexAIEmbeddings
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
|
|
@ -9,17 +9,45 @@ class VertexAIEmbeddingsComponent(CustomComponent):
|
|||
|
||||
def build_config(self):
|
||||
return {
|
||||
"credentials": {"display_name": "Credentials", "value": "", "file_types": [".json"], "field_type": "file"},
|
||||
"instance": {"display_name": "instance", "advanced": True, "field_type": "dict"},
|
||||
"location": {"display_name": "Location", "value": "us-central1", "advanced": True},
|
||||
"credentials": {
|
||||
"display_name": "Credentials",
|
||||
"value": "",
|
||||
"file_types": [".json"],
|
||||
"field_type": "file",
|
||||
},
|
||||
"instance": {
|
||||
"display_name": "instance",
|
||||
"advanced": True,
|
||||
"field_type": "dict",
|
||||
},
|
||||
"location": {
|
||||
"display_name": "Location",
|
||||
"value": "us-central1",
|
||||
"advanced": True,
|
||||
},
|
||||
"max_output_tokens": {"display_name": "Max Output Tokens", "value": 128},
|
||||
"max_retries": {"display_name": "Max Retries", "value": 6, "advanced": True},
|
||||
"model_name": {"display_name": "Model Name", "value": "textembedding-gecko"},
|
||||
"max_retries": {
|
||||
"display_name": "Max Retries",
|
||||
"value": 6,
|
||||
"advanced": True,
|
||||
},
|
||||
"model_name": {
|
||||
"display_name": "Model Name",
|
||||
"value": "textembedding-gecko",
|
||||
},
|
||||
"n": {"display_name": "N", "value": 1, "advanced": True},
|
||||
"project": {"display_name": "Project", "advanced": True},
|
||||
"request_parallelism": {"display_name": "Request Parallelism", "value": 5, "advanced": True},
|
||||
"request_parallelism": {
|
||||
"display_name": "Request Parallelism",
|
||||
"value": 5,
|
||||
"advanced": True,
|
||||
},
|
||||
"stop": {"display_name": "Stop", "advanced": True},
|
||||
"streaming": {"display_name": "Streaming", "value": False, "advanced": True},
|
||||
"streaming": {
|
||||
"display_name": "Streaming",
|
||||
"value": False,
|
||||
"advanced": True,
|
||||
},
|
||||
"temperature": {"display_name": "Temperature", "value": 0.0},
|
||||
"top_k": {"display_name": "Top K", "value": 40, "advanced": True},
|
||||
"top_p": {"display_name": "Top P", "value": 0.95, "advanced": True},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from typing import Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
|
|
@ -25,9 +24,9 @@ class ChatInput(CustomComponent):
|
|||
"display_name": "Session ID",
|
||||
"info": "Session ID of the chat history.",
|
||||
},
|
||||
"as_record": {
|
||||
"display_name": "As Record",
|
||||
"info": "If true, the message will be returned as a Record.",
|
||||
"return_record": {
|
||||
"display_name": "Return Record",
|
||||
"info": "Return the message as a record containing the sender, sender_name, and session_id.",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -36,25 +35,24 @@ class ChatInput(CustomComponent):
|
|||
sender: Optional[str] = "User",
|
||||
sender_name: Optional[str] = "User",
|
||||
message: Optional[str] = None,
|
||||
as_record: Optional[bool] = False,
|
||||
session_id: Optional[str] = None,
|
||||
) -> Union[Text, Record]:
|
||||
self.status = message
|
||||
if as_record:
|
||||
return_record: Optional[bool] = False,
|
||||
) -> Record:
|
||||
if return_record:
|
||||
if isinstance(message, Record):
|
||||
# Update the data of the record
|
||||
message.data["sender"] = sender
|
||||
message.data["sender_name"] = sender_name
|
||||
message.data["session_id"] = session_id
|
||||
return message
|
||||
return Record(
|
||||
text=message,
|
||||
data={
|
||||
"sender": sender,
|
||||
"sender_name": sender_name,
|
||||
"session_id": session_id,
|
||||
},
|
||||
)
|
||||
else:
|
||||
message = Record(
|
||||
text=message,
|
||||
data={
|
||||
"sender": sender,
|
||||
"sender_name": sender_name,
|
||||
"session_id": session_id,
|
||||
},
|
||||
)
|
||||
if not message:
|
||||
message = ""
|
||||
self.status = message
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ class ChatOutput(CustomComponent):
|
|||
"info": "Session ID of the chat history.",
|
||||
"input_types": ["Text"],
|
||||
},
|
||||
"as_record": {
|
||||
"display_name": "As Record",
|
||||
"info": "If true, the message will be returned as a Record.",
|
||||
"return_record": {
|
||||
"display_name": "Return Record",
|
||||
"info": "Return the message as a record containing the sender, sender_name, and session_id.",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -40,25 +40,23 @@ class ChatOutput(CustomComponent):
|
|||
sender_name: Optional[str] = "AI",
|
||||
session_id: Optional[str] = None,
|
||||
message: Optional[str] = None,
|
||||
as_record: Optional[bool] = False,
|
||||
return_record: Optional[bool] = False,
|
||||
) -> Union[Text, Record]:
|
||||
self.status = message
|
||||
if as_record:
|
||||
if return_record:
|
||||
if isinstance(message, Record):
|
||||
# Update the data of the record
|
||||
message.data["sender"] = sender
|
||||
message.data["sender_name"] = sender_name
|
||||
message.data["session_id"] = session_id
|
||||
|
||||
return message
|
||||
return Record(
|
||||
text=message,
|
||||
data={
|
||||
"sender": sender,
|
||||
"sender_name": sender_name,
|
||||
"session_id": session_id,
|
||||
},
|
||||
)
|
||||
else:
|
||||
message = Record(
|
||||
text=message,
|
||||
data={
|
||||
"sender": sender,
|
||||
"sender_name": sender_name,
|
||||
"session_id": session_id,
|
||||
},
|
||||
)
|
||||
if not message:
|
||||
message = ""
|
||||
self.status = message
|
||||
|
|
|
|||
|
|
@ -9,9 +9,6 @@ class TextInput(CustomComponent):
|
|||
description = "Used to pass text input to the next component."
|
||||
|
||||
field_config = {
|
||||
"code": {
|
||||
"show": False,
|
||||
},
|
||||
"value": {"display_name": "Value"},
|
||||
}
|
||||
|
||||
|
|
|
|||
19
src/backend/langflow/components/io/TextOutput.py
Normal file
19
src/backend/langflow/components/io/TextOutput.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class TextOutput(CustomComponent):
|
||||
display_name = "Text Output"
|
||||
description = "Used to pass text output to the next component."
|
||||
|
||||
field_config = {
|
||||
"value": {"display_name": "Value"},
|
||||
}
|
||||
|
||||
def build(self, value: Optional[str] = "") -> Text:
|
||||
self.status = value
|
||||
if not value:
|
||||
value = ""
|
||||
return value
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.llms.base import BaseLLM
|
||||
from langchain.llms.bedrock import Bedrock
|
||||
from langchain_community.llms.bedrock import Bedrock
|
||||
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
|
||||
from langchain.llms.base import BaseLanguageModel
|
||||
from langchain_community.chat_models.azure_openai import AzureChatOpenAI
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
class AzureChatOpenAIComponent(CustomComponent):
|
||||
|
||||
class AzureChatOpenAISpecsComponent(CustomComponent):
|
||||
display_name: str = "AzureChatOpenAI"
|
||||
description: str = "LLM model from Azure OpenAI."
|
||||
documentation: str = "https://python.langchain.com/docs/integrations/llms/azure_openai"
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.llms.huggingface_endpoint import HuggingFaceEndpoint
|
||||
|
||||
from langchain.llms.base import BaseLLM
|
||||
from langchain.llms.huggingface_endpoint import HuggingFaceEndpoint
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class HuggingFaceEndpointsComponent(CustomComponent):
|
||||
|
|
@ -31,11 +32,11 @@ class HuggingFaceEndpointsComponent(CustomComponent):
|
|||
model_kwargs: Optional[dict] = None,
|
||||
) -> BaseLLM:
|
||||
try:
|
||||
output = HuggingFaceEndpoint(
|
||||
output = HuggingFaceEndpoint( # type: ignore
|
||||
endpoint_url=endpoint_url,
|
||||
task=task,
|
||||
huggingfacehub_api_token=huggingfacehub_api_token,
|
||||
model_kwargs=model_kwargs,
|
||||
model_kwargs=model_kwargs or {},
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValueError("Could not connect to HuggingFace Endpoints API.") from e
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain_community.chat_models.bedrock import BedrockChat
|
||||
from langflow.field_typing import Text
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class AmazonBedrockComponent(CustomComponent):
|
||||
display_name: str = "Amazon Bedrock model"
|
||||
display_name: str = "Amazon Bedrock Model"
|
||||
description: str = "Generate text using LLM model from Amazon Bedrock."
|
||||
|
||||
def build_config(self):
|
||||
|
|
@ -2,14 +2,13 @@ from typing import Optional
|
|||
|
||||
from langchain_community.chat_models.anthropic import ChatAnthropic
|
||||
from pydantic.v1 import SecretStr
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class AnthropicLLM(CustomComponent):
|
||||
display_name: str = "Anthropic model"
|
||||
display_name: str = "AnthropicModel"
|
||||
description: str = "Generate text using Anthropic Chat&Completion large language models."
|
||||
|
||||
def build_config(self):
|
||||
|
|
@ -67,7 +66,7 @@ class AnthropicLLM(CustomComponent):
|
|||
try:
|
||||
output = ChatAnthropic(
|
||||
model_name=model,
|
||||
anthropic_api_key=SecretStr(anthropic_api_key) if anthropic_api_key else None,
|
||||
anthropic_api_key=(SecretStr(anthropic_api_key) if anthropic_api_key else None),
|
||||
max_tokens_to_sample=max_tokens, # type: ignore
|
||||
temperature=temperature,
|
||||
anthropic_api_url=api_endpoint,
|
||||
|
|
@ -7,7 +7,7 @@ from langflow import CustomComponent
|
|||
|
||||
|
||||
class AzureChatOpenAIComponent(CustomComponent):
|
||||
display_name: str = "AzureOpenAI model"
|
||||
display_name: str = "AzureOpenAI Model"
|
||||
description: str = "Generate text using LLM model from Azure OpenAI."
|
||||
documentation: str = "https://python.langchain.com/docs/integrations/llms/azure_openai"
|
||||
beta = False
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
from typing import Dict, Optional
|
||||
|
||||
from langchain_community.llms.ctransformers import CTransformers
|
||||
from langflow.field_typing import Text
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class CTransformersComponent(CustomComponent):
|
||||
display_name = "CTransformers model"
|
||||
display_name = "CTransformersModel"
|
||||
description = "Generate text using CTransformers LLM models"
|
||||
documentation = "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/ctransformers"
|
||||
|
||||
|
|
@ -31,7 +31,14 @@ class CTransformersComponent(CustomComponent):
|
|||
"inputs": {"display_name": "Input"},
|
||||
}
|
||||
|
||||
def build(self, model: str, model_file: str, inputs: str, model_type: str, config: Optional[Dict] = None) -> Text:
|
||||
def build(
|
||||
self,
|
||||
model: str,
|
||||
model_file: str,
|
||||
inputs: str,
|
||||
model_type: str,
|
||||
config: Optional[Dict] = None,
|
||||
) -> Text:
|
||||
output = CTransformers(model=model, model_file=model_file, model_type=model_type, config=config)
|
||||
message = output.invoke(inputs)
|
||||
result = message.content if hasattr(message, "content") else message
|
||||
|
|
@ -1,18 +1,33 @@
|
|||
from langchain_community.chat_models.cohere import ChatCohere
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class CohereComponent(CustomComponent):
|
||||
display_name = "Cohere model"
|
||||
display_name = "CohereModel"
|
||||
description = "Generate text using Cohere large language models."
|
||||
documentation = "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/cohere"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"cohere_api_key": {"display_name": "Cohere API Key", "type": "password", "password": True},
|
||||
"max_tokens": {"display_name": "Max Tokens", "default": 256, "type": "int", "show": True},
|
||||
"temperature": {"display_name": "Temperature", "default": 0.75, "type": "float", "show": True},
|
||||
"cohere_api_key": {
|
||||
"display_name": "Cohere API Key",
|
||||
"type": "password",
|
||||
"password": True,
|
||||
},
|
||||
"max_tokens": {
|
||||
"display_name": "Max Tokens",
|
||||
"default": 256,
|
||||
"type": "int",
|
||||
"show": True,
|
||||
},
|
||||
"temperature": {
|
||||
"display_name": "Temperature",
|
||||
"default": 0.75,
|
||||
"type": "float",
|
||||
"show": True,
|
||||
},
|
||||
"inputs": {"display_name": "Input"},
|
||||
}
|
||||
|
||||
|
|
@ -23,8 +38,13 @@ class CohereComponent(CustomComponent):
|
|||
max_tokens: int = 256,
|
||||
temperature: float = 0.75,
|
||||
) -> Text:
|
||||
output = ChatCohere(cohere_api_key=cohere_api_key, max_tokens=max_tokens, temperature=temperature)
|
||||
output = ChatCohere(
|
||||
cohere_api_key=cohere_api_key,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
)
|
||||
message = output.invoke(inputs)
|
||||
result = message.content if hasattr(message, "content") else message
|
||||
self.status = result
|
||||
return result
|
||||
return result
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain_google_genai import ChatGoogleGenerativeAI # type: ignore
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import RangeSpec
|
||||
from pydantic.v1.types import SecretStr
|
||||
from langflow.field_typing import Text
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import RangeSpec, Text
|
||||
|
||||
|
||||
class GoogleGenerativeAIComponent(CustomComponent):
|
||||
display_name: str = "Google Generative AI model"
|
||||
display_name: str = "Google Generative AIModel"
|
||||
description: str = "Generate text using Google Generative AI to generate text."
|
||||
documentation: str = "http://docs.langflow.org/components/custom"
|
||||
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.llms.huggingface_endpoint import HuggingFaceEndpoint
|
||||
|
||||
from langchain_community.chat_models.huggingface import ChatHuggingFace
|
||||
from langchain_community.llms.huggingface_endpoint import HuggingFaceEndpoint
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
from typing import Optional, List, Dict, Any
|
||||
from langflow import CustomComponent
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from langchain_community.llms.llamacpp import LlamaCpp
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class LlamaCppComponent(CustomComponent):
|
||||
display_name = "LlamaCpp model"
|
||||
display_name = "LlamaCppModel"
|
||||
description = "Generate text using llama.cpp model."
|
||||
documentation = "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/llamacpp"
|
||||
|
||||
|
|
@ -17,7 +19,10 @@ class LlamaCppComponent(CustomComponent):
|
|||
"echo": {"display_name": "Echo", "advanced": True},
|
||||
"f16_kv": {"display_name": "F16 KV", "advanced": True},
|
||||
"grammar_path": {"display_name": "Grammar Path", "advanced": True},
|
||||
"last_n_tokens_size": {"display_name": "Last N Tokens Size", "advanced": True},
|
||||
"last_n_tokens_size": {
|
||||
"display_name": "Last N Tokens Size",
|
||||
"advanced": True,
|
||||
},
|
||||
"logits_all": {"display_name": "Logits All", "advanced": True},
|
||||
"logprobs": {"display_name": "Logprobs", "advanced": True},
|
||||
"lora_base": {"display_name": "Lora Base", "advanced": True},
|
||||
|
|
@ -134,3 +139,5 @@ class LlamaCppComponent(CustomComponent):
|
|||
result = message.content if hasattr(message, "content") else message
|
||||
self.status = result
|
||||
return result
|
||||
self.status = result
|
||||
return result
|
||||
|
|
@ -12,7 +12,7 @@ from langflow.field_typing import Text
|
|||
|
||||
|
||||
class ChatOllamaComponent(CustomComponent):
|
||||
display_name = "ChatOllama model"
|
||||
display_name = "ChatOllamaModel"
|
||||
description = "Generate text using Local LLM for chat with Ollama."
|
||||
|
||||
def build_config(self) -> dict:
|
||||
|
|
@ -7,7 +7,7 @@ from langflow.field_typing import Text
|
|||
|
||||
|
||||
class ChatVertexAIComponent(CustomComponent):
|
||||
display_name = "ChatVertexAI model"
|
||||
display_name = "ChatVertexAIModel"
|
||||
description = "Generate text using Vertex AI Chat large language models API."
|
||||
|
||||
def build_config(self):
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.retrievers import AmazonKendraRetriever
|
||||
|
||||
from langchain.schema import BaseRetriever
|
||||
from langchain_community.retrievers import AmazonKendraRetriever
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class AmazonKendraRetrieverComponent(CustomComponent):
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.retrievers import MetalRetriever
|
||||
|
||||
from langchain.schema import BaseRetriever
|
||||
from langchain_community.retrievers import MetalRetriever
|
||||
from metal_sdk.metal import Metal # type: ignore
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class MetalRetrieverComponent(CustomComponent):
|
||||
display_name: str = "Metal Retriever"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
from typing import List
|
||||
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class DocumentToRecordComponent(CustomComponent):
|
||||
display_name = "Documents to Records"
|
||||
description = "Convert documents to records."
|
||||
|
||||
field_config = {
|
||||
"documents": {"display_name": "Documents"},
|
||||
}
|
||||
|
||||
def build(self, documents: List[Document]) -> List[Record]:
|
||||
if isinstance(documents, Document):
|
||||
documents = [documents]
|
||||
records = [Record.from_document(document) for document in documents]
|
||||
self.status = records
|
||||
return records
|
||||
|
|
@ -27,7 +27,10 @@ class RecordsAsTextComponent(CustomComponent):
|
|||
if isinstance(records, Record):
|
||||
records = [records]
|
||||
|
||||
formated_records = [template.format(text=record.text, **record.data) for record in records]
|
||||
formated_records = [
|
||||
template.format(text=record.text, data=record.data, **record.data)
|
||||
for record in records
|
||||
]
|
||||
result_string = "\n".join(formated_records)
|
||||
self.status = result_string
|
||||
return result_string
|
||||
|
|
|
|||
22
src/backend/langflow/components/utilities/SQLDatabase.py
Normal file
22
src/backend/langflow/components/utilities/SQLDatabase.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from langchain_experimental.sql.base import SQLDatabase
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class SQLDatabaseComponent(CustomComponent):
|
||||
display_name = "SQLDatabase"
|
||||
description = "SQL Database"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"uri": {"display_name": "URI", "info": "URI to the database."},
|
||||
}
|
||||
|
||||
def clean_up_uri(self, uri: str) -> str:
|
||||
if uri.startswith("postgresql://"):
|
||||
uri = uri.replace("postgresql://", "postgres://")
|
||||
return uri.strip()
|
||||
|
||||
def build(self, uri: str) -> SQLDatabase:
|
||||
uri = self.clean_up_uri(uri)
|
||||
return SQLDatabase.from_uri(uri)
|
||||
56
src/backend/langflow/components/utilities/SQLExecutor.py
Normal file
56
src/backend/langflow/components/utilities/SQLExecutor.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from langchain_community.tools.sql_database.tool import QuerySQLDataBaseTool
|
||||
from langchain_experimental.sql.base import SQLDatabase
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class SQLExecutorComponent(CustomComponent):
|
||||
display_name = "SQL Executor"
|
||||
description = "Execute SQL query."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"database": {"display_name": "Database"},
|
||||
"include_columns": {
|
||||
"display_name": "Include Columns",
|
||||
"info": "Include columns in the result.",
|
||||
},
|
||||
"passthrough": {
|
||||
"display_name": "Passthrough",
|
||||
"info": "If an error occurs, return the query instead of raising an exception.",
|
||||
},
|
||||
"add_error": {
|
||||
"display_name": "Add Error",
|
||||
"info": "Add the error to the result.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
query: str,
|
||||
database: SQLDatabase,
|
||||
include_columns: bool = False,
|
||||
passthrough: bool = False,
|
||||
add_error: bool = False,
|
||||
) -> Text:
|
||||
error = None
|
||||
try:
|
||||
tool = QuerySQLDataBaseTool(db=database)
|
||||
result = tool.run(query, include_columns=include_columns)
|
||||
self.status = result
|
||||
except Exception as e:
|
||||
result = str(e)
|
||||
self.status = result
|
||||
if not passthrough:
|
||||
raise e
|
||||
error = repr(e)
|
||||
|
||||
if add_error and error is not None:
|
||||
result = f"{result}\n\nError: {error}\n\nQuery: {query}"
|
||||
elif error is not None:
|
||||
# Then we won't add the error to the result
|
||||
# but since we are in passthrough mode, we will return the query
|
||||
result = query
|
||||
|
||||
return result
|
||||
48
src/backend/langflow/components/utilities/ShouldRunNext.py
Normal file
48
src/backend/langflow/components/utilities/ShouldRunNext.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Implement ShouldRunNext component
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, Prompt
|
||||
|
||||
|
||||
class ShouldRunNext(CustomComponent):
|
||||
display_name = "Should Run Next"
|
||||
description = "Decides whether to run the next component."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"prompt": {
|
||||
"display_name": "Prompt",
|
||||
"info": "The prompt to use for the decision. It should generate a boolean response (True or False).",
|
||||
},
|
||||
"llm": {
|
||||
"display_name": "LLM",
|
||||
"info": "The language model to use for the decision.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(self, template: Prompt, llm: BaseLanguageModel, **kwargs) -> dict:
|
||||
# This is a simple component that always returns True
|
||||
prompt_template = PromptTemplate.from_template(template)
|
||||
|
||||
attributes_to_check = ["text", "page_content"]
|
||||
for key, value in kwargs.items():
|
||||
for attribute in attributes_to_check:
|
||||
if hasattr(value, attribute):
|
||||
kwargs[key] = getattr(value, attribute)
|
||||
|
||||
chain = prompt_template | llm
|
||||
result = chain.invoke(kwargs)
|
||||
if hasattr(result, "content") and isinstance(result.content, str):
|
||||
result = result.content
|
||||
elif isinstance(result, str):
|
||||
result = result
|
||||
else:
|
||||
result = result.get("response")
|
||||
|
||||
if result.lower() not in ["true", "false"]:
|
||||
raise ValueError("The prompt should generate a boolean response (True or False).")
|
||||
# The string should be the words true or false
|
||||
# if not raise an error
|
||||
bool_result = result.lower() == "true"
|
||||
return {"condition": bool_result, "result": kwargs}
|
||||
|
|
@ -5,6 +5,7 @@ from langchain.embeddings.base import Embeddings
|
|||
from langchain.schema import BaseRetriever, Document
|
||||
from langchain_community.vectorstores import VectorStore
|
||||
from langchain_community.vectorstores.chroma import Chroma
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
|
|
@ -83,7 +84,8 @@ class ChromaComponent(CustomComponent):
|
|||
|
||||
if chroma_server_host is not None:
|
||||
chroma_settings = chromadb.config.Settings(
|
||||
chroma_server_cors_allow_origins=chroma_server_cors_allow_origins or None,
|
||||
chroma_server_cors_allow_origins=chroma_server_cors_allow_origins
|
||||
or None,
|
||||
chroma_server_host=chroma_server_host,
|
||||
chroma_server_port=chroma_server_port or None,
|
||||
chroma_server_grpc_port=chroma_server_grpc_port or None,
|
||||
|
|
@ -98,7 +100,9 @@ class ChromaComponent(CustomComponent):
|
|||
|
||||
if documents is not None and embedding is not None:
|
||||
if len(documents) == 0:
|
||||
raise ValueError("If documents are provided, there must be at least one document.")
|
||||
raise ValueError(
|
||||
"If documents are provided, there must be at least one document."
|
||||
)
|
||||
chroma = Chroma.from_documents(
|
||||
documents=documents, # type: ignore
|
||||
persist_directory=index_directory,
|
||||
|
|
@ -107,5 +111,9 @@ class ChromaComponent(CustomComponent):
|
|||
client_settings=chroma_settings,
|
||||
)
|
||||
else:
|
||||
chroma = Chroma(persist_directory=index_directory, client_settings=chroma_settings)
|
||||
chroma = Chroma(
|
||||
persist_directory=index_directory,
|
||||
client_settings=chroma_settings,
|
||||
embedding_function=embedding,
|
||||
)
|
||||
return chroma
|
||||
|
|
|
|||
|
|
@ -109,32 +109,6 @@ embeddings:
|
|||
OllamaEmbeddings:
|
||||
documentation: "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/ollama"
|
||||
|
||||
llms:
|
||||
OpenAI:
|
||||
documentation: "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/openai"
|
||||
ChatOpenAI:
|
||||
documentation: "https://python.langchain.com/docs/modules/model_io/models/chat/integrations/openai"
|
||||
LlamaCpp:
|
||||
documentation: "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/llamacpp"
|
||||
CTransformers:
|
||||
documentation: "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/ctransformers"
|
||||
Cohere:
|
||||
documentation: "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/cohere"
|
||||
Anthropic:
|
||||
documentation: ""
|
||||
ChatAnthropic:
|
||||
documentation: "https://python.langchain.com/docs/modules/model_io/models/chat/integrations/anthropic"
|
||||
HuggingFaceHub:
|
||||
documentation: "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/huggingface_hub"
|
||||
VertexAI:
|
||||
documentation: "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/google_vertex_ai_palm"
|
||||
###
|
||||
# There's a bug in this component deactivating until we get it sorted: _language_models.py", line 804, in send_message
|
||||
# is_blocked=safety_attributes.get("blocked", False),
|
||||
# AttributeError: 'list' object has no attribute 'get'
|
||||
ChatVertexAI:
|
||||
documentation: "https://python.langchain.com/docs/modules/model_io/models/chat/integrations/google_vertex_ai_palm"
|
||||
###
|
||||
memories:
|
||||
# https://github.com/supabase-community/supabase-py/issues/482
|
||||
# ZepChatMessageHistory:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
from typing import TYPE_CHECKING, Any, List, Optional
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langflow.graph.edge.utils import build_clean_params
|
||||
from langflow.services.deps import get_monitor_service
|
||||
from langflow.services.monitor.utils import log_message
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
|
|
@ -135,11 +136,11 @@ class ContractEdge(Edge):
|
|||
|
||||
log_transaction(self, source, target, "success")
|
||||
# If the target vertex is a power component we log messages
|
||||
if (
|
||||
target.vertex_type == "ChatOutput"
|
||||
and isinstance(target.params.get("message"), str)
|
||||
or isinstance(target.params.get("message"), dict)
|
||||
if target.vertex_type == "ChatOutput" and (
|
||||
isinstance(target.params.get("message"), str) or isinstance(target.params.get("message"), dict)
|
||||
):
|
||||
if target.params.get("message") == "":
|
||||
return self.result
|
||||
await log_message(
|
||||
sender=target.params.get("sender", ""),
|
||||
sender_name=target.params.get("sender_name", ""),
|
||||
|
|
@ -168,3 +169,4 @@ def log_transaction(edge: ContractEdge, source: "Vertex", target: "Vertex", stat
|
|||
monitor_service.add_row(table_name="transactions", data=data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error logging transaction: {e}")
|
||||
logger.error(f"Error logging transaction: {e}")
|
||||
|
|
|
|||
|
|
@ -9,12 +9,8 @@ from langflow.graph.graph.constants import lazy_load_vertex_dict
|
|||
from langflow.graph.graph.utils import process_flow
|
||||
from langflow.graph.schema import InterfaceComponentTypes
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.graph.vertex.types import (
|
||||
ChatVertex,
|
||||
FileToolVertex,
|
||||
LLMVertex,
|
||||
ToolkitVertex,
|
||||
)
|
||||
from langflow.graph.vertex.types import (ChatVertex, FileToolVertex, LLMVertex,
|
||||
RoutingVertex, ToolkitVertex)
|
||||
from langflow.interface.tools.constants import FILE_TOOLS
|
||||
from langflow.utils import payload
|
||||
|
||||
|
|
@ -26,6 +22,7 @@ class Graph:
|
|||
self,
|
||||
nodes: List[Dict],
|
||||
edges: List[Dict[str, str]],
|
||||
flow_id: Optional[str] = None,
|
||||
) -> None:
|
||||
self.inputs = []
|
||||
self.outputs = []
|
||||
|
|
@ -34,6 +31,7 @@ class Graph:
|
|||
self.raw_graph_data = {"nodes": nodes, "edges": edges}
|
||||
self._runs = 0
|
||||
self._updates = 0
|
||||
self.flow_id = flow_id
|
||||
|
||||
self.top_level_vertices = []
|
||||
for vertex in self._vertices:
|
||||
|
|
@ -82,7 +80,9 @@ class Graph:
|
|||
def build_parent_child_map(self):
|
||||
parent_child_map = defaultdict(list)
|
||||
for vertex in self.vertices:
|
||||
parent_child_map[vertex.id] = [child.id for child in self.get_successors(vertex)]
|
||||
parent_child_map[vertex.id] = [
|
||||
child.id for child in self.get_successors(vertex)
|
||||
]
|
||||
return parent_child_map
|
||||
|
||||
def increment_run_count(self):
|
||||
|
|
@ -113,7 +113,7 @@ class Graph:
|
|||
return predecessor_map, successor_map
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload: Dict) -> "Graph":
|
||||
def from_payload(cls, payload: Dict, flow_id: str) -> "Graph":
|
||||
"""
|
||||
Creates a graph from a payload.
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ class Graph:
|
|||
try:
|
||||
vertices = payload["nodes"]
|
||||
edges = payload["edges"]
|
||||
return cls(vertices, edges)
|
||||
return cls(vertices, edges, flow_id)
|
||||
except KeyError as exc:
|
||||
logger.exception(exc)
|
||||
raise ValueError(
|
||||
|
|
@ -146,6 +146,28 @@ class Graph:
|
|||
# both graphs have the same vertices and edges
|
||||
# but the data of the vertices might be different
|
||||
|
||||
def update_edges_from_vertex(self, vertex: Vertex, other_vertex: Vertex) -> None:
|
||||
"""Updates the edges of a vertex in the Graph."""
|
||||
new_edges = []
|
||||
for edge in self.edges:
|
||||
if edge.source_id == other_vertex.id or edge.target_id == other_vertex.id:
|
||||
continue
|
||||
new_edges.append(edge)
|
||||
new_edges += other_vertex.edges
|
||||
self.edges = new_edges
|
||||
|
||||
def vertex_data_is_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool:
|
||||
return vertex.__repr__() == other_vertex.__repr__()
|
||||
|
||||
def vertex_edges_are_identical(self, vertex: Vertex, other_vertex: Vertex) -> bool:
|
||||
same_length = len(vertex.edges) == len(other_vertex.edges)
|
||||
if not same_length:
|
||||
return False
|
||||
for edge in vertex.edges:
|
||||
if edge not in other_vertex.edges:
|
||||
return False
|
||||
return True
|
||||
|
||||
def update(self, other: "Graph") -> None:
|
||||
# Existing vertices in self graph
|
||||
existing_vertex_ids = set(vertex.id for vertex in self.vertices)
|
||||
|
|
@ -162,9 +184,11 @@ class Graph:
|
|||
for vertex_id in existing_vertex_ids.intersection(other_vertex_ids):
|
||||
self_vertex = self.get_vertex(vertex_id)
|
||||
other_vertex = other.get_vertex(vertex_id)
|
||||
if self_vertex.__repr__() != other_vertex.__repr__():
|
||||
if not self.vertex_data_is_identical(self_vertex, other_vertex):
|
||||
self_vertex._data = other_vertex._data
|
||||
self_vertex._parse_data()
|
||||
# Now we update the edges of the vertex
|
||||
self.update_edges_from_vertex(self_vertex, other_vertex)
|
||||
self_vertex.params = {}
|
||||
self_vertex._build_params()
|
||||
self_vertex.graph = self
|
||||
|
|
@ -252,7 +276,11 @@ class Graph:
|
|||
return
|
||||
self.vertices.remove(vertex)
|
||||
self.vertex_map.pop(vertex_id)
|
||||
self.edges = [edge for edge in self.edges if edge.source_id != vertex_id and edge.target_id != vertex_id]
|
||||
self.edges = [
|
||||
edge
|
||||
for edge in self.edges
|
||||
if edge.source_id != vertex_id and edge.target_id != vertex_id
|
||||
]
|
||||
|
||||
def _build_vertex_params(self) -> None:
|
||||
"""Identifies and handles the LLM vertex within the graph."""
|
||||
|
|
@ -273,7 +301,9 @@ class Graph:
|
|||
return
|
||||
for vertex in self.vertices:
|
||||
if not self._validate_vertex(vertex):
|
||||
raise ValueError(f"{vertex.vertex_type} is not connected to any other components")
|
||||
raise ValueError(
|
||||
f"{vertex.display_name} is not connected to any other components"
|
||||
)
|
||||
|
||||
def _validate_vertex(self, vertex: Vertex) -> bool:
|
||||
"""Validates a vertex."""
|
||||
|
|
@ -289,7 +319,11 @@ class Graph:
|
|||
|
||||
def get_vertex_edges(self, vertex_id: str) -> List[ContractEdge]:
|
||||
"""Returns a list of edges for a given vertex."""
|
||||
return [edge for edge in self.edges if edge.source_id == vertex_id or edge.target_id == vertex_id]
|
||||
return [
|
||||
edge
|
||||
for edge in self.edges
|
||||
if edge.source_id == vertex_id or edge.target_id == vertex_id
|
||||
]
|
||||
|
||||
def get_vertices_with_target(self, vertex_id: str) -> List[Vertex]:
|
||||
"""Returns the vertices connected to a vertex."""
|
||||
|
|
@ -327,7 +361,9 @@ class Graph:
|
|||
def dfs(vertex):
|
||||
if state[vertex] == 1:
|
||||
# We have a cycle
|
||||
raise ValueError("Graph contains a cycle, cannot perform topological sort")
|
||||
raise ValueError(
|
||||
"Graph contains a cycle, cannot perform topological sort"
|
||||
)
|
||||
if state[vertex] == 0:
|
||||
state[vertex] = 1
|
||||
for edge in vertex.edges:
|
||||
|
|
@ -351,11 +387,17 @@ class Graph:
|
|||
|
||||
def get_predecessors(self, vertex):
|
||||
"""Returns the predecessors of a vertex."""
|
||||
return [self.get_vertex(source_id) for source_id in self.predecessor_map.get(vertex.id, [])]
|
||||
return [
|
||||
self.get_vertex(source_id)
|
||||
for source_id in self.predecessor_map.get(vertex.id, [])
|
||||
]
|
||||
|
||||
def get_successors(self, vertex):
|
||||
"""Returns the successors of a vertex."""
|
||||
return [self.get_vertex(target_id) for target_id in self.successor_map.get(vertex.id, [])]
|
||||
return [
|
||||
self.get_vertex(target_id)
|
||||
for target_id in self.successor_map.get(vertex.id, [])
|
||||
]
|
||||
|
||||
def get_vertex_neighbors(self, vertex: Vertex) -> Dict[Vertex, int]:
|
||||
"""Returns the neighbors of a vertex."""
|
||||
|
|
@ -394,16 +436,19 @@ class Graph:
|
|||
edges.append(ContractEdge(source, target, edge))
|
||||
return edges
|
||||
|
||||
def _get_vertex_class(self, node_type: str, node_base_type: str, node_id: str) -> Type[Vertex]:
|
||||
def _get_vertex_class(
|
||||
self, node_type: str, node_base_type: str, node_id: str
|
||||
) -> Type[Vertex]:
|
||||
"""Returns the node class based on the node type."""
|
||||
# First we check for the node_base_type
|
||||
node_name = node_id.split("-")[0]
|
||||
if node_name in ["ChatOutput", "ChatInput"]:
|
||||
return ChatVertex
|
||||
if node_base_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP:
|
||||
elif node_name in ["ShouldRunNext"]:
|
||||
return RoutingVertex
|
||||
elif node_base_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP:
|
||||
return lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_base_type]
|
||||
|
||||
if node_name in lazy_load_vertex_dict.VERTEX_TYPE_MAP:
|
||||
elif node_name in lazy_load_vertex_dict.VERTEX_TYPE_MAP:
|
||||
return lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_name]
|
||||
|
||||
if node_type in FILE_TOOLS:
|
||||
|
|
@ -424,14 +469,18 @@ class Graph:
|
|||
vertex_type: str = vertex_data["type"] # type: ignore
|
||||
vertex_base_type: str = vertex_data["node"]["template"]["_type"] # type: ignore
|
||||
|
||||
VertexClass = self._get_vertex_class(vertex_type, vertex_base_type, vertex_data["id"])
|
||||
VertexClass = self._get_vertex_class(
|
||||
vertex_type, vertex_base_type, vertex_data["id"]
|
||||
)
|
||||
vertex_instance = VertexClass(vertex, graph=self)
|
||||
vertex_instance.set_top_level(self.top_level_vertices)
|
||||
vertices.append(vertex_instance)
|
||||
|
||||
return vertices
|
||||
|
||||
def get_children_by_vertex_type(self, vertex: Vertex, vertex_type: str) -> List[Vertex]:
|
||||
def get_children_by_vertex_type(
|
||||
self, vertex: Vertex, vertex_type: str
|
||||
) -> List[Vertex]:
|
||||
"""Returns the children of a vertex based on the vertex type."""
|
||||
children = []
|
||||
vertex_types = [vertex.data["type"]]
|
||||
|
|
@ -443,7 +492,9 @@ class Graph:
|
|||
|
||||
def __repr__(self):
|
||||
vertex_ids = [vertex.id for vertex in self.vertices]
|
||||
edges_repr = "\n".join([f"{edge.source_id} --> {edge.target_id}" for edge in self.edges])
|
||||
edges_repr = "\n".join(
|
||||
[f"{edge.source_id} --> {edge.target_id}" for edge in self.edges]
|
||||
)
|
||||
return f"Graph:\nNodes: {vertex_ids}\nConnections:\n{edges_repr}"
|
||||
|
||||
def sort_up_to_vertex(self, vertex_id: str) -> "Graph":
|
||||
|
|
@ -474,7 +525,9 @@ class Graph:
|
|||
"""Performs a layered topological sort of the vertices in the graph."""
|
||||
|
||||
# Queue for vertices with no incoming edges
|
||||
queue = deque(vertex.id for vertex in vertices if self.in_degree_map[vertex.id] == 0)
|
||||
queue = deque(
|
||||
vertex.id for vertex in vertices if self.in_degree_map[vertex.id] == 0
|
||||
)
|
||||
layers = []
|
||||
|
||||
current_layer = 0
|
||||
|
|
@ -530,7 +583,9 @@ class Graph:
|
|||
|
||||
return refined_layers
|
||||
|
||||
def sort_chat_inputs_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
|
||||
def sort_chat_inputs_first(
|
||||
self, vertices_layers: List[List[str]]
|
||||
) -> List[List[str]]:
|
||||
chat_inputs_first = []
|
||||
for layer in vertices_layers:
|
||||
for vertex_id in layer:
|
||||
|
|
@ -558,11 +613,15 @@ class Graph:
|
|||
self.increment_run_count()
|
||||
return vertices_layers
|
||||
|
||||
def sort_interface_components_first(self, vertices_layers: List[List[str]]) -> List[List[str]]:
|
||||
def sort_interface_components_first(
|
||||
self, vertices_layers: List[List[str]]
|
||||
) -> List[List[str]]:
|
||||
"""Sorts the vertices in the graph so that vertices containing ChatInput or ChatOutput come first."""
|
||||
|
||||
def contains_interface_component(vertex):
|
||||
return any(component.value in vertex for component in InterfaceComponentTypes)
|
||||
return any(
|
||||
component.value in vertex for component in InterfaceComponentTypes
|
||||
)
|
||||
|
||||
# Sort each inner list so that vertices containing ChatInput or ChatOutput come first
|
||||
sorted_vertices = [
|
||||
|
|
@ -581,9 +640,13 @@ class Graph:
|
|||
"""Sorts the vertices in the graph so that vertices with the lowest average build time come first."""
|
||||
if len(vertices_ids) == 1:
|
||||
return vertices_ids
|
||||
vertices_ids.sort(key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time)
|
||||
vertices_ids.sort(
|
||||
key=lambda vertex_id: self.get_vertex(vertex_id).avg_build_time
|
||||
)
|
||||
|
||||
return vertices_ids
|
||||
|
||||
sorted_vertices = [sort_layer_by_avg_build_time(layer) for layer in vertices_layers]
|
||||
sorted_vertices = [
|
||||
sort_layer_by_avg_build_time(layer) for layer in vertices_layers
|
||||
]
|
||||
return sorted_vertices
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
from langflow.graph.vertex import types
|
||||
from langflow.interface.agents.base import agent_creator
|
||||
from langflow.interface.chains.base import chain_creator
|
||||
from langflow.interface.custom.base import custom_component_creator
|
||||
from langflow.interface.document_loaders.base import documentloader_creator
|
||||
from langflow.interface.embeddings.base import embedding_creator
|
||||
from langflow.interface.llms.base import llm_creator
|
||||
from langflow.interface.memories.base import memory_creator
|
||||
from langflow.interface.output_parsers.base import output_parser_creator
|
||||
from langflow.interface.prompts.base import prompt_creator
|
||||
|
|
@ -15,7 +13,8 @@ from langflow.interface.tools.base import tool_creator
|
|||
from langflow.interface.wrappers.base import wrapper_creator
|
||||
from langflow.utils.lazy_load import LazyLoadDictBase
|
||||
|
||||
chat_components = ["ChatInput", "ChatOutput", "TextInput", "SessionID"]
|
||||
CHAT_COMPONENTS = ["ChatInput", "ChatOutput", "TextInput", "SessionID"]
|
||||
ROUTING_COMPONENTS = ["ShouldRunNext"]
|
||||
|
||||
|
||||
class VertexTypesDict(LazyLoadDictBase):
|
||||
|
|
@ -37,11 +36,11 @@ class VertexTypesDict(LazyLoadDictBase):
|
|||
return {
|
||||
**{t: types.PromptVertex for t in prompt_creator.to_list()},
|
||||
**{t: types.AgentVertex for t in agent_creator.to_list()},
|
||||
**{t: types.ChainVertex for t in chain_creator.to_list()},
|
||||
# **{t: types.ChainVertex for t in chain_creator.to_list()},
|
||||
**{t: types.ToolVertex for t in tool_creator.to_list()},
|
||||
**{t: types.ToolkitVertex for t in toolkits_creator.to_list()},
|
||||
**{t: types.WrapperVertex for t in wrapper_creator.to_list()},
|
||||
**{t: types.LLMVertex for t in llm_creator.to_list()},
|
||||
# **{t: types.LLMVertex for t in llm_creator.to_list()},
|
||||
**{t: types.MemoryVertex for t in memory_creator.to_list()},
|
||||
**{t: types.EmbeddingVertex for t in embedding_creator.to_list()},
|
||||
# **{t: types.VectorStoreVertex for t in vectorstore_creator.to_list()},
|
||||
|
|
@ -50,7 +49,8 @@ class VertexTypesDict(LazyLoadDictBase):
|
|||
**{t: types.OutputParserVertex for t in output_parser_creator.to_list()},
|
||||
**{t: types.CustomComponentVertex for t in custom_component_creator.to_list()},
|
||||
**{t: types.RetrieverVertex for t in retriever_creator.to_list()},
|
||||
**{t: types.ChatVertex for t in chat_components},
|
||||
**{t: types.ChatVertex for t in CHAT_COMPONENTS},
|
||||
**{t: types.RoutingVertex for t in ROUTING_COMPONENTS},
|
||||
}
|
||||
|
||||
def get_custom_component_vertex_type(self):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import ast
|
|||
import inspect
|
||||
import types
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Dict, List, Optional
|
||||
from typing import (TYPE_CHECKING, Any, Callable, Coroutine, Dict, List,
|
||||
Optional)
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
|
@ -72,11 +73,17 @@ class Vertex:
|
|||
|
||||
def set_state(self, state: str):
|
||||
self.state = VertexStates[state]
|
||||
if self.state == VertexStates.INACTIVE and self.graph.in_degree_map[self.id] < 2:
|
||||
if (
|
||||
self.state == VertexStates.INACTIVE
|
||||
and self.graph.in_degree_map[self.id] < 2
|
||||
):
|
||||
# If the vertex is inactive and has only one in degree
|
||||
# it means that it is not a merge point in the graph
|
||||
self.graph.inactive_vertices.add(self.id)
|
||||
elif self.state == VertexStates.ACTIVE and self.id in self.graph.inactive_vertices:
|
||||
elif (
|
||||
self.state == VertexStates.ACTIVE
|
||||
and self.id in self.graph.inactive_vertices
|
||||
):
|
||||
self.graph.inactive_vertices.remove(self.id)
|
||||
|
||||
@property
|
||||
|
|
@ -104,7 +111,9 @@ class Vertex:
|
|||
):
|
||||
if edge.target_id not in edge_results:
|
||||
edge_results[edge.target_id] = {}
|
||||
edge_results[edge.target_id][edge.target_param] = await edge.get_result(source=self, target=target)
|
||||
edge_results[edge.target_id][edge.target_param] = await edge.get_result(
|
||||
source=self, target=target
|
||||
)
|
||||
return edge_results
|
||||
|
||||
def set_result(self, result: "ResultData") -> None:
|
||||
|
|
@ -114,7 +123,9 @@ class Vertex:
|
|||
# If the Vertex.type is a power component
|
||||
# then we need to return the built object
|
||||
# instead of the result dict
|
||||
if self.is_interface_component and not isinstance(self._built_object, UnbuiltObject):
|
||||
if self.is_interface_component and not isinstance(
|
||||
self._built_object, UnbuiltObject
|
||||
):
|
||||
result = self._built_object
|
||||
# if it is not a dict or a string and hasattr model_dump then
|
||||
# return the model_dump
|
||||
|
|
@ -124,7 +135,11 @@ class Vertex:
|
|||
|
||||
if isinstance(self._built_result, UnbuiltResult):
|
||||
return {}
|
||||
return self._built_result if isinstance(self._built_result, dict) else {"result": self._built_result}
|
||||
return (
|
||||
self._built_result
|
||||
if isinstance(self._built_result, dict)
|
||||
else {"result": self._built_result}
|
||||
)
|
||||
|
||||
def set_artifacts(self) -> None:
|
||||
pass
|
||||
|
|
@ -185,18 +200,31 @@ class Vertex:
|
|||
def _parse_data(self) -> None:
|
||||
self.data = self._data["data"]
|
||||
self.output = self.data["node"]["base_classes"]
|
||||
self.display_name = self.data["node"]["display_name"]
|
||||
self.pinned = self.data["node"].get("pinned", False)
|
||||
template_dicts = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)}
|
||||
template_dicts = {
|
||||
key: value
|
||||
for key, value in self.data["node"]["template"].items()
|
||||
if isinstance(value, dict)
|
||||
}
|
||||
|
||||
self.required_inputs = [
|
||||
template_dicts[key]["type"] for key, value in template_dicts.items() if value["required"]
|
||||
template_dicts[key]["type"]
|
||||
for key, value in template_dicts.items()
|
||||
if value["required"]
|
||||
]
|
||||
self.optional_inputs = [
|
||||
template_dicts[key]["type"] for key, value in template_dicts.items() if not value["required"]
|
||||
template_dicts[key]["type"]
|
||||
for key, value in template_dicts.items()
|
||||
if not value["required"]
|
||||
]
|
||||
# Add the template_dicts[key]["input_types"] to the optional_inputs
|
||||
self.optional_inputs.extend(
|
||||
[input_type for value in template_dicts.values() for input_type in value.get("input_types", [])]
|
||||
[
|
||||
input_type
|
||||
for value in template_dicts.values()
|
||||
for input_type in value.get("input_types", [])
|
||||
]
|
||||
)
|
||||
|
||||
template_dict = self.data["node"]["template"]
|
||||
|
|
@ -239,7 +267,11 @@ class Vertex:
|
|||
if self.graph is None:
|
||||
raise ValueError("Graph not found")
|
||||
|
||||
template_dict = {key: value for key, value in self.data["node"]["template"].items() if isinstance(value, dict)}
|
||||
template_dict = {
|
||||
key: value
|
||||
for key, value in self.data["node"]["template"].items()
|
||||
if isinstance(value, dict)
|
||||
}
|
||||
params = {}
|
||||
|
||||
for edge in self.edges:
|
||||
|
|
@ -277,7 +309,7 @@ class Vertex:
|
|||
full_path = storage_service.build_full_path(flow_id, file_name)
|
||||
params[key] = full_path
|
||||
else:
|
||||
raise ValueError(f"File path not found for {self.vertex_type}")
|
||||
raise ValueError(f"File path not found for {self.display_name}")
|
||||
elif value.get("type") in DIRECT_TYPES and params.get(key) is None:
|
||||
val = value.get("value")
|
||||
if value.get("type") == "code":
|
||||
|
|
@ -290,7 +322,11 @@ class Vertex:
|
|||
# list of dicts, so we need to convert it to a dict
|
||||
# before passing it to the build method
|
||||
if isinstance(val, list):
|
||||
params[key] = {k: v for item in value.get("value", []) for k, v in item.items()}
|
||||
params[key] = {
|
||||
k: v
|
||||
for item in value.get("value", [])
|
||||
for k, v in item.items()
|
||||
}
|
||||
elif isinstance(val, dict):
|
||||
params[key] = val
|
||||
elif value.get("type") == "int" and val is not None:
|
||||
|
|
@ -306,7 +342,10 @@ class Vertex:
|
|||
elif value.get("type") == "str" and val is not None:
|
||||
# val may contain escaped \n, \t, etc.
|
||||
# so we need to unescape it
|
||||
params[key] = val.encode().decode("unicode_escape")
|
||||
if isinstance(val, list):
|
||||
params[key] = [v.encode().decode("unicode_escape") for v in val]
|
||||
elif isinstance(val, str):
|
||||
params[key] = val.encode().decode("unicode_escape")
|
||||
elif val is not None and val != "":
|
||||
params[key] = val
|
||||
|
||||
|
|
@ -323,7 +362,7 @@ class Vertex:
|
|||
"""
|
||||
Initiate the build process.
|
||||
"""
|
||||
logger.debug(f"Building {self.vertex_type}")
|
||||
logger.debug(f"Building {self.display_name}")
|
||||
await self._build_each_node_in_params_dict(user_id)
|
||||
await self._get_and_instantiate_class(user_id)
|
||||
self._validate_built_object()
|
||||
|
|
@ -350,7 +389,9 @@ class Vertex:
|
|||
if isinstance(self._built_object, str):
|
||||
self._built_result = self._built_object
|
||||
|
||||
result = await generate_result(self._built_object, inputs, self.has_external_output, session_id)
|
||||
result = await generate_result(
|
||||
self._built_object, inputs, self.has_external_output, session_id
|
||||
)
|
||||
self._built_result = result
|
||||
|
||||
async def _build_each_node_in_params_dict(self, user_id=None):
|
||||
|
|
@ -378,7 +419,9 @@ class Vertex:
|
|||
"""
|
||||
return all(self._is_node(node) for node in value)
|
||||
|
||||
async def get_result(self, requester: Optional["Vertex"] = None, user_id=None, timeout=None) -> Any:
|
||||
async def get_result(
|
||||
self, requester: Optional["Vertex"] = None, user_id=None, timeout=None
|
||||
) -> Any:
|
||||
# PLEASE REVIEW THIS IF STATEMENT
|
||||
# Check if the Vertex was built already
|
||||
if self._built:
|
||||
|
|
@ -412,7 +455,9 @@ class Vertex:
|
|||
self._extend_params_list_with_result(key, result)
|
||||
self.params[key] = result
|
||||
|
||||
async def _build_list_of_nodes_and_update_params(self, key, nodes: List["Vertex"], user_id=None):
|
||||
async def _build_list_of_nodes_and_update_params(
|
||||
self, key, nodes: List["Vertex"], user_id=None
|
||||
):
|
||||
"""
|
||||
Iterates over a list of nodes, builds each and updates the params dictionary.
|
||||
"""
|
||||
|
|
@ -453,7 +498,7 @@ class Vertex:
|
|||
Gets the class from a dictionary and instantiates it with the params.
|
||||
"""
|
||||
if self.base_type is None:
|
||||
raise ValueError(f"Base type for node {self.vertex_type} not found")
|
||||
raise ValueError(f"Base type for node {self.display_name} not found")
|
||||
try:
|
||||
result = await loading.instantiate_class(
|
||||
node_type=self.vertex_type,
|
||||
|
|
@ -464,7 +509,9 @@ class Vertex:
|
|||
self._update_built_object_and_artifacts(result)
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise ValueError(f"Error building node {self.vertex_type}(ID:{self.id}): {str(exc)}") from exc
|
||||
raise ValueError(
|
||||
f"Error building node {self.display_name}: {str(exc)}"
|
||||
) from exc
|
||||
|
||||
def _update_built_object_and_artifacts(self, result):
|
||||
"""
|
||||
|
|
@ -480,9 +527,9 @@ class Vertex:
|
|||
Checks if the built object is None and raises a ValueError if so.
|
||||
"""
|
||||
if isinstance(self._built_object, UnbuiltObject):
|
||||
raise ValueError(f"{self.vertex_type}: {self._built_object_repr()}")
|
||||
raise ValueError(f"{self.display_name}: {self._built_object_repr()}")
|
||||
elif self._built_object is None:
|
||||
message = f"{self.vertex_type} returned None."
|
||||
message = f"{self.display_name} returned None."
|
||||
if self.base_type == "custom_components":
|
||||
message += " Make sure your build method returns a component."
|
||||
|
||||
|
|
@ -494,6 +541,7 @@ class Vertex:
|
|||
self._built_result = UnbuiltResult()
|
||||
self.artifacts = {}
|
||||
self.steps_ran = []
|
||||
self._build_params()
|
||||
|
||||
def build_inactive(self):
|
||||
# Just set the results to None
|
||||
|
|
@ -534,16 +582,24 @@ class Vertex:
|
|||
return self._built_object
|
||||
|
||||
# Get the requester edge
|
||||
requester_edge = next((edge for edge in self.edges if edge.target_id == requester.id), None)
|
||||
requester_edge = next(
|
||||
(edge for edge in self.edges if edge.target_id == requester.id), None
|
||||
)
|
||||
# Return the result of the requester edge
|
||||
return None if requester_edge is None else await requester_edge.get_result(source=self, target=requester)
|
||||
return (
|
||||
None
|
||||
if requester_edge is None
|
||||
else await requester_edge.get_result(source=self, target=requester)
|
||||
)
|
||||
|
||||
def add_edge(self, edge: "ContractEdge") -> None:
|
||||
if edge not in self.edges:
|
||||
self.edges.append(edge)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Vertex(id={self.id}, data={self.data})"
|
||||
return (
|
||||
f"Vertex(display_name={self.display_name}, id={self.id}, data={self.data})"
|
||||
)
|
||||
|
||||
def __eq__(self, __o: object) -> bool:
|
||||
try:
|
||||
|
|
@ -556,7 +612,11 @@ class Vertex:
|
|||
|
||||
def _built_object_repr(self):
|
||||
# Add a message with an emoji, stars for sucess,
|
||||
return "Built sucessfully ✨" if self._built_object is not None else "Failed to build 😵💫"
|
||||
return (
|
||||
"Built sucessfully ✨"
|
||||
if self._built_object is not None
|
||||
else "Failed to build 😵💫"
|
||||
)
|
||||
|
||||
|
||||
class StatefulVertex(Vertex):
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import ast
|
||||
import json
|
||||
from typing import Callable, Dict, List, Optional, Union
|
||||
from typing import AsyncIterator, Callable, Dict, Iterator, List, Optional, Union
|
||||
|
||||
import yaml
|
||||
from langchain_core.messages import AIMessage
|
||||
from loguru import logger
|
||||
|
||||
from langflow.graph.utils import UnbuiltObject, flatten_list
|
||||
from langflow.graph.vertex.base import StatefulVertex, StatelessVertex
|
||||
from langflow.interface.utils import extract_input_variables_from_prompt
|
||||
from langflow.schema import Record
|
||||
from langflow.services.monitor.utils import log_message
|
||||
from langflow.utils.schemas import ChatOutputResponse
|
||||
|
||||
|
||||
|
|
@ -65,7 +67,7 @@ class LLMVertex(StatelessVertex):
|
|||
class_built_object = None
|
||||
|
||||
def __init__(self, data: Dict, graph, params: Optional[Dict] = None):
|
||||
super().__init__(data, graph=graph, base_type="llms", params=params)
|
||||
super().__init__(data, graph=graph, base_type="models", params=params)
|
||||
self.steps: List[Callable] = [self._custom_build]
|
||||
|
||||
async def _custom_build(self, *args, **kwargs):
|
||||
|
|
@ -120,10 +122,12 @@ class DocumentLoaderVertex(StatefulVertex):
|
|||
# show how many documents are in the list?
|
||||
|
||||
if not isinstance(self._built_object, UnbuiltObject):
|
||||
avg_length = sum(len(doc.page_content) for doc in self._built_object if hasattr(doc, "page_content")) / len(
|
||||
self._built_object
|
||||
)
|
||||
return f"""{self.vertex_type}({len(self._built_object)} documents)
|
||||
avg_length = sum(
|
||||
len(doc.page_content)
|
||||
for doc in self._built_object
|
||||
if hasattr(doc, "page_content")
|
||||
) / len(self._built_object)
|
||||
return f"""{self.display_name}({len(self._built_object)} documents)
|
||||
\nAvg. Document Length (characters): {int(avg_length)}
|
||||
Documents: {self._built_object[:3]}..."""
|
||||
return f"{self.vertex_type}()"
|
||||
|
|
@ -195,7 +199,9 @@ class TextSplitterVertex(StatefulVertex):
|
|||
# show how many documents are in the list?
|
||||
|
||||
if not isinstance(self._built_object, UnbuiltObject):
|
||||
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(self._built_object)
|
||||
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(
|
||||
self._built_object
|
||||
)
|
||||
return f"""{self.vertex_type}({len(self._built_object)} documents)
|
||||
\nAvg. Document Length (characters): {int(avg_length)}
|
||||
\nDocuments: {self._built_object[:3]}..."""
|
||||
|
|
@ -242,18 +248,27 @@ class PromptVertex(StatelessVertex):
|
|||
user_id = kwargs.get("user_id", None)
|
||||
tools = kwargs.get("tools", [])
|
||||
if not self._built or force:
|
||||
if "input_variables" not in self.params or self.params["input_variables"] is None:
|
||||
if (
|
||||
"input_variables" not in self.params
|
||||
or self.params["input_variables"] is None
|
||||
):
|
||||
self.params["input_variables"] = []
|
||||
# Check if it is a ZeroShotPrompt and needs a tool
|
||||
if "ShotPrompt" in self.vertex_type:
|
||||
tools = [tool_node.build(user_id=user_id) for tool_node in tools] if tools is not None else []
|
||||
tools = (
|
||||
[tool_node.build(user_id=user_id) for tool_node in tools]
|
||||
if tools is not None
|
||||
else []
|
||||
)
|
||||
# flatten the list of tools if it is a list of lists
|
||||
# first check if it is a list
|
||||
if tools and isinstance(tools, list) and isinstance(tools[0], list):
|
||||
tools = flatten_list(tools)
|
||||
self.params["tools"] = tools
|
||||
prompt_params = [
|
||||
key for key, value in self.params.items() if isinstance(value, str) and key != "format_instructions"
|
||||
key
|
||||
for key, value in self.params.items()
|
||||
if isinstance(value, str) and key != "format_instructions"
|
||||
]
|
||||
else:
|
||||
prompt_params = ["template"]
|
||||
|
|
@ -263,14 +278,20 @@ class PromptVertex(StatelessVertex):
|
|||
prompt_text = self.params[param]
|
||||
variables = extract_input_variables_from_prompt(prompt_text)
|
||||
self.params["input_variables"].extend(variables)
|
||||
self.params["input_variables"] = list(set(self.params["input_variables"]))
|
||||
self.params["input_variables"] = list(
|
||||
set(self.params["input_variables"])
|
||||
)
|
||||
elif isinstance(self.params, dict):
|
||||
self.params.pop("input_variables", None)
|
||||
|
||||
await self._build(user_id=user_id)
|
||||
|
||||
def _built_object_repr(self):
|
||||
if not self.artifacts or self._built_object is None or not hasattr(self._built_object, "format"):
|
||||
if (
|
||||
not self.artifacts
|
||||
or self._built_object is None
|
||||
or not hasattr(self._built_object, "format")
|
||||
):
|
||||
return super()._built_object_repr()
|
||||
elif isinstance(self._built_object, UnbuiltObject):
|
||||
return super()._built_object_repr()
|
||||
|
|
@ -282,7 +303,9 @@ class PromptVertex(StatelessVertex):
|
|||
# so the prompt format doesn't break
|
||||
artifacts.pop("handle_keys", None)
|
||||
try:
|
||||
if not hasattr(self._built_object, "template") and hasattr(self._built_object, "prompt"):
|
||||
if not hasattr(self._built_object, "template") and hasattr(
|
||||
self._built_object, "prompt"
|
||||
):
|
||||
template = self._built_object.prompt.template
|
||||
else:
|
||||
template = self._built_object.template
|
||||
|
|
@ -290,7 +313,11 @@ class PromptVertex(StatelessVertex):
|
|||
if value:
|
||||
replace_key = "{" + key + "}"
|
||||
template = template.replace(replace_key, value)
|
||||
return template if isinstance(template, str) else f"{self.vertex_type}({template})"
|
||||
return (
|
||||
template
|
||||
if isinstance(template, str)
|
||||
else f"{self.vertex_type}({template})"
|
||||
)
|
||||
except KeyError:
|
||||
return str(self._built_object)
|
||||
|
||||
|
|
@ -314,6 +341,20 @@ class ChatVertex(StatelessVertex):
|
|||
super().__init__(data, graph=graph, base_type="custom_components", is_task=True)
|
||||
self.steps = [self._build, self._run]
|
||||
|
||||
def build_stream_url(self):
|
||||
return f"/api/v1/build/{self.graph.flow_id}/{self.id}/stream"
|
||||
|
||||
async def _build(self, user_id=None):
|
||||
"""
|
||||
Initiate the build process.
|
||||
"""
|
||||
logger.debug(f"Building {self.vertex_type}")
|
||||
await self._build_each_node_in_params_dict(user_id)
|
||||
await self._get_and_instantiate_class(user_id)
|
||||
self._validate_built_object()
|
||||
|
||||
self._built = True
|
||||
|
||||
def _built_object_repr(self):
|
||||
if self.task_id and self.is_task:
|
||||
if task := self.get_task():
|
||||
|
|
@ -332,6 +373,8 @@ class ChatVertex(StatelessVertex):
|
|||
artifacts = None
|
||||
sender = self.params.get("sender", None)
|
||||
sender_name = self.params.get("sender_name", None)
|
||||
message = self.params.get("message", None)
|
||||
stream_url = None
|
||||
if isinstance(self._built_object, AIMessage):
|
||||
artifacts = ChatOutputResponse.from_message(
|
||||
self._built_object,
|
||||
|
|
@ -345,8 +388,13 @@ class ChatVertex(StatelessVertex):
|
|||
message = dict_to_codeblock(self._built_object)
|
||||
elif isinstance(self._built_object, Record):
|
||||
message = self._built_object.text
|
||||
elif isinstance(message, (AsyncIterator, Iterator)):
|
||||
stream_url = self.build_stream_url()
|
||||
message = ""
|
||||
elif not isinstance(self._built_object, str):
|
||||
message = str(self._built_object)
|
||||
# if the message is a generator or iterator
|
||||
# it means that it is a stream of messages
|
||||
else:
|
||||
message = self._built_object
|
||||
|
||||
|
|
@ -354,18 +402,60 @@ class ChatVertex(StatelessVertex):
|
|||
message=message,
|
||||
sender=sender,
|
||||
sender_name=sender_name,
|
||||
stream_url=stream_url,
|
||||
)
|
||||
if artifacts:
|
||||
self.artifacts = artifacts.model_dump()
|
||||
if isinstance(self._built_object, (AsyncIterator, Iterator)):
|
||||
if self.params["as_record"]:
|
||||
self._built_object = Record(text=message, data=self.artifacts)
|
||||
else:
|
||||
self._built_object = message
|
||||
self._built_result = self._built_object
|
||||
|
||||
else:
|
||||
await super()._run(*args, **kwargs)
|
||||
|
||||
async def stream(self):
|
||||
iterator = self.params.get("message", None)
|
||||
if not isinstance(iterator, (AsyncIterator, Iterator)):
|
||||
raise ValueError("The message must be an iterator or an async iterator.")
|
||||
is_async = isinstance(iterator, AsyncIterator)
|
||||
complete_message = ""
|
||||
if is_async:
|
||||
async for message in iterator:
|
||||
message = message.content if hasattr(message, "content") else message
|
||||
message = message.text if hasattr(message, "text") else message
|
||||
yield message
|
||||
complete_message += message
|
||||
else:
|
||||
for message in iterator:
|
||||
message = message.content if hasattr(message, "content") else message
|
||||
message = message.text if hasattr(message, "text") else message
|
||||
yield message
|
||||
complete_message += message
|
||||
self._built_object = Record(text=complete_message, data=self.artifacts)
|
||||
self._built_result = complete_message
|
||||
# Update artifacts with the message
|
||||
# and remove the stream_url
|
||||
self.artifacts = ChatOutputResponse(
|
||||
message=complete_message,
|
||||
sender=self.params.get("sender", ""),
|
||||
sender_name=self.params.get("sender_name", ""),
|
||||
).model_dump()
|
||||
|
||||
await log_message(
|
||||
sender=self.params.get("sender", ""),
|
||||
sender_name=self.params.get("sender_name", ""),
|
||||
message=complete_message,
|
||||
session_id=self.params.get("session_id", ""),
|
||||
artifacts=self.artifacts,
|
||||
)
|
||||
|
||||
|
||||
class RoutingVertex(StatelessVertex):
|
||||
def __init__(self, data: Dict, graph):
|
||||
super().__init__(data, graph=graph, base_type="routing")
|
||||
super().__init__(data, graph=graph, base_type="custom_components")
|
||||
self.use_result = True
|
||||
self.steps = [self._build, self._run]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import operator
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, ClassVar, List, Optional, Union
|
||||
from typing import Any, Callable, ClassVar, List, Optional, Sequence, Union
|
||||
from uuid import UUID
|
||||
|
||||
import yaml
|
||||
from cachetools import TTLCache, cachedmethod
|
||||
from fastapi import HTTPException
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow.interface.custom.code_parser.utils import (
|
||||
extract_inner_type_from_generic_alias,
|
||||
extract_union_types_from_generic_alias,
|
||||
)
|
||||
from langflow.interface.custom.custom_component.component import Component
|
||||
from langflow.schema import Record
|
||||
from langflow.services.database.models.flow import Flow
|
||||
from langflow.services.database.utils import session_getter
|
||||
from langflow.services.deps import (
|
||||
|
|
@ -86,6 +88,54 @@ class CustomComponent(Component):
|
|||
def tree(self):
|
||||
return self.get_code_tree(self.code or "")
|
||||
|
||||
def to_records(self, data: Any, text_key: str = "text", data_key: str = "data") -> List[dict]:
|
||||
"""
|
||||
Convert data into a list of records.
|
||||
|
||||
Args:
|
||||
data (Any): The input data to be converted.
|
||||
text_key (str, optional): The key to extract the text from a dictionary item. Defaults to "text".
|
||||
data_key (str, optional): The key to extract the data from a dictionary item. Defaults to "data".
|
||||
|
||||
Returns:
|
||||
List[dict]: A list of records, where each record is a dictionary with 'text' and 'data' keys.
|
||||
"""
|
||||
records = []
|
||||
if not isinstance(data, Sequence):
|
||||
data = [data]
|
||||
for item in data:
|
||||
if isinstance(item, str):
|
||||
records.append(Record(text=item))
|
||||
elif isinstance(item, dict):
|
||||
records.append(Record(text=item.get(text_key), data=item.get(data_key)))
|
||||
elif isinstance(item, Document):
|
||||
records.append(Record(text=item.page_content, data=item.metadata))
|
||||
else:
|
||||
raise ValueError(f"Invalid data type: {type(item)}")
|
||||
|
||||
return records
|
||||
|
||||
def create_references_from_records(self, records: List[dict], include_data: bool = False) -> str:
|
||||
"""
|
||||
Create references from a list of records.
|
||||
|
||||
Args:
|
||||
records (List[dict]): A list of records, where each record is a dictionary.
|
||||
include_data (bool, optional): Whether to include data in the references. Defaults to False.
|
||||
|
||||
Returns:
|
||||
str: A string containing the references in markdown format.
|
||||
"""
|
||||
if not records:
|
||||
return ""
|
||||
markdown_string = "---\n"
|
||||
for record in records:
|
||||
markdown_string += f"- Text: {record['text']}"
|
||||
if include_data:
|
||||
markdown_string += f" Data: {record['data']}"
|
||||
markdown_string += "\n"
|
||||
return markdown_string
|
||||
|
||||
@property
|
||||
def get_function_entrypoint_args(self) -> list:
|
||||
build_method = self.get_build_method()
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ def update_field_dict(field_dict):
|
|||
field_dict["refresh"] = True
|
||||
|
||||
if "value" in field_dict and callable(field_dict["value"]):
|
||||
field_dict["value"] = field_dict["value"](field_dict.get("options", []))
|
||||
field_dict["value"] = field_dict["value"]()
|
||||
field_dict["refresh"] = True
|
||||
|
||||
# Let's check if "range_spec" is a RangeSpec object
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ def import_by_type(_type: str, name: str) -> Any:
|
|||
func_dict = {
|
||||
"agents": import_agent,
|
||||
"prompts": import_prompt,
|
||||
"llms": {"llm": import_llm, "chat": import_chat_llm},
|
||||
"models": {"llm": import_llm, "chat": import_chat_llm},
|
||||
"tools": import_tool,
|
||||
"chains": import_chain,
|
||||
"toolkits": import_toolkit,
|
||||
|
|
@ -50,7 +50,7 @@ def import_by_type(_type: str, name: str) -> Any:
|
|||
"retrievers": import_retriever,
|
||||
"custom_components": import_custom_component,
|
||||
}
|
||||
if _type == "llms":
|
||||
if _type == "models":
|
||||
key = "chat" if "chat" in name.lower() else "llm"
|
||||
loaded_func = func_dict[_type][key] # type: ignore
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@ from langflow.interface.custom.utils import get_function
|
|||
from langflow.interface.custom_lists import CUSTOM_NODES
|
||||
from langflow.interface.importing.utils import import_by_type
|
||||
from langflow.interface.initialize.llm import initialize_vertexai
|
||||
from langflow.interface.initialize.utils import handle_format_kwargs, handle_node_type, handle_partial_variables
|
||||
from langflow.interface.initialize.utils import (
|
||||
handle_format_kwargs,
|
||||
handle_node_type,
|
||||
handle_partial_variables,
|
||||
)
|
||||
from langflow.interface.initialize.vector_store import vecstore_initializer
|
||||
from langflow.interface.output_parsers.base import output_parser_creator
|
||||
from langflow.interface.retrievers.base import retriever_creator
|
||||
|
|
@ -105,7 +109,7 @@ async def instantiate_based_on_type(class_object, base_type, node_type, params,
|
|||
return instantiate_chains(node_type, class_object, params)
|
||||
elif base_type == "output_parsers":
|
||||
return instantiate_output_parser(node_type, class_object, params)
|
||||
elif base_type == "llms":
|
||||
elif base_type == "models":
|
||||
return instantiate_llm(node_type, class_object, params)
|
||||
elif base_type == "retrievers":
|
||||
return instantiate_retriever(node_type, class_object, params)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
from typing import Dict, List, Optional, Type
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.custom_lists import llm_type_to_cls_dict
|
||||
from langflow.services.deps import get_settings_service
|
||||
|
||||
from langflow.template.frontend_node.llms import LLMFrontendNode
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class
|
||||
|
||||
|
||||
class LLMCreator(LangChainTypeCreator):
|
||||
type_name: str = "llms"
|
||||
type_name: str = "models"
|
||||
|
||||
@property
|
||||
def frontend_node_class(self) -> Type[LLMFrontendNode]:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from langflow.interface.embeddings.base import embedding_creator
|
|||
from langflow.interface.llms.base import llm_creator
|
||||
from langflow.interface.memories.base import memory_creator
|
||||
from langflow.interface.output_parsers.base import output_parser_creator
|
||||
from langflow.interface.prompts.base import prompt_creator
|
||||
from langflow.interface.retrievers.base import retriever_creator
|
||||
from langflow.interface.text_splitters.base import textsplitter_creator
|
||||
from langflow.interface.toolkits.base import toolkits_creator
|
||||
|
|
@ -39,7 +38,7 @@ def build_langchain_types_dict(): # sourcery skip: dict-assign-update-to-union
|
|||
creators = [
|
||||
chain_creator,
|
||||
agent_creator,
|
||||
prompt_creator,
|
||||
# prompt_creator,
|
||||
llm_creator,
|
||||
memory_creator,
|
||||
tool_creator,
|
||||
|
|
|
|||
|
|
@ -1,35 +1,23 @@
|
|||
from typing import ClassVar, Dict, List, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from langchain_community.utilities import requests, sql_database
|
||||
from langchain_community.utilities import requests
|
||||
from loguru import logger
|
||||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.utils.util import build_template_from_class, build_template_from_method
|
||||
from langflow.utils.util import build_template_from_class
|
||||
|
||||
|
||||
class WrapperCreator(LangChainTypeCreator):
|
||||
type_name: str = "wrappers"
|
||||
|
||||
from_method_nodes: ClassVar[Dict] = {"SQLDatabase": "from_uri"}
|
||||
|
||||
@property
|
||||
def type_to_loader_dict(self) -> Dict:
|
||||
if self.type_dict is None:
|
||||
self.type_dict = {
|
||||
wrapper.__name__: wrapper for wrapper in [requests.TextRequestsWrapper, sql_database.SQLDatabase]
|
||||
}
|
||||
self.type_dict = {wrapper.__name__: wrapper for wrapper in [requests.TextRequestsWrapper]}
|
||||
return self.type_dict
|
||||
|
||||
def get_signature(self, name: str) -> Optional[Dict]:
|
||||
try:
|
||||
if name in self.from_method_nodes:
|
||||
return build_template_from_method(
|
||||
name,
|
||||
type_to_cls_dict=self.type_to_loader_dict,
|
||||
add_function=True,
|
||||
method_name=self.from_method_nodes[name],
|
||||
)
|
||||
|
||||
return build_template_from_class(name, self.type_to_loader_dict)
|
||||
except ValueError as exc:
|
||||
raise ValueError("Wrapper not found") from exc
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ from starlette.websockets import WebSocket
|
|||
|
||||
from langflow.services.database.models.api_key.model import ApiKey
|
||||
from langflow.services.database.models.api_key.crud import check_key
|
||||
from langflow.services.database.models.user.crud import get_user_by_id, get_user_by_username, update_user_last_login_at
|
||||
from langflow.services.database.models.user.crud import (
|
||||
get_user_by_id,
|
||||
get_user_by_username,
|
||||
update_user_last_login_at,
|
||||
)
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
|
||||
|
|
@ -323,7 +327,7 @@ def add_padding(s):
|
|||
|
||||
def get_fernet(settings_service=Depends(get_settings_service)):
|
||||
SECRET_KEY = settings_service.auth_settings.SECRET_KEY
|
||||
# It's important that your secret key is 32 url-safe base64-encoded bytes
|
||||
# It's important that your secret key is 32 url-safe base64-encoded byte
|
||||
padded_secret_key = add_padding(SECRET_KEY)
|
||||
fernet = Fernet(padded_secret_key)
|
||||
return fernet
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
from typing import Any, Callable, Optional, Union
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
model_serializer,
|
||||
)
|
||||
|
||||
from langflow.field_typing.range_spec import RangeSpec
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_serializer, model_serializer
|
||||
|
||||
|
||||
class TemplateField(BaseModel):
|
||||
|
|
@ -28,7 +36,7 @@ class TemplateField(BaseModel):
|
|||
"""The value of the field. Default is None."""
|
||||
|
||||
file_types: list[str] = Field(default=[], serialization_alias="fileTypes")
|
||||
"""List of file types associated with the field. Default is an empty list. (duplicate)"""
|
||||
"""List of file types associated with the field . Default is an empty list."""
|
||||
|
||||
file_path: Optional[str] = ""
|
||||
"""The file path of the field if it is a file. Defaults to None."""
|
||||
|
|
@ -63,7 +71,7 @@ class TemplateField(BaseModel):
|
|||
range_spec: Optional[RangeSpec] = Field(default=None, serialization_alias="rangeSpec")
|
||||
"""Range specification for the field. Defaults to None."""
|
||||
|
||||
title_case: bool = True
|
||||
title_case: bool = False
|
||||
"""Specifies if the field should be displayed in title case. Defaults to True."""
|
||||
|
||||
def to_dict(self):
|
||||
|
|
@ -101,3 +109,12 @@ class TemplateField(BaseModel):
|
|||
if self.title_case:
|
||||
value = value.title()
|
||||
return value
|
||||
|
||||
@field_validator("file_types")
|
||||
def validate_file_types(cls, value):
|
||||
if not isinstance(value, list):
|
||||
raise ValueError("file_types must be a list")
|
||||
return [
|
||||
(f".{file_type}" if isinstance(file_type, str) and not file_type.startswith(".") else file_type)
|
||||
for file_type in value
|
||||
]
|
||||
|
|
|
|||
|
|
@ -158,8 +158,8 @@ LOADERS_INFO: List[Dict[str, Any]] = [
|
|||
"loader": "UnstructuredMarkdownLoader",
|
||||
"name": "Unstructured Markdown (.md)",
|
||||
"import": "langchain_community.document_loaders.UnstructuredMarkdownLoader",
|
||||
"defaultFor": ["md"],
|
||||
"allowdTypes": ["md"],
|
||||
"defaultFor": ["md", "mdx"],
|
||||
"allowdTypes": ["md", "mdx"],
|
||||
},
|
||||
{
|
||||
"loader": "UnstructuredPowerPointLoader",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class ChatOutputResponse(BaseModel):
|
|||
message: Union[str, List[Union[str, Dict]]]
|
||||
sender: Optional[str] = "Machine"
|
||||
sender_name: Optional[str] = "AI"
|
||||
stream_url: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def from_message(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ from typing import Dict, List, Optional, Union
|
|||
|
||||
from langflow.field_typing.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
|
||||
|
||||
|
||||
PROMPT_INPUT_TYPES = ["Document", "BaseOutputParser", "Text", "Record"]
|
||||
|
||||
|
||||
|
|
@ -204,7 +203,9 @@ def prepare_global_scope(code, module):
|
|||
for alias in node.names:
|
||||
exec_globals[alias.name] = getattr(imported_module, alias.name)
|
||||
except ModuleNotFoundError as e:
|
||||
raise ModuleNotFoundError(f"Module {node.module} not found. Please install it and try again.") from e
|
||||
raise ModuleNotFoundError(
|
||||
f"Module {node.module} not found. Please install it and try again. Error: {repr(e)}"
|
||||
)
|
||||
return exec_globals
|
||||
|
||||
|
||||
|
|
|
|||
39
src/frontend/package-lock.json
generated
39
src/frontend/package-lock.json
generated
|
|
@ -44,6 +44,7 @@
|
|||
"clsx": "^1.2.1",
|
||||
"dompurify": "^3.0.5",
|
||||
"esbuild": "^0.17.19",
|
||||
"framer-motion": "^11.0.6",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.331.0",
|
||||
"moment": "^2.29.4",
|
||||
|
|
@ -5918,6 +5919,44 @@
|
|||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "11.0.6",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.6.tgz",
|
||||
"integrity": "sha512-BpO3mWF8UwxzO3Ca5AmSkrg14QYTeJa9vKgoLOoBdBdTPj0e81i1dMwnX6EQJXRieUx20uiDBXq8bA6y7N6b8Q==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@emotion/is-prop-valid": "^0.8.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion/node_modules/@emotion/is-prop-valid": {
|
||||
"version": "0.8.8",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
|
||||
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "0.7.4"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion/node_modules/@emotion/memoize": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
|
||||
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
"clsx": "^1.2.1",
|
||||
"dompurify": "^3.0.5",
|
||||
"esbuild": "^0.17.19",
|
||||
"framer-motion": "^11.0.6",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.331.0",
|
||||
"moment": "^2.29.4",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import Router from "./routes";
|
|||
import useAlertStore from "./stores/alertStore";
|
||||
import { useDarkStore } from "./stores/darkStore";
|
||||
import useFlowsManagerStore from "./stores/flowsManagerStore";
|
||||
import { useStoreStore } from "./stores/storeStore";
|
||||
import { useTypesStore } from "./stores/typesStore";
|
||||
|
||||
export default function App() {
|
||||
|
|
@ -28,7 +29,6 @@ export default function App() {
|
|||
const tempNotificationList = useAlertStore(
|
||||
(state) => state.tempNotificationList
|
||||
);
|
||||
const loading = useAlertStore((state) => state.loading);
|
||||
const [fetchError, setFetchError] = useState(false);
|
||||
const isLoading = useFlowsManagerStore((state) => state.isLoading);
|
||||
|
||||
|
|
@ -38,9 +38,11 @@ export default function App() {
|
|||
|
||||
const { isAuthenticated } = useContext(AuthContext);
|
||||
const refreshFlows = useFlowsManagerStore((state) => state.refreshFlows);
|
||||
const fetchApiData = useStoreStore((state) => state.fetchApiData);
|
||||
const getTypes = useTypesStore((state) => state.getTypes);
|
||||
const refreshVersion = useDarkStore((state) => state.refreshVersion);
|
||||
const refreshStars = useDarkStore((state) => state.refreshStars);
|
||||
const checkHasStore = useStoreStore((state) => state.checkHasStore);
|
||||
|
||||
useEffect(() => {
|
||||
refreshStars();
|
||||
|
|
@ -52,6 +54,8 @@ export default function App() {
|
|||
getTypes().then(() => {
|
||||
refreshFlows();
|
||||
});
|
||||
checkHasStore();
|
||||
fetchApiData();
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
|
|
|
|||
|
|
@ -324,7 +324,9 @@ export default function ParameterComponent({
|
|||
) : (
|
||||
title
|
||||
)}
|
||||
<span className="text-status-red">{required ? " *" : ""}</span>
|
||||
<span className={(info === "" ? "" : "ml-1 ") + " text-status-red"}>
|
||||
{required ? " *" : ""}
|
||||
</span>
|
||||
<div className="">
|
||||
{info !== "" && (
|
||||
<ShadTooltip content={infoHtml.current}>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { NodeToolbar } from "reactflow";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import Tooltip from "../../components/TooltipComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Checkmark from "../../components/ui/checkmark";
|
||||
import Loading from "../../components/ui/loading";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import Xmark from "../../components/ui/xmark";
|
||||
import { priorityFields } from "../../constants/constants";
|
||||
import { BuildStatus } from "../../constants/enums";
|
||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { useTypesStore } from "../../stores/typesStore";
|
||||
|
|
@ -43,6 +45,7 @@ export default function GenericNode({
|
|||
const [nodeDescription, setNodeDescription] = useState(
|
||||
data.node?.description!
|
||||
);
|
||||
const buildStatus = useFlowStore((state) => state.flowBuildStatus[data.id]);
|
||||
const [validationStatus, setValidationStatus] =
|
||||
useState<validationStatusType | null>(null);
|
||||
const [handles, setHandles] = useState<number>(0);
|
||||
|
|
@ -89,14 +92,13 @@ export default function GenericNode({
|
|||
|
||||
// should be empty string if no duration
|
||||
// else should be `Duration: ${duration}`
|
||||
const getDurationString = (duration: number | null): string => {
|
||||
if (duration === null) {
|
||||
const getDurationString = (duration: number | undefined): string => {
|
||||
if (duration === undefined) {
|
||||
return "";
|
||||
} else {
|
||||
return `Duration: ${duration}`;
|
||||
}
|
||||
};
|
||||
|
||||
const durationString = getDurationString(validationStatus?.data.duration);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -118,10 +120,9 @@ export default function GenericNode({
|
|||
} else {
|
||||
setValidationStatus(null);
|
||||
}
|
||||
}, [flowPool, data.id]);
|
||||
}, [flowPool[data.id], data.id]);
|
||||
|
||||
const showNode = data.showNode ?? true;
|
||||
const pinned = data.node?.pinned ?? false;
|
||||
|
||||
const nameEditable = data.node?.flow || data.type === "CustomComponent";
|
||||
|
||||
|
|
@ -136,7 +137,6 @@ export default function GenericNode({
|
|||
const iconClassName = `generic-node-icon ${
|
||||
!showNode ? "absolute inset-x-6 h-12 w-12" : ""
|
||||
}`;
|
||||
|
||||
if (iconElement && isEmoji) {
|
||||
return nodeIconFragment(iconElement);
|
||||
} else {
|
||||
|
|
@ -158,36 +158,45 @@ export default function GenericNode({
|
|||
);
|
||||
};
|
||||
|
||||
const getIconPlayOrPauseComponent = (name, className) => (
|
||||
<IconComponent
|
||||
name={name}
|
||||
className={`absolute h-5 stroke-2 ${className} ml-0.5`}
|
||||
/>
|
||||
);
|
||||
|
||||
const getStatusClassName = (
|
||||
validationStatus: validationStatusType | null
|
||||
) => {
|
||||
if (validationStatus && validationStatus.valid) {
|
||||
return "green-status";
|
||||
} else if (validationStatus && !validationStatus.valid) {
|
||||
return "red-status";
|
||||
} else if (!validationStatus) {
|
||||
return "yellow-status";
|
||||
} else {
|
||||
return "status-build-animation";
|
||||
}
|
||||
};
|
||||
|
||||
const renderIconPlayOrPauseComponents = (
|
||||
const isDark = useDarkStore((state) => state.dark);
|
||||
const renderIconStatus = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null
|
||||
) => {
|
||||
if (buildStatus === BuildStatus.BUILDING) {
|
||||
return <Loading />;
|
||||
return <Loading className="text-medium-indigo" />;
|
||||
} else {
|
||||
const className = getStatusClassName(validationStatus);
|
||||
return <>{getIconPlayOrPauseComponent("Play", className)}</>;
|
||||
return (
|
||||
<>
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-medium-indigo opacity-0 transition-all group-hover:opacity-100"
|
||||
/>
|
||||
{validationStatus && validationStatus.valid ? (
|
||||
<Checkmark
|
||||
className="absolute ml-0.5 h-5 stroke-2 text-status-green opacity-100 transition-all group-hover:opacity-0"
|
||||
isVisible={true}
|
||||
/>
|
||||
) : validationStatus &&
|
||||
!validationStatus.valid &&
|
||||
buildStatus === BuildStatus.INACTIVE ? (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
) : validationStatus && !validationStatus.valid ? (
|
||||
<Xmark
|
||||
isVisible={true}
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground opacity-100 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -195,14 +204,18 @@ export default function GenericNode({
|
|||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null
|
||||
) => {
|
||||
if (
|
||||
buildStatus === BuildStatus.BUILT &&
|
||||
validationStatus &&
|
||||
!validationStatus.valid
|
||||
) {
|
||||
return "border-none ring ring-red-300";
|
||||
let isInvalid = validationStatus && !validationStatus.valid;
|
||||
|
||||
if (buildStatus === BuildStatus.INACTIVE && isInvalid) {
|
||||
// INACTIVE should have its own class
|
||||
return "inactive-status";
|
||||
}
|
||||
if (buildStatus === BuildStatus.BUILT && isInvalid) {
|
||||
return isDark
|
||||
? "border-none ring ring-[#751C1C]"
|
||||
: "built-invalid-status";
|
||||
} else if (buildStatus === BuildStatus.BUILDING) {
|
||||
return "border-none ring";
|
||||
return "building-status";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
|
@ -214,11 +227,17 @@ export default function GenericNode({
|
|||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null
|
||||
) => {
|
||||
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
|
||||
buildStatus,
|
||||
validationStatus
|
||||
);
|
||||
const baseBorderClass = getBaseBorderClass(selected);
|
||||
const nodeSizeClass = getNodeSizeClass(showNode);
|
||||
return classNames(
|
||||
getBaseBorderClass(selected),
|
||||
getNodeSizeClass(showNode),
|
||||
baseBorderClass,
|
||||
nodeSizeClass,
|
||||
"generic-node-div",
|
||||
getSpecificClassFromBuildStatus(buildStatus, validationStatus)
|
||||
specificClassFromBuildStatus
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -250,12 +269,11 @@ export default function GenericNode({
|
|||
onCloseAdvancedModal={() => {}}
|
||||
></NodeToolbarComponent>
|
||||
</NodeToolbar>
|
||||
|
||||
<div
|
||||
className={getNodeBorderClassName(
|
||||
selected,
|
||||
showNode,
|
||||
data?.build_status,
|
||||
buildStatus,
|
||||
validationStatus
|
||||
)}
|
||||
>
|
||||
|
|
@ -312,28 +330,37 @@ export default function GenericNode({
|
|||
</div>
|
||||
) : (
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
onDoubleClick={(event) => {
|
||||
if (nameEditable) {
|
||||
setInputName(true);
|
||||
}
|
||||
takeSnapshot();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-testid={"title-" + data.node?.display_name}
|
||||
className="generic-node-tooltip-div text-primary"
|
||||
>
|
||||
{data.node?.display_name}
|
||||
</div>
|
||||
<div className="group flex items-center gap-2.5">
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div
|
||||
onDoubleClick={(event) => {
|
||||
if (nameEditable) {
|
||||
setInputName(true);
|
||||
}
|
||||
takeSnapshot();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
data-testid={"title-" + data.node?.display_name}
|
||||
className="generic-node-tooltip-div text-primary"
|
||||
>
|
||||
{data.node?.display_name}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
{nameEditable && (
|
||||
<IconComponent
|
||||
name="Pencil"
|
||||
className="h-4 w-4 text-ring"
|
||||
/>
|
||||
<div
|
||||
onClick={(event) => {
|
||||
setInputName(true);
|
||||
takeSnapshot();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="Pencil"
|
||||
className="hidden h-4 w-4 animate-pulse text-status-blue group-hover:block"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
|
|
@ -431,80 +458,40 @@ export default function GenericNode({
|
|||
</div>
|
||||
{showNode && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-9 px-1.5"
|
||||
variant="secondary"
|
||||
className={"group h-9 px-1.5"}
|
||||
onClick={() => {
|
||||
setNode(data.id, (old) => ({
|
||||
...old,
|
||||
data: {
|
||||
...old.data,
|
||||
node: {
|
||||
...old.data.node,
|
||||
pinned: old.data?.node?.pinned ? false : true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title={<span>{pinned ? "Pin Output" : "Unpin Output"}</span>}
|
||||
>
|
||||
<div className="generic-node-status-position flex items-center">
|
||||
<IconComponent
|
||||
name={"Pin"}
|
||||
className={cn(
|
||||
"h-5 fill-transparent stroke-chat-trigger stroke-2 transition-all",
|
||||
pinned ? "animate-wiggle fill-chat-trigger" : ""
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
)}
|
||||
{showNode && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={"h-9 px-1.5"}
|
||||
onClick={() => {
|
||||
if (data?.build_status === BuildStatus.BUILDING || isBuilding)
|
||||
if (buildStatus === BuildStatus.BUILDING || isBuilding)
|
||||
return;
|
||||
setValidationStatus(null);
|
||||
buildFlow(data.id);
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Tooltip
|
||||
title={
|
||||
data?.build_status === BuildStatus.BUILDING ? (
|
||||
<ShadTooltip
|
||||
content={
|
||||
buildStatus === BuildStatus.BUILDING ? (
|
||||
<span>Building...</span>
|
||||
) : !validationStatus ? (
|
||||
<span className="flex">
|
||||
Build{" "}
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className=" h-5 stroke-build-trigger stroke-2"
|
||||
/>{" "}
|
||||
flow to validate status.
|
||||
</span>
|
||||
<span className="flex">Build to validate status.</span>
|
||||
) : (
|
||||
<div className="max-h-96 overflow-auto">
|
||||
{typeof validationStatus.params === "string"
|
||||
? `${durationString}\n${validationStatus.params}`
|
||||
? (`${durationString}\n${validationStatus.params}`
|
||||
.split("\n")
|
||||
.map((line, index) => (
|
||||
<div key={index}>{line}</div>
|
||||
))
|
||||
)))
|
||||
: durationString}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
side="bottom"
|
||||
>
|
||||
<div className="generic-node-status-position flex items-center justify-center">
|
||||
{renderIconPlayOrPauseComponents(
|
||||
data?.build_status,
|
||||
validationStatus
|
||||
)}
|
||||
{renderIconStatus(buildStatus, validationStatus)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
49
src/frontend/src/alerts_constants.tsx
Normal file
49
src/frontend/src/alerts_constants.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// ERROR
|
||||
export const MISSED_ERROR_ALERT = "Oops! Looks like you missed something";
|
||||
export const INVALID_FILE_ALERT = "Please select a valid file. Only these file types are allowed:";
|
||||
export const CONSOLE_ERROR_MSG = "Error occurred while uploading file";
|
||||
export const CONSOLE_SUCCESS_MSG = "File uploaded successfully";
|
||||
export const INFO_MISSING_ALERT = "Oops! Looks like you missed some required information:";
|
||||
export const FUNC_ERROR_ALERT = "There is an error in your function"
|
||||
export const IMPORT_ERROR_ALERT = "There is an error in your imports"
|
||||
export const BUG_ALERT = "Something went wrong, please try again"
|
||||
export const CODE_ERROR_ALERT = "There is something wrong with this code, please review it"
|
||||
export const CHAT_ERROR_ALERT = "Please build the flow again before using the chat."
|
||||
export const MSG_ERROR_ALERT = "There was an error sending the message"
|
||||
export const PROMPT_ERROR_ALERT = "There is something wrong with this prompt, please review it"
|
||||
export const API_ERROR_ALERT = "There was an error saving the API Key, please try again."
|
||||
export const USER_DEL_ERROR_ALERT = "Error on delete user"
|
||||
export const USER_EDIT_ERROR_ALERT = "Error on edit user"
|
||||
export const USER_ADD_ERROR_ALERT = "Error when adding new user"
|
||||
export const SIGNIN_ERROR_ALERT = "Error signing in"
|
||||
export const DEL_KEY_ERROR_ALERT = "Error on delete key"
|
||||
export const UPLOAD_ERROR_ALERT = "Error uploading file"
|
||||
export const WRONG_FILE_ERROR_ALERT = "Invalid file type"
|
||||
export const UPLOAD_ALERT_LIST = "Please upload a JSON file"
|
||||
export const INVALID_SELECTION_ERROR_ALERT = "Invalid selection"
|
||||
export const EDIT_PASSWORD_ERROR_ALERT = "Error changing password"
|
||||
export const EDIT_PASSWORD_ALERT_LIST = "Passwords do not match"
|
||||
export const SAVE_ERROR_ALERT = "Error saving changes"
|
||||
export const SIGNUP_ERROR_ALERT = "Error signing up"
|
||||
export const APIKEY_ERROR_ALERT = "API Key Error"
|
||||
export const NOAPI_ERROR_ALERT = "You don't have an API Key. Please add one to use the Langflow Store."
|
||||
export const INVALID_API_ERROR_ALERT = "Your API Key is not valid. Please add a valid API Key to use the Langflow Store."
|
||||
export const COMPONENTS_ERROR_ALERT = "Error getting components."
|
||||
|
||||
// NOTICE
|
||||
export const NOCHATOUTPUT_NOTICE_ALERT = "There is no ChatOutput node in the flow."
|
||||
export const API_WARNING_NOTICE_ALERT = "Warning: Critical data, JSON file may include API keys."
|
||||
export const COPIED_NOTICE_ALERT = "API Key copied!"
|
||||
export const TEMP_NOTICE_ALERT = "Your template does not have any variables."
|
||||
|
||||
|
||||
// SUCCESS
|
||||
export const CODE_SUCCESS_ALERT = "Code is ready to run"
|
||||
export const PROMPT_SUCCESS_ALERT = "Prompt is ready"
|
||||
export const API_SUCCESS_ALERT = "Success! Your API Key has been saved."
|
||||
export const USER_DEL_SUCCESS_ALERT = "Success! User deleted!"
|
||||
export const USER_EDIT_SUCCESS_ALERT = "Success! User edited!"
|
||||
export const USER_ADD_SUCCESS_ALERT = "Success! New user added!"
|
||||
export const DEL_KEY_SUCCESS_ALERT = "Success! Key deleted!"
|
||||
export const FLOW_BUILD_SUCCESS_ALERT = `Flow built successfully`
|
||||
export const SAVE_SUCCESS_ALERT = "Changes saved successfully!"
|
||||
|
|
@ -49,7 +49,7 @@ export default function AccordionComponent({
|
|||
>
|
||||
{trigger}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="AccordionContent">
|
||||
<AccordionContent className="AccordionContent flex flex-col">
|
||||
{children}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ export default function IOInputField({
|
|||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const node = nodes.find((node) => node.id === inputId);
|
||||
function handleInputType() {
|
||||
if (!node) return "no node found";
|
||||
if (!node) return <>"No node found!"</>;
|
||||
switch (inputType) {
|
||||
case "TextInput":
|
||||
return (
|
||||
<Textarea
|
||||
className="h-full w-full custom-scroll"
|
||||
className="w-full"
|
||||
placeholder={"Enter text..."}
|
||||
value={node.data.node!.template["value"].value}
|
||||
onChange={(e) => {
|
||||
|
|
@ -47,7 +47,7 @@ export default function IOInputField({
|
|||
default:
|
||||
return (
|
||||
<Textarea
|
||||
className="h-full w-full custom-scroll"
|
||||
className="w-full custom-scroll"
|
||||
placeholder={"Enter text..."}
|
||||
value={node.data.node!.template["value"]}
|
||||
onChange={(e) => {
|
||||
|
|
@ -62,10 +62,5 @@ export default function IOInputField({
|
|||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="font-xl flex h-full w-full flex-col gap-4 p-4 font-semibold">
|
||||
{inputType}
|
||||
{handleInputType()}
|
||||
</div>
|
||||
);
|
||||
return handleInputType();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,15 +12,15 @@ export default function IOOutputView({
|
|||
const flowPool = useFlowStore((state) => state.flowPool);
|
||||
const node = nodes.find((node) => node.id === outputId);
|
||||
function handleOutputType() {
|
||||
if (!node) return "no node found";
|
||||
if (!node) return <>"No node found!"</>;
|
||||
switch (outputType) {
|
||||
case "TextOutput":
|
||||
return (
|
||||
<Textarea
|
||||
className="h-full w-full custom-scroll"
|
||||
placeholder={"Enter text..."}
|
||||
className="w-full custom-scroll"
|
||||
placeholder={"Empty"}
|
||||
// update to real value on flowPool
|
||||
value={flowPool[node.id][flowPool[node.id].length - 1].data.results}
|
||||
value={((flowPool[node.id] ?? [])[(flowPool[node.id]?.length ?? 1) - 1])?.params ?? ""}
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
|
|
@ -28,7 +28,7 @@ export default function IOOutputView({
|
|||
default:
|
||||
return (
|
||||
<Textarea
|
||||
className="h-full w-full custom-scroll"
|
||||
className="w-full custom-scroll"
|
||||
placeholder={"Enter text..."}
|
||||
value={node.data.node!.template["value"]}
|
||||
onChange={(e) => {
|
||||
|
|
@ -43,10 +43,5 @@ export default function IOOutputView({
|
|||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="font-xl flex h-full w-full flex-col gap-4 p-4 font-semibold">
|
||||
{outputType}
|
||||
{handleOutputType()}
|
||||
</div>
|
||||
);
|
||||
return handleOutputType();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { ReactNode, useState } from "react";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import BaseModal from "../../modals/baseModal";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { NodeType } from "../../types/flow";
|
||||
import { isInputType, isOutputType } from "../../utils/reactflowUtils";
|
||||
import { cn } from "../../utils/utils";
|
||||
import AccordionComponent from "../AccordionComponent";
|
||||
import IOInputField from "../IOInputField";
|
||||
|
|
@ -12,62 +12,70 @@ import IconComponent from "../genericIconComponent";
|
|||
import NewChatView from "../newChatView";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Button } from "../ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
|
||||
export default function IOView({ children, open, setOpen }): JSX.Element {
|
||||
const inputs = useFlowStore((state) => state.inputs);
|
||||
const outputs = useFlowStore((state) => state.outputs);
|
||||
const inputIds = inputs.map((obj) => obj.id);
|
||||
const outputIds = outputs.map((obj) => obj.id);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const categories = getCategories();
|
||||
const [selectedCategory, setSelectedCategory] = useState<number>(0);
|
||||
const [showChat, setShowChat] = useState<boolean>(false);
|
||||
const [selectedView, setSelectedView] = useState<{
|
||||
type: string;
|
||||
id?: string;
|
||||
}>(handleInitialView());
|
||||
const inputs = useFlowStore((state) => state.inputs).filter(
|
||||
(input) => input.type !== "ChatInput"
|
||||
);
|
||||
const chatInput = useFlowStore((state) => state.inputs).find(
|
||||
(input) => input.type === "ChatInput"
|
||||
);
|
||||
const outputs = useFlowStore((state) => state.outputs).filter(
|
||||
(output) => output.type !== "ChatOutput"
|
||||
);
|
||||
const chatOutput = useFlowStore((state) => state.outputs).find(
|
||||
(output) => output.type === "ChatOutput"
|
||||
);
|
||||
const nodes = useFlowStore((state) => state.nodes).filter(
|
||||
(node) =>
|
||||
inputs.some((input) => input.id === node.id) ||
|
||||
outputs.some((output) => output.id === node.id)
|
||||
);
|
||||
const haveChat = chatInput || chatOutput;
|
||||
const [selectedTab, setSelectedTab] = useState(
|
||||
inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0
|
||||
);
|
||||
const [selectedViewField, setSelectedViewField] = useState<
|
||||
{ type: string; id: string } | undefined
|
||||
>(undefined);
|
||||
|
||||
type CategoriesType = { name: string; icon: string };
|
||||
const { getNode, setNode, buildFlow, getFlow } = useFlowStore();
|
||||
const { setErrorData } = useAlertStore();
|
||||
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
|
||||
const [lockChat, setLockChat] = useState(false);
|
||||
const [chatValue, setChatValue] = useState("");
|
||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
|
||||
function handleInitialView() {
|
||||
if (
|
||||
outputs.map((output) => output.type).includes("ChatOutput") ||
|
||||
inputs.map((input) => input.type).includes("ChatInput")
|
||||
) {
|
||||
return { type: "ChatOutput" };
|
||||
async function sendMessage(count = 1): Promise<void> {
|
||||
if (isBuilding) return;
|
||||
const { nodes, edges } = getFlow();
|
||||
setIsBuilding(true);
|
||||
setLockChat(true);
|
||||
setChatValue("");
|
||||
const chatInputNode = nodes.find((node) => node.id === chatInput?.id);
|
||||
if (chatInputNode) {
|
||||
let newNode = cloneDeep(chatInputNode);
|
||||
newNode.data.node!.template["message"].value = chatValue;
|
||||
setNode(chatInput!.id, newNode);
|
||||
}
|
||||
return { type: "" };
|
||||
for (let i = 0; i < count; i++) {
|
||||
await buildFlow().catch((err) => {
|
||||
console.error(err);
|
||||
setLockChat(false);
|
||||
});
|
||||
}
|
||||
setLockChat(false);
|
||||
}
|
||||
|
||||
function getCategories() {
|
||||
const categories: CategoriesType[] = [];
|
||||
if (inputs.filter((input) => input.type !== "ChatInput").length > 0)
|
||||
categories.push({ name: "Inputs", icon: "TextCursorInput" });
|
||||
if (outputs.filter((output) => output.type !== "ChatOutput").length > 0)
|
||||
categories.push({ name: "Outputs", icon: "TerminalSquare" });
|
||||
return categories;
|
||||
}
|
||||
|
||||
function handleSelectChange(): ReactNode {
|
||||
const { type, id } = selectedView;
|
||||
if (type === "ChatOutput") return <NewChatView />;
|
||||
if (isInputType(type))
|
||||
return <IOInputField inputId={id!} inputType={type} />;
|
||||
if (isOutputType(type))
|
||||
return <IOOutputView outputId={id!} outputType={type} />;
|
||||
else return undefined;
|
||||
}
|
||||
|
||||
function UpdateAccordion() {
|
||||
return (categories[selectedCategory]?.name ?? "Inputs") === "Inputs"
|
||||
? inputs
|
||||
: outputs;
|
||||
}
|
||||
useEffect(() => {
|
||||
setSelectedViewField(undefined);
|
||||
setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0);
|
||||
}, [inputs.length, outputs.length]);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
size={handleSelectChange() ? "large" : "small"}
|
||||
size={haveChat ? (selectedTab === 0 ? "large-thin" : "large") : "small"}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
>
|
||||
|
|
@ -84,130 +92,229 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
|
|||
</div>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<div className="flex-max-width mt-2 h-[80vh]">
|
||||
<div
|
||||
className={cn(
|
||||
"mr-6 flex h-full w-2/6 flex-col justify-start overflow-auto scrollbar-hide",
|
||||
handleSelectChange() ? "w-2/6" : "w-full"
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between py-2">
|
||||
<div className="flex items-start gap-4">
|
||||
{categories.map((category, index) => {
|
||||
return (
|
||||
//hide chat button if chat is alredy on the view
|
||||
<Button
|
||||
onClick={() => setSelectedCategory(index)}
|
||||
variant={
|
||||
index === selectedCategory ? "primary" : "secondary"
|
||||
}
|
||||
key={index}
|
||||
>
|
||||
<IconComponent
|
||||
name={category.icon}
|
||||
className=" file-component-variable"
|
||||
/>
|
||||
<span className="file-component-variables-span text-md">
|
||||
{category.name}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{(outputs.map((output) => output.type).includes("ChatOutput") ||
|
||||
inputs.map((output) => output.type).includes("chatInput")) &&
|
||||
selectedView.type !== "ChatOutput" && (
|
||||
<Button
|
||||
onClick={() => setSelectedView({ type: "ChatOutput" })}
|
||||
variant="outline"
|
||||
key={"chat"}
|
||||
className="self-end px-2.5"
|
||||
>
|
||||
<IconComponent
|
||||
name="MessageSquareMore"
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
</Button>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex-max-width mt-2 h-full">
|
||||
{selectedTab !== 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start overflow-auto scrollbar-hide",
|
||||
haveChat ? "w-2/6" : "w-full"
|
||||
)}
|
||||
</div>
|
||||
<div className="mx-2 mb-2 mt-4 flex items-center gap-2 font-semibold">
|
||||
{categories[selectedCategory]?.name === "Inputs" && (
|
||||
<>
|
||||
<IconComponent name={"FormInput"} />
|
||||
Text Inputs
|
||||
</>
|
||||
)}
|
||||
{categories[selectedCategory]?.name === "Outputs" && (
|
||||
<>
|
||||
<IconComponent name={"ChevronRightSquare"} />
|
||||
Prompt Outputs
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{UpdateAccordion()
|
||||
.filter(
|
||||
(input) =>
|
||||
input.type !== "ChatInput" && input.type !== "ChatOutput"
|
||||
)
|
||||
.map((input, index) => {
|
||||
const node: NodeType = nodes.find(
|
||||
(node) => node.id === input.id
|
||||
)!;
|
||||
return (
|
||||
<div className="file-component-accordion-div" key={index}>
|
||||
<AccordionComponent
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<Badge variant="gray" size="md">
|
||||
{input.id}
|
||||
</Badge>
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedView({
|
||||
type: input.type,
|
||||
id: input.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={input.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{node &&
|
||||
(categories[selectedCategory]?.name === "Inputs" ? (
|
||||
<IOInputField
|
||||
inputType={input.type}
|
||||
inputId={input.id}
|
||||
/>
|
||||
) : (
|
||||
<IOOutputView
|
||||
outputType={input.type}
|
||||
outputId={input.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
>
|
||||
<Tabs
|
||||
value={selectedTab.toString()}
|
||||
className={
|
||||
"flex h-full flex-col overflow-hidden rounded-md border bg-muted text-center"
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
setSelectedTab(Number(value));
|
||||
}}
|
||||
>
|
||||
<div className="api-modal-tablist-div">
|
||||
<TabsList>
|
||||
{inputs.length > 0 && (
|
||||
<TabsTrigger value={"1"}>Inputs</TabsTrigger>
|
||||
)}
|
||||
{outputs.length > 0 && (
|
||||
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<TabsContent
|
||||
value={"1"}
|
||||
className="api-modal-tabs-content mt-4"
|
||||
>
|
||||
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
|
||||
<IconComponent className="h-4 w-4" name={"Type"} />
|
||||
Text Inputs
|
||||
</div>
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
inputs.some((input) => input.id === node.id)
|
||||
)
|
||||
.map((node, index) => {
|
||||
const input = inputs.find(
|
||||
(input) => input.id === node.id
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div"
|
||||
key={index}
|
||||
>
|
||||
<AccordionComponent
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<Badge variant="gray" size="md">
|
||||
{input.id}
|
||||
</Badge>
|
||||
{haveChat && (
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(input);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={input.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{input && (
|
||||
<IOInputField
|
||||
inputType={input.type}
|
||||
inputId={input.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value={"2"}
|
||||
className="api-modal-tabs-content mt-4"
|
||||
>
|
||||
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
|
||||
<IconComponent className="h-4 w-4" name={"Braces"} />
|
||||
Prompt Outputs
|
||||
</div>
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
outputs.some((output) => output.id === node.id)
|
||||
)
|
||||
.map((node, index) => {
|
||||
const output = outputs.find(
|
||||
(output) => output.id === node.id
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div"
|
||||
key={index}
|
||||
>
|
||||
<AccordionComponent
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<Badge variant="gray" size="md">
|
||||
{output.id}
|
||||
</Badge>
|
||||
{haveChat && (
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(output);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={output.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{output && (
|
||||
<IOOutputView
|
||||
outputType={output.type}
|
||||
outputId={output.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{haveChat ? (
|
||||
<div className="flex h-full w-full">
|
||||
{selectedViewField && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col items-start gap-4 p-4",
|
||||
!selectedViewField ? "hidden" : ""
|
||||
)}
|
||||
>
|
||||
<div className="font-xl flex items-center justify-center gap-3 font-semibold">
|
||||
<button onClick={() => setSelectedViewField(undefined)}>
|
||||
<IconComponent
|
||||
name={"ArrowLeft"}
|
||||
className="h-6 w-6"
|
||||
></IconComponent>
|
||||
</button>
|
||||
{selectedViewField.type}
|
||||
</div>
|
||||
<div className="h-full">
|
||||
{inputs.some(
|
||||
(input) => input.id === selectedViewField.id
|
||||
) ? (
|
||||
<IOInputField
|
||||
inputType={selectedViewField.type!}
|
||||
inputId={selectedViewField.id!}
|
||||
/>
|
||||
) : (
|
||||
<IOOutputView
|
||||
outputType={selectedViewField.type!}
|
||||
outputId={selectedViewField.id!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn("flex w-full h-full",selectedViewField ? "hidden" : "")}
|
||||
>
|
||||
<NewChatView
|
||||
sendMessage={sendMessage}
|
||||
chatValue={chatValue}
|
||||
setChatValue={setChatValue}
|
||||
lockChat={lockChat}
|
||||
setLockChat={setLockChat}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute bottom-8 right-8"></div>
|
||||
)}
|
||||
</div>
|
||||
{handleSelectChange() ? (
|
||||
handleSelectChange()
|
||||
) : (
|
||||
<div className="absolute bottom-8 right-8">
|
||||
<Button className="px-3">
|
||||
<IconComponent name="Play" className="h-6 w-6" />
|
||||
{!haveChat && (
|
||||
<div className="flex w-full justify-end pt-6">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="flex gap-2 px-3"
|
||||
onClick={() => sendMessage(1)}
|
||||
>
|
||||
<IconComponent
|
||||
name={isBuilding ? "Loader2" : "Play"}
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
isBuilding
|
||||
? "animate-spin"
|
||||
: "fill-current text-medium-indigo"
|
||||
)}
|
||||
/>
|
||||
Run Flow
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ShadToolTipType } from "../../types/components";
|
||||
import { cn } from "../../utils/utils";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
|
||||
export default function ShadTooltip({
|
||||
|
|
@ -14,7 +15,7 @@ export default function ShadTooltip({
|
|||
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
|
||||
|
||||
<TooltipContent
|
||||
className={styleClasses}
|
||||
className={cn(styleClasses, "max-w-96") }
|
||||
side={side}
|
||||
avoidCollisions={false}
|
||||
sticky="always"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default function ChatTrigger({}): JSX.Element {
|
|||
>
|
||||
<div className="flex gap-3">
|
||||
<IconComponent
|
||||
name="Sliders"
|
||||
name="Zap"
|
||||
className={"message-button-icon h-6 w-6 transition-all"}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import useFlowStore from "../../../stores/flowStore";
|
|||
import { validateNodes } from "../../../utils/reactflowUtils";
|
||||
import RadialProgressComponent from "../../RadialProgress";
|
||||
import IconComponent from "../../genericIconComponent";
|
||||
import { MISSED_ERROR_ALERT } from "../../../alerts_constants";
|
||||
|
||||
export default function BuildTrigger({
|
||||
open,
|
||||
|
|
@ -36,7 +37,7 @@ export default function BuildTrigger({
|
|||
const errors = validateNodes(nodes, edges);
|
||||
if (errors.length > 0) {
|
||||
setErrorData({
|
||||
title: "Oops! Looks like you missed something",
|
||||
title: MISSED_ERROR_ALERT,
|
||||
list: errors,
|
||||
});
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import useAlertStore from "../../../../stores/alertStore";
|
|||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
import Tooltip from "../../../TooltipComponent";
|
||||
import ShadTooltip from "../../../ShadTooltipComponent";
|
||||
import IconComponent from "../../../genericIconComponent";
|
||||
import { Button } from "../../../ui/button";
|
||||
|
||||
|
|
@ -125,8 +125,8 @@ export const MenuBar = ({
|
|||
setOpen={setOpenSettings}
|
||||
></FlowSettingsModal>
|
||||
</div>
|
||||
<Tooltip
|
||||
title={
|
||||
<ShadTooltip
|
||||
content={
|
||||
"Last saved at " +
|
||||
new Date(currentFlow.updated_at ?? "").toLocaleString("en-US", {
|
||||
hour: "numeric",
|
||||
|
|
@ -134,8 +134,10 @@ export const MenuBar = ({
|
|||
second: "numeric",
|
||||
})
|
||||
}
|
||||
side="bottom"
|
||||
styleClasses="cursor-default"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<div className="flex cursor-default items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<IconComponent
|
||||
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
|
||||
className={cn(
|
||||
|
|
@ -145,7 +147,7 @@ export const MenuBar = ({
|
|||
/>
|
||||
{printByBuildStatus()}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import useAlertStore from "../../stores/alertStore";
|
|||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { FileComponentType } from "../../types/components";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import { CONSOLE_ERROR_MSG, CONSOLE_SUCCESS_MSG, INVALID_FILE_ALERT } from "../../alerts_constants";
|
||||
|
||||
export default function InputFileComponent({
|
||||
value,
|
||||
|
|
@ -61,7 +62,7 @@ export default function InputFileComponent({
|
|||
uploadFile(file, currentFlowId)
|
||||
.then((res) => res.data)
|
||||
.then((data) => {
|
||||
console.log("File uploaded successfully");
|
||||
console.log(CONSOLE_SUCCESS_MSG);
|
||||
// Get the file name from the response
|
||||
const { file_path } = data;
|
||||
console.log("File name:", file_path);
|
||||
|
|
@ -75,14 +76,13 @@ export default function InputFileComponent({
|
|||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
console.error("Error occurred while uploading file");
|
||||
console.error(CONSOLE_ERROR_MSG);
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
// Show an error if the file type is not allowed
|
||||
setErrorData({
|
||||
title:
|
||||
"Please select a valid file. Only these file types are allowed:",
|
||||
title: INVALID_FILE_ALERT,
|
||||
list: fileTypes,
|
||||
});
|
||||
setLoading(false);
|
||||
|
|
|
|||
|
|
@ -17,12 +17,65 @@ export default function ChatMessage({
|
|||
chat,
|
||||
lockChat,
|
||||
lastMessage,
|
||||
updateChat,
|
||||
}: chatMessagePropsType): JSX.Element {
|
||||
const convert = new Convert({ newline: true });
|
||||
const [hidden, setHidden] = useState(true);
|
||||
const template = chat.template;
|
||||
const [promptOpen, setPromptOpen] = useState(false);
|
||||
const chat_message = chat.message.toString();
|
||||
const [streamUrl, setStreamUrl] = useState(chat.stream_url);
|
||||
const [chatMessage, setChatMessage] = useState(chat.message.toString());
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
|
||||
// The idea now is that chat.stream_url MAY be a URL if we should stream the output of the chat
|
||||
// probably the message is empty when we have a stream_url
|
||||
// what we need is to update the chat_message with the SSE data
|
||||
const streamChunks = (url: string) => {
|
||||
setIsStreaming(true); // Streaming starts
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const eventSource = new EventSource(url);
|
||||
eventSource.onmessage = (event) => {
|
||||
let parsedData = JSON.parse(event.data);
|
||||
if (parsedData.chunk) {
|
||||
setChatMessage((prev) => prev + parsedData.chunk);
|
||||
}
|
||||
};
|
||||
eventSource.onerror = (event) => {
|
||||
reject(new Error("Streaming failed"));
|
||||
setIsStreaming(false);
|
||||
eventSource.close();
|
||||
};
|
||||
eventSource.addEventListener("close", (event) => {
|
||||
setStreamUrl(null); // Update state to reflect the stream is closed
|
||||
resolve(true);
|
||||
setIsStreaming(false);
|
||||
eventSource.close();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (streamUrl && chat.message === "") {
|
||||
streamChunks(streamUrl)
|
||||
.then(() => {
|
||||
if (updateChat) {
|
||||
updateChat(chat, chatMessage, streamUrl);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
}, [streamUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
// This effect is specifically for calling updateChat after streaming ends
|
||||
if (!isStreaming && streamUrl) {
|
||||
if (updateChat) {
|
||||
updateChat(chat, chatMessage, streamUrl);
|
||||
}
|
||||
}
|
||||
}, [isStreaming]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.getElementById("last-chat-message");
|
||||
|
|
@ -103,7 +156,7 @@ export default function ChatMessage({
|
|||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeMathjax]}
|
||||
className="markdown prose min-w-full text-primary word-break-break-word
|
||||
dark:prose-invert"
|
||||
dark:prose-invert"
|
||||
components={{
|
||||
pre({ node, ...props }) {
|
||||
return <>{props.children}</>;
|
||||
|
|
@ -161,10 +214,10 @@ export default function ChatMessage({
|
|||
},
|
||||
}}
|
||||
>
|
||||
{chat_message}
|
||||
{chatMessage}
|
||||
</Markdown>
|
||||
),
|
||||
[chat.message, chat_message]
|
||||
[chat.message, chatMessage]
|
||||
)}
|
||||
</div>
|
||||
{chat.files && (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { deleteFlowPool } from "../../controllers/API";
|
||||
|
|
@ -11,31 +11,23 @@ import {
|
|||
ChatOutputType,
|
||||
FlowPoolObjectType,
|
||||
} from "../../types/chat";
|
||||
import { NodeType } from "../../types/flow";
|
||||
import { validateNodes } from "../../utils/reactflowUtils";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import ChatInput from "./chatInput";
|
||||
import ChatMessage from "./chatMessage";
|
||||
import { INFO_MISSING_ALERT, NOCHATOUTPUT_NOTICE_ALERT } from "../../alerts_constants";
|
||||
|
||||
export default function newChatView(): JSX.Element {
|
||||
const [chatValue, setChatValue] = useState("");
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
|
||||
const {
|
||||
flowPool,
|
||||
outputs,
|
||||
inputs,
|
||||
getNode,
|
||||
setNode,
|
||||
buildFlow,
|
||||
getFlow,
|
||||
CleanFlowPool,
|
||||
} = useFlowStore();
|
||||
const { setErrorData, setNoticeData } = useAlertStore();
|
||||
export default function NewChatView({
|
||||
sendMessage,
|
||||
chatValue,
|
||||
setChatValue,
|
||||
lockChat,
|
||||
setLockChat,
|
||||
}): JSX.Element {
|
||||
const { flowPool, outputs, inputs, CleanFlowPool } = useFlowStore();
|
||||
const { setNoticeData } = useAlertStore();
|
||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
|
||||
const [lockChat, setLockChat] = useState(false);
|
||||
const messagesRef = useRef<HTMLDivElement | null>(null);
|
||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
|
||||
|
||||
const inputTypes = inputs.map((obj) => obj.type);
|
||||
const inputIds = inputs.map((obj) => obj.id);
|
||||
|
|
@ -44,7 +36,7 @@ export default function newChatView(): JSX.Element {
|
|||
|
||||
useEffect(() => {
|
||||
if (!outputTypes.includes("ChatOutput")) {
|
||||
setNoticeData({ title: "There is no ChatOutput node in the flow." });
|
||||
setNoticeData({ title: NOCHATOUTPUT_NOTICE_ALERT });
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
@ -67,20 +59,30 @@ export default function newChatView(): JSX.Element {
|
|||
});
|
||||
const chatMessages: ChatMessageType[] = chatOutputResponses
|
||||
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
|
||||
.filter((output) => !!output.data.artifacts?.message)
|
||||
.map((output) => {
|
||||
//
|
||||
.filter((output) => output.data.artifacts?.message !== null)
|
||||
.map((output, index) => {
|
||||
try {
|
||||
const { sender, message, sender_name } = output.data
|
||||
const { sender, message, sender_name, stream_url } = output.data
|
||||
.artifacts as ChatOutputType;
|
||||
|
||||
const componentId = output.id + index;
|
||||
|
||||
const is_ai = sender === "Machine" || sender === null;
|
||||
return { isSend: !is_ai, message: message, sender_name };
|
||||
return {
|
||||
isSend: !is_ai,
|
||||
message: message,
|
||||
sender_name,
|
||||
id: componentId,
|
||||
stream_url: stream_url,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
isSend: false,
|
||||
message: "Error parsing message",
|
||||
sender_name: "Error",
|
||||
id: output.id + index,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
@ -105,40 +107,6 @@ export default function newChatView(): JSX.Element {
|
|||
}
|
||||
}, []);
|
||||
|
||||
async function sendMessage(count = 1): Promise<void> {
|
||||
if (isBuilding) return;
|
||||
const { nodes, edges } = getFlow();
|
||||
let nodeValidationErrors = validateNodes(nodes, edges);
|
||||
if (nodeValidationErrors.length === 0) {
|
||||
setIsBuilding(true);
|
||||
setLockChat(true);
|
||||
setChatValue("");
|
||||
const chatInputId = inputIds.find((inputId) =>
|
||||
inputId.includes("ChatInput")
|
||||
);
|
||||
const chatInput: NodeType = getNode(chatInputId!) as NodeType;
|
||||
if (chatInput) {
|
||||
let newNode = cloneDeep(chatInput);
|
||||
newNode.data.node!.template["message"].value = chatValue;
|
||||
setNode(chatInputId!, newNode);
|
||||
}
|
||||
for (let i = 0; i < count; i++) {
|
||||
await buildFlow().catch((err) => {
|
||||
console.error(err);
|
||||
setLockChat(false);
|
||||
});
|
||||
}
|
||||
setLockChat(false);
|
||||
|
||||
//set chat message in the flow and run build
|
||||
//@ts-ignore
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Oops! Looks like you missed some required information:",
|
||||
list: nodeValidationErrors,
|
||||
});
|
||||
}
|
||||
}
|
||||
function clearChat(): void {
|
||||
setChatHistory([]);
|
||||
deleteFlowPool(currentFlowId).then((_) => {
|
||||
|
|
@ -148,6 +116,32 @@ export default function newChatView(): JSX.Element {
|
|||
if (lockChat) setLockChat(false);
|
||||
}
|
||||
|
||||
function updateChat(
|
||||
chat: ChatMessageType,
|
||||
message: string,
|
||||
stream_url: string | null
|
||||
) {
|
||||
if (message === "") return;
|
||||
console.log(`updateChat: ${message}`);
|
||||
console.log("chatHistory:", chatHistory);
|
||||
chat.message = message;
|
||||
chat.stream_url = stream_url;
|
||||
// chat is one of the chatHistory
|
||||
setChatHistory((oldChatHistory) => {
|
||||
const index = oldChatHistory.findIndex((ch) => ch.id === chat.id);
|
||||
|
||||
if (index === -1) return oldChatHistory;
|
||||
let newChatHistory = _.cloneDeep(oldChatHistory);
|
||||
newChatHistory = [
|
||||
...newChatHistory.slice(0, index),
|
||||
chat,
|
||||
...newChatHistory.slice(index + 1),
|
||||
];
|
||||
console.log("newChatHistory:", newChatHistory);
|
||||
return newChatHistory;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="eraser-column-arrangement">
|
||||
<div className="eraser-size">
|
||||
|
|
@ -172,7 +166,8 @@ export default function newChatView(): JSX.Element {
|
|||
lockChat={lockChat}
|
||||
chat={chat}
|
||||
lastMessage={chatHistory.length - 1 === index ? true : false}
|
||||
key={index}
|
||||
key={`${chat.id}-${index}`}
|
||||
updateChat={updateChat}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -20,48 +20,42 @@ export default function TextAreaComponent({
|
|||
}, [disabled]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"flex w-full items-center " + (disabled ? "pointer-events-none" : "")
|
||||
}
|
||||
>
|
||||
<GenericModal
|
||||
type={TypeModal.TEXT}
|
||||
buttonText="Finishing Editing"
|
||||
modalTitle="Edit Text"
|
||||
value={value}
|
||||
setValue={(value: string) => {
|
||||
onChange(value);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center" data-testid={"div-" + id}>
|
||||
<Input
|
||||
id={id}
|
||||
data-testid={id}
|
||||
<div className={"flex w-full items-center " + (disabled ? "" : "")}>
|
||||
<div className="flex w-full items-center" data-testid={"div-" + id}>
|
||||
<Input
|
||||
id={id}
|
||||
data-testid={id}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
className={editNode ? "input-edit-node w-full" : " w-full"}
|
||||
placeholder={"Type something..."}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<GenericModal
|
||||
type={TypeModal.TEXT}
|
||||
buttonText="Finish Editing"
|
||||
modalTitle="Edit Text"
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
className={
|
||||
editNode
|
||||
? "input-edit-node pointer-events-none "
|
||||
: " pointer-events-none"
|
||||
}
|
||||
placeholder={"Type something..."}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
setValue={(value: string) => {
|
||||
onChange(value);
|
||||
}}
|
||||
/>
|
||||
{!editNode && (
|
||||
<IconComponent
|
||||
id={id}
|
||||
name="ExternalLink"
|
||||
className={
|
||||
"icons-parameters-comp" +
|
||||
(disabled ? " text-ring" : " hover:text-accent-foreground")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{!editNode && (
|
||||
<IconComponent
|
||||
id={id}
|
||||
name="ExternalLink"
|
||||
className={
|
||||
"icons-parameters-comp" +
|
||||
(disabled ? " text-ring" : " hover:text-accent-foreground")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</GenericModal>
|
||||
</div>
|
||||
</GenericModal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
31
src/frontend/src/components/ui/checkmark.tsx
Normal file
31
src/frontend/src/components/ui/checkmark.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { AnimatePresence, motion } from "framer-motion";
|
||||
export default function Checkmark({ initial = true, isVisible, className }) {
|
||||
return (
|
||||
<AnimatePresence initial={initial}>
|
||||
{isVisible && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={"CheckIcon " + className}
|
||||
>
|
||||
<motion.path
|
||||
initial={{ pathLength: 0, pathOffset: 1 }}
|
||||
animate={{ pathLength: 1, pathOffset: 0 }}
|
||||
exit={{ pathLength: 0, pathOffset: 1 }}
|
||||
transition={{
|
||||
type: "tween",
|
||||
duration: 0.3,
|
||||
ease: isVisible ? "easeOut" : "easeIn",
|
||||
}}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M20 6 9 17l-5-5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
46
src/frontend/src/components/ui/xmark.tsx
Normal file
46
src/frontend/src/components/ui/xmark.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { AnimatePresence, motion } from "framer-motion";
|
||||
export default function Xmark({ initial = true, isVisible, className }) {
|
||||
return (
|
||||
<AnimatePresence initial={initial}>
|
||||
{isVisible && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={"CheckIcon " + className}
|
||||
>
|
||||
<motion.path
|
||||
|
||||
initial={{ pathLength: 0, pathOffset: 1, strokeLinecap: "butt" }}
|
||||
animate={{ pathLength: 1, pathOffset: 0, strokeLinecap: "round" }}
|
||||
exit={{ pathLength: 0, pathOffset: 1, strokeLinecap: "butt" }}
|
||||
transition={{
|
||||
type: "tween",
|
||||
duration: 0.3,
|
||||
ease: isVisible ? "easeOut" : "easeIn",
|
||||
delay: 0.2,
|
||||
}}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M18 6 6 18"
|
||||
/>
|
||||
<motion.path
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
exit={{ pathLength: 0 }}
|
||||
transition={{
|
||||
type: "tween",
|
||||
duration: 0.3,
|
||||
ease: isVisible ? "easeOut" : "easeIn",
|
||||
}}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m6 6 12 12"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
|
@ -681,4 +681,4 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
|
|||
export const priorityFields = new Set(["code", "template"]);
|
||||
|
||||
export const INPUT_TYPES = new Set(["ChatInput", "TextInput"]);
|
||||
export const OUTPUT_TYPES = new Set(["ChatOutput"]);
|
||||
export const OUTPUT_TYPES = new Set(["ChatOutput", "TextOutput"]);
|
||||
|
|
|
|||
|
|
@ -11,4 +11,5 @@ export enum BuildStatus {
|
|||
BUILDING = "BUILDING",
|
||||
TO_BUILD = "TO_BUILD",
|
||||
BUILT = "BUILT",
|
||||
INACTIVE = "INACTIVE",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import useAlertStore from "../../stores/alertStore";
|
|||
import { ApiKeyType } from "../../types/components";
|
||||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import BaseModal from "../baseModal";
|
||||
import { COPIED_NOTICE_ALERT } from "../../alerts_constants";
|
||||
|
||||
export default function SecretKeyModal({
|
||||
title,
|
||||
|
|
@ -47,7 +48,7 @@ export default function SecretKeyModal({
|
|||
inputRef?.current?.focus();
|
||||
inputRef?.current?.select();
|
||||
setSuccessData({
|
||||
title: "API Key copied!",
|
||||
title: COPIED_NOTICE_ALERT,
|
||||
});
|
||||
setTextCopied(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import useAlertStore from "../../stores/alertStore";
|
|||
import { useStoreStore } from "../../stores/storeStore";
|
||||
import { StoreApiKeyType } from "../../types/components";
|
||||
import BaseModal from "../baseModal";
|
||||
import { API_ERROR_ALERT, API_SUCCESS_ALERT } from "../../alerts_constants";
|
||||
|
||||
export default function StoreApiKeyModal({
|
||||
children,
|
||||
|
|
@ -32,7 +33,7 @@ export default function StoreApiKeyModal({
|
|||
addApiKeyStore(apiKeyValue).then(
|
||||
() => {
|
||||
setSuccessData({
|
||||
title: "Success! Your API Key has been saved.",
|
||||
title: API_SUCCESS_ALERT,
|
||||
});
|
||||
storeApiKey(apiKeyValue);
|
||||
setOpen(false);
|
||||
|
|
@ -42,7 +43,7 @@ export default function StoreApiKeyModal({
|
|||
},
|
||||
(error) => {
|
||||
setErrorData({
|
||||
title: "There was an error saving the API Key, please try again.",
|
||||
title: API_ERROR_ALERT,
|
||||
list: [error["response"]["data"]["detail"]],
|
||||
});
|
||||
setHasApiKey(false);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ type TriggerProps = {
|
|||
};
|
||||
|
||||
const Content: React.FC<ContentProps> = ({ children }) => {
|
||||
return <div className="h-full w-full">{children}</div>;
|
||||
return <div className="h-full w-full flex flex-col">{children}</div>;
|
||||
};
|
||||
const Trigger: React.FC<TriggerProps> = ({ children, asChild, disable }) => {
|
||||
return (
|
||||
|
|
@ -66,6 +66,7 @@ interface BaseModalProps {
|
|||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "large-thin"
|
||||
| "large-h-full"
|
||||
| "small-h-full"
|
||||
| "medium-h-full"
|
||||
|
|
@ -128,6 +129,10 @@ function BaseModal({
|
|||
minWidth = "min-w-[80vw]";
|
||||
height = "h-[80vh]";
|
||||
break;
|
||||
case "large-thin":
|
||||
minWidth = "min-w-[65vw]";
|
||||
height = "h-[80vh]";
|
||||
break;
|
||||
case "large-h-full":
|
||||
minWidth = "min-w-[80vw]";
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { useDarkStore } from "../../stores/darkStore";
|
|||
import useFlowStore from "../../stores/flowStore";
|
||||
import { codeAreaModalPropsType } from "../../types/components";
|
||||
import BaseModal from "../baseModal";
|
||||
import { BUG_ALERT, CODE_ERROR_ALERT, CODE_SUCCESS_ALERT, FUNC_ERROR_ALERT, IMPORT_ERROR_ALERT } from "../../alerts_constants";
|
||||
|
||||
export default function CodeAreaModal({
|
||||
value,
|
||||
|
|
@ -58,7 +59,7 @@ export default function CodeAreaModal({
|
|||
let funcErrors = apiReturn.data.function.errors;
|
||||
if (funcErrors.length === 0 && importsErrors.length === 0) {
|
||||
setSuccessData({
|
||||
title: "Code is ready to run",
|
||||
title: CODE_SUCCESS_ALERT,
|
||||
});
|
||||
setOpen(false);
|
||||
setValue(code);
|
||||
|
|
@ -66,26 +67,26 @@ export default function CodeAreaModal({
|
|||
} else {
|
||||
if (funcErrors.length !== 0) {
|
||||
setErrorData({
|
||||
title: "There is an error in your function",
|
||||
title: FUNC_ERROR_ALERT,
|
||||
list: funcErrors,
|
||||
});
|
||||
}
|
||||
if (importsErrors.length !== 0) {
|
||||
setErrorData({
|
||||
title: "There is an error in your imports",
|
||||
title: IMPORT_ERROR_ALERT,
|
||||
list: importsErrors,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Something went wrong, please try again",
|
||||
title: BUG_ALERT,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((_) => {
|
||||
setErrorData({
|
||||
title: "There is something wrong with this code, please review it",
|
||||
title: CODE_ERROR_ALERT,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue