Merge remote-tracking branch 'origin/dev' into multipart_endpoint
This commit is contained in:
commit
0dd16ee794
202 changed files with 21559 additions and 12347 deletions
6
.githooks/pre-commit
Executable file → Normal file
6
.githooks/pre-commit
Executable file → Normal file
|
|
@ -1,2 +1,6 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
make format
|
added_files=$(git diff --name-only --cached --diff-filter=d)
|
||||||
|
|
||||||
|
make format
|
||||||
|
git add ${added_files}
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -54,4 +54,4 @@ jobs:
|
||||||
token: ${{ secrets.SERVE_GITHUB_TOKEN }}
|
token: ${{ secrets.SERVE_GITHUB_TOKEN }}
|
||||||
repository: jina-ai/langchain-serve
|
repository: jina-ai/langchain-serve
|
||||||
event-type: langflow-push
|
event-type: langflow-push
|
||||||
client-payload: '{"push_token": "${{ secrets.LCSERVE_PUSH_TOKEN }}", "branch": "dev"}'
|
client-payload: '{"push_token": "${{ secrets.LCSERVE_PUSH_TOKEN }}", "branch": "main"}'
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -241,3 +241,4 @@ dmypy.json
|
||||||
|
|
||||||
# Poetry
|
# Poetry
|
||||||
.testenv/*
|
.testenv/*
|
||||||
|
langflow.db
|
||||||
|
|
|
||||||
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Python: FastAPI",
|
"name": "Debug Backend",
|
||||||
"type": "python",
|
"type": "python",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "uvicorn",
|
"module": "uvicorn",
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
"debug"
|
"debug"
|
||||||
],
|
],
|
||||||
"jinja": true,
|
"jinja": true,
|
||||||
"justMyCode": false
|
"justMyCode": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Python: Remote Attach",
|
"name": "Python: Remote Attach",
|
||||||
|
|
@ -30,6 +30,13 @@
|
||||||
"remoteRoot": "."
|
"remoteRoot": "."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug Frontend",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:3000/",
|
||||||
|
"webRoot": "${workspaceRoot}/src/frontend"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install gcc g++ git make -y && apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
RUN useradd -m -u 1000 user
|
||||||
|
USER user
|
||||||
|
ENV HOME=/home/user \
|
||||||
|
PATH=/home/user/.local/bin:$PATH
|
||||||
|
|
||||||
|
WORKDIR $HOME/app
|
||||||
|
|
||||||
|
COPY --chown=user . $HOME/app
|
||||||
|
|
||||||
|
RUN pip install langflow>==0.0.86 -U --user
|
||||||
|
CMD ["python", "-m", "langflow", "--host", "0.0.0.0", "--port", "7860"]
|
||||||
15
Makefile
15
Makefile
|
|
@ -43,7 +43,18 @@ install_backend:
|
||||||
poetry install
|
poetry install
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
poetry run uvicorn langflow.main:app --port 7860 --reload --log-level debug
|
make install_backend
|
||||||
|
poetry run uvicorn src.backend.langflow.main:app --port 7860 --reload --log-level debug
|
||||||
|
|
||||||
|
build_and_run:
|
||||||
|
echo 'Removing dist folder'
|
||||||
|
rm -rf dist
|
||||||
|
make build && poetry run pip install dist/*.tar.gz && poetry run langflow
|
||||||
|
|
||||||
|
build_and_install:
|
||||||
|
echo 'Removing dist folder'
|
||||||
|
rm -rf dist
|
||||||
|
make build && poetry run pip install dist/*.tar.gz
|
||||||
|
|
||||||
build_frontend:
|
build_frontend:
|
||||||
cd src/frontend && CI='' npm run build
|
cd src/frontend && CI='' npm run build
|
||||||
|
|
@ -59,7 +70,7 @@ lcserve_push:
|
||||||
make build_frontend
|
make build_frontend
|
||||||
@version=$$(poetry version --short); \
|
@version=$$(poetry version --short); \
|
||||||
lc-serve push --app langflow.lcserve:app --app-dir . \
|
lc-serve push --app langflow.lcserve:app --app-dir . \
|
||||||
--image-name langflow --image-tag $${version} --verbose
|
--image-name langflow --image-tag $${version} --verbose --public
|
||||||
|
|
||||||
lcserve_deploy:
|
lcserve_deploy:
|
||||||
@:$(if $(uses),,$(error `uses` is not set. Please run `make uses=... lcserve_deploy`))
|
@:$(if $(uses),,$(error `uses` is not set. Please run `make uses=... lcserve_deploy`))
|
||||||
|
|
|
||||||
61
README.md
61
README.md
|
|
@ -2,10 +2,9 @@
|
||||||
|
|
||||||
# ⛓️ LangFlow
|
# ⛓️ LangFlow
|
||||||
|
|
||||||
~ A User Interface For [LangChain](https://github.com/hwchase17/langchain) ~
|
~ An effortless way to experiment and prototype [LangChain](https://github.com/hwchase17/langchain) pipelines ~
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://huggingface.co/spaces/Logspace/LangFlow"><img src="https://huggingface.co/datasets/huggingface/badges/raw/main/open-in-hf-spaces-sm.svg" alt="HuggingFace Spaces"></a>
|
|
||||||
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/logspace-ai/langflow" />
|
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/logspace-ai/langflow" />
|
||||||
<img alt="GitHub Last Commit" src="https://img.shields.io/github/last-commit/logspace-ai/langflow" />
|
<img alt="GitHub Last Commit" src="https://img.shields.io/github/last-commit/logspace-ai/langflow" />
|
||||||
<img alt="" src="https://img.shields.io/github/repo-size/logspace-ai/langflow" />
|
<img alt="" src="https://img.shields.io/github/repo-size/logspace-ai/langflow" />
|
||||||
|
|
@ -14,10 +13,17 @@
|
||||||
<img alt="Github License" src="https://img.shields.io/github/license/logspace-ai/langflow" />
|
<img alt="Github License" src="https://img.shields.io/github/license/logspace-ai/langflow" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://discord.gg/FUhJnnJ9"><img alt="Discord Server" src="https://dcbadge.vercel.app/api/server/FUhJnnJ9?compact=true&style=flat"/></a>
|
||||||
|
<a href="https://huggingface.co/spaces/Logspace/LangFlow"><img src="https://huggingface.co/datasets/huggingface/badges/raw/main/open-in-hf-spaces-sm.svg" alt="HuggingFace Spaces"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<a href="https://github.com/logspace-ai/langflow">
|
<a href="https://github.com/logspace-ai/langflow">
|
||||||
<img width="100%" src="https://github.com/logspace-ai/langflow/blob/main/img/langflow-demo.gif?raw=true"></a>
|
<img width="100%" src="https://github.com/logspace-ai/langflow/blob/main/img/langflow-demo.gif?raw=true"></a>
|
||||||
|
|
||||||
LangFlow is a GUI for [LangChain](https://github.com/hwchase17/langchain), designed with [react-flow](https://github.com/wbkd/react-flow) to provide an effortless way to experiment and prototype flows with drag-and-drop components and a chat box.
|
|
||||||
|
<p>
|
||||||
|
</p>
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
### <b>Locally</b>
|
### <b>Locally</b>
|
||||||
|
|
@ -50,11 +56,11 @@ Alternatively, click the **"Open in Cloud Shell"** button below to launch Google
|
||||||
|
|
||||||
Langflow integrates with langchain-serve to provide a one-command deployment to Jina AI Cloud.
|
Langflow integrates with langchain-serve to provide a one-command deployment to Jina AI Cloud.
|
||||||
|
|
||||||
Start by installing `langchain-serve` with
|
Start by installing `langchain-serve` with
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -U langchain-serve
|
pip install -U langchain-serve
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, run:
|
Then, run:
|
||||||
|
|
||||||
|
|
@ -109,24 +115,38 @@ You can use Langflow directly on your browser, or use the API endpoints on Jina
|
||||||
<summary>Show API usage (with python)</summary>
|
<summary>Show API usage (with python)</summary>
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import json
|
import requests
|
||||||
import requests
|
|
||||||
|
|
||||||
FLOW_PATH = "Time_traveller.json"
|
BASE_API_URL = "https://langflow-e3dd8820ec.wolf.jina.ai/api/v1/predict"
|
||||||
|
FLOW_ID = "864c4f98-2e59-468b-8e13-79cd8da07468"
|
||||||
|
# You can tweak the flow by adding a tweaks dictionary
|
||||||
|
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
|
||||||
|
TWEAKS = {
|
||||||
|
"ChatOpenAI-g4jEr": {},
|
||||||
|
"ConversationChain-UidfJ": {}
|
||||||
|
}
|
||||||
|
|
||||||
# HOST = 'http://localhost:7860'
|
def run_flow(message: str, flow_id: str, tweaks: dict = None) -> dict:
|
||||||
HOST = 'https://langflow-f1ed20e309.wolf.jina.ai'
|
"""
|
||||||
API_URL = f'{HOST}/predict'
|
Run a flow with a given message and optional tweaks.
|
||||||
|
|
||||||
def predict(message):
|
:param message: The message to send to the flow
|
||||||
with open(FLOW_PATH, "r") as f:
|
:param flow_id: The ID of the flow to run
|
||||||
json_data = json.load(f)
|
:param tweaks: Optional tweaks to customize the flow
|
||||||
payload = {'exported_flow': json_data, 'message': message}
|
:return: The JSON response from the flow
|
||||||
response = requests.post(API_URL, json=payload)
|
"""
|
||||||
return response.json()
|
api_url = f"{BASE_API_URL}/{flow_id}"
|
||||||
|
|
||||||
|
payload = {"message": message}
|
||||||
|
|
||||||
predict('Take me to 1920s Bangalore')
|
if tweaks:
|
||||||
|
payload["tweaks"] = tweaks
|
||||||
|
|
||||||
|
response = requests.post(api_url, json=payload)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
# Setup any tweaks you want to apply to the flow
|
||||||
|
print(run_flow("Your message", flow_id=FLOW_ID, tweaks=TWEAKS))
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
@ -164,6 +184,11 @@ flow("Hey, have you heard of LangFlow?")
|
||||||
We welcome contributions from developers of all levels to our open-source project on GitHub. If you'd like to contribute, please check our [contributing guidelines](./CONTRIBUTING.md) and help make LangFlow more accessible.
|
We welcome contributions from developers of all levels to our open-source project on GitHub. If you'd like to contribute, please check our [contributing guidelines](./CONTRIBUTING.md) and help make LangFlow more accessible.
|
||||||
|
|
||||||
|
|
||||||
|
Join our [Discord](https://discord.com/invite/EqksyE2EX9) server to ask questions, make suggestions and showcase your projects! 🦾
|
||||||
|
|
||||||
|
<p>
|
||||||
|
</p>
|
||||||
|
|
||||||
[](https://star-history.com/#logspace-ai/langflow&Date)
|
[](https://star-history.com/#logspace-ai/langflow&Date)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "reactFlow",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"vite-plugin-svgr": "^3.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1926
poetry.lock
generated
1926
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,11 +1,12 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "langflow"
|
name = "langflow"
|
||||||
version = "0.0.79"
|
version = "0.1.5"
|
||||||
description = "A Python package with a built-in web application"
|
description = "A Python package with a built-in web application"
|
||||||
authors = ["Logspace <contact@logspace.ai>"]
|
authors = ["Logspace <contact@logspace.ai>"]
|
||||||
maintainers = [
|
maintainers = [
|
||||||
|
"Cristhian Zanforlin <cristhian.lousa@gmail.com>",
|
||||||
"Gabriel Almeida <gabriel@logspace.ai>",
|
"Gabriel Almeida <gabriel@logspace.ai>",
|
||||||
"Ibis Prevedello <ibiscp@gmail.com>",
|
"Gustavo Schaedler <gustavopoa@gmail.com>",
|
||||||
"Lucas Eduoli <lucaseduoli@gmail.com>",
|
"Lucas Eduoli <lucaseduoli@gmail.com>",
|
||||||
"Otávio Anovazzi <otavio2204@gmail.com>",
|
"Otávio Anovazzi <otavio2204@gmail.com>",
|
||||||
]
|
]
|
||||||
|
|
@ -22,22 +23,21 @@ langflow = "langflow.__main__:main"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.9,<3.12"
|
python = ">=3.9,<3.12"
|
||||||
fastapi = "^0.95.0"
|
fastapi = "^0.97.0"
|
||||||
uvicorn = "^0.20.0"
|
uvicorn = "^0.22.0"
|
||||||
beautifulsoup4 = "^4.11.2"
|
beautifulsoup4 = "^4.11.2"
|
||||||
google-search-results = "^2.4.1"
|
google-search-results = "^2.4.1"
|
||||||
google-api-python-client = "^2.79.0"
|
google-api-python-client = "^2.79.0"
|
||||||
typer = "^0.7.0"
|
typer = "^0.9.0"
|
||||||
gunicorn = "^20.1.0"
|
gunicorn = "^20.1.0"
|
||||||
langchain = "^0.0.186"
|
langchain = "^0.0.202"
|
||||||
openai = "^0.27.7"
|
openai = "^0.27.8"
|
||||||
types-pyyaml = "^6.0.12.8"
|
types-pyyaml = "^6.0.12.8"
|
||||||
dill = "^0.3.6"
|
|
||||||
pandas = "^1.5.3"
|
pandas = "^1.5.3"
|
||||||
chromadb = "^0.3.21"
|
chromadb = "^0.3.21"
|
||||||
huggingface-hub = "^0.13.3"
|
huggingface-hub = "^0.13.3"
|
||||||
rich = "^13.3.3"
|
rich = "^13.4.2"
|
||||||
llama-cpp-python = "^0.1.50"
|
llama-cpp-python = "~0.1.0"
|
||||||
networkx = "^3.1"
|
networkx = "^3.1"
|
||||||
unstructured = "^0.5.11"
|
unstructured = "^0.5.11"
|
||||||
pypdf = "^3.7.1"
|
pypdf = "^3.7.1"
|
||||||
|
|
@ -46,18 +46,25 @@ pysrt = "^1.1.2"
|
||||||
fake-useragent = "^1.1.3"
|
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 = "^12.0.0"
|
||||||
tiktoken = "^0.3.3"
|
tiktoken = "~0.4.0"
|
||||||
wikipedia = "^1.4.0"
|
wikipedia = "^1.4.0"
|
||||||
langchain-serve = { version = "^0.0.38", optional = true }
|
langchain-serve = { version = ">0.0.39", optional = true }
|
||||||
qdrant-client = "^1.2.0"
|
qdrant-client = "^1.2.0"
|
||||||
websockets = "^11.0.3"
|
websockets = "^11.0.3"
|
||||||
weaviate-client = "^3.19.2"
|
weaviate-client = "^3.21.0"
|
||||||
jina = "3.15.2"
|
jina = "3.15.2"
|
||||||
sentence-transformers = "^2.2.2"
|
sentence-transformers = "^2.2.2"
|
||||||
ctransformers = "^0.2.2"
|
ctransformers = "^0.2.2"
|
||||||
cohere = "^4.6.0"
|
cohere = "^4.6.0"
|
||||||
python-multipart = "^0.0.6"
|
python-multipart = "^0.0.6"
|
||||||
|
sqlmodel = "^0.0.8"
|
||||||
|
faiss-cpu = "^1.7.4"
|
||||||
|
anthropic = "^0.2.10"
|
||||||
|
orjson = "^3.9.1"
|
||||||
|
multiprocess = "^0.70.14"
|
||||||
|
cachetools = "^5.3.1"
|
||||||
|
types-cachetools = "^5.3.0.5"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|
@ -77,6 +84,15 @@ types-pillow = "^9.5.0.2"
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
deploy = ["langchain-serve"]
|
deploy = ["langchain-serve"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
minversion = "6.0"
|
||||||
|
addopts = "-ra"
|
||||||
|
testpaths = ["tests", "integration"]
|
||||||
|
console_output_style = "progress"
|
||||||
|
filterwarnings = ["ignore::DeprecationWarning"]
|
||||||
|
log_cli = true
|
||||||
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
|
from importlib import metadata
|
||||||
from langflow.cache import cache_manager
|
from langflow.cache import cache_manager
|
||||||
from langflow.interface.loading import load_flow_from_json
|
from langflow.processing.process import load_flow_from_json
|
||||||
|
|
||||||
|
try:
|
||||||
|
__version__ = metadata.version(__package__)
|
||||||
|
except metadata.PackageNotFoundError:
|
||||||
|
# Case where package metadata is not available.
|
||||||
|
__version__ = ""
|
||||||
|
del metadata # optional, avoids polluting the results of dir(__package__)
|
||||||
|
|
||||||
__all__ = ["load_flow_from_json", "cache_manager"]
|
__all__ = ["load_flow_from_json", "cache_manager"]
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,46 @@
|
||||||
import multiprocessing
|
import sys
|
||||||
|
import time
|
||||||
|
from fastapi import FastAPI
|
||||||
|
import httpx
|
||||||
|
from multiprocess import Process, cpu_count # type: ignore
|
||||||
import platform
|
import platform
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
import socket
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich import box
|
||||||
|
from rich import print as rprint
|
||||||
import typer
|
import typer
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
from langflow.main import create_app
|
from langflow.main import create_app
|
||||||
from langflow.settings import settings
|
from langflow.settings import settings
|
||||||
from langflow.utils.logger import configure
|
from langflow.utils.logger import configure, logger
|
||||||
|
import webbrowser
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
def get_number_of_workers(workers=None):
|
def get_number_of_workers(workers=None):
|
||||||
if workers == -1:
|
if workers == -1:
|
||||||
workers = (multiprocessing.cpu_count() * 2) + 1
|
workers = (cpu_count() * 2) + 1
|
||||||
return workers
|
return workers
|
||||||
|
|
||||||
|
|
||||||
def update_settings(config: str, dev: bool = False):
|
def update_settings(
|
||||||
|
config: str,
|
||||||
|
dev: bool = False,
|
||||||
|
database_url: Optional[str] = None,
|
||||||
|
remove_api_keys: 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, dev=dev)
|
settings.update_from_yaml(config, dev=dev)
|
||||||
|
if database_url:
|
||||||
|
settings.update_settings(database_url=database_url)
|
||||||
|
if remove_api_keys:
|
||||||
|
settings.update_settings(remove_api_keys=remove_api_keys)
|
||||||
|
|
||||||
|
|
||||||
def serve_on_jcloud():
|
def serve_on_jcloud():
|
||||||
|
|
@ -77,10 +96,28 @@ def serve(
|
||||||
timeout: int = typer.Option(60, help="Worker timeout in seconds."),
|
timeout: int = typer.Option(60, help="Worker timeout in seconds."),
|
||||||
port: int = typer.Option(7860, help="Port to listen on."),
|
port: int = typer.Option(7860, help="Port to listen on."),
|
||||||
config: str = typer.Option("config.yaml", help="Path to the configuration file."),
|
config: str = typer.Option("config.yaml", help="Path to the configuration file."),
|
||||||
log_level: str = typer.Option("info", help="Logging level."),
|
# .env file param
|
||||||
|
env_file: Path = typer.Option(
|
||||||
|
".env", help="Path to the .env file containing environment variables."
|
||||||
|
),
|
||||||
|
log_level: str = typer.Option("critical", help="Logging level."),
|
||||||
log_file: Path = typer.Option("logs/langflow.log", help="Path to the log file."),
|
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"),
|
jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"),
|
||||||
dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"),
|
dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"),
|
||||||
|
database_url: str = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Database URL to connect to. If not provided, a local SQLite database will be used.",
|
||||||
|
),
|
||||||
|
path: str = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Path to the frontend directory containing build files. This is for development purposes only.",
|
||||||
|
),
|
||||||
|
open_browser: bool = typer.Option(
|
||||||
|
True, help="Open the browser after starting the server."
|
||||||
|
),
|
||||||
|
remove_api_keys: bool = typer.Option(
|
||||||
|
False, help="Remove API keys from the projects saved in the database."
|
||||||
|
),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Run the Langflow server.
|
Run the Langflow server.
|
||||||
|
|
@ -89,17 +126,25 @@ def serve(
|
||||||
if jcloud:
|
if jcloud:
|
||||||
return serve_on_jcloud()
|
return serve_on_jcloud()
|
||||||
|
|
||||||
|
load_dotenv(env_file)
|
||||||
|
|
||||||
configure(log_level=log_level, log_file=log_file)
|
configure(log_level=log_level, log_file=log_file)
|
||||||
update_settings(config, dev=dev)
|
update_settings(
|
||||||
app = create_app()
|
config, dev=dev, database_url=database_url, remove_api_keys=remove_api_keys
|
||||||
# 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",
|
|
||||||
)
|
)
|
||||||
|
# get the directory of the current file
|
||||||
|
if not path:
|
||||||
|
frontend_path = Path(__file__).parent
|
||||||
|
static_files_dir = frontend_path / "frontend"
|
||||||
|
else:
|
||||||
|
static_files_dir = Path(path)
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
setup_static_files(app, static_files_dir)
|
||||||
|
# check if port is being used
|
||||||
|
if is_port_in_use(port, host):
|
||||||
|
port = get_free_port(port)
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
"bind": f"{host}:{port}",
|
"bind": f"{host}:{port}",
|
||||||
"workers": get_number_of_workers(workers),
|
"workers": get_number_of_workers(workers),
|
||||||
|
|
@ -107,17 +152,147 @@ def serve(
|
||||||
"timeout": timeout,
|
"timeout": timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
if platform.system() in ["Darwin", "Windows"]:
|
if platform.system() in ["Windows"]:
|
||||||
# Run using uvicorn on MacOS and Windows
|
# Run using uvicorn on MacOS and Windows
|
||||||
# Windows doesn't support gunicorn
|
# Windows doesn't support gunicorn
|
||||||
# MacOS requires an env variable to be set to use gunicorn
|
# MacOS requires an env variable to be set to use gunicorn
|
||||||
import uvicorn
|
run_on_windows(host, port, log_level, options, app)
|
||||||
|
|
||||||
uvicorn.run(app, host=host, port=port, log_level=log_level)
|
|
||||||
else:
|
else:
|
||||||
from langflow.server import LangflowApplication
|
# Run using gunicorn on Linux
|
||||||
|
run_on_mac_or_linux(host, port, log_level, options, app, open_browser)
|
||||||
|
|
||||||
LangflowApplication(app, options).run()
|
|
||||||
|
def run_on_mac_or_linux(host, port, log_level, options, app, open_browser=True):
|
||||||
|
webapp_process = Process(
|
||||||
|
target=run_langflow, args=(host, port, log_level, options, app)
|
||||||
|
)
|
||||||
|
webapp_process.start()
|
||||||
|
status_code = 0
|
||||||
|
while status_code != 200:
|
||||||
|
try:
|
||||||
|
status_code = httpx.get(f"http://{host}:{port}/health").status_code
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print_banner(host, port)
|
||||||
|
if open_browser:
|
||||||
|
webbrowser.open(f"http://{host}:{port}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_on_windows(host, port, log_level, options, app):
|
||||||
|
"""
|
||||||
|
Run the Langflow server on Windows.
|
||||||
|
"""
|
||||||
|
print_banner(host, port)
|
||||||
|
run_langflow(host, port, log_level, options, app)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_static_files(app: FastAPI, static_files_dir: Path):
|
||||||
|
"""
|
||||||
|
Setup the static files directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app (FastAPI): FastAPI app.
|
||||||
|
path (str): Path to the static files directory.
|
||||||
|
"""
|
||||||
|
app.mount(
|
||||||
|
"/",
|
||||||
|
StaticFiles(directory=static_files_dir, html=True),
|
||||||
|
name="static",
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(404)
|
||||||
|
async def custom_404_handler(request, __):
|
||||||
|
path = static_files_dir / "index.html"
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
raise RuntimeError(f"File at path {path} does not exist.")
|
||||||
|
return FileResponse(path)
|
||||||
|
|
||||||
|
|
||||||
|
def is_port_in_use(port, host="localhost"):
|
||||||
|
"""
|
||||||
|
Check if a port is in use.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port (int): The port number to check.
|
||||||
|
host (str): The host to check the port on. Defaults to 'localhost'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the port is in use, False otherwise.
|
||||||
|
"""
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
return s.connect_ex((host, port)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_free_port(port):
|
||||||
|
"""
|
||||||
|
Given a used port, find a free port.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port (int): The port number to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: A free port number.
|
||||||
|
"""
|
||||||
|
while is_port_in_use(port):
|
||||||
|
port += 1
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
|
def print_banner(host, port):
|
||||||
|
# console = Console()
|
||||||
|
|
||||||
|
word = "LangFlow"
|
||||||
|
colors = ["#3300cc"]
|
||||||
|
|
||||||
|
styled_word = ""
|
||||||
|
|
||||||
|
for i, char in enumerate(word):
|
||||||
|
color = colors[i % len(colors)]
|
||||||
|
styled_word += f"[{color}]{char}[/]"
|
||||||
|
|
||||||
|
# Title with emojis and gradient text
|
||||||
|
title = (
|
||||||
|
f"[bold]Welcome to :chains: {styled_word} [/bold]\n\n"
|
||||||
|
f"Access [link=http://{host}:{port}]http://{host}:{port}[/link]"
|
||||||
|
)
|
||||||
|
info_text = (
|
||||||
|
"Collaborate, and contribute at our "
|
||||||
|
"[bold][link=https://github.com/logspace-ai/langflow]GitHub Repo[/link][/bold] :rocket:"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a panel with the title and the info text, and a border around it
|
||||||
|
panel = Panel(
|
||||||
|
f"{title}\n{info_text}", box=box.ROUNDED, border_style="blue", expand=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Print the banner with a separator line before and after
|
||||||
|
rprint(panel)
|
||||||
|
|
||||||
|
|
||||||
|
def run_langflow(host, port, log_level, options, app):
|
||||||
|
"""
|
||||||
|
Run Langflow server on localhost
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
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()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from langflow.api.router import router
|
||||||
|
|
||||||
|
__all__ = ["router"]
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
from fastapi import (
|
|
||||||
APIRouter,
|
|
||||||
WebSocket,
|
|
||||||
WebSocketDisconnect,
|
|
||||||
WebSocketException,
|
|
||||||
status,
|
|
||||||
)
|
|
||||||
|
|
||||||
from langflow.api.chat_manager import ChatManager
|
|
||||||
from langflow.utils.logger import logger
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
chat_manager = ChatManager()
|
|
||||||
|
|
||||||
|
|
||||||
@router.websocket("/chat/{client_id}")
|
|
||||||
async def websocket_endpoint(client_id: str, websocket: WebSocket):
|
|
||||||
"""Websocket endpoint for chat."""
|
|
||||||
try:
|
|
||||||
await chat_manager.handle_websocket(client_id, websocket)
|
|
||||||
except WebSocketException as exc:
|
|
||||||
logger.error(exc)
|
|
||||||
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc))
|
|
||||||
except WebSocketDisconnect as exc:
|
|
||||||
logger.error(exc)
|
|
||||||
await websocket.close(code=status.WS_1000_NORMAL_CLOSURE, reason=str(exc))
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import logging
|
|
||||||
from importlib.metadata import version
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, UploadFile
|
|
||||||
|
|
||||||
from langflow.api.schemas import (
|
|
||||||
ExportedFlow,
|
|
||||||
GraphData,
|
|
||||||
PredictRequest,
|
|
||||||
PredictResponse,
|
|
||||||
)
|
|
||||||
from langflow.cache.base import save_uploaded_file
|
|
||||||
from langflow.interface.run import process_graph_cached
|
|
||||||
from langflow.interface.types import build_langchain_types_dict
|
|
||||||
|
|
||||||
# build router
|
|
||||||
router = APIRouter()
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/all")
|
|
||||||
def get_all():
|
|
||||||
return build_langchain_types_dict()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/predict", response_model=PredictResponse)
|
|
||||||
async def get_load(predict_request: PredictRequest):
|
|
||||||
try:
|
|
||||||
exported_flow: ExportedFlow = predict_request.exported_flow
|
|
||||||
graph_data: GraphData = exported_flow.data
|
|
||||||
data = graph_data.dict()
|
|
||||||
response = process_graph_cached(data, predict_request.message)
|
|
||||||
return PredictResponse(result=response.get("result", ""))
|
|
||||||
except Exception as e:
|
|
||||||
# Log stack trace
|
|
||||||
logger.exception(e)
|
|
||||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
||||||
|
|
||||||
|
|
||||||
# Endpoint to upload file
|
|
||||||
@router.post("/upload/{client_id}")
|
|
||||||
async def create_upload_file(file: UploadFile, client_id: str):
|
|
||||||
# Cache file
|
|
||||||
file_path = save_uploaded_file(file.file, file_name=client_id)
|
|
||||||
|
|
||||||
return {"file_path": file_path}
|
|
||||||
|
|
||||||
|
|
||||||
# get endpoint to return version of langflow
|
|
||||||
@router.get("/version")
|
|
||||||
def get_version():
|
|
||||||
return {"version": version("langflow")}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/health")
|
|
||||||
def get_health():
|
|
||||||
return {"status": "OK"}
|
|
||||||
18
src/backend/langflow/api/router.py
Normal file
18
src/backend/langflow/api/router.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Router for base api
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from langflow.api.v1 import (
|
||||||
|
chat_router,
|
||||||
|
endpoints_router,
|
||||||
|
validate_router,
|
||||||
|
flows_router,
|
||||||
|
flow_styles_router,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/api/v1",
|
||||||
|
)
|
||||||
|
router.include_router(chat_router)
|
||||||
|
router.include_router(endpoints_router)
|
||||||
|
router.include_router(validate_router)
|
||||||
|
router.include_router(flows_router)
|
||||||
|
router.include_router(flow_styles_router)
|
||||||
24
src/backend/langflow/api/utils.py
Normal file
24
src/backend/langflow/api/utils.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
API_WORDS = ["api", "key", "token"]
|
||||||
|
|
||||||
|
|
||||||
|
def has_api_terms(word: str):
|
||||||
|
return "api" in word and (
|
||||||
|
"key" in word or ("token" in word and "tokens" not in word)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_api_keys(flow: dict):
|
||||||
|
"""Remove api keys from flow data."""
|
||||||
|
if flow.get("data") and flow["data"].get("nodes"):
|
||||||
|
for node in flow["data"]["nodes"]:
|
||||||
|
node_data = node.get("data").get("node")
|
||||||
|
template = node_data.get("template")
|
||||||
|
for value in template.values():
|
||||||
|
if (
|
||||||
|
isinstance(value, dict)
|
||||||
|
and has_api_terms(value["name"])
|
||||||
|
and value.get("password")
|
||||||
|
):
|
||||||
|
value["value"] = None
|
||||||
|
|
||||||
|
return flow
|
||||||
13
src/backend/langflow/api/v1/__init__.py
Normal file
13
src/backend/langflow/api/v1/__init__.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from langflow.api.v1.endpoints import router as endpoints_router
|
||||||
|
from langflow.api.v1.validate import router as validate_router
|
||||||
|
from langflow.api.v1.chat import router as chat_router
|
||||||
|
from langflow.api.v1.flows import router as flows_router
|
||||||
|
from langflow.api.v1.flow_styles import router as flow_styles_router
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"chat_router",
|
||||||
|
"endpoints_router",
|
||||||
|
"validate_router",
|
||||||
|
"flows_router",
|
||||||
|
"flow_styles_router",
|
||||||
|
]
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from pydantic import BaseModel, validator
|
from pydantic import BaseModel, validator
|
||||||
|
|
||||||
from langflow.graph.utils import extract_input_variables_from_prompt
|
from langflow.interface.utils import extract_input_variables_from_prompt
|
||||||
|
|
||||||
|
|
||||||
class CacheResponse(BaseModel):
|
class CacheResponse(BaseModel):
|
||||||
|
|
@ -3,7 +3,7 @@ from typing import Any
|
||||||
|
|
||||||
from langchain.callbacks.base import AsyncCallbackHandler, BaseCallbackHandler
|
from langchain.callbacks.base import AsyncCallbackHandler, BaseCallbackHandler
|
||||||
|
|
||||||
from langflow.api.schemas import ChatResponse
|
from langflow.api.v1.schemas import ChatResponse
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/hwchase17/chat-langchain/blob/master/callback.py
|
# https://github.com/hwchase17/chat-langchain/blob/master/callback.py
|
||||||
128
src/backend/langflow/api/v1/chat.py
Normal file
128
src/backend/langflow/api/v1/chat.py
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import json
|
||||||
|
from fastapi import (
|
||||||
|
APIRouter,
|
||||||
|
HTTPException,
|
||||||
|
WebSocket,
|
||||||
|
WebSocketException,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from langflow.api.v1.schemas import BuiltResponse, InitResponse
|
||||||
|
|
||||||
|
from langflow.chat.manager import ChatManager
|
||||||
|
from langflow.graph.graph.base import Graph
|
||||||
|
from langflow.utils.logger import logger
|
||||||
|
from cachetools import LRUCache
|
||||||
|
|
||||||
|
router = APIRouter(tags=["Chat"])
|
||||||
|
chat_manager = ChatManager()
|
||||||
|
flow_data_store: LRUCache = LRUCache(maxsize=10)
|
||||||
|
|
||||||
|
|
||||||
|
@router.websocket("/chat/{client_id}")
|
||||||
|
async def chat(client_id: str, websocket: WebSocket):
|
||||||
|
"""Websocket endpoint for chat."""
|
||||||
|
try:
|
||||||
|
if client_id in chat_manager.in_memory_cache:
|
||||||
|
await chat_manager.handle_websocket(client_id, websocket)
|
||||||
|
else:
|
||||||
|
message = "Please, build the flow before sending messages"
|
||||||
|
await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason=message)
|
||||||
|
except WebSocketException as exc:
|
||||||
|
logger.error(exc)
|
||||||
|
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/build/init", response_model=InitResponse, status_code=201)
|
||||||
|
async def init_build(graph_data: dict):
|
||||||
|
"""Initialize the build by storing graph data and returning a unique session ID."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
flow_id = graph_data.get("id")
|
||||||
|
if flow_id is None:
|
||||||
|
raise ValueError("No ID provided")
|
||||||
|
flow_data_store[flow_id] = graph_data
|
||||||
|
|
||||||
|
return InitResponse(flowId=flow_id)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(exc)
|
||||||
|
return HTTPException(status_code=500, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/build/{flow_id}/status", response_model=BuiltResponse)
|
||||||
|
async def build_status(flow_id: str):
|
||||||
|
"""Check the flow_id is in the flow_data_store."""
|
||||||
|
try:
|
||||||
|
built = flow_id in flow_data_store and not isinstance(
|
||||||
|
flow_data_store[flow_id], dict
|
||||||
|
)
|
||||||
|
|
||||||
|
return BuiltResponse(
|
||||||
|
built=built,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(exc)
|
||||||
|
return HTTPException(status_code=500, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/build/stream/{flow_id}", response_class=StreamingResponse)
|
||||||
|
async def stream_build(flow_id: str):
|
||||||
|
"""Stream the build process based on stored flow data."""
|
||||||
|
|
||||||
|
async def event_stream(flow_id):
|
||||||
|
final_response = json.dumps({"end_of_stream": True})
|
||||||
|
try:
|
||||||
|
if flow_id not in flow_data_store:
|
||||||
|
error_message = "Invalid session ID"
|
||||||
|
yield f"data: {json.dumps({'error': error_message})}\n\n"
|
||||||
|
return
|
||||||
|
|
||||||
|
graph_data = flow_data_store[flow_id].get("data")
|
||||||
|
|
||||||
|
if not graph_data:
|
||||||
|
error_message = "No data provided"
|
||||||
|
yield f"data: {json.dumps({'error': error_message})}\n\n"
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug("Building langchain object")
|
||||||
|
graph = Graph.from_payload(graph_data)
|
||||||
|
number_of_nodes = len(graph.nodes)
|
||||||
|
for i, vertex in enumerate(graph.generator_build(), 1):
|
||||||
|
try:
|
||||||
|
log_dict = {
|
||||||
|
"log": f"Building node {vertex.vertex_type}",
|
||||||
|
"progress": round(i / number_of_nodes, 2),
|
||||||
|
}
|
||||||
|
yield f"data: {json.dumps(log_dict)}\n\n"
|
||||||
|
vertex.build()
|
||||||
|
params = vertex._built_object_repr()
|
||||||
|
valid = True
|
||||||
|
logger.debug(
|
||||||
|
f"Building node {params[:50]}{'...' if len(params) > 50 else ''}"
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
params = str(exc)
|
||||||
|
valid = False
|
||||||
|
|
||||||
|
response = json.dumps(
|
||||||
|
{
|
||||||
|
"valid": valid,
|
||||||
|
"params": params,
|
||||||
|
"id": vertex.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
yield f"data: {response}\n\n"
|
||||||
|
|
||||||
|
chat_manager.set_cache(flow_id, graph.build())
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Error while building the flow: %s", exc)
|
||||||
|
yield f"error: {json.dumps({'error': str(exc)})}\n\n"
|
||||||
|
finally:
|
||||||
|
yield f"data: {final_response}\n\n"
|
||||||
|
|
||||||
|
try:
|
||||||
|
return StreamingResponse(event_stream(flow_id), media_type="text/event-stream")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(exc)
|
||||||
|
raise HTTPException(status_code=500, detail=str(exc))
|
||||||
72
src/backend/langflow/api/v1/endpoints.py
Normal file
72
src/backend/langflow/api/v1/endpoints.py
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
from langflow.database.models.flow import Flow
|
||||||
|
from langflow.processing.process import process_graph_cached, process_tweaks
|
||||||
|
from langflow.utils.logger import logger
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile
|
||||||
|
|
||||||
|
from langflow.api.v1.schemas import (
|
||||||
|
PredictRequest,
|
||||||
|
PredictResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
from langflow.interface.types import build_langchain_types_dict
|
||||||
|
from langflow.database.base import get_session
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
# build router
|
||||||
|
router = APIRouter(tags=["Base"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/all")
|
||||||
|
def get_all():
|
||||||
|
return build_langchain_types_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/predict/{flow_id}", response_model=PredictResponse)
|
||||||
|
async def predict_flow(
|
||||||
|
predict_request: PredictRequest,
|
||||||
|
flow_id: str,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Endpoint to process a message using the flow passed in the bearer token.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
flow = session.get(Flow, flow_id)
|
||||||
|
if flow is None:
|
||||||
|
raise ValueError(f"Flow {flow_id} not found")
|
||||||
|
|
||||||
|
if flow.data is None:
|
||||||
|
raise ValueError(f"Flow {flow_id} has no data")
|
||||||
|
graph_data = flow.data
|
||||||
|
if predict_request.tweaks:
|
||||||
|
try:
|
||||||
|
graph_data = process_tweaks(graph_data, predict_request.tweaks)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"Error processing tweaks: {exc}")
|
||||||
|
response = process_graph_cached(graph_data, predict_request.message)
|
||||||
|
return PredictResponse(
|
||||||
|
result=response.get("result", ""),
|
||||||
|
intermediate_steps=response.get("thought", ""),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Log stack trace
|
||||||
|
logger.exception(e)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
@router.post("/upload/{client_id}")
|
||||||
|
async def create_upload_file(file: UploadFile, client_id: str):
|
||||||
|
# Cache file
|
||||||
|
file_path = save_uploaded_file(file.file, file_name=client_id)
|
||||||
|
|
||||||
|
return {"file_path": file_path}
|
||||||
|
|
||||||
|
|
||||||
|
# get endpoint to return version of langflow
|
||||||
|
@router.get("/version")
|
||||||
|
def get_version():
|
||||||
|
from langflow import __version__
|
||||||
|
|
||||||
|
return {"version": __version__}
|
||||||
|
|
||||||
83
src/backend/langflow/api/v1/flow_styles.py
Normal file
83
src/backend/langflow/api/v1/flow_styles.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
from uuid import UUID
|
||||||
|
from langflow.database.models.flow_style import (
|
||||||
|
FlowStyle,
|
||||||
|
FlowStyleCreate,
|
||||||
|
FlowStyleRead,
|
||||||
|
FlowStyleUpdate,
|
||||||
|
)
|
||||||
|
from langflow.database.base import get_session
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
# build router
|
||||||
|
router = APIRouter(prefix="/flow_styles", tags=["FlowStyles"])
|
||||||
|
|
||||||
|
# FlowStyleCreate:
|
||||||
|
# class FlowStyleBase(SQLModel):
|
||||||
|
# color: str = Field(index=True)
|
||||||
|
# emoji: str = Field(index=False)
|
||||||
|
# flow_id: UUID = Field(default=None, foreign_key="flow.id")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=FlowStyleRead)
|
||||||
|
def create_flow_style(
|
||||||
|
*, session: Session = Depends(get_session), flow_style: FlowStyleCreate
|
||||||
|
):
|
||||||
|
"""Create a new flow_style."""
|
||||||
|
db_flow_style = FlowStyle.from_orm(flow_style)
|
||||||
|
session.add(db_flow_style)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_flow_style)
|
||||||
|
return db_flow_style
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=list[FlowStyleRead])
|
||||||
|
def read_flow_styles(*, session: Session = Depends(get_session)):
|
||||||
|
"""Read all flows."""
|
||||||
|
try:
|
||||||
|
flows = session.exec(select(FlowStyle)).all()
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
return flows
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{flow_styles_id}", response_model=FlowStyleRead)
|
||||||
|
def read_flow_style(*, session: Session = Depends(get_session), flow_styles_id: UUID):
|
||||||
|
"""Read a flow_style."""
|
||||||
|
if flow_style := session.get(FlowStyle, flow_styles_id):
|
||||||
|
return flow_style
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="FlowStyle not found")
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{flow_style_id}", response_model=FlowStyleRead)
|
||||||
|
def update_flow_style(
|
||||||
|
*,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
flow_style_id: UUID,
|
||||||
|
flow_style: FlowStyleUpdate,
|
||||||
|
):
|
||||||
|
"""Update a flow_style."""
|
||||||
|
db_flow_style = session.get(FlowStyle, flow_style_id)
|
||||||
|
if not db_flow_style:
|
||||||
|
raise HTTPException(status_code=404, detail="FlowStyle not found")
|
||||||
|
flow_data = flow_style.dict(exclude_unset=True)
|
||||||
|
for key, value in flow_data.items():
|
||||||
|
if hasattr(db_flow_style, key) and value is not None:
|
||||||
|
setattr(db_flow_style, key, value)
|
||||||
|
session.add(db_flow_style)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_flow_style)
|
||||||
|
return db_flow_style
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{flow_id}")
|
||||||
|
def delete_flow_style(*, session: Session = Depends(get_session), flow_id: UUID):
|
||||||
|
"""Delete a flow_style."""
|
||||||
|
flow_style = session.get(FlowStyle, flow_id)
|
||||||
|
if not flow_style:
|
||||||
|
raise HTTPException(status_code=404, detail="FlowStyle not found")
|
||||||
|
session.delete(flow_style)
|
||||||
|
session.commit()
|
||||||
|
return {"message": "FlowStyle deleted successfully"}
|
||||||
120
src/backend/langflow/api/v1/flows.py
Normal file
120
src/backend/langflow/api/v1/flows.py
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
from typing import List
|
||||||
|
from uuid import UUID
|
||||||
|
from langflow.settings import settings
|
||||||
|
from langflow.api.utils import remove_api_keys
|
||||||
|
from langflow.api.v1.schemas import FlowListCreate, FlowListRead
|
||||||
|
from langflow.database.models.flow import (
|
||||||
|
Flow,
|
||||||
|
FlowCreate,
|
||||||
|
FlowRead,
|
||||||
|
FlowReadWithStyle,
|
||||||
|
FlowUpdate,
|
||||||
|
)
|
||||||
|
from langflow.database.base import get_session
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
|
||||||
|
from fastapi import File, UploadFile
|
||||||
|
import json
|
||||||
|
|
||||||
|
# build router
|
||||||
|
router = APIRouter(prefix="/flows", tags=["Flows"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=FlowRead, status_code=201)
|
||||||
|
def create_flow(*, session: Session = Depends(get_session), flow: FlowCreate):
|
||||||
|
"""Create a new flow."""
|
||||||
|
db_flow = Flow.from_orm(flow)
|
||||||
|
session.add(db_flow)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_flow)
|
||||||
|
return db_flow
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=list[FlowReadWithStyle], status_code=200)
|
||||||
|
def read_flows(*, session: Session = Depends(get_session)):
|
||||||
|
"""Read all flows."""
|
||||||
|
try:
|
||||||
|
flows = session.exec(select(Flow)).all()
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
return [jsonable_encoder(flow) for flow in flows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{flow_id}", response_model=FlowReadWithStyle, status_code=200)
|
||||||
|
def read_flow(*, session: Session = Depends(get_session), flow_id: UUID):
|
||||||
|
"""Read a flow."""
|
||||||
|
if flow := session.get(Flow, flow_id):
|
||||||
|
return flow
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Flow not found")
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{flow_id}", response_model=FlowRead, status_code=200)
|
||||||
|
def update_flow(
|
||||||
|
*, session: Session = Depends(get_session), flow_id: UUID, flow: FlowUpdate
|
||||||
|
):
|
||||||
|
"""Update a flow."""
|
||||||
|
|
||||||
|
db_flow = session.get(Flow, flow_id)
|
||||||
|
if not db_flow:
|
||||||
|
raise HTTPException(status_code=404, detail="Flow not found")
|
||||||
|
flow_data = flow.dict(exclude_unset=True)
|
||||||
|
if settings.remove_api_keys:
|
||||||
|
flow_data = remove_api_keys(flow_data)
|
||||||
|
for key, value in flow_data.items():
|
||||||
|
setattr(db_flow, key, value)
|
||||||
|
session.add(db_flow)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_flow)
|
||||||
|
return db_flow
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{flow_id}", status_code=200)
|
||||||
|
def delete_flow(*, session: Session = Depends(get_session), flow_id: UUID):
|
||||||
|
"""Delete a flow."""
|
||||||
|
flow = session.get(Flow, flow_id)
|
||||||
|
if not flow:
|
||||||
|
raise HTTPException(status_code=404, detail="Flow not found")
|
||||||
|
session.delete(flow)
|
||||||
|
session.commit()
|
||||||
|
return {"message": "Flow deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
# Define a new model to handle multiple flows
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/batch/", response_model=List[FlowRead], status_code=201)
|
||||||
|
def create_flows(*, session: Session = Depends(get_session), flow_list: FlowListCreate):
|
||||||
|
"""Create multiple new flows."""
|
||||||
|
db_flows = []
|
||||||
|
for flow in flow_list.flows:
|
||||||
|
db_flow = Flow.from_orm(flow)
|
||||||
|
session.add(db_flow)
|
||||||
|
db_flows.append(db_flow)
|
||||||
|
session.commit()
|
||||||
|
for db_flow in db_flows:
|
||||||
|
session.refresh(db_flow)
|
||||||
|
return db_flows
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/upload/", response_model=List[FlowRead], status_code=201)
|
||||||
|
async def upload_file(
|
||||||
|
*, session: Session = Depends(get_session), file: UploadFile = File(...)
|
||||||
|
):
|
||||||
|
"""Upload flows from a file."""
|
||||||
|
contents = await file.read()
|
||||||
|
data = json.loads(contents)
|
||||||
|
if "flows" in data:
|
||||||
|
flow_list = FlowListCreate(**data)
|
||||||
|
else:
|
||||||
|
flow_list = FlowListCreate(flows=[FlowCreate(**flow) for flow in data])
|
||||||
|
return create_flows(session=session, flow_list=flow_list)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/download/", response_model=FlowListRead, status_code=200)
|
||||||
|
async def download_file(*, session: Session = Depends(get_session)):
|
||||||
|
"""Download all flows as a file."""
|
||||||
|
flows = read_flows(session=session)
|
||||||
|
return FlowListRead(flows=flows)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
from langflow.database.models.flow import FlowCreate, FlowRead
|
||||||
from pydantic import BaseModel, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
|
|
||||||
|
|
||||||
class GraphData(BaseModel):
|
class GraphData(BaseModel):
|
||||||
|
|
@ -23,13 +23,30 @@ class PredictRequest(BaseModel):
|
||||||
"""Predict request schema."""
|
"""Predict request schema."""
|
||||||
|
|
||||||
message: str
|
message: str
|
||||||
exported_flow: ExportedFlow
|
tweaks: Optional[Dict[str, Dict[str, str]]] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"message": "Hello, how are you?",
|
||||||
|
"tweaks": {
|
||||||
|
"dndnode_986363f0-4677-4035-9f38-74b94af5dd78": {
|
||||||
|
"name": "A tool name",
|
||||||
|
"description": "A tool description",
|
||||||
|
},
|
||||||
|
"dndnode_986363f0-4677-4035-9f38-74b94af57378": {
|
||||||
|
"template": "A {template}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PredictResponse(BaseModel):
|
class PredictResponse(BaseModel):
|
||||||
"""Predict response schema."""
|
"""Predict response schema."""
|
||||||
|
|
||||||
result: str
|
result: str
|
||||||
|
intermediate_steps: str = ""
|
||||||
|
|
||||||
|
|
||||||
class ChatMessage(BaseModel):
|
class ChatMessage(BaseModel):
|
||||||
|
|
@ -68,3 +85,19 @@ class FileResponse(ChatMessage):
|
||||||
if v not in ["image", "csv"]:
|
if v not in ["image", "csv"]:
|
||||||
raise ValueError("data_type must be image or csv")
|
raise ValueError("data_type must be image or csv")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class FlowListCreate(BaseModel):
|
||||||
|
flows: List[FlowCreate]
|
||||||
|
|
||||||
|
|
||||||
|
class FlowListRead(BaseModel):
|
||||||
|
flows: List[FlowRead]
|
||||||
|
|
||||||
|
|
||||||
|
class InitResponse(BaseModel):
|
||||||
|
flowId: str
|
||||||
|
|
||||||
|
|
||||||
|
class BuiltResponse(BaseModel):
|
||||||
|
built: bool
|
||||||
|
|
@ -2,20 +2,20 @@ import json
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
from langflow.api.base import (
|
from langflow.api.v1.base import (
|
||||||
Code,
|
Code,
|
||||||
CodeValidationResponse,
|
CodeValidationResponse,
|
||||||
Prompt,
|
Prompt,
|
||||||
PromptValidationResponse,
|
PromptValidationResponse,
|
||||||
validate_prompt,
|
validate_prompt,
|
||||||
)
|
)
|
||||||
from langflow.graph.nodes import VectorStoreNode
|
from langflow.graph.vertex.types import VectorStoreVertex
|
||||||
from langflow.interface.run import build_graph
|
from langflow.graph import Graph
|
||||||
from langflow.utils.logger import logger
|
from langflow.utils.logger import logger
|
||||||
from langflow.utils.validate import validate_code
|
from langflow.utils.validate import validate_code
|
||||||
|
|
||||||
# build router
|
# build router
|
||||||
router = APIRouter(prefix="/validate", tags=["validate"])
|
router = APIRouter(prefix="/validate", tags=["Validate"])
|
||||||
|
|
||||||
|
|
||||||
@router.post("/code", status_code=200, response_model=CodeValidationResponse)
|
@router.post("/code", status_code=200, response_model=CodeValidationResponse)
|
||||||
|
|
@ -44,12 +44,12 @@ def post_validate_prompt(prompt: Prompt):
|
||||||
def post_validate_node(node_id: str, data: dict):
|
def post_validate_node(node_id: str, data: dict):
|
||||||
try:
|
try:
|
||||||
# build graph
|
# build graph
|
||||||
graph = build_graph(data)
|
graph = Graph.from_payload(data)
|
||||||
# validate node
|
# validate node
|
||||||
node = graph.get_node(node_id)
|
node = graph.get_node(node_id)
|
||||||
if node is None:
|
if node is None:
|
||||||
raise ValueError(f"Node {node_id} not found")
|
raise ValueError(f"Node {node_id} not found")
|
||||||
if not isinstance(node, VectorStoreNode):
|
if not isinstance(node, VectorStoreVertex):
|
||||||
node.build()
|
node.build()
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
8
src/backend/langflow/cache/__init__.py
vendored
8
src/backend/langflow/cache/__init__.py
vendored
|
|
@ -1 +1,7 @@
|
||||||
from langflow.cache.manager import cache_manager # noqa
|
from langflow.cache.manager import cache_manager
|
||||||
|
from langflow.cache.flow import InMemoryCache
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"cache_manager",
|
||||||
|
"InMemoryCache",
|
||||||
|
]
|
||||||
|
|
|
||||||
225
src/backend/langflow/cache/base.py
vendored
225
src/backend/langflow/cache/base.py
vendored
|
|
@ -1,167 +1,84 @@
|
||||||
import base64
|
import abc
|
||||||
import contextlib
|
|
||||||
import functools
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
from collections import OrderedDict
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
import dill # type: ignore
|
|
||||||
|
|
||||||
CACHE: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def create_cache_folder(func):
|
class BaseCache(abc.ABC):
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
# Get the destination folder
|
|
||||||
cache_path = Path(tempfile.gettempdir()) / PREFIX
|
|
||||||
|
|
||||||
# Create the destination folder if it doesn't exist
|
|
||||||
os.makedirs(cache_path, exist_ok=True)
|
|
||||||
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def memoize_dict(maxsize=128):
|
|
||||||
cache = OrderedDict()
|
|
||||||
|
|
||||||
def decorator(func):
|
|
||||||
@functools.wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
hashed = compute_dict_hash(args[0])
|
|
||||||
key = (func.__name__, hashed, frozenset(kwargs.items()))
|
|
||||||
if key not in cache:
|
|
||||||
result = func(*args, **kwargs)
|
|
||||||
cache[key] = result
|
|
||||||
if len(cache) > maxsize:
|
|
||||||
cache.popitem(last=False)
|
|
||||||
else:
|
|
||||||
result = cache[key]
|
|
||||||
return result
|
|
||||||
|
|
||||||
def clear_cache():
|
|
||||||
cache.clear()
|
|
||||||
|
|
||||||
wrapper.clear_cache = clear_cache # type: ignore
|
|
||||||
wrapper.cache = cache # type: ignore
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
PREFIX = "langflow_cache"
|
|
||||||
|
|
||||||
|
|
||||||
@create_cache_folder
|
|
||||||
def clear_old_cache_files(max_cache_size: int = 3):
|
|
||||||
cache_dir = Path(tempfile.gettempdir()) / PREFIX
|
|
||||||
cache_files = list(cache_dir.glob("*.dill"))
|
|
||||||
|
|
||||||
if len(cache_files) > max_cache_size:
|
|
||||||
cache_files_sorted_by_mtime = sorted(
|
|
||||||
cache_files, key=lambda x: x.stat().st_mtime, reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
for cache_file in cache_files_sorted_by_mtime[max_cache_size:]:
|
|
||||||
with contextlib.suppress(OSError):
|
|
||||||
os.remove(cache_file)
|
|
||||||
|
|
||||||
|
|
||||||
def compute_dict_hash(graph_data):
|
|
||||||
graph_data = filter_json(graph_data)
|
|
||||||
|
|
||||||
cleaned_graph_json = json.dumps(graph_data, sort_keys=True)
|
|
||||||
return hashlib.sha256(cleaned_graph_json.encode("utf-8")).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def filter_json(json_data):
|
|
||||||
filtered_data = json_data.copy()
|
|
||||||
|
|
||||||
# Remove 'viewport' and 'chatHistory' keys
|
|
||||||
if "viewport" in filtered_data:
|
|
||||||
del filtered_data["viewport"]
|
|
||||||
if "chatHistory" in filtered_data:
|
|
||||||
del filtered_data["chatHistory"]
|
|
||||||
|
|
||||||
# Filter nodes
|
|
||||||
if "nodes" in filtered_data:
|
|
||||||
for node in filtered_data["nodes"]:
|
|
||||||
if "position" in node:
|
|
||||||
del node["position"]
|
|
||||||
if "positionAbsolute" in node:
|
|
||||||
del node["positionAbsolute"]
|
|
||||||
if "selected" in node:
|
|
||||||
del node["selected"]
|
|
||||||
if "dragging" in node:
|
|
||||||
del node["dragging"]
|
|
||||||
|
|
||||||
return filtered_data
|
|
||||||
|
|
||||||
|
|
||||||
@create_cache_folder
|
|
||||||
def save_binary_file(content: str, file_name: str, accepted_types: list[str]) -> str:
|
|
||||||
"""
|
"""
|
||||||
Save a binary file to the specified folder.
|
Abstract base class for a cache.
|
||||||
|
|
||||||
Args:
|
|
||||||
content: The content of the file as a bytes object.
|
|
||||||
file_name: The name of the file, including its extension.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The path to the saved file.
|
|
||||||
"""
|
"""
|
||||||
if not any(file_name.endswith(suffix) for suffix in accepted_types):
|
|
||||||
raise ValueError(f"File {file_name} is not accepted")
|
|
||||||
|
|
||||||
# Get the destination folder
|
@abc.abstractmethod
|
||||||
cache_path = Path(tempfile.gettempdir()) / PREFIX
|
def get(self, key):
|
||||||
if not content:
|
"""
|
||||||
raise ValueError("Please, reload the file in the loader.")
|
Retrieve an item from the cache.
|
||||||
data = content.split(",")[1]
|
|
||||||
decoded_bytes = base64.b64decode(data)
|
|
||||||
|
|
||||||
# Create the full file path
|
Args:
|
||||||
file_path = os.path.join(cache_path, file_name)
|
key: The key of the item to retrieve.
|
||||||
|
|
||||||
# Save the binary content to the file
|
Returns:
|
||||||
with open(file_path, "wb") as file:
|
The value associated with the key, or None if the key is not found.
|
||||||
file.write(decoded_bytes)
|
"""
|
||||||
|
|
||||||
return file_path
|
@abc.abstractmethod
|
||||||
|
def set(self, key, value):
|
||||||
|
"""
|
||||||
|
Add an item to the cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item.
|
||||||
|
value: The value to cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def delete(self, key):
|
||||||
|
"""
|
||||||
|
Remove an item from the cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item to remove.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def clear(self):
|
||||||
|
"""
|
||||||
|
Clear all items from the cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __contains__(self, key):
|
||||||
|
"""
|
||||||
|
Check if the key is in the cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the key is in the cache, False otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __getitem__(self, key):
|
||||||
|
"""
|
||||||
|
Retrieve an item from the cache using the square bracket notation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item to retrieve.
|
||||||
|
|
||||||
|
|
||||||
@create_cache_folder
|
@abc.abstractmethod
|
||||||
def save_uploaded_file(file, file_name):
|
def __setitem__(self, key, value):
|
||||||
cache_path = Path(tempfile.gettempdir()) / PREFIX
|
"""
|
||||||
file_path = cache_path / file_name
|
Add an item to the cache using the square bracket notation.
|
||||||
|
|
||||||
with open(file_path, "wb") as new_file:
|
Args:
|
||||||
# Iterate over the uploaded file in small chunks to conserve memory
|
key: The key of the item.
|
||||||
while chunk := file.read(8192): # Read 8KB at a time (adjust as needed)
|
value: The value to cache.
|
||||||
new_file.write(chunk)
|
"""
|
||||||
|
|
||||||
return file_path
|
@abc.abstractmethod
|
||||||
|
def __delitem__(self, key):
|
||||||
|
"""
|
||||||
|
Remove an item from the cache using the square bracket notation.
|
||||||
|
|
||||||
|
Args:
|
||||||
@create_cache_folder
|
key: The key of the item to remove.
|
||||||
def save_cache(hash_val: str, chat_data, clean_old_cache_files: bool):
|
"""
|
||||||
cache_path = Path(tempfile.gettempdir()) / PREFIX / f"{hash_val}.dill"
|
|
||||||
with cache_path.open("wb") as cache_file:
|
|
||||||
dill.dump(chat_data, cache_file)
|
|
||||||
|
|
||||||
if clean_old_cache_files:
|
|
||||||
clear_old_cache_files()
|
|
||||||
|
|
||||||
|
|
||||||
@create_cache_folder
|
|
||||||
def load_cache(hash_val):
|
|
||||||
cache_path = Path(tempfile.gettempdir()) / PREFIX / f"{hash_val}.dill"
|
|
||||||
if cache_path.exists():
|
|
||||||
with cache_path.open("rb") as cache_file:
|
|
||||||
return dill.load(cache_file)
|
|
||||||
return None
|
|
||||||
|
|
|
||||||
146
src/backend/langflow/cache/flow.py
vendored
Normal file
146
src/backend/langflow/cache/flow.py
vendored
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from langflow.cache.base import BaseCache
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryCache(BaseCache):
|
||||||
|
"""
|
||||||
|
A simple in-memory cache using an OrderedDict.
|
||||||
|
|
||||||
|
This cache supports setting a maximum size and expiration time for cached items.
|
||||||
|
When the cache is full, it uses a Least Recently Used (LRU) eviction policy.
|
||||||
|
Thread-safe using a threading Lock.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
max_size (int, optional): Maximum number of items to store in the cache.
|
||||||
|
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
cache = InMemoryCache(max_size=3, expiration_time=5)
|
||||||
|
|
||||||
|
# setting cache values
|
||||||
|
cache.set("a", 1)
|
||||||
|
cache.set("b", 2)
|
||||||
|
cache["c"] = 3
|
||||||
|
|
||||||
|
# getting cache values
|
||||||
|
a = cache.get("a")
|
||||||
|
b = cache["b"]
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, max_size=None, expiration_time=60 * 60):
|
||||||
|
"""
|
||||||
|
Initialize a new InMemoryCache instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_size (int, optional): Maximum number of items to store in the cache.
|
||||||
|
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
|
||||||
|
"""
|
||||||
|
self._cache = OrderedDict()
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self.max_size = max_size
|
||||||
|
self.expiration_time = expiration_time
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
"""
|
||||||
|
Retrieve an item from the cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item to retrieve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The value associated with the key, or None if the key is not found or the item has expired.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if key in self._cache:
|
||||||
|
item = self._cache.pop(key)
|
||||||
|
if (
|
||||||
|
self.expiration_time is None
|
||||||
|
or time.time() - item["time"] < self.expiration_time
|
||||||
|
):
|
||||||
|
# Move the key to the end to make it recently used
|
||||||
|
self._cache[key] = item
|
||||||
|
return item["value"]
|
||||||
|
else:
|
||||||
|
self.delete(key)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
"""
|
||||||
|
Add an item to the cache.
|
||||||
|
|
||||||
|
If the cache is full, the least recently used item is evicted.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item.
|
||||||
|
value: The value to cache.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if key in self._cache:
|
||||||
|
# Remove existing key before re-inserting to update order
|
||||||
|
self.delete(key)
|
||||||
|
elif self.max_size and len(self._cache) >= self.max_size:
|
||||||
|
# Remove least recently used item
|
||||||
|
self._cache.popitem(last=False)
|
||||||
|
self._cache[key] = {"value": value, "time": time.time()}
|
||||||
|
|
||||||
|
def get_or_set(self, key, value):
|
||||||
|
"""
|
||||||
|
Retrieve an item from the cache. If the item does not exist, set it with the provided value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item.
|
||||||
|
value: The value to cache if the item doesn't exist.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The cached value associated with the key.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if key in self._cache:
|
||||||
|
return self.get(key)
|
||||||
|
self.set(key, value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def delete(self, key):
|
||||||
|
"""
|
||||||
|
Remove an item from the cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The key of the item to remove.
|
||||||
|
"""
|
||||||
|
# with self._lock:
|
||||||
|
self._cache.pop(key, None)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""
|
||||||
|
Clear all items from the cache.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
self._cache.clear()
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
"""Check if the key is in the cache."""
|
||||||
|
return key in self._cache
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
"""Retrieve an item from the cache using the square bracket notation."""
|
||||||
|
return self.get(key)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
"""Add an item to the cache using the square bracket notation."""
|
||||||
|
self.set(key, value)
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
"""Remove an item from the cache using the square bracket notation."""
|
||||||
|
self.delete(key)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""Return the number of items in the cache."""
|
||||||
|
return len(self._cache)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""Return a string representation of the InMemoryCache instance."""
|
||||||
|
return f"InMemoryCache(max_size={self.max_size}, expiration_time={self.expiration_time})"
|
||||||
6
src/backend/langflow/cache/manager.py
vendored
6
src/backend/langflow/cache/manager.py
vendored
|
|
@ -54,7 +54,7 @@ class CacheManager(Subject):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.CACHE = {}
|
self._cache = {}
|
||||||
self.current_client_id = None
|
self.current_client_id = None
|
||||||
self.current_cache = {}
|
self.current_cache = {}
|
||||||
|
|
||||||
|
|
@ -68,12 +68,12 @@ class CacheManager(Subject):
|
||||||
"""
|
"""
|
||||||
previous_client_id = self.current_client_id
|
previous_client_id = self.current_client_id
|
||||||
self.current_client_id = client_id
|
self.current_client_id = client_id
|
||||||
self.current_cache = self.CACHE.setdefault(client_id, {})
|
self.current_cache = self._cache.setdefault(client_id, {})
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
self.current_client_id = previous_client_id
|
self.current_client_id = previous_client_id
|
||||||
self.current_cache = self.CACHE.get(self.current_client_id, {})
|
self.current_cache = self._cache.get(self.current_client_id, {})
|
||||||
|
|
||||||
def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None):
|
def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
167
src/backend/langflow/cache/utils.py
vendored
Normal file
167
src/backend/langflow/cache/utils.py
vendored
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
import base64
|
||||||
|
import contextlib
|
||||||
|
import functools
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from collections import OrderedDict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
|
CACHE: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def create_cache_folder(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# Get the destination folder
|
||||||
|
cache_path = Path(tempfile.gettempdir()) / PREFIX
|
||||||
|
|
||||||
|
# Create the destination folder if it doesn't exist
|
||||||
|
os.makedirs(cache_path, exist_ok=True)
|
||||||
|
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def memoize_dict(maxsize=128):
|
||||||
|
cache = OrderedDict()
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
hashed = compute_dict_hash(args[0])
|
||||||
|
key = (func.__name__, hashed, frozenset(kwargs.items()))
|
||||||
|
if key not in cache:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
cache[key] = result
|
||||||
|
if len(cache) > maxsize:
|
||||||
|
cache.popitem(last=False)
|
||||||
|
else:
|
||||||
|
result = cache[key]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def clear_cache():
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
wrapper.clear_cache = clear_cache # type: ignore
|
||||||
|
wrapper.cache = cache # type: ignore
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
PREFIX = "langflow_cache"
|
||||||
|
|
||||||
|
|
||||||
|
@create_cache_folder
|
||||||
|
def clear_old_cache_files(max_cache_size: int = 3):
|
||||||
|
cache_dir = Path(tempfile.gettempdir()) / PREFIX
|
||||||
|
cache_files = list(cache_dir.glob("*.dill"))
|
||||||
|
|
||||||
|
if len(cache_files) > max_cache_size:
|
||||||
|
cache_files_sorted_by_mtime = sorted(
|
||||||
|
cache_files, key=lambda x: x.stat().st_mtime, reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for cache_file in cache_files_sorted_by_mtime[max_cache_size:]:
|
||||||
|
with contextlib.suppress(OSError):
|
||||||
|
os.remove(cache_file)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_dict_hash(graph_data):
|
||||||
|
graph_data = filter_json(graph_data)
|
||||||
|
|
||||||
|
cleaned_graph_json = json.dumps(graph_data, sort_keys=True)
|
||||||
|
return hashlib.sha256(cleaned_graph_json.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def filter_json(json_data):
|
||||||
|
filtered_data = json_data.copy()
|
||||||
|
|
||||||
|
# Remove 'viewport' and 'chatHistory' keys
|
||||||
|
if "viewport" in filtered_data:
|
||||||
|
del filtered_data["viewport"]
|
||||||
|
if "chatHistory" in filtered_data:
|
||||||
|
del filtered_data["chatHistory"]
|
||||||
|
|
||||||
|
# Filter nodes
|
||||||
|
if "nodes" in filtered_data:
|
||||||
|
for node in filtered_data["nodes"]:
|
||||||
|
if "position" in node:
|
||||||
|
del node["position"]
|
||||||
|
if "positionAbsolute" in node:
|
||||||
|
del node["positionAbsolute"]
|
||||||
|
if "selected" in node:
|
||||||
|
del node["selected"]
|
||||||
|
if "dragging" in node:
|
||||||
|
del node["dragging"]
|
||||||
|
|
||||||
|
return filtered_data
|
||||||
|
|
||||||
|
|
||||||
|
@create_cache_folder
|
||||||
|
def save_binary_file(content: str, file_name: str, accepted_types: list[str]) -> str:
|
||||||
|
"""
|
||||||
|
Save a binary file to the specified folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The content of the file as a bytes object.
|
||||||
|
file_name: The name of the file, including its extension.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The path to the saved file.
|
||||||
|
"""
|
||||||
|
if not any(file_name.endswith(suffix) for suffix in accepted_types):
|
||||||
|
raise ValueError(f"File {file_name} is not accepted")
|
||||||
|
|
||||||
|
# Get the destination folder
|
||||||
|
cache_path = Path(tempfile.gettempdir()) / PREFIX
|
||||||
|
if not content:
|
||||||
|
raise ValueError("Please, reload the file in the loader.")
|
||||||
|
data = content.split(",")[1]
|
||||||
|
decoded_bytes = base64.b64decode(data)
|
||||||
|
|
||||||
|
# Create the full file path
|
||||||
|
file_path = os.path.join(cache_path, file_name)
|
||||||
|
|
||||||
|
# Save the binary content to the file
|
||||||
|
with open(file_path, "wb") as file:
|
||||||
|
file.write(decoded_bytes)
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@create_cache_folder
|
||||||
|
def save_uploaded_file(file, file_name):
|
||||||
|
cache_path = Path(tempfile.gettempdir()) / PREFIX
|
||||||
|
file_path = cache_path / file_name
|
||||||
|
|
||||||
|
with open(file_path, "wb") as new_file:
|
||||||
|
# Iterate over the uploaded file in small chunks to conserve memory
|
||||||
|
while chunk := file.read(8192): # Read 8KB at a time (adjust as needed)
|
||||||
|
new_file.write(chunk)
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
|
@create_cache_folder
|
||||||
|
def save_cache(hash_val: str, chat_data, clean_old_cache_files: bool):
|
||||||
|
cache_path = Path(tempfile.gettempdir()) / PREFIX / f"{hash_val}.dill"
|
||||||
|
with cache_path.open("wb") as cache_file:
|
||||||
|
dill.dump(chat_data, cache_file)
|
||||||
|
|
||||||
|
if clean_old_cache_files:
|
||||||
|
clear_old_cache_files()
|
||||||
|
|
||||||
|
|
||||||
|
@create_cache_folder
|
||||||
|
def load_cache(hash_val):
|
||||||
|
cache_path = Path(tempfile.gettempdir()) / PREFIX / f"{hash_val}.dill"
|
||||||
|
if cache_path.exists():
|
||||||
|
with cache_path.open("rb") as cache_file:
|
||||||
|
return dill.load(cache_file)
|
||||||
|
return None
|
||||||
0
src/backend/langflow/chat/__init__.py
Normal file
0
src/backend/langflow/chat/__init__.py
Normal file
|
|
@ -1,21 +1,20 @@
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
from fastapi import WebSocket, status
|
from fastapi import WebSocket, status
|
||||||
|
from langflow.api.v1.schemas import ChatMessage, ChatResponse, FileResponse
|
||||||
from langflow.api.schemas import ChatMessage, ChatResponse, FileResponse
|
|
||||||
from langflow.cache import cache_manager
|
from langflow.cache import cache_manager
|
||||||
from langflow.cache.manager import Subject
|
from langflow.cache.manager import Subject
|
||||||
from langflow.interface.run import (
|
from langflow.chat.utils import process_graph
|
||||||
get_result_and_steps,
|
from langflow.interface.utils import pil_to_base64
|
||||||
load_or_build_langchain_object,
|
|
||||||
)
|
|
||||||
from langflow.interface.utils import pil_to_base64, try_setting_streaming_options
|
|
||||||
from langflow.utils.logger import logger
|
from langflow.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from langflow.cache.flow import InMemoryCache
|
||||||
|
|
||||||
|
|
||||||
class ChatHistory(Subject):
|
class ChatHistory(Subject):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
@ -49,6 +48,7 @@ class ChatManager:
|
||||||
self.chat_history = ChatHistory()
|
self.chat_history = ChatHistory()
|
||||||
self.cache_manager = cache_manager
|
self.cache_manager = cache_manager
|
||||||
self.cache_manager.attach(self.update)
|
self.cache_manager.attach(self.update)
|
||||||
|
self.in_memory_cache = InMemoryCache()
|
||||||
|
|
||||||
def on_chat_history_update(self):
|
def on_chat_history_update(self):
|
||||||
"""Send the last chat message to the client."""
|
"""Send the last chat message to the client."""
|
||||||
|
|
@ -102,24 +102,30 @@ class ChatManager:
|
||||||
websocket = self.active_connections[client_id]
|
websocket = self.active_connections[client_id]
|
||||||
await websocket.send_json(message.dict())
|
await websocket.send_json(message.dict())
|
||||||
|
|
||||||
async def process_message(self, client_id: str, payload: Dict):
|
async def close_connection(self, client_id: str, code: int, reason: str):
|
||||||
|
if websocket := self.active_connections[client_id]:
|
||||||
|
await websocket.close(code=code, reason=reason)
|
||||||
|
self.disconnect(client_id)
|
||||||
|
|
||||||
|
async def process_message(
|
||||||
|
self, client_id: str, payload: Dict, langchain_object: Any
|
||||||
|
):
|
||||||
# Process the graph data and chat message
|
# Process the graph data and chat message
|
||||||
chat_message = payload.pop("message", "")
|
chat_message = payload.pop("message", "")
|
||||||
chat_message = ChatMessage(message=chat_message)
|
chat_message = ChatMessage(message=chat_message)
|
||||||
self.chat_history.add_message(client_id, chat_message)
|
self.chat_history.add_message(client_id, chat_message)
|
||||||
|
|
||||||
graph_data = payload
|
# graph_data = payload
|
||||||
start_resp = ChatResponse(message=None, type="start", intermediate_steps="")
|
start_resp = ChatResponse(message=None, type="start", intermediate_steps="")
|
||||||
await self.send_json(client_id, start_resp)
|
await self.send_json(client_id, start_resp)
|
||||||
|
|
||||||
is_first_message = len(self.chat_history.get_history(client_id=client_id)) <= 1
|
# is_first_message = len(self.chat_history.get_history(client_id=client_id)) <= 1
|
||||||
# Generate result and thought
|
# Generate result and thought
|
||||||
try:
|
try:
|
||||||
logger.debug("Generating result and thought")
|
logger.debug("Generating result and thought")
|
||||||
|
|
||||||
result, intermediate_steps = await process_graph(
|
result, intermediate_steps = await process_graph(
|
||||||
graph_data=graph_data,
|
langchain_object=langchain_object,
|
||||||
is_first_message=is_first_message,
|
|
||||||
chat_message=chat_message,
|
chat_message=chat_message,
|
||||||
websocket=self.active_connections[client_id],
|
websocket=self.active_connections[client_id],
|
||||||
)
|
)
|
||||||
|
|
@ -152,6 +158,14 @@ class ChatManager:
|
||||||
await self.send_json(client_id, response)
|
await self.send_json(client_id, response)
|
||||||
self.chat_history.add_message(client_id, response)
|
self.chat_history.add_message(client_id, response)
|
||||||
|
|
||||||
|
def set_cache(self, client_id: str, langchain_object: Any) -> bool:
|
||||||
|
"""
|
||||||
|
Set the cache for a client.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.in_memory_cache.set(client_id, langchain_object)
|
||||||
|
return client_id in self.in_memory_cache
|
||||||
|
|
||||||
async def handle_websocket(self, client_id: str, websocket: WebSocket):
|
async def handle_websocket(self, client_id: str, websocket: WebSocket):
|
||||||
await self.connect(client_id, websocket)
|
await self.connect(client_id, websocket)
|
||||||
|
|
||||||
|
|
@ -172,52 +186,24 @@ class ChatManager:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
with self.cache_manager.set_client_id(client_id):
|
with self.cache_manager.set_client_id(client_id):
|
||||||
await self.process_message(client_id, payload)
|
langchain_object = self.in_memory_cache.get(client_id)
|
||||||
|
await self.process_message(client_id, payload, langchain_object)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle any exceptions that might occur
|
# Handle any exceptions that might occur
|
||||||
logger.exception(e)
|
logger.error(e)
|
||||||
# send a message to the client
|
await self.close_connection(
|
||||||
await self.active_connections[client_id].close(
|
client_id=client_id,
|
||||||
code=status.WS_1011_INTERNAL_ERROR, reason=str(e)[:120]
|
code=status.WS_1011_INTERNAL_ERROR,
|
||||||
|
reason=str(e)[:120],
|
||||||
)
|
)
|
||||||
self.disconnect(client_id)
|
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
connection = self.active_connections.get(client_id)
|
await self.close_connection(
|
||||||
if connection:
|
client_id=client_id,
|
||||||
await connection.close(code=1000, reason="Client disconnected")
|
code=status.WS_1000_NORMAL_CLOSURE,
|
||||||
self.disconnect(client_id)
|
reason="Client disconnected",
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.error(e)
|
||||||
self.disconnect(client_id)
|
self.disconnect(client_id)
|
||||||
|
|
||||||
|
|
||||||
async def process_graph(
|
|
||||||
graph_data: Dict,
|
|
||||||
is_first_message: bool,
|
|
||||||
chat_message: ChatMessage,
|
|
||||||
websocket: WebSocket,
|
|
||||||
):
|
|
||||||
langchain_object = load_or_build_langchain_object(graph_data, is_first_message)
|
|
||||||
langchain_object = try_setting_streaming_options(langchain_object, websocket)
|
|
||||||
logger.debug("Loaded langchain object")
|
|
||||||
|
|
||||||
if langchain_object is None:
|
|
||||||
# Raise user facing error
|
|
||||||
raise ValueError(
|
|
||||||
"There was an error loading the langchain_object. Please, check all the nodes and try again."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate result and thought
|
|
||||||
try:
|
|
||||||
logger.debug("Generating result and thought")
|
|
||||||
result, intermediate_steps = await get_result_and_steps(
|
|
||||||
langchain_object, chat_message.message or "", websocket=websocket
|
|
||||||
)
|
|
||||||
logger.debug("Generated result and intermediate_steps")
|
|
||||||
return result, intermediate_steps
|
|
||||||
except Exception as e:
|
|
||||||
# Log stack trace
|
|
||||||
logger.exception(e)
|
|
||||||
raise e
|
|
||||||
33
src/backend/langflow/chat/utils.py
Normal file
33
src/backend/langflow/chat/utils.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
from fastapi import WebSocket
|
||||||
|
from langflow.api.v1.schemas import ChatMessage
|
||||||
|
from langflow.processing.base import get_result_and_steps
|
||||||
|
from langflow.interface.utils import try_setting_streaming_options
|
||||||
|
from langflow.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
async def process_graph(
|
||||||
|
langchain_object,
|
||||||
|
chat_message: ChatMessage,
|
||||||
|
websocket: WebSocket,
|
||||||
|
):
|
||||||
|
langchain_object = try_setting_streaming_options(langchain_object, websocket)
|
||||||
|
logger.debug("Loaded langchain object")
|
||||||
|
|
||||||
|
if langchain_object is None:
|
||||||
|
# Raise user facing error
|
||||||
|
raise ValueError(
|
||||||
|
"There was an error loading the langchain_object. Please, check all the nodes and try again."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate result and thought
|
||||||
|
try:
|
||||||
|
logger.debug("Generating result and thought")
|
||||||
|
result, intermediate_steps = await get_result_and_steps(
|
||||||
|
langchain_object, chat_message.message or "", websocket=websocket
|
||||||
|
)
|
||||||
|
logger.debug("Generated result and intermediate_steps")
|
||||||
|
return result, intermediate_steps
|
||||||
|
except Exception as e:
|
||||||
|
# Log stack trace
|
||||||
|
logger.exception(e)
|
||||||
|
raise e
|
||||||
|
|
@ -3,7 +3,7 @@ agents:
|
||||||
- ZeroShotAgent
|
- ZeroShotAgent
|
||||||
- JsonAgent
|
- JsonAgent
|
||||||
- CSVAgent
|
- CSVAgent
|
||||||
- initialize_agent
|
- AgentInitializer
|
||||||
- VectorStoreAgent
|
- VectorStoreAgent
|
||||||
- VectorStoreRouterAgent
|
- VectorStoreRouterAgent
|
||||||
- SQLAgent
|
- SQLAgent
|
||||||
|
|
@ -16,6 +16,10 @@ chains:
|
||||||
- MidJourneyPromptChain
|
- MidJourneyPromptChain
|
||||||
- TimeTravelGuideChain
|
- TimeTravelGuideChain
|
||||||
- SQLDatabaseChain
|
- SQLDatabaseChain
|
||||||
|
- RetrievalQA
|
||||||
|
- RetrievalQAWithSourcesChain
|
||||||
|
- ConversationalRetrievalChain
|
||||||
|
- CombineDocsChain
|
||||||
documentloaders:
|
documentloaders:
|
||||||
- AirbyteJSONLoader
|
- AirbyteJSONLoader
|
||||||
- CoNLLULoader
|
- CoNLLULoader
|
||||||
|
|
@ -51,10 +55,14 @@ embeddings:
|
||||||
llms:
|
llms:
|
||||||
- OpenAI
|
- OpenAI
|
||||||
# - AzureOpenAI
|
# - AzureOpenAI
|
||||||
|
# - AzureChatOpenAI
|
||||||
- ChatOpenAI
|
- ChatOpenAI
|
||||||
- LlamaCpp
|
- LlamaCpp
|
||||||
- CTransformers
|
- CTransformers
|
||||||
- Cohere
|
- Cohere
|
||||||
|
- Anthropic
|
||||||
|
- ChatAnthropic
|
||||||
|
- HuggingFaceHub
|
||||||
memories:
|
memories:
|
||||||
- ConversationBufferMemory
|
- ConversationBufferMemory
|
||||||
- ConversationSummaryMemory
|
- ConversationSummaryMemory
|
||||||
|
|
@ -73,12 +81,14 @@ toolkits:
|
||||||
- JsonToolkit
|
- JsonToolkit
|
||||||
- VectorStoreInfo
|
- VectorStoreInfo
|
||||||
- VectorStoreRouterToolkit
|
- VectorStoreRouterToolkit
|
||||||
|
- VectorStoreToolkit
|
||||||
tools:
|
tools:
|
||||||
- Search
|
- Search
|
||||||
- PAL-MATH
|
- PAL-MATH
|
||||||
- Calculator
|
- Calculator
|
||||||
- Serper Search
|
- Serper Search
|
||||||
- Tool
|
- Tool
|
||||||
|
- PythonFunctionTool
|
||||||
- PythonFunction
|
- PythonFunction
|
||||||
- JsonSpec
|
- JsonSpec
|
||||||
- News API
|
- News API
|
||||||
|
|
@ -118,6 +128,7 @@ vectorstores:
|
||||||
- Chroma
|
- Chroma
|
||||||
- Qdrant
|
- Qdrant
|
||||||
- Weaviate
|
- Weaviate
|
||||||
|
- FAISS
|
||||||
wrappers:
|
wrappers:
|
||||||
- RequestsWrapper
|
- RequestsWrapper
|
||||||
# - ChatPromptTemplate
|
# - ChatPromptTemplate
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,18 @@ from langflow.template import frontend_node
|
||||||
|
|
||||||
# These should always be instantiated
|
# These should always be instantiated
|
||||||
CUSTOM_NODES = {
|
CUSTOM_NODES = {
|
||||||
"prompts": {"ZeroShotPrompt": frontend_node.prompts.ZeroShotPromptNode()},
|
"prompts": {
|
||||||
|
"ZeroShotPrompt": frontend_node.prompts.ZeroShotPromptNode(),
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"PythonFunctionTool": frontend_node.tools.PythonFunctionToolNode(),
|
||||||
"PythonFunction": frontend_node.tools.PythonFunctionNode(),
|
"PythonFunction": frontend_node.tools.PythonFunctionNode(),
|
||||||
"Tool": frontend_node.tools.ToolNode(),
|
"Tool": frontend_node.tools.ToolNode(),
|
||||||
},
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"JsonAgent": frontend_node.agents.JsonAgentNode(),
|
"JsonAgent": frontend_node.agents.JsonAgentNode(),
|
||||||
"CSVAgent": frontend_node.agents.CSVAgentNode(),
|
"CSVAgent": frontend_node.agents.CSVAgentNode(),
|
||||||
"initialize_agent": frontend_node.agents.InitializeAgentNode(),
|
"AgentInitializer": frontend_node.agents.InitializeAgentNode(),
|
||||||
"VectorStoreAgent": frontend_node.agents.VectorStoreAgentNode(),
|
"VectorStoreAgent": frontend_node.agents.VectorStoreAgentNode(),
|
||||||
"VectorStoreRouterAgent": frontend_node.agents.VectorStoreRouterAgentNode(),
|
"VectorStoreRouterAgent": frontend_node.agents.VectorStoreRouterAgentNode(),
|
||||||
"SQLAgent": frontend_node.agents.SQLAgentNode(),
|
"SQLAgent": frontend_node.agents.SQLAgentNode(),
|
||||||
|
|
@ -22,6 +25,7 @@ CUSTOM_NODES = {
|
||||||
"SeriesCharacterChain": frontend_node.chains.SeriesCharacterChainNode(),
|
"SeriesCharacterChain": frontend_node.chains.SeriesCharacterChainNode(),
|
||||||
"TimeTravelGuideChain": frontend_node.chains.TimeTravelGuideChainNode(),
|
"TimeTravelGuideChain": frontend_node.chains.TimeTravelGuideChainNode(),
|
||||||
"MidJourneyPromptChain": frontend_node.chains.MidJourneyPromptChainNode(),
|
"MidJourneyPromptChain": frontend_node.chains.MidJourneyPromptChainNode(),
|
||||||
|
"load_qa_chain": frontend_node.chains.CombineDocsChainNode(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
0
src/backend/langflow/database/__init__.py
Normal file
0
src/backend/langflow/database/__init__.py
Normal file
18
src/backend/langflow/database/base.py
Normal file
18
src/backend/langflow/database/base.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from langflow.settings import settings
|
||||||
|
from sqlmodel import SQLModel, Session, create_engine
|
||||||
|
|
||||||
|
|
||||||
|
if settings.database_url.startswith("sqlite"):
|
||||||
|
connect_args = {"check_same_thread": False}
|
||||||
|
else:
|
||||||
|
connect_args = {}
|
||||||
|
engine = create_engine(settings.database_url, connect_args=connect_args)
|
||||||
|
|
||||||
|
|
||||||
|
def create_db_and_tables():
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
||||||
|
def get_session():
|
||||||
|
with Session(engine) as session:
|
||||||
|
yield session
|
||||||
0
src/backend/langflow/database/models/__init__.py
Normal file
0
src/backend/langflow/database/models/__init__.py
Normal file
14
src/backend/langflow/database/models/base.py
Normal file
14
src/backend/langflow/database/models/base.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
|
||||||
|
def orjson_dumps(v, *, default):
|
||||||
|
# orjson.dumps returns bytes, to match standard json.dumps we need to decode
|
||||||
|
return orjson.dumps(v, default=default).decode()
|
||||||
|
|
||||||
|
|
||||||
|
class SQLModelSerializable(SQLModel):
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
json_loads = orjson.loads
|
||||||
|
json_dumps = orjson_dumps
|
||||||
60
src/backend/langflow/database/models/flow.py
Normal file
60
src/backend/langflow/database/models/flow.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Path: src/backend/langflow/database/models/flow.py
|
||||||
|
|
||||||
|
from langflow.database.models.base import SQLModelSerializable
|
||||||
|
from pydantic import validator
|
||||||
|
from sqlmodel import Field, Relationship, JSON, Column
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
# if TYPE_CHECKING:
|
||||||
|
from langflow.database.models.flow_style import FlowStyle, FlowStyleRead
|
||||||
|
|
||||||
|
|
||||||
|
class FlowBase(SQLModelSerializable):
|
||||||
|
name: str = Field(index=True)
|
||||||
|
description: Optional[str] = Field(index=True)
|
||||||
|
data: Optional[Dict] = Field(default=None)
|
||||||
|
|
||||||
|
@validator("data")
|
||||||
|
def validate_json(v):
|
||||||
|
# dict_keys(['description', 'name', 'id', 'data'])
|
||||||
|
if not v:
|
||||||
|
return v
|
||||||
|
if not isinstance(v, dict):
|
||||||
|
raise ValueError("Flow must be a valid JSON")
|
||||||
|
|
||||||
|
# data must contain nodes and edges
|
||||||
|
if "nodes" not in v.keys():
|
||||||
|
raise ValueError("Flow must have nodes")
|
||||||
|
if "edges" not in v.keys():
|
||||||
|
raise ValueError("Flow must have edges")
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class Flow(FlowBase, table=True):
|
||||||
|
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||||
|
data: Optional[Dict] = Field(default=None, sa_column=Column(JSON))
|
||||||
|
style: Optional["FlowStyle"] = Relationship(
|
||||||
|
back_populates="flow",
|
||||||
|
# use "uselist=False" to make it a one-to-one relationship
|
||||||
|
sa_relationship_kwargs={"uselist": False},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FlowCreate(FlowBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FlowRead(FlowBase):
|
||||||
|
id: UUID
|
||||||
|
|
||||||
|
|
||||||
|
class FlowReadWithStyle(FlowRead):
|
||||||
|
style: Optional["FlowStyleRead"] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FlowUpdate(SQLModelSerializable):
|
||||||
|
name: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
data: Optional[Dict] = None
|
||||||
33
src/backend/langflow/database/models/flow_style.py
Normal file
33
src/backend/langflow/database/models/flow_style.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Path: src/backend/langflow/database/models/flowstyle.py
|
||||||
|
|
||||||
|
from langflow.database.models.base import SQLModelSerializable
|
||||||
|
from sqlmodel import Field, Relationship
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from langflow.database.models.flow import Flow
|
||||||
|
|
||||||
|
|
||||||
|
class FlowStyleBase(SQLModelSerializable):
|
||||||
|
color: str
|
||||||
|
emoji: str
|
||||||
|
flow_id: UUID = Field(default=None, foreign_key="flow.id")
|
||||||
|
|
||||||
|
|
||||||
|
class FlowStyle(FlowStyleBase, table=True):
|
||||||
|
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||||
|
flow: "Flow" = Relationship(back_populates="style")
|
||||||
|
|
||||||
|
|
||||||
|
class FlowStyleUpdate(SQLModelSerializable):
|
||||||
|
color: Optional[str] = None
|
||||||
|
emoji: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FlowStyleCreate(FlowStyleBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FlowStyleRead(FlowStyleBase):
|
||||||
|
id: UUID
|
||||||
|
|
@ -1,4 +1,35 @@
|
||||||
from langflow.graph.base import Edge, Node
|
from langflow.graph.edge.base import Edge
|
||||||
from langflow.graph.graph import Graph
|
from langflow.graph.graph.base import Graph
|
||||||
|
from langflow.graph.vertex.base import Vertex
|
||||||
|
from langflow.graph.vertex.types import (
|
||||||
|
AgentVertex,
|
||||||
|
ChainVertex,
|
||||||
|
DocumentLoaderVertex,
|
||||||
|
EmbeddingVertex,
|
||||||
|
LLMVertex,
|
||||||
|
MemoryVertex,
|
||||||
|
PromptVertex,
|
||||||
|
TextSplitterVertex,
|
||||||
|
ToolVertex,
|
||||||
|
ToolkitVertex,
|
||||||
|
VectorStoreVertex,
|
||||||
|
WrapperVertex,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = ["Graph", "Node", "Edge"]
|
__all__ = [
|
||||||
|
"Graph",
|
||||||
|
"Vertex",
|
||||||
|
"Edge",
|
||||||
|
"AgentVertex",
|
||||||
|
"ChainVertex",
|
||||||
|
"DocumentLoaderVertex",
|
||||||
|
"EmbeddingVertex",
|
||||||
|
"LLMVertex",
|
||||||
|
"MemoryVertex",
|
||||||
|
"PromptVertex",
|
||||||
|
"TextSplitterVertex",
|
||||||
|
"ToolVertex",
|
||||||
|
"ToolkitVertex",
|
||||||
|
"VectorStoreVertex",
|
||||||
|
"WrapperVertex",
|
||||||
|
]
|
||||||
|
|
|
||||||
0
src/backend/langflow/graph/edge/__init__.py
Normal file
0
src/backend/langflow/graph/edge/__init__.py
Normal file
52
src/backend/langflow/graph/edge/base.py
Normal file
52
src/backend/langflow/graph/edge/base.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
from langflow.utils.logger import logger
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from langflow.graph.vertex.base import Vertex
|
||||||
|
|
||||||
|
|
||||||
|
class Edge:
|
||||||
|
def __init__(self, source: "Vertex", target: "Vertex"):
|
||||||
|
self.source: "Vertex" = source
|
||||||
|
self.target: "Vertex" = target
|
||||||
|
self.validate_edge()
|
||||||
|
|
||||||
|
def validate_edge(self) -> None:
|
||||||
|
# Validate that the outputs of the source node are valid inputs
|
||||||
|
# for the target node
|
||||||
|
self.source_types = self.source.output
|
||||||
|
self.target_reqs = self.target.required_inputs + self.target.optional_inputs
|
||||||
|
# Both lists contain strings and sometimes a string contains the value we are
|
||||||
|
# looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"]
|
||||||
|
# so we need to check if any of the strings in source_types is in target_reqs
|
||||||
|
self.valid = any(
|
||||||
|
output in target_req
|
||||||
|
for output in self.source_types
|
||||||
|
for target_req in self.target_reqs
|
||||||
|
)
|
||||||
|
# Get what type of input the target node is expecting
|
||||||
|
|
||||||
|
self.matched_type = next(
|
||||||
|
(
|
||||||
|
output
|
||||||
|
for output in self.source_types
|
||||||
|
for target_req in self.target_reqs
|
||||||
|
if output in target_req
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
no_matched_type = self.matched_type is None
|
||||||
|
if no_matched_type:
|
||||||
|
logger.debug(self.source_types)
|
||||||
|
logger.debug(self.target_reqs)
|
||||||
|
if no_matched_type:
|
||||||
|
raise ValueError(
|
||||||
|
f"Edge between {self.source.vertex_type} and {self.target.vertex_type} "
|
||||||
|
f"has no matched type"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"Edge(source={self.source.id}, target={self.target.id}, valid={self.valid}"
|
||||||
|
f", matched_type={self.matched_type})"
|
||||||
|
)
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
from typing import Dict, List, Type, Union
|
|
||||||
|
|
||||||
from langflow.graph.base import Edge, Node
|
|
||||||
from langflow.graph.nodes import (
|
|
||||||
AgentNode,
|
|
||||||
ChainNode,
|
|
||||||
DocumentLoaderNode,
|
|
||||||
EmbeddingNode,
|
|
||||||
FileToolNode,
|
|
||||||
LLMNode,
|
|
||||||
MemoryNode,
|
|
||||||
PromptNode,
|
|
||||||
TextSplitterNode,
|
|
||||||
ToolkitNode,
|
|
||||||
ToolNode,
|
|
||||||
VectorStoreNode,
|
|
||||||
WrapperNode,
|
|
||||||
)
|
|
||||||
from langflow.interface.agents.base import agent_creator
|
|
||||||
from langflow.interface.chains.base import chain_creator
|
|
||||||
from langflow.interface.document_loaders.base import documentloader_creator
|
|
||||||
from langflow.interface.embeddings.base import embedding_creator
|
|
||||||
from langflow.interface.llms.base import llm_creator
|
|
||||||
from langflow.interface.memories.base import memory_creator
|
|
||||||
from langflow.interface.prompts.base import prompt_creator
|
|
||||||
from langflow.interface.text_splitters.base import textsplitter_creator
|
|
||||||
from langflow.interface.toolkits.base import toolkits_creator
|
|
||||||
from langflow.interface.tools.base import tool_creator
|
|
||||||
from langflow.interface.tools.constants import FILE_TOOLS
|
|
||||||
from langflow.interface.vector_store.base import vectorstore_creator
|
|
||||||
from langflow.interface.wrappers.base import wrapper_creator
|
|
||||||
from langflow.utils import payload
|
|
||||||
|
|
||||||
|
|
||||||
class Graph:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
nodes: List[Dict[str, Union[str, Dict[str, Union[str, List[str]]]]]],
|
|
||||||
edges: List[Dict[str, str]],
|
|
||||||
) -> None:
|
|
||||||
self._nodes = nodes
|
|
||||||
self._edges = edges
|
|
||||||
self._build_graph()
|
|
||||||
|
|
||||||
def _build_graph(self) -> None:
|
|
||||||
self.nodes = self._build_nodes()
|
|
||||||
self.edges = self._build_edges()
|
|
||||||
for edge in self.edges:
|
|
||||||
edge.source.add_edge(edge)
|
|
||||||
edge.target.add_edge(edge)
|
|
||||||
|
|
||||||
# This is a hack to make sure that the LLM node is sent to
|
|
||||||
# the toolkit node
|
|
||||||
llm_node = None
|
|
||||||
for node in self.nodes:
|
|
||||||
node._build_params()
|
|
||||||
|
|
||||||
if isinstance(node, LLMNode):
|
|
||||||
llm_node = node
|
|
||||||
|
|
||||||
for node in self.nodes:
|
|
||||||
if isinstance(node, ToolkitNode):
|
|
||||||
node.params["llm"] = llm_node
|
|
||||||
# remove invalid nodes
|
|
||||||
self.nodes = [
|
|
||||||
node
|
|
||||||
for node in self.nodes
|
|
||||||
if self._validate_node(node)
|
|
||||||
or (len(self.nodes) == 1 and len(self.edges) == 0)
|
|
||||||
]
|
|
||||||
|
|
||||||
def _validate_node(self, node: Node) -> bool:
|
|
||||||
# All nodes that do not have edges are invalid
|
|
||||||
return len(node.edges) > 0
|
|
||||||
|
|
||||||
def get_node(self, node_id: str) -> Union[None, Node]:
|
|
||||||
return next((node for node in self.nodes if node.id == node_id), None)
|
|
||||||
|
|
||||||
def get_nodes_with_target(self, node: Node) -> List[Node]:
|
|
||||||
connected_nodes: List[Node] = [
|
|
||||||
edge.source for edge in self.edges if edge.target == node
|
|
||||||
]
|
|
||||||
return connected_nodes
|
|
||||||
|
|
||||||
def build(self) -> List[Node]:
|
|
||||||
# Get root node
|
|
||||||
root_node = payload.get_root_node(self)
|
|
||||||
if root_node is None:
|
|
||||||
raise ValueError("No root node found")
|
|
||||||
return root_node.build()
|
|
||||||
|
|
||||||
def get_node_neighbors(self, node: Node) -> Dict[Node, int]:
|
|
||||||
neighbors: Dict[Node, int] = {}
|
|
||||||
for edge in self.edges:
|
|
||||||
if edge.source == node:
|
|
||||||
neighbor = edge.target
|
|
||||||
if neighbor not in neighbors:
|
|
||||||
neighbors[neighbor] = 0
|
|
||||||
neighbors[neighbor] += 1
|
|
||||||
elif edge.target == node:
|
|
||||||
neighbor = edge.source
|
|
||||||
if neighbor not in neighbors:
|
|
||||||
neighbors[neighbor] = 0
|
|
||||||
neighbors[neighbor] += 1
|
|
||||||
return neighbors
|
|
||||||
|
|
||||||
def _build_edges(self) -> List[Edge]:
|
|
||||||
# Edge takes two nodes as arguments, so we need to build the nodes first
|
|
||||||
# and then build the edges
|
|
||||||
# if we can't find a node, we raise an error
|
|
||||||
|
|
||||||
edges: List[Edge] = []
|
|
||||||
for edge in self._edges:
|
|
||||||
source = self.get_node(edge["source"])
|
|
||||||
target = self.get_node(edge["target"])
|
|
||||||
if source is None:
|
|
||||||
raise ValueError(f"Source node {edge['source']} not found")
|
|
||||||
if target is None:
|
|
||||||
raise ValueError(f"Target node {edge['target']} not found")
|
|
||||||
edges.append(Edge(source, target))
|
|
||||||
return edges
|
|
||||||
|
|
||||||
def _get_node_class(self, node_type: str, node_lc_type: str) -> Type[Node]:
|
|
||||||
node_type_map: Dict[str, Type[Node]] = {
|
|
||||||
**{t: PromptNode for t in prompt_creator.to_list()},
|
|
||||||
**{t: AgentNode for t in agent_creator.to_list()},
|
|
||||||
**{t: ChainNode for t in chain_creator.to_list()},
|
|
||||||
**{t: ToolNode for t in tool_creator.to_list()},
|
|
||||||
**{t: ToolkitNode for t in toolkits_creator.to_list()},
|
|
||||||
**{t: WrapperNode for t in wrapper_creator.to_list()},
|
|
||||||
**{t: LLMNode for t in llm_creator.to_list()},
|
|
||||||
**{t: MemoryNode for t in memory_creator.to_list()},
|
|
||||||
**{t: EmbeddingNode for t in embedding_creator.to_list()},
|
|
||||||
**{t: VectorStoreNode for t in vectorstore_creator.to_list()},
|
|
||||||
**{t: DocumentLoaderNode for t in documentloader_creator.to_list()},
|
|
||||||
**{t: TextSplitterNode for t in textsplitter_creator.to_list()},
|
|
||||||
}
|
|
||||||
|
|
||||||
if node_type in FILE_TOOLS:
|
|
||||||
return FileToolNode
|
|
||||||
if node_type in node_type_map:
|
|
||||||
return node_type_map[node_type]
|
|
||||||
if node_lc_type in node_type_map:
|
|
||||||
return node_type_map[node_lc_type]
|
|
||||||
return Node
|
|
||||||
|
|
||||||
def _build_nodes(self) -> List[Node]:
|
|
||||||
nodes: List[Node] = []
|
|
||||||
for node in self._nodes:
|
|
||||||
node_data = node["data"]
|
|
||||||
node_type: str = node_data["type"] # type: ignore
|
|
||||||
node_lc_type: str = node_data["node"]["template"]["_type"] # type: ignore
|
|
||||||
|
|
||||||
NodeClass = self._get_node_class(node_type, node_lc_type)
|
|
||||||
nodes.append(NodeClass(node))
|
|
||||||
|
|
||||||
return nodes
|
|
||||||
|
|
||||||
def get_children_by_node_type(self, node: Node, node_type: str) -> List[Node]:
|
|
||||||
children = []
|
|
||||||
node_types = [node.data["type"]]
|
|
||||||
if "node" in node.data:
|
|
||||||
node_types += node.data["node"]["base_classes"]
|
|
||||||
if node_type in node_types:
|
|
||||||
children.append(node)
|
|
||||||
return children
|
|
||||||
0
src/backend/langflow/graph/graph/__init__.py
Normal file
0
src/backend/langflow/graph/graph/__init__.py
Normal file
216
src/backend/langflow/graph/graph/base.py
Normal file
216
src/backend/langflow/graph/graph/base.py
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
from typing import Dict, Generator, List, Type, Union
|
||||||
|
|
||||||
|
from langflow.graph.edge.base import Edge
|
||||||
|
from langflow.graph.graph.constants import VERTEX_TYPE_MAP
|
||||||
|
from langflow.graph.vertex.base import Vertex
|
||||||
|
from langflow.graph.vertex.types import (
|
||||||
|
FileToolVertex,
|
||||||
|
LLMVertex,
|
||||||
|
ToolkitVertex,
|
||||||
|
)
|
||||||
|
from langflow.interface.tools.constants import FILE_TOOLS
|
||||||
|
from langflow.utils import payload
|
||||||
|
from langflow.utils.logger import logger
|
||||||
|
from langchain.chains.base import Chain
|
||||||
|
|
||||||
|
|
||||||
|
class Graph:
|
||||||
|
"""A class representing a graph of nodes and edges."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
nodes: List[Dict[str, Union[str, Dict[str, Union[str, List[str]]]]]],
|
||||||
|
edges: List[Dict[str, str]],
|
||||||
|
) -> None:
|
||||||
|
self._nodes = nodes
|
||||||
|
self._edges = edges
|
||||||
|
self._build_graph()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_payload(cls, payload: Dict) -> "Graph":
|
||||||
|
"""
|
||||||
|
Creates a graph from a payload.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
payload (Dict): The payload to create the graph from.˜`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Graph: The created graph.
|
||||||
|
"""
|
||||||
|
if "data" in payload:
|
||||||
|
payload = payload["data"]
|
||||||
|
try:
|
||||||
|
nodes = payload["nodes"]
|
||||||
|
edges = payload["edges"]
|
||||||
|
return cls(nodes, edges)
|
||||||
|
except KeyError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid payload. Expected keys 'nodes' and 'edges'. Found {list(payload.keys())}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
def _build_graph(self) -> None:
|
||||||
|
"""Builds the graph from the nodes and edges."""
|
||||||
|
self.nodes = self._build_vertices()
|
||||||
|
self.edges = self._build_edges()
|
||||||
|
for edge in self.edges:
|
||||||
|
edge.source.add_edge(edge)
|
||||||
|
edge.target.add_edge(edge)
|
||||||
|
|
||||||
|
# This is a hack to make sure that the LLM node is sent to
|
||||||
|
# the toolkit node
|
||||||
|
self._build_node_params()
|
||||||
|
# remove invalid nodes
|
||||||
|
self._remove_invalid_nodes()
|
||||||
|
|
||||||
|
def _build_node_params(self) -> None:
|
||||||
|
"""Identifies and handles the LLM node within the graph."""
|
||||||
|
llm_node = None
|
||||||
|
for node in self.nodes:
|
||||||
|
node._build_params()
|
||||||
|
if isinstance(node, LLMVertex):
|
||||||
|
llm_node = node
|
||||||
|
|
||||||
|
if llm_node:
|
||||||
|
for node in self.nodes:
|
||||||
|
if isinstance(node, ToolkitVertex):
|
||||||
|
node.params["llm"] = llm_node
|
||||||
|
|
||||||
|
def _remove_invalid_nodes(self) -> None:
|
||||||
|
"""Removes invalid nodes from the graph."""
|
||||||
|
self.nodes = [
|
||||||
|
node
|
||||||
|
for node in self.nodes
|
||||||
|
if self._validate_node(node)
|
||||||
|
or (len(self.nodes) == 1 and len(self.edges) == 0)
|
||||||
|
]
|
||||||
|
|
||||||
|
def _validate_node(self, node: Vertex) -> bool:
|
||||||
|
"""Validates a node."""
|
||||||
|
# All nodes that do not have edges are invalid
|
||||||
|
return len(node.edges) > 0
|
||||||
|
|
||||||
|
def get_node(self, node_id: str) -> Union[None, Vertex]:
|
||||||
|
"""Returns a node by id."""
|
||||||
|
return next((node for node in self.nodes if node.id == node_id), None)
|
||||||
|
|
||||||
|
def get_nodes_with_target(self, node: Vertex) -> List[Vertex]:
|
||||||
|
"""Returns the nodes connected to a node."""
|
||||||
|
connected_nodes: List[Vertex] = [
|
||||||
|
edge.source for edge in self.edges if edge.target == node
|
||||||
|
]
|
||||||
|
return connected_nodes
|
||||||
|
|
||||||
|
def build(self) -> Chain:
|
||||||
|
"""Builds the graph."""
|
||||||
|
# Get root node
|
||||||
|
root_node = payload.get_root_node(self)
|
||||||
|
if root_node is None:
|
||||||
|
raise ValueError("No root node found")
|
||||||
|
return root_node.build()
|
||||||
|
|
||||||
|
def topological_sort(self) -> List[Vertex]:
|
||||||
|
"""
|
||||||
|
Performs a topological sort of the vertices in the graph.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Vertex]: A list of vertices in topological order.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the graph contains a cycle.
|
||||||
|
"""
|
||||||
|
# States: 0 = unvisited, 1 = visiting, 2 = visited
|
||||||
|
state = {node: 0 for node in self.nodes}
|
||||||
|
sorted_vertices = []
|
||||||
|
|
||||||
|
def dfs(node):
|
||||||
|
if state[node] == 1:
|
||||||
|
# We have a cycle
|
||||||
|
raise ValueError(
|
||||||
|
"Graph contains a cycle, cannot perform topological sort"
|
||||||
|
)
|
||||||
|
if state[node] == 0:
|
||||||
|
state[node] = 1
|
||||||
|
for edge in node.edges:
|
||||||
|
if edge.source == node:
|
||||||
|
dfs(edge.target)
|
||||||
|
state[node] = 2
|
||||||
|
sorted_vertices.append(node)
|
||||||
|
|
||||||
|
# Visit each node
|
||||||
|
for node in self.nodes:
|
||||||
|
if state[node] == 0:
|
||||||
|
dfs(node)
|
||||||
|
|
||||||
|
return list(reversed(sorted_vertices))
|
||||||
|
|
||||||
|
def generator_build(self) -> Generator:
|
||||||
|
"""Builds each vertex in the graph and yields it."""
|
||||||
|
sorted_vertices = self.topological_sort()
|
||||||
|
logger.info("Sorted vertices: %s", sorted_vertices)
|
||||||
|
yield from sorted_vertices
|
||||||
|
|
||||||
|
def get_node_neighbors(self, node: Vertex) -> Dict[Vertex, int]:
|
||||||
|
"""Returns the neighbors of a node."""
|
||||||
|
neighbors: Dict[Vertex, int] = {}
|
||||||
|
for edge in self.edges:
|
||||||
|
if edge.source == node:
|
||||||
|
neighbor = edge.target
|
||||||
|
if neighbor not in neighbors:
|
||||||
|
neighbors[neighbor] = 0
|
||||||
|
neighbors[neighbor] += 1
|
||||||
|
elif edge.target == node:
|
||||||
|
neighbor = edge.source
|
||||||
|
if neighbor not in neighbors:
|
||||||
|
neighbors[neighbor] = 0
|
||||||
|
neighbors[neighbor] += 1
|
||||||
|
return neighbors
|
||||||
|
|
||||||
|
def _build_edges(self) -> List[Edge]:
|
||||||
|
"""Builds the edges of the graph."""
|
||||||
|
# Edge takes two nodes as arguments, so we need to build the nodes first
|
||||||
|
# and then build the edges
|
||||||
|
# if we can't find a node, we raise an error
|
||||||
|
|
||||||
|
edges: List[Edge] = []
|
||||||
|
for edge in self._edges:
|
||||||
|
source = self.get_node(edge["source"])
|
||||||
|
target = self.get_node(edge["target"])
|
||||||
|
if source is None:
|
||||||
|
raise ValueError(f"Source node {edge['source']} not found")
|
||||||
|
if target is None:
|
||||||
|
raise ValueError(f"Target node {edge['target']} not found")
|
||||||
|
edges.append(Edge(source, target))
|
||||||
|
return edges
|
||||||
|
|
||||||
|
def _get_vertex_class(self, node_type: str, node_lc_type: str) -> Type[Vertex]:
|
||||||
|
"""Returns the node class based on the node type."""
|
||||||
|
if node_type in FILE_TOOLS:
|
||||||
|
return FileToolVertex
|
||||||
|
if node_type in VERTEX_TYPE_MAP:
|
||||||
|
return VERTEX_TYPE_MAP[node_type]
|
||||||
|
return (
|
||||||
|
VERTEX_TYPE_MAP[node_lc_type] if node_lc_type in VERTEX_TYPE_MAP else Vertex
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_vertices(self) -> List[Vertex]:
|
||||||
|
"""Builds the vertices of the graph."""
|
||||||
|
nodes: List[Vertex] = []
|
||||||
|
for node in self._nodes:
|
||||||
|
node_data = node["data"]
|
||||||
|
node_type: str = node_data["type"] # type: ignore
|
||||||
|
node_lc_type: str = node_data["node"]["template"]["_type"] # type: ignore
|
||||||
|
|
||||||
|
VertexClass = self._get_vertex_class(node_type, node_lc_type)
|
||||||
|
nodes.append(VertexClass(node))
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
def get_children_by_node_type(self, node: Vertex, node_type: str) -> List[Vertex]:
|
||||||
|
"""Returns the children of a node based on the node type."""
|
||||||
|
children = []
|
||||||
|
node_types = [node.data["type"]]
|
||||||
|
if "node" in node.data:
|
||||||
|
node_types += node.data["node"]["base_classes"]
|
||||||
|
if node_type in node_types:
|
||||||
|
children.append(node)
|
||||||
|
return children
|
||||||
49
src/backend/langflow/graph/graph/constants.py
Normal file
49
src/backend/langflow/graph/graph/constants.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
from langflow.graph.vertex.base import Vertex
|
||||||
|
from langflow.graph.vertex.types import (
|
||||||
|
AgentVertex,
|
||||||
|
ChainVertex,
|
||||||
|
DocumentLoaderVertex,
|
||||||
|
EmbeddingVertex,
|
||||||
|
LLMVertex,
|
||||||
|
MemoryVertex,
|
||||||
|
PromptVertex,
|
||||||
|
TextSplitterVertex,
|
||||||
|
ToolVertex,
|
||||||
|
ToolkitVertex,
|
||||||
|
VectorStoreVertex,
|
||||||
|
WrapperVertex,
|
||||||
|
)
|
||||||
|
from langflow.interface.agents.base import agent_creator
|
||||||
|
from langflow.interface.chains.base import chain_creator
|
||||||
|
from langflow.interface.document_loaders.base import documentloader_creator
|
||||||
|
from langflow.interface.embeddings.base import embedding_creator
|
||||||
|
from langflow.interface.llms.base import llm_creator
|
||||||
|
from langflow.interface.memories.base import memory_creator
|
||||||
|
from langflow.interface.prompts.base import prompt_creator
|
||||||
|
from langflow.interface.text_splitters.base import textsplitter_creator
|
||||||
|
from langflow.interface.toolkits.base import toolkits_creator
|
||||||
|
from langflow.interface.tools.base import tool_creator
|
||||||
|
from langflow.interface.vector_store.base import vectorstore_creator
|
||||||
|
from langflow.interface.wrappers.base import wrapper_creator
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Dict, Type
|
||||||
|
|
||||||
|
|
||||||
|
DIRECT_TYPES = ["str", "bool", "code", "int", "float", "Any", "prompt"]
|
||||||
|
|
||||||
|
|
||||||
|
VERTEX_TYPE_MAP: Dict[str, Type[Vertex]] = {
|
||||||
|
**{t: PromptVertex for t in prompt_creator.to_list()},
|
||||||
|
**{t: AgentVertex for t in agent_creator.to_list()},
|
||||||
|
**{t: ChainVertex for t in chain_creator.to_list()},
|
||||||
|
**{t: ToolVertex for t in tool_creator.to_list()},
|
||||||
|
**{t: ToolkitVertex for t in toolkits_creator.to_list()},
|
||||||
|
**{t: WrapperVertex for t in wrapper_creator.to_list()},
|
||||||
|
**{t: LLMVertex for t in llm_creator.to_list()},
|
||||||
|
**{t: MemoryVertex for t in memory_creator.to_list()},
|
||||||
|
**{t: EmbeddingVertex for t in embedding_creator.to_list()},
|
||||||
|
**{t: VectorStoreVertex for t in vectorstore_creator.to_list()},
|
||||||
|
**{t: DocumentLoaderVertex for t in documentloader_creator.to_list()},
|
||||||
|
**{t: TextSplitterVertex for t in textsplitter_creator.to_list()},
|
||||||
|
}
|
||||||
0
src/backend/langflow/graph/graph/utils.py
Normal file
0
src/backend/langflow/graph/graph/utils.py
Normal file
|
|
@ -1,4 +1,6 @@
|
||||||
import re
|
from typing import Any, Union
|
||||||
|
|
||||||
|
from langflow.interface.utils import extract_input_variables_from_prompt
|
||||||
|
|
||||||
|
|
||||||
def validate_prompt(prompt: str):
|
def validate_prompt(prompt: str):
|
||||||
|
|
@ -14,6 +16,12 @@ def fix_prompt(prompt: str):
|
||||||
return prompt + " {input}"
|
return prompt + " {input}"
|
||||||
|
|
||||||
|
|
||||||
def extract_input_variables_from_prompt(prompt: str) -> list[str]:
|
def flatten_list(list_of_lists: list[Union[list, Any]]) -> list:
|
||||||
"""Extract input variables from prompt."""
|
"""Flatten list of lists."""
|
||||||
return re.findall(r"{(.*?)}", prompt)
|
new_list = []
|
||||||
|
for item in list_of_lists:
|
||||||
|
if isinstance(item, list):
|
||||||
|
new_list.extend(item)
|
||||||
|
else:
|
||||||
|
new_list.append(item)
|
||||||
|
return new_list
|
||||||
|
|
|
||||||
0
src/backend/langflow/graph/vertex/__init__.py
Normal file
0
src/backend/langflow/graph/vertex/__init__.py
Normal file
|
|
@ -1,26 +1,27 @@
|
||||||
# Description: Graph class for building a graph of nodes and edges
|
from langflow.cache import utils as cache_utils
|
||||||
# Insights:
|
from langflow.graph.vertex.constants import DIRECT_TYPES
|
||||||
# - Defer prompts building to the last moment or when they have all the tools
|
|
||||||
# - Build each inner agent first, then build the outer agent
|
|
||||||
|
|
||||||
import contextlib
|
|
||||||
import inspect
|
|
||||||
import types
|
|
||||||
import warnings
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
|
|
||||||
from langflow.graph.constants import DIRECT_TYPES
|
|
||||||
from langflow.interface import loading
|
from langflow.interface import loading
|
||||||
from langflow.interface.listing import ALL_TYPES_DICT
|
from langflow.interface.listing import ALL_TYPES_DICT
|
||||||
from langflow.utils.logger import logger
|
from langflow.utils.logger import logger
|
||||||
from langflow.utils.util import sync_to_async
|
from langflow.utils.util import sync_to_async
|
||||||
|
|
||||||
|
|
||||||
class Node:
|
import contextlib
|
||||||
|
import inspect
|
||||||
|
import types
|
||||||
|
import warnings
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from langflow.graph.edge.base import Edge
|
||||||
|
|
||||||
|
|
||||||
|
class Vertex:
|
||||||
def __init__(self, data: Dict, base_type: Optional[str] = None) -> None:
|
def __init__(self, data: Dict, base_type: Optional[str] = None) -> None:
|
||||||
self.id: str = data["id"]
|
self.id: str = data["id"]
|
||||||
self._data = data
|
self._data = data
|
||||||
self.edges: List[Edge] = []
|
self.edges: List["Edge"] = []
|
||||||
self.base_type: Optional[str] = base_type
|
self.base_type: Optional[str] = base_type
|
||||||
self._parse_data()
|
self._parse_data()
|
||||||
self._built_object = None
|
self._built_object = None
|
||||||
|
|
@ -47,12 +48,15 @@ class Node:
|
||||||
]
|
]
|
||||||
|
|
||||||
template_dict = self.data["node"]["template"]
|
template_dict = self.data["node"]["template"]
|
||||||
self.node_type = (
|
self.vertex_type = (
|
||||||
self.data["type"] if "Tool" not in self.output else template_dict["_type"]
|
self.data["type"]
|
||||||
|
if "Tool" not in self.output or template_dict["_type"].islower()
|
||||||
|
else template_dict["_type"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.base_type is None:
|
if self.base_type is None:
|
||||||
for base_type, value in ALL_TYPES_DICT.items():
|
for base_type, value in ALL_TYPES_DICT.items():
|
||||||
if self.node_type in value:
|
if self.vertex_type in value:
|
||||||
self.base_type = base_type
|
self.base_type = base_type
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -107,7 +111,7 @@ class Node:
|
||||||
if value["required"] and not edges:
|
if value["required"] and not edges:
|
||||||
# If a required parameter is not found, raise an error
|
# If a required parameter is not found, raise an error
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Required input {key} for module {self.node_type} not found"
|
f"Required input {key} for module {self.vertex_type} not found"
|
||||||
)
|
)
|
||||||
elif value["list"]:
|
elif value["list"]:
|
||||||
# If this is a list parameter, append all sources to a list
|
# If this is a list parameter, append all sources to a list
|
||||||
|
|
@ -122,7 +126,7 @@ class Node:
|
||||||
# so we need to check if value has value
|
# so we need to check if value has value
|
||||||
new_value = value.get("value")
|
new_value = value.get("value")
|
||||||
if new_value is None:
|
if new_value is None:
|
||||||
warnings.warn(f"Value for {key} in {self.node_type} is None. ")
|
warnings.warn(f"Value for {key} in {self.vertex_type} is None. ")
|
||||||
if value.get("type") == "int":
|
if value.get("type") == "int":
|
||||||
with contextlib.suppress(TypeError, ValueError):
|
with contextlib.suppress(TypeError, ValueError):
|
||||||
new_value = int(new_value) # type: ignore
|
new_value = int(new_value) # type: ignore
|
||||||
|
|
@ -142,12 +146,12 @@ class Node:
|
||||||
# and continue
|
# and continue
|
||||||
# Another aspect is that the node_type is the class that we need to import
|
# Another aspect is that the node_type is the class that we need to import
|
||||||
# and instantiate with these built params
|
# and instantiate with these built params
|
||||||
logger.debug(f"Building {self.node_type}")
|
logger.debug(f"Building {self.vertex_type}")
|
||||||
# Build each node in the params dict
|
# Build each node in the params dict
|
||||||
for key, value in self.params.copy().items():
|
for key, value in self.params.copy().items():
|
||||||
# Check if Node or list of Nodes and not self
|
# Check if Node or list of Nodes and not self
|
||||||
# to avoid recursion
|
# to avoid recursion
|
||||||
if isinstance(value, Node):
|
if isinstance(value, Vertex):
|
||||||
if value == self:
|
if value == self:
|
||||||
del self.params[key]
|
del self.params[key]
|
||||||
continue
|
continue
|
||||||
|
|
@ -168,10 +172,16 @@ class Node:
|
||||||
# turn result which is a function into a coroutine
|
# turn result which is a function into a coroutine
|
||||||
# so that it can be awaited
|
# so that it can be awaited
|
||||||
self.params["coroutine"] = sync_to_async(result)
|
self.params["coroutine"] = sync_to_async(result)
|
||||||
|
if isinstance(result, list):
|
||||||
|
# If the result is a list, then we need to extend the list
|
||||||
|
# with the result but first check if the key exists
|
||||||
|
# if it doesn't, then we need to create a new list
|
||||||
|
if isinstance(self.params[key], list):
|
||||||
|
self.params[key].extend(result)
|
||||||
|
|
||||||
self.params[key] = result
|
self.params[key] = result
|
||||||
elif isinstance(value, list) and all(
|
elif isinstance(value, list) and all(
|
||||||
isinstance(node, Node) for node in value
|
isinstance(node, Vertex) for node in value
|
||||||
):
|
):
|
||||||
self.params[key] = []
|
self.params[key] = []
|
||||||
for node in value:
|
for node in value:
|
||||||
|
|
@ -187,17 +197,17 @@ class Node:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._built_object = loading.instantiate_class(
|
self._built_object = loading.instantiate_class(
|
||||||
node_type=self.node_type,
|
node_type=self.vertex_type,
|
||||||
base_type=self.base_type,
|
base_type=self.base_type,
|
||||||
params=self.params,
|
params=self.params,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Error building node {self.node_type}: {str(exc)}"
|
f"Error building node {self.vertex_type}: {str(exc)}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
if self._built_object is None:
|
if self._built_object is None:
|
||||||
raise ValueError(f"Node type {self.node_type} not found")
|
raise ValueError(f"Node type {self.vertex_type} not found")
|
||||||
|
|
||||||
self._built = True
|
self._built = True
|
||||||
|
|
||||||
|
|
@ -214,57 +224,10 @@ class Node:
|
||||||
return f"Node(id={self.id}, data={self.data})"
|
return f"Node(id={self.id}, data={self.data})"
|
||||||
|
|
||||||
def __eq__(self, __o: object) -> bool:
|
def __eq__(self, __o: object) -> bool:
|
||||||
return self.id == __o.id if isinstance(__o, Node) else False
|
return self.id == __o.id if isinstance(__o, Vertex) else False
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return id(self)
|
return id(self)
|
||||||
|
|
||||||
def _built_object_repr(self):
|
def _built_object_repr(self):
|
||||||
return repr(self._built_object)
|
return repr(self._built_object)
|
||||||
|
|
||||||
|
|
||||||
class Edge:
|
|
||||||
def __init__(self, source: "Node", target: "Node"):
|
|
||||||
self.source: "Node" = source
|
|
||||||
self.target: "Node" = target
|
|
||||||
self.validate_edge()
|
|
||||||
|
|
||||||
def validate_edge(self) -> None:
|
|
||||||
# Validate that the outputs of the source node are valid inputs
|
|
||||||
# for the target node
|
|
||||||
self.source_types = self.source.output
|
|
||||||
self.target_reqs = self.target.required_inputs + self.target.optional_inputs
|
|
||||||
# Both lists contain strings and sometimes a string contains the value we are
|
|
||||||
# looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"]
|
|
||||||
# so we need to check if any of the strings in source_types is in target_reqs
|
|
||||||
self.valid = any(
|
|
||||||
output in target_req
|
|
||||||
for output in self.source_types
|
|
||||||
for target_req in self.target_reqs
|
|
||||||
)
|
|
||||||
# Get what type of input the target node is expecting
|
|
||||||
|
|
||||||
self.matched_type = next(
|
|
||||||
(
|
|
||||||
output
|
|
||||||
for output in self.source_types
|
|
||||||
for target_req in self.target_reqs
|
|
||||||
if output in target_req
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
no_matched_type = self.matched_type is None
|
|
||||||
if no_matched_type:
|
|
||||||
logger.debug(self.source_types)
|
|
||||||
logger.debug(self.target_reqs)
|
|
||||||
if no_matched_type:
|
|
||||||
raise ValueError(
|
|
||||||
f"Edge between {self.source.node_type} and {self.target.node_type} "
|
|
||||||
f"has no matched type"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
f"Edge(source={self.source.id}, target={self.target.id}, valid={self.valid}"
|
|
||||||
f", matched_type={self.matched_type})"
|
|
||||||
)
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
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.vertex.base import Vertex
|
||||||
from langflow.graph.utils import extract_input_variables_from_prompt
|
from langflow.graph.utils import flatten_list
|
||||||
|
from langflow.interface.utils import extract_input_variables_from_prompt
|
||||||
|
|
||||||
|
|
||||||
class AgentNode(Node):
|
class AgentVertex(Vertex):
|
||||||
def __init__(self, data: Dict):
|
def __init__(self, data: Dict):
|
||||||
super().__init__(data, base_type="agents")
|
super().__init__(data, base_type="agents")
|
||||||
|
|
||||||
self.tools: List[ToolNode] = []
|
self.tools: List[Union[ToolkitVertex, ToolVertex]] = []
|
||||||
self.chains: List[ChainNode] = []
|
self.chains: List[ChainVertex] = []
|
||||||
|
|
||||||
def _set_tools_and_chains(self) -> None:
|
def _set_tools_and_chains(self) -> None:
|
||||||
for edge in self.edges:
|
for edge in self.edges:
|
||||||
source_node = edge.source
|
source_node = edge.source
|
||||||
if isinstance(source_node, ToolNode):
|
if isinstance(source_node, (ToolVertex, ToolkitVertex)):
|
||||||
self.tools.append(source_node)
|
self.tools.append(source_node)
|
||||||
elif isinstance(source_node, ChainNode):
|
elif isinstance(source_node, ChainVertex):
|
||||||
self.chains.append(source_node)
|
self.chains.append(source_node)
|
||||||
|
|
||||||
def build(self, force: bool = False) -> Any:
|
def build(self, force: bool = False) -> Any:
|
||||||
|
|
@ -32,25 +33,130 @@ class AgentNode(Node):
|
||||||
|
|
||||||
self._build()
|
self._build()
|
||||||
|
|
||||||
#! Cannot deepcopy VectorStore, VectorStoreRouter, or SQL agents
|
|
||||||
if self.node_type in ["VectorStoreAgent", "VectorStoreRouterAgent", "SQLAgent"]:
|
|
||||||
return self._built_object
|
|
||||||
return self._built_object
|
return self._built_object
|
||||||
|
|
||||||
|
|
||||||
class ToolNode(Node):
|
class ToolVertex(Vertex):
|
||||||
def __init__(self, data: Dict):
|
def __init__(self, data: Dict):
|
||||||
super().__init__(data, base_type="tools")
|
super().__init__(data, base_type="tools")
|
||||||
|
|
||||||
|
|
||||||
class PromptNode(Node):
|
class LLMVertex(Vertex):
|
||||||
|
built_node_type = None
|
||||||
|
class_built_object = None
|
||||||
|
|
||||||
|
def __init__(self, data: Dict):
|
||||||
|
super().__init__(data, base_type="llms")
|
||||||
|
|
||||||
|
def build(self, force: bool = False) -> Any:
|
||||||
|
# LLM is different because some models might take up too much memory
|
||||||
|
# or time to load. So we only load them when we need them.ß
|
||||||
|
if self.vertex_type == self.built_node_type:
|
||||||
|
return self.class_built_object
|
||||||
|
if not self._built or force:
|
||||||
|
self._build()
|
||||||
|
self.built_node_type = self.vertex_type
|
||||||
|
self.class_built_object = self._built_object
|
||||||
|
# Avoid deepcopying the LLM
|
||||||
|
# that are loaded from a file
|
||||||
|
return self._built_object
|
||||||
|
|
||||||
|
|
||||||
|
class ToolkitVertex(Vertex):
|
||||||
|
def __init__(self, data: Dict):
|
||||||
|
super().__init__(data, base_type="toolkits")
|
||||||
|
|
||||||
|
|
||||||
|
class FileToolVertex(ToolVertex):
|
||||||
|
def __init__(self, data: Dict):
|
||||||
|
super().__init__(data)
|
||||||
|
|
||||||
|
|
||||||
|
class WrapperVertex(Vertex):
|
||||||
|
def __init__(self, data: Dict):
|
||||||
|
super().__init__(data, base_type="wrappers")
|
||||||
|
|
||||||
|
def build(self, force: bool = False) -> Any:
|
||||||
|
if not self._built or force:
|
||||||
|
if "headers" in self.params:
|
||||||
|
self.params["headers"] = eval(self.params["headers"])
|
||||||
|
self._build()
|
||||||
|
return self._built_object
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentLoaderVertex(Vertex):
|
||||||
|
def __init__(self, data: Dict):
|
||||||
|
super().__init__(data, base_type="documentloaders")
|
||||||
|
|
||||||
|
def _built_object_repr(self):
|
||||||
|
# This built_object is a list of documents. Maybe we should
|
||||||
|
# show how many documents are in the list?
|
||||||
|
if self._built_object:
|
||||||
|
return f"""{self.vertex_type}({len(self._built_object)} documents)
|
||||||
|
Documents: {self._built_object[:3]}..."""
|
||||||
|
return f"{self.vertex_type}()"
|
||||||
|
|
||||||
|
|
||||||
|
class EmbeddingVertex(Vertex):
|
||||||
|
def __init__(self, data: Dict):
|
||||||
|
super().__init__(data, base_type="embeddings")
|
||||||
|
|
||||||
|
|
||||||
|
class VectorStoreVertex(Vertex):
|
||||||
|
def __init__(self, data: Dict):
|
||||||
|
super().__init__(data, base_type="vectorstores")
|
||||||
|
|
||||||
|
def _built_object_repr(self):
|
||||||
|
return "Vector stores can take time to build. It will build on the first query."
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryVertex(Vertex):
|
||||||
|
def __init__(self, data: Dict):
|
||||||
|
super().__init__(data, base_type="memory")
|
||||||
|
|
||||||
|
|
||||||
|
class TextSplitterVertex(Vertex):
|
||||||
|
def __init__(self, data: Dict):
|
||||||
|
super().__init__(data, base_type="textsplitters")
|
||||||
|
|
||||||
|
def _built_object_repr(self):
|
||||||
|
# This built_object is a list of documents. Maybe we should
|
||||||
|
# show how many documents are in the list?
|
||||||
|
if self._built_object:
|
||||||
|
return f"""{self.vertex_type}({len(self._built_object)} documents)
|
||||||
|
\nDocuments: {self._built_object[:3]}..."""
|
||||||
|
return f"{self.vertex_type}()"
|
||||||
|
|
||||||
|
|
||||||
|
class ChainVertex(Vertex):
|
||||||
|
def __init__(self, data: Dict):
|
||||||
|
super().__init__(data, base_type="chains")
|
||||||
|
|
||||||
|
def build(
|
||||||
|
self,
|
||||||
|
force: bool = False,
|
||||||
|
tools: Optional[List[Union[ToolkitVertex, ToolVertex]]] = None,
|
||||||
|
) -> Any:
|
||||||
|
if not self._built or force:
|
||||||
|
# Check if the chain requires a PromptVertex
|
||||||
|
for key, value in self.params.items():
|
||||||
|
if isinstance(value, PromptVertex):
|
||||||
|
# Build the PromptVertex, passing the tools if available
|
||||||
|
self.params[key] = value.build(tools=tools, force=force)
|
||||||
|
|
||||||
|
self._build()
|
||||||
|
|
||||||
|
return self._built_object
|
||||||
|
|
||||||
|
|
||||||
|
class PromptVertex(Vertex):
|
||||||
def __init__(self, data: Dict):
|
def __init__(self, data: Dict):
|
||||||
super().__init__(data, base_type="prompts")
|
super().__init__(data, base_type="prompts")
|
||||||
|
|
||||||
def build(
|
def build(
|
||||||
self,
|
self,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
tools: Optional[Union[List[Node], List[ToolNode]]] = None,
|
tools: Optional[List[Union[ToolkitVertex, ToolVertex]]] = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if not self._built or force:
|
if not self._built or force:
|
||||||
if (
|
if (
|
||||||
|
|
@ -59,12 +165,16 @@ class PromptNode(Node):
|
||||||
):
|
):
|
||||||
self.params["input_variables"] = []
|
self.params["input_variables"] = []
|
||||||
# Check if it is a ZeroShotPrompt and needs a tool
|
# Check if it is a ZeroShotPrompt and needs a tool
|
||||||
if "ShotPrompt" in self.node_type:
|
if "ShotPrompt" in self.vertex_type:
|
||||||
tools = (
|
tools = (
|
||||||
[tool_node.build() for tool_node in tools]
|
[tool_node.build() for tool_node in tools]
|
||||||
if tools is not None
|
if tools is not None
|
||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
|
# flatten the list of tools if it is a list of lists
|
||||||
|
# first check if it is a list
|
||||||
|
if tools and isinstance(tools, list) and isinstance(tools[0], list):
|
||||||
|
tools = flatten_list(tools)
|
||||||
self.params["tools"] = tools
|
self.params["tools"] = tools
|
||||||
prompt_params = [
|
prompt_params = [
|
||||||
key
|
key
|
||||||
|
|
@ -81,113 +191,3 @@ class PromptNode(Node):
|
||||||
|
|
||||||
self._build()
|
self._build()
|
||||||
return self._built_object
|
return self._built_object
|
||||||
|
|
||||||
|
|
||||||
class ChainNode(Node):
|
|
||||||
def __init__(self, data: Dict):
|
|
||||||
super().__init__(data, base_type="chains")
|
|
||||||
|
|
||||||
def build(
|
|
||||||
self,
|
|
||||||
force: bool = False,
|
|
||||||
tools: Optional[Union[List[Node], List[ToolNode]]] = None,
|
|
||||||
) -> Any:
|
|
||||||
if not self._built or force:
|
|
||||||
# Check if the chain requires a PromptNode
|
|
||||||
for key, value in self.params.items():
|
|
||||||
if isinstance(value, PromptNode):
|
|
||||||
# Build the PromptNode, passing the tools if available
|
|
||||||
self.params[key] = value.build(tools=tools, force=force)
|
|
||||||
|
|
||||||
self._build()
|
|
||||||
|
|
||||||
#! Cannot deepcopy SQLDatabaseChain
|
|
||||||
if self.node_type in ["SQLDatabaseChain"]:
|
|
||||||
return self._built_object
|
|
||||||
return self._built_object
|
|
||||||
|
|
||||||
|
|
||||||
class LLMNode(Node):
|
|
||||||
built_node_type = None
|
|
||||||
class_built_object = None
|
|
||||||
|
|
||||||
def __init__(self, data: Dict):
|
|
||||||
super().__init__(data, base_type="llms")
|
|
||||||
|
|
||||||
def build(self, force: bool = False) -> Any:
|
|
||||||
# LLM is different because some models might take up too much memory
|
|
||||||
# or time to load. So we only load them when we need them.ß
|
|
||||||
if self.node_type == self.built_node_type:
|
|
||||||
return self.class_built_object
|
|
||||||
if not self._built or force:
|
|
||||||
self._build()
|
|
||||||
self.built_node_type = self.node_type
|
|
||||||
self.class_built_object = self._built_object
|
|
||||||
# Avoid deepcopying the LLM
|
|
||||||
# that are loaded from a file
|
|
||||||
return self._built_object
|
|
||||||
|
|
||||||
|
|
||||||
class ToolkitNode(Node):
|
|
||||||
def __init__(self, data: Dict):
|
|
||||||
super().__init__(data, base_type="toolkits")
|
|
||||||
|
|
||||||
|
|
||||||
class FileToolNode(ToolNode):
|
|
||||||
def __init__(self, data: Dict):
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
|
|
||||||
class WrapperNode(Node):
|
|
||||||
def __init__(self, data: Dict):
|
|
||||||
super().__init__(data, base_type="wrappers")
|
|
||||||
|
|
||||||
def build(self, force: bool = False) -> Any:
|
|
||||||
if not self._built or force:
|
|
||||||
if "headers" in self.params:
|
|
||||||
self.params["headers"] = eval(self.params["headers"])
|
|
||||||
self._build()
|
|
||||||
return self._built_object
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentLoaderNode(Node):
|
|
||||||
def __init__(self, data: Dict):
|
|
||||||
super().__init__(data, base_type="documentloaders")
|
|
||||||
|
|
||||||
def _built_object_repr(self):
|
|
||||||
# This built_object is a list of documents. Maybe we should
|
|
||||||
# show how many documents are in the list?
|
|
||||||
if self._built_object:
|
|
||||||
return f"""{self.node_type}({len(self._built_object)} documents)
|
|
||||||
Documents: {self._built_object[:3]}..."""
|
|
||||||
return f"{self.node_type}()"
|
|
||||||
|
|
||||||
|
|
||||||
class EmbeddingNode(Node):
|
|
||||||
def __init__(self, data: Dict):
|
|
||||||
super().__init__(data, base_type="embeddings")
|
|
||||||
|
|
||||||
|
|
||||||
class VectorStoreNode(Node):
|
|
||||||
def __init__(self, data: Dict):
|
|
||||||
super().__init__(data, base_type="vectorstores")
|
|
||||||
|
|
||||||
def _built_object_repr(self):
|
|
||||||
return "Vector stores can take time to build. It will build on the first query."
|
|
||||||
|
|
||||||
|
|
||||||
class MemoryNode(Node):
|
|
||||||
def __init__(self, data: Dict):
|
|
||||||
super().__init__(data, base_type="memory")
|
|
||||||
|
|
||||||
|
|
||||||
class TextSplitterNode(Node):
|
|
||||||
def __init__(self, data: Dict):
|
|
||||||
super().__init__(data, base_type="textsplitters")
|
|
||||||
|
|
||||||
def _built_object_repr(self):
|
|
||||||
# This built_object is a list of documents. Maybe we should
|
|
||||||
# show how many documents are in the list?
|
|
||||||
if self._built_object:
|
|
||||||
return f"""{self.node_type}({len(self._built_object)} documents)\nDocuments: {self._built_object[:3]}..."""
|
|
||||||
return f"{self.node_type}()"
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
from abc import ABC
|
|
||||||
from typing import Any, List, Optional
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
from langchain import LLMChain
|
from langchain import LLMChain
|
||||||
|
|
@ -33,24 +32,7 @@ from langchain.memory.chat_memory import BaseChatMemory
|
||||||
from langchain.sql_database import SQLDatabase
|
from langchain.sql_database import SQLDatabase
|
||||||
from langchain.tools.python.tool import PythonAstREPLTool
|
from langchain.tools.python.tool import PythonAstREPLTool
|
||||||
from langchain.tools.sql_database.prompt import QUERY_CHECKER
|
from langchain.tools.sql_database.prompt import QUERY_CHECKER
|
||||||
|
from langflow.interface.base import CustomAgentExecutor
|
||||||
|
|
||||||
class CustomAgentExecutor(AgentExecutor, ABC):
|
|
||||||
"""Custom agent executor"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def function_name():
|
|
||||||
return "CustomAgentExecutor"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def initialize(cls, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
|
||||||
return super().run(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class JsonAgent(CustomAgentExecutor):
|
class JsonAgent(CustomAgentExecutor):
|
||||||
|
|
@ -69,7 +51,7 @@ class JsonAgent(CustomAgentExecutor):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_toolkit_and_llm(cls, toolkit: JsonToolkit, llm: BaseLanguageModel):
|
def from_toolkit_and_llm(cls, toolkit: JsonToolkit, llm: BaseLanguageModel):
|
||||||
tools = toolkit.get_tools()
|
tools = toolkit if isinstance(toolkit, list) else toolkit.get_tools()
|
||||||
tool_names = {tool.name for tool in tools}
|
tool_names = {tool.name for tool in tools}
|
||||||
prompt = ZeroShotAgent.create_prompt(
|
prompt = ZeroShotAgent.create_prompt(
|
||||||
tools,
|
tools,
|
||||||
|
|
@ -82,7 +64,9 @@ class JsonAgent(CustomAgentExecutor):
|
||||||
llm=llm,
|
llm=llm,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
)
|
)
|
||||||
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names) # type: ignore
|
agent = ZeroShotAgent(
|
||||||
|
llm_chain=llm_chain, allowed_tools=tool_names # type: ignore
|
||||||
|
)
|
||||||
return cls.from_agent_and_tools(agent=agent, tools=tools, verbose=True)
|
return cls.from_agent_and_tools(agent=agent, tools=tools, verbose=True)
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
|
|
@ -129,7 +113,9 @@ class CSVAgent(CustomAgentExecutor):
|
||||||
prompt=partial_prompt,
|
prompt=partial_prompt,
|
||||||
)
|
)
|
||||||
tool_names = {tool.name for tool in tools}
|
tool_names = {tool.name for tool in tools}
|
||||||
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs) # type: ignore
|
agent = ZeroShotAgent(
|
||||||
|
llm_chain=llm_chain, allowed_tools=tool_names, **kwargs # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
return cls.from_agent_and_tools(agent=agent, tools=tools, verbose=True)
|
return cls.from_agent_and_tools(agent=agent, tools=tools, verbose=True)
|
||||||
|
|
||||||
|
|
@ -138,7 +124,7 @@ class CSVAgent(CustomAgentExecutor):
|
||||||
|
|
||||||
|
|
||||||
class VectorStoreAgent(CustomAgentExecutor):
|
class VectorStoreAgent(CustomAgentExecutor):
|
||||||
"""Vector Store agent"""
|
"""Vector store agent"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def function_name():
|
def function_name():
|
||||||
|
|
@ -166,7 +152,9 @@ class VectorStoreAgent(CustomAgentExecutor):
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
)
|
)
|
||||||
tool_names = {tool.name for tool in tools}
|
tool_names = {tool.name for tool in tools}
|
||||||
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs) # type: ignore
|
agent = ZeroShotAgent(
|
||||||
|
llm_chain=llm_chain, allowed_tools=tool_names, **kwargs # type: ignore
|
||||||
|
)
|
||||||
return AgentExecutor.from_agent_and_tools(
|
return AgentExecutor.from_agent_and_tools(
|
||||||
agent=agent, tools=tools, verbose=True
|
agent=agent, tools=tools, verbose=True
|
||||||
)
|
)
|
||||||
|
|
@ -193,7 +181,7 @@ class SQLAgent(CustomAgentExecutor):
|
||||||
def from_toolkit_and_llm(
|
def from_toolkit_and_llm(
|
||||||
cls, llm: BaseLanguageModel, database_uri: str, **kwargs: Any
|
cls, llm: BaseLanguageModel, database_uri: str, **kwargs: Any
|
||||||
):
|
):
|
||||||
"""Construct a sql agent from an LLM and tools."""
|
"""Construct an SQL agent from an LLM and tools."""
|
||||||
db = SQLDatabase.from_uri(database_uri)
|
db = SQLDatabase.from_uri(database_uri)
|
||||||
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
|
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
|
||||||
|
|
||||||
|
|
@ -234,7 +222,9 @@ class SQLAgent(CustomAgentExecutor):
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
)
|
)
|
||||||
tool_names = {tool.name for tool in tools} # type: ignore
|
tool_names = {tool.name for tool in tools} # type: ignore
|
||||||
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs) # type: ignore
|
agent = ZeroShotAgent(
|
||||||
|
llm_chain=llm_chain, allowed_tools=tool_names, **kwargs # type: ignore
|
||||||
|
)
|
||||||
return AgentExecutor.from_agent_and_tools(
|
return AgentExecutor.from_agent_and_tools(
|
||||||
agent=agent,
|
agent=agent,
|
||||||
tools=tools, # type: ignore
|
tools=tools, # type: ignore
|
||||||
|
|
@ -270,14 +260,20 @@ class VectorStoreRouterAgent(CustomAgentExecutor):
|
||||||
):
|
):
|
||||||
"""Construct a vector store router agent from an LLM and tools."""
|
"""Construct a vector store router agent from an LLM and tools."""
|
||||||
|
|
||||||
tools = vectorstoreroutertoolkit.get_tools()
|
tools = (
|
||||||
|
vectorstoreroutertoolkit
|
||||||
|
if isinstance(vectorstoreroutertoolkit, list)
|
||||||
|
else vectorstoreroutertoolkit.get_tools()
|
||||||
|
)
|
||||||
prompt = ZeroShotAgent.create_prompt(tools, prefix=VECTORSTORE_ROUTER_PREFIX)
|
prompt = ZeroShotAgent.create_prompt(tools, prefix=VECTORSTORE_ROUTER_PREFIX)
|
||||||
llm_chain = LLMChain(
|
llm_chain = LLMChain(
|
||||||
llm=llm,
|
llm=llm,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
)
|
)
|
||||||
tool_names = {tool.name for tool in tools}
|
tool_names = {tool.name for tool in tools}
|
||||||
agent = ZeroShotAgent(llm_chain=llm_chain, allowed_tools=tool_names, **kwargs) # type: ignore
|
agent = ZeroShotAgent(
|
||||||
|
llm_chain=llm_chain, allowed_tools=tool_names, **kwargs # type: ignore
|
||||||
|
)
|
||||||
return AgentExecutor.from_agent_and_tools(
|
return AgentExecutor.from_agent_and_tools(
|
||||||
agent=agent, tools=tools, verbose=True
|
agent=agent, tools=tools, verbose=True
|
||||||
)
|
)
|
||||||
|
|
@ -287,11 +283,11 @@ class VectorStoreRouterAgent(CustomAgentExecutor):
|
||||||
|
|
||||||
|
|
||||||
class InitializeAgent(CustomAgentExecutor):
|
class InitializeAgent(CustomAgentExecutor):
|
||||||
"""Implementation of initialize_agent function"""
|
"""Implementation of AgentInitializer function"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def function_name():
|
def function_name():
|
||||||
return "initialize_agent"
|
return "AgentInitializer"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def initialize(
|
def initialize(
|
||||||
|
|
@ -320,7 +316,7 @@ class InitializeAgent(CustomAgentExecutor):
|
||||||
CUSTOM_AGENTS = {
|
CUSTOM_AGENTS = {
|
||||||
"JsonAgent": JsonAgent,
|
"JsonAgent": JsonAgent,
|
||||||
"CSVAgent": CSVAgent,
|
"CSVAgent": CSVAgent,
|
||||||
"initialize_agent": InitializeAgent,
|
"AgentInitializer": InitializeAgent,
|
||||||
"VectorStoreAgent": VectorStoreAgent,
|
"VectorStoreAgent": VectorStoreAgent,
|
||||||
"VectorStoreRouterAgent": VectorStoreRouterAgent,
|
"VectorStoreRouterAgent": VectorStoreRouterAgent,
|
||||||
"SQLAgent": SQLAgent,
|
"SQLAgent": SQLAgent,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Dict, List, Optional, Type, Union
|
from typing import Any, Dict, List, Optional, Type, Union
|
||||||
|
from langchain.chains.base import Chain
|
||||||
|
from langchain.agents import AgentExecutor
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from langflow.template.field.base import TemplateField
|
from langflow.template.field.base import TemplateField
|
||||||
|
|
@ -81,5 +82,42 @@ class LangChainTypeCreator(BaseModel, ABC):
|
||||||
)
|
)
|
||||||
|
|
||||||
signature.add_extra_fields()
|
signature.add_extra_fields()
|
||||||
|
signature.add_extra_base_classes()
|
||||||
|
|
||||||
return signature
|
return signature
|
||||||
|
|
||||||
|
|
||||||
|
class CustomChain(Chain, ABC):
|
||||||
|
"""Custom chain"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def function_name():
|
||||||
|
return "CustomChain"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def initialize(cls, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def run(self, *args, **kwargs):
|
||||||
|
return super().run(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomAgentExecutor(AgentExecutor, ABC):
|
||||||
|
"""Custom chain"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def function_name():
|
||||||
|
return "CustomChain"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def initialize(cls, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def run(self, *args, **kwargs):
|
||||||
|
return super().run(*args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
from typing import Dict, List, Optional, Type
|
from typing import Any, Dict, List, Optional, Type
|
||||||
|
|
||||||
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 chain_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.chains 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, build_template_from_method
|
||||||
|
from langchain import chains
|
||||||
|
|
||||||
# Assuming necessary imports for Field, Template, and FrontendNode classes
|
# Assuming necessary imports for Field, Template, and FrontendNode classes
|
||||||
|
|
||||||
|
|
@ -18,10 +19,19 @@ class ChainCreator(LangChainTypeCreator):
|
||||||
def frontend_node_class(self) -> Type[ChainFrontendNode]:
|
def frontend_node_class(self) -> Type[ChainFrontendNode]:
|
||||||
return ChainFrontendNode
|
return ChainFrontendNode
|
||||||
|
|
||||||
|
#! We need to find a better solution for this
|
||||||
|
from_method_nodes = {
|
||||||
|
"ConversationalRetrievalChain": "from_llm",
|
||||||
|
"LLMCheckerChain": "from_llm",
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type_to_loader_dict(self) -> Dict:
|
def type_to_loader_dict(self) -> Dict:
|
||||||
if self.type_dict is None:
|
if self.type_dict is None:
|
||||||
self.type_dict = chain_type_to_cls_dict
|
self.type_dict: dict[str, Any] = {
|
||||||
|
chain_name: import_class(f"langchain.chains.{chain_name}")
|
||||||
|
for chain_name in chains.__all__
|
||||||
|
}
|
||||||
from langflow.interface.chains.custom import CUSTOM_CHAINS
|
from langflow.interface.chains.custom import CUSTOM_CHAINS
|
||||||
|
|
||||||
self.type_dict.update(CUSTOM_CHAINS)
|
self.type_dict.update(CUSTOM_CHAINS)
|
||||||
|
|
@ -37,20 +47,32 @@ class ChainCreator(LangChainTypeCreator):
|
||||||
try:
|
try:
|
||||||
if name in get_custom_nodes(self.type_name).keys():
|
if name in get_custom_nodes(self.type_name).keys():
|
||||||
return get_custom_nodes(self.type_name)[name]
|
return get_custom_nodes(self.type_name)[name]
|
||||||
|
elif name in self.from_method_nodes.keys():
|
||||||
|
return build_template_from_method(
|
||||||
|
name,
|
||||||
|
type_to_cls_dict=self.type_to_loader_dict,
|
||||||
|
method_name=self.from_method_nodes[name],
|
||||||
|
add_function=True,
|
||||||
|
)
|
||||||
return build_template_from_class(
|
return build_template_from_class(
|
||||||
name, self.type_to_loader_dict, add_function=True
|
name, self.type_to_loader_dict, add_function=True
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ValueError("Chain not found") from exc
|
raise ValueError(f"Chain {name} not found: {exc}") from exc
|
||||||
except AttributeError as exc:
|
except AttributeError as exc:
|
||||||
logger.error(f"Chain {name} not loaded: {exc}")
|
logger.error(f"Chain {name} not loaded: {exc}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def to_list(self) -> List[str]:
|
def to_list(self) -> List[str]:
|
||||||
custom_chains = list(get_custom_nodes("chains").keys())
|
names = []
|
||||||
default_chains = list(self.type_to_loader_dict.keys())
|
for _, chain in self.type_to_loader_dict.items():
|
||||||
|
chain_name = (
|
||||||
return default_chains + custom_chains
|
chain.function_name()
|
||||||
|
if hasattr(chain, "function_name")
|
||||||
|
else chain.__name__
|
||||||
|
)
|
||||||
|
names.append(chain_name)
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
chain_creator = ChainCreator()
|
chain_creator = ChainCreator()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
from typing import Dict, Optional, Type
|
from typing import Dict, Optional, Type, Union
|
||||||
|
|
||||||
from langchain.chains import ConversationChain
|
from langchain.chains import ConversationChain
|
||||||
from langchain.memory.buffer import ConversationBufferMemory
|
from langchain.memory.buffer import ConversationBufferMemory
|
||||||
from langchain.schema import BaseMemory
|
from langchain.schema import BaseMemory
|
||||||
|
from langflow.interface.base import CustomChain
|
||||||
from pydantic import Field, root_validator
|
from pydantic import Field, root_validator
|
||||||
|
from langchain.chains.question_answering import load_qa_chain
|
||||||
from langflow.graph.utils import extract_input_variables_from_prompt
|
from langflow.interface.utils import extract_input_variables_from_prompt
|
||||||
|
from langchain.base_language import BaseLanguageModel
|
||||||
|
|
||||||
DEFAULT_SUFFIX = """"
|
DEFAULT_SUFFIX = """"
|
||||||
Current conversation:
|
Current conversation:
|
||||||
|
|
@ -14,7 +16,7 @@ Human: {input}
|
||||||
{ai_prefix}"""
|
{ai_prefix}"""
|
||||||
|
|
||||||
|
|
||||||
class BaseCustomChain(ConversationChain):
|
class BaseCustomConversationChain(ConversationChain):
|
||||||
"""BaseCustomChain is a chain you can use to have a conversation with a custom character."""
|
"""BaseCustomChain is a chain you can use to have a conversation with a custom character."""
|
||||||
|
|
||||||
template: Optional[str]
|
template: Optional[str]
|
||||||
|
|
@ -47,7 +49,7 @@ class BaseCustomChain(ConversationChain):
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
class SeriesCharacterChain(BaseCustomChain):
|
class SeriesCharacterChain(BaseCustomConversationChain):
|
||||||
"""SeriesCharacterChain is a chain you can use to have a conversation with a character from a series."""
|
"""SeriesCharacterChain is a chain you can use to have a conversation with a character from a series."""
|
||||||
|
|
||||||
character: str
|
character: str
|
||||||
|
|
@ -66,7 +68,7 @@ Human: {input}
|
||||||
"""Default memory store."""
|
"""Default memory store."""
|
||||||
|
|
||||||
|
|
||||||
class MidJourneyPromptChain(BaseCustomChain):
|
class MidJourneyPromptChain(BaseCustomConversationChain):
|
||||||
"""MidJourneyPromptChain is a chain you can use to generate new MidJourney prompts."""
|
"""MidJourneyPromptChain is a chain you can use to generate new MidJourney prompts."""
|
||||||
|
|
||||||
template: Optional[
|
template: Optional[
|
||||||
|
|
@ -84,7 +86,7 @@ class MidJourneyPromptChain(BaseCustomChain):
|
||||||
AI:""" # noqa: E501
|
AI:""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
class TimeTravelGuideChain(BaseCustomChain):
|
class TimeTravelGuideChain(BaseCustomConversationChain):
|
||||||
template: Optional[
|
template: Optional[
|
||||||
str
|
str
|
||||||
] = """I want you to act as my time travel guide. You are helpful and creative. I will provide you with the historical period or future time I want to visit and you will suggest the best events, sights, or people to experience. Provide the suggestions and any necessary information.
|
] = """I want you to act as my time travel guide. You are helpful and creative. I will provide you with the historical period or future time I want to visit and you will suggest the best events, sights, or people to experience. Provide the suggestions and any necessary information.
|
||||||
|
|
@ -94,7 +96,26 @@ class TimeTravelGuideChain(BaseCustomChain):
|
||||||
AI:""" # noqa: E501
|
AI:""" # noqa: E501
|
||||||
|
|
||||||
|
|
||||||
CUSTOM_CHAINS: Dict[str, Type[ConversationChain]] = {
|
class CombineDocsChain(CustomChain):
|
||||||
|
"""Implementation of load_qa_chain function"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def function_name():
|
||||||
|
return "load_qa_chain"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def initialize(cls, llm: BaseLanguageModel, chain_type: str):
|
||||||
|
return load_qa_chain(llm=llm, chain_type=chain_type)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def run(self, *args, **kwargs):
|
||||||
|
return super().run(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
CUSTOM_CHAINS: Dict[str, Type[Union[ConversationChain, CustomChain]]] = {
|
||||||
|
"CombineDocsChain": CombineDocsChain,
|
||||||
"SeriesCharacterChain": SeriesCharacterChain,
|
"SeriesCharacterChain": SeriesCharacterChain,
|
||||||
"MidJourneyPromptChain": MidJourneyPromptChain,
|
"MidJourneyPromptChain": MidJourneyPromptChain,
|
||||||
"TimeTravelGuideChain": TimeTravelGuideChain,
|
"TimeTravelGuideChain": TimeTravelGuideChain,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import inspect
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from langchain import (
|
from langchain import (
|
||||||
chains,
|
|
||||||
document_loaders,
|
document_loaders,
|
||||||
embeddings,
|
embeddings,
|
||||||
llms,
|
llms,
|
||||||
|
|
@ -11,21 +10,21 @@ from langchain import (
|
||||||
text_splitter,
|
text_splitter,
|
||||||
)
|
)
|
||||||
from langchain.agents import agent_toolkits
|
from langchain.agents import agent_toolkits
|
||||||
from langchain.chat_models import ChatOpenAI
|
from langchain.chat_models import AzureChatOpenAI, ChatOpenAI
|
||||||
|
from langchain.chat_models import ChatAnthropic
|
||||||
|
|
||||||
from langflow.interface.importing.utils import import_class
|
from langflow.interface.importing.utils import import_class
|
||||||
|
from langflow.interface.agents.custom import CUSTOM_AGENTS
|
||||||
|
from langflow.interface.chains.custom import CUSTOM_CHAINS
|
||||||
|
|
||||||
## LLMs
|
# LLMs
|
||||||
llm_type_to_cls_dict = llms.type_to_cls_dict
|
llm_type_to_cls_dict = llms.type_to_cls_dict
|
||||||
|
llm_type_to_cls_dict["anthropic-chat"] = ChatAnthropic # type: ignore
|
||||||
|
llm_type_to_cls_dict["azure-chat"] = AzureChatOpenAI # type: ignore
|
||||||
llm_type_to_cls_dict["openai-chat"] = ChatOpenAI # type: ignore
|
llm_type_to_cls_dict["openai-chat"] = ChatOpenAI # type: ignore
|
||||||
|
|
||||||
## Chains
|
|
||||||
chain_type_to_cls_dict: dict[str, Any] = {
|
|
||||||
chain_name: import_class(f"langchain.chains.{chain_name}")
|
|
||||||
for chain_name in chains.__all__
|
|
||||||
}
|
|
||||||
|
|
||||||
## Toolkits
|
# Toolkits
|
||||||
toolkit_type_to_loader_dict: dict[str, Any] = {
|
toolkit_type_to_loader_dict: dict[str, Any] = {
|
||||||
toolkit_name: import_class(f"langchain.agents.agent_toolkits.{toolkit_name}")
|
toolkit_name: import_class(f"langchain.agents.agent_toolkits.{toolkit_name}")
|
||||||
# if toolkit_name is lower case it is a loader
|
# if toolkit_name is lower case it is a loader
|
||||||
|
|
@ -40,25 +39,25 @@ toolkit_type_to_cls_dict: dict[str, Any] = {
|
||||||
if not toolkit_name.islower()
|
if not toolkit_name.islower()
|
||||||
}
|
}
|
||||||
|
|
||||||
## Memories
|
# Memories
|
||||||
memory_type_to_cls_dict: dict[str, Any] = {
|
memory_type_to_cls_dict: dict[str, Any] = {
|
||||||
memory_name: import_class(f"langchain.memory.{memory_name}")
|
memory_name: import_class(f"langchain.memory.{memory_name}")
|
||||||
for memory_name in memory.__all__
|
for memory_name in memory.__all__
|
||||||
}
|
}
|
||||||
|
|
||||||
## Wrappers
|
# Wrappers
|
||||||
wrapper_type_to_cls_dict: dict[str, Any] = {
|
wrapper_type_to_cls_dict: dict[str, Any] = {
|
||||||
wrapper.__name__: wrapper for wrapper in [requests.RequestsWrapper]
|
wrapper.__name__: wrapper for wrapper in [requests.RequestsWrapper]
|
||||||
}
|
}
|
||||||
|
|
||||||
## Embeddings
|
# Embeddings
|
||||||
embedding_type_to_cls_dict: dict[str, Any] = {
|
embedding_type_to_cls_dict: dict[str, Any] = {
|
||||||
embedding_name: import_class(f"langchain.embeddings.{embedding_name}")
|
embedding_name: import_class(f"langchain.embeddings.{embedding_name}")
|
||||||
for embedding_name in embeddings.__all__
|
for embedding_name in embeddings.__all__
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
## Document Loaders
|
# Document Loaders
|
||||||
documentloaders_type_to_cls_dict: dict[str, Any] = {
|
documentloaders_type_to_cls_dict: dict[str, Any] = {
|
||||||
documentloader_name: import_class(
|
documentloader_name: import_class(
|
||||||
f"langchain.document_loaders.{documentloader_name}"
|
f"langchain.document_loaders.{documentloader_name}"
|
||||||
|
|
@ -66,7 +65,10 @@ documentloaders_type_to_cls_dict: dict[str, Any] = {
|
||||||
for documentloader_name in document_loaders.__all__
|
for documentloader_name in document_loaders.__all__
|
||||||
}
|
}
|
||||||
|
|
||||||
## Text Splitters
|
# Text Splitters
|
||||||
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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# merge CUSTOM_AGENTS and CUSTOM_CHAINS
|
||||||
|
CUSTOM_NODES = {**CUSTOM_AGENTS, **CUSTOM_CHAINS} # type: ignore
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,20 @@
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Type
|
||||||
|
|
||||||
from langflow.interface.base import LangChainTypeCreator
|
from langflow.interface.base import LangChainTypeCreator
|
||||||
|
from langflow.template.frontend_node.documentloaders import DocumentLoaderFrontNode
|
||||||
from langflow.interface.custom_lists import documentloaders_type_to_cls_dict
|
from langflow.interface.custom_lists import documentloaders_type_to_cls_dict
|
||||||
from langflow.settings import settings
|
from langflow.settings import settings
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
def build_file_path_template(
|
|
||||||
suffixes: list, fileTypes: list, name: str = "file_path"
|
|
||||||
) -> Dict:
|
|
||||||
"""Build a file path template for a document loader."""
|
|
||||||
return {
|
|
||||||
"type": "file",
|
|
||||||
"required": True,
|
|
||||||
"show": True,
|
|
||||||
"name": name,
|
|
||||||
"value": "",
|
|
||||||
"suffixes": suffixes,
|
|
||||||
"fileTypes": fileTypes,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentLoaderCreator(LangChainTypeCreator):
|
class DocumentLoaderCreator(LangChainTypeCreator):
|
||||||
type_name: str = "documentloaders"
|
type_name: str = "documentloaders"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def frontend_node_class(self) -> Type[DocumentLoaderFrontNode]:
|
||||||
|
return DocumentLoaderFrontNode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type_to_loader_dict(self) -> Dict:
|
def type_to_loader_dict(self) -> Dict:
|
||||||
return documentloaders_type_to_cls_dict
|
return documentloaders_type_to_cls_dict
|
||||||
|
|
@ -32,106 +22,7 @@ class DocumentLoaderCreator(LangChainTypeCreator):
|
||||||
def get_signature(self, name: str) -> Optional[Dict]:
|
def get_signature(self, name: str) -> Optional[Dict]:
|
||||||
"""Get the signature of a document loader."""
|
"""Get the signature of a document loader."""
|
||||||
try:
|
try:
|
||||||
signature = build_template_from_class(
|
return build_template_from_class(name, documentloaders_type_to_cls_dict)
|
||||||
name, documentloaders_type_to_cls_dict
|
|
||||||
)
|
|
||||||
|
|
||||||
file_path_templates = {
|
|
||||||
"AirbyteJSONLoader": build_file_path_template(
|
|
||||||
suffixes=[".json"], fileTypes=["json"]
|
|
||||||
),
|
|
||||||
"CoNLLULoader": build_file_path_template(
|
|
||||||
suffixes=[".csv"], fileTypes=["csv"]
|
|
||||||
),
|
|
||||||
"CSVLoader": build_file_path_template(
|
|
||||||
suffixes=[".csv"], fileTypes=["csv"]
|
|
||||||
),
|
|
||||||
"UnstructuredEmailLoader": build_file_path_template(
|
|
||||||
suffixes=[".eml"], fileTypes=["eml"]
|
|
||||||
),
|
|
||||||
"EverNoteLoader": build_file_path_template(
|
|
||||||
suffixes=[".xml"], fileTypes=["xml"]
|
|
||||||
),
|
|
||||||
"FacebookChatLoader": build_file_path_template(
|
|
||||||
suffixes=[".json"], fileTypes=["json"]
|
|
||||||
),
|
|
||||||
"GutenbergLoader": build_file_path_template(
|
|
||||||
suffixes=[".txt"], fileTypes=["txt"]
|
|
||||||
),
|
|
||||||
"BSHTMLLoader": build_file_path_template(
|
|
||||||
suffixes=[".html"], fileTypes=["html"]
|
|
||||||
),
|
|
||||||
"UnstructuredHTMLLoader": build_file_path_template(
|
|
||||||
suffixes=[".html"], fileTypes=["html"]
|
|
||||||
),
|
|
||||||
"UnstructuredImageLoader": build_file_path_template(
|
|
||||||
suffixes=[".jpg", ".jpeg", ".png", ".gif", ".bmp"],
|
|
||||||
fileTypes=["jpg", "jpeg", "png", "gif", "bmp"],
|
|
||||||
),
|
|
||||||
"UnstructuredMarkdownLoader": build_file_path_template(
|
|
||||||
suffixes=[".md"], fileTypes=["md"]
|
|
||||||
),
|
|
||||||
"PyPDFLoader": build_file_path_template(
|
|
||||||
suffixes=[".pdf"], fileTypes=["pdf"]
|
|
||||||
),
|
|
||||||
"UnstructuredPowerPointLoader": build_file_path_template(
|
|
||||||
suffixes=[".pptx", ".ppt"], fileTypes=["pptx", "ppt"]
|
|
||||||
),
|
|
||||||
"SRTLoader": build_file_path_template(
|
|
||||||
suffixes=[".srt"], fileTypes=["srt"]
|
|
||||||
),
|
|
||||||
"TelegramChatLoader": build_file_path_template(
|
|
||||||
suffixes=[".json"], fileTypes=["json"]
|
|
||||||
),
|
|
||||||
"TextLoader": build_file_path_template(
|
|
||||||
suffixes=[".txt"], fileTypes=["txt"]
|
|
||||||
),
|
|
||||||
"UnstructuredWordDocumentLoader": build_file_path_template(
|
|
||||||
suffixes=[".docx", ".doc"], fileTypes=["docx", "doc"]
|
|
||||||
),
|
|
||||||
"SlackDirectoryLoader": build_file_path_template(
|
|
||||||
suffixes=[".zip"], fileTypes=["zip"]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
if name in file_path_templates:
|
|
||||||
signature["template"]["file_path"] = file_path_templates[name]
|
|
||||||
elif name in {
|
|
||||||
"WebBaseLoader",
|
|
||||||
"AZLyricsLoader",
|
|
||||||
"CollegeConfidentialLoader",
|
|
||||||
"HNLoader",
|
|
||||||
"IFixitLoader",
|
|
||||||
"IMSDbLoader",
|
|
||||||
}:
|
|
||||||
signature["template"]["web_path"] = {
|
|
||||||
"type": "str",
|
|
||||||
"required": True,
|
|
||||||
"show": True,
|
|
||||||
"name": "web_path",
|
|
||||||
"value": "",
|
|
||||||
"display_name": "Web Page",
|
|
||||||
}
|
|
||||||
elif name in {"GitbookLoader"}:
|
|
||||||
signature["template"]["web_page"] = {
|
|
||||||
"type": "str",
|
|
||||||
"required": True,
|
|
||||||
"show": True,
|
|
||||||
"name": "web_page",
|
|
||||||
"value": "",
|
|
||||||
"display_name": "Web Page",
|
|
||||||
}
|
|
||||||
elif name in {"ReadTheDocsLoader", "NotionDirectoryLoader"}:
|
|
||||||
signature["template"]["path"] = {
|
|
||||||
"type": "str",
|
|
||||||
"required": True,
|
|
||||||
"show": True,
|
|
||||||
"name": "path",
|
|
||||||
"value": "",
|
|
||||||
"display_name": "Web Page",
|
|
||||||
}
|
|
||||||
|
|
||||||
return signature
|
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ValueError(f"Documment Loader {name} not found") from exc
|
raise ValueError(f"Documment Loader {name} not found") from exc
|
||||||
except AttributeError as exc:
|
except AttributeError as exc:
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from langchain.base_language import BaseLanguageModel
|
||||||
from langchain.chains.base import Chain
|
from langchain.chains.base import Chain
|
||||||
from langchain.chat_models.base import BaseChatModel
|
from langchain.chat_models.base import BaseChatModel
|
||||||
from langchain.tools import BaseTool
|
from langchain.tools import BaseTool
|
||||||
|
from langflow.utils import validate
|
||||||
|
|
||||||
|
|
||||||
def import_module(module_path: str) -> Any:
|
def import_module(module_path: str) -> Any:
|
||||||
|
|
@ -147,3 +148,10 @@ def import_utility(utility: str) -> Any:
|
||||||
if utility == "SQLDatabase":
|
if utility == "SQLDatabase":
|
||||||
return import_class(f"langchain.sql_database.{utility}")
|
return import_class(f"langchain.sql_database.{utility}")
|
||||||
return import_class(f"langchain.utilities.{utility}")
|
return import_class(f"langchain.utilities.{utility}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_function(code):
|
||||||
|
"""Get the function"""
|
||||||
|
function_name = validate.extract_function_name(code)
|
||||||
|
|
||||||
|
return validate.create_function(code, function_name)
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@ from langchain.chains.loading import load_chain_from_config
|
||||||
from langchain.llms.loading import load_llm_from_config
|
from langchain.llms.loading import load_llm_from_config
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from langflow.interface.agents.custom import CUSTOM_AGENTS
|
from langflow.interface.custom_lists import CUSTOM_NODES
|
||||||
from langflow.interface.importing.utils import import_by_type
|
from langflow.interface.importing.utils import get_function, import_by_type
|
||||||
from langflow.interface.run import fix_memory_inputs
|
|
||||||
from langflow.interface.toolkits.base import toolkits_creator
|
from langflow.interface.toolkits.base import toolkits_creator
|
||||||
|
from langflow.interface.chains.base import chain_creator
|
||||||
from langflow.interface.types import get_type_list
|
from langflow.interface.types import get_type_list
|
||||||
from langflow.interface.utils import load_file_into_dict
|
from langflow.interface.utils import load_file_into_dict
|
||||||
from langflow.utils import util, validate
|
from langflow.utils import util, validate
|
||||||
|
|
@ -33,10 +33,11 @@ def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any:
|
||||||
"""Instantiate class from module type and key, and params"""
|
"""Instantiate class from module type and key, and params"""
|
||||||
params = convert_params_to_sets(params)
|
params = convert_params_to_sets(params)
|
||||||
params = convert_kwargs(params)
|
params = convert_kwargs(params)
|
||||||
if node_type in CUSTOM_AGENTS:
|
if node_type in CUSTOM_NODES:
|
||||||
custom_agent = CUSTOM_AGENTS.get(node_type)
|
if custom_node := CUSTOM_NODES.get(node_type):
|
||||||
if custom_agent:
|
if hasattr(custom_node, "initialize"):
|
||||||
return custom_agent.initialize(**params)
|
return custom_node.initialize(**params)
|
||||||
|
return custom_node(**params)
|
||||||
|
|
||||||
class_object = import_by_type(_type=base_type, name=node_type)
|
class_object = import_by_type(_type=base_type, name=node_type)
|
||||||
return instantiate_based_on_type(class_object, base_type, node_type, params)
|
return instantiate_based_on_type(class_object, base_type, node_type, params)
|
||||||
|
|
@ -80,10 +81,24 @@ def instantiate_based_on_type(class_object, base_type, node_type, params):
|
||||||
return instantiate_textsplitter(class_object, params)
|
return instantiate_textsplitter(class_object, params)
|
||||||
elif base_type == "utilities":
|
elif base_type == "utilities":
|
||||||
return instantiate_utility(node_type, class_object, params)
|
return instantiate_utility(node_type, class_object, params)
|
||||||
|
elif base_type == "chains":
|
||||||
|
return instantiate_chains(node_type, class_object, params)
|
||||||
else:
|
else:
|
||||||
return class_object(**params)
|
return class_object(**params)
|
||||||
|
|
||||||
|
|
||||||
|
def instantiate_chains(node_type, class_object, params):
|
||||||
|
if "retriever" in params and hasattr(params["retriever"], "as_retriever"):
|
||||||
|
params["retriever"] = params["retriever"].as_retriever()
|
||||||
|
if node_type in chain_creator.from_method_nodes:
|
||||||
|
method = chain_creator.from_method_nodes[node_type]
|
||||||
|
if class_method := getattr(class_object, method, None):
|
||||||
|
return class_method(**params)
|
||||||
|
raise ValueError(f"Method {method} not found in {class_object}")
|
||||||
|
|
||||||
|
return class_object(**params)
|
||||||
|
|
||||||
|
|
||||||
def instantiate_agent(class_object, params):
|
def instantiate_agent(class_object, params):
|
||||||
return load_agent_executor(class_object, params)
|
return load_agent_executor(class_object, params)
|
||||||
|
|
||||||
|
|
@ -100,6 +115,10 @@ def instantiate_tool(node_type, class_object, params):
|
||||||
if node_type == "JsonSpec":
|
if node_type == "JsonSpec":
|
||||||
params["dict_"] = load_file_into_dict(params.pop("path"))
|
params["dict_"] = load_file_into_dict(params.pop("path"))
|
||||||
return class_object(**params)
|
return class_object(**params)
|
||||||
|
elif node_type == "PythonFunctionTool":
|
||||||
|
params["func"] = get_function(params.get("code"))
|
||||||
|
return class_object(**params)
|
||||||
|
# For backward compatibility
|
||||||
elif node_type == "PythonFunction":
|
elif node_type == "PythonFunction":
|
||||||
function_string = params["code"]
|
function_string = params["code"]
|
||||||
if isinstance(function_string, str):
|
if isinstance(function_string, str):
|
||||||
|
|
@ -112,8 +131,11 @@ def instantiate_tool(node_type, class_object, params):
|
||||||
|
|
||||||
def instantiate_toolkit(node_type, class_object, params):
|
def instantiate_toolkit(node_type, class_object, params):
|
||||||
loaded_toolkit = class_object(**params)
|
loaded_toolkit = class_object(**params)
|
||||||
if toolkits_creator.has_create_function(node_type):
|
# Commenting this out for now to use toolkits as normal tools
|
||||||
return load_toolkits_executor(node_type, loaded_toolkit, params)
|
# if toolkits_creator.has_create_function(node_type):
|
||||||
|
# return load_toolkits_executor(node_type, loaded_toolkit, params)
|
||||||
|
if isinstance(loaded_toolkit, BaseToolkit):
|
||||||
|
return loaded_toolkit.get_tools()
|
||||||
return loaded_toolkit
|
return loaded_toolkit
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -137,6 +159,14 @@ def instantiate_vectorstore(class_object, params):
|
||||||
"The source you provided did not load correctly or was empty."
|
"The source you provided did not load correctly or was empty."
|
||||||
"This may cause an error in the vectorstore."
|
"This may cause an error in the vectorstore."
|
||||||
)
|
)
|
||||||
|
# Chroma requires all metadata values to not be None
|
||||||
|
if class_object.__name__ == "Chroma":
|
||||||
|
for doc in params["documents"]:
|
||||||
|
if doc.metadata is None:
|
||||||
|
doc.metadata = {}
|
||||||
|
for key, value in doc.metadata.items():
|
||||||
|
if value is None:
|
||||||
|
doc.metadata[key] = ""
|
||||||
return class_object.from_documents(**params)
|
return class_object.from_documents(**params)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -168,38 +198,6 @@ def instantiate_utility(node_type, class_object, params):
|
||||||
return class_object(**params)
|
return class_object(**params)
|
||||||
|
|
||||||
|
|
||||||
def load_flow_from_json(path: str, build=True):
|
|
||||||
"""Load flow from json file"""
|
|
||||||
# This is done to avoid circular imports
|
|
||||||
from langflow.graph import Graph
|
|
||||||
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
flow_graph = json.load(f)
|
|
||||||
data_graph = flow_graph["data"]
|
|
||||||
nodes = data_graph["nodes"]
|
|
||||||
# Substitute ZeroShotPrompt with PromptTemplate
|
|
||||||
# nodes = replace_zero_shot_prompt_with_prompt_template(nodes)
|
|
||||||
# Add input variables
|
|
||||||
# nodes = payload.extract_input_variables(nodes)
|
|
||||||
|
|
||||||
# Nodes, edges and root node
|
|
||||||
edges = data_graph["edges"]
|
|
||||||
graph = Graph(nodes, edges)
|
|
||||||
if build:
|
|
||||||
langchain_object = graph.build()
|
|
||||||
if hasattr(langchain_object, "verbose"):
|
|
||||||
langchain_object.verbose = True
|
|
||||||
|
|
||||||
if hasattr(langchain_object, "return_intermediate_steps"):
|
|
||||||
# https://github.com/hwchase17/langchain/issues/2068
|
|
||||||
# Deactivating until we have a frontend solution
|
|
||||||
# to display intermediate steps
|
|
||||||
langchain_object.return_intermediate_steps = False
|
|
||||||
fix_memory_inputs(langchain_object)
|
|
||||||
return langchain_object
|
|
||||||
return graph
|
|
||||||
|
|
||||||
|
|
||||||
def replace_zero_shot_prompt_with_prompt_template(nodes):
|
def replace_zero_shot_prompt_with_prompt_template(nodes):
|
||||||
"""Replace ZeroShotPrompt with PromptTemplate"""
|
"""Replace ZeroShotPrompt with PromptTemplate"""
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from typing import Dict, List, Optional, Type
|
||||||
from langchain.prompts import PromptTemplate
|
from langchain.prompts import PromptTemplate
|
||||||
from pydantic import root_validator
|
from pydantic import root_validator
|
||||||
|
|
||||||
from langflow.graph.utils import extract_input_variables_from_prompt
|
from langflow.interface.utils import extract_input_variables_from_prompt
|
||||||
|
|
||||||
# Steps to create a BaseCustomPrompt:
|
# Steps to create a BaseCustomPrompt:
|
||||||
# 1. Create a prompt template that endes with:
|
# 1. Create a prompt template that endes with:
|
||||||
|
|
@ -71,7 +71,3 @@ Human: {input}
|
||||||
CUSTOM_PROMPTS: Dict[str, Type[BaseCustomPrompt]] = {
|
CUSTOM_PROMPTS: Dict[str, Type[BaseCustomPrompt]] = {
|
||||||
"SeriesCharacterPrompt": SeriesCharacterPrompt
|
"SeriesCharacterPrompt": SeriesCharacterPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
prompt = SeriesCharacterPrompt(character="Harry Potter", series="Harry Potter")
|
|
||||||
print(prompt.template)
|
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,8 @@
|
||||||
import contextlib
|
from langflow.cache.utils import memoize_dict
|
||||||
import io
|
from langflow.graph import Graph
|
||||||
from typing import Any, Dict, List, Tuple
|
|
||||||
|
|
||||||
from langchain.schema import AgentAction
|
|
||||||
|
|
||||||
from langflow.api.callback import AsyncStreamingLLMCallbackHandler, StreamingLLMCallbackHandler # type: ignore
|
|
||||||
from langflow.cache.base import compute_dict_hash, load_cache, memoize_dict
|
|
||||||
from langflow.graph.graph import Graph
|
|
||||||
from langflow.utils.logger import logger
|
from langflow.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
def load_langchain_object(data_graph, is_first_message=False):
|
|
||||||
"""
|
|
||||||
Load langchain object from cache if it exists, otherwise build it.
|
|
||||||
"""
|
|
||||||
computed_hash = compute_dict_hash(data_graph)
|
|
||||||
if is_first_message:
|
|
||||||
langchain_object = build_langchain_object(data_graph)
|
|
||||||
else:
|
|
||||||
logger.debug("Loading langchain object from cache")
|
|
||||||
langchain_object = load_cache(computed_hash)
|
|
||||||
|
|
||||||
return computed_hash, langchain_object
|
|
||||||
|
|
||||||
|
|
||||||
def load_or_build_langchain_object(data_graph, is_first_message=False):
|
|
||||||
"""
|
|
||||||
Load langchain object from cache if it exists, otherwise build it.
|
|
||||||
"""
|
|
||||||
if is_first_message:
|
|
||||||
build_langchain_object_with_caching.clear_cache()
|
|
||||||
return build_langchain_object_with_caching(data_graph)
|
|
||||||
|
|
||||||
|
|
||||||
@memoize_dict(maxsize=10)
|
@memoize_dict(maxsize=10)
|
||||||
def build_langchain_object_with_caching(data_graph):
|
def build_langchain_object_with_caching(data_graph):
|
||||||
"""
|
"""
|
||||||
|
|
@ -40,16 +10,10 @@ def build_langchain_object_with_caching(data_graph):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.debug("Building langchain object")
|
logger.debug("Building langchain object")
|
||||||
graph = build_graph(data_graph)
|
graph = Graph.from_payload(data_graph)
|
||||||
return graph.build()
|
return graph.build()
|
||||||
|
|
||||||
|
|
||||||
def build_graph(data_graph):
|
|
||||||
nodes = data_graph["nodes"]
|
|
||||||
edges = data_graph["edges"]
|
|
||||||
return Graph(nodes, edges)
|
|
||||||
|
|
||||||
|
|
||||||
def build_langchain_object(data_graph):
|
def build_langchain_object(data_graph):
|
||||||
"""
|
"""
|
||||||
Build langchain object from data_graph.
|
Build langchain object from data_graph.
|
||||||
|
|
@ -66,29 +30,6 @@ def build_langchain_object(data_graph):
|
||||||
return graph.build()
|
return graph.build()
|
||||||
|
|
||||||
|
|
||||||
def process_graph_cached(data_graph: Dict[str, Any], message: str):
|
|
||||||
"""
|
|
||||||
Process graph by extracting input variables and replacing ZeroShotPrompt
|
|
||||||
with PromptTemplate,then run the graph and return the result and thought.
|
|
||||||
"""
|
|
||||||
# Load langchain object
|
|
||||||
is_first_message = len(data_graph.get("chatHistory", [])) == 0
|
|
||||||
langchain_object = load_or_build_langchain_object(data_graph, is_first_message)
|
|
||||||
logger.debug("Loaded langchain object")
|
|
||||||
|
|
||||||
if langchain_object is None:
|
|
||||||
# Raise user facing error
|
|
||||||
raise ValueError(
|
|
||||||
"There was an error loading the langchain_object. Please, check all the nodes and try again."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate result and thought
|
|
||||||
logger.debug("Generating result and thought")
|
|
||||||
result, thought = get_result_and_thought(langchain_object, message)
|
|
||||||
logger.debug("Generated result and thought")
|
|
||||||
return {"result": str(result), "thought": thought.strip()}
|
|
||||||
|
|
||||||
|
|
||||||
def get_memory_key(langchain_object):
|
def get_memory_key(langchain_object):
|
||||||
"""
|
"""
|
||||||
Given a LangChain object, this function retrieves the current memory key from the object's memory attribute.
|
Given a LangChain object, this function retrieves the current memory key from the object's memory attribute.
|
||||||
|
|
@ -124,147 +65,3 @@ def update_memory_keys(langchain_object, possible_new_mem_key):
|
||||||
langchain_object.memory.input_key = input_key
|
langchain_object.memory.input_key = input_key
|
||||||
langchain_object.memory.output_key = output_key
|
langchain_object.memory.output_key = output_key
|
||||||
langchain_object.memory.memory_key = possible_new_mem_key
|
langchain_object.memory.memory_key = possible_new_mem_key
|
||||||
|
|
||||||
|
|
||||||
def fix_memory_inputs(langchain_object):
|
|
||||||
"""
|
|
||||||
Given a LangChain object, this function checks if it has a memory attribute and if that memory key exists in the
|
|
||||||
object's input variables. If so, it does nothing. Otherwise, it gets a possible new memory key using the
|
|
||||||
get_memory_key function and updates the memory keys using the update_memory_keys function.
|
|
||||||
"""
|
|
||||||
if hasattr(langchain_object, "memory") and langchain_object.memory is not None:
|
|
||||||
try:
|
|
||||||
if langchain_object.memory.memory_key in langchain_object.input_variables:
|
|
||||||
return
|
|
||||||
except AttributeError:
|
|
||||||
input_variables = (
|
|
||||||
langchain_object.prompt.input_variables
|
|
||||||
if hasattr(langchain_object, "prompt")
|
|
||||||
else langchain_object.input_keys
|
|
||||||
)
|
|
||||||
if langchain_object.memory.memory_key in input_variables:
|
|
||||||
return
|
|
||||||
|
|
||||||
possible_new_mem_key = get_memory_key(langchain_object)
|
|
||||||
if possible_new_mem_key is not None:
|
|
||||||
update_memory_keys(langchain_object, possible_new_mem_key)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_result_and_steps(langchain_object, message: str, **kwargs):
|
|
||||||
"""Get result and thought from extracted json"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
if hasattr(langchain_object, "verbose"):
|
|
||||||
langchain_object.verbose = True
|
|
||||||
chat_input = None
|
|
||||||
memory_key = ""
|
|
||||||
if hasattr(langchain_object, "memory") and langchain_object.memory is not None:
|
|
||||||
memory_key = langchain_object.memory.memory_key
|
|
||||||
|
|
||||||
if hasattr(langchain_object, "input_keys"):
|
|
||||||
for key in langchain_object.input_keys:
|
|
||||||
if key not in [memory_key, "chat_history"]:
|
|
||||||
chat_input = {key: message}
|
|
||||||
else:
|
|
||||||
chat_input = message # type: ignore
|
|
||||||
|
|
||||||
if hasattr(langchain_object, "return_intermediate_steps"):
|
|
||||||
# https://github.com/hwchase17/langchain/issues/2068
|
|
||||||
# Deactivating until we have a frontend solution
|
|
||||||
# to display intermediate steps
|
|
||||||
langchain_object.return_intermediate_steps = True
|
|
||||||
|
|
||||||
fix_memory_inputs(langchain_object)
|
|
||||||
try:
|
|
||||||
async_callbacks = [AsyncStreamingLLMCallbackHandler(**kwargs)]
|
|
||||||
output = await langchain_object.acall(chat_input, callbacks=async_callbacks)
|
|
||||||
except Exception as exc:
|
|
||||||
# make the error message more informative
|
|
||||||
logger.debug(f"Error: {str(exc)}")
|
|
||||||
sync_callbacks = [StreamingLLMCallbackHandler(**kwargs)]
|
|
||||||
output = langchain_object(chat_input, callbacks=sync_callbacks)
|
|
||||||
|
|
||||||
intermediate_steps = (
|
|
||||||
output.get("intermediate_steps", []) if isinstance(output, dict) else []
|
|
||||||
)
|
|
||||||
|
|
||||||
result = (
|
|
||||||
output.get(langchain_object.output_keys[0])
|
|
||||||
if isinstance(output, dict)
|
|
||||||
else output
|
|
||||||
)
|
|
||||||
thought = format_actions(intermediate_steps) if intermediate_steps else ""
|
|
||||||
except Exception as exc:
|
|
||||||
raise ValueError(f"Error: {str(exc)}") from exc
|
|
||||||
return result, thought
|
|
||||||
|
|
||||||
|
|
||||||
def get_result_and_thought(langchain_object, message: str):
|
|
||||||
"""Get result and thought from extracted json"""
|
|
||||||
try:
|
|
||||||
if hasattr(langchain_object, "verbose"):
|
|
||||||
langchain_object.verbose = True
|
|
||||||
chat_input = None
|
|
||||||
memory_key = ""
|
|
||||||
if hasattr(langchain_object, "memory") and langchain_object.memory is not None:
|
|
||||||
memory_key = langchain_object.memory.memory_key
|
|
||||||
|
|
||||||
if hasattr(langchain_object, "input_keys"):
|
|
||||||
for key in langchain_object.input_keys:
|
|
||||||
if key not in [memory_key, "chat_history"]:
|
|
||||||
chat_input = {key: message}
|
|
||||||
else:
|
|
||||||
chat_input = message # type: ignore
|
|
||||||
|
|
||||||
if hasattr(langchain_object, "return_intermediate_steps"):
|
|
||||||
# https://github.com/hwchase17/langchain/issues/2068
|
|
||||||
# Deactivating until we have a frontend solution
|
|
||||||
# to display intermediate steps
|
|
||||||
langchain_object.return_intermediate_steps = False
|
|
||||||
|
|
||||||
fix_memory_inputs(langchain_object)
|
|
||||||
|
|
||||||
with io.StringIO() as output_buffer, contextlib.redirect_stdout(output_buffer):
|
|
||||||
try:
|
|
||||||
# if hasattr(langchain_object, "acall"):
|
|
||||||
# output = await langchain_object.acall(chat_input)
|
|
||||||
# else:
|
|
||||||
output = langchain_object(chat_input)
|
|
||||||
except ValueError as exc:
|
|
||||||
# make the error message more informative
|
|
||||||
logger.debug(f"Error: {str(exc)}")
|
|
||||||
output = langchain_object.run(chat_input)
|
|
||||||
|
|
||||||
intermediate_steps = (
|
|
||||||
output.get("intermediate_steps", []) if isinstance(output, dict) else []
|
|
||||||
)
|
|
||||||
|
|
||||||
result = (
|
|
||||||
output.get(langchain_object.output_keys[0])
|
|
||||||
if isinstance(output, dict)
|
|
||||||
else output
|
|
||||||
)
|
|
||||||
if intermediate_steps:
|
|
||||||
thought = format_actions(intermediate_steps)
|
|
||||||
else:
|
|
||||||
thought = output_buffer.getvalue()
|
|
||||||
|
|
||||||
except Exception as exc:
|
|
||||||
raise ValueError(f"Error: {str(exc)}") from exc
|
|
||||||
return result, thought
|
|
||||||
|
|
||||||
|
|
||||||
def format_actions(actions: List[Tuple[AgentAction, str]]) -> str:
|
|
||||||
"""Format a list of (AgentAction, answer) tuples into a string."""
|
|
||||||
output = []
|
|
||||||
for action, answer in actions:
|
|
||||||
log = action.log
|
|
||||||
tool = action.tool
|
|
||||||
tool_input = action.tool_input
|
|
||||||
output.append(f"Log: {log}")
|
|
||||||
if "Action" not in log and "Action Input" not in log:
|
|
||||||
output.append(f"Tool: {tool}")
|
|
||||||
output.append(f"Tool Input: {tool_input}")
|
|
||||||
output.append(f"Answer: {answer}")
|
|
||||||
output.append("") # Add a blank line
|
|
||||||
return "\n".join(output)
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Type
|
||||||
|
|
||||||
from langflow.interface.base import LangChainTypeCreator
|
from langflow.interface.base import LangChainTypeCreator
|
||||||
|
from langflow.template.frontend_node.textsplitters import TextSplittersFrontendNode
|
||||||
from langflow.interface.custom_lists import textsplitter_type_to_cls_dict
|
from langflow.interface.custom_lists import textsplitter_type_to_cls_dict
|
||||||
from langflow.settings import settings
|
from langflow.settings import settings
|
||||||
from langflow.utils.logger import logger
|
from langflow.utils.logger import logger
|
||||||
|
|
@ -10,6 +11,10 @@ from langflow.utils.util import build_template_from_class
|
||||||
class TextSplitterCreator(LangChainTypeCreator):
|
class TextSplitterCreator(LangChainTypeCreator):
|
||||||
type_name: str = "textsplitters"
|
type_name: str = "textsplitters"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def frontend_node_class(self) -> Type[TextSplittersFrontendNode]:
|
||||||
|
return TextSplittersFrontendNode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type_to_loader_dict(self) -> Dict:
|
def type_to_loader_dict(self) -> Dict:
|
||||||
return textsplitter_type_to_cls_dict
|
return textsplitter_type_to_cls_dict
|
||||||
|
|
@ -17,47 +22,7 @@ class TextSplitterCreator(LangChainTypeCreator):
|
||||||
def get_signature(self, name: str) -> Optional[Dict]:
|
def get_signature(self, name: str) -> Optional[Dict]:
|
||||||
"""Get the signature of a text splitter."""
|
"""Get the signature of a text splitter."""
|
||||||
try:
|
try:
|
||||||
signature = build_template_from_class(name, textsplitter_type_to_cls_dict)
|
return build_template_from_class(name, textsplitter_type_to_cls_dict)
|
||||||
|
|
||||||
signature["template"]["documents"] = {
|
|
||||||
"type": "BaseLoader",
|
|
||||||
"required": True,
|
|
||||||
"show": True,
|
|
||||||
"name": "documents",
|
|
||||||
}
|
|
||||||
if name == "RecursiveCharacterTextSplitter":
|
|
||||||
separator_name = "separators"
|
|
||||||
else:
|
|
||||||
separator_name = "separator"
|
|
||||||
|
|
||||||
signature["template"][separator_name] = {
|
|
||||||
"type": "str",
|
|
||||||
"required": True,
|
|
||||||
"show": True,
|
|
||||||
"value": ".",
|
|
||||||
"name": separator_name,
|
|
||||||
"display_name": separator_name.title(),
|
|
||||||
}
|
|
||||||
|
|
||||||
signature["template"]["chunk_size"] = {
|
|
||||||
"type": "int",
|
|
||||||
"required": True,
|
|
||||||
"show": True,
|
|
||||||
"value": 1000,
|
|
||||||
"name": "chunk_size",
|
|
||||||
"display_name": "Chunk Size",
|
|
||||||
}
|
|
||||||
|
|
||||||
signature["template"]["chunk_overlap"] = {
|
|
||||||
"type": "int",
|
|
||||||
"required": True,
|
|
||||||
"show": True,
|
|
||||||
"value": 200,
|
|
||||||
"name": "chunk_overlap",
|
|
||||||
"display_name": "Chunk Overlap",
|
|
||||||
}
|
|
||||||
|
|
||||||
return signature
|
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ValueError(f"Text Splitter {name} not found") from exc
|
raise ValueError(f"Text Splitter {name} not found") from exc
|
||||||
except AttributeError as exc:
|
except AttributeError as exc:
|
||||||
|
|
|
||||||
|
|
@ -42,24 +42,27 @@ class ToolkitCreator(LangChainTypeCreator):
|
||||||
|
|
||||||
def get_signature(self, name: str) -> Optional[Dict]:
|
def get_signature(self, name: str) -> Optional[Dict]:
|
||||||
try:
|
try:
|
||||||
return build_template_from_class(name, self.type_to_loader_dict)
|
template = build_template_from_class(name, self.type_to_loader_dict)
|
||||||
|
# add Tool to base_classes
|
||||||
|
if "toolkit" in name.lower() and template:
|
||||||
|
template["base_classes"].append("Tool")
|
||||||
|
return template
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ValueError("Prompt not found") from exc
|
raise ValueError("Toolkit not found") from exc
|
||||||
except AttributeError as exc:
|
except AttributeError as exc:
|
||||||
logger.error(f"Prompt {name} not loaded: {exc}")
|
logger.error(f"Toolkit {name} not loaded: {exc}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def to_list(self) -> List[str]:
|
def to_list(self) -> List[str]:
|
||||||
return list(self.type_to_loader_dict.keys())
|
return list(self.type_to_loader_dict.keys())
|
||||||
|
|
||||||
def get_create_function(self, name: str) -> Callable:
|
def get_create_function(self, name: str) -> Callable:
|
||||||
if loader_name := self.create_functions.get(name, None):
|
if loader_name := self.create_functions.get(name):
|
||||||
# import loader
|
|
||||||
return import_module(
|
return import_module(
|
||||||
f"from langchain.agents.agent_toolkits import {loader_name[0]}"
|
f"from langchain.agents.agent_toolkits import {loader_name[0]}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Loader not found")
|
raise ValueError("Toolkit not found")
|
||||||
|
|
||||||
def has_create_function(self, name: str) -> bool:
|
def has_create_function(self, name: str) -> bool:
|
||||||
# check if the function list is not empty
|
# check if the function list is not empty
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,8 @@ class ToolCreator(LangChainTypeCreator):
|
||||||
|
|
||||||
for tool, tool_fcn in ALL_TOOLS_NAMES.items():
|
for tool, tool_fcn in ALL_TOOLS_NAMES.items():
|
||||||
tool_params = get_tool_params(tool_fcn)
|
tool_params = get_tool_params(tool_fcn)
|
||||||
tool_name = tool_params.get("name", tool)
|
|
||||||
|
tool_name = tool_params.get("name") or tool
|
||||||
|
|
||||||
if tool_name in settings.tools or settings.dev:
|
if tool_name in settings.tools or settings.dev:
|
||||||
if tool_name == "JsonSpec":
|
if tool_name == "JsonSpec":
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,14 @@ from langchain.agents.load_tools import (
|
||||||
from langchain.tools.json.tool import JsonSpec
|
from langchain.tools.json.tool import JsonSpec
|
||||||
|
|
||||||
from langflow.interface.importing.utils import import_class
|
from langflow.interface.importing.utils import import_class
|
||||||
from langflow.interface.tools.custom import PythonFunction
|
from langflow.interface.tools.custom import PythonFunctionTool, PythonFunction
|
||||||
|
|
||||||
FILE_TOOLS = {"JsonSpec": JsonSpec}
|
FILE_TOOLS = {"JsonSpec": JsonSpec}
|
||||||
CUSTOM_TOOLS = {"Tool": Tool, "PythonFunction": PythonFunction}
|
CUSTOM_TOOLS = {
|
||||||
|
"Tool": Tool,
|
||||||
|
"PythonFunctionTool": PythonFunctionTool,
|
||||||
|
"PythonFunction": PythonFunction,
|
||||||
|
}
|
||||||
|
|
||||||
OTHER_TOOLS = {tool: import_class(f"langchain.tools.{tool}") for tool in tools.__all__}
|
OTHER_TOOLS = {tool: import_class(f"langchain.tools.{tool}") for tool in tools.__all__}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
from langflow.interface.importing.utils import get_function
|
||||||
|
|
||||||
from pydantic import BaseModel, validator
|
from pydantic import BaseModel, validator
|
||||||
|
|
||||||
from langflow.utils import validate
|
from langflow.utils import validate
|
||||||
|
from langchain.agents.tools import Tool
|
||||||
|
|
||||||
|
|
||||||
class Function(BaseModel):
|
class Function(BaseModel):
|
||||||
|
|
@ -31,6 +33,21 @@ class Function(BaseModel):
|
||||||
return validate.create_function(self.code, function_name)
|
return validate.create_function(self.code, function_name)
|
||||||
|
|
||||||
|
|
||||||
|
class PythonFunctionTool(Function, Tool):
|
||||||
|
"""Python function"""
|
||||||
|
|
||||||
|
name: str = "Custom Tool"
|
||||||
|
description: str
|
||||||
|
code: str
|
||||||
|
|
||||||
|
def ___init__(self, name: str, description: str, code: str):
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
self.code = code
|
||||||
|
self.func = get_function(self.code)
|
||||||
|
super().__init__(name=name, description=description, func=self.func)
|
||||||
|
|
||||||
|
|
||||||
class PythonFunction(Function):
|
class PythonFunction(Function):
|
||||||
"""Python function"""
|
"""Python function"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
import re
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from langchain.base_language import BaseLanguageModel
|
from langchain.base_language import BaseLanguageModel
|
||||||
|
|
@ -44,7 +45,16 @@ def try_setting_streaming_options(langchain_object, websocket):
|
||||||
langchain_object.llm_chain, "llm"
|
langchain_object.llm_chain, "llm"
|
||||||
):
|
):
|
||||||
llm = langchain_object.llm_chain.llm
|
llm = langchain_object.llm_chain.llm
|
||||||
if isinstance(llm, BaseLanguageModel) and hasattr(llm, "streaming"):
|
|
||||||
llm.streaming = True
|
if isinstance(llm, BaseLanguageModel):
|
||||||
|
if hasattr(llm, "streaming") and isinstance(llm.streaming, bool):
|
||||||
|
llm.streaming = True
|
||||||
|
elif hasattr(llm, "stream") and isinstance(llm.stream, bool):
|
||||||
|
llm.stream = True
|
||||||
|
|
||||||
return langchain_object
|
return langchain_object
|
||||||
|
|
||||||
|
|
||||||
|
def extract_input_variables_from_prompt(prompt: str) -> list[str]:
|
||||||
|
"""Extract input variables from prompt."""
|
||||||
|
return re.findall(r"{(.*?)}", prompt)
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from langflow.api.chat import router as chat_router
|
from langflow.api import router
|
||||||
from langflow.api.endpoints import router as endpoints_router
|
from langflow.database.base import create_db_and_tables
|
||||||
from langflow.api.validate import router as validate_router
|
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
"""Create the FastAPI app and include the router."""
|
"""Create the FastAPI app and include the router."""
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
origins = [
|
origins = [
|
||||||
"*",
|
"*",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def get_health():
|
||||||
|
return {"status": "OK"}
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=origins,
|
allow_origins=origins,
|
||||||
|
|
@ -22,9 +26,8 @@ def create_app():
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.include_router(endpoints_router)
|
app.include_router(router)
|
||||||
app.include_router(validate_router)
|
app.on_event("startup")(create_db_and_tables)
|
||||||
app.include_router(chat_router)
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
0
src/backend/langflow/processing/__init__.py
Normal file
0
src/backend/langflow/processing/__init__.py
Normal file
55
src/backend/langflow/processing/base.py
Normal file
55
src/backend/langflow/processing/base.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
from langflow.api.v1.callback import (
|
||||||
|
AsyncStreamingLLMCallbackHandler,
|
||||||
|
StreamingLLMCallbackHandler,
|
||||||
|
)
|
||||||
|
from langflow.processing.process import fix_memory_inputs, format_actions
|
||||||
|
from langflow.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
async def get_result_and_steps(langchain_object, message: str, **kwargs):
|
||||||
|
"""Get result and thought from extracted json"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(langchain_object, "verbose"):
|
||||||
|
langchain_object.verbose = True
|
||||||
|
chat_input = None
|
||||||
|
memory_key = ""
|
||||||
|
if hasattr(langchain_object, "memory") and langchain_object.memory is not None:
|
||||||
|
memory_key = langchain_object.memory.memory_key
|
||||||
|
|
||||||
|
if hasattr(langchain_object, "input_keys"):
|
||||||
|
for key in langchain_object.input_keys:
|
||||||
|
if key not in [memory_key, "chat_history"]:
|
||||||
|
chat_input = {key: message}
|
||||||
|
else:
|
||||||
|
chat_input = message # type: ignore
|
||||||
|
|
||||||
|
if hasattr(langchain_object, "return_intermediate_steps"):
|
||||||
|
# https://github.com/hwchase17/langchain/issues/2068
|
||||||
|
# Deactivating until we have a frontend solution
|
||||||
|
# to display intermediate steps
|
||||||
|
langchain_object.return_intermediate_steps = True
|
||||||
|
|
||||||
|
fix_memory_inputs(langchain_object)
|
||||||
|
try:
|
||||||
|
async_callbacks = [AsyncStreamingLLMCallbackHandler(**kwargs)]
|
||||||
|
output = await langchain_object.acall(chat_input, callbacks=async_callbacks)
|
||||||
|
except Exception as exc:
|
||||||
|
# make the error message more informative
|
||||||
|
logger.debug(f"Error: {str(exc)}")
|
||||||
|
sync_callbacks = [StreamingLLMCallbackHandler(**kwargs)]
|
||||||
|
output = langchain_object(chat_input, callbacks=sync_callbacks)
|
||||||
|
|
||||||
|
intermediate_steps = (
|
||||||
|
output.get("intermediate_steps", []) if isinstance(output, dict) else []
|
||||||
|
)
|
||||||
|
|
||||||
|
result = (
|
||||||
|
output.get(langchain_object.output_keys[0])
|
||||||
|
if isinstance(output, dict)
|
||||||
|
else output
|
||||||
|
)
|
||||||
|
thought = format_actions(intermediate_steps) if intermediate_steps else ""
|
||||||
|
except Exception as exc:
|
||||||
|
raise ValueError(f"Error: {str(exc)}") from exc
|
||||||
|
return result, thought
|
||||||
239
src/backend/langflow/processing/process.py
Normal file
239
src/backend/langflow/processing/process.py
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
from langchain.schema import AgentAction
|
||||||
|
import json
|
||||||
|
from langflow.interface.run import (
|
||||||
|
build_langchain_object_with_caching,
|
||||||
|
get_memory_key,
|
||||||
|
update_memory_keys,
|
||||||
|
)
|
||||||
|
from langflow.utils.logger import logger
|
||||||
|
from langflow.graph import Graph
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
|
||||||
|
def fix_memory_inputs(langchain_object):
|
||||||
|
"""
|
||||||
|
Given a LangChain object, this function checks if it has a memory attribute and if that memory key exists in the
|
||||||
|
object's input variables. If so, it does nothing. Otherwise, it gets a possible new memory key using the
|
||||||
|
get_memory_key function and updates the memory keys using the update_memory_keys function.
|
||||||
|
"""
|
||||||
|
if not hasattr(langchain_object, "memory") or langchain_object.memory is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if langchain_object.memory.memory_key in langchain_object.input_variables:
|
||||||
|
return
|
||||||
|
except AttributeError:
|
||||||
|
input_variables = (
|
||||||
|
langchain_object.prompt.input_variables
|
||||||
|
if hasattr(langchain_object, "prompt")
|
||||||
|
else langchain_object.input_keys
|
||||||
|
)
|
||||||
|
if langchain_object.memory.memory_key in input_variables:
|
||||||
|
return
|
||||||
|
|
||||||
|
possible_new_mem_key = get_memory_key(langchain_object)
|
||||||
|
if possible_new_mem_key is not None:
|
||||||
|
update_memory_keys(langchain_object, possible_new_mem_key)
|
||||||
|
|
||||||
|
|
||||||
|
def format_actions(actions: List[Tuple[AgentAction, str]]) -> str:
|
||||||
|
"""Format a list of (AgentAction, answer) tuples into a string."""
|
||||||
|
output = []
|
||||||
|
for action, answer in actions:
|
||||||
|
log = action.log
|
||||||
|
tool = action.tool
|
||||||
|
tool_input = action.tool_input
|
||||||
|
output.append(f"Log: {log}")
|
||||||
|
if "Action" not in log and "Action Input" not in log:
|
||||||
|
output.append(f"Tool: {tool}")
|
||||||
|
output.append(f"Tool Input: {tool_input}")
|
||||||
|
output.append(f"Answer: {answer}")
|
||||||
|
output.append("") # Add a blank line
|
||||||
|
return "\n".join(output)
|
||||||
|
|
||||||
|
|
||||||
|
def get_result_and_thought(langchain_object, message: str):
|
||||||
|
"""Get result and thought from extracted json"""
|
||||||
|
try:
|
||||||
|
if hasattr(langchain_object, "verbose"):
|
||||||
|
langchain_object.verbose = True
|
||||||
|
chat_input = None
|
||||||
|
memory_key = ""
|
||||||
|
if hasattr(langchain_object, "memory") and langchain_object.memory is not None:
|
||||||
|
memory_key = langchain_object.memory.memory_key
|
||||||
|
|
||||||
|
if hasattr(langchain_object, "input_keys"):
|
||||||
|
for key in langchain_object.input_keys:
|
||||||
|
if key not in [memory_key, "chat_history"]:
|
||||||
|
chat_input = {key: message}
|
||||||
|
else:
|
||||||
|
chat_input = message # type: ignore
|
||||||
|
|
||||||
|
if hasattr(langchain_object, "return_intermediate_steps"):
|
||||||
|
# https://github.com/hwchase17/langchain/issues/2068
|
||||||
|
# Deactivating until we have a frontend solution
|
||||||
|
# to display intermediate steps
|
||||||
|
langchain_object.return_intermediate_steps = False
|
||||||
|
|
||||||
|
fix_memory_inputs(langchain_object)
|
||||||
|
|
||||||
|
with io.StringIO() as output_buffer, contextlib.redirect_stdout(output_buffer):
|
||||||
|
try:
|
||||||
|
# if hasattr(langchain_object, "acall"):
|
||||||
|
# output = await langchain_object.acall(chat_input)
|
||||||
|
# else:
|
||||||
|
output = langchain_object(chat_input)
|
||||||
|
except ValueError as exc:
|
||||||
|
# make the error message more informative
|
||||||
|
logger.debug(f"Error: {str(exc)}")
|
||||||
|
output = langchain_object.run(chat_input)
|
||||||
|
|
||||||
|
intermediate_steps = (
|
||||||
|
output.get("intermediate_steps", []) if isinstance(output, dict) else []
|
||||||
|
)
|
||||||
|
|
||||||
|
result = (
|
||||||
|
output.get(langchain_object.output_keys[0])
|
||||||
|
if isinstance(output, dict)
|
||||||
|
else output
|
||||||
|
)
|
||||||
|
if intermediate_steps:
|
||||||
|
thought = format_actions(intermediate_steps)
|
||||||
|
else:
|
||||||
|
thought = output_buffer.getvalue()
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
raise ValueError(f"Error: {str(exc)}") from exc
|
||||||
|
return result, thought
|
||||||
|
|
||||||
|
|
||||||
|
def process_graph_cached(data_graph: Dict[str, Any], message: str):
|
||||||
|
"""
|
||||||
|
Process graph by extracting input variables and replacing ZeroShotPrompt
|
||||||
|
with PromptTemplate,then run the graph and return the result and thought.
|
||||||
|
"""
|
||||||
|
# Load langchain object
|
||||||
|
langchain_object = build_langchain_object_with_caching(data_graph)
|
||||||
|
logger.debug("Loaded langchain object")
|
||||||
|
|
||||||
|
if langchain_object is None:
|
||||||
|
# Raise user facing error
|
||||||
|
raise ValueError(
|
||||||
|
"There was an error loading the langchain_object. Please, check all the nodes and try again."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate result and thought
|
||||||
|
logger.debug("Generating result and thought")
|
||||||
|
result, thought = get_result_and_thought(langchain_object, message)
|
||||||
|
logger.debug("Generated result and thought")
|
||||||
|
return {"result": str(result), "thought": thought.strip()}
|
||||||
|
|
||||||
|
|
||||||
|
def load_flow_from_json(
|
||||||
|
input: Union[Path, str, dict], tweaks: Optional[dict] = None, build=True
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Load flow from a JSON file or a JSON object.
|
||||||
|
|
||||||
|
:param input: JSON file path or JSON object
|
||||||
|
:param tweaks: Optional tweaks to be processed
|
||||||
|
:param build: If True, build the graph, otherwise return the graph object
|
||||||
|
:return: Langchain object or Graph object depending on the build parameter
|
||||||
|
"""
|
||||||
|
# If input is a file path, load JSON from the file
|
||||||
|
if isinstance(input, (str, Path)):
|
||||||
|
with open(input, "r", encoding="utf-8") as f:
|
||||||
|
flow_graph = json.load(f)
|
||||||
|
# If input is a dictionary, assume it's a JSON object
|
||||||
|
elif isinstance(input, dict):
|
||||||
|
flow_graph = input
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
"Input must be either a file path (str) or a JSON object (dict)"
|
||||||
|
)
|
||||||
|
|
||||||
|
graph_data = flow_graph["data"]
|
||||||
|
if tweaks is not None:
|
||||||
|
graph_data = process_tweaks(graph_data, tweaks)
|
||||||
|
nodes = graph_data["nodes"]
|
||||||
|
edges = graph_data["edges"]
|
||||||
|
graph = Graph(nodes, edges)
|
||||||
|
|
||||||
|
if build:
|
||||||
|
langchain_object = graph.build()
|
||||||
|
|
||||||
|
if hasattr(langchain_object, "verbose"):
|
||||||
|
langchain_object.verbose = True
|
||||||
|
|
||||||
|
if hasattr(langchain_object, "return_intermediate_steps"):
|
||||||
|
# Deactivating until we have a frontend solution
|
||||||
|
# to display intermediate steps
|
||||||
|
langchain_object.return_intermediate_steps = False
|
||||||
|
|
||||||
|
fix_memory_inputs(langchain_object)
|
||||||
|
return langchain_object
|
||||||
|
|
||||||
|
return graph
|
||||||
|
|
||||||
|
|
||||||
|
def validate_input(
|
||||||
|
graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]]
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
if not isinstance(graph_data, dict) or not isinstance(tweaks, dict):
|
||||||
|
raise ValueError("graph_data and tweaks should be dictionaries")
|
||||||
|
|
||||||
|
nodes = graph_data.get("data", {}).get("nodes") or graph_data.get("nodes")
|
||||||
|
|
||||||
|
if not isinstance(nodes, list):
|
||||||
|
raise ValueError(
|
||||||
|
"graph_data should contain a list of nodes under 'data' key or directly under 'nodes' key"
|
||||||
|
)
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def apply_tweaks(node: Dict[str, Any], node_tweaks: Dict[str, Any]) -> None:
|
||||||
|
template_data = node.get("data", {}).get("node", {}).get("template")
|
||||||
|
|
||||||
|
if not isinstance(template_data, dict):
|
||||||
|
logger.warning(
|
||||||
|
f"Template data for node {node.get('id')} should be a dictionary"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for tweak_name, tweak_value in node_tweaks.items():
|
||||||
|
if tweak_name and tweak_value and tweak_name in template_data:
|
||||||
|
template_data[tweak_name]["value"] = tweak_value
|
||||||
|
|
||||||
|
|
||||||
|
def process_tweaks(
|
||||||
|
graph_data: Dict[str, Any], tweaks: Dict[str, Dict[str, Any]]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
This function is used to tweak the graph data using the node id and the tweaks dict.
|
||||||
|
|
||||||
|
:param graph_data: The dictionary containing the graph data. It must contain a 'data' key with
|
||||||
|
'nodes' as its child or directly contain 'nodes' key. Each node should have an 'id' and 'data'.
|
||||||
|
:param tweaks: A dictionary where the key is the node id and the value is a dictionary of the tweaks.
|
||||||
|
The inner dictionary contains the name of a certain parameter as the key and the value to be tweaked.
|
||||||
|
|
||||||
|
:return: The modified graph_data dictionary.
|
||||||
|
|
||||||
|
:raises ValueError: If the input is not in the expected format.
|
||||||
|
"""
|
||||||
|
nodes = validate_input(graph_data, tweaks)
|
||||||
|
|
||||||
|
for node in nodes:
|
||||||
|
if isinstance(node, dict) and isinstance(node.get("id"), str):
|
||||||
|
node_id = node["id"]
|
||||||
|
if node_tweaks := tweaks.get(node_id):
|
||||||
|
apply_tweaks(node, node_tweaks)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Each node should be a dictionary with an 'id' key of type str"
|
||||||
|
)
|
||||||
|
|
||||||
|
return graph_data
|
||||||
|
|
@ -20,10 +20,13 @@ class Settings(BaseSettings):
|
||||||
textsplitters: List[str] = []
|
textsplitters: List[str] = []
|
||||||
utilities: List[str] = []
|
utilities: List[str] = []
|
||||||
dev: bool = False
|
dev: bool = False
|
||||||
|
database_url: str = "sqlite:///./langflow.db"
|
||||||
|
remove_api_keys: bool = False
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
validate_assignment = True
|
validate_assignment = True
|
||||||
extra = "ignore"
|
extra = "ignore"
|
||||||
|
env_prefix = "LANGFLOW_"
|
||||||
|
|
||||||
@root_validator(allow_reuse=True)
|
@root_validator(allow_reuse=True)
|
||||||
def validate_lists(cls, values):
|
def validate_lists(cls, values):
|
||||||
|
|
@ -46,6 +49,11 @@ class Settings(BaseSettings):
|
||||||
self.utilities = new_settings.utilities or []
|
self.utilities = new_settings.utilities or []
|
||||||
self.dev = dev
|
self.dev = dev
|
||||||
|
|
||||||
|
def update_settings(self, **kwargs):
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
if hasattr(self, key):
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
|
||||||
def save_settings_to_yaml(settings: Settings, file_path: str):
|
def save_settings_to_yaml(settings: Settings, file_path: str):
|
||||||
with open(file_path, "w") as f:
|
with open(file_path, "w") as f:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ from langflow.template.frontend_node import (
|
||||||
prompts,
|
prompts,
|
||||||
tools,
|
tools,
|
||||||
vectorstores,
|
vectorstores,
|
||||||
|
documentloaders,
|
||||||
|
textsplitters,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|
@ -18,4 +20,6 @@ __all__ = [
|
||||||
"llms",
|
"llms",
|
||||||
"prompts",
|
"prompts",
|
||||||
"vectorstores",
|
"vectorstores",
|
||||||
|
"documentloaders",
|
||||||
|
"textsplitters",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ class SQLAgentNode(FrontendNode):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
description: str = """Construct a sql agent from an LLM and tools."""
|
description: str = """Construct an SQL agent from an LLM and tools."""
|
||||||
base_classes: list[str] = ["AgentExecutor"]
|
base_classes: list[str] = ["AgentExecutor"]
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
|
@ -146,7 +146,7 @@ class CSVAgentNode(FrontendNode):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
description: str = """Construct a json agent from a CSV and tools."""
|
description: str = """Construct a CSV agent from a CSV and tools."""
|
||||||
base_classes: list[str] = ["AgentExecutor"]
|
base_classes: list[str] = ["AgentExecutor"]
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
|
@ -154,9 +154,10 @@ class CSVAgentNode(FrontendNode):
|
||||||
|
|
||||||
|
|
||||||
class InitializeAgentNode(FrontendNode):
|
class InitializeAgentNode(FrontendNode):
|
||||||
name: str = "initialize_agent"
|
name: str = "AgentInitializer"
|
||||||
|
display_name: str = "AgentInitializer"
|
||||||
template: Template = Template(
|
template: Template = Template(
|
||||||
type_name="initailize_agent",
|
type_name="initialize_agent",
|
||||||
fields=[
|
fields=[
|
||||||
TemplateField(
|
TemplateField(
|
||||||
field_type="str",
|
field_type="str",
|
||||||
|
|
@ -194,7 +195,7 @@ class InitializeAgentNode(FrontendNode):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
description: str = """Construct a json agent from an LLM and tools."""
|
description: str = """Construct a zero shot agent from an LLM and tools."""
|
||||||
base_classes: list[str] = ["AgentExecutor", "function"]
|
base_classes: list[str] = ["AgentExecutor", "function"]
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ class FrontendNode(BaseModel):
|
||||||
description: str
|
description: str
|
||||||
base_classes: List[str]
|
base_classes: List[str]
|
||||||
name: str = ""
|
name: str = ""
|
||||||
|
display_name: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
|
|
@ -21,12 +22,16 @@ class FrontendNode(BaseModel):
|
||||||
"template": self.template.to_dict(self.format_field),
|
"template": self.template.to_dict(self.format_field),
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
"base_classes": self.base_classes,
|
"base_classes": self.base_classes,
|
||||||
}
|
"display_name": self.display_name or self.name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def add_extra_fields(self) -> None:
|
def add_extra_fields(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def add_extra_base_classes(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
||||||
"""Formats a given field based on its attributes and value."""
|
"""Formats a given field based on its attributes and value."""
|
||||||
|
|
@ -117,14 +122,30 @@ class FrontendNode(BaseModel):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handles specific field values for certain fields."""
|
"""Handles specific field values for certain fields."""
|
||||||
if key == "headers":
|
if key == "headers":
|
||||||
field.value = """{'Authorization':
|
field.value = """{'Authorization': 'Bearer <token>'}"""
|
||||||
'Bearer <token>'}"""
|
FrontendNode._handle_model_specific_field_values(field, key, name)
|
||||||
if name == "OpenAI" and key == "model_name":
|
FrontendNode._handle_api_key_specific_field_values(field, key, name)
|
||||||
field.options = constants.OPENAI_MODELS
|
|
||||||
field.is_list = True
|
@staticmethod
|
||||||
elif name == "ChatOpenAI" and key == "model_name":
|
def _handle_model_specific_field_values(
|
||||||
field.options = constants.CHAT_OPENAI_MODELS
|
field: TemplateField, key: str, name: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""Handles specific field values related to models."""
|
||||||
|
model_dict = {
|
||||||
|
"OpenAI": constants.OPENAI_MODELS,
|
||||||
|
"ChatOpenAI": constants.CHAT_OPENAI_MODELS,
|
||||||
|
"Anthropic": constants.ANTHROPIC_MODELS,
|
||||||
|
"ChatAnthropic": constants.ANTHROPIC_MODELS,
|
||||||
|
}
|
||||||
|
if name in model_dict and key == "model_name":
|
||||||
|
field.options = model_dict[name]
|
||||||
field.is_list = True
|
field.is_list = True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_api_key_specific_field_values(
|
||||||
|
field: TemplateField, key: str, name: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""Handles specific field values related to API keys."""
|
||||||
if "api_key" in key and "OpenAI" in str(name):
|
if "api_key" in key and "OpenAI" in str(name):
|
||||||
field.display_name = "OpenAI API Key"
|
field.display_name = "OpenAI API Key"
|
||||||
field.required = False
|
field.required = False
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,24 @@ from typing import Optional
|
||||||
|
|
||||||
from langflow.template.field.base import TemplateField
|
from langflow.template.field.base import TemplateField
|
||||||
from langflow.template.frontend_node.base import FrontendNode
|
from langflow.template.frontend_node.base import FrontendNode
|
||||||
|
from langflow.template.frontend_node.constants import QA_CHAIN_TYPES
|
||||||
from langflow.template.template.base import Template
|
from langflow.template.template.base import Template
|
||||||
|
|
||||||
|
|
||||||
class ChainFrontendNode(FrontendNode):
|
class ChainFrontendNode(FrontendNode):
|
||||||
|
def add_extra_fields(self) -> None:
|
||||||
|
if self.template.type_name == "ConversationalRetrievalChain":
|
||||||
|
# add memory
|
||||||
|
self.template.add_field(
|
||||||
|
TemplateField(
|
||||||
|
field_type="BaseChatMemory",
|
||||||
|
required=False,
|
||||||
|
show=True,
|
||||||
|
name="memory",
|
||||||
|
advanced=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
||||||
FrontendNode.format_field(field, name)
|
FrontendNode.format_field(field, name)
|
||||||
|
|
@ -19,6 +33,14 @@ class ChainFrontendNode(FrontendNode):
|
||||||
field.show = True
|
field.show = True
|
||||||
field.advanced = True
|
field.advanced = True
|
||||||
|
|
||||||
|
# We should think of a way to deal with this later
|
||||||
|
# if field.field_type == "PromptTemplate":
|
||||||
|
# field.field_type = "str"
|
||||||
|
# field.multiline = True
|
||||||
|
# field.show = True
|
||||||
|
# field.advanced = False
|
||||||
|
# field.value = field.value.template
|
||||||
|
|
||||||
# Separated for possible future changes
|
# Separated for possible future changes
|
||||||
if field.name == "prompt" and field.value is None:
|
if field.name == "prompt" and field.value is None:
|
||||||
field.required = True
|
field.required = True
|
||||||
|
|
@ -112,7 +134,7 @@ class TimeTravelGuideChainNode(FrontendNode):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
description: str = "Time travel guide chain to be used in the flow."
|
description: str = "Time travel guide chain."
|
||||||
base_classes: list[str] = [
|
base_classes: list[str] = [
|
||||||
"LLMChain",
|
"LLMChain",
|
||||||
"BaseCustomChain",
|
"BaseCustomChain",
|
||||||
|
|
@ -155,3 +177,41 @@ class MidJourneyPromptChainNode(FrontendNode):
|
||||||
"ConversationChain",
|
"ConversationChain",
|
||||||
"MidJourneyPromptChain",
|
"MidJourneyPromptChain",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CombineDocsChainNode(FrontendNode):
|
||||||
|
name: str = "CombineDocsChain"
|
||||||
|
template: Template = Template(
|
||||||
|
type_name="load_qa_chain",
|
||||||
|
fields=[
|
||||||
|
TemplateField(
|
||||||
|
field_type="str",
|
||||||
|
required=True,
|
||||||
|
is_list=True,
|
||||||
|
show=True,
|
||||||
|
multiline=False,
|
||||||
|
options=QA_CHAIN_TYPES,
|
||||||
|
value=QA_CHAIN_TYPES[0],
|
||||||
|
name="chain_type",
|
||||||
|
advanced=False,
|
||||||
|
),
|
||||||
|
TemplateField(
|
||||||
|
field_type="BaseLanguageModel",
|
||||||
|
required=True,
|
||||||
|
show=True,
|
||||||
|
name="llm",
|
||||||
|
display_name="LLM",
|
||||||
|
advanced=False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
description: str = """Load question answering chain."""
|
||||||
|
base_classes: list[str] = ["BaseCombineDocumentsChain", "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
|
||||||
|
|
|
||||||
|
|
@ -30,3 +30,5 @@ You are a good listener and you can talk about anything.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
HUMAN_PROMPT = "{input}"
|
HUMAN_PROMPT = "{input}"
|
||||||
|
|
||||||
|
QA_CHAIN_TYPES = ["stuff", "map_reduce", "map_rerank", "refine"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
from langflow.template.field.base import TemplateField
|
||||||
|
from langflow.template.frontend_node.base import FrontendNode
|
||||||
|
|
||||||
|
|
||||||
|
def build_template(
|
||||||
|
suffixes: list, fileTypes: list, name: str = "file_path"
|
||||||
|
) -> TemplateField:
|
||||||
|
"""Build a template field for a document loader."""
|
||||||
|
return TemplateField(
|
||||||
|
field_type="file",
|
||||||
|
required=True,
|
||||||
|
show=True,
|
||||||
|
name=name,
|
||||||
|
value="",
|
||||||
|
suffixes=suffixes,
|
||||||
|
fileTypes=fileTypes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentLoaderFrontNode(FrontendNode):
|
||||||
|
file_path_templates = {
|
||||||
|
"AirbyteJSONLoader": build_template(suffixes=[".json"], fileTypes=["json"]),
|
||||||
|
"CoNLLULoader": build_template(suffixes=[".csv"], fileTypes=["csv"]),
|
||||||
|
"CSVLoader": build_template(suffixes=[".csv"], fileTypes=["csv"]),
|
||||||
|
"UnstructuredEmailLoader": build_template(suffixes=[".eml"], fileTypes=["eml"]),
|
||||||
|
"EverNoteLoader": build_template(suffixes=[".xml"], fileTypes=["xml"]),
|
||||||
|
"FacebookChatLoader": build_template(suffixes=[".json"], fileTypes=["json"]),
|
||||||
|
"GutenbergLoader": build_template(suffixes=[".txt"], fileTypes=["txt"]),
|
||||||
|
"BSHTMLLoader": build_template(suffixes=[".html"], fileTypes=["html"]),
|
||||||
|
"UnstructuredHTMLLoader": build_template(
|
||||||
|
suffixes=[".html"], fileTypes=["html"]
|
||||||
|
),
|
||||||
|
"UnstructuredImageLoader": build_template(
|
||||||
|
suffixes=[".jpg", ".jpeg", ".png", ".gif", ".bmp"],
|
||||||
|
fileTypes=["jpg", "jpeg", "png", "gif", "bmp"],
|
||||||
|
),
|
||||||
|
"UnstructuredMarkdownLoader": build_template(
|
||||||
|
suffixes=[".md"], fileTypes=["md"]
|
||||||
|
),
|
||||||
|
"PyPDFLoader": build_template(suffixes=[".pdf"], fileTypes=["pdf"]),
|
||||||
|
"UnstructuredPowerPointLoader": build_template(
|
||||||
|
suffixes=[".pptx", ".ppt"], fileTypes=["pptx", "ppt"]
|
||||||
|
),
|
||||||
|
"SRTLoader": build_template(suffixes=[".srt"], fileTypes=["srt"]),
|
||||||
|
"TelegramChatLoader": build_template(suffixes=[".json"], fileTypes=["json"]),
|
||||||
|
"TextLoader": build_template(suffixes=[".txt"], fileTypes=["txt"]),
|
||||||
|
"UnstructuredWordDocumentLoader": build_template(
|
||||||
|
suffixes=[".docx", ".doc"], fileTypes=["docx", "doc"]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def add_extra_fields(self) -> None:
|
||||||
|
name = None
|
||||||
|
if self.template.type_name in self.file_path_templates:
|
||||||
|
self.template.add_field(self.file_path_templates[self.template.type_name])
|
||||||
|
elif self.template.type_name in {
|
||||||
|
"WebBaseLoader",
|
||||||
|
"AZLyricsLoader",
|
||||||
|
"CollegeConfidentialLoader",
|
||||||
|
"HNLoader",
|
||||||
|
"IFixitLoader",
|
||||||
|
"IMSDbLoader",
|
||||||
|
}:
|
||||||
|
name = "web_path"
|
||||||
|
elif self.template.type_name in {"GitbookLoader"}:
|
||||||
|
name = "web_page"
|
||||||
|
elif self.template.type_name in {"ReadTheDocsLoader"}:
|
||||||
|
name = "path"
|
||||||
|
if name:
|
||||||
|
self.template.add_field(
|
||||||
|
TemplateField(
|
||||||
|
field_type="str",
|
||||||
|
required=True,
|
||||||
|
show=True,
|
||||||
|
name=name,
|
||||||
|
value="",
|
||||||
|
display_name="Web Page",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -12,17 +12,44 @@ class LLMFrontendNode(FrontendNode):
|
||||||
field.name.title().replace("Openai", "OpenAI").replace("_", " ")
|
field.name.title().replace("Openai", "OpenAI").replace("_", " ")
|
||||||
).replace("Api", "API")
|
).replace("Api", "API")
|
||||||
|
|
||||||
|
if "key" not in field.name.lower() and "token" not in field.name.lower():
|
||||||
|
field.password = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_azure_field(field: TemplateField):
|
||||||
|
if field.name == "model_name":
|
||||||
|
field.show = False # Azure uses deployment_name instead of model_name.
|
||||||
|
elif field.name == "openai_api_type":
|
||||||
|
field.show = False
|
||||||
|
field.password = False
|
||||||
|
field.value = "azure"
|
||||||
|
elif field.name == "openai_api_version":
|
||||||
|
field.password = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_llama_field(field: TemplateField):
|
||||||
|
field.show = True
|
||||||
|
field.advanced = not field.required
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
||||||
display_names_dict = {
|
display_names_dict = {
|
||||||
"huggingfacehub_api_token": "HuggingFace Hub API Token",
|
"huggingfacehub_api_token": "HuggingFace Hub API Token",
|
||||||
}
|
}
|
||||||
FrontendNode.format_field(field, name)
|
FrontendNode.format_field(field, name)
|
||||||
|
LLMFrontendNode.format_openai_field(field)
|
||||||
|
if name and "azure" in name.lower():
|
||||||
|
LLMFrontendNode.format_azure_field(field)
|
||||||
|
if name and "llama" in name.lower():
|
||||||
|
LLMFrontendNode.format_llama_field(field)
|
||||||
SHOW_FIELDS = ["repo_id"]
|
SHOW_FIELDS = ["repo_id"]
|
||||||
if field.name in SHOW_FIELDS:
|
if field.name in SHOW_FIELDS:
|
||||||
field.show = True
|
field.show = True
|
||||||
|
|
||||||
if "api" in field.name and ("key" in field.name or "token" in field.name):
|
if "api" in field.name and (
|
||||||
|
"key" in field.name
|
||||||
|
or ("token" in field.name and "tokens" not in field.name)
|
||||||
|
):
|
||||||
field.password = True
|
field.password = True
|
||||||
field.show = True
|
field.show = True
|
||||||
# Required should be False to support
|
# Required should be False to support
|
||||||
|
|
@ -34,7 +61,8 @@ class LLMFrontendNode(FrontendNode):
|
||||||
field.required = True
|
field.required = True
|
||||||
field.show = True
|
field.show = True
|
||||||
field.is_list = True
|
field.is_list = True
|
||||||
field.options = ["text-generation", "text2text-generation"]
|
field.options = ["text-generation", "text2text-generation", "summarization"]
|
||||||
|
field.value = field.options[0]
|
||||||
field.advanced = True
|
field.advanced = True
|
||||||
|
|
||||||
if display_name := display_names_dict.get(field.name):
|
if display_name := display_names_dict.get(field.name):
|
||||||
|
|
@ -43,8 +71,12 @@ class LLMFrontendNode(FrontendNode):
|
||||||
field.field_type = "code"
|
field.field_type = "code"
|
||||||
field.advanced = True
|
field.advanced = True
|
||||||
field.show = True
|
field.show = True
|
||||||
elif field.name in ["model_name", "temperature", "model_file", "model_type"]:
|
elif field.name in [
|
||||||
|
"model_name",
|
||||||
|
"temperature",
|
||||||
|
"model_file",
|
||||||
|
"model_type",
|
||||||
|
"deployment_name",
|
||||||
|
]:
|
||||||
field.advanced = False
|
field.advanced = False
|
||||||
field.show = True
|
field.show = True
|
||||||
|
|
||||||
LLMFrontendNode.format_openai_field(field)
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,20 @@ from langflow.template.frontend_node.base import FrontendNode
|
||||||
|
|
||||||
|
|
||||||
class MemoryFrontendNode(FrontendNode):
|
class MemoryFrontendNode(FrontendNode):
|
||||||
|
#! Needs testing
|
||||||
|
def add_extra_fields(self) -> None:
|
||||||
|
# add return_messages field
|
||||||
|
self.template.add_field(
|
||||||
|
TemplateField(
|
||||||
|
field_type="bool",
|
||||||
|
required=False,
|
||||||
|
show=True,
|
||||||
|
name="return_messages",
|
||||||
|
advanced=False,
|
||||||
|
value=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
||||||
FrontendNode.format_field(field, name)
|
FrontendNode.format_field(field, name)
|
||||||
|
|
@ -18,3 +32,7 @@ class MemoryFrontendNode(FrontendNode):
|
||||||
field.value = 10
|
field.value = 10
|
||||||
field.display_name = "Memory Size"
|
field.display_name = "Memory Size"
|
||||||
field.password = False
|
field.password = False
|
||||||
|
if field.name == "return_messages":
|
||||||
|
field.required = False
|
||||||
|
field.show = True
|
||||||
|
field.advanced = False
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ class BasePromptFrontendNode(FrontendNode):
|
||||||
class ZeroShotPromptNode(BasePromptFrontendNode):
|
class ZeroShotPromptNode(BasePromptFrontendNode):
|
||||||
name: str = "ZeroShotPrompt"
|
name: str = "ZeroShotPrompt"
|
||||||
template: Template = Template(
|
template: Template = Template(
|
||||||
type_name="zero_shot",
|
type_name="ZeroShotPrompt",
|
||||||
fields=[
|
fields=[
|
||||||
TemplateField(
|
TemplateField(
|
||||||
field_type="str",
|
field_type="str",
|
||||||
|
|
|
||||||
49
src/backend/langflow/template/frontend_node/textsplitters.py
Normal file
49
src/backend/langflow/template/frontend_node/textsplitters.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
from langflow.template.field.base import TemplateField
|
||||||
|
from langflow.template.frontend_node.base import FrontendNode
|
||||||
|
|
||||||
|
|
||||||
|
class TextSplittersFrontendNode(FrontendNode):
|
||||||
|
def add_extra_fields(self) -> None:
|
||||||
|
self.template.add_field(
|
||||||
|
TemplateField(
|
||||||
|
field_type="BaseLoader",
|
||||||
|
required=True,
|
||||||
|
show=True,
|
||||||
|
name="documents",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
name = "separator"
|
||||||
|
if self.template.type_name == "CharacterTextSplitter":
|
||||||
|
name = "separator"
|
||||||
|
elif self.template.type_name == "RecursiveCharacterTextSplitter":
|
||||||
|
name = "separators"
|
||||||
|
self.template.add_field(
|
||||||
|
TemplateField(
|
||||||
|
field_type="str",
|
||||||
|
required=True,
|
||||||
|
show=True,
|
||||||
|
value=".",
|
||||||
|
name=name,
|
||||||
|
display_name="Separator",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.template.add_field(
|
||||||
|
TemplateField(
|
||||||
|
field_type="int",
|
||||||
|
required=True,
|
||||||
|
show=True,
|
||||||
|
value=1000,
|
||||||
|
name="chunk_size",
|
||||||
|
display_name="Chunk Size",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.template.add_field(
|
||||||
|
TemplateField(
|
||||||
|
field_type="int",
|
||||||
|
required=True,
|
||||||
|
show=True,
|
||||||
|
value=200,
|
||||||
|
name="chunk_overlap",
|
||||||
|
display_name="Chunk Overlap",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -52,7 +52,53 @@ class ToolNode(FrontendNode):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
description: str = "Tool to be used in the flow."
|
description: str = "Converts a chain, agent or function into a tool."
|
||||||
|
base_classes: list[str] = ["Tool"]
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return super().to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
class PythonFunctionToolNode(FrontendNode):
|
||||||
|
name: str = "PythonFunctionTool"
|
||||||
|
template: Template = Template(
|
||||||
|
type_name="PythonFunctionTool",
|
||||||
|
fields=[
|
||||||
|
TemplateField(
|
||||||
|
field_type="str",
|
||||||
|
required=True,
|
||||||
|
placeholder="",
|
||||||
|
is_list=False,
|
||||||
|
show=True,
|
||||||
|
multiline=False,
|
||||||
|
value="",
|
||||||
|
name="name",
|
||||||
|
advanced=False,
|
||||||
|
),
|
||||||
|
TemplateField(
|
||||||
|
field_type="str",
|
||||||
|
required=True,
|
||||||
|
placeholder="",
|
||||||
|
is_list=False,
|
||||||
|
show=True,
|
||||||
|
multiline=False,
|
||||||
|
value="",
|
||||||
|
name="description",
|
||||||
|
advanced=False,
|
||||||
|
),
|
||||||
|
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] = ["Tool"]
|
base_classes: list[str] = ["Tool"]
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
|
@ -62,7 +108,7 @@ class ToolNode(FrontendNode):
|
||||||
class PythonFunctionNode(FrontendNode):
|
class PythonFunctionNode(FrontendNode):
|
||||||
name: str = "PythonFunction"
|
name: str = "PythonFunction"
|
||||||
template: Template = Template(
|
template: Template = Template(
|
||||||
type_name="python_function",
|
type_name="PythonFunction",
|
||||||
fields=[
|
fields=[
|
||||||
TemplateField(
|
TemplateField(
|
||||||
field_type="code",
|
field_type="code",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ class VectorStoreFrontendNode(FrontendNode):
|
||||||
|
|
||||||
self.template.add_field(extra_field)
|
self.template.add_field(extra_field)
|
||||||
|
|
||||||
|
def add_extra_base_classes(self) -> None:
|
||||||
|
self.base_classes.append("BaseRetriever")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
||||||
FrontendNode.format_field(field, name)
|
FrontendNode.format_field(field, name)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,31 @@ OPENAI_MODELS = [
|
||||||
"text-babbage-001",
|
"text-babbage-001",
|
||||||
"text-ada-001",
|
"text-ada-001",
|
||||||
]
|
]
|
||||||
CHAT_OPENAI_MODELS = ["gpt-3.5-turbo", "gpt-4", "gpt-4-32k"]
|
CHAT_OPENAI_MODELS = [
|
||||||
|
"gpt-3.5-turbo-0613",
|
||||||
|
"gpt-3.5-turbo",
|
||||||
|
"gpt-3.5-turbo-16k-0613",
|
||||||
|
"gpt-3.5-turbo-16k",
|
||||||
|
"gpt-4-0613",
|
||||||
|
"gpt-4-32k-0613",
|
||||||
|
"gpt-4",
|
||||||
|
"gpt-4-32k",
|
||||||
|
]
|
||||||
|
|
||||||
|
ANTHROPIC_MODELS = [
|
||||||
|
"claude-v1", # largest model, ideal for a wide range of more complex tasks.
|
||||||
|
"claude-v1-100k", # An enhanced version of claude-v1 with a 100,000 token (roughly 75,000 word) context window.
|
||||||
|
"claude-instant-v1", # A smaller model with far lower latency, sampling at roughly 40 words/sec!
|
||||||
|
"claude-instant-v1-100k", # Like claude-instant-v1 with a 100,000 token context window but retains its performance.
|
||||||
|
# Specific sub-versions of the above models:
|
||||||
|
"claude-v1.3", # Vs claude-v1.2: better instruction-following, code, and non-English dialogue and writing.
|
||||||
|
"claude-v1.3-100k", # An enhanced version of claude-v1.3 with a 100,000 token (roughly 75,000 word) context window.
|
||||||
|
"claude-v1.2", # Vs claude-v1.1: small adv in general helpfulness, instruction following, coding, and other tasks.
|
||||||
|
"claude-v1.0", # An earlier version of claude-v1.
|
||||||
|
"claude-instant-v1.1", # Latest version of claude-instant-v1. Better than claude-instant-v1.0 at most tasks.
|
||||||
|
"claude-instant-v1.1-100k", # Version of claude-instant-v1.1 with a 100K token context window.
|
||||||
|
"claude-instant-v1.0", # An earlier version of claude-instant-v1.
|
||||||
|
]
|
||||||
|
|
||||||
DEFAULT_PYTHON_FUNCTION = """
|
DEFAULT_PYTHON_FUNCTION = """
|
||||||
def python_function(text: str) -> str:
|
def python_function(text: str) -> str:
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,9 @@ def format_dict(d, name: Optional[str] = None):
|
||||||
elif name == "ChatOpenAI" and key == "model_name":
|
elif name == "ChatOpenAI" and key == "model_name":
|
||||||
value["options"] = constants.CHAT_OPENAI_MODELS
|
value["options"] = constants.CHAT_OPENAI_MODELS
|
||||||
value["list"] = True
|
value["list"] = True
|
||||||
|
elif (name == "Anthropic" or name == "ChatAnthropic") and key == "model_name":
|
||||||
|
value["options"] = constants.ANTHROPIC_MODELS
|
||||||
|
value["list"] = True
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import ast
|
import ast
|
||||||
|
import contextlib
|
||||||
import importlib
|
import importlib
|
||||||
import types
|
import types
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
@ -147,11 +148,8 @@ def create_function(code, function_name):
|
||||||
code_obj = compile(
|
code_obj = compile(
|
||||||
ast.Module(body=[function_code], type_ignores=[]), "<string>", "exec"
|
ast.Module(body=[function_code], type_ignores=[]), "<string>", "exec"
|
||||||
)
|
)
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
exec(code_obj, exec_globals, locals())
|
exec(code_obj, exec_globals, locals())
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
exec_globals[function_name] = locals()[function_name]
|
exec_globals[function_name] = locals()[function_name]
|
||||||
|
|
||||||
# Return a function that imports necessary modules and calls the target function
|
# Return a function that imports necessary modules and calls the target function
|
||||||
|
|
|
||||||
17449
src/frontend/package-lock.json
generated
17449
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,83 +1,106 @@
|
||||||
{
|
{
|
||||||
"name": "langflow",
|
"name": "langflow",
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@headlessui/react": "^1.7.10",
|
"@headlessui/react": "^1.7.10",
|
||||||
"@heroicons/react": "^2.0.15",
|
"@heroicons/react": "^2.0.15",
|
||||||
"@mui/material": "^5.11.9",
|
"@mui/material": "^5.11.9",
|
||||||
"@tabler/icons-react": "^2.18.0",
|
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@radix-ui/react-menubar": "^1.0.3",
|
||||||
"@tailwindcss/line-clamp": "^0.4.4",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"ace-builds": "^1.16.0",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"ansi-to-html": "^0.7.2",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"axios": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"base64-js": "^1.5.1",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
"lodash": "^4.17.21",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"react": "^18.2.0",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"react-ace": "^10.1.0",
|
"@radix-ui/react-tooltip": "^1.0.6",
|
||||||
"react-cookie": "^4.1.1",
|
"@tabler/icons-react": "^2.18.0",
|
||||||
"react-dom": "^18.2.0",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"react-error-boundary": "^4.0.2",
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"react-icons": "^4.8.0",
|
"ace-builds": "^1.16.0",
|
||||||
"react-laag": "^2.0.5",
|
"add": "^2.0.6",
|
||||||
"react-markdown": "^8.0.7",
|
"ansi-to-html": "^0.7.2",
|
||||||
"react-router-dom": "^6.8.1",
|
"axios": "^1.3.2",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"base64-js": "^1.5.1",
|
||||||
"react-tabs": "^6.0.0",
|
"class-variance-authority": "^0.6.0",
|
||||||
"react-tooltip": "^5.13.1",
|
"clsx": "^1.2.1",
|
||||||
"reactflow": "^11.5.5",
|
"esbuild": "^0.17.18",
|
||||||
"rehype-mathjax": "^4.0.2",
|
"lodash": "^4.17.21",
|
||||||
"remark-gfm": "^3.0.1",
|
"lucide-react": "^0.233.0",
|
||||||
"remark-math": "^5.1.1",
|
"react": "^18.2.0",
|
||||||
"uuid": "^9.0.0",
|
"react-ace": "^10.1.0",
|
||||||
"vite-plugin-svgr": "^3.2.0",
|
"react-cookie": "^4.1.1",
|
||||||
"web-vitals": "^2.1.4"
|
"react-dom": "^18.2.0",
|
||||||
},
|
"react-error-boundary": "^4.0.2",
|
||||||
"scripts": {
|
"react-icons": "^4.8.0",
|
||||||
"dev:docker": "vite --host 0.0.0.0",
|
"react-laag": "^2.0.5",
|
||||||
"start": "vite",
|
"react-markdown": "^8.0.7",
|
||||||
"build": "vite build",
|
"react-router-dom": "^6.8.1",
|
||||||
"serve": "vite preview",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"format": "npx prettier --write \"src/**/*.{js,jsx,ts,tsx,json,md}\""
|
"react-tabs": "^6.0.0",
|
||||||
},
|
"react-tooltip": "^5.13.1",
|
||||||
"eslintConfig": {
|
"reactflow": "^11.5.5",
|
||||||
"extends": [
|
"rehype-mathjax": "^4.0.2",
|
||||||
"react-app",
|
"remark-gfm": "^3.0.1",
|
||||||
"react-app/jest"
|
"remark-math": "^5.1.1",
|
||||||
]
|
"shadcn-ui": "^0.1.3",
|
||||||
},
|
"short-unique-id": "^4.4.4",
|
||||||
"browserslist": {
|
"switch": "^0.0.0",
|
||||||
"production": [
|
"table": "^6.8.1",
|
||||||
">0.2%",
|
"tailwind-merge": "^1.13.0",
|
||||||
"not dead",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"not op_mini all"
|
"uuid": "^9.0.0",
|
||||||
],
|
"vite-plugin-svgr": "^3.2.0",
|
||||||
"development": [
|
"web-vitals": "^2.1.4"
|
||||||
"last 1 chrome version",
|
},
|
||||||
"last 1 firefox version",
|
"scripts": {
|
||||||
"last 1 safari version"
|
"dev:docker": "vite --host 0.0.0.0",
|
||||||
]
|
"start": "vite",
|
||||||
},
|
"build": "vite build",
|
||||||
"proxy": "http://127.0.0.1:7860",
|
"serve": "vite preview",
|
||||||
"devDependencies": {
|
"format": "npx prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\""
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
},
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"eslintConfig": {
|
||||||
"@testing-library/react": "^13.4.0",
|
"extends": [
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"react-app",
|
||||||
"@types/jest": "^27.5.2",
|
"react-app/jest"
|
||||||
"@types/lodash": "^4.14.194",
|
]
|
||||||
"@types/node": "^16.18.12",
|
},
|
||||||
"@types/react": "^18.0.28",
|
"browserslist": {
|
||||||
"@types/react-dom": "^18.0.11",
|
"production": [
|
||||||
"@types/uuid": "^9.0.1",
|
">0.2%",
|
||||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
"not dead",
|
||||||
"autoprefixer": "^10.4.14",
|
"not op_mini all"
|
||||||
"postcss": "^8.4.23",
|
],
|
||||||
"tailwindcss": "^3.3.2",
|
"development": [
|
||||||
"typescript": "^5.0.2",
|
"last 1 chrome version",
|
||||||
"vite": "^4.3.5"
|
"last 1 firefox version",
|
||||||
}
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"proxy": "http://127.0.0.1:7860",
|
||||||
|
"devDependencies": {
|
||||||
|
"@swc/cli": "^0.1.62",
|
||||||
|
"@swc/core": "^1.3.62",
|
||||||
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"@types/jest": "^27.5.2",
|
||||||
|
"@types/lodash": "^4.14.194",
|
||||||
|
"@types/node": "^16.18.12",
|
||||||
|
"@types/react": "^18.0.28",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@types/uuid": "^9.0.1",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.3.9"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@ module.exports = {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,14 @@ import _ from "lodash";
|
||||||
import ErrorAlert from "./alerts/error";
|
import ErrorAlert from "./alerts/error";
|
||||||
import NoticeAlert from "./alerts/notice";
|
import NoticeAlert from "./alerts/notice";
|
||||||
import SuccessAlert from "./alerts/success";
|
import SuccessAlert from "./alerts/success";
|
||||||
import ExtraSidebar from "./components/ExtraSidebarComponent";
|
|
||||||
import { alertContext } from "./contexts/alertContext";
|
import { alertContext } from "./contexts/alertContext";
|
||||||
import { locationContext } from "./contexts/locationContext";
|
import { locationContext } from "./contexts/locationContext";
|
||||||
import TabsManagerComponent from "./pages/FlowPage/components/tabsManagerComponent";
|
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
import CrashErrorComponent from "./components/CrashErrorComponent";
|
import CrashErrorComponent from "./components/CrashErrorComponent";
|
||||||
import { TabsContext } from "./contexts/tabsContext";
|
import { TabsContext } from "./contexts/tabsContext";
|
||||||
|
import { getVersion } from "./controllers/API";
|
||||||
|
import Router from "./routes";
|
||||||
|
import Header from "./components/headerComponent";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
let { setCurrent, setShowSideBar, setIsStackedOpen } =
|
let { setCurrent, setShowSideBar, setIsStackedOpen } =
|
||||||
|
|
@ -46,15 +47,6 @@ export default function App() {
|
||||||
}>
|
}>
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
// Initialize state variable for the version
|
|
||||||
const [version, setVersion] = useState("");
|
|
||||||
useEffect(() => {
|
|
||||||
fetch("/version")
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
setVersion(data.version);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
// Use effect hook to update alertsList when a new alert is added
|
// Use effect hook to update alertsList when a new alert is added
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If there is an error alert open with data, add it to the alertsList
|
// If there is an error alert open with data, add it to the alertsList
|
||||||
|
|
@ -112,7 +104,6 @@ export default function App() {
|
||||||
return (
|
return (
|
||||||
//need parent component with width and height
|
//need parent component with width and height
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex grow-0 shrink basis-auto"></div>
|
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
onReset={() => {
|
onReset={() => {
|
||||||
window.localStorage.removeItem("tabsData");
|
window.localStorage.removeItem("tabsData");
|
||||||
|
|
@ -122,19 +113,14 @@ export default function App() {
|
||||||
}}
|
}}
|
||||||
FallbackComponent={CrashErrorComponent}
|
FallbackComponent={CrashErrorComponent}
|
||||||
>
|
>
|
||||||
<div className="flex grow shrink basis-auto min-h-0 flex-1 overflow-hidden">
|
<Header />
|
||||||
<ExtraSidebar />
|
<Router />
|
||||||
{/* Main area */}
|
|
||||||
<main className="min-w-0 flex-1 border-t border-gray-200 dark:border-gray-700 flex">
|
|
||||||
{/* Primary column */}
|
|
||||||
<div className="w-full h-full">
|
|
||||||
<TabsManagerComponent></TabsManagerComponent>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<div></div>
|
<div></div>
|
||||||
<div className="flex z-40 flex-col-reverse fixed bottom-5 left-5">
|
<div
|
||||||
|
className="flex flex-col-reverse fixed bottom-5 left-5"
|
||||||
|
style={{ zIndex: 999 }}
|
||||||
|
>
|
||||||
{alertsList.map((alert) => (
|
{alertsList.map((alert) => (
|
||||||
<div key={alert.id}>
|
<div key={alert.id}>
|
||||||
{alert.type === "error" ? (
|
{alert.type === "error" ? (
|
||||||
|
|
@ -164,14 +150,6 @@ export default function App() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<a
|
|
||||||
target={"_blank"}
|
|
||||||
href="https://logspace.ai/"
|
|
||||||
className="absolute left-7 bottom-2 flex h-6 cursor-pointer flex-col items-center justify-start overflow-hidden rounded-lg bg-gray-800 px-2 text-center font-sans text-xs tracking-wide text-gray-300 transition-all duration-500 ease-in-out hover:h-12 dark:bg-gray-100 dark:text-gray-800"
|
|
||||||
>
|
|
||||||
{version && <div className="mt-1">⛓️ LangFlow v{version}</div>}
|
|
||||||
<div className="mt-2">Created by Logspace</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
||||||
import Tooltip from "../../../../components/TooltipComponent";
|
import {
|
||||||
import { classNames, isValidConnection } from "../../../../utils";
|
classNames,
|
||||||
|
groupByFamily,
|
||||||
|
isValidConnection,
|
||||||
|
} from "../../../../utils";
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import InputComponent from "../../../../components/inputComponent";
|
import InputComponent from "../../../../components/inputComponent";
|
||||||
import ToggleComponent from "../../../../components/toggleComponent";
|
|
||||||
import InputListComponent from "../../../../components/inputListComponent";
|
import InputListComponent from "../../../../components/inputListComponent";
|
||||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||||
import { typesContext } from "../../../../contexts/typesContext";
|
import { typesContext } from "../../../../contexts/typesContext";
|
||||||
|
|
@ -15,6 +17,12 @@ import InputFileComponent from "../../../../components/inputFileComponent";
|
||||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||||
import IntComponent from "../../../../components/intComponent";
|
import IntComponent from "../../../../components/intComponent";
|
||||||
import PromptAreaComponent from "../../../../components/promptComponent";
|
import PromptAreaComponent from "../../../../components/promptComponent";
|
||||||
|
import { nodeNames, nodeIcons } from "../../../../utils";
|
||||||
|
import React from "react";
|
||||||
|
import { nodeColors } from "../../../../utils";
|
||||||
|
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
||||||
|
import { PopUpContext } from "../../../../contexts/popUpContext";
|
||||||
|
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||||
|
|
||||||
export default function ParameterComponent({
|
export default function ParameterComponent({
|
||||||
left,
|
left,
|
||||||
|
|
@ -28,14 +36,18 @@ export default function ParameterComponent({
|
||||||
required = false,
|
required = false,
|
||||||
}: ParameterComponentType) {
|
}: ParameterComponentType) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
const refHtml = useRef(null);
|
||||||
const updateNodeInternals = useUpdateNodeInternals();
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
const [position, setPosition] = useState(0);
|
const [position, setPosition] = useState(0);
|
||||||
|
const { closePopUp } = useContext(PopUpContext);
|
||||||
|
const { setTabsState, tabId } = useContext(TabsContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
||||||
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2);
|
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2);
|
||||||
updateNodeInternals(data.id);
|
updateNodeInternals(data.id);
|
||||||
}
|
}
|
||||||
}, [data.id, ref, updateNodeInternals]);
|
}, [data.id, ref, ref.current, ref.current?.offsetTop, updateNodeInternals]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateNodeInternals(data.id);
|
updateNodeInternals(data.id);
|
||||||
|
|
@ -44,15 +56,72 @@ export default function ParameterComponent({
|
||||||
const [enabled, setEnabled] = useState(
|
const [enabled, setEnabled] = useState(
|
||||||
data.node.template[name]?.value ?? false
|
data.node.template[name]?.value ?? false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {}, [closePopUp, data.node.template]);
|
||||||
|
|
||||||
const { reactFlowInstance } = useContext(typesContext);
|
const { reactFlowInstance } = useContext(typesContext);
|
||||||
let disabled =
|
let disabled =
|
||||||
reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false;
|
reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false;
|
||||||
const { save } = useContext(TabsContext);
|
const [myData, setMyData] = useState(useContext(typesContext).data);
|
||||||
|
|
||||||
|
const handleOnNewValue = (newValue: any) => {
|
||||||
|
data.node.template[name].value = newValue;
|
||||||
|
// Set state to pending
|
||||||
|
setTabsState((prev) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[tabId]: {
|
||||||
|
isPending: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const groupedObj = groupByFamily(myData, tooltipTitle);
|
||||||
|
|
||||||
|
refHtml.current = groupedObj.map((item, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className={classNames(
|
||||||
|
i > 0 ? "items-center flex mt-3" : "items-center flex"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-5 w-5"
|
||||||
|
style={{
|
||||||
|
color: nodeColors[item.family],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{React.createElement(nodeIcons[item.family])}
|
||||||
|
</div>
|
||||||
|
<span className="ps-2 text-gray-950">
|
||||||
|
{nodeNames[item.family] ?? ""}{" "}
|
||||||
|
<span className={classNames(left ? "hidden" : "")}>
|
||||||
|
{" "}
|
||||||
|
-
|
||||||
|
{item.type.split(", ").length > 2
|
||||||
|
? item.type.split(", ").map((el, i) => (
|
||||||
|
<>
|
||||||
|
<span key={i}>
|
||||||
|
{i == item.type.split(", ").length - 1
|
||||||
|
? el
|
||||||
|
: (el += `, `)}
|
||||||
|
</span>
|
||||||
|
{i % 2 == 0 && i > 0 && <br></br>}
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
: item.type}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
));
|
||||||
|
}, [tooltipTitle]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="w-full flex flex-wrap justify-between items-center bg-gray-50 dark:bg-gray-800 dark:text-white mt-1 px-5 py-2"
|
className="w-full flex flex-wrap justify-between items-center bg-muted dark:bg-gray-800 dark:text-white mt-1 px-5 py-2"
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div className={"text-sm truncate w-full " + (left ? "" : "text-end")}>
|
<div className={"text-sm truncate w-full " + (left ? "" : "text-end")}>
|
||||||
|
|
@ -69,7 +138,12 @@ export default function ParameterComponent({
|
||||||
type === "int") ? (
|
type === "int") ? (
|
||||||
<></>
|
<></>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip title={tooltipTitle + (required ? " (required)" : "")}>
|
<ShadTooltip
|
||||||
|
delayDuration={0}
|
||||||
|
content={refHtml.current}
|
||||||
|
side={left ? "left" : "right"}
|
||||||
|
open={refHtml?.current?.length > 0}
|
||||||
|
>
|
||||||
<Handle
|
<Handle
|
||||||
type={left ? "target" : "source"}
|
type={left ? "target" : "source"}
|
||||||
position={left ? Position.Left : Position.Right}
|
position={left ? Position.Left : Position.Right}
|
||||||
|
|
@ -86,7 +160,7 @@ export default function ParameterComponent({
|
||||||
top: position,
|
top: position,
|
||||||
}}
|
}}
|
||||||
></Handle>
|
></Handle>
|
||||||
</Tooltip>
|
</ShadTooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{left === true &&
|
{left === true &&
|
||||||
|
|
@ -102,19 +176,13 @@ export default function ParameterComponent({
|
||||||
? [""]
|
? [""]
|
||||||
: data.node.template[name].value
|
: data.node.template[name].value
|
||||||
}
|
}
|
||||||
onChange={(t: string[]) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
save();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : data.node.template[name].multiline ? (
|
) : data.node.template[name].multiline ? (
|
||||||
<TextAreaComponent
|
<TextAreaComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
save();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<InputComponent
|
<InputComponent
|
||||||
|
|
@ -122,59 +190,52 @@ export default function ParameterComponent({
|
||||||
disableCopyPaste={true}
|
disableCopyPaste={true}
|
||||||
password={data.node.template[name].password ?? false}
|
password={data.node.template[name].password ?? false}
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
save();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : left === true && type === "bool" ? (
|
) : left === true && type === "bool" ? (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<ToggleComponent
|
<ToggleShadComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
enabled={enabled}
|
enabled={enabled}
|
||||||
setEnabled={(t) => {
|
setEnabled={(t) => {
|
||||||
data.node.template[name].value = t;
|
handleOnNewValue(t);
|
||||||
setEnabled(t);
|
setEnabled(t);
|
||||||
save();
|
|
||||||
}}
|
}}
|
||||||
|
size="large"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : left === true && type === "float" ? (
|
) : left === true && type === "float" ? (
|
||||||
<FloatComponent
|
<div className="mt-2 w-full">
|
||||||
disabled={disabled}
|
<FloatComponent
|
||||||
disableCopyPaste={true}
|
disabled={disabled}
|
||||||
value={data.node.template[name].value ?? ""}
|
disableCopyPaste={true}
|
||||||
onChange={(t) => {
|
value={data.node.template[name].value ?? ""}
|
||||||
data.node.template[name].value = t;
|
onChange={handleOnNewValue}
|
||||||
save();
|
/>
|
||||||
}}
|
</div>
|
||||||
/>
|
|
||||||
) : left === true &&
|
) : left === true &&
|
||||||
type === "str" &&
|
type === "str" &&
|
||||||
data.node.template[name].options ? (
|
data.node.template[name].options ? (
|
||||||
<Dropdown
|
<div className="w-full">
|
||||||
options={data.node.template[name].options}
|
<Dropdown
|
||||||
onSelect={(newValue) => (data.node.template[name].value = newValue)}
|
options={data.node.template[name].options}
|
||||||
value={data.node.template[name].value ?? "Choose an option"}
|
onSelect={handleOnNewValue}
|
||||||
></Dropdown>
|
value={data.node.template[name].value ?? "Choose an option"}
|
||||||
|
></Dropdown>
|
||||||
|
</div>
|
||||||
) : left === true && type === "code" ? (
|
) : left === true && type === "code" ? (
|
||||||
<CodeAreaComponent
|
<CodeAreaComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
save();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : left === true && type === "file" ? (
|
) : left === true && type === "file" ? (
|
||||||
<InputFileComponent
|
<InputFileComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
}}
|
|
||||||
fileTypes={data.node.template[name].fileTypes}
|
fileTypes={data.node.template[name].fileTypes}
|
||||||
suffixes={data.node.template[name].suffixes}
|
suffixes={data.node.template[name].suffixes}
|
||||||
onFileChange={(t: string) => {
|
onFileChange={(t: string) => {
|
||||||
|
|
@ -183,23 +244,19 @@ export default function ParameterComponent({
|
||||||
}}
|
}}
|
||||||
></InputFileComponent>
|
></InputFileComponent>
|
||||||
) : left === true && type === "int" ? (
|
) : left === true && type === "int" ? (
|
||||||
<IntComponent
|
<div className="mt-2 w-full">
|
||||||
disabled={disabled}
|
<IntComponent
|
||||||
disableCopyPaste={true}
|
disabled={disabled}
|
||||||
value={data.node.template[name].value ?? ""}
|
disableCopyPaste={true}
|
||||||
onChange={(t) => {
|
value={data.node.template[name].value ?? ""}
|
||||||
data.node.template[name].value = t;
|
onChange={handleOnNewValue}
|
||||||
save();
|
/>
|
||||||
}}
|
</div>
|
||||||
/>
|
|
||||||
) : left === true && type === "prompt" ? (
|
) : left === true && type === "prompt" ? (
|
||||||
<PromptAreaComponent
|
<PromptAreaComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={data.node.template[name].value ?? ""}
|
value={data.node.template[name].value ?? ""}
|
||||||
onChange={(t: string) => {
|
onChange={handleOnNewValue}
|
||||||
data.node.template[name].value = t;
|
|
||||||
save();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,18 @@
|
||||||
import {
|
import { classNames, nodeColors, nodeIcons, toTitleCase } from "../../utils";
|
||||||
BugAntIcon,
|
|
||||||
Cog6ToothIcon,
|
|
||||||
InformationCircleIcon,
|
|
||||||
TrashIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
import {
|
|
||||||
CheckCircleIcon,
|
|
||||||
EllipsisHorizontalCircleIcon,
|
|
||||||
ExclamationCircleIcon,
|
|
||||||
} from "@heroicons/react/24/solid";
|
|
||||||
|
|
||||||
import {
|
|
||||||
classNames,
|
|
||||||
nodeColors,
|
|
||||||
nodeIcons,
|
|
||||||
toNormalCase,
|
|
||||||
toTitleCase,
|
|
||||||
} from "../../utils";
|
|
||||||
import ParameterComponent from "./components/parameterComponent";
|
import ParameterComponent from "./components/parameterComponent";
|
||||||
import { typesContext } from "../../contexts/typesContext";
|
import { typesContext } from "../../contexts/typesContext";
|
||||||
import { useContext, useState, useEffect, useRef, Fragment } from "react";
|
import { useContext, useState, useEffect, useRef } from "react";
|
||||||
import { NodeDataType } from "../../types/flow";
|
import { NodeDataType } from "../../types/flow";
|
||||||
import { alertContext } from "../../contexts/alertContext";
|
import { alertContext } from "../../contexts/alertContext";
|
||||||
import { PopUpContext } from "../../contexts/popUpContext";
|
import { PopUpContext } from "../../contexts/popUpContext";
|
||||||
import NodeModal from "../../modals/NodeModal";
|
import NodeModal from "../../modals/NodeModal";
|
||||||
import { useCallback } from "react";
|
|
||||||
import { TabsContext } from "../../contexts/tabsContext";
|
|
||||||
import { debounce } from "../../utils";
|
|
||||||
import TooltipReact from "../../components/ReactTooltipComponent";
|
|
||||||
import Tooltip from "../../components/TooltipComponent";
|
import Tooltip from "../../components/TooltipComponent";
|
||||||
|
import { NodeToolbar } from "reactflow";
|
||||||
|
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||||
|
|
||||||
|
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||||
|
import { useSSE } from "../../contexts/SSEContext";
|
||||||
|
|
||||||
export default function GenericNode({
|
export default function GenericNode({
|
||||||
data,
|
data,
|
||||||
selected,
|
selected,
|
||||||
|
|
@ -40,50 +23,30 @@ export default function GenericNode({
|
||||||
const { setErrorData } = useContext(alertContext);
|
const { setErrorData } = useContext(alertContext);
|
||||||
const showError = useRef(true);
|
const showError = useRef(true);
|
||||||
const { types, deleteNode } = useContext(typesContext);
|
const { types, deleteNode } = useContext(typesContext);
|
||||||
const { openPopUp } = useContext(PopUpContext);
|
|
||||||
|
const { closePopUp, openPopUp } = useContext(PopUpContext);
|
||||||
|
|
||||||
const Icon = nodeIcons[data.type] || nodeIcons[types[data.type]];
|
const Icon = nodeIcons[data.type] || nodeIcons[types[data.type]];
|
||||||
const [validationStatus, setValidationStatus] = useState(null);
|
const [validationStatus, setValidationStatus] = useState(null);
|
||||||
// State for outline color
|
// State for outline color
|
||||||
const [isValid, setIsValid] = useState(false);
|
const { sseData, isBuilding } = useSSE();
|
||||||
const { save } = useContext(TabsContext);
|
|
||||||
const { reactFlowInstance } = useContext(typesContext);
|
|
||||||
const [params, setParams] = useState([]);
|
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (reactFlowInstance) {
|
||||||
|
// setParams(Object.values(reactFlowInstance.toObject()));
|
||||||
|
// }
|
||||||
|
// }, [save]);
|
||||||
|
|
||||||
|
// New useEffect to watch for changes in sseData and update validation status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (reactFlowInstance) {
|
const relevantData = sseData[data.id];
|
||||||
setParams(Object.values(reactFlowInstance.toObject()));
|
if (relevantData) {
|
||||||
|
// Extract validation information from relevantData and update the validationStatus state
|
||||||
|
setValidationStatus(relevantData);
|
||||||
|
} else {
|
||||||
|
setValidationStatus(null);
|
||||||
}
|
}
|
||||||
}, [save]);
|
}, [sseData, data.id]);
|
||||||
|
|
||||||
const validateNode = useCallback(
|
|
||||||
debounce(async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/validate/node/${data.id}`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(reactFlowInstance.toObject()),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
let jsonResponse = await response.json();
|
|
||||||
let jsonResponseParsed = await JSON.parse(jsonResponse);
|
|
||||||
setValidationStatus(jsonResponseParsed);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// console.error("Error validating node:", error);
|
|
||||||
setValidationStatus("error");
|
|
||||||
}
|
|
||||||
}, 1000), // Adjust the debounce delay (500ms) as needed
|
|
||||||
[reactFlowInstance, data.id]
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
|
||||||
if (params.length > 0) {
|
|
||||||
validateNode();
|
|
||||||
}
|
|
||||||
}, [params, validateNode]);
|
|
||||||
|
|
||||||
if (!Icon) {
|
if (!Icon) {
|
||||||
if (showError.current) {
|
if (showError.current) {
|
||||||
|
|
@ -97,199 +60,174 @@ export default function GenericNode({
|
||||||
deleteNode(data.id);
|
deleteNode(data.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// console.log(data);
|
|
||||||
|
useEffect(() => {}, [closePopUp, data.node.template]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className={classNames(
|
<NodeToolbar>
|
||||||
selected ? "border border-blue-500" : "border dark:border-gray-700",
|
<NodeToolbarComponent
|
||||||
"prompt-node relative flex w-96 flex-col justify-center rounded-lg bg-white dark:bg-gray-900"
|
data={data}
|
||||||
)}
|
openPopUp={openPopUp}
|
||||||
>
|
deleteNode={deleteNode}
|
||||||
<div className="flex w-full items-center justify-between gap-8 rounded-t-lg border-b bg-gray-50 p-4 dark:border-b-gray-700 dark:bg-gray-800 dark:text-white ">
|
></NodeToolbarComponent>
|
||||||
<div className="flex w-full items-center gap-2 truncate text-lg">
|
</NodeToolbar>
|
||||||
<Icon
|
|
||||||
className="h-10 w-10 rounded p-1"
|
<div
|
||||||
style={{
|
className={classNames(
|
||||||
color: nodeColors[types[data.type]] ?? nodeColors.unknown,
|
selected ? "border border-ring" : "border dark:border-gray-700",
|
||||||
}}
|
"prompt-node relative flex w-96 flex-col justify-center rounded-lg bg-white dark:bg-gray-900"
|
||||||
/>
|
)}
|
||||||
<div className="ml-2 truncate">
|
>
|
||||||
<TooltipReact
|
<div className="flex w-full items-center justify-between gap-8 rounded-t-lg border-b bg-muted p-4 dark:border-b-gray-700 dark:bg-gray-800 dark:text-white ">
|
||||||
delayShow={1000}
|
<div className="flex w-full items-center gap-2 truncate text-lg">
|
||||||
selector={`node-selector-${data.type}`}
|
<Icon
|
||||||
htmlContent={data.type}
|
className="h-10 w-10 rounded p-1"
|
||||||
position="top"
|
style={{
|
||||||
>
|
color: nodeColors[types[data.type]] ?? nodeColors.unknown,
|
||||||
<div className="ml-2 truncate">{data.type}</div>
|
}}
|
||||||
</TooltipReact>
|
/>
|
||||||
</div>
|
<div className="ml-2 truncate">
|
||||||
</div>
|
<ShadTooltip delayDuration={1500} content={data.type}>
|
||||||
<div className="flex gap-3">
|
<div className="ml-2 truncate text-gray-800">{data.type}</div>
|
||||||
<button
|
</ShadTooltip>
|
||||||
className="relative"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
openPopUp(<NodeModal data={data} />);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className=" absolute -right-1 -top-2 text-red-600">
|
|
||||||
{Object.keys(data.node.template).some(
|
|
||||||
(t) =>
|
|
||||||
data.node.template[t].advanced &&
|
|
||||||
data.node.template[t].required
|
|
||||||
)
|
|
||||||
? " *"
|
|
||||||
: ""}
|
|
||||||
</div>
|
</div>
|
||||||
<Cog6ToothIcon
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
className="relative"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
openPopUp(<NodeModal data={data} />);
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
!validationStatus ? (
|
||||||
|
"Validating..."
|
||||||
|
) : (
|
||||||
|
<div className="max-h-96 overflow-auto">
|
||||||
|
{validationStatus.params ||
|
||||||
|
""
|
||||||
|
.split("\n")
|
||||||
|
.map((line, index) => <div key={index}>{line}</div>)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="w-5 h-5 relative top-[3px]">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
validationStatus && validationStatus.valid
|
||||||
|
? "w-4 h-4 rounded-full bg-green-500 opacity-100"
|
||||||
|
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
||||||
|
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
||||||
|
)}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
validationStatus && !validationStatus.valid
|
||||||
|
? "w-4 h-4 rounded-full bg-red-500 opacity-100"
|
||||||
|
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
||||||
|
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
||||||
|
)}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
!validationStatus || isBuilding
|
||||||
|
? "w-4 h-4 rounded-full bg-yellow-500 opacity-100"
|
||||||
|
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
||||||
|
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
||||||
|
)}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-full w-full py-5 text-gray-800">
|
||||||
|
<div className="w-full px-5 pb-3 text-sm text-muted-foreground">
|
||||||
|
{data.node.description}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<>
|
||||||
|
{Object.keys(data.node.template)
|
||||||
|
.filter((t) => t.charAt(0) !== "_")
|
||||||
|
.map((t: string, idx) => (
|
||||||
|
<div key={idx}>
|
||||||
|
{/* {idx === 0 ? (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"px-5 py-2 mt-2 dark:text-white text-center",
|
||||||
|
Object.keys(data.node.template).filter(
|
||||||
|
(key) =>
|
||||||
|
!key.startsWith("_") &&
|
||||||
|
data.node.template[key].show &&
|
||||||
|
!data.node.template[key].advanced
|
||||||
|
).length === 0
|
||||||
|
? "hidden"
|
||||||
|
: ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Inputs
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)} */}
|
||||||
|
{data.node.template[t].show &&
|
||||||
|
!data.node.template[t].advanced ? (
|
||||||
|
<ParameterComponent
|
||||||
|
data={data}
|
||||||
|
color={
|
||||||
|
nodeColors[types[data.node.template[t].type]] ??
|
||||||
|
nodeColors.unknown
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
data.node.template[t].display_name
|
||||||
|
? data.node.template[t].display_name
|
||||||
|
: data.node.template[t].name
|
||||||
|
? toTitleCase(data.node.template[t].name)
|
||||||
|
: toTitleCase(t)
|
||||||
|
}
|
||||||
|
name={t}
|
||||||
|
tooltipTitle={data.node.template[t].type}
|
||||||
|
required={data.node.template[t].required}
|
||||||
|
id={data.node.template[t].type + "|" + t + "|" + data.id}
|
||||||
|
left={true}
|
||||||
|
type={data.node.template[t].type}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
Object.keys(data.node.template).some(
|
Object.keys(data.node.template).length < 1 ? "hidden" : "",
|
||||||
(t) =>
|
"flex w-full justify-center"
|
||||||
data.node.template[t].advanced && data.node.template[t].show
|
|
||||||
)
|
|
||||||
? ""
|
|
||||||
: "hidden",
|
|
||||||
"w-5 h-5 dark:text-gray-300"
|
|
||||||
)}
|
)}
|
||||||
></Cog6ToothIcon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
deleteNode(data.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrashIcon className="w-5 h-5 dark:text-gray-300"></TrashIcon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
!validationStatus ? (
|
|
||||||
"Validating..."
|
|
||||||
) : (
|
|
||||||
<div className="max-h-96 overflow-auto">
|
|
||||||
{validationStatus.params.split("\n").map((line, index) => (
|
|
||||||
<div key={index}>{line}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="w-5 h-5 relative top-[3px]">
|
{" "}
|
||||||
<div
|
</div>
|
||||||
className={classNames(
|
{/* <div className="px-5 py-2 mt-2 dark:text-white text-center">
|
||||||
validationStatus && validationStatus.valid
|
Output
|
||||||
? "w-4 h-4 rounded-full bg-green-500 opacity-100"
|
</div> */}
|
||||||
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
<ParameterComponent
|
||||||
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
data={data}
|
||||||
)}
|
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||||
></div>
|
title={data.type}
|
||||||
<div
|
tooltipTitle={`${data.node.base_classes.join("\n")}`}
|
||||||
className={classNames(
|
id={[data.type, data.id, ...data.node.base_classes].join("|")}
|
||||||
validationStatus && !validationStatus.valid
|
type={data.node.base_classes.join("|")}
|
||||||
? "w-4 h-4 rounded-full bg-red-500 opacity-100"
|
left={false}
|
||||||
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
/>
|
||||||
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
</>
|
||||||
)}
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
!validationStatus
|
|
||||||
? "w-4 h-4 rounded-full bg-yellow-500 opacity-100"
|
|
||||||
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
|
||||||
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
|
||||||
)}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
<div className="h-full w-full py-5">
|
|
||||||
<div className="w-full px-5 pb-3 text-sm text-gray-500 dark:text-gray-300">
|
|
||||||
{data.node.description}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<>
|
|
||||||
{Object.keys(data.node.template)
|
|
||||||
.filter((t) => t.charAt(0) !== "_")
|
|
||||||
.map((t: string, idx) => (
|
|
||||||
<div key={idx}>
|
|
||||||
{/* {idx === 0 ? (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"px-5 py-2 mt-2 dark:text-white text-center",
|
|
||||||
Object.keys(data.node.template).filter(
|
|
||||||
(key) =>
|
|
||||||
!key.startsWith("_") &&
|
|
||||||
data.node.template[key].show &&
|
|
||||||
!data.node.template[key].advanced
|
|
||||||
).length === 0
|
|
||||||
? "hidden"
|
|
||||||
: ""
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Inputs
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)} */}
|
|
||||||
{data.node.template[t].show &&
|
|
||||||
!data.node.template[t].advanced ? (
|
|
||||||
<ParameterComponent
|
|
||||||
data={data}
|
|
||||||
color={
|
|
||||||
nodeColors[types[data.node.template[t].type]] ??
|
|
||||||
nodeColors.unknown
|
|
||||||
}
|
|
||||||
title={
|
|
||||||
data.node.template[t].display_name
|
|
||||||
? data.node.template[t].display_name
|
|
||||||
: data.node.template[t].name
|
|
||||||
? toTitleCase(data.node.template[t].name)
|
|
||||||
: toTitleCase(t)
|
|
||||||
}
|
|
||||||
name={t}
|
|
||||||
tooltipTitle={
|
|
||||||
"Type: " +
|
|
||||||
data.node.template[t].type +
|
|
||||||
(data.node.template[t].list ? " list" : "")
|
|
||||||
}
|
|
||||||
required={data.node.template[t].required}
|
|
||||||
id={data.node.template[t].type + "|" + t + "|" + data.id}
|
|
||||||
left={true}
|
|
||||||
type={data.node.template[t].type}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
Object.keys(data.node.template).length < 1 ? "hidden" : "",
|
|
||||||
"flex w-full justify-center"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
</div>
|
|
||||||
{/* <div className="px-5 py-2 mt-2 dark:text-white text-center">
|
|
||||||
Output
|
|
||||||
</div> */}
|
|
||||||
<ParameterComponent
|
|
||||||
data={data}
|
|
||||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
|
||||||
title={data.type}
|
|
||||||
tooltipTitle={`Type: ${data.node.base_classes.join(" | ")}`}
|
|
||||||
id={[data.type, data.id, ...data.node.base_classes].join("|")}
|
|
||||||
type={data.node.base_classes.join("|")}
|
|
||||||
left={false}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export default function SingleAlert({
|
||||||
{dropItem.link ? (
|
{dropItem.link ? (
|
||||||
<Link
|
<Link
|
||||||
to={dropItem.link}
|
to={dropItem.link}
|
||||||
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 dark:hover:text-blue-100 hover:text-blue-600"
|
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 dark:hover:text-blue-100 hover:text-ring"
|
||||||
>
|
>
|
||||||
Details
|
Details
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export default function NoticeAlert({
|
||||||
{link !== "" ? (
|
{link !== "" ? (
|
||||||
<Link
|
<Link
|
||||||
to={link}
|
to={link}
|
||||||
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 hover:dark:text-blue-10 hover:text-blue-600"
|
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 hover:dark:text-blue-10 hover:text-ring"
|
||||||
>
|
>
|
||||||
Details
|
Details
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue