Release 0.0.75 (#329)
🚀 Release 0.0.75 We are excited to announce the latest updates and features that have been added to Langflow! Here’s a summary of the recent changes: New Features: - Deploy Langflow using Langchain-Serve: Thanks to the hard work of @deepankarm, Langflow can now be deployed using Langchain-Serve to Jina AI Cloud. This integration brings enhanced functionality and improves the overall performance of Langflow. Just run `langflow --jcloud`. - Copy and Paste Nodes and Flows: @anovazzi1 and @lucaseduoli have implemented the copy and paste functionality in Langflow. Now you can easily duplicate nodes and flows, improving productivity and workflow efficiency. - Undo/Redo Functionality: @anovazzi1 and @lucaseduoli have also added an undo/redo feature to LangFlow, allowing you to easily revert and redo changes made to nodes and flows. This enhances the editing experience and provides greater control over your work. Improvements and Fixes: - Build: The latest update includes making Langchain-Serve optional, allowing for more flexibility in deployment. - Documentation: The README has been updated to provide information about JCloud deployment. - Validation Fixes: @ogabrielluiz has made several updates to improve validation in Langflow, including returning exception messages in responses and adding validation status icons and tooltips to node properties. - Bug Fixes: Several bug fixes have been implemented, including resizing issues in the chat input, copy and paste bugs in the code area, and padding adjustments in the UI. Special thanks to @anovazzi1 and @lucaseduoli for their contributions in fixing these issues. We appreciate the contributions from the following members: - @deepankarm - @anovazzi1 - @lucaseduoli Thank you to everyone who has contributed to this release. Your efforts have made Langflow even better! If you have any feedback or suggestions, please don’t hesitate to reach out. Happy coding with Langflow! 🎉
This commit is contained in:
commit
9d910b769a
37 changed files with 2450 additions and 1050 deletions
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
|
|
@ -47,3 +47,11 @@ jobs:
|
|||
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }}
|
||||
run: |
|
||||
poetry publish
|
||||
|
||||
- name: Trigger build and push on langchain-serve
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.SERVE_GITHUB_TOKEN }}
|
||||
repository: jina-ai/langchain-serve
|
||||
event-type: langflow-push
|
||||
client-payload: '{"push_token": "${{ secrets.LCSERVE_PUSH_TOKEN }}", "branch": "dev"}'
|
||||
|
|
|
|||
11
Makefile
11
Makefile
|
|
@ -46,6 +46,17 @@ build:
|
|||
poetry build --format sdist
|
||||
rm -rf src/backend/langflow/frontend
|
||||
|
||||
lcserve_push:
|
||||
make build_frontend
|
||||
@version=$$(poetry version --short); \
|
||||
lc-serve push --app langflow.lcserve:app --app-dir . \
|
||||
--image-name langflow --image-tag $${version} --verbose
|
||||
|
||||
lcserve_deploy:
|
||||
@:$(if $(uses),,$(error `uses` is not set. Please run `make uses=... lcserve_deploy`))
|
||||
lc-serve deploy jcloud --app langflow.lcserve:app --app-dir . \
|
||||
--uses $(uses) --config src/backend/langflow/jcloud.yml --verbose
|
||||
|
||||
dev:
|
||||
make install_frontend
|
||||
ifeq ($(build),1)
|
||||
|
|
|
|||
93
README.md
93
README.md
|
|
@ -46,13 +46,98 @@ Alternatively, click the **"Open in Cloud Shell"** button below to launch Google
|
|||
[](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/logspace-ai/langflow&working_dir=scripts&shellonly=true&tutorial=walkthroughtutorial_spot.md)
|
||||
|
||||
|
||||
### Deploy Langflow on Google Cloud Platform
|
||||
### Deploy Langflow on [Jina AI Cloud](https://github.com/jina-ai/langchain-serve)
|
||||
|
||||
Follow our step-by-step guide to deploy Langflow on Google Cloud Platform (GCP) using Google Cloud Shell. The guide is available in the [**Langflow in Google Cloud Platform**](GCP_DEPLOYMENT.md) document.
|
||||
Langflow integrates with langchain-serve to provide a one-command deployment to Jina AI Cloud.
|
||||
|
||||
Alternatively, click the **"Open in Cloud Shell"** button below to launch Google Cloud Shell, clone the Langflow repository, and start an **interactive tutorial** that will guide you through the process of setting up the necessary resources and deploying Langflow on your GCP project.
|
||||
Start by installing `langchain-serve` with
|
||||
|
||||
[](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/genome21/langflow&working_dir=scripts&shellonly=true&tutorial=walkthroughtutorial_spot.md)
|
||||
```bash
|
||||
pip install -U langchain-serve
|
||||
```
|
||||
|
||||
Then, run:
|
||||
|
||||
```bash
|
||||
langflow --jcloud
|
||||
```
|
||||
|
||||
```text
|
||||
🎉 Langflow server successfully deployed on Jina AI Cloud 🎉
|
||||
🔗 Click on the link to open the server (please allow ~1-2 minutes for the server to startup): https://<your-app>.wolf.jina.ai/
|
||||
📖 Read more about managing the server: https://github.com/jina-ai/langchain-serve
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Show complete (example) output</summary>
|
||||
|
||||
```text
|
||||
🚀 Deploying Langflow server on Jina AI Cloud
|
||||
╭───────────────────────── 🎉 Flow is available! ──────────────────────────╮
|
||||
│ │
|
||||
│ ID langflow-e3dd8820ec │
|
||||
│ Gateway (Websocket) wss://langflow-e3dd8820ec.wolf.jina.ai │
|
||||
│ Dashboard https://dashboard.wolf.jina.ai/flow/e3dd8820ec │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯
|
||||
╭──────────────┬──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ App ID │ langflow-e3dd8820ec │
|
||||
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Phase │ Serving │
|
||||
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Endpoint │ wss://langflow-e3dd8820ec.wolf.jina.ai │
|
||||
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ App logs │ dashboards.wolf.jina.ai │
|
||||
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Swagger UI │ https://langflow-e3dd8820ec.wolf.jina.ai/docs │
|
||||
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ OpenAPI JSON │ https://langflow-e3dd8820ec.wolf.jina.ai/openapi.json │
|
||||
╰──────────────┴──────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
🎉 Langflow server successfully deployed on Jina AI Cloud 🎉
|
||||
🔗 Click on the link to open the server (please allow ~1-2 minutes for the server to startup): https://langflow-e3dd8820ec.wolf.jina.ai/
|
||||
📖 Read more about managing the server: https://github.com/jina-ai/langchain-serve
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### API Usage
|
||||
|
||||
You can use Langflow directly on your browser, or use the API endpoints on Jina AI Cloud to interact with the server.
|
||||
|
||||
<details>
|
||||
<summary>Show API usage (with python)</summary>
|
||||
|
||||
```python
|
||||
import json
|
||||
import requests
|
||||
|
||||
FLOW_PATH = "Time_traveller.json"
|
||||
|
||||
# HOST = 'http://localhost:7860'
|
||||
HOST = 'https://langflow-f1ed20e309.wolf.jina.ai'
|
||||
API_URL = f'{HOST}/predict'
|
||||
|
||||
def predict(message):
|
||||
with open(FLOW_PATH, "r") as f:
|
||||
json_data = json.load(f)
|
||||
payload = {'exported_flow': json_data, 'message': message}
|
||||
response = requests.post(API_URL, json=payload)
|
||||
return response.json()
|
||||
|
||||
|
||||
predict('Take me to 1920s Bangalore')
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"result": "Great choice! Bangalore in the 1920s was a vibrant city with a rich cultural and political scene. Here are some suggestions for things to see and do:\n\n1. Visit the Bangalore Palace - built in 1887, this stunning palace is a perfect example of Tudor-style architecture. It was home to the Maharaja of Mysore and is now open to the public.\n\n2. Attend a performance at the Ravindra Kalakshetra - this cultural center was built in the 1920s and is still a popular venue for music and dance performances.\n\n3. Explore the neighborhoods of Basavanagudi and Malleswaram - both of these areas have retained much of their old-world charm and are great places to walk around and soak up the atmosphere.\n\n4. Check out the Bangalore Club - founded in 1868, this exclusive social club was a favorite haunt of the British expat community in the 1920s.\n\n5. Attend a meeting of the Indian National Congress - founded in 1885, the INC was a major force in the Indian independence movement and held many meetings and rallies in Bangalore in the 1920s.\n\nHope you enjoy your trip to 1920s Bangalore!"
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
> Read more about resource customization, cost, and management of Langflow apps on Jina AI Cloud in the **[langchain-serve](https://github.com/jina-ai/langchain-serve)** repository.
|
||||
|
||||
|
||||
## 🎨 Creating Flows
|
||||
|
|
|
|||
16
lcserve.Dockerfile
Normal file
16
lcserve.Dockerfile
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# This file is used by `lc-serve` to build the image.
|
||||
# Don't change the name of this file.
|
||||
|
||||
FROM jinawolf/serving-gateway:${version}
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y build-essential libpq-dev
|
||||
|
||||
COPY . /appdir/
|
||||
|
||||
RUN pip install poetry==1.4.0 && cd /appdir && pip install . && \
|
||||
pip uninstall -y poetry && \
|
||||
apt-get remove --auto-remove -y build-essential libpq-dev && \
|
||||
apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* && rm -rf /tmp/*
|
||||
|
||||
ENTRYPOINT [ "jina", "gateway", "--uses", "config.yml" ]
|
||||
1229
poetry.lock
generated
1229
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "langflow"
|
||||
version = "0.0.74"
|
||||
version = "0.0.75"
|
||||
description = "A Python package with a built-in web application"
|
||||
authors = ["Logspace <contact@logspace.ai>"]
|
||||
maintainers = [
|
||||
|
|
@ -29,7 +29,7 @@ google-search-results = "^2.4.1"
|
|||
google-api-python-client = "^2.79.0"
|
||||
typer = "^0.7.0"
|
||||
gunicorn = "^20.1.0"
|
||||
langchain = "^0.0.170"
|
||||
langchain = "^0.0.176"
|
||||
openai = "^0.27.2"
|
||||
types-pyyaml = "^6.0.12.8"
|
||||
dill = "^0.3.6"
|
||||
|
|
@ -51,6 +51,7 @@ websockets = "^11.0.2"
|
|||
tiktoken = "^0.3.3"
|
||||
wikipedia = "^1.4.0"
|
||||
gptcache = "^0.1.23"
|
||||
langchain-serve = { version = "^0.0.33", optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^23.1.0"
|
||||
|
|
@ -66,6 +67,9 @@ pandas-stubs = "^2.0.0.230412"
|
|||
types-pillow = "^9.5.0.2"
|
||||
|
||||
|
||||
[tool.poetry.extras]
|
||||
deploy = ["langchain-serve"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
|
||||
|
|
|
|||
|
|
@ -33,11 +33,15 @@ def serve(
|
|||
config: str = typer.Option("config.yaml", help="Path to the configuration file."),
|
||||
log_level: str = typer.Option("info", help="Logging level."),
|
||||
log_file: Path = typer.Option("logs/langflow.log", help="Path to the log file."),
|
||||
jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"),
|
||||
):
|
||||
"""
|
||||
Run the Langflow server.
|
||||
"""
|
||||
|
||||
if jcloud:
|
||||
return serve_on_jcloud()
|
||||
|
||||
configure(log_level=log_level, log_file=log_file)
|
||||
update_settings(config)
|
||||
app = create_app()
|
||||
|
|
@ -69,6 +73,52 @@ def serve(
|
|||
LangflowApplication(app, options).run()
|
||||
|
||||
|
||||
def serve_on_jcloud():
|
||||
"""
|
||||
Deploy Langflow server on Jina AI Cloud
|
||||
"""
|
||||
import asyncio
|
||||
from importlib.metadata import version as mod_version
|
||||
|
||||
import click
|
||||
|
||||
try:
|
||||
from lcserve.__main__ import serve_on_jcloud # type: ignore
|
||||
except ImportError:
|
||||
click.secho(
|
||||
"🚨 Please install langchain-serve to deploy Langflow server on Jina AI Cloud "
|
||||
"using `pip install langchain-serve`",
|
||||
fg="red",
|
||||
)
|
||||
return
|
||||
|
||||
app_name = "langflow.lcserve:app"
|
||||
app_dir = str(Path(__file__).parent)
|
||||
version = mod_version("langflow")
|
||||
base_image = "jinaai+docker://deepankarm/langflow"
|
||||
|
||||
click.echo("🚀 Deploying Langflow server on Jina AI Cloud")
|
||||
app_id = asyncio.run(
|
||||
serve_on_jcloud(
|
||||
fastapi_app_str=app_name,
|
||||
app_dir=app_dir,
|
||||
uses=f"{base_image}:{version}",
|
||||
name="langflow",
|
||||
)
|
||||
)
|
||||
click.secho(
|
||||
"🎉 Langflow server successfully deployed on Jina AI Cloud 🎉", fg="green"
|
||||
)
|
||||
click.secho(
|
||||
"🔗 Click on the link to open the server (please allow ~1-2 minutes for the server to startup): ",
|
||||
nl=False,
|
||||
fg="green",
|
||||
)
|
||||
click.secho(f"https://{app_id}.wolf.jina.ai/", fg="blue")
|
||||
click.secho("📖 Read more about managing the server: ", nl=False, fg="green")
|
||||
click.secho("https://github.com/jina-ai/langchain-serve", fg="blue")
|
||||
|
||||
|
||||
def main():
|
||||
app()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from importlib.metadata import version
|
||||
import logging
|
||||
from importlib.metadata import version
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
|
|
|
|||
|
|
@ -54,4 +54,4 @@ def post_validate_node(node_id: str, data: dict):
|
|||
return json.dumps({"valid": True, "params": str(node._built_object_repr())})
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return json.dumps({"valid": False})
|
||||
return json.dumps({"valid": False, "params": str(e)})
|
||||
|
|
|
|||
|
|
@ -143,7 +143,8 @@ class DocumentLoaderNode(Node):
|
|||
# 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}({len(self._built_object)} documents)
|
||||
Documents: {self._built_object[:3]}..."""
|
||||
return f"{self.node_type}()"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Type
|
||||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.custom_lists import embedding_type_to_cls_dict
|
||||
from langflow.settings import settings
|
||||
from langflow.template.base import FrontendNode
|
||||
from langflow.template.nodes import EmbeddingFrontendNode
|
||||
from langflow.utils.logger import logger
|
||||
from langflow.utils.util import build_template_from_class
|
||||
|
||||
|
|
@ -14,6 +16,10 @@ class EmbeddingCreator(LangChainTypeCreator):
|
|||
def type_to_loader_dict(self) -> Dict:
|
||||
return embedding_type_to_cls_dict
|
||||
|
||||
@property
|
||||
def frontend_node_class(self) -> Type[FrontendNode]:
|
||||
return EmbeddingFrontendNode
|
||||
|
||||
def get_signature(self, name: str) -> Optional[Dict]:
|
||||
"""Get the signature of an embedding."""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ def instantiate_toolkit(node_type, class_object, params):
|
|||
|
||||
def instantiate_embedding(class_object, params):
|
||||
params.pop("model", None)
|
||||
params.pop("headers", None)
|
||||
try:
|
||||
return class_object(**params)
|
||||
except ValidationError:
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import io
|
|||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from chromadb.errors import NotEnoughElementsException # type: ignore
|
||||
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 langchain.schema import AgentAction
|
||||
|
||||
|
||||
def load_langchain_object(data_graph, is_first_message=False):
|
||||
|
|
|
|||
2
src/backend/langflow/jcloud.yml
Normal file
2
src/backend/langflow/jcloud.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
instance: C4
|
||||
autoscale_min: 1
|
||||
16
src/backend/langflow/lcserve.py
Normal file
16
src/backend/langflow/lcserve.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# This file is used by lc-serve to load the mounted app and serve it.
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from langflow.main import create_app
|
||||
|
||||
app = create_app()
|
||||
path = Path(__file__).parent
|
||||
static_files_dir = path / "frontend"
|
||||
app.mount(
|
||||
"/",
|
||||
StaticFiles(directory=static_files_dir, html=True),
|
||||
name="static",
|
||||
)
|
||||
|
|
@ -122,6 +122,7 @@ class MidJourneyPromptChainNode(FrontendNode):
|
|||
advanced=False,
|
||||
multiline=False,
|
||||
name="llm",
|
||||
display_name="LLM",
|
||||
),
|
||||
TemplateField(
|
||||
field_type="BaseChatMemory",
|
||||
|
|
@ -156,6 +157,7 @@ class TimeTravelGuideChainNode(FrontendNode):
|
|||
advanced=False,
|
||||
multiline=False,
|
||||
name="llm",
|
||||
display_name="LLM",
|
||||
),
|
||||
TemplateField(
|
||||
field_type="BaseChatMemory",
|
||||
|
|
@ -210,6 +212,7 @@ class SeriesCharacterChainNode(FrontendNode):
|
|||
advanced=False,
|
||||
multiline=False,
|
||||
name="llm",
|
||||
display_name="LLM",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
|
@ -295,6 +298,7 @@ class JsonAgentNode(FrontendNode):
|
|||
required=True,
|
||||
show=True,
|
||||
name="llm",
|
||||
display_name="LLM",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
|
@ -341,6 +345,7 @@ class InitializeAgentNode(FrontendNode):
|
|||
required=True,
|
||||
show=True,
|
||||
name="llm",
|
||||
display_name="LLM",
|
||||
advanced=False,
|
||||
),
|
||||
],
|
||||
|
|
@ -376,6 +381,7 @@ class CSVAgentNode(FrontendNode):
|
|||
required=True,
|
||||
show=True,
|
||||
name="llm",
|
||||
display_name="LLM",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
|
@ -614,3 +620,11 @@ class LLMFrontendNode(FrontendNode):
|
|||
elif field.name in ["model_name", "temperature"]:
|
||||
field.advanced = False
|
||||
field.show = True
|
||||
|
||||
|
||||
class EmbeddingFrontendNode(FrontendNode):
|
||||
@staticmethod
|
||||
def format_field(field: TemplateField, name: Optional[str] = None) -> None:
|
||||
FrontendNode.format_field(field, name)
|
||||
if field.name == "headers":
|
||||
field.show = False
|
||||
|
|
|
|||
|
|
@ -16,153 +16,162 @@ import CrashErrorComponent from "./components/CrashErrorComponent";
|
|||
import { TabsContext } from "./contexts/tabsContext";
|
||||
|
||||
export default function App() {
|
||||
let { setCurrent, setShowSideBar, setIsStackedOpen } =
|
||||
useContext(locationContext);
|
||||
let location = useLocation();
|
||||
useEffect(() => {
|
||||
setCurrent(location.pathname.replace(/\/$/g, "").split("/"));
|
||||
setShowSideBar(true);
|
||||
setIsStackedOpen(true);
|
||||
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
|
||||
const { hardReset } = useContext(TabsContext);
|
||||
const {
|
||||
errorData,
|
||||
errorOpen,
|
||||
setErrorOpen,
|
||||
noticeData,
|
||||
noticeOpen,
|
||||
setNoticeOpen,
|
||||
successData,
|
||||
successOpen,
|
||||
setSuccessOpen,
|
||||
} = useContext(alertContext);
|
||||
|
||||
let { setCurrent, setShowSideBar, setIsStackedOpen } =
|
||||
useContext(locationContext);
|
||||
let location = useLocation();
|
||||
useEffect(() => {
|
||||
setCurrent(location.pathname.replace(/\/$/g, "").split("/"));
|
||||
setShowSideBar(true);
|
||||
setIsStackedOpen(true);
|
||||
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
|
||||
const { hardReset } = useContext(TabsContext);
|
||||
const {
|
||||
errorData,
|
||||
errorOpen,
|
||||
setErrorOpen,
|
||||
noticeData,
|
||||
noticeOpen,
|
||||
setNoticeOpen,
|
||||
successData,
|
||||
successOpen,
|
||||
setSuccessOpen,
|
||||
} = useContext(alertContext);
|
||||
// Initialize state variable for the list of alerts
|
||||
const [alertsList, setAlertsList] = useState<
|
||||
Array<{
|
||||
type: string;
|
||||
data: { title: string; list?: Array<string>; link?: string };
|
||||
id: string;
|
||||
}>
|
||||
>([]);
|
||||
|
||||
// Initialize state variable for the list of alerts
|
||||
const [alertsList, setAlertsList] = useState<
|
||||
Array<{
|
||||
type: string;
|
||||
data: { title: string; list?: Array<string>; link?: string };
|
||||
id: string;
|
||||
}>
|
||||
>([]);
|
||||
// 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
|
||||
useEffect(() => {
|
||||
// If there is an error alert open with data, add it to the alertsList
|
||||
if (errorOpen && errorData) {
|
||||
setErrorOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "error", data: _.cloneDeep(errorData), id: _.uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
// If there is a notice alert open with data, add it to the alertsList
|
||||
else if (noticeOpen && noticeData) {
|
||||
setNoticeOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "notice", data: _.cloneDeep(noticeData), id: _.uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
// If there is a success alert open with data, add it to the alertsList
|
||||
else if (successOpen && successData) {
|
||||
setSuccessOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "success", data: _.cloneDeep(successData), id: _.uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
}, [
|
||||
_,
|
||||
errorData,
|
||||
errorOpen,
|
||||
noticeData,
|
||||
noticeOpen,
|
||||
setErrorOpen,
|
||||
setNoticeOpen,
|
||||
setSuccessOpen,
|
||||
successData,
|
||||
successOpen,
|
||||
]);
|
||||
|
||||
// Use effect hook to update alertsList when a new alert is added
|
||||
useEffect(() => {
|
||||
// If there is an error alert open with data, add it to the alertsList
|
||||
if (errorOpen && errorData) {
|
||||
setErrorOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "error", data: _.cloneDeep(errorData), id: _.uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
// If there is a notice alert open with data, add it to the alertsList
|
||||
else if (noticeOpen && noticeData) {
|
||||
setNoticeOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "notice", data: _.cloneDeep(noticeData), id: _.uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
// If there is a success alert open with data, add it to the alertsList
|
||||
else if (successOpen && successData) {
|
||||
setSuccessOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "success", data: _.cloneDeep(successData), id: _.uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
}, [
|
||||
_,
|
||||
errorData,
|
||||
errorOpen,
|
||||
noticeData,
|
||||
noticeOpen,
|
||||
setErrorOpen,
|
||||
setNoticeOpen,
|
||||
setSuccessOpen,
|
||||
successData,
|
||||
successOpen,
|
||||
]);
|
||||
const removeAlert = (id: string) => {
|
||||
setAlertsList((prevAlertsList) =>
|
||||
prevAlertsList.filter((alert) => alert.id !== id)
|
||||
);
|
||||
};
|
||||
|
||||
const removeAlert = (id: string) => {
|
||||
setAlertsList((prevAlertsList) =>
|
||||
prevAlertsList.filter((alert) => alert.id !== id)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
//need parent component with width and height
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex grow-0 shrink basis-auto"></div>
|
||||
<ErrorBoundary
|
||||
onReset={() => {
|
||||
window.localStorage.removeItem("tabsData");
|
||||
window.localStorage.clear();
|
||||
hardReset();
|
||||
window.location.href = window.location.href;
|
||||
}}
|
||||
FallbackComponent={CrashErrorComponent}
|
||||
>
|
||||
<div className="flex grow shrink basis-auto min-h-0 flex-1 overflow-hidden">
|
||||
<ExtraSidebar />
|
||||
{/* 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>
|
||||
<div></div>
|
||||
<div className="flex z-40 flex-col-reverse fixed bottom-5 left-5">
|
||||
{alertsList.map((alert) => (
|
||||
<div key={alert.id}>
|
||||
{alert.type === "error" ? (
|
||||
<ErrorAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
list={alert.data.list}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : alert.type === "notice" ? (
|
||||
<NoticeAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
link={alert.data.link}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : (
|
||||
<SuccessAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
target={"_blank"}
|
||||
href="https://logspace.ai/"
|
||||
className="absolute bottom-2 left-6 text-gray-300 px-2 rounded-lg text-xs cursor-pointer font-sans tracking-wide bg-gray-800 dark:bg-gray-300 dark:text-gray-800 "
|
||||
>
|
||||
Created by Logspace
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
//need parent component with width and height
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex grow-0 shrink basis-auto"></div>
|
||||
<ErrorBoundary
|
||||
onReset={() => {
|
||||
window.localStorage.removeItem("tabsData");
|
||||
window.localStorage.clear();
|
||||
hardReset();
|
||||
window.location.href = window.location.href;
|
||||
}}
|
||||
FallbackComponent={CrashErrorComponent}
|
||||
>
|
||||
<div className="flex grow shrink basis-auto min-h-0 flex-1 overflow-hidden">
|
||||
<ExtraSidebar />
|
||||
{/* 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>
|
||||
<div></div>
|
||||
<div className="flex z-40 flex-col-reverse fixed bottom-5 left-5">
|
||||
{alertsList.map((alert) => (
|
||||
<div key={alert.id}>
|
||||
{alert.type === "error" ? (
|
||||
<ErrorAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
list={alert.data.list}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : alert.type === "notice" ? (
|
||||
<NoticeAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
link={alert.data.link}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : (
|
||||
<SuccessAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
import { BugAntIcon, Cog6ToothIcon, ExclamationCircleIcon, InformationCircleIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
BugAntIcon,
|
||||
CheckCircleIcon,
|
||||
Cog6ToothIcon,
|
||||
EllipsisHorizontalCircleIcon,
|
||||
ExclamationCircleIcon,
|
||||
InformationCircleIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { classNames, nodeColors, nodeIcons, toNormalCase } from "../../utils";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
|
|
@ -12,11 +20,11 @@ import { TabsContext } from "../../contexts/tabsContext";
|
|||
import { debounce } from "../../utils";
|
||||
import Tooltip from "../../components/TooltipComponent";
|
||||
export default function GenericNode({
|
||||
data,
|
||||
selected,
|
||||
data,
|
||||
selected,
|
||||
}: {
|
||||
data: NodeDataType;
|
||||
selected: boolean;
|
||||
data: NodeDataType;
|
||||
selected: boolean;
|
||||
}) {
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const showError = useRef(true);
|
||||
|
|
@ -30,32 +38,28 @@ export default function GenericNode({
|
|||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const [params, setParams] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (reactFlowInstance) {
|
||||
setParams(Object.values(reactFlowInstance.toObject()));
|
||||
}
|
||||
}, [save]);
|
||||
useEffect(() => {
|
||||
if (reactFlowInstance) {
|
||||
setParams(Object.values(reactFlowInstance.toObject()));
|
||||
}
|
||||
}, [save]);
|
||||
|
||||
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()),
|
||||
});
|
||||
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);
|
||||
console.log(jsonResponseParsed);
|
||||
if(jsonResponseParsed.valid){
|
||||
setValidationStatus(jsonResponseParsed.params);
|
||||
} else {
|
||||
setValidationStatus("error");
|
||||
}
|
||||
setValidationStatus(jsonResponseParsed);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error("Error validating node:", error);
|
||||
|
|
@ -70,31 +74,22 @@ export default function GenericNode({
|
|||
}
|
||||
}, [params, validateNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (validationStatus !== "error") {
|
||||
setIsValid(true);
|
||||
} else {
|
||||
setIsValid(false);
|
||||
if (!Icon) {
|
||||
if (showError.current) {
|
||||
setErrorData({
|
||||
title: data.type
|
||||
? `The ${data.type} node could not be rendered, please review your json file`
|
||||
: "There was a node that can't be rendered, please review your json file",
|
||||
});
|
||||
showError.current = false;
|
||||
}
|
||||
}, [validationStatus]);
|
||||
|
||||
if (!Icon) {
|
||||
if (showError.current) {
|
||||
setErrorData({
|
||||
title: data.type
|
||||
? `The ${data.type} node could not be rendered, please review your json file`
|
||||
: "There was a node that can't be rendered, please review your json file",
|
||||
});
|
||||
showError.current = false;
|
||||
}
|
||||
deleteNode(data.id);
|
||||
return;
|
||||
}
|
||||
deleteNode(data.id);
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
isValid ? "animate-pulse-green" : "border-red-outline",
|
||||
selected ? "border border-blue-500" : "border dark:border-gray-700",
|
||||
"prompt-node relative bg-white dark:bg-gray-900 w-96 rounded-lg flex flex-col justify-center"
|
||||
)}
|
||||
|
|
@ -108,15 +103,48 @@ export default function GenericNode({
|
|||
}}
|
||||
/>
|
||||
<div className="ml-2 truncate">{data.type}</div>
|
||||
{validationStatus && validationStatus !== "error" ?
|
||||
<Tooltip title={
|
||||
<div className="max-h-96 overflow-auto">
|
||||
{validationStatus}
|
||||
</div>}>
|
||||
<ExclamationCircleIcon className="w-5 hover:text-gray-500 hover:dark:text-gray-300" />
|
||||
</Tooltip>
|
||||
: <></>
|
||||
}
|
||||
<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="relative w-5 h-5">
|
||||
<CheckCircleIcon
|
||||
className={classNames(
|
||||
validationStatus && validationStatus.valid
|
||||
? "text-green-500 opacity-100"
|
||||
: "text-green-500 opacity-0 animate-spin",
|
||||
"absolute w-5 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
<ExclamationCircleIcon
|
||||
className={classNames(
|
||||
validationStatus && !validationStatus.valid
|
||||
? "text-red-500 opacity-100"
|
||||
: "text-red-500 opacity-0 animate-spin",
|
||||
"w-5 absolute hover:text-gray-500 hover:dark:text-gray-600 transition-all ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
<EllipsisHorizontalCircleIcon
|
||||
className={classNames(
|
||||
!validationStatus
|
||||
? "text-yellow-500 opacity-100"
|
||||
: "text-yellow-500 opacity-0 animate-spin",
|
||||
"w-5 absolute hover:text-gray-500 hover:dark:text-gray-600 transition-all ease-in-out duration-300"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
|
|
@ -162,12 +190,12 @@ export default function GenericNode({
|
|||
{data.node.description}
|
||||
</div>
|
||||
|
||||
<>
|
||||
{Object.keys(data.node.template)
|
||||
.filter((t) => t.charAt(0) !== "_")
|
||||
.map((t: string, idx) => (
|
||||
<div key={idx}>
|
||||
{/* {idx === 0 ? (
|
||||
<>
|
||||
{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",
|
||||
|
|
@ -186,59 +214,59 @@ export default function GenericNode({
|
|||
) : (
|
||||
<></>
|
||||
)} */}
|
||||
{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
|
||||
? toNormalCase(data.node.template[t].name)
|
||||
: toNormalCase(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" : "",
|
||||
"w-full flex justify-center"
|
||||
)}
|
||||
>
|
||||
{" "}
|
||||
</div>
|
||||
{/* <div className="px-5 py-2 mt-2 dark:text-white text-center">
|
||||
{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
|
||||
? toNormalCase(data.node.template[t].name)
|
||||
: toNormalCase(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" : "",
|
||||
"w-full flex 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>
|
||||
);
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ export default function ChatTrigger({ open, setOpen }) {
|
|||
leaveFrom="translate-y-0"
|
||||
leaveTo="translate-y-96"
|
||||
>
|
||||
<div className="absolute bottom-6 right-3">
|
||||
<div className="absolute bottom-4 right-3">
|
||||
<div
|
||||
style={{ backgroundColor: nodeColors["chat"] }}
|
||||
className="border flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full dark:bg-gray-800 dark:border-gray-600 dark:text-white"
|
||||
// style={{ backgroundColor: nodeColors["chat"] }}
|
||||
className="border flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 dark:border-gray-600"
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default function FloatComponent({
|
|||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
const {setDisableCP} = useContext(TabsContext)
|
||||
const {setDisableCopyPaste} = useContext(TabsContext)
|
||||
return (
|
||||
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
|
||||
<input
|
||||
|
|
@ -30,10 +30,10 @@ export default function FloatComponent({
|
|||
onChange(e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setDisableCP(false)
|
||||
setDisableCopyPaste(false)
|
||||
}}
|
||||
onFocus={() => {
|
||||
setDisableCP(true)
|
||||
setDisableCopyPaste(true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue