Hotfix: fix bug on refresh and and for sending files to backend (#384)

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-05-28 12:01:10 -03:00 • committed by GitHub
commit 4ef3e1a0aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
118 changed files with 7090 additions and 5905 deletions

2
.githooks/pre-commit Executable file
View file

@ -0,0 +1,2 @@
make format

View file

@ -9,7 +9,7 @@ env:
POETRY_VERSION: "1.4.0" POETRY_VERSION: "1.4.0"
jobs: jobs:
build: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:

3
.gitignore vendored
View file

@ -5,12 +5,13 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log* lerna-debug.log*
qdrant_storage
# Mac # Mac
.DS_Store .DS_Store
# VSCode # VSCode
.vscode .vscode/settings.json
.chroma .chroma
.ruff_cache .ruff_cache

35
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,35 @@
{
"configurations": [
{
"name": "Python: FastAPI",
"type": "python",
"request": "launch",
"module": "uvicorn",
"args": [
"langflow.main:app",
"--port",
"7860",
"--reload",
"--log-level",
"debug"
],
"jinja": true,
"justMyCode": false
},
{
"name": "Python: Remote Attach",
"type": "python",
"request": "attach",
"justMyCode": true,
"connect": {
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "."
}
]
}
]
}

View file

@ -42,6 +42,35 @@ the system we use to tag our issues and pull requests.
### Local development ### Local development
You can develop LangFlow using docker compose, or locally. You can develop LangFlow using docker compose, or locally.
We provide a .vscode/launch.json file for debugging the backend in VSCode, which is a lot faster than using docker compose.
Setting up hooks:
```bash
make init
```
This will install the pre-commit hooks, which will run `make format` on every commit.
It is advised to run `make lint` before pushing to the repository.
#### **Locally**
Run locally by cloning the repository and installing the dependencies. We recommend using a virtual environment to isolate the dependencies from your system.
Before you start, make sure you have the following installed:
- Poetry (>=1.4)
- Node.js
For the backend, you will need to install the dependencies and start the development server.
```bash
make install_backend
make backend
```
For the frontend, you will need to install the dependencies and start the development server.
```bash
make frontend
```
#### **Docker compose** #### **Docker compose**
This will run the backend and frontend in separate containers. The frontend will be available at `localhost:3000` and the backend at `localhost:7860`. This will run the backend and frontend in separate containers. The frontend will be available at `localhost:3000` and the backend at `localhost:7860`.
```bash ```bash
@ -50,22 +79,4 @@ docker compose up --build
make dev build=1 make dev build=1
``` ```
#### **Locally**
Run locally by cloning the repository and installing the dependencies. We recommend using a virtual environment to isolate the dependencies from your system.
Before you start, make sure you have the following installed:
- Poetry
- Node.js
For the backend, you will need to install the dependencies and start the development server.
```bash
poetry install
make run_backend
```
For the frontend, you will need to install the dependencies and start the development server.
```bash
cd src/frontend
npm install
npm start
```

View file

@ -1,7 +1,11 @@
.PHONY: all format lint build build_frontend install_frontend run_frontend run_backend dev help tests coverage .PHONY: all init format lint build build_frontend install_frontend run_frontend run_backend dev help tests coverage
all: help all: help
init:
@echo 'Installing pre-commit hooks'
git config core.hooksPath .githooks
coverage: coverage:
poetry run pytest --cov \ poetry run pytest --cov \
--cov-config=.coveragerc \ --cov-config=.coveragerc \
@ -13,7 +17,8 @@ tests:
format: format:
poetry run black . poetry run black .
poetry run ruff --select I --fix . poetry run ruff . --fix
cd src/frontend && npm run format
lint: lint:
poetry run mypy . poetry run mypy .

1202
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -21,7 +21,7 @@ include = ["src/backend/langflow/*", "src/backend/langflow/**/*"]
langflow = "langflow.__main__:main" langflow = "langflow.__main__:main"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = ">=3.9,<3.12"
fastapi = "^0.92.0" fastapi = "^0.92.0"
uvicorn = "^0.20.0" uvicorn = "^0.20.0"
beautifulsoup4 = "^4.11.2" beautifulsoup4 = "^4.11.2"
@ -47,11 +47,14 @@ fake-useragent = "^1.1.3"
docstring-parser = "^0.15" docstring-parser = "^0.15"
psycopg2-binary = "^2.9.6" psycopg2-binary = "^2.9.6"
pyarrow = "^11.0.0" pyarrow = "^11.0.0"
websockets = "^11.0.2"
tiktoken = "^0.3.3" tiktoken = "^0.3.3"
wikipedia = "^1.4.0" wikipedia = "^1.4.0"
gptcache = "^0.1.23"
langchain-serve = { version = "^0.0.33", optional = true } langchain-serve = { version = "^0.0.33", optional = true }
qdrant-client = "^1.2.0"
websockets = "^11.0.3"
weaviate-client = "^3.19.2"
jina = "3.15.2"
sentence-transformers = "^2.2.2"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "^23.1.0" black = "^23.1.0"

View file

@ -18,59 +18,10 @@ def get_number_of_workers(workers=None):
return workers return workers
def update_settings(config: str): def update_settings(config: str, dev: bool = False):
"""Update the settings from a config file.""" """Update the settings from a config file."""
if config: if config:
settings.update_from_yaml(config) settings.update_from_yaml(config, dev=dev)
@app.command()
def serve(
host: str = typer.Option("127.0.0.1", help="Host to bind the server to."),
workers: int = typer.Option(1, help="Number of worker processes."),
timeout: int = typer.Option(60, help="Worker timeout in seconds."),
port: int = typer.Option(7860, help="Port to listen on."),
config: str = typer.Option("config.yaml", help="Path to the configuration file."),
log_level: str = typer.Option("info", help="Logging level."),
log_file: Path = typer.Option("logs/langflow.log", help="Path to the log file."),
jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"),
):
"""
Run the Langflow server.
"""
if jcloud:
return serve_on_jcloud()
configure(log_level=log_level, log_file=log_file)
update_settings(config)
app = create_app()
# get the directory of the current file
path = Path(__file__).parent
static_files_dir = path / "frontend"
app.mount(
"/",
StaticFiles(directory=static_files_dir, html=True),
name="static",
)
options = {
"bind": f"{host}:{port}",
"workers": get_number_of_workers(workers),
"worker_class": "uvicorn.workers.UvicornWorker",
"timeout": timeout,
}
if platform.system() in ["Darwin", "Windows"]:
# Run using uvicorn on MacOS and Windows
# Windows doesn't support gunicorn
# MacOS requires an env variable to be set to use gunicorn
import uvicorn
uvicorn.run(app, host=host, port=port, log_level=log_level)
else:
from langflow.server import LangflowApplication
LangflowApplication(app, options).run()
def serve_on_jcloud(): def serve_on_jcloud():
@ -119,6 +70,56 @@ def serve_on_jcloud():
click.secho("https://github.com/jina-ai/langchain-serve", fg="blue") click.secho("https://github.com/jina-ai/langchain-serve", fg="blue")
@app.command()
def serve(
host: str = typer.Option("127.0.0.1", help="Host to bind the server to."),
workers: int = typer.Option(1, help="Number of worker processes."),
timeout: int = typer.Option(60, help="Worker timeout in seconds."),
port: int = typer.Option(7860, help="Port to listen on."),
config: str = typer.Option("config.yaml", help="Path to the configuration file."),
log_level: str = typer.Option("info", help="Logging level."),
log_file: Path = typer.Option("logs/langflow.log", help="Path to the log file."),
jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"),
dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"),
):
"""
Run the Langflow server.
"""
if jcloud:
return serve_on_jcloud()
configure(log_level=log_level, log_file=log_file)
update_settings(config, dev=dev)
app = create_app()
# get the directory of the current file
path = Path(__file__).parent
static_files_dir = path / "frontend"
app.mount(
"/",
StaticFiles(directory=static_files_dir, html=True),
name="static",
)
options = {
"bind": f"{host}:{port}",
"workers": get_number_of_workers(workers),
"worker_class": "uvicorn.workers.UvicornWorker",
"timeout": timeout,
}
if platform.system() in ["Darwin", "Windows"]:
# Run using uvicorn on MacOS and Windows
# Windows doesn't support gunicorn
# MacOS requires an env variable to be set to use gunicorn
import uvicorn
uvicorn.run(app, host=host, port=port, log_level=log_level)
else:
from langflow.server import LangflowApplication
LangflowApplication(app, options).run()
def main(): def main():
app() app()

View file

@ -29,10 +29,10 @@ class ChatHistory(Subject):
if not isinstance(message, FileResponse): if not isinstance(message, FileResponse):
self.notify() self.notify()
def get_history(self, client_id: str, filter=True) -> List[ChatMessage]: def get_history(self, client_id: str, filter_messages=True) -> List[ChatMessage]:
"""Get the chat history for a client.""" """Get the chat history for a client."""
if history := self.history.get(client_id, []): if history := self.history.get(client_id, []):
if filter: if filter_messages:
return [msg for msg in history if msg.type not in ["start", "stream"]] return [msg for msg in history if msg.type not in ["start", "stream"]]
return history return history
else: else:
@ -54,7 +54,9 @@ class ChatManager:
"""Send the last chat message to the client.""" """Send the last chat message to the client."""
client_id = self.cache_manager.current_client_id client_id = self.cache_manager.current_client_id
if client_id in self.active_connections: if client_id in self.active_connections:
chat_response = self.chat_history.get_history(client_id, filter=False)[-1] chat_response = self.chat_history.get_history(
client_id, filter_messages=False
)[-1]
if chat_response.is_bot: if chat_response.is_bot:
# Process FileResponse # Process FileResponse
if isinstance(chat_response, FileResponse): if isinstance(chat_response, FileResponse):
@ -128,7 +130,7 @@ class ChatManager:
raise e raise e
# Send a response back to the frontend, if needed # Send a response back to the frontend, if needed
intermediate_steps = intermediate_steps or "" intermediate_steps = intermediate_steps or ""
history = self.chat_history.get_history(client_id, filter=False) history = self.chat_history.get_history(client_id, filter_messages=False)
file_responses = [] file_responses = []
if history: if history:
# Iterate backwards through the history # Iterate backwards through the history

View file

@ -120,7 +120,7 @@ def save_binary_file(content: str, file_name: str, accepted_types: list[str]) ->
# Get the destination folder # Get the destination folder
cache_path = Path(tempfile.gettempdir()) / PREFIX cache_path = Path(tempfile.gettempdir()) / PREFIX
if content is None: if not content:
raise ValueError("Please, reload the file in the loader.") raise ValueError("Please, reload the file in the loader.")
data = content.split(",")[1] data = content.split(",")[1]
decoded_bytes = base64.b64decode(data) decoded_bytes = base64.b64decode(data)

View file

@ -1,3 +1,12 @@
---
agents:
- ZeroShotAgent
- JsonAgent
- CSVAgent
- initialize_agent
- VectorStoreAgent
- VectorStoreRouterAgent
- SQLAgent
chains: chains:
- LLMChain - LLMChain
- LLMMathChain - LLMMathChain
@ -7,24 +16,35 @@ chains:
- MidJourneyPromptChain - MidJourneyPromptChain
- TimeTravelGuideChain - TimeTravelGuideChain
- SQLDatabaseChain - SQLDatabaseChain
documentloaders:
agents: - AirbyteJSONLoader
- ZeroShotAgent - CoNLLULoader
- JsonAgent - CSVLoader
- CSVAgent - UnstructuredEmailLoader
- initialize_agent - EverNoteLoader
- VectorStoreAgent - FacebookChatLoader
- VectorStoreRouterAgent - GutenbergLoader
- SQLAgent - BSHTMLLoader
- UnstructuredHTMLLoader
prompts: # - UnstructuredImageLoader # Issue with Python 3.11 (https://github.com/Unstructured-IO/unstructured-inference/issues/83)
- PromptTemplate - UnstructuredMarkdownLoader
- FewShotPromptTemplate - PyPDFLoader
- ZeroShotPrompt - UnstructuredPowerPointLoader
# Wait more tests - SRTLoader
# - ChatPromptTemplate - TelegramChatLoader
# - SystemMessagePromptTemplate - TextLoader
# - HumanMessagePromptTemplate - UnstructuredWordDocumentLoader
- WebBaseLoader
- AZLyricsLoader
- CollegeConfidentialLoader
- HNLoader
- IFixitLoader
- IMSDbLoader
- GitbookLoader
- ReadTheDocsLoader
embeddings:
- OpenAIEmbeddings
- HuggingFaceEmbeddings
llms: llms:
- OpenAI - OpenAI
@ -32,7 +52,21 @@ llms:
- ChatOpenAI - ChatOpenAI
- HuggingFaceHub - HuggingFaceHub
- LlamaCpp - LlamaCpp
memories:
- ConversationBufferMemory
- ConversationSummaryMemory
- ConversationKGMemory
prompts:
- PromptTemplate
- FewShotPromptTemplate
- ZeroShotPrompt
textsplitters:
- CharacterTextSplitter
toolkits:
- OpenAPIToolkit
- JsonToolkit
- VectorStoreInfo
- VectorStoreRouterToolkit
tools: tools:
- Search - Search
- PAL-MATH - PAL-MATH
@ -63,57 +97,6 @@ tools:
- RequestsDeleteTool - RequestsDeleteTool
- WikipediaQueryRun - WikipediaQueryRun
- WolframAlphaQueryRun - WolframAlphaQueryRun
wrappers:
- RequestsWrapper
toolkits:
- OpenAPIToolkit
- JsonToolkit
- VectorStoreInfo
- VectorStoreRouterToolkit
memories:
- ConversationBufferMemory
- ConversationSummaryMemory
- ConversationKGMemory
embeddings:
- OpenAIEmbeddings
vectorstores:
- Chroma
documentloaders:
- AirbyteJSONLoader
- CoNLLULoader
- CSVLoader
- UnstructuredEmailLoader
- EverNoteLoader
- FacebookChatLoader
- GutenbergLoader
- BSHTMLLoader
- UnstructuredHTMLLoader
# - UnstructuredImageLoader # Issue with Python 3.11 (https://github.com/Unstructured-IO/unstructured-inference/issues/83)
- UnstructuredMarkdownLoader
- PyPDFLoader
- UnstructuredPowerPointLoader
- SRTLoader
- TelegramChatLoader
- TextLoader
- UnstructuredWordDocumentLoader
- WebBaseLoader
- AZLyricsLoader
- CollegeConfidentialLoader
- HNLoader
- IFixitLoader
- IMSDbLoader
- GitbookLoader
- ReadTheDocsLoader
textsplitters:
- CharacterTextSplitter
utilities: utilities:
- BingSearchAPIWrapper - BingSearchAPIWrapper
- GoogleSearchAPIWrapper - GoogleSearchAPIWrapper
@ -125,5 +108,12 @@ utilities:
- WolframAlphaAPIWrapper - WolframAlphaAPIWrapper
# - ZapierNLAWrapper # - ZapierNLAWrapper
- SQLDatabase - SQLDatabase
vectorstores:
dev: false - Chroma
- Qdrant
- Weaviate
wrappers:
- RequestsWrapper # Wait more tests
# - ChatPromptTemplate
# - SystemMessagePromptTemplate
# - HumanMessagePromptTemplate

View file

@ -1,24 +1,27 @@
from langflow.template import nodes from langflow.template import frontend_node
# These should always be instantiated # These should always be instantiated
CUSTOM_NODES = { CUSTOM_NODES = {
"prompts": {"ZeroShotPrompt": nodes.ZeroShotPromptNode()}, "prompts": {"ZeroShotPrompt": frontend_node.prompts.ZeroShotPromptNode()},
"tools": {"PythonFunction": nodes.PythonFunctionNode(), "Tool": nodes.ToolNode()}, "tools": {
"PythonFunction": frontend_node.tools.PythonFunctionNode(),
"Tool": frontend_node.tools.ToolNode(),
},
"agents": { "agents": {
"JsonAgent": nodes.JsonAgentNode(), "JsonAgent": frontend_node.agents.JsonAgentNode(),
"CSVAgent": nodes.CSVAgentNode(), "CSVAgent": frontend_node.agents.CSVAgentNode(),
"initialize_agent": nodes.InitializeAgentNode(), "initialize_agent": frontend_node.agents.InitializeAgentNode(),
"VectorStoreAgent": nodes.VectorStoreAgentNode(), "VectorStoreAgent": frontend_node.agents.VectorStoreAgentNode(),
"VectorStoreRouterAgent": nodes.VectorStoreRouterAgentNode(), "VectorStoreRouterAgent": frontend_node.agents.VectorStoreRouterAgentNode(),
"SQLAgent": nodes.SQLAgentNode(), "SQLAgent": frontend_node.agents.SQLAgentNode(),
}, },
"utilities": { "utilities": {
"SQLDatabase": nodes.SQLDatabaseNode(), "SQLDatabase": frontend_node.agents.SQLDatabaseNode(),
}, },
"chains": { "chains": {
"SeriesCharacterChain": nodes.SeriesCharacterChainNode(), "SeriesCharacterChain": frontend_node.chains.SeriesCharacterChainNode(),
"TimeTravelGuideChain": nodes.TimeTravelGuideChainNode(), "TimeTravelGuideChain": frontend_node.chains.TimeTravelGuideChainNode(),
"MidJourneyPromptChain": nodes.MidJourneyPromptChainNode(), "MidJourneyPromptChain": frontend_node.chains.MidJourneyPromptChainNode(),
}, },
} }

View file

@ -180,7 +180,13 @@ class Node:
elif isinstance(value, list) and all( elif isinstance(value, list) and all(
isinstance(node, Node) for node in value isinstance(node, Node) for node in value
): ):
self.params[key] = [node.build() for node in value] # type: ignore self.params[key] = []
for node in value:
built = node.build()
if isinstance(built, list):
self.params[key].extend(built)
else:
self.params[key].append(built)
# Get the class from LANGCHAIN_TYPES_DICT # Get the class from LANGCHAIN_TYPES_DICT
# and instantiate it with the params # and instantiate it with the params

View file

@ -1,4 +1,3 @@
from copy import deepcopy
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from langflow.graph.base import Node from langflow.graph.base import Node

View file

@ -28,7 +28,6 @@ from langchain.agents.agent_toolkits.vectorstore.prompt import (
ROUTER_PREFIX as VECTORSTORE_ROUTER_PREFIX, ROUTER_PREFIX as VECTORSTORE_ROUTER_PREFIX,
) )
from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS
from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS as SQL_FORMAT_INSTRUCTIONS
from langchain.base_language import BaseLanguageModel from langchain.base_language import BaseLanguageModel
from langchain.memory.chat_memory import BaseChatMemory from langchain.memory.chat_memory import BaseChatMemory
from langchain.sql_database import SQLDatabase from langchain.sql_database import SQLDatabase
@ -220,7 +219,7 @@ class SQLAgent(CustomAgentExecutor):
QuerySQLDataBaseTool(db=db), # type: ignore QuerySQLDataBaseTool(db=db), # type: ignore
InfoSQLDatabaseTool(db=db), # type: ignore InfoSQLDatabaseTool(db=db), # type: ignore
ListSQLDatabaseTool(db=db), # type: ignore ListSQLDatabaseTool(db=db), # type: ignore
QueryCheckerTool(db=db, llm_chain=llmchain), # type: ignore QueryCheckerTool(db=db, llm_chain=llmchain, llm=llm), # type: ignore
] ]
prefix = SQL_PREFIX.format(dialect=toolkit.dialect, top_k=10) prefix = SQL_PREFIX.format(dialect=toolkit.dialect, top_k=10)
@ -228,7 +227,7 @@ class SQLAgent(CustomAgentExecutor):
tools=tools, # type: ignore tools=tools, # type: ignore
prefix=prefix, prefix=prefix,
suffix=SQL_SUFFIX, suffix=SQL_SUFFIX,
format_instructions=SQL_FORMAT_INSTRUCTIONS, format_instructions=FORMAT_INSTRUCTIONS,
) )
llm_chain = LLMChain( llm_chain = LLMChain(
llm=llm, llm=llm,

View file

@ -3,7 +3,9 @@ from typing import Any, Dict, List, Optional, Type, Union
from pydantic import BaseModel from pydantic import BaseModel
from langflow.template.base import FrontendNode, Template, TemplateField from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
from langflow.template.template.base import Template
from langflow.utils.logger import logger from langflow.utils.logger import logger
# Assuming necessary imports for Field, Template, and FrontendNode classes # Assuming necessary imports for Field, Template, and FrontendNode classes
@ -42,7 +44,7 @@ class LangChainTypeCreator(BaseModel, ABC):
# so we should update the result dict # so we should update the result dict
node = self.frontend_node(name) node = self.frontend_node(name)
if node is not None: if node is not None:
node = node.to_dict() node = node.to_dict() # type: ignore
result[self.type_name].update(node) result[self.type_name].update(node)
return result return result

View file

@ -4,7 +4,7 @@ from langflow.custom.customs import get_custom_nodes
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.custom_lists import chain_type_to_cls_dict from langflow.interface.custom_lists import chain_type_to_cls_dict
from langflow.settings import settings from langflow.settings import settings
from langflow.template.nodes import ChainFrontendNode from langflow.template.frontend_node.chains import ChainFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class

View file

@ -9,12 +9,9 @@ from langchain import (
memory, memory,
requests, requests,
text_splitter, text_splitter,
utilities,
vectorstores,
) )
from langchain.agents import agent_toolkits from langchain.agents import agent_toolkits
from langchain.chat_models import ChatOpenAI from langchain.chat_models import ChatOpenAI
from langchain.sql_database import SQLDatabase
from langflow.interface.importing.utils import import_class from langflow.interface.importing.utils import import_class
@ -60,11 +57,6 @@ embedding_type_to_cls_dict: dict[str, Any] = {
for embedding_name in embeddings.__all__ for embedding_name in embeddings.__all__
} }
## Vector Stores
vectorstores_type_to_cls_dict: dict[str, Any] = {
vectorstore_name: import_class(f"langchain.vectorstores.{vectorstore_name}")
for vectorstore_name in vectorstores.__all__
}
## Document Loaders ## Document Loaders
documentloaders_type_to_cls_dict: dict[str, Any] = { documentloaders_type_to_cls_dict: dict[str, Any] = {
@ -78,9 +70,3 @@ documentloaders_type_to_cls_dict: dict[str, Any] = {
textsplitter_type_to_cls_dict: dict[str, Any] = dict( textsplitter_type_to_cls_dict: dict[str, Any] = dict(
inspect.getmembers(text_splitter, inspect.isclass) inspect.getmembers(text_splitter, inspect.isclass)
) )
## Utilities
utility_type_to_cls_dict: dict[str, Any] = dict(
inspect.getmembers(utilities, inspect.isclass)
)
utility_type_to_cls_dict["SQLDatabase"] = SQLDatabase

View file

@ -3,8 +3,8 @@ from typing import Dict, List, Optional, Type
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.custom_lists import embedding_type_to_cls_dict from langflow.interface.custom_lists import embedding_type_to_cls_dict
from langflow.settings import settings from langflow.settings import settings
from langflow.template.base import FrontendNode from langflow.template.frontend_node.base import FrontendNode
from langflow.template.nodes import EmbeddingFrontendNode from langflow.template.frontend_node.embeddings import EmbeddingFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class

View file

@ -71,9 +71,9 @@ def import_class(class_path: str) -> Any:
def import_prompt(prompt: str) -> Type[PromptTemplate]: def import_prompt(prompt: str) -> Type[PromptTemplate]:
"""Import prompt from prompt name"""
from langflow.interface.prompts.custom import CUSTOM_PROMPTS from langflow.interface.prompts.custom import CUSTOM_PROMPTS
"""Import prompt from prompt name"""
if prompt == "ZeroShotPrompt": if prompt == "ZeroShotPrompt":
return import_class("langchain.prompts.PromptTemplate") return import_class("langchain.prompts.PromptTemplate")
elif prompt in CUSTOM_PROMPTS: elif prompt in CUSTOM_PROMPTS:

View file

@ -3,7 +3,7 @@ from typing import Dict, List, Optional, Type
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.custom_lists import llm_type_to_cls_dict from langflow.interface.custom_lists import llm_type_to_cls_dict
from langflow.settings import settings from langflow.settings import settings
from langflow.template.nodes import LLMFrontendNode from langflow.template.frontend_node.llms import LLMFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class

View file

@ -152,10 +152,10 @@ def instantiate_utility(node_type, class_object, params):
def load_flow_from_json(path: str, build=True): def load_flow_from_json(path: str, build=True):
"""Load flow from json file"""
# This is done to avoid circular imports # This is done to avoid circular imports
from langflow.graph import Graph from langflow.graph import Graph
"""Load flow from json file"""
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
flow_graph = json.load(f) flow_graph = json.load(f)
data_graph = flow_graph["data"] data_graph = flow_graph["data"]

View file

@ -3,8 +3,8 @@ from typing import Dict, List, Optional, Type
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.custom_lists import memory_type_to_cls_dict from langflow.interface.custom_lists import memory_type_to_cls_dict
from langflow.settings import settings from langflow.settings import settings
from langflow.template.base import FrontendNode from langflow.template.frontend_node.base import FrontendNode
from langflow.template.nodes import MemoryFrontendNode from langflow.template.frontend_node.memories import MemoryFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class

View file

@ -6,7 +6,7 @@ from langflow.custom.customs import get_custom_nodes
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.importing.utils import import_class from langflow.interface.importing.utils import import_class
from langflow.settings import settings from langflow.settings import settings
from langflow.template.nodes import PromptFrontendNode from langflow.template.frontend_node.prompts import PromptFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class

View file

@ -16,7 +16,8 @@ from langflow.interface.tools.constants import (
) )
from langflow.interface.tools.util import get_tool_params from langflow.interface.tools.util import get_tool_params
from langflow.settings import settings from langflow.settings import settings
from langflow.template.base import Template, TemplateField from langflow.template.field.base import TemplateField
from langflow.template.template.base import Template
from langflow.utils import util from langflow.utils import util
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class

View file

@ -1,9 +1,12 @@
from typing import Dict, List, Optional from typing import Dict, List, Optional, Type
from langchain import SQLDatabase, utilities
from langflow.custom.customs import get_custom_nodes from langflow.custom.customs import get_custom_nodes
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.custom_lists import utility_type_to_cls_dict from langflow.interface.importing.utils import import_class
from langflow.settings import settings from langflow.settings import settings
from langflow.template.frontend_node.utilities import UtilitiesFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_class
@ -11,16 +14,39 @@ from langflow.utils.util import build_template_from_class
class UtilityCreator(LangChainTypeCreator): class UtilityCreator(LangChainTypeCreator):
type_name: str = "utilities" type_name: str = "utilities"
@property
def frontend_node_class(self) -> Type[UtilitiesFrontendNode]:
return UtilitiesFrontendNode
@property @property
def type_to_loader_dict(self) -> Dict: def type_to_loader_dict(self) -> Dict:
return utility_type_to_cls_dict """
Returns a dictionary mapping utility names to their corresponding loader classes.
If the dictionary has not been created yet, it is created by importing all utility classes
from the langchain.chains module and filtering them according to the settings.utilities list.
"""
if self.type_dict is None:
self.type_dict = {
utility_name: import_class(f"langchain.utilities.{utility_name}")
for utility_name in utilities.__all__
}
self.type_dict["SQLDatabase"] = SQLDatabase
# Filter according to settings.utilities
self.type_dict = {
name: utility
for name, utility in self.type_dict.items()
if name in settings.utilities or settings.dev
}
return self.type_dict
def get_signature(self, name: str) -> Optional[Dict]: def get_signature(self, name: str) -> Optional[Dict]:
"""Get the signature of a utility.""" """Get the signature of a utility."""
try: try:
if name in get_custom_nodes(self.type_name).keys(): custom_nodes = get_custom_nodes(self.type_name)
return get_custom_nodes(self.type_name)[name] if name in custom_nodes.keys():
return build_template_from_class(name, utility_type_to_cls_dict) return custom_nodes[name]
return build_template_from_class(name, self.type_to_loader_dict)
except ValueError as exc: except ValueError as exc:
raise ValueError(f"Utility {name} not found") from exc raise ValueError(f"Utility {name} not found") from exc
@ -29,11 +55,7 @@ class UtilityCreator(LangChainTypeCreator):
return None return None
def to_list(self) -> List[str]: def to_list(self) -> List[str]:
return [ return list(self.type_to_loader_dict.keys())
utility.__name__
for utility in self.type_to_loader_dict.values()
if utility.__name__ in settings.utilities or settings.dev
]
utility_creator = UtilityCreator() utility_creator = UtilityCreator()

View file

@ -1,43 +1,41 @@
from typing import Dict, List, Optional from typing import Any, Dict, List, Optional, Type
from langchain import vectorstores
from langflow.interface.base import LangChainTypeCreator from langflow.interface.base import LangChainTypeCreator
from langflow.interface.custom_lists import vectorstores_type_to_cls_dict from langflow.interface.importing.utils import import_class
from langflow.settings import settings from langflow.settings import settings
from langflow.template.frontend_node.vectorstores import VectorStoreFrontendNode
from langflow.utils.logger import logger from langflow.utils.logger import logger
from langflow.utils.util import build_template_from_class from langflow.utils.util import build_template_from_method
class VectorstoreCreator(LangChainTypeCreator): class VectorstoreCreator(LangChainTypeCreator):
type_name: str = "vectorstores" type_name: str = "vectorstores"
@property
def frontend_node_class(self) -> Type[VectorStoreFrontendNode]:
return VectorStoreFrontendNode
@property @property
def type_to_loader_dict(self) -> Dict: def type_to_loader_dict(self) -> Dict:
return vectorstores_type_to_cls_dict if self.type_dict is None:
self.type_dict: dict[str, Any] = {
vectorstore_name: import_class(
f"langchain.vectorstores.{vectorstore_name}"
)
for vectorstore_name in vectorstores.__all__
}
return self.type_dict
def get_signature(self, name: str) -> Optional[Dict]: def get_signature(self, name: str) -> Optional[Dict]:
"""Get the signature of an embedding.""" """Get the signature of an embedding."""
try: try:
signature = build_template_from_class(name, vectorstores_type_to_cls_dict) return build_template_from_method(
name,
# TODO: Use FrontendendNode class to build the signature type_to_cls_dict=self.type_to_loader_dict,
signature["template"] = { method_name="from_texts",
"documents": { )
"type": "TextSplitter",
"required": True,
"show": True,
"name": "documents",
"display_name": "Text Splitter",
},
"embedding": {
"type": "Embeddings",
"required": True,
"show": True,
"name": "embedding",
"display_name": "Embedding",
},
}
return signature
except ValueError as exc: except ValueError as exc:
raise ValueError(f"Vector Store {name} not found") from exc raise ValueError(f"Vector Store {name} not found") from exc
except AttributeError as exc: except AttributeError as exc:

View file

@ -32,7 +32,7 @@ class Settings(BaseSettings):
values[key] = [] values[key] = []
return values return values
def update_from_yaml(self, file_path: str): def update_from_yaml(self, file_path: str, dev: bool = False):
new_settings = load_settings_from_yaml(file_path) new_settings = load_settings_from_yaml(file_path)
self.chains = new_settings.chains or [] self.chains = new_settings.chains or []
self.agents = new_settings.agents or [] self.agents = new_settings.agents or []
@ -44,7 +44,7 @@ class Settings(BaseSettings):
self.toolkits = new_settings.toolkits or [] self.toolkits = new_settings.toolkits or []
self.textsplitters = new_settings.textsplitters or [] self.textsplitters = new_settings.textsplitters or []
self.utilities = new_settings.utilities or [] self.utilities = new_settings.utilities or []
self.dev = new_settings.dev or False self.dev = dev
def save_settings_to_yaml(settings: Settings, file_path: str): def save_settings_to_yaml(settings: Settings, file_path: str):

View file

@ -1,251 +1 @@
from abc import ABC
from typing import Any, Callable, Dict, Optional, Union
from pydantic import BaseModel
from langflow.template.constants import FORCE_SHOW_FIELDS
from langflow.utils import constants
class TemplateFieldCreator(BaseModel, ABC):
field_type: str = "str"
required: bool = False
placeholder: str = ""
is_list: bool = False
show: bool = True
multiline: bool = False
value: Any = None
suffixes: list[str] = []
fileTypes: list[str] = []
file_types: list[str] = []
content: Union[str, None] = None
password: bool = False
options: list[str] = []
name: str = ""
display_name: Optional[str] = None
advanced: bool = False
def to_dict(self):
result = self.dict()
# Remove key if it is None
for key in list(result.keys()):
if result[key] is None or result[key] == []:
del result[key]
result["type"] = result.pop("field_type")
result["list"] = result.pop("is_list")
if result.get("file_types"):
result["fileTypes"] = result.pop("file_types")
if self.field_type == "file":
result["content"] = self.content
return result
def process_field(
self, key: str, value: Dict[str, Any], name: Optional[str] = None
) -> None:
_type = value["type"]
# Remove 'Optional' wrapper
if "Optional" in _type:
_type = _type.replace("Optional[", "")[:-1]
# Check for list type
if "List" in _type:
_type = _type.replace("List[", "")[:-1]
self.is_list = True
# Replace 'Mapping' with 'dict'
if "Mapping" in _type:
_type = _type.replace("Mapping", "dict")
# Change type from str to Tool
self.field_type = "Tool" if key in {"allowed_tools"} else self.field_type
self.field_type = "int" if key in {"max_value_length"} else self.field_type
# Show or not field
self.show = bool(
(self.required and key not in ["input_variables"])
or key in FORCE_SHOW_FIELDS
or "api_key" in key
)
# Add password field
self.password = any(
text in key.lower() for text in {"password", "token", "api", "key"}
)
# Add multline
self.multiline = key in {
"suffix",
"prefix",
"template",
"examples",
"code",
"headers",
}
# Replace dict type with str
if "dict" in self.field_type.lower():
self.field_type = "code"
if key == "dict_":
self.field_type = "file"
self.suffixes = [".json", ".yaml", ".yml"]
self.file_types = ["json", "yaml", "yml"]
# Replace default value with actual value
if "default" in value:
self.value = value["default"]
if key == "headers":
self.value = """{'Authorization':
'Bearer <token>'}"""
# Add options to openai
if name == "OpenAI" and key == "model_name":
self.options = constants.OPENAI_MODELS
self.is_list = True
elif name == "ChatOpenAI" and key == "model_name":
self.options = constants.CHAT_OPENAI_MODELS
self.is_list = True
class TemplateField(TemplateFieldCreator):
pass
class Template(BaseModel):
type_name: str
fields: list[TemplateField]
def process_fields(
self,
name: Optional[str] = None,
format_field_func: Union[Callable, None] = None,
):
if format_field_func:
for field in self.fields:
format_field_func(field, name)
def to_dict(self, format_field_func=None):
self.process_fields(self.type_name, format_field_func)
result = {field.name: field.to_dict() for field in self.fields}
result["_type"] = self.type_name # type: ignore
return result
class FrontendNode(BaseModel):
template: Template
description: str
base_classes: list
name: str = ""
def to_dict(self):
return {
self.name: {
"template": self.template.to_dict(self.format_field),
"description": self.description,
"base_classes": self.base_classes,
}
}
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
key = field.name
value = field.to_dict()
_type = value["type"]
# Remove 'Optional' wrapper
if "Optional" in _type:
_type = _type.replace("Optional[", "")[:-1]
# Check for list type
if "List" in _type or "Sequence" in _type:
_type = _type.replace("List[", "")
_type = _type.replace("Sequence[", "")[:-1]
field.is_list = True
# Replace 'Mapping' with 'dict'
if "Mapping" in _type:
_type = _type.replace("Mapping", "dict")
# {'type': 'Union[float, Tuple[float, float], NoneType]'} != {'type': 'float'}
if "Union" in _type:
_type = _type.replace("Union[", "")[:-1]
_type = _type.split(",")[0]
_type = _type.replace("]", "").replace("[", "")
field.field_type = _type
# Change type from str to Tool
field.field_type = "Tool" if key in {"allowed_tools"} else field.field_type
field.field_type = "int" if key in {"max_value_length"} else field.field_type
# Show or not field
field.show = bool(
(field.required and key not in ["input_variables"])
or key in FORCE_SHOW_FIELDS
or "api" in key
or ("key" in key and "input" not in key and "output" not in key)
)
# Add password field
field.password = (
any(text in key.lower() for text in {"password", "token", "api", "key"})
and field.show
)
# Add multline
field.multiline = key in {
"suffix",
"prefix",
"template",
"examples",
"code",
"headers",
"description",
}
# Replace dict type with str
if "dict" in field.field_type.lower():
field.field_type = "code"
if key == "dict_":
field.field_type = "file"
field.suffixes = [".json", ".yaml", ".yml"]
field.file_types = ["json", "yaml", "yml"]
# Replace default value with actual value
if "default" in value:
field.value = value["default"]
if key == "headers":
field.value = """{'Authorization':
'Bearer <token>'}"""
# Add options to openai
if name == "OpenAI" and key == "model_name":
field.options = constants.OPENAI_MODELS
field.is_list = True
elif name == "ChatOpenAI":
if key == "model_name":
field.options = constants.CHAT_OPENAI_MODELS
field.is_list = True
if "api_key" in key and "OpenAI" in str(name):
field.display_name = "OpenAI API Key"
field.required = False
if field.value is None:
field.value = ""
if "kwargs" in field.name.lower():
field.advanced = True
field.required = False
field.show = False
# If the field.name contains api or api and key, then it might be an api key
# other conditions are to make sure that it is not an input or output variable
if "api" in key.lower() and "key" in key.lower():
field.required = False
field.advanced = False

View file

@ -0,0 +1,43 @@
from abc import ABC
from typing import Any, Optional, Union
from pydantic import BaseModel
class TemplateFieldCreator(BaseModel, ABC):
field_type: str = "str"
required: bool = False
placeholder: str = ""
is_list: bool = False
show: bool = True
multiline: bool = False
value: Any = None
suffixes: list[str] = []
fileTypes: list[str] = []
file_types: list[str] = []
content: Union[str, None] = None
password: bool = False
options: list[str] = []
name: str = ""
display_name: Optional[str] = None
advanced: bool = False
def to_dict(self):
result = self.dict()
# Remove key if it is None
for key in list(result.keys()):
if result[key] is None or result[key] == []:
del result[key]
result["type"] = result.pop("field_type")
result["list"] = result.pop("is_list")
if result.get("file_types"):
result["fileTypes"] = result.pop("file_types")
if self.field_type == "file":
result["content"] = self.content
return result
class TemplateField(TemplateFieldCreator):
pass

View file

@ -0,0 +1,21 @@
from langflow.template.frontend_node import (
agents,
chains,
embeddings,
llms,
memories,
prompts,
tools,
vectorstores,
)
__all__ = [
"agents",
"chains",
"embeddings",
"memories",
"tools",
"llms",
"prompts",
"vectorstores",
]

View file

@ -0,0 +1,233 @@
from typing import Optional
from langchain.agents import types
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
from langflow.template.template.base import Template
NON_CHAT_AGENTS = {
agent_type: agent_class
for agent_type, agent_class in types.AGENT_TO_CLASS.items()
if "chat" not in agent_type.value
}
class SQLAgentNode(FrontendNode):
name: str = "SQLAgent"
template: Template = Template(
type_name="sql_agent",
fields=[
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=False,
value="",
name="database_uri",
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
display_name="LLM",
),
],
)
description: str = """Construct a sql agent from an LLM and tools."""
base_classes: list[str] = ["AgentExecutor"]
def to_dict(self):
return super().to_dict()
class VectorStoreRouterAgentNode(FrontendNode):
name: str = "VectorStoreRouterAgent"
template: Template = Template(
type_name="vectorstorerouter_agent",
fields=[
TemplateField(
field_type="VectorStoreRouterToolkit",
required=True,
show=True,
name="vectorstoreroutertoolkit",
display_name="Vector Store Router Toolkit",
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
display_name="LLM",
),
],
)
description: str = """Construct an agent from a Vector Store Router."""
base_classes: list[str] = ["AgentExecutor"]
def to_dict(self):
return super().to_dict()
class VectorStoreAgentNode(FrontendNode):
name: str = "VectorStoreAgent"
template: Template = Template(
type_name="vectorstore_agent",
fields=[
TemplateField(
field_type="VectorStoreInfo",
required=True,
show=True,
name="vectorstoreinfo",
display_name="Vector Store Info",
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
display_name="LLM",
),
],
)
description: str = """Construct an agent from a Vector Store."""
base_classes: list[str] = ["AgentExecutor"]
def to_dict(self):
return super().to_dict()
class SQLDatabaseNode(FrontendNode):
name: str = "SQLDatabase"
template: Template = Template(
type_name="sql_database",
fields=[
TemplateField(
field_type="str",
required=True,
is_list=False,
show=True,
multiline=False,
value="",
name="uri",
),
],
)
description: str = """SQLAlchemy wrapper around a database."""
base_classes: list[str] = ["SQLDatabase"]
def to_dict(self):
return super().to_dict()
class CSVAgentNode(FrontendNode):
name: str = "CSVAgent"
template: Template = Template(
type_name="csv_agent",
fields=[
TemplateField(
field_type="file",
required=True,
show=True,
name="path",
value="",
suffixes=[".csv"],
fileTypes=["csv"],
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
display_name="LLM",
),
],
)
description: str = """Construct a json agent from a CSV and tools."""
base_classes: list[str] = ["AgentExecutor"]
def to_dict(self):
return super().to_dict()
class InitializeAgentNode(FrontendNode):
name: str = "initialize_agent"
template: Template = Template(
type_name="initailize_agent",
fields=[
TemplateField(
field_type="str",
required=True,
is_list=True,
show=True,
multiline=False,
options=list(NON_CHAT_AGENTS.keys()),
value=list(NON_CHAT_AGENTS.keys())[0],
name="agent",
advanced=False,
),
TemplateField(
field_type="BaseChatMemory",
required=False,
show=True,
name="memory",
advanced=False,
),
TemplateField(
field_type="Tool",
required=False,
show=True,
name="tools",
is_list=True,
advanced=False,
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
display_name="LLM",
advanced=False,
),
],
)
description: str = """Construct a json agent from an LLM and tools."""
base_classes: list[str] = ["AgentExecutor", "function"]
def to_dict(self):
return super().to_dict()
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
# do nothing and don't return anything
pass
class JsonAgentNode(FrontendNode):
name: str = "JsonAgent"
template: Template = Template(
type_name="json_agent",
fields=[
TemplateField(
field_type="BaseToolkit",
required=True,
show=True,
name="toolkit",
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
display_name="LLM",
),
],
)
description: str = """Construct a json agent from an LLM and tools."""
base_classes: list[str] = ["AgentExecutor"]
def to_dict(self):
return super().to_dict()

View file

@ -0,0 +1,200 @@
import re
from typing import List, Optional
from pydantic import BaseModel
from langflow.template.constants import FORCE_SHOW_FIELDS
from langflow.template.field.base import TemplateField
from langflow.template.template.base import Template
from langflow.utils import constants
class FrontendNode(BaseModel):
template: Template
description: str
base_classes: List[str]
name: str = ""
def to_dict(self) -> dict:
return {
self.name: {
"template": self.template.to_dict(self.format_field),
"description": self.description,
"base_classes": self.base_classes,
}
}
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
"""Formats a given field based on its attributes and value."""
SPECIAL_FIELD_HANDLERS = {
"allowed_tools": lambda field: "Tool",
"max_value_length": lambda field: "int",
}
key = field.name
value = field.to_dict()
_type = value["type"]
_type = FrontendNode.remove_optional(_type)
_type, is_list = FrontendNode.check_for_list_type(_type)
field.is_list = is_list or field.is_list
_type = FrontendNode.replace_mapping_with_dict(_type)
_type = FrontendNode.handle_union_type(_type)
field.field_type = FrontendNode.handle_special_field(
field, key, _type, SPECIAL_FIELD_HANDLERS
)
field.field_type = FrontendNode.handle_dict_type(field, _type)
field.show = FrontendNode.should_show_field(key, field.required)
field.password = FrontendNode.should_be_password(key, field.show)
field.multiline = FrontendNode.should_be_multiline(key)
FrontendNode.replace_default_value(field, value)
FrontendNode.handle_specific_field_values(field, key, name)
FrontendNode.handle_kwargs_field(field)
FrontendNode.handle_api_key_field(field, key)
@staticmethod
def remove_optional(_type: str) -> str:
"""Removes 'Optional' wrapper from the type if present."""
return re.sub(r"Optional\[(.*)\]", r"\1", _type)
@staticmethod
def check_for_list_type(_type: str) -> tuple:
"""Checks for list type and returns the modified type and a boolean indicating if it's a list."""
is_list = "List" in _type or "Sequence" in _type
if is_list:
_type = re.sub(r"(List|Sequence)\[(.*)\]", r"\2", _type)
return _type, is_list
@staticmethod
def replace_mapping_with_dict(_type: str) -> str:
"""Replaces 'Mapping' with 'dict'."""
return _type.replace("Mapping", "dict")
@staticmethod
def handle_union_type(_type: str) -> str:
"""Simplifies the 'Union' type to the first type in the Union."""
if "Union" in _type:
_type = _type.replace("Union[", "")[:-1]
_type = _type.split(",")[0]
_type = _type.replace("]", "").replace("[", "")
return _type
@staticmethod
def handle_special_field(
field, key: str, _type: str, SPECIAL_FIELD_HANDLERS
) -> str:
"""Handles special field by using the respective handler if present."""
handler = SPECIAL_FIELD_HANDLERS.get(key)
return handler(field) if handler else _type
@staticmethod
def handle_dict_type(field: TemplateField, _type: str) -> str:
"""Handles 'dict' type by replacing it with 'code' or 'file' based on the field name."""
if "dict" in _type.lower():
if field.name == "dict_":
field.field_type = "file"
field.suffixes = [".json", ".yaml", ".yml"]
field.file_types = ["json", "yaml", "yml"]
else:
field.field_type = "code"
return _type
@staticmethod
def replace_default_value(field: TemplateField, value: dict) -> None:
"""Replaces default value with actual value if 'default' is present in value."""
if "default" in value:
field.value = value["default"]
@staticmethod
def handle_specific_field_values(
field: TemplateField, key: str, name: Optional[str] = None
) -> None:
"""Handles specific field values for certain fields."""
if key == "headers":
field.value = """{'Authorization':
'Bearer <token>'}"""
if name == "OpenAI" and key == "model_name":
field.options = constants.OPENAI_MODELS
field.is_list = True
elif name == "ChatOpenAI" and key == "model_name":
field.options = constants.CHAT_OPENAI_MODELS
field.is_list = True
if "api_key" in key and "OpenAI" in str(name):
field.display_name = "OpenAI API Key"
field.required = False
if field.value is None:
field.value = ""
@staticmethod
def handle_kwargs_field(field: TemplateField) -> None:
"""Handles kwargs field by setting certain attributes."""
if "kwargs" in field.name.lower():
field.advanced = True
field.required = False
field.show = False
@staticmethod
def handle_api_key_field(field: TemplateField, key: str) -> None:
"""Handles api key field by setting certain attributes."""
if "api" in key.lower() and "key" in key.lower():
field.required = False
field.advanced = False
field.display_name = key.replace("_", " ").title()
field.display_name = field.display_name.replace("Api", "API")
@staticmethod
def should_show_field(key: str, required: bool) -> bool:
"""Determines whether the field should be shown."""
return (
(required and key not in ["input_variables"])
or key in FORCE_SHOW_FIELDS
or "api" in key
or ("key" in key and "input" not in key and "output" not in key)
)
@staticmethod
def should_be_password(key: str, show: bool) -> bool:
"""Determines whether the field should be a password field."""
return (
any(text in key.lower() for text in {"password", "token", "api", "key"})
and show
)
@staticmethod
def should_be_multiline(key: str) -> bool:
"""Determines whether the field should be multiline."""
return key in {
"suffix",
"prefix",
"template",
"examples",
"code",
"headers",
"description",
}
@staticmethod
def replace_dict_with_code_or_file(
field: TemplateField, _type: str, key: str
) -> str:
"""Replaces 'dict' type with 'code' or 'file'."""
if "dict" in _type.lower():
if key == "dict_":
field.field_type = "file"
field.suffixes = [".json", ".yaml", ".yml"]
field.file_types = ["json", "yaml", "yml"]
else:
field.field_type = "code"
return field.field_type
@staticmethod
def set_field_default_value(field: TemplateField, value: dict, key: str) -> None:
"""Sets the field value with the default value if present."""
if "default" in value:
field.value = value["default"]
if key == "headers":
field.value = """{'Authorization': 'Bearer <token>'}"""

View file

@ -0,0 +1,157 @@
from typing import Optional
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
from langflow.template.template.base import Template
class ChainFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
field.advanced = False
if "key" in field.name:
field.password = False
field.show = False
if field.name in ["input_key", "output_key"]:
field.required = True
field.show = True
field.advanced = True
# Separated for possible future changes
if field.name == "prompt" and field.value is None:
field.required = True
field.show = True
field.advanced = False
if field.name == "memory":
field.required = False
field.show = True
field.advanced = False
if field.name == "verbose":
field.required = False
field.show = True
field.advanced = True
if field.name == "llm":
field.required = True
field.show = True
field.advanced = False
class SeriesCharacterChainNode(FrontendNode):
name: str = "SeriesCharacterChain"
template: Template = Template(
type_name="SeriesCharacterChain",
fields=[
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="character",
),
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="series",
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="llm",
display_name="LLM",
),
],
)
description: str = "SeriesCharacterChain is a chain you can use to have a conversation with a character from a series." # noqa
base_classes: list[str] = [
"LLMChain",
"BaseCustomChain",
"Chain",
"ConversationChain",
"SeriesCharacterChain",
"function",
]
class TimeTravelGuideChainNode(FrontendNode):
name: str = "TimeTravelGuideChain"
template: Template = Template(
type_name="TimeTravelGuideChain",
fields=[
TemplateField(
field_type="BaseLanguageModel",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="llm",
display_name="LLM",
),
TemplateField(
field_type="BaseChatMemory",
required=False,
show=True,
name="memory",
advanced=False,
),
],
)
description: str = "Time travel guide chain to be used in the flow."
base_classes: list[str] = [
"LLMChain",
"BaseCustomChain",
"TimeTravelGuideChain",
"Chain",
"ConversationChain",
]
class MidJourneyPromptChainNode(FrontendNode):
name: str = "MidJourneyPromptChain"
template: Template = Template(
type_name="MidJourneyPromptChain",
fields=[
TemplateField(
field_type="BaseLanguageModel",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="llm",
display_name="LLM",
),
TemplateField(
field_type="BaseChatMemory",
required=False,
show=True,
name="memory",
advanced=False,
),
],
)
description: str = "MidJourneyPromptChain is a chain you can use to generate new MidJourney prompts."
base_classes: list[str] = [
"LLMChain",
"BaseCustomChain",
"Chain",
"ConversationChain",
"MidJourneyPromptChain",
]

View file

@ -0,0 +1,38 @@
from typing import Optional
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
class EmbeddingFrontendNode(FrontendNode):
@staticmethod
def format_jina_fields(field: TemplateField):
if "jina" in field.name:
field.show = True
field.advanced = False
if "auth" in field.name or "token" in field.name:
field.password = True
field.show = True
field.advanced = False
if field.name == "jina_api_url":
field.show = True
field.advanced = True
field.display_name = "Jina API URL"
field.password = False
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
field.advanced = not field.required
field.show = True
if field.name == "headers":
field.show = False
if "openai" in field.name:
field.show = True
field.advanced = "api_key" not in field.name
# Format Jina fields
EmbeddingFrontendNode.format_jina_fields(field)

View file

@ -0,0 +1,50 @@
from typing import Optional
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
class LLMFrontendNode(FrontendNode):
@staticmethod
def format_openai_field(field: TemplateField):
if "openai" in field.name.lower():
field.display_name = (
field.name.title().replace("Openai", "OpenAI").replace("_", " ")
).replace("Api", "API")
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
display_names_dict = {
"huggingfacehub_api_token": "HuggingFace Hub API Token",
}
FrontendNode.format_field(field, name)
SHOW_FIELDS = ["repo_id"]
if field.name in SHOW_FIELDS:
field.show = True
if "api" in field.name and ("key" in field.name or "token" in field.name):
field.password = True
field.show = True
# Required should be False to support
# loading the API key from environment variables
field.required = False
field.advanced = False
if field.name == "task":
field.required = True
field.show = True
field.is_list = True
field.options = ["text-generation", "text2text-generation"]
field.advanced = True
if display_name := display_names_dict.get(field.name):
field.display_name = display_name
if field.name == "model_kwargs":
field.field_type = "code"
field.advanced = True
field.show = True
elif field.name in ["model_name", "temperature"]:
field.advanced = False
field.show = True
LLMFrontendNode.format_openai_field(field)

View file

@ -0,0 +1,20 @@
from typing import Optional
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
class MemoryFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
if not isinstance(field.value, str):
field.value = None
if field.name == "k":
field.required = True
field.show = True
field.field_type = "int"
field.value = 10
field.display_name = "Memory Size"
field.password = False

View file

@ -0,0 +1,111 @@
from typing import Optional
from langchain.agents.mrkl import prompt
from langflow.template.constants import DEFAULT_PROMPT, HUMAN_PROMPT, SYSTEM_PROMPT
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
from langflow.template.template.base import Template
class PromptFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
# if field.field_type == "StringPromptTemplate"
# change it to str
PROMPT_FIELDS = [
"template",
"suffix",
"prefix",
"examples",
"format_instructions",
]
if field.field_type == "StringPromptTemplate" and "Message" in str(name):
field.field_type = "prompt"
field.multiline = True
field.value = HUMAN_PROMPT if "Human" in field.name else SYSTEM_PROMPT
if field.name == "template" and field.value == "":
field.value = DEFAULT_PROMPT
if field.name in PROMPT_FIELDS:
field.field_type = "prompt"
field.advanced = False
if (
"Union" in field.field_type
and "BaseMessagePromptTemplate" in field.field_type
):
field.field_type = "BaseMessagePromptTemplate"
# All prompt fields should be password=False
field.password = False
class PromptTemplateNode(FrontendNode):
name: str = "PromptTemplate"
template: Template
description: str
base_classes: list[str] = ["BasePromptTemplate"]
def to_dict(self):
return super().to_dict()
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
if field.name == "examples":
field.advanced = False
class BasePromptFrontendNode(FrontendNode):
name: str
template: Template
description: str
base_classes: list[str]
def to_dict(self):
return super().to_dict()
class ZeroShotPromptNode(BasePromptFrontendNode):
name: str = "ZeroShotPrompt"
template: Template = Template(
type_name="zero_shot",
fields=[
TemplateField(
field_type="str",
required=False,
placeholder="",
is_list=False,
show=True,
multiline=True,
value=prompt.PREFIX,
name="prefix",
),
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=True,
value=prompt.SUFFIX,
name="suffix",
),
TemplateField(
field_type="str",
required=False,
placeholder="",
is_list=False,
show=True,
multiline=True,
value=prompt.FORMAT_INSTRUCTIONS,
name="format_instructions",
),
],
)
description: str = "Prompt template for Zero Shot Agent."
base_classes: list[str] = ["BasePromptTemplate"]
def to_dict(self):
return super().to_dict()

View file

@ -0,0 +1,83 @@
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
from langflow.template.template.base import Template
from langflow.utils.constants import DEFAULT_PYTHON_FUNCTION
class ToolNode(FrontendNode):
name: str = "Tool"
template: Template = Template(
type_name="Tool",
fields=[
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=True,
value="",
name="name",
advanced=False,
),
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=True,
value="",
name="description",
advanced=False,
),
TemplateField(
name="func",
field_type="function",
required=True,
is_list=False,
show=True,
multiline=True,
advanced=False,
),
TemplateField(
field_type="bool",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=False,
value=False,
name="return_direct",
),
],
)
description: str = "Tool to be used in the flow."
base_classes: list[str] = ["Tool"]
def to_dict(self):
return super().to_dict()
class PythonFunctionNode(FrontendNode):
name: str = "PythonFunction"
template: Template = Template(
type_name="python_function",
fields=[
TemplateField(
field_type="code",
required=True,
placeholder="",
is_list=False,
show=True,
value=DEFAULT_PYTHON_FUNCTION,
name="code",
advanced=False,
)
],
)
description: str = "Python function to be executed."
base_classes: list[str] = ["function"]
def to_dict(self):
return super().to_dict()

View file

@ -0,0 +1,22 @@
import ast
import json
from typing import Optional
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
class UtilitiesFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
# field.field_type could be "Literal['news', 'search', 'places', 'images']
# we need to convert it to a list
if "Literal" in field.field_type:
field.options = ast.literal_eval(field.field_type.replace("Literal", ""))
field.is_list = True
field.field_type = "str"
if isinstance(field.value, dict):
field.field_type = "code"
field.value = json.dumps(field.value, indent=4)

View file

@ -0,0 +1,64 @@
from typing import Optional
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
class VectorStoreFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
# Define common field attributes
basic_fields = ["work_dir", "collection_name", "api_key", "location"]
advanced_fields = [
"n_dim",
"key",
"prefix",
"distance_func",
"content_payload_key",
"metadata_payload_key",
"timeout",
"host",
"path",
"url",
"port",
"https",
"prefer_grpc",
"grpc_port",
]
# Check and set field attributes
if field.name == "texts":
field.name = "documents"
field.field_type = "TextSplitter"
field.display_name = "Text Splitter"
field.required = True
field.show = True
field.advanced = False
elif "embedding" in field.name:
# for backwards compatibility
field.name = "embedding"
field.required = True
field.show = True
field.advanced = False
field.display_name = "Embedding"
field.field_type = "Embeddings"
elif field.name in basic_fields:
field.show = True
field.advanced = False
if field.name == "api_key":
field.display_name = "API Key"
field.password = True
elif field.name == "location":
field.value = ":memory:"
field.placeholder = ":memory:"
elif field.name in advanced_fields:
field.show = True
field.advanced = True
if "key" in field.name:
field.password = False
# TODO: Weaviate requires weaviate_url to be passed as it is not part of
# the class or from_texts method. We need the add_extra_fields to fix this

View file

@ -1,630 +1 @@
from typing import Optional
from langchain.agents import loading
from langchain.agents.mrkl import prompt
from langflow.template.base import FrontendNode, Template, TemplateField
from langflow.template.constants import DEFAULT_PROMPT, HUMAN_PROMPT, SYSTEM_PROMPT
from langflow.utils.constants import DEFAULT_PYTHON_FUNCTION
NON_CHAT_AGENTS = {
agent_type: agent_class
for agent_type, agent_class in loading.AGENT_TO_CLASS.items()
if "chat" not in agent_type.value
}
class BasePromptFrontendNode(FrontendNode):
name: str
template: Template
description: str
base_classes: list[str]
def to_dict(self):
return super().to_dict()
class ZeroShotPromptNode(BasePromptFrontendNode):
name: str = "ZeroShotPrompt"
template: Template = Template(
type_name="zero_shot",
fields=[
TemplateField(
field_type="str",
required=False,
placeholder="",
is_list=False,
show=True,
multiline=True,
value=prompt.PREFIX,
name="prefix",
),
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=True,
value=prompt.SUFFIX,
name="suffix",
),
TemplateField(
field_type="str",
required=False,
placeholder="",
is_list=False,
show=True,
multiline=True,
value=prompt.FORMAT_INSTRUCTIONS,
name="format_instructions",
),
],
)
description: str = "Prompt template for Zero Shot Agent."
base_classes: list[str] = ["BasePromptTemplate"]
def to_dict(self):
return super().to_dict()
class PromptTemplateNode(FrontendNode):
name: str = "PromptTemplate"
template: Template
description: str
base_classes: list[str] = ["BasePromptTemplate"]
def to_dict(self):
return super().to_dict()
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
if field.name == "examples":
field.advanced = False
class PythonFunctionNode(FrontendNode):
name: str = "PythonFunction"
template: Template = Template(
type_name="python_function",
fields=[
TemplateField(
field_type="code",
required=True,
placeholder="",
is_list=False,
show=True,
value=DEFAULT_PYTHON_FUNCTION,
name="code",
advanced=False,
)
],
)
description: str = "Python function to be executed."
base_classes: list[str] = ["function"]
def to_dict(self):
return super().to_dict()
class MidJourneyPromptChainNode(FrontendNode):
name: str = "MidJourneyPromptChain"
template: Template = Template(
type_name="MidJourneyPromptChain",
fields=[
TemplateField(
field_type="BaseLanguageModel",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="llm",
display_name="LLM",
),
TemplateField(
field_type="BaseChatMemory",
required=False,
show=True,
name="memory",
advanced=False,
),
],
)
description: str = "MidJourneyPromptChain is a chain you can use to generate new MidJourney prompts."
base_classes: list[str] = [
"LLMChain",
"BaseCustomChain",
"Chain",
"ConversationChain",
"MidJourneyPromptChain",
]
class TimeTravelGuideChainNode(FrontendNode):
name: str = "TimeTravelGuideChain"
template: Template = Template(
type_name="TimeTravelGuideChain",
fields=[
TemplateField(
field_type="BaseLanguageModel",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="llm",
display_name="LLM",
),
TemplateField(
field_type="BaseChatMemory",
required=False,
show=True,
name="memory",
advanced=False,
),
],
)
description: str = "Time travel guide chain to be used in the flow."
base_classes: list[str] = [
"LLMChain",
"BaseCustomChain",
"TimeTravelGuideChain",
"Chain",
"ConversationChain",
]
class SeriesCharacterChainNode(FrontendNode):
name: str = "SeriesCharacterChain"
template: Template = Template(
type_name="SeriesCharacterChain",
fields=[
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="character",
),
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="series",
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
placeholder="",
is_list=False,
show=True,
advanced=False,
multiline=False,
name="llm",
display_name="LLM",
),
],
)
description: str = "SeriesCharacterChain is a chain you can use to have a conversation with a character from a series." # noqa
base_classes: list[str] = [
"LLMChain",
"BaseCustomChain",
"Chain",
"ConversationChain",
"SeriesCharacterChain",
"function",
]
class ToolNode(FrontendNode):
name: str = "Tool"
template: Template = Template(
type_name="Tool",
fields=[
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=True,
value="",
name="name",
advanced=False,
),
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=True,
value="",
name="description",
advanced=False,
),
TemplateField(
name="func",
field_type="function",
required=True,
is_list=False,
show=True,
multiline=True,
advanced=False,
),
TemplateField(
field_type="bool",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=False,
value=False,
name="return_direct",
),
],
)
description: str = "Tool to be used in the flow."
base_classes: list[str] = ["Tool"]
def to_dict(self):
return super().to_dict()
class JsonAgentNode(FrontendNode):
name: str = "JsonAgent"
template: Template = Template(
type_name="json_agent",
fields=[
TemplateField(
field_type="BaseToolkit",
required=True,
show=True,
name="toolkit",
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
display_name="LLM",
),
],
)
description: str = """Construct a json agent from an LLM and tools."""
base_classes: list[str] = ["AgentExecutor"]
def to_dict(self):
return super().to_dict()
class InitializeAgentNode(FrontendNode):
name: str = "initialize_agent"
template: Template = Template(
type_name="initailize_agent",
fields=[
TemplateField(
field_type="str",
required=True,
is_list=True,
show=True,
multiline=False,
options=list(NON_CHAT_AGENTS.keys()),
value=list(NON_CHAT_AGENTS.keys())[0],
name="agent",
advanced=False,
),
TemplateField(
field_type="BaseChatMemory",
required=False,
show=True,
name="memory",
advanced=False,
),
TemplateField(
field_type="Tool",
required=False,
show=True,
name="tools",
is_list=True,
advanced=False,
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
display_name="LLM",
advanced=False,
),
],
)
description: str = """Construct a json agent from an LLM and tools."""
base_classes: list[str] = ["AgentExecutor", "function"]
def to_dict(self):
return super().to_dict()
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
# do nothing and don't return anything
pass
class CSVAgentNode(FrontendNode):
name: str = "CSVAgent"
template: Template = Template(
type_name="csv_agent",
fields=[
TemplateField(
field_type="file",
required=True,
show=True,
name="path",
value="",
suffixes=[".csv"],
fileTypes=["csv"],
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
display_name="LLM",
),
],
)
description: str = """Construct a json agent from a CSV and tools."""
base_classes: list[str] = ["AgentExecutor"]
def to_dict(self):
return super().to_dict()
class SQLDatabaseNode(FrontendNode):
name: str = "SQLDatabase"
template: Template = Template(
type_name="sql_database",
fields=[
TemplateField(
field_type="str",
required=True,
is_list=False,
show=True,
multiline=False,
value="",
name="uri",
),
],
)
description: str = """SQLAlchemy wrapper around a database."""
base_classes: list[str] = ["SQLDatabase"]
def to_dict(self):
return super().to_dict()
class VectorStoreAgentNode(FrontendNode):
name: str = "VectorStoreAgent"
template: Template = Template(
type_name="vectorstore_agent",
fields=[
TemplateField(
field_type="VectorStoreInfo",
required=True,
show=True,
name="vectorstoreinfo",
display_name="Vector Store Info",
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
display_name="LLM",
),
],
)
description: str = """Construct an agent from a Vector Store."""
base_classes: list[str] = ["AgentExecutor"]
def to_dict(self):
return super().to_dict()
class VectorStoreRouterAgentNode(FrontendNode):
name: str = "VectorStoreRouterAgent"
template: Template = Template(
type_name="vectorstorerouter_agent",
fields=[
TemplateField(
field_type="VectorStoreRouterToolkit",
required=True,
show=True,
name="vectorstoreroutertoolkit",
display_name="Vector Store Router Toolkit",
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
display_name="LLM",
),
],
)
description: str = """Construct an agent from a Vector Store Router."""
base_classes: list[str] = ["AgentExecutor"]
def to_dict(self):
return super().to_dict()
class SQLAgentNode(FrontendNode):
name: str = "SQLAgent"
template: Template = Template(
type_name="sql_agent",
fields=[
TemplateField(
field_type="str",
required=True,
placeholder="",
is_list=False,
show=True,
multiline=False,
value="",
name="database_uri",
),
TemplateField(
field_type="BaseLanguageModel",
required=True,
show=True,
name="llm",
display_name="LLM",
),
],
)
description: str = """Construct an agent from a Vector Store Router."""
base_classes: list[str] = ["AgentExecutor"]
def to_dict(self):
return super().to_dict()
class PromptFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
# if field.field_type == "StringPromptTemplate"
# change it to str
PROMPT_FIELDS = [
"template",
"suffix",
"prefix",
"examples",
"format_instructions",
]
if field.field_type == "StringPromptTemplate" and "Message" in str(name):
field.field_type = "prompt"
field.multiline = True
field.value = HUMAN_PROMPT if "Human" in field.name else SYSTEM_PROMPT
if field.name == "template" and field.value == "":
field.value = DEFAULT_PROMPT
if field.name in PROMPT_FIELDS:
field.field_type = "prompt"
field.advanced = False
if (
"Union" in field.field_type
and "BaseMessagePromptTemplate" in field.field_type
):
field.field_type = "BaseMessagePromptTemplate"
# All prompt fields should be password=False
field.password = False
class MemoryFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
if not isinstance(field.value, str):
field.value = None
if field.name == "k":
field.required = True
field.show = True
field.field_type = "int"
field.value = 10
field.display_name = "Memory Size"
field.password = False
class ChainFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
field.advanced = False
if "key" in field.name:
field.password = False
field.show = False
if field.name in ["input_key", "output_key"]:
field.required = True
field.show = True
field.advanced = True
# Separated for possible future changes
if field.name == "prompt" and field.value is None:
# if no prompt is provided, use the default prompt
field.required = False
field.show = True
field.advanced = False
if field.name == "memory":
field.required = False
field.show = True
field.advanced = False
if field.name == "verbose":
field.required = False
field.show = True
field.advanced = True
if field.name == "llm":
field.required = True
field.show = True
field.advanced = False
class LLMFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
display_names_dict = {
"huggingfacehub_api_token": "HuggingFace Hub API Token",
}
FrontendNode.format_field(field, name)
SHOW_FIELDS = ["repo_id"]
if field.name in SHOW_FIELDS:
field.show = True
if "api" in field.name and ("key" in field.name or "token" in field.name):
field.password = True
field.show = True
# Required should be False to support
# loading the API key from environment variables
field.required = False
field.advanced = False
if field.name == "task":
field.required = True
field.show = True
field.is_list = True
field.options = ["text-generation", "text2text-generation"]
field.advanced = True
if display_name := display_names_dict.get(field.name):
field.display_name = display_name
if field.name == "model_kwargs":
field.field_type = "code"
field.advanced = True
field.show = True
elif field.name in ["model_name", "temperature"]:
field.advanced = False
field.show = True
class EmbeddingFrontendNode(FrontendNode):
@staticmethod
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
FrontendNode.format_field(field, name)
if field.name == "headers":
field.show = False

View file

@ -0,0 +1,25 @@
from typing import Callable, Optional, Union
from pydantic import BaseModel
from langflow.template.field.base import TemplateField
class Template(BaseModel):
type_name: str
fields: list[TemplateField]
def process_fields(
self,
name: Optional[str] = None,
format_field_func: Union[Callable, None] = None,
):
if format_field_func:
for field in self.fields:
format_field_func(field, name)
def to_dict(self, format_field_func=None):
self.process_fields(self.type_name, format_field_func)
result = {field.name: field.to_dict() for field in self.fields}
result["_type"] = self.type_name # type: ignore
return result

View file

@ -10,49 +10,6 @@ from langflow.template.constants import FORCE_SHOW_FIELDS
from langflow.utils import constants from langflow.utils import constants
def build_template_from_parameters(
name: str, type_to_loader_dict: Dict, add_function: bool = False
):
# Retrieve the function that matches the provided name
func = None
for _, v in type_to_loader_dict.items():
if v.__name__ == name:
func = v
break
if func is None:
raise ValueError(f"{name} not found")
# Process parameters
parameters = func.__annotations__
variables = {}
for param_name, param_type in parameters.items():
if param_name in ["return", "kwargs"]:
continue
variables[param_name] = {
"type": param_type.__name__,
"default": parameters[param_name].__repr_args__()[0][1],
# Op
"placeholder": "",
}
# Get the base classes of the return type
return_type = parameters.get("return")
base_classes = get_base_classes(return_type) if return_type else []
if add_function:
base_classes.append("function")
# Get the function's docstring
docs = inspect.getdoc(func) or ""
return {
"template": format_dict(variables, name),
"description": docs["Description"], # type: ignore
"base_classes": base_classes,
}
def build_template_from_function( def build_template_from_function(
name: str, type_to_loader_dict: Dict, add_function: bool = False name: str, type_to_loader_dict: Dict, add_function: bool = False
): ):
@ -160,6 +117,70 @@ def build_template_from_class(
} }
def build_template_from_method(
class_name: str,
method_name: str,
type_to_cls_dict: Dict,
add_function: bool = False,
):
classes = [item.__name__ for item in type_to_cls_dict.values()]
# Raise error if class_name is not in classes
if class_name not in classes:
raise ValueError(f"{class_name} not found.")
for _type, v in type_to_cls_dict.items():
if v.__name__ == class_name:
_class = v
# Check if the method exists in this class
if not hasattr(_class, method_name):
raise ValueError(
f"Method {method_name} not found in class {class_name}"
)
# Get the method
method = getattr(_class, method_name)
# Get the docstring
docs = parse(method.__doc__)
# Get the signature of the method
sig = inspect.signature(method)
# Get the parameters of the method
params = sig.parameters
# Initialize the variables dictionary with method parameters
variables = {
"_type": _type,
**{
name: {
"default": param.default
if param.default != param.empty
else None,
"type": param.annotation
if param.annotation != param.empty
else None,
"required": param.default == param.empty,
}
for name, param in params.items()
},
}
base_classes = get_base_classes(_class)
# Adding function to base classes to allow the output to be a function
if add_function:
base_classes.append("function")
return {
"template": format_dict(variables, class_name),
"description": docs.short_description or "",
"base_classes": base_classes,
}
def get_base_classes(cls): def get_base_classes(cls):
"""Get the base classes of a class. """Get the base classes of a class.
These are used to determine the output of the nodes. These are used to determine the output of the nodes.

View file

@ -38,7 +38,8 @@
"dev:docker": "vite --host 0.0.0.0", "dev:docker": "vite --host 0.0.0.0",
"start": "vite", "start": "vite",
"build": "vite build", "build": "vite build",
"serve": "vite preview" "serve": "vite preview",
"format": "npx prettier --write \"src/**/*.{js,jsx,ts,tsx,json,md}\""
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View file

@ -58,7 +58,6 @@ export default function GenericNode({
if (response.status === 200) { if (response.status === 200) {
let jsonResponse = await response.json(); let jsonResponse = await response.json();
let jsonResponseParsed = await JSON.parse(jsonResponse); let jsonResponseParsed = await JSON.parse(jsonResponse);
console.log(jsonResponseParsed);
setValidationStatus(jsonResponseParsed); setValidationStatus(jsonResponseParsed);
} }
} catch (error) { } catch (error) {
@ -86,6 +85,7 @@ export default function GenericNode({
deleteNode(data.id); deleteNode(data.id);
return; return;
} }
console.log(data);
return ( return (
<div <div

View file

@ -34,7 +34,10 @@ export default function SingleAlert({
key={dropItem.id} key={dropItem.id}
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400 dark:text-red-50" aria-hidden="true" /> <XCircleIcon
className="h-5 w-5 text-red-400 dark:text-red-50"
aria-hidden="true"
/>
</div> </div>
<div className="ml-3"> <div className="ml-3">
<h3 className="text-sm break-words font-medium text-red-800 dark:text-white/80"> <h3 className="text-sm break-words font-medium text-red-800 dark:text-white/80">
@ -44,7 +47,9 @@ export default function SingleAlert({
<div className="mt-2 text-sm text-red-700 dark:text-red-50"> <div className="mt-2 text-sm text-red-700 dark:text-red-50">
<ul className="list-disc space-y-1 pl-5"> <ul className="list-disc space-y-1 pl-5">
{dropItem.list.map((item, idx) => ( {dropItem.list.map((item, idx) => (
<li className="break-words" key={idx}>{item}</li> <li className="break-words" key={idx}>
{item}
</li>
))} ))}
</ul> </ul>
</div> </div>
@ -82,7 +87,9 @@ export default function SingleAlert({
/> />
</div> </div>
<div className="ml-3 flex-1 md:flex md:justify-between"> <div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-blue-700 dark:text-white/80">{dropItem.title}</p> <p className="text-sm text-blue-700 dark:text-white/80">
{dropItem.title}
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6"> <p className="mt-3 text-sm md:mt-0 md:ml-6">
{dropItem.link ? ( {dropItem.link ? (
<Link <Link

View file

@ -43,10 +43,15 @@ export default function ErrorAlert({
> >
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400 dark:text-red-50" aria-hidden="true" /> <XCircleIcon
className="h-5 w-5 text-red-400 dark:text-red-50"
aria-hidden="true"
/>
</div> </div>
<div className="ml-3"> <div className="ml-3">
<h3 className="text-sm font-medium text-red-800 dark:text-white/80">{title}</h3> <h3 className="text-sm font-medium text-red-800 dark:text-white/80">
{title}
</h3>
{list.length !== 0 ? ( {list.length !== 0 ? (
<div className="mt-2 text-sm text-red-700 dark:text-red-50"> <div className="mt-2 text-sm text-red-700 dark:text-red-50">
<ul className="list-disc space-y-1 pl-5"> <ul className="list-disc space-y-1 pl-5">

View file

@ -44,7 +44,9 @@ export default function SuccessAlert({
/> />
</div> </div>
<div className="ml-3"> <div className="ml-3">
<p className="text-sm font-medium text-green-800 dark:text-white/80">{title}</p> <p className="text-sm font-medium text-green-800 dark:text-white/80">
{title}
</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -14,7 +14,6 @@ export default function FloatComponent({
onChange(""); onChange("");
} }
}, [disabled, onChange]); }, [disabled, onChange]);
const {setDisableCopyPaste} = useContext(TabsContext)
return ( return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}> <div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<input <input
@ -29,12 +28,6 @@ export default function FloatComponent({
setMyValue(e.target.value); setMyValue(e.target.value);
onChange(e.target.value); onChange(e.target.value);
}} }}
onBlur={() => {
setDisableCopyPaste(false)
}}
onFocus={() => {
setDisableCopyPaste(true)
}}
/> />
</div> </div>
); );

View file

@ -11,7 +11,6 @@ export default function InputComponent({
}: InputComponentType) { }: InputComponentType) {
const [myValue, setMyValue] = useState(value ?? ""); const [myValue, setMyValue] = useState(value ?? "");
const [pwdVisible, setPwdVisible] = useState(false); const [pwdVisible, setPwdVisible] = useState(false);
const {setDisableCopyPaste} = useContext(TabsContext)
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
setMyValue(""); setMyValue("");
@ -28,12 +27,6 @@ export default function InputComponent({
> >
<input <input
value={myValue} value={myValue}
onBlur={() => {
setDisableCopyPaste(false)
}}
onFocus={() => {
setDisableCopyPaste(true)
}}
className={classNames( className={classNames(
"block w-full pr-12 form-input dark:bg-gray-900 dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm", "block w-full pr-12 form-input dark:bg-gray-900 dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
disabled ? " bg-gray-200 dark:bg-gray-700" : "", disabled ? " bg-gray-200 dark:bg-gray-700" : "",

View file

@ -17,7 +17,6 @@ export default function InputListComponent({
onChange([""]); onChange([""]);
} }
}, [disabled, onChange]); }, [disabled, onChange]);
const {setDisableCopyPaste} = useContext(TabsContext)
return ( return (
<div <div
className={ className={
@ -43,12 +42,6 @@ export default function InputListComponent({
}); });
onChange(inputList); onChange(inputList);
}} }}
onBlur={() => {
setDisableCopyPaste(false)
}}
onFocus={() => {
setDisableCopyPaste(true)
}}
/> />
{idx === inputList.length - 1 ? ( {idx === inputList.length - 1 ? (
<button <button

View file

@ -14,7 +14,6 @@ export default function IntComponent({
onChange(""); onChange("");
} }
}, [disabled, onChange]); }, [disabled, onChange]);
const {setDisableCopyPaste} =useContext(TabsContext)
return ( return (
<div <div
className={ className={
@ -45,14 +44,7 @@ export default function IntComponent({
setMyValue(e.target.value); setMyValue(e.target.value);
onChange(e.target.value); onChange(e.target.value);
}} }}
onBlur={() => {
setDisableCopyPaste(false)
}}
onFocus={() => {
setDisableCopyPaste(true)
}}
/> />
</div> </div>
); );
} }

View file

@ -4,7 +4,7 @@ import { PopUpContext } from "../../contexts/popUpContext";
import CodeAreaModal from "../../modals/codeAreaModal"; import CodeAreaModal from "../../modals/codeAreaModal";
import TextAreaModal from "../../modals/textAreaModal"; import TextAreaModal from "../../modals/textAreaModal";
import { TextAreaComponentType } from "../../types/components"; import { TextAreaComponentType } from "../../types/components";
import PromptAreaModal from "../../modals/promptModal"; import GenericModal from "../../modals/genericModal";
export default function PromptAreaComponent({ export default function PromptAreaComponent({
value, value,
@ -29,8 +29,10 @@ export default function PromptAreaComponent({
<span <span
onClick={() => { onClick={() => {
openPopUp( openPopUp(
<PromptAreaModal <GenericModal
value={myValue} value={myValue}
buttonText="Check & Save"
modalTitle="Edit Prompt"
setValue={(t: string) => { setValue={(t: string) => {
setMyValue(t); setMyValue(t);
onChange(t); onChange(t);
@ -48,8 +50,10 @@ export default function PromptAreaComponent({
<button <button
onClick={() => { onClick={() => {
openPopUp( openPopUp(
<PromptAreaModal <GenericModal
value={myValue} value={myValue}
buttonText="Check & Save"
modalTitle="Edit Prompt"
setValue={(t: string) => { setValue={(t: string) => {
setMyValue(t); setMyValue(t);
onChange(t); onChange(t);

View file

@ -1,10 +1,14 @@
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import TextAreaModal from "../../modals/textAreaModal";
import { TextAreaComponentType } from "../../types/components"; import { TextAreaComponentType } from "../../types/components";
import GenericModal from "../../modals/genericModal";
export default function TextAreaComponent({ value, onChange, disabled }:TextAreaComponentType) { export default function TextAreaComponent({
value,
onChange,
disabled,
}: TextAreaComponentType) {
const [myValue, setMyValue] = useState(value); const [myValue, setMyValue] = useState(value);
const { openPopUp } = useContext(PopUpContext); const { openPopUp } = useContext(PopUpContext);
useEffect(() => { useEffect(() => {
@ -16,15 +20,42 @@ export default function TextAreaComponent({ value, onChange, disabled }:TextArea
return ( return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}> <div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<div className="w-full flex items-center gap-3"> <div className="w-full flex items-center gap-3">
<span onClick={()=>{openPopUp(<TextAreaModal value={myValue} setValue={(t:string) => {setMyValue(t); onChange(t);}}/>)}} <span
onClick={() => {
openPopUp(
<GenericModal
buttonText="Finishing Editing"
modalTitle="Edit Text"
value={myValue}
setValue={(t: string) => {
setMyValue(t);
onChange(t);
}}
/>
);
}}
className={ className={
"truncate block w-full text-gray-500 dark:text-gray-100 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + "truncate block w-full text-gray-500 dark:text-gray-100 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
(disabled ? " bg-gray-200" : "") (disabled ? " bg-gray-200" : "")
} }
> >
{myValue !== "" ? myValue : 'Text empty'} {myValue !== "" ? myValue : "Text empty"}
</span> </span>
<button onClick={()=>{openPopUp(<TextAreaModal value={myValue} setValue={(t:string) => {setMyValue(t); onChange(t);}}/>)}}> <button
onClick={() => {
openPopUp(
<GenericModal
buttonText="Finishing Editing"
modalTitle="Edit Text"
value={myValue}
setValue={(t: string) => {
setMyValue(t);
onChange(t);
}}
/>
);
}}
>
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600 dark:text-gray-300" /> <ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600 dark:text-gray-300" />
</button> </button>
</div> </div>

View file

@ -21,8 +21,8 @@ export default function ToggleComponent({
setEnabled(x); setEnabled(x);
}} }}
className={classNames( className={classNames(
enabled ? 'bg-indigo-600' : 'bg-gray-200', enabled ? "bg-indigo-600" : "bg-gray-200",
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2' "relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
)} )}
> >
<span className="sr-only">Use setting</span> <span className="sr-only">Use setting</span>
@ -43,8 +43,7 @@ export default function ToggleComponent({
"absolute inset-0 flex h-full w-full items-center justify-center transition-opacity" "absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
)} )}
aria-hidden="true" aria-hidden="true"
> ></span>
</span>
<span <span
className={classNames( className={classNames(
enabled enabled
@ -53,8 +52,7 @@ export default function ToggleComponent({
"absolute inset-0 flex h-full w-full items-center justify-center transition-opacity" "absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
)} )}
aria-hidden="true" aria-hidden="true"
> ></span>
</span>
</span> </span>
</Switch> </Switch>
</div> </div>

View file

@ -14,6 +14,7 @@ import { typesContext } from "./typesContext";
import { APITemplateType, TemplateVariableType } from "../types/api"; import { APITemplateType, TemplateVariableType } from "../types/api";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { addEdge } from "reactflow"; import { addEdge } from "reactflow";
import _ from "lodash";
const TabsContextInitialValue: TabsContextType = { const TabsContextInitialValue: TabsContextType = {
save: () => {}, save: () => {},
@ -27,10 +28,13 @@ const TabsContextInitialValue: TabsContextType = {
downloadFlow: (flow: FlowType) => {}, downloadFlow: (flow: FlowType) => {},
uploadFlow: () => {}, uploadFlow: () => {},
hardReset: () => {}, hardReset: () => {},
disableCopyPaste:false, disableCopyPaste: false,
setDisableCopyPaste:(state:boolean)=>{}, setDisableCopyPaste: (state: boolean) => {},
getNodeId: () => "", getNodeId: () => "",
paste: (selection: {nodes: any, edges: any}, position: {x: number, y: number}) => {}, paste: (
selection: { nodes: any; edges: any },
position: { x: number; y: number }
) => {},
}; };
export const TabsContext = createContext<TabsContextType>( export const TabsContext = createContext<TabsContextType>(
@ -50,30 +54,30 @@ export function TabsProvider({ children }: { children: ReactNode }) {
return newNodeId.current; return newNodeId.current;
} }
function save() { function save() {
let Saveflows = [...flows]; // added clone deep to avoid mutating the original object
if (Saveflows.length !== 0) let Saveflows = _.cloneDeep(flows);
if (Saveflows.length !== 0) {
Saveflows.forEach((flow) => { Saveflows.forEach((flow) => {
if(flow.data && flow.data?.nodes) flow.data?.nodes.forEach((node) => { if (flow.data && flow.data?.nodes)
console.log(node.data.type) flow.data?.nodes.forEach((node) => {
console.log(node.data.type);
//looking for file fields to prevent saving the content and breaking the flow for exceeding the the data limite for local storage
Object.keys(node.data.node.template).forEach((key) => { Object.keys(node.data.node.template).forEach((key) => {
console.log(node.data.node.template[key].type) console.log(node.data.node.template[key].type);
if(node.data.node.template[key].type==="file"){ if (node.data.node.template[key].type === "file") {
console.log(node.data.node.template[key]) console.log(node.data.node.template[key]);
node.data.node.template[key].content = ""; node.data.node.template[key].content = null;
node.data.node.template[key].value = "";
} }
}) });
}) });
}) });
window.localStorage.setItem( window.localStorage.setItem(
"tabsData", "tabsData",
JSON.stringify({ tabIndex, flows:Saveflows, id}) JSON.stringify({ tabIndex, flows: Saveflows, id })
); );
} }
useEffect(() => { }
//save tabs locally
// console.log(id)
save();
}, [flows, id, tabIndex, newNodeId]);
useEffect(() => { useEffect(() => {
//get tabs locally saved //get tabs locally saved
@ -99,6 +103,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
} }
}, [templates]); }, [templates]);
useEffect(() => {
//save tabs locally
console.log(id);
save();
}, [flows, id, tabIndex, newNodeId]);
function hardReset() { function hardReset() {
newNodeId.current = uuidv4(); newNodeId.current = uuidv4();
setTabIndex(0); setTabIndex(0);
@ -185,9 +195,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* @param flow Optional flow to add. * @param flow Optional flow to add.
*/ */
function paste(selectionInstance, position){ function paste(selectionInstance, position) {
console.log(position);
console.log(selectionInstance)
let minimumX = Infinity; let minimumX = Infinity;
let minimumY = Infinity; let minimumY = Infinity;
let idsMap = {}; let idsMap = {};
@ -226,8 +234,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// Add the new node to the list of nodes in state // Add the new node to the list of nodes in state
nodes = nodes nodes = nodes
.map((e) => ({ ...e, selected: false })) .map((e) => ({ ...e, selected: false }))
.concat({ ...newNode, selected: false }) .concat({ ...newNode, selected: false });
console.log(nodes);
}); });
reactFlowInstance.setNodes(nodes); reactFlowInstance.setNodes(nodes);
@ -263,10 +270,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}, },
edges.map((e) => ({ ...e, selected: false })) edges.map((e) => ({ ...e, selected: false }))
); );
console.log(edges);
}); });
reactFlowInstance.setEdges(edges); reactFlowInstance.setEdges(edges);
}; }
function addFlow(flow?: FlowType) { function addFlow(flow?: FlowType) {
// Get data from the flow or set it to null if there's no flow provided. // Get data from the flow or set it to null if there's no flow provided.

View file

@ -5,7 +5,7 @@ import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import ContextWrapper from "./contexts"; import ContextWrapper from "./contexts";
import './index.css'; import "./index.css";
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement document.getElementById("root") as HTMLElement

View file

@ -1,6 +1,10 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { IconCheck, IconClipboard, IconDownload } from '@tabler/icons-react'; import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react";
import { XMarkIcon, CommandLineIcon, CodeBracketSquareIcon } from "@heroicons/react/24/outline"; import {
XMarkIcon,
CommandLineIcon,
CodeBracketSquareIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useContext, useRef, useState } from "react"; import { Fragment, useContext, useRef, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext"; import { PopUpContext } from "../../contexts/popUpContext";
import "ace-builds/src-noconflict/mode-python"; import "ace-builds/src-noconflict/mode-python";
@ -9,10 +13,8 @@ import "ace-builds/src-noconflict/theme-twilight";
import "ace-builds/src-noconflict/ext-language_tools"; import "ace-builds/src-noconflict/ext-language_tools";
// import "ace-builds/webpack-resolver"; // import "ace-builds/webpack-resolver";
import { darkContext } from "../../contexts/darkContext"; import { darkContext } from "../../contexts/darkContext";
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
export default function ApiModal({ flowName }) { export default function ApiModal({ flowName }) {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
@ -68,7 +70,8 @@ flow("Hey, have you heard of LangFlow?")`;
{ {
name: "Python API", name: "Python API",
mode: "python", mode: "python",
image: "https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w", image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
code: pythonApiCode, code: pythonApiCode,
}, },
{ {
@ -77,8 +80,7 @@ flow("Hey, have you heard of LangFlow?")`;
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png", image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
code: pythonCode, code: pythonCode,
}, },
];
]
return ( return (
<Transition.Root show={open} appear={true} as={Fragment}> <Transition.Root show={open} appear={true} as={Fragment}>
<Dialog <Dialog
@ -144,9 +146,17 @@ flow("Hey, have you heard of LangFlow?")`;
<div className="flex flex-col h-full w-full "> <div className="flex flex-col h-full w-full ">
<div className="flex px-5 z-10"> <div className="flex px-5 z-10">
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (
<button onClick={() => { <button
onClick={() => {
setActiveTab(index); setActiveTab(index);
}} className={"p-2 rounded-t-lg w-44 border border-b-0 border-gray-300 dark:border-gray-700 dark:text-gray-300 -mr-px flex justify-center items-center gap-4 " + (activeTab === index ? " bg-white dark:bg-gray-800" : "bg-gray-100 dark:bg-gray-900")}> }}
className={
"p-2 rounded-t-lg w-44 border border-b-0 border-gray-300 dark:border-gray-700 dark:text-gray-300 -mr-px flex justify-center items-center gap-4 " +
(activeTab === index
? " bg-white dark:bg-gray-800"
: "bg-gray-100 dark:bg-gray-900")
}
>
{tab.name} {tab.name}
<img src={tab.image} className="w-6" /> <img src={tab.image} className="w-6" />
</button> </button>
@ -161,8 +171,12 @@ flow("Hey, have you heard of LangFlow?")`;
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300" className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
onClick={copyToClipboard} onClick={copyToClipboard}
> >
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />} {isCopied ? (
{isCopied ? 'Copied!' : 'Copy code'} <IconCheck size={18} />
) : (
<IconClipboard size={18} />
)}
{isCopied ? "Copied!" : "Copy code"}
</button> </button>
</div> </div>
<SyntaxHighlighter <SyntaxHighlighter

View file

@ -17,8 +17,6 @@ export default function ChatInput({
} }
}, [chatValue]); }, [chatValue]);
const { setDisableCopyPaste } = useContext(TabsContext);
return ( return (
<div className="relative"> <div className="relative">
<textarea <textarea
@ -27,12 +25,6 @@ export default function ChatInput({
sendMessage(); sendMessage();
} }
}} }}
onBlur={() => {
setDisableCopyPaste(false)
}}
onFocus={() => {
setDisableCopyPaste(true)
}}
rows={1} rows={1}
ref={inputRef} ref={inputRef}
disabled={lockChat} disabled={lockChat}

View file

@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
import { ChatMessageType } from "../../../types/chat"; import { ChatMessageType } from "../../../types/chat";
import { classNames } from "../../../utils"; import { classNames } from "../../../utils";
import AiIcon from "../../../assets/Gooey Ring-5s-271px.svg"; import AiIcon from "../../../assets/Gooey Ring-5s-271px.svg";
import AiIconStill from "../../../assets/froze-flow.png" import AiIconStill from "../../../assets/froze-flow.png";
import { UserIcon } from "@heroicons/react/24/solid"; import { UserIcon } from "@heroicons/react/24/solid";
import FileCard from "../fileComponent"; import FileCard from "../fileComponent";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
@ -13,7 +13,13 @@ import remarkMath from "remark-math";
import { CodeBlock } from "./codeBlock"; import { CodeBlock } from "./codeBlock";
import Convert from "ansi-to-html"; import Convert from "ansi-to-html";
export default function ChatMessage({ chat, lockChat }: { chat: ChatMessageType, lockChat: boolean }) { export default function ChatMessage({
chat,
lockChat,
}: {
chat: ChatMessageType;
lockChat: boolean;
}) {
const convert = new Convert({ newline: true }); const convert = new Convert({ newline: true });
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const imgRef = useRef(null); const imgRef = useRef(null);
@ -35,11 +41,27 @@ export default function ChatMessage({ chat, lockChat }: { chat: ChatMessageType,
"rounded-full overflow-hidden w-8 h-8 flex items-center my-3 justify-center" "rounded-full overflow-hidden w-8 h-8 flex items-center my-3 justify-center"
)} )}
> >
{!chat.isSend && <div className="relative w-8 h-8"> {!chat.isSend && (
<img className={"absolute transition-opacity duration-500 scale-150 " + (lockChat ? "opacity-100" : "opacity-0")} src={AiIcon} /> <div className="relative w-8 h-8">
<img className={"absolute transition-opacity duration-500 scale-150 " + (lockChat ? "opacity-0" : "opacity-100")} src={AiIconStill} /> <img
</div>} className={
{chat.isSend && <UserIcon className="w-6 h-6 -mb-1 text-gray-800 dark:text-gray-200" />} "absolute transition-opacity duration-500 scale-150 " +
(lockChat ? "opacity-100" : "opacity-0")
}
src={AiIcon}
/>
<img
className={
"absolute transition-opacity duration-500 scale-150 " +
(lockChat ? "opacity-0" : "opacity-100")
}
src={AiIconStill}
/>
</div>
)}
{chat.isSend && (
<UserIcon className="w-6 h-6 -mb-1 text-gray-800 dark:text-gray-200" />
)}
</div> </div>
{!chat.isSend ? ( {!chat.isSend ? (
<div className="w-full text-start flex items-center"> <div className="w-full text-start flex items-center">

View file

@ -15,7 +15,7 @@ import { sendAllProps } from "../../types/api";
import { ChatMessageType, ChatType } from "../../types/chat"; import { ChatMessageType, ChatType } from "../../types/chat";
import ChatInput from "./chatInput"; import ChatInput from "./chatInput";
import _ from "lodash"; import _, { set } from "lodash";
export default function ChatModal({ export default function ChatModal({
flow, flow,
@ -100,9 +100,9 @@ export default function ChatModal({
function handleOnClose(event: CloseEvent) { function handleOnClose(event: CloseEvent) {
if (isOpen.current) { if (isOpen.current) {
setErrorData({ title: event.reason }); setErrorData({ title: event.reason });
setLockChat(false);
setTimeout(() => { setTimeout(() => {
connectWS(); connectWS();
setLockChat(false);
}, 1000); }, 1000);
} }
} }
@ -183,7 +183,6 @@ export default function ChatModal({
newWs.onopen = () => { newWs.onopen = () => {
console.log("WebSocket connection established!"); console.log("WebSocket connection established!");
}; };
console.log(flow.id);
newWs.onmessage = (event) => { newWs.onmessage = (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
console.log("Received data:", data); console.log("Received data:", data);
@ -236,6 +235,16 @@ export default function ChatModal({
}; };
}, []); }, []);
useEffect(() => {
if (
ws.current.readyState === ws.current.CLOSED ||
ws.current.readyState === ws.current.CLOSING
) {
connectWS();
setLockChat(false);
}
}, [lockChat]);
async function sendAll(data: sendAllProps) { async function sendAll(data: sendAllProps) {
try { try {
if (ws) { if (ws) {
@ -340,6 +349,7 @@ export default function ChatModal({
function clearChat() { function clearChat() {
setChatHistory([]); setChatHistory([]);
ws.current.send(JSON.stringify({ clear_history: true })); ws.current.send(JSON.stringify({ clear_history: true }));
if (lockChat) setLockChat(false);
} }
function setModalOpen(x: boolean) { function setModalOpen(x: boolean) {

View file

@ -23,7 +23,6 @@ export default function CodeAreaModal({
const [code, setCode] = useState(value); const [code, setCode] = useState(value);
const { dark } = useContext(darkContext); const { dark } = useContext(darkContext);
const { setErrorData, setSuccessData } = useContext(alertContext); const { setErrorData, setSuccessData } = useContext(alertContext);
const { setDisableCopyPaste } = useContext(TabsContext);
const { closePopUp } = useContext(PopUpContext); const { closePopUp } = useContext(PopUpContext);
const ref = useRef(); const ref = useRef();
function setModalOpen(x: boolean) { function setModalOpen(x: boolean) {
@ -111,12 +110,6 @@ export default function CodeAreaModal({
onChange={(value) => { onChange={(value) => {
setCode(value); setCode(value);
}} }}
onBlur={() => {
setDisableCopyPaste(false)
}}
onFocus={() => {
setDisableCopyPaste(true)
}}
className="h-full w-full rounded-lg" className="h-full w-full rounded-lg"
/> />
</div> </div>
@ -129,9 +122,7 @@ export default function CodeAreaModal({
onClick={() => { onClick={() => {
checkCode(code) checkCode(code)
.then((apiReturn) => { .then((apiReturn) => {
console.log(apiReturn);
if (apiReturn.data) { if (apiReturn.data) {
console.log(apiReturn.data);
let importsErrors = apiReturn.data.imports.errors; let importsErrors = apiReturn.data.imports.errors;
let funcErrors = apiReturn.data.function.errors; let funcErrors = apiReturn.data.function.errors;
if ( if (

View file

@ -16,7 +16,7 @@ export default function ExportModal() {
const { closePopUp } = useContext(PopUpContext); const { closePopUp } = useContext(PopUpContext);
const ref = useRef(); const ref = useRef();
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const { flows, tabIndex, updateFlow, downloadFlow,setDisableCopyPaste } = useContext(TabsContext); const { flows, tabIndex, updateFlow, downloadFlow } = useContext(TabsContext);
function setModalOpen(x: boolean) { function setModalOpen(x: boolean) {
setOpen(x); setOpen(x);
if (x === false) { if (x === false) {
@ -113,12 +113,6 @@ export default function ExportModal() {
placeholder="File name" placeholder="File name"
id="name" id="name"
className="focus:border focus:border-blue block w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500 text-gray-900 dark:text-gray-100" className="focus:border focus:border-blue block w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500 text-gray-900 dark:text-gray-100"
onBlur={() => {
setDisableCopyPaste(false);
}}
onFocus={() => {
setDisableCopyPaste(true);
}}
/> />
</div> </div>
<div className="w-full"> <div className="w-full">
@ -133,12 +127,6 @@ export default function ExportModal() {
</span> </span>
</label> </label>
<textarea <textarea
onBlur={() => {
setDisableCopyPaste(false);
}}
onFocus={() => {
setDisableCopyPaste(true);
}}
name="description" name="description"
id="description" id="description"
onChange={(event) => { onChange={(event) => {
@ -163,12 +151,6 @@ export default function ExportModal() {
id="checkbox" id="checkbox"
type="checkbox" type="checkbox"
className="h-4 w-4 text-blue-600 border-gray-300 rounded dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500" className="h-4 w-4 text-blue-600 border-gray-300 rounded dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500"
onBlur={() => {
setDisableCopyPaste(false);
}}
onFocus={() => {
setDisableCopyPaste(true);
}}
/> />
<span className="ml-2 font-medium text-gray-700 dark:text-white"> <span className="ml-2 font-medium text-gray-700 dark:text-white">
Save with my API keys Save with my API keys

View file

@ -0,0 +1,160 @@
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon, DocumentTextIcon } from "@heroicons/react/24/outline";
import { Fragment, useContext, useRef, useState } from "react";
import { PopUpContext } from "../../contexts/popUpContext";
import { darkContext } from "../../contexts/darkContext";
import { checkPrompt } from "../../controllers/API";
import { alertContext } from "../../contexts/alertContext";
export default function PromptAreaModal({
value,
setValue,
buttonText,
modalTitle,
}: {
setValue: (value: string) => void;
value: string;
buttonText: string;
modalTitle: string;
}) {
const [myButtonText, setmyButtonText] = useState(buttonText);
const [myModalTitle, setMyModalTitle] = useState(modalTitle);
const [open, setOpen] = useState(true);
const [myValue, setMyValue] = useState(value);
const { dark } = useContext(darkContext);
const { setErrorData, setSuccessData } = useContext(alertContext);
const { closePopUp } = useContext(PopUpContext);
const ref = useRef();
function setModalOpen(x: boolean) {
setOpen(x);
if (x === false) {
setTimeout(() => {
closePopUp();
}, 300);
}
}
return (
<Transition.Root show={open} appear={true} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
onClose={setModalOpen}
initialFocus={ref}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative flex flex-col justify-between transform h-[600px] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[700px]">
<div className=" z-50 absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="rounded-md text-gray-400 hover:text-gray-500"
onClick={() => {
setModalOpen(false);
}}
>
<span className="sr-only">Close</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
<div className="h-full w-full flex flex-col justify-center items-center">
<div className="flex w-full pb-4 z-10 justify-center shadow-sm">
<div className="mx-auto mt-4 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-gray-900 sm:mx-0 sm:h-10 sm:w-10">
<DocumentTextIcon
className="h-6 w-6 text-blue-600"
aria-hidden="true"
/>
</div>
<div className="mt-4 text-center sm:ml-4 sm:text-left">
<Dialog.Title
as="h3"
className="text-lg font-medium dark:text-white leading-10 text-gray-900"
>
{myModalTitle}
</Dialog.Title>
</div>
</div>
<div className="h-full w-full bg-gray-200 overflow-auto dark:bg-gray-900 p-4 gap-4 flex flex-row justify-center items-center">
<div className="flex h-full w-full">
<div className="overflow-hidden px-4 py-5 sm:p-6 w-full h-full rounded-lg bg-white dark:bg-gray-800 shadow">
<textarea
ref={ref}
className="form-input h-full w-full rounded-lg border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white"
value={myValue}
onChange={(e) => {
setMyValue(e.target.value);
setValue(e.target.value);
}}
/>
</div>
</div>
</div>
<div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4">
<button
type="button"
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
checkPrompt(myValue)
.then((apiReturn) => {
if (apiReturn.data) {
let inputVariables =
apiReturn.data.input_variables;
if (inputVariables.length === 0) {
setErrorData({
title:
"The template you are attempting to use does not contain any variables for data entry.",
});
} else {
setSuccessData({
title: "Prompt is ready",
});
setModalOpen(false);
setValue(myValue);
}
} else {
setErrorData({
title: "Something went wrong, please try again",
});
}
})
.catch((error) => {
return setErrorData({
title:
"There is something wrong with this prompt, please review it",
list: [error.response.data.detail],
});
});
}}
>
{myButtonText}
</button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}

View file

@ -75,7 +75,6 @@ export default function ButtonBox({
break; break;
} }
return ( return (
<button disabled={deactivate} onClick={onClick}> <button disabled={deactivate} onClick={onClick}>
<div <div

View file

@ -109,7 +109,6 @@ export default function PromptAreaModal({
onClick={() => { onClick={() => {
checkPrompt(myValue) checkPrompt(myValue)
.then((apiReturn) => { .then((apiReturn) => {
console.log(apiReturn);
if (apiReturn.data) { if (apiReturn.data) {
let inputVariables = let inputVariables =
apiReturn.data.input_variables; apiReturn.data.input_variables;

View file

@ -14,7 +14,7 @@ export default function TabComponent({
selected: boolean; selected: boolean;
onClick: () => void; onClick: () => void;
}) { }) {
const { removeFlow, updateFlow, flows, setDisableCopyPaste } = useContext(TabsContext); const { removeFlow, updateFlow, flows } = useContext(TabsContext);
const [isRename, setIsRename] = useState(false); const [isRename, setIsRename] = useState(false);
const [value, setValue] = useState(""); const [value, setValue] = useState("");
return ( return (
@ -40,14 +40,10 @@ export default function TabComponent({
<div className="bg-white dark:text-white dark:bg-gray-700/60 flex select-none justify-between w-44 items-center border border-b-0 border-gray-300 dark:border-gray-600 px-4 py-1 rounded-t-xl -ml-px"> <div className="bg-white dark:text-white dark:bg-gray-700/60 flex select-none justify-between w-44 items-center border border-b-0 border-gray-300 dark:border-gray-600 px-4 py-1 rounded-t-xl -ml-px">
{isRename ? ( {isRename ? (
<input <input
onFocus={() => {
setDisableCopyPaste(true);
}}
autoFocus autoFocus
className="bg-transparent focus:border-none active:outline hover:outline focus:outline outline-gray-300 rounded-md w-28" className="bg-transparent focus:border-none active:outline hover:outline focus:outline outline-gray-300 rounded-md w-28"
onBlur={() => { onBlur={() => {
setIsRename(false); setIsRename(false);
setDisableCopyPaste(false);
if (value !== "") { if (value !== "") {
let newFlow = _.cloneDeep(flow); let newFlow = _.cloneDeep(flow);
newFlow.name = value; newFlow.name = value;

View file

@ -58,7 +58,9 @@ export default function TabsManagerComponent() {
/> />
<div className="ml-auto mr-2 flex gap-3"> <div className="ml-auto mr-2 flex gap-3">
<button <button
onClick={() => openPopUp(<ApiModal flowName={flows[tabIndex].name} />)} onClick={() =>
openPopUp(<ApiModal flowName={flows[tabIndex].name} />)
}
className="flex items-center gap-1 pr-2 border-gray-400 border-r text-sm text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200" className="flex items-center gap-1 pr-2 border-gray-400 border-r text-sm text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200"
> >
Code <CodeBracketSquareIcon className="w-5 h-5" /> Code <CodeBracketSquareIcon className="w-5 h-5" />

View file

@ -1,5 +1,7 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useContext, useEffect, useState } from "react";
import { Edge, Node, useReactFlow } from 'reactflow'; import { Edge, Node, useReactFlow } from "reactflow";
import { TabsContext } from "../../../contexts/tabsContext";
import { cloneDeep } from "lodash";
type UseUndoRedoOptions = { type UseUndoRedoOptions = {
maxHistorySize: number; maxHistorySize: number;
@ -30,47 +32,112 @@ export const useUndoRedo: UseUndoRedo = ({
enableShortcuts = defaultOptions.enableShortcuts, enableShortcuts = defaultOptions.enableShortcuts,
} = defaultOptions) => { } = defaultOptions) => {
// the past and future arrays store the states that we can jump to // the past and future arrays store the states that we can jump to
const [past, setPast] = useState<HistoryItem[]>([]); const { tabIndex, flows } = useContext(TabsContext);
const [future, setFuture] = useState<HistoryItem[]>([]);
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));
useEffect(() => {
// whenever the flows variable changes, we need to add one array to the past and future states
setPast((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
setFuture((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
}, [flows]);
const { setNodes, setEdges, getNodes, getEdges } = useReactFlow(); const { setNodes, setEdges, getNodes, getEdges } = useReactFlow();
const takeSnapshot = useCallback(() => { const takeSnapshot = useCallback(() => {
// push the current graph to the past state // push the current graph to the past state
setPast((past) => [ setPast((old) => {
...past.slice(past.length - maxHistorySize + 1, past.length), let newPast = cloneDeep(old);
{ nodes: getNodes(), edges: getEdges() }, newPast[tabIndex] = old[tabIndex].slice(
]); old[tabIndex].length - maxHistorySize + 1,
old[tabIndex].length
);
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
return newPast;
});
// whenever we take a new snapshot, the redo operations need to be cleared to avoid state mismatches // whenever we take a new snapshot, the redo operations need to be cleared to avoid state mismatches
setFuture([]); setFuture((old) => {
}, [getNodes, getEdges, maxHistorySize]); let newFuture = cloneDeep(old);
newFuture[tabIndex] = [];
return newFuture;
});
}, [
getNodes,
getEdges,
past,
future,
tabIndex,
setPast,
setFuture,
maxHistorySize,
]);
const undo = useCallback(() => { const undo = useCallback(() => {
// get the last state that we want to go back to // get the last state that we want to go back to
const pastState = past[past.length - 1]; const pastState = past[tabIndex][past[tabIndex].length - 1];
if (pastState) { if (pastState) {
// first we remove the state from the history // first we remove the state from the history
setPast((past) => past.slice(0, past.length - 1)); setPast((old) => {
let newPast = cloneDeep(old);
newPast[tabIndex] = old[tabIndex].slice(0, old[tabIndex].length - 1);
return newPast;
});
// we store the current graph for the redo operation // we store the current graph for the redo operation
setFuture((future) => [...future, { nodes: getNodes(), edges: getEdges() }]); setFuture((old) => {
let newFuture = cloneDeep(old);
newFuture[tabIndex] = old[tabIndex];
newFuture[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
return newFuture;
});
// now we can set the graph to the past state // now we can set the graph to the past state
setNodes(pastState.nodes); setNodes(pastState.nodes);
setEdges(pastState.edges); setEdges(pastState.edges);
} }
}, [setNodes, setEdges, getNodes, getEdges, past]); }, [
setNodes,
setEdges,
getNodes,
getEdges,
future,
past,
setFuture,
setPast,
tabIndex,
]);
const redo = useCallback(() => { const redo = useCallback(() => {
const futureState = future[future.length - 1]; const futureState = future[tabIndex][future[tabIndex].length - 1];
if (futureState) { if (futureState) {
setFuture((future) => future.slice(0, future.length - 1)); setFuture((old) => {
setPast((past) => [...past, { nodes: getNodes(), edges: getEdges() }]); let newFuture = cloneDeep(old);
newFuture[tabIndex] = old[tabIndex].slice(0, old[tabIndex].length - 1);
return newFuture;
});
setPast((old) => {
let newPast = cloneDeep(old);
newPast[tabIndex] = old[tabIndex];
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
return newPast;
});
setNodes(futureState.nodes); setNodes(futureState.nodes);
setEdges(futureState.edges); setEdges(futureState.edges);
} }
}, [setNodes, setEdges, getNodes, getEdges, future]); }, [
future,
past,
setFuture,
setPast,
setNodes,
setEdges,
getNodes,
getEdges,
future,
tabIndex,
]);
useEffect(() => { useEffect(() => {
// this effect is used to attach the global event handlers // this effect is used to attach the global event handlers
@ -79,21 +146,24 @@ export const useUndoRedo: UseUndoRedo = ({
} }
const keyDownHandler = (event: KeyboardEvent) => { const keyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'z' && (event.ctrlKey || event.metaKey) && event.shiftKey) { if (
event.key === "z" &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey
) {
redo(); redo();
} } else if (event.key === "y" && (event.ctrlKey || event.metaKey)) {
else if (event.key === 'y' && (event.ctrlKey || event.metaKey)) {
event.preventDefault(); // prevent the default action event.preventDefault(); // prevent the default action
redo(); redo();
} else if (event.key === 'z' && (event.ctrlKey || event.metaKey)) { } else if (event.key === "z" && (event.ctrlKey || event.metaKey)) {
undo(); undo();
} }
}; };
document.addEventListener('keydown', keyDownHandler); document.addEventListener("keydown", keyDownHandler);
return () => { return () => {
document.removeEventListener('keydown', keyDownHandler); document.removeEventListener("keydown", keyDownHandler);
}; };
}, [undo, redo, enableShortcuts]); }, [undo, redo, enableShortcuts]);

View file

@ -56,7 +56,6 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
// this effect is used to attach the global event handlers // this effect is used to attach the global event handlers
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
console.log("keydownou", lastCopiedSelection, position);
if ( if (
(event.ctrlKey || event.metaKey) && (event.ctrlKey || event.metaKey) &&
event.key === "c" && event.key === "c" &&
@ -156,10 +155,6 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
params.targetHandle.split("|")[0] === "Text" params.targetHandle.split("|")[0] === "Text"
? { stroke: "#333333", strokeWidth: 2 } ? { stroke: "#333333", strokeWidth: 2 }
: { stroke: "#222222" }, : { stroke: "#222222" },
className:
params.targetHandle.split("|")[0] === "Text"
? ""
: "animate-pulse",
animated: params.targetHandle.split("|")[0] === "Text", animated: params.targetHandle.split("|")[0] === "Text",
}, },
eds eds
@ -307,6 +302,8 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
setLastSelection(flow); setLastSelection(flow);
}, []); }, []);
const { setDisableCopyPaste } = useContext(TabsContext);
return ( return (
<div className="w-full h-full" ref={reactFlowWrapper}> <div className="w-full h-full" ref={reactFlowWrapper}>
{Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? ( {Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? (
@ -317,6 +314,15 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
updateFlow({ ...flow, data: reactFlowInstance.toObject() }); updateFlow({ ...flow, data: reactFlowInstance.toObject() });
}} }}
edges={edges} edges={edges}
onPaneClick={() => {
setDisableCopyPaste(false);
}}
onNodeClick={() => {
setDisableCopyPaste(true);
}}
onPaneMouseLeave={() => {
setDisableCopyPaste(true);
}}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChangeMod} onEdgesChange={onEdgesChangeMod}
onConnect={onConnect} onConnect={onConnect}

View file

@ -1,4 +1,9 @@
import { ForwardRefExoticComponent, ReactElement, ReactFragment, ReactNode } from "react"; import {
ForwardRefExoticComponent,
ReactElement,
ReactFragment,
ReactNode,
} from "react";
import { NodeDataType } from "../flow/index"; import { NodeDataType } from "../flow/index";
export type InputComponentType = { export type InputComponentType = {
value: string; value: string;

View file

@ -6,17 +6,20 @@ export type TabsContextType = {
setTabIndex: (index: number) => void; setTabIndex: (index: number) => void;
flows: Array<FlowType>; flows: Array<FlowType>;
removeFlow: (id: string) => void; removeFlow: (id: string) => void;
addFlow: (flowData?: FlowType,newFlow?:boolean) => void; addFlow: (flowData?: FlowType, newFlow?: boolean) => void;
updateFlow: (newFlow: FlowType) => void; updateFlow: (newFlow: FlowType) => void;
incrementNodeId: () => string; incrementNodeId: () => string;
downloadFlow: (flow: FlowType) => void; downloadFlow: (flow: FlowType) => void;
uploadFlow: (newFlow?:boolean) => void; uploadFlow: (newFlow?: boolean) => void;
hardReset: () => void; hardReset: () => void;
//disable CopyPaste //disable CopyPaste
disableCopyPaste: boolean; disableCopyPaste: boolean;
setDisableCopyPaste: (value: boolean) => void; setDisableCopyPaste: (value: boolean) => void;
getNodeId: () => string; getNodeId: () => string;
paste: (selection: {nodes: any, edges: any}, position: {x: number, y: number}) => void; paste: (
selection: { nodes: any; edges: any },
position: { x: number; y: number }
) => void;
}; };
export type LangFlowState = { export type LangFlowState = {

View file

@ -317,11 +317,9 @@ export function toFirstUpperCase(str: string) {
} }
export function snakeToSpaces(str: string) { export function snakeToSpaces(str: string) {
let result = str let result = str.split("_").join(" ");
.split("_")
.join(" ");
return result return result;
} }
export function toNormalCase(str: string) { export function toNormalCase(str: string) {

View file

@ -1,5 +1,7 @@
import pytest import pytest
from langflow.template.base import FrontendNode, Template, TemplateField from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
from langflow.template.template.base import Template
@pytest.fixture @pytest.fixture