Merge remote-tracking branch 'origin/dev' into two_edges
This commit is contained in:
commit
79e35dc2a2
1180 changed files with 74386 additions and 33420 deletions
1
src/backend/.gitignore
vendored
1
src/backend/.gitignore
vendored
|
|
@ -131,3 +131,4 @@ dmypy.json
|
|||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
*.db
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
FROM logspace/backend_build as backend_build
|
||||
FROM langflowai/backend_build as backend_build
|
||||
|
||||
FROM python:3.10-slim
|
||||
WORKDIR /app
|
||||
|
|
|
|||
583
src/backend/base/langflow/__main__.py
Normal file
583
src/backend/base/langflow/__main__.py
Normal file
|
|
@ -0,0 +1,583 @@
|
|||
import platform
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
import httpx
|
||||
import typer
|
||||
from dotenv import load_dotenv
|
||||
from multiprocess import Process, cpu_count # type: ignore
|
||||
from packaging import version as pkg_version
|
||||
from rich import box
|
||||
from rich import print as rprint
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from sqlmodel import select
|
||||
|
||||
from langflow.main import setup_app
|
||||
from langflow.services.database.models.folder.utils import create_default_folder_if_it_doesnt_exist
|
||||
from langflow.services.database.utils import session_getter
|
||||
from langflow.services.deps import get_db_service, get_settings_service, session_scope
|
||||
from langflow.services.settings.constants import DEFAULT_SUPERUSER
|
||||
from langflow.services.utils import initialize_services
|
||||
from langflow.utils.logger import configure, logger
|
||||
from langflow.utils.util import update_settings
|
||||
|
||||
console = Console()
|
||||
|
||||
app = typer.Typer(no_args_is_help=True)
|
||||
|
||||
|
||||
def get_number_of_workers(workers=None):
|
||||
if workers == -1 or workers is None:
|
||||
workers = (cpu_count() * 2) + 1
|
||||
logger.debug(f"Number of workers: {workers}")
|
||||
return workers
|
||||
|
||||
|
||||
def display_results(results):
|
||||
"""
|
||||
Display the results of the migration.
|
||||
"""
|
||||
for table_results in results:
|
||||
table = Table(title=f"Migration {table_results.table_name}")
|
||||
table.add_column("Name")
|
||||
table.add_column("Type")
|
||||
table.add_column("Status")
|
||||
|
||||
for result in table_results.results:
|
||||
status = "Success" if result.success else "Failure"
|
||||
color = "green" if result.success else "red"
|
||||
table.add_row(result.name, result.type, f"[{color}]{status}[/{color}]")
|
||||
|
||||
console.print(table)
|
||||
console.print() # Print a new line
|
||||
|
||||
|
||||
def set_var_for_macos_issue():
|
||||
# OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
|
||||
# we need to set this var is we are running on MacOS
|
||||
# otherwise we get an error when running gunicorn
|
||||
|
||||
if platform.system() in ["Darwin"]:
|
||||
import os
|
||||
|
||||
os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
|
||||
# https://stackoverflow.com/questions/75747888/uwsgi-segmentation-fault-with-flask-python-app-behind-nginx-after-running-for-2 # noqa
|
||||
os.environ["no_proxy"] = "*" # to avoid error with gunicorn
|
||||
logger.debug("Set OBJC_DISABLE_INITIALIZE_FORK_SAFETY to YES to avoid error")
|
||||
|
||||
|
||||
@app.command()
|
||||
def run(
|
||||
host: str = typer.Option("127.0.0.1", help="Host to bind the server to.", envvar="LANGFLOW_HOST"),
|
||||
workers: int = typer.Option(1, help="Number of worker processes.", envvar="LANGFLOW_WORKERS"),
|
||||
timeout: int = typer.Option(300, help="Worker timeout in seconds.", envvar="LANGFLOW_WORKER_TIMEOUT"),
|
||||
port: int = typer.Option(7860, help="Port to listen on.", envvar="LANGFLOW_PORT"),
|
||||
components_path: Optional[Path] = typer.Option(
|
||||
Path(__file__).parent / "components",
|
||||
help="Path to the directory containing custom components.",
|
||||
envvar="LANGFLOW_COMPONENTS_PATH",
|
||||
),
|
||||
# .env file param
|
||||
env_file: Path = typer.Option(None, help="Path to the .env file containing environment variables."),
|
||||
log_level: str = typer.Option("critical", help="Logging level.", envvar="LANGFLOW_LOG_LEVEL"),
|
||||
log_file: Path = typer.Option("logs/langflow.log", help="Path to the log file.", envvar="LANGFLOW_LOG_FILE"),
|
||||
cache: Optional[str] = typer.Option(
|
||||
envvar="LANGFLOW_LANGCHAIN_CACHE",
|
||||
help="Type of cache to use. (InMemoryCache, SQLiteCache)",
|
||||
default=None,
|
||||
),
|
||||
dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"),
|
||||
path: str = typer.Option(
|
||||
None,
|
||||
help="Path to the frontend directory containing build files. This is for development purposes only.",
|
||||
envvar="LANGFLOW_FRONTEND_PATH",
|
||||
),
|
||||
open_browser: bool = typer.Option(
|
||||
True,
|
||||
help="Open the browser after starting the server.",
|
||||
envvar="LANGFLOW_OPEN_BROWSER",
|
||||
),
|
||||
remove_api_keys: bool = typer.Option(
|
||||
False,
|
||||
help="Remove API keys from the projects saved in the database.",
|
||||
envvar="LANGFLOW_REMOVE_API_KEYS",
|
||||
),
|
||||
backend_only: bool = typer.Option(
|
||||
False,
|
||||
help="Run only the backend server without the frontend.",
|
||||
envvar="LANGFLOW_BACKEND_ONLY",
|
||||
),
|
||||
store: bool = typer.Option(
|
||||
True,
|
||||
help="Enables the store features.",
|
||||
envvar="LANGFLOW_STORE",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Run the Langflow.
|
||||
"""
|
||||
|
||||
configure(log_level=log_level, log_file=log_file)
|
||||
set_var_for_macos_issue()
|
||||
# override env variables with .env file
|
||||
|
||||
if env_file:
|
||||
load_dotenv(env_file, override=True)
|
||||
|
||||
update_settings(
|
||||
dev=dev,
|
||||
remove_api_keys=remove_api_keys,
|
||||
cache=cache,
|
||||
components_path=components_path,
|
||||
store=store,
|
||||
)
|
||||
# create path object if path is provided
|
||||
static_files_dir: Optional[Path] = Path(path) if path else None
|
||||
app = setup_app(static_files_dir=static_files_dir, backend_only=backend_only)
|
||||
# check if port is being used
|
||||
if is_port_in_use(port, host):
|
||||
port = get_free_port(port)
|
||||
|
||||
settings_service = get_settings_service()
|
||||
|
||||
settings_service.set("worker_timeout", timeout)
|
||||
|
||||
options = {
|
||||
"bind": f"{host}:{port}",
|
||||
"workers": get_number_of_workers(workers),
|
||||
"timeout": timeout,
|
||||
}
|
||||
|
||||
# Define an env variable to know if we are just testing the server
|
||||
if "pytest" in sys.modules:
|
||||
return
|
||||
try:
|
||||
if platform.system() in ["Windows"]:
|
||||
# Run using uvicorn on MacOS and Windows
|
||||
# Windows doesn't support gunicorn
|
||||
# MacOS requires an env variable to be set to use gunicorn
|
||||
process = run_on_windows(host, port, log_level, options, app)
|
||||
else:
|
||||
# Run using gunicorn on Linux
|
||||
process = run_on_mac_or_linux(host, port, log_level, options, app)
|
||||
if open_browser:
|
||||
click.launch(f"http://{host}:{port}")
|
||||
if process:
|
||||
process.join()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
def wait_for_server_ready(host, port):
|
||||
"""
|
||||
Wait for the server to become ready by polling the health endpoint.
|
||||
"""
|
||||
status_code = 0
|
||||
while status_code != 200:
|
||||
try:
|
||||
status_code = httpx.get(f"http://{host}:{port}/health").status_code
|
||||
except Exception:
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def run_on_mac_or_linux(host, port, log_level, options, app):
|
||||
webapp_process = Process(target=run_langflow, args=(host, port, log_level, options, app))
|
||||
webapp_process.start()
|
||||
wait_for_server_ready(host, port)
|
||||
|
||||
print_banner(host, port)
|
||||
return webapp_process
|
||||
|
||||
|
||||
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)
|
||||
return None
|
||||
|
||||
|
||||
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 version_is_prerelease(version: str):
|
||||
"""
|
||||
Check if a version is a pre-release version.
|
||||
"""
|
||||
return "a" in version or "b" in version or "rc" in version
|
||||
|
||||
|
||||
def get_letter_from_version(version: str):
|
||||
"""
|
||||
Get the letter from a pre-release version.
|
||||
"""
|
||||
if "a" in version:
|
||||
return "a"
|
||||
if "b" in version:
|
||||
return "b"
|
||||
if "rc" in version:
|
||||
return "rc"
|
||||
return None
|
||||
|
||||
|
||||
def build_new_version_notice(current_version: str, package_name: str):
|
||||
"""
|
||||
Build a new version notice.
|
||||
"""
|
||||
# The idea here is that we want to show a notice to the user
|
||||
# when a new version of Langflow is available.
|
||||
# The key is that if the version the user has is a pre-release
|
||||
# e.g 0.0.0a1, then we find the latest version that is pre-release
|
||||
# otherwise we find the latest stable version.
|
||||
# we will show the notice either way, but only if the version
|
||||
# the user has is not the latest version.
|
||||
if version_is_prerelease(current_version):
|
||||
# curl -s "https://pypi.org/pypi/langflow/json" | jq -r '.releases | keys | .[]' | sort -V | tail -n 1
|
||||
# this command will give us the latest pre-release version
|
||||
package_info = httpx.get(f"https://pypi.org/pypi/{package_name}/json").json()
|
||||
# 4.0.0a1 or 4.0.0b1 or 4.0.0rc1
|
||||
# find which type of pre-release version we have
|
||||
# could be a1, b1, rc1
|
||||
# we want the a, b, or rc and the number
|
||||
suffix_letter = get_letter_from_version(current_version)
|
||||
number_version = current_version.split(suffix_letter)[0]
|
||||
latest_version = sorted(
|
||||
package_info["releases"].keys(),
|
||||
key=lambda x: x.split(suffix_letter)[-1] and number_version in x,
|
||||
)[-1]
|
||||
if version_is_prerelease(latest_version) and latest_version != current_version:
|
||||
return (
|
||||
True,
|
||||
f"A new pre-release version of {package_name} is available: {latest_version}",
|
||||
)
|
||||
else:
|
||||
latest_version = httpx.get(f"https://pypi.org/pypi/{package_name}/json").json()["info"]["version"]
|
||||
if not version_is_prerelease(latest_version):
|
||||
return (
|
||||
False,
|
||||
f"A new version of {package_name} is available: {latest_version}",
|
||||
)
|
||||
return False, ""
|
||||
|
||||
|
||||
def is_prerelease(version: str) -> bool:
|
||||
return "a" in version or "b" in version or "rc" in version
|
||||
|
||||
|
||||
def fetch_latest_version(package_name: str, include_prerelease: bool) -> Optional[str]:
|
||||
response = httpx.get(f"https://pypi.org/pypi/{package_name}/json")
|
||||
versions = response.json()["releases"].keys()
|
||||
valid_versions = [v for v in versions if include_prerelease or not is_prerelease(v)]
|
||||
if not valid_versions:
|
||||
return None # Handle case where no valid versions are found
|
||||
return max(valid_versions, key=lambda v: pkg_version.parse(v))
|
||||
|
||||
|
||||
def build_version_notice(current_version: str, package_name: str) -> str:
|
||||
latest_version = fetch_latest_version(package_name, is_prerelease(current_version))
|
||||
if latest_version and pkg_version.parse(current_version) < pkg_version.parse(latest_version):
|
||||
release_type = "pre-release" if is_prerelease(latest_version) else "version"
|
||||
return f"A new {release_type} of {package_name} is available: {latest_version}"
|
||||
return ""
|
||||
|
||||
|
||||
def generate_pip_command(package_names, is_pre_release):
|
||||
"""
|
||||
Generate the pip install command based on the packages and whether it's a pre-release.
|
||||
"""
|
||||
base_command = "pip install"
|
||||
if is_pre_release:
|
||||
return f"{base_command} {' '.join(package_names)} -U --pre"
|
||||
else:
|
||||
return f"{base_command} {' '.join(package_names)} -U"
|
||||
|
||||
|
||||
def stylize_text(text: str, to_style: str, is_prerelease: bool) -> str:
|
||||
color = "#42a7f5" if is_prerelease else "#6e42f5"
|
||||
# return "".join(f"[{color}]{char}[/]" for char in text)
|
||||
styled_text = f"[{color}]{to_style}[/]"
|
||||
return text.replace(to_style, styled_text)
|
||||
|
||||
|
||||
def print_banner(host: str, port: int):
|
||||
notices = []
|
||||
package_names = [] # Track package names for pip install instructions
|
||||
is_pre_release = False # Track if any package is a pre-release
|
||||
package_name = ""
|
||||
|
||||
try:
|
||||
from langflow.version import __version__ as langflow_version # type: ignore
|
||||
|
||||
is_pre_release |= is_prerelease(langflow_version) # Update pre-release status
|
||||
notice = build_version_notice(langflow_version, "langflow")
|
||||
notice = stylize_text(notice, "langflow", is_pre_release)
|
||||
if notice:
|
||||
notices.append(notice)
|
||||
package_names.append("langflow")
|
||||
package_name = "Langflow"
|
||||
except ImportError:
|
||||
langflow_version = None
|
||||
|
||||
# Attempt to handle langflow-base similarly
|
||||
if langflow_version is None: # This means langflow.version was not imported
|
||||
try:
|
||||
from importlib import metadata
|
||||
|
||||
langflow_base_version = metadata.version("langflow-base")
|
||||
is_pre_release |= is_prerelease(langflow_base_version) # Update pre-release status
|
||||
notice = build_version_notice(langflow_base_version, "langflow-base")
|
||||
notice = stylize_text(notice, "langflow-base", is_pre_release)
|
||||
if notice:
|
||||
notices.append(notice)
|
||||
package_names.append("langflow-base")
|
||||
package_name = "Langflow Base"
|
||||
except ImportError as e:
|
||||
logger.exception(e)
|
||||
raise e
|
||||
|
||||
# Generate pip command based on the collected data
|
||||
pip_command = generate_pip_command(package_names, is_pre_release)
|
||||
|
||||
# Add pip install command to notices if any package needs an update
|
||||
if notices:
|
||||
notices.append(f"Run '{pip_command}' to update.")
|
||||
|
||||
styled_notices = [f"[bold]{notice}[/bold]" for notice in notices if notice]
|
||||
styled_package_name = stylize_text(package_name, package_name, any("pre-release" in notice for notice in notices))
|
||||
|
||||
title = f"[bold]Welcome to :chains: {styled_package_name}[/bold]\n"
|
||||
info_text = "Collaborate, and contribute at our [bold][link=https://github.com/langflow-ai/langflow]GitHub Repo[/link][/bold] :rocket:"
|
||||
access_link = f"Access [link=http://{host}:{port}]http://{host}:{port}[/link]"
|
||||
|
||||
panel_content = "\n\n".join([title, *styled_notices, info_text, access_link])
|
||||
panel = Panel(panel_content, box=box.ROUNDED, border_style="blue", expand=False)
|
||||
rprint(panel)
|
||||
|
||||
|
||||
def run_langflow(host, port, log_level, options, app):
|
||||
"""
|
||||
Run Langflow server on localhost
|
||||
"""
|
||||
try:
|
||||
if platform.system() in ["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.lower(),
|
||||
loop="asyncio",
|
||||
)
|
||||
else:
|
||||
from langflow.server import LangflowApplication
|
||||
|
||||
LangflowApplication(app, options).run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def superuser(
|
||||
username: str = typer.Option(..., prompt=True, help="Username for the superuser."),
|
||||
password: str = typer.Option(..., prompt=True, hide_input=True, help="Password for the superuser."),
|
||||
log_level: str = typer.Option("error", help="Logging level.", envvar="LANGFLOW_LOG_LEVEL"),
|
||||
):
|
||||
"""
|
||||
Create a superuser.
|
||||
"""
|
||||
configure(log_level=log_level)
|
||||
initialize_services()
|
||||
db_service = get_db_service()
|
||||
with session_getter(db_service) as session:
|
||||
from langflow.services.auth.utils import create_super_user
|
||||
|
||||
if create_super_user(db=session, username=username, password=password):
|
||||
# Verify that the superuser was created
|
||||
from langflow.services.database.models.user.model import User
|
||||
|
||||
user: User = session.exec(select(User).where(User.username == username)).first()
|
||||
if user is None or not user.is_superuser:
|
||||
typer.echo("Superuser creation failed.")
|
||||
return
|
||||
# Now create the first folder for the user
|
||||
result = create_default_folder_if_it_doesnt_exist(session, user.id)
|
||||
if result:
|
||||
typer.echo("Default folder created successfully.")
|
||||
else:
|
||||
raise RuntimeError("Could not create default folder.")
|
||||
typer.echo("Superuser created successfully.")
|
||||
|
||||
else:
|
||||
typer.echo("Superuser creation failed.")
|
||||
|
||||
|
||||
# command to copy the langflow database from the cache to the current directory
|
||||
# because now the database is stored per installation
|
||||
@app.command()
|
||||
def copy_db():
|
||||
"""
|
||||
Copy the database files to the current directory.
|
||||
|
||||
This function copies the 'langflow.db' and 'langflow-pre.db' files from the cache directory to the current directory.
|
||||
If the files exist in the cache directory, they will be copied to the same directory as this script (__main__.py).
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
import shutil
|
||||
|
||||
from platformdirs import user_cache_dir
|
||||
|
||||
cache_dir = Path(user_cache_dir("langflow"))
|
||||
db_path = cache_dir / "langflow.db"
|
||||
pre_db_path = cache_dir / "langflow-pre.db"
|
||||
# It should be copied to the current directory
|
||||
# this file is __main__.py and it should be in the same directory as the database
|
||||
destination_folder = Path(__file__).parent
|
||||
if db_path.exists():
|
||||
shutil.copy(db_path, destination_folder)
|
||||
typer.echo(f"Database copied to {destination_folder}")
|
||||
else:
|
||||
typer.echo("Database not found in the cache directory.")
|
||||
if pre_db_path.exists():
|
||||
shutil.copy(pre_db_path, destination_folder)
|
||||
typer.echo(f"Pre-release database copied to {destination_folder}")
|
||||
else:
|
||||
typer.echo("Pre-release database not found in the cache directory.")
|
||||
|
||||
|
||||
@app.command()
|
||||
def migration(
|
||||
test: bool = typer.Option(True, help="Run migrations in test mode."),
|
||||
fix: bool = typer.Option(
|
||||
False,
|
||||
help="Fix migrations. This is a destructive operation, and should only be used if you know what you are doing.",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Run or test migrations.
|
||||
"""
|
||||
if fix:
|
||||
if not typer.confirm(
|
||||
"This will delete all data necessary to fix migrations. Are you sure you want to continue?"
|
||||
):
|
||||
raise typer.Abort()
|
||||
|
||||
initialize_services(fix_migration=fix)
|
||||
db_service = get_db_service()
|
||||
if not test:
|
||||
db_service.run_migrations()
|
||||
results = db_service.run_migrations_test()
|
||||
display_results(results)
|
||||
|
||||
|
||||
@app.command()
|
||||
def api_key(
|
||||
log_level: str = typer.Option("error", help="Logging level.", envvar="LANGFLOW_LOG_LEVEL"),
|
||||
):
|
||||
"""
|
||||
Creates an API key for the default superuser if AUTO_LOGIN is enabled.
|
||||
|
||||
Args:
|
||||
log_level (str, optional): Logging level. Defaults to "error".
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
configure(log_level=log_level)
|
||||
initialize_services()
|
||||
settings_service = get_settings_service()
|
||||
auth_settings = settings_service.auth_settings
|
||||
if not auth_settings.AUTO_LOGIN:
|
||||
typer.echo("Auto login is disabled. API keys cannot be created through the CLI.")
|
||||
return
|
||||
with session_scope() as session:
|
||||
from langflow.services.database.models.user.model import User
|
||||
|
||||
superuser = session.exec(select(User).where(User.username == DEFAULT_SUPERUSER)).first()
|
||||
if not superuser:
|
||||
typer.echo("Default superuser not found. This command requires a superuser and AUTO_LOGIN to be enabled.")
|
||||
return
|
||||
from langflow.services.database.models.api_key import ApiKey, ApiKeyCreate
|
||||
from langflow.services.database.models.api_key.crud import create_api_key, delete_api_key
|
||||
|
||||
api_key = session.exec(select(ApiKey).where(ApiKey.user_id == superuser.id)).first()
|
||||
if api_key:
|
||||
delete_api_key(session, api_key.id)
|
||||
|
||||
api_key_create = ApiKeyCreate(name="CLI")
|
||||
unmasked_api_key = create_api_key(session, api_key_create, user_id=superuser.id)
|
||||
session.commit()
|
||||
# Create a banner to display the API key and tell the user it won't be shown again
|
||||
api_key_banner(unmasked_api_key)
|
||||
|
||||
|
||||
def api_key_banner(unmasked_api_key):
|
||||
is_mac = platform.system() == "Darwin"
|
||||
import pyperclip # type: ignore
|
||||
|
||||
pyperclip.copy(unmasked_api_key.api_key)
|
||||
panel = Panel(
|
||||
f"[bold]API Key Created Successfully:[/bold]\n\n"
|
||||
f"[bold blue]{unmasked_api_key.api_key}[/bold blue]\n\n"
|
||||
"This is the only time the API key will be displayed. \n"
|
||||
"Make sure to store it in a secure location. \n\n"
|
||||
f"The API key has been copied to your clipboard. [bold]{['Ctrl','Cmd'][is_mac]} + V[/bold] to paste it.",
|
||||
box=box.ROUNDED,
|
||||
border_style="blue",
|
||||
expand=False,
|
||||
)
|
||||
console = Console()
|
||||
console.print(panel)
|
||||
|
||||
|
||||
def main():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
app()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import warnings
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
|
|
@ -62,24 +63,32 @@ def run_migrations_online() -> None:
|
|||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
from langflow.services.deps import get_db_service
|
||||
|
||||
try:
|
||||
from langflow.services.database.factory import DatabaseServiceFactory
|
||||
from langflow.services.deps import get_db_service
|
||||
from langflow.services.manager import initialize_settings_service, service_manager
|
||||
|
||||
initialize_settings_service()
|
||||
service_manager.register_factory(DatabaseServiceFactory())
|
||||
connectable = get_db_service().engine
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting database engine: {e}")
|
||||
url = os.getenv("LANGFLOW_DATABASE_URL")
|
||||
url = url or config.get_main_option("sqlalchemy.url")
|
||||
if url:
|
||||
config.set_main_option("sqlalchemy.url", url)
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata, render_as_batch=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata, render_as_batch=True)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
|
|
@ -23,10 +23,12 @@ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
${downgrades if downgrades else "pass"}
|
||||
|
|
@ -26,20 +26,13 @@ def upgrade() -> None:
|
|||
flow_constraints = inspector.get_unique_constraints("flow")
|
||||
user_constraints = inspector.get_unique_constraints("user")
|
||||
try:
|
||||
if not any(
|
||||
constraint["name"] == "uq_apikey_id" for constraint in api_key_constraints
|
||||
):
|
||||
if not any(constraint["column_names"] == ["id"] for constraint in api_key_constraints):
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
|
||||
batch_op.create_unique_constraint("uq_apikey_id", ["id"])
|
||||
if not any(
|
||||
constraint["name"] == "uq_flow_id" for constraint in flow_constraints
|
||||
):
|
||||
if not any(constraint["column_names"] == ["id"] for constraint in flow_constraints):
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.create_unique_constraint("uq_flow_id", ["id"])
|
||||
if not any(
|
||||
constraint["name"] == "uq_user_id" for constraint in user_constraints
|
||||
):
|
||||
if not any(constraint["column_names"] == ["id"] for constraint in user_constraints):
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.create_unique_constraint("uq_user_id", ["id"])
|
||||
except Exception as e:
|
||||
|
|
@ -57,16 +50,13 @@ def downgrade() -> None:
|
|||
flow_constraints = inspector.get_unique_constraints("flow")
|
||||
user_constraints = inspector.get_unique_constraints("user")
|
||||
try:
|
||||
if any(
|
||||
constraint["name"] == "uq_apikey_id" for constraint in api_key_constraints
|
||||
):
|
||||
if any(constraint["name"] == "uq_apikey_id" for constraint in api_key_constraints):
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("uq_user_id", type_="unique")
|
||||
if any(constraint["name"] == "uq_flow_id" for constraint in flow_constraints):
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("uq_flow_id", type_="unique")
|
||||
if any(constraint["name"] == "uq_user_id" for constraint in user_constraints):
|
||||
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("uq_apikey_id", type_="unique")
|
||||
except Exception as e:
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
"""Add Folder table
|
||||
|
||||
Revision ID: 012fb73ac359
|
||||
Revises: c153816fd85f
|
||||
Create Date: 2024-05-07 12:52:16.954691
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "012fb73ac359"
|
||||
down_revision: Union[str, None] = "c153816fd85f"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "folder" not in table_names:
|
||||
op.create_table(
|
||||
"folder",
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("parent_id", sqlmodel.sql.sqltypes.GUID(), nullable=True),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["parent_id"],
|
||||
["folder.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["user.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
indexes = inspector.get_indexes("folder")
|
||||
if "ix_folder_name" not in [index["name"] for index in indexes]:
|
||||
with op.batch_alter_table("folder", schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f("ix_folder_name"), ["name"], unique=False)
|
||||
|
||||
column_names = [column["name"] for column in inspector.get_columns("flow")]
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "folder_id" not in column_names:
|
||||
batch_op.add_column(sa.Column("folder_id", sqlmodel.sql.sqltypes.GUID(), nullable=True))
|
||||
batch_op.create_foreign_key("flow_folder_id_fkey", "folder", ["folder_id"], ["id"])
|
||||
if "folder" in column_names:
|
||||
batch_op.drop_column("folder")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
column_names = [column["name"] for column in inspector.get_columns("flow")]
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "folder" not in column_names:
|
||||
batch_op.add_column(sa.Column("folder", sa.VARCHAR(), nullable=True))
|
||||
if "folder_id" in column_names:
|
||||
batch_op.drop_column("folder_id")
|
||||
batch_op.drop_constraint("flow_folder_id_fkey", type_="foreignkey")
|
||||
|
||||
indexes = inspector.get_indexes("folder")
|
||||
if "ix_folder_name" in [index["name"] for index in indexes]:
|
||||
with op.batch_alter_table("folder", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_folder_name"))
|
||||
|
||||
if "folder" in table_names:
|
||||
op.drop_table("folder")
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
"""Replace Credential table with Variable
|
||||
|
||||
Revision ID: 1a110b568907
|
||||
Revises: 63b9c451fd30
|
||||
Create Date: 2024-03-25 09:40:02.743453
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "1a110b568907"
|
||||
down_revision: Union[str, None] = "63b9c451fd30"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "variable" not in table_names:
|
||||
op.create_table(
|
||||
"variable",
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("value", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("type", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name="fk_variable_user_id"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
if "credential" in table_names:
|
||||
op.drop_table("credential")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "credential" not in table_names:
|
||||
op.create_table(
|
||||
"credential",
|
||||
sa.Column("name", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("value", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("provider", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("user_id", sa.CHAR(length=32), nullable=False),
|
||||
sa.Column("id", sa.CHAR(length=32), nullable=False),
|
||||
sa.Column("created_at", sa.DATETIME(), nullable=False),
|
||||
sa.Column("updated_at", sa.DATETIME(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name="fk_credential_user_id"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
if "variable" in table_names:
|
||||
op.drop_table("variable")
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -24,10 +24,8 @@ def upgrade() -> None:
|
|||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"name", existing_type=sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
)
|
||||
except Exception as e:
|
||||
batch_op.alter_column("name", existing_type=sqlmodel.sql.sqltypes.AutoString(), nullable=True)
|
||||
except Exception:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
|
@ -37,6 +35,6 @@ def downgrade() -> None:
|
|||
try:
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=False)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
"""Add default_fields column
|
||||
|
||||
Revision ID: 1f4d6df60295
|
||||
Revises: 6e7b581b5648
|
||||
Create Date: 2024-04-29 09:49:46.864145
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "1f4d6df60295"
|
||||
down_revision: Union[str, None] = "6e7b581b5648"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
column_names = [column["name"] for column in inspector.get_columns("variable")]
|
||||
with op.batch_alter_table("variable", schema=None) as batch_op:
|
||||
if "default_fields" not in column_names:
|
||||
batch_op.add_column(sa.Column("default_fields", sa.JSON(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
column_names = [column["name"] for column in inspector.get_columns("variable")]
|
||||
with op.batch_alter_table("variable", schema=None) as batch_op:
|
||||
if "default_fields" in column_names:
|
||||
batch_op.drop_column("default_fields")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -31,23 +31,16 @@ def upgrade() -> None:
|
|||
# and other related indices
|
||||
if "flowstyle" in existing_tables:
|
||||
op.drop_table("flowstyle")
|
||||
if "ix_flowstyle_flow_id" in [
|
||||
index["name"] for index in inspector.get_indexes("flowstyle")
|
||||
]:
|
||||
op.drop_index(
|
||||
"ix_flowstyle_flow_id", table_name="flowstyle", if_exists=True
|
||||
)
|
||||
if "ix_flowstyle_flow_id" in [index["name"] for index in inspector.get_indexes("flowstyle")]:
|
||||
op.drop_index("ix_flowstyle_flow_id", table_name="flowstyle", if_exists=True)
|
||||
|
||||
existing_indices_flow = []
|
||||
existing_fks_flow = []
|
||||
if "flow" in existing_tables:
|
||||
existing_indices_flow = [
|
||||
index["name"] for index in inspector.get_indexes("flow")
|
||||
]
|
||||
existing_indices_flow = [index["name"] for index in inspector.get_indexes("flow")]
|
||||
# Existing foreign keys for the 'flow' table, if it exists
|
||||
existing_fks_flow = [
|
||||
fk["referred_table"] + "." + fk["referred_columns"][0]
|
||||
for fk in inspector.get_foreign_keys("flow")
|
||||
fk["referred_table"] + "." + fk["referred_columns"][0] for fk in inspector.get_foreign_keys("flow")
|
||||
]
|
||||
# Now check if the columns user_id exists in the 'flow' table
|
||||
# If it does not exist, we need to create the foreign key
|
||||
|
|
@ -67,9 +60,7 @@ def upgrade() -> None:
|
|||
sa.UniqueConstraint("id", name="uq_user_id"),
|
||||
)
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_user_username"), ["username"], unique=True
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_user_username"), ["username"], unique=True)
|
||||
|
||||
if "apikey" not in existing_tables:
|
||||
op.create_table(
|
||||
|
|
@ -82,20 +73,14 @@ def upgrade() -> None:
|
|||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("api_key", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"], ["user.id"], name="fk_apikey_user_id_user"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name="fk_apikey_user_id_user"),
|
||||
sa.PrimaryKeyConstraint("id", name="pk_apikey"),
|
||||
sa.UniqueConstraint("id", name="uq_apikey_id"),
|
||||
)
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_apikey_api_key"), ["api_key"], unique=True
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_apikey_api_key"), ["api_key"], unique=True)
|
||||
batch_op.create_index(batch_op.f("ix_apikey_name"), ["name"], unique=False)
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_apikey_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_apikey_user_id"), ["user_id"], unique=False)
|
||||
if "flow" not in existing_tables:
|
||||
op.create_table(
|
||||
"flow",
|
||||
|
|
@ -104,9 +89,7 @@ def upgrade() -> None:
|
|||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"], ["user.id"], name="fk_flow_user_id_user"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name="fk_flow_user_id_user"),
|
||||
sa.PrimaryKeyConstraint("id", name="pk_flow"),
|
||||
sa.UniqueConstraint("id", name="uq_flow_id"),
|
||||
)
|
||||
|
|
@ -129,16 +112,12 @@ def upgrade() -> None:
|
|||
if "user.id" not in existing_fks_flow:
|
||||
batch_op.create_foreign_key("fk_flow_user_id", "user", ["user_id"], ["id"])
|
||||
if "ix_flow_description" not in existing_indices_flow:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_flow_description"), ["description"], unique=False
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_flow_description"), ["description"], unique=False)
|
||||
if "ix_flow_name" not in existing_indices_flow:
|
||||
batch_op.create_index(batch_op.f("ix_flow_name"), ["name"], unique=False)
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "ix_flow_user_id" not in existing_indices_flow:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_flow_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_flow_user_id"), ["user_id"], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
|
@ -169,10 +148,4 @@ def downgrade() -> None:
|
|||
batch_op.drop_index(batch_op.f("ix_user_username"), if_exists=True)
|
||||
|
||||
op.drop_table("user")
|
||||
|
||||
if "flowstyle" in existing_tables:
|
||||
op.drop_table("flowstyle")
|
||||
|
||||
if "component" in existing_tables:
|
||||
op.drop_table("component")
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
"""Add missing index
|
||||
|
||||
Revision ID: 29fe8f1f806b
|
||||
Revises: 012fb73ac359
|
||||
Create Date: 2024-05-21 09:23:48.772367
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
revision: str = "29fe8f1f806b"
|
||||
down_revision: Union[str, None] = "012fb73ac359"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
indexes = inspector.get_indexes("flow")
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
indexes_names = [index["name"] for index in indexes]
|
||||
if "ix_flow_folder_id" not in indexes_names:
|
||||
batch_op.create_index(batch_op.f("ix_flow_folder_id"), ["folder_id"], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
indexes = inspector.get_indexes("flow")
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
indexes_names = [index["name"] for index in indexes]
|
||||
if "ix_flow_folder_id" in indexes_names:
|
||||
batch_op.drop_index(batch_op.f("ix_flow_folder_id"))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -31,9 +31,7 @@ def upgrade() -> None:
|
|||
"credential",
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("value", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column(
|
||||
"provider", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
),
|
||||
sa.Column("provider", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
"""Fix date times again
|
||||
|
||||
Revision ID: 4e5980a44eaa
|
||||
Revises: 79e675cb6752
|
||||
Create Date: 2024-04-12 18:11:06.454037
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from loguru import logger
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "4e5980a44eaa"
|
||||
down_revision: Union[str, None] = "79e675cb6752"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "apikey" in table_names:
|
||||
columns = inspector.get_columns("apikey")
|
||||
created_at_column = next((column for column in columns if column["name"] == "created_at"), None)
|
||||
if created_at_column is not None and isinstance(created_at_column["type"], postgresql.TIMESTAMP):
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=False,
|
||||
)
|
||||
else:
|
||||
if created_at_column is None:
|
||||
logger.warning("Column 'created_at' not found in table 'apikey'")
|
||||
else:
|
||||
logger.warning(f"Column 'created_at' has type {created_at_column['type']} in table 'apikey'")
|
||||
if "variable" in table_names:
|
||||
columns = inspector.get_columns("variable")
|
||||
created_at_column = next((column for column in columns if column["name"] == "created_at"), None)
|
||||
updated_at_column = next((column for column in columns if column["name"] == "updated_at"), None)
|
||||
with op.batch_alter_table("variable", schema=None) as batch_op:
|
||||
if created_at_column is not None and isinstance(created_at_column["type"], postgresql.TIMESTAMP):
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True,
|
||||
)
|
||||
else:
|
||||
if created_at_column is None:
|
||||
logger.warning("Column 'created_at' not found in table 'variable'")
|
||||
else:
|
||||
logger.warning(f"Column 'created_at' has type {created_at_column['type']} in table 'variable'")
|
||||
if updated_at_column is not None and isinstance(updated_at_column["type"], postgresql.TIMESTAMP):
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True,
|
||||
)
|
||||
else:
|
||||
if updated_at_column is None:
|
||||
logger.warning("Column 'updated_at' not found in table 'variable'")
|
||||
else:
|
||||
logger.warning(f"Column 'updated_at' has type {updated_at_column['type']} in table 'variable'")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "variable" in table_names:
|
||||
columns = inspector.get_columns("variable")
|
||||
created_at_column = next((column for column in columns if column["name"] == "created_at"), None)
|
||||
updated_at_column = next((column for column in columns if column["name"] == "updated_at"), None)
|
||||
with op.batch_alter_table("variable", schema=None) as batch_op:
|
||||
if updated_at_column is not None and isinstance(updated_at_column["type"], sa.DateTime):
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True,
|
||||
)
|
||||
else:
|
||||
if updated_at_column is None:
|
||||
logger.warning("Column 'updated_at' not found in table 'variable'")
|
||||
else:
|
||||
logger.warning(f"Column 'updated_at' has type {updated_at_column['type']} in table 'variable'")
|
||||
if created_at_column is not None and isinstance(created_at_column["type"], sa.DateTime):
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True,
|
||||
)
|
||||
else:
|
||||
if created_at_column is None:
|
||||
logger.warning("Column 'created_at' not found in table 'variable'")
|
||||
else:
|
||||
logger.warning(f"Column 'created_at' has type {created_at_column['type']} in table 'variable'")
|
||||
|
||||
if "apikey" in table_names:
|
||||
columns = inspector.get_columns("apikey")
|
||||
created_at_column = next((column for column in columns if column["name"] == "created_at"), None)
|
||||
if created_at_column is not None and isinstance(created_at_column["type"], sa.DateTime):
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=False,
|
||||
)
|
||||
else:
|
||||
if created_at_column is None:
|
||||
logger.warning("Column 'created_at' not found in table 'apikey'")
|
||||
else:
|
||||
logger.warning(f"Column 'created_at' has type {created_at_column['type']} in table 'apikey'")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
"""Modify nullable
|
||||
|
||||
Revision ID: 58b28437a398
|
||||
Revises: 4e5980a44eaa
|
||||
Create Date: 2024-04-13 10:57:23.061709
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from loguru import logger
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
down_revision: Union[str, None] = "4e5980a44eaa"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
# Revision identifiers, used by Alembic.
|
||||
revision = "58b28437a398"
|
||||
down_revision = "4e5980a44eaa"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn)
|
||||
tables = ["apikey", "variable"] # List of tables to modify
|
||||
|
||||
for table_name in tables:
|
||||
modify_nullable(conn, inspector, table_name, upgrade=True)
|
||||
|
||||
|
||||
def downgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn)
|
||||
tables = ["apikey", "variable"] # List of tables to revert
|
||||
|
||||
for table_name in tables:
|
||||
modify_nullable(conn, inspector, table_name, upgrade=False)
|
||||
|
||||
|
||||
def modify_nullable(conn, inspector, table_name, upgrade=True):
|
||||
columns = inspector.get_columns(table_name)
|
||||
nullable_changes = {"apikey": {"created_at": False}, "variable": {"created_at": True, "updated_at": True}}
|
||||
|
||||
if table_name in columns:
|
||||
with op.batch_alter_table(table_name, schema=None) as batch_op:
|
||||
for column_name, nullable_setting in nullable_changes.get(table_name, {}).items():
|
||||
column_info = next((col for col in columns if col["name"] == column_name), None)
|
||||
if column_info:
|
||||
current_nullable = column_info["nullable"]
|
||||
target_nullable = nullable_setting if upgrade else not nullable_setting
|
||||
|
||||
if current_nullable != target_nullable:
|
||||
batch_op.alter_column(
|
||||
column_name, existing_type=sa.DateTime(timezone=True), nullable=target_nullable
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Column '{column_name}' in table '{table_name}' already has nullable={target_nullable}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Column '{column_name}' not found in table '{table_name}'")
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
"""Add icon and icon_bg_color to Flow
|
||||
|
||||
Revision ID: 63b9c451fd30
|
||||
Revises: bc2f01c40e4a
|
||||
Create Date: 2024-03-06 10:53:47.148658
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "63b9c451fd30"
|
||||
down_revision: Union[str, None] = "bc2f01c40e4a"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names() # noqa
|
||||
column_names = [column["name"] for column in inspector.get_columns("flow")]
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "icon" not in column_names:
|
||||
batch_op.add_column(sa.Column("icon", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
if "icon_bg_color" not in column_names:
|
||||
batch_op.add_column(sa.Column("icon_bg_color", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names() # noqa
|
||||
column_names = [column["name"] for column in inspector.get_columns("flow")]
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "icon" in column_names:
|
||||
batch_op.drop_column("icon")
|
||||
if "icon_bg_color" in column_names:
|
||||
batch_op.drop_column("icon_bg_color")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
"""Fix nullable
|
||||
|
||||
Revision ID: 6e7b581b5648
|
||||
Revises: 58b28437a398
|
||||
Create Date: 2024-04-30 09:17:45.024688
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "6e7b581b5648"
|
||||
down_revision: Union[str, None] = "58b28437a398"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
# table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
columns = inspector.get_columns("apikey")
|
||||
column_names = {column["name"]: column for column in columns}
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
created_at_column = [column for column in columns if column["name"] == "created_at"][0]
|
||||
if "created_at" in column_names and created_at_column.get("nullable"):
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.DATETIME(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text("(CURRENT_TIMESTAMP)"), # type: ignore
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
# table_names = inspector.get_table_names()
|
||||
columns = inspector.get_columns("apikey")
|
||||
column_names = {column["name"]: column for column in columns}
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
created_at_column = [column for column in columns if column["name"] == "created_at"][0]
|
||||
if "created_at" in column_names and not created_at_column.get("nullable"):
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.DATETIME(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text("(CURRENT_TIMESTAMP)"), # type: ignore
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -29,18 +29,14 @@ def upgrade() -> None:
|
|||
try:
|
||||
if "is_component" not in flow_columns:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("is_component", sa.Boolean(), nullable=True)
|
||||
)
|
||||
except Exception as e:
|
||||
batch_op.add_column(sa.Column("is_component", sa.Boolean(), nullable=True))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if "store_api_key" not in user_columns:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("store_api_key", sqlmodel.AutoString(), nullable=True)
|
||||
)
|
||||
except Exception as e:
|
||||
batch_op.add_column(sa.Column("store_api_key", sqlmodel.AutoString(), nullable=True))
|
||||
except Exception:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
"""Change datetime type
|
||||
|
||||
Revision ID: 79e675cb6752
|
||||
Revises: e3bc869fa272
|
||||
Create Date: 2024-04-11 19:23:10.697335
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
from loguru import logger
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "79e675cb6752"
|
||||
down_revision: Union[str, None] = "e3bc869fa272"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "apikey" in table_names:
|
||||
columns = inspector.get_columns("apikey")
|
||||
created_at_column = next((column for column in columns if column["name"] == "created_at"), None)
|
||||
if created_at_column is not None and isinstance(created_at_column["type"], postgresql.TIMESTAMP):
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=False,
|
||||
)
|
||||
else:
|
||||
if created_at_column is None:
|
||||
logger.warning("Column 'created_at' not found in table 'apikey'")
|
||||
else:
|
||||
logger.warning(f"Column 'created_at' has type {created_at_column['type']} in table 'apikey'")
|
||||
if "variable" in table_names:
|
||||
columns = inspector.get_columns("variable")
|
||||
created_at_column = next((column for column in columns if column["name"] == "created_at"), None)
|
||||
updated_at_column = next((column for column in columns if column["name"] == "updated_at"), None)
|
||||
with op.batch_alter_table("variable", schema=None) as batch_op:
|
||||
if created_at_column is not None and isinstance(created_at_column["type"], postgresql.TIMESTAMP):
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True,
|
||||
)
|
||||
else:
|
||||
if created_at_column is None:
|
||||
logger.warning("Column 'created_at' not found in table 'variable'")
|
||||
else:
|
||||
logger.warning(f"Column 'created_at' has type {created_at_column['type']} in table 'variable'")
|
||||
if updated_at_column is not None and isinstance(updated_at_column["type"], postgresql.TIMESTAMP):
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=True,
|
||||
)
|
||||
else:
|
||||
if updated_at_column is None:
|
||||
logger.warning("Column 'updated_at' not found in table 'variable'")
|
||||
else:
|
||||
logger.warning(f"Column 'updated_at' has type {updated_at_column['type']} in table 'variable'")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "variable" in table_names:
|
||||
columns = inspector.get_columns("variable")
|
||||
created_at_column = next((column for column in columns if column["name"] == "created_at"), None)
|
||||
updated_at_column = next((column for column in columns if column["name"] == "updated_at"), None)
|
||||
with op.batch_alter_table("variable", schema=None) as batch_op:
|
||||
if updated_at_column is not None and isinstance(updated_at_column["type"], sa.DateTime):
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True,
|
||||
)
|
||||
else:
|
||||
if updated_at_column is None:
|
||||
logger.warning("Column 'updated_at' not found in table 'variable'")
|
||||
else:
|
||||
logger.warning(f"Column 'updated_at' has type {updated_at_column['type']} in table 'variable'")
|
||||
if created_at_column is not None and isinstance(created_at_column["type"], sa.DateTime):
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=True,
|
||||
)
|
||||
else:
|
||||
if created_at_column is None:
|
||||
logger.warning("Column 'created_at' not found in table 'variable'")
|
||||
else:
|
||||
logger.warning(f"Column 'created_at' has type {created_at_column['type']} in table 'variable'")
|
||||
|
||||
if "apikey" in table_names:
|
||||
columns = inspector.get_columns("apikey")
|
||||
created_at_column = next((column for column in columns if column["name"] == "created_at"), None)
|
||||
if created_at_column is not None and isinstance(created_at_column["type"], sa.DateTime):
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=False,
|
||||
)
|
||||
else:
|
||||
if created_at_column is None:
|
||||
logger.warning("Column 'created_at' not found in table 'apikey'")
|
||||
else:
|
||||
logger.warning(f"Column 'created_at' has type {created_at_column['type']} in table 'apikey'")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -30,9 +30,7 @@ def upgrade() -> None:
|
|||
try:
|
||||
if "name" in api_key_columns:
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"name", existing_type=sa.VARCHAR(), nullable=False
|
||||
)
|
||||
batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=False)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
|
@ -40,15 +38,9 @@ def upgrade() -> None:
|
|||
try:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "updated_at" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("updated_at", sa.DateTime(), nullable=True))
|
||||
if "folder" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
)
|
||||
)
|
||||
batch_op.add_column(sa.Column("folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
|
@ -60,15 +52,19 @@ def upgrade() -> None:
|
|||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
column_names = [column["name"] for column in inspector.get_columns("flow")]
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.drop_column("folder")
|
||||
batch_op.drop_column("updated_at")
|
||||
if "folder" in column_names:
|
||||
batch_op.drop_column("folder")
|
||||
if "updated_at" in column_names:
|
||||
batch_op.drop_column("updated_at")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
try:
|
||||
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=True)
|
||||
except Exception as e:
|
||||
|
|
@ -32,33 +32,19 @@ def upgrade() -> None:
|
|||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
flow_columns = [column["name"] for column in inspector.get_columns("flow")]
|
||||
if "is_component" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("is_component", sa.Boolean(), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("is_component", sa.Boolean(), nullable=True))
|
||||
if "updated_at" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("updated_at", sa.DateTime(), nullable=True))
|
||||
if "folder" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
)
|
||||
)
|
||||
batch_op.add_column(sa.Column("folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
if "user_id" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=True))
|
||||
indices = inspector.get_indexes("flow")
|
||||
indices_names = [index["name"] for index in indices]
|
||||
if "ix_flow_user_id" not in indices_names:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_flow_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_flow_user_id"), ["user_id"], unique=False)
|
||||
if "fk_flow_user_id_user" not in indices_names:
|
||||
batch_op.create_foreign_key(
|
||||
"fk_flow_user_id_user", "user", ["user_id"], ["id"]
|
||||
)
|
||||
batch_op.create_foreign_key("fk_flow_user_id_user", "user", ["user_id"], ["id"])
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -33,21 +33,13 @@ def upgrade() -> None:
|
|||
if "updated_at" not in flow_columns:
|
||||
batch_op.add_column(sa.Column("updated_at", sa.DateTime(), nullable=True))
|
||||
if "folder" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
if "user_id" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=True))
|
||||
if "ix_flow_user_id" not in flow_indexes:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_flow_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_flow_user_id"), ["user_id"], unique=False)
|
||||
if "flow_user_id_fkey" not in flow_fks:
|
||||
batch_op.create_foreign_key(
|
||||
"flow_user_id_fkey", "user", ["user_id"], ["id"]
|
||||
)
|
||||
batch_op.create_foreign_key("flow_user_id_fkey", "user", ["user_id"], ["id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
"""Set name and value to not nullable
|
||||
|
||||
Revision ID: c153816fd85f
|
||||
Revises: 1f4d6df60295
|
||||
Create Date: 2024-04-30 14:31:23.898995
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "c153816fd85f"
|
||||
down_revision: Union[str, None] = "1f4d6df60295"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
columns = inspector.get_columns("variable")
|
||||
with op.batch_alter_table("variable", schema=None) as batch_op:
|
||||
name_column = [column for column in columns if column["name"] == "name"][0]
|
||||
if name_column and name_column["nullable"]:
|
||||
batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=False)
|
||||
value_column = [column for column in columns if column["name"] == "value"][0]
|
||||
if value_column and value_column["nullable"]:
|
||||
batch_op.alter_column("value", existing_type=sa.VARCHAR(), nullable=False)
|
||||
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
columns = inspector.get_columns("variable")
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("variable", schema=None) as batch_op:
|
||||
name_column = [column for column in columns if column["name"] == "name"][0]
|
||||
if name_column and not name_column["nullable"]:
|
||||
batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=True)
|
||||
value_column = [column for column in columns if column["name"] == "value"][0]
|
||||
if value_column and not value_column["nullable"]:
|
||||
batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=True)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
"""Fix nullable
|
||||
|
||||
Revision ID: e3bc869fa272
|
||||
Revises: 1a110b568907
|
||||
Create Date: 2024-04-10 19:17:22.820455
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "e3bc869fa272"
|
||||
down_revision: Union[str, None] = "1a110b568907"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "variable" not in table_names:
|
||||
return
|
||||
columns = [column for column in inspector.get_columns("variable")]
|
||||
column_names = [column["name"] for column in columns]
|
||||
|
||||
with op.batch_alter_table("variable", schema=None) as batch_op:
|
||||
if "created_at" in column_names:
|
||||
created_at_colunmn = next(column for column in columns if column["name"] == "created_at")
|
||||
if created_at_colunmn["nullable"] is False:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
nullable=True,
|
||||
# existing_server_default expects str | bool | Identity | Computed | None
|
||||
# sa.text("now()") is not a valid value for existing_server_default
|
||||
existing_server_default=False,
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "variable" not in table_names:
|
||||
return
|
||||
columns = [column for column in inspector.get_columns("variable")]
|
||||
column_names = [column["name"] for column in columns]
|
||||
with op.batch_alter_table("variable", schema=None) as batch_op:
|
||||
if "created_at" in column_names:
|
||||
created_at_colunmn = next(column for column in columns if column["name"] == "created_at")
|
||||
if created_at_colunmn["nullable"] is True:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
nullable=False,
|
||||
existing_server_default=False,
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -19,7 +19,7 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
connection = op.get_bind()
|
||||
connection = op.get_bind() # noqa
|
||||
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -22,9 +22,7 @@ def upgrade() -> None:
|
|||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"user_id", existing_type=sa.CHAR(length=32), nullable=True
|
||||
)
|
||||
batch_op.alter_column("user_id", existing_type=sa.CHAR(length=32), nullable=True)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
|
@ -36,9 +34,7 @@ def downgrade() -> None:
|
|||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"user_id", existing_type=sa.CHAR(length=32), nullable=False
|
||||
)
|
||||
batch_op.alter_column("user_id", existing_type=sa.CHAR(length=32), nullable=False)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
|
@ -31,9 +31,7 @@ def upgrade() -> None:
|
|||
try:
|
||||
if "credential" in tables and "fk_credential_user_id" not in foreign_keys_names:
|
||||
with op.batch_alter_table("credential", schema=None) as batch_op:
|
||||
batch_op.create_foreign_key(
|
||||
"fk_credential_user_id", "user", ["user_id"], ["id"]
|
||||
)
|
||||
batch_op.create_foreign_key("fk_credential_user_id", "user", ["user_id"], ["id"])
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
|
@ -4,7 +4,6 @@ from fastapi import APIRouter
|
|||
from langflow.api.v1 import (
|
||||
api_key_router,
|
||||
chat_router,
|
||||
credentials_router,
|
||||
endpoints_router,
|
||||
files_router,
|
||||
flows_router,
|
||||
|
|
@ -13,6 +12,8 @@ from langflow.api.v1 import (
|
|||
store_router,
|
||||
users_router,
|
||||
validate_router,
|
||||
variables_router,
|
||||
folders_router,
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
|
|
@ -26,6 +27,7 @@ router.include_router(flows_router)
|
|||
router.include_router(users_router)
|
||||
router.include_router(api_key_router)
|
||||
router.include_router(login_router)
|
||||
router.include_router(credentials_router)
|
||||
router.include_router(variables_router)
|
||||
router.include_router(files_router)
|
||||
router.include_router(monitor_router)
|
||||
router.include_router(folders_router)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from platformdirs import user_cache_dir
|
||||
|
|
@ -13,6 +13,7 @@ from langflow.services.store.schema import StoreComponentCreate
|
|||
from langflow.services.store.utils import get_lf_version_from_pypi
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.services.database.models.flow.model import Flow
|
||||
|
||||
|
||||
|
|
@ -124,6 +125,9 @@ def update_template_field(frontend_template, key, value_dict):
|
|||
template_field["value"] = ""
|
||||
template_field["file_path"] = file_path_value
|
||||
|
||||
if "load_from_db" in value_dict and value_dict["load_from_db"]:
|
||||
template_field["load_from_db"] = value_dict["load_from_db"]
|
||||
|
||||
|
||||
def get_file_path_value(file_path):
|
||||
"""Get the file path value if the file exists, else return empty string."""
|
||||
|
|
@ -136,12 +140,15 @@ def get_file_path_value(file_path):
|
|||
# If the path is not in the cache dir, return empty string
|
||||
# This is to prevent access to files outside the cache dir
|
||||
# If the path is not a file, return empty string
|
||||
if not path.exists() or not str(path).startswith(user_cache_dir("langflow", "langflow")):
|
||||
if not str(path).startswith(user_cache_dir("langflow", "langflow")):
|
||||
return ""
|
||||
|
||||
if not path.exists():
|
||||
return ""
|
||||
return file_path
|
||||
|
||||
|
||||
def validate_is_component(flows: List["Flow"]):
|
||||
def validate_is_component(flows: list["Flow"]):
|
||||
for flow in flows:
|
||||
if not flow.data or flow.is_component is not None:
|
||||
continue
|
||||
|
|
@ -160,7 +167,7 @@ def get_is_component_from_data(data: dict):
|
|||
|
||||
|
||||
async def check_langflow_version(component: StoreComponentCreate):
|
||||
from langflow import __version__ as current_version
|
||||
from langflow.version.version import __version__ as current_version # type: ignore
|
||||
|
||||
if not component.last_tested_version:
|
||||
component.last_tested_version = current_version
|
||||
|
|
@ -197,22 +204,28 @@ def format_elapsed_time(elapsed_time: float) -> str:
|
|||
return f"{minutes} {minutes_unit}, {seconds} {seconds_unit}"
|
||||
|
||||
|
||||
def build_and_cache_graph(
|
||||
async def build_and_cache_graph_from_db(
|
||||
flow_id: str,
|
||||
session: Session,
|
||||
chat_service: "ChatService",
|
||||
graph: Optional[Graph] = None,
|
||||
):
|
||||
"""Build and cache the graph."""
|
||||
flow: Optional[Flow] = session.get(Flow, flow_id)
|
||||
if not flow or not flow.data:
|
||||
raise ValueError("Invalid flow ID")
|
||||
other_graph = Graph.from_payload(flow.data, flow_id)
|
||||
if graph is None:
|
||||
graph = other_graph
|
||||
else:
|
||||
graph = graph.update(other_graph)
|
||||
chat_service.set_cache(flow_id, graph)
|
||||
graph = Graph.from_payload(flow.data, flow_id)
|
||||
await chat_service.set_cache(flow_id, graph)
|
||||
return graph
|
||||
|
||||
|
||||
async def build_and_cache_graph_from_data(
|
||||
flow_id: str,
|
||||
chat_service: "ChatService",
|
||||
graph_data: dict,
|
||||
): # -> Graph | Any:
|
||||
"""Build and cache the graph."""
|
||||
graph = Graph.from_payload(graph_data, flow_id)
|
||||
await chat_service.set_cache(flow_id, graph)
|
||||
return graph
|
||||
|
||||
|
||||
|
|
@ -238,3 +251,69 @@ def format_exception_message(exc: Exception) -> str:
|
|||
if isinstance(causing_exception, SyntaxError):
|
||||
return format_syntax_error_message(causing_exception)
|
||||
return str(exc)
|
||||
|
||||
|
||||
async def get_next_runnable_vertices(
|
||||
graph: Graph,
|
||||
vertex: "Vertex",
|
||||
vertex_id: str,
|
||||
chat_service: ChatService,
|
||||
flow_id: str,
|
||||
):
|
||||
"""
|
||||
Retrieves the next runnable vertices in the graph for a given vertex.
|
||||
|
||||
Args:
|
||||
graph (Graph): The graph object representing the flow.
|
||||
vertex (Vertex): The current vertex.
|
||||
vertex_id (str): The ID of the current vertex.
|
||||
chat_service (ChatService): The chat service object.
|
||||
flow_id (str): The ID of the flow.
|
||||
|
||||
Returns:
|
||||
list: A list of IDs of the next runnable vertices.
|
||||
|
||||
"""
|
||||
async with chat_service._cache_locks[flow_id] as lock:
|
||||
graph.remove_from_predecessors(vertex_id)
|
||||
direct_successors_ready = [v for v in vertex.successors_ids if graph.is_vertex_runnable(v)]
|
||||
if not direct_successors_ready:
|
||||
# No direct successors ready, look for runnable predecessors of successors
|
||||
next_runnable_vertices = graph.find_runnable_predecessors_for_successors(vertex_id)
|
||||
else:
|
||||
next_runnable_vertices = direct_successors_ready
|
||||
|
||||
for v_id in set(next_runnable_vertices): # Use set to avoid duplicates
|
||||
graph.vertices_to_run.remove(v_id)
|
||||
graph.remove_from_predecessors(v_id)
|
||||
await chat_service.set_cache(key=flow_id, data=graph, lock=lock)
|
||||
return next_runnable_vertices
|
||||
|
||||
|
||||
def get_top_level_vertices(graph, vertices_ids):
|
||||
"""
|
||||
Retrieves the top-level vertices from the given graph based on the provided vertex IDs.
|
||||
|
||||
Args:
|
||||
graph (Graph): The graph object containing the vertices.
|
||||
vertices_ids (list): A list of vertex IDs.
|
||||
|
||||
Returns:
|
||||
list: A list of top-level vertex IDs.
|
||||
|
||||
"""
|
||||
top_level_vertices = []
|
||||
for vertex_id in vertices_ids:
|
||||
vertex = graph.get_vertex(vertex_id)
|
||||
if vertex.parent_is_top_level:
|
||||
top_level_vertices.append(vertex.parent_node_id)
|
||||
else:
|
||||
top_level_vertices.append(vertex_id)
|
||||
return top_level_vertices
|
||||
|
||||
|
||||
def parse_exception(exc):
|
||||
"""Parse the exception message."""
|
||||
if hasattr(exc, "body"):
|
||||
return exc.body["message"]
|
||||
return str(exc)
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
from langflow.api.v1.api_key import router as api_key_router
|
||||
from langflow.api.v1.chat import router as chat_router
|
||||
from langflow.api.v1.credential import router as credentials_router
|
||||
from langflow.api.v1.endpoints import router as endpoints_router
|
||||
from langflow.api.v1.files import router as files_router
|
||||
from langflow.api.v1.flows import router as flows_router
|
||||
|
|
@ -9,6 +8,8 @@ from langflow.api.v1.monitor import router as monitor_router
|
|||
from langflow.api.v1.store import router as store_router
|
||||
from langflow.api.v1.users import router as users_router
|
||||
from langflow.api.v1.validate import router as validate_router
|
||||
from langflow.api.v1.variable import router as variables_router
|
||||
from langflow.api.v1.folders import router as folders_router
|
||||
|
||||
__all__ = [
|
||||
"chat_router",
|
||||
|
|
@ -19,7 +20,8 @@ __all__ = [
|
|||
"users_router",
|
||||
"api_key_router",
|
||||
"login_router",
|
||||
"credentials_router",
|
||||
"variables_router",
|
||||
"monitor_router",
|
||||
"files_router",
|
||||
"folders_router",
|
||||
]
|
||||
|
|
@ -8,20 +8,10 @@ from langflow.api.v1.schemas import ApiKeyCreateRequest, ApiKeysResponse
|
|||
from langflow.services.auth import utils as auth_utils
|
||||
|
||||
# Assuming you have these methods in your service layer
|
||||
from langflow.services.database.models.api_key.crud import (
|
||||
create_api_key,
|
||||
delete_api_key,
|
||||
get_api_keys,
|
||||
)
|
||||
from langflow.services.database.models.api_key.model import (
|
||||
ApiKeyCreate,
|
||||
UnmaskedApiKeyRead,
|
||||
)
|
||||
from langflow.services.database.models.api_key.crud import create_api_key, delete_api_key, get_api_keys
|
||||
from langflow.services.database.models.api_key.model import ApiKeyCreate, UnmaskedApiKeyRead
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import (
|
||||
get_session,
|
||||
get_settings_service,
|
||||
)
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
165
src/backend/base/langflow/api/v1/base.py
Normal file
165
src/backend/base/langflow/api/v1/base.py
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, field_validator, model_serializer
|
||||
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
|
||||
|
||||
class CacheResponse(BaseModel):
|
||||
data: dict
|
||||
|
||||
|
||||
class Code(BaseModel):
|
||||
code: str
|
||||
|
||||
|
||||
class FrontendNodeRequest(FrontendNode):
|
||||
template: dict # type: ignore
|
||||
|
||||
@model_serializer(mode="wrap")
|
||||
def serialize_model(self, handler):
|
||||
# Override the default serialization method in FrontendNode
|
||||
# because we don't need the name in the response (i.e. {name: {}})
|
||||
return handler(self)
|
||||
|
||||
|
||||
class ValidatePromptRequest(BaseModel):
|
||||
name: str
|
||||
template: str
|
||||
custom_fields: Optional[dict] = None
|
||||
frontend_node: Optional[FrontendNodeRequest] = None
|
||||
|
||||
|
||||
# Build ValidationResponse class for {"imports": {"errors": []}, "function": {"errors": []}}
|
||||
class CodeValidationResponse(BaseModel):
|
||||
imports: dict
|
||||
function: dict
|
||||
|
||||
@field_validator("imports")
|
||||
@classmethod
|
||||
def validate_imports(cls, v):
|
||||
return v or {"errors": []}
|
||||
|
||||
@field_validator("function")
|
||||
@classmethod
|
||||
def validate_function(cls, v):
|
||||
return v or {"errors": []}
|
||||
|
||||
|
||||
class PromptValidationResponse(BaseModel):
|
||||
input_variables: list
|
||||
# object return for tweak call
|
||||
frontend_node: Optional[FrontendNodeRequest] = None
|
||||
|
||||
|
||||
INVALID_CHARACTERS = {
|
||||
" ",
|
||||
",",
|
||||
".",
|
||||
":",
|
||||
";",
|
||||
"!",
|
||||
"?",
|
||||
"/",
|
||||
"\\",
|
||||
"(",
|
||||
")",
|
||||
"[",
|
||||
"]",
|
||||
}
|
||||
|
||||
INVALID_NAMES = {
|
||||
"input_variables",
|
||||
"output_parser",
|
||||
"partial_variables",
|
||||
"template",
|
||||
"template_format",
|
||||
"validate_template",
|
||||
}
|
||||
|
||||
|
||||
def is_json_like(var):
|
||||
if var.startswith("{{") and var.endswith("}}"):
|
||||
# If it is a double brance variable
|
||||
# we don't want to validate any of its content
|
||||
return True
|
||||
# the above doesn't work on all cases because the json string can be multiline
|
||||
# or indented which can add \n or spaces at the start or end of the string
|
||||
# test_case_3 new_var == '\n{{\n "test": "hello",\n "text": "world"\n}}\n'
|
||||
# what we can do is to remove the \n and spaces from the start and end of the string
|
||||
# and then check if the string starts with {{ and ends with }}
|
||||
var = var.strip()
|
||||
var = var.replace("\n", "")
|
||||
var = var.replace(" ", "")
|
||||
# Now it should be a valid json string
|
||||
return var.startswith("{{") and var.endswith("}}")
|
||||
|
||||
|
||||
def fix_variable(var, invalid_chars, wrong_variables):
|
||||
if not var:
|
||||
return var, invalid_chars, wrong_variables
|
||||
new_var = var
|
||||
|
||||
# Handle variables starting with a number
|
||||
if var[0].isdigit():
|
||||
invalid_chars.append(var[0])
|
||||
new_var, invalid_chars, wrong_variables = fix_variable(var[1:], invalid_chars, wrong_variables)
|
||||
|
||||
# Temporarily replace {{ and }} to avoid treating them as invalid
|
||||
new_var = new_var.replace("{{", "ᴛᴇᴍᴘᴏᴘᴇɴ").replace("}}", "ᴛᴇᴍᴘᴄʟᴏsᴇ")
|
||||
|
||||
# Remove invalid characters
|
||||
for char in new_var:
|
||||
if char in INVALID_CHARACTERS:
|
||||
invalid_chars.append(char)
|
||||
new_var = new_var.replace(char, "")
|
||||
if var not in wrong_variables: # Avoid duplicating entries
|
||||
wrong_variables.append(var)
|
||||
|
||||
# Restore {{ and }}
|
||||
new_var = new_var.replace("ᴛᴇᴍᴘᴏᴘᴇɴ", "{{").replace("ᴛᴇᴍᴘᴄʟᴏsᴇ", "}}")
|
||||
|
||||
return new_var, invalid_chars, wrong_variables
|
||||
|
||||
|
||||
def check_variable(var, invalid_chars, wrong_variables, empty_variables):
|
||||
if any(char in invalid_chars for char in var):
|
||||
wrong_variables.append(var)
|
||||
elif var == "":
|
||||
empty_variables.append(var)
|
||||
return wrong_variables, empty_variables
|
||||
|
||||
|
||||
def check_for_errors(input_variables, fixed_variables, wrong_variables, empty_variables):
|
||||
if any(var for var in input_variables if var not in fixed_variables):
|
||||
error_message = (
|
||||
f"Error: Input variables contain invalid characters or formats. \n"
|
||||
f"Invalid variables: {', '.join(wrong_variables)}.\n"
|
||||
f"Empty variables: {', '.join(empty_variables)}. \n"
|
||||
f"Fixed variables: {', '.join(fixed_variables)}."
|
||||
)
|
||||
raise ValueError(error_message)
|
||||
|
||||
|
||||
def check_input_variables(input_variables):
|
||||
invalid_chars = []
|
||||
fixed_variables = []
|
||||
wrong_variables = []
|
||||
empty_variables = []
|
||||
variables_to_check = []
|
||||
|
||||
for var in input_variables:
|
||||
# First, let's check if the variable is a JSON string
|
||||
# because if it is, it won't be considered a variable
|
||||
# and we don't need to validate it
|
||||
if is_json_like(var):
|
||||
continue
|
||||
|
||||
new_var, wrong_variables, empty_variables = fix_variable(var, invalid_chars, wrong_variables)
|
||||
wrong_variables, empty_variables = check_variable(var, INVALID_CHARACTERS, wrong_variables, empty_variables)
|
||||
fixed_variables.append(new_var)
|
||||
variables_to_check.append(var)
|
||||
|
||||
check_for_errors(variables_to_check, fixed_variables, wrong_variables, empty_variables)
|
||||
|
||||
return fixed_variables
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from langchain.schema import AgentAction, AgentFinish
|
||||
from langchain_core.callbacks.base import AsyncCallbackHandler
|
||||
from loguru import logger
|
||||
|
||||
from langflow.api.v1.schemas import ChatResponse, PromptResponse
|
||||
from langflow.services.deps import get_chat_service, get_socket_service
|
||||
from langflow.utils.util import remove_ansi_escape_codes
|
||||
from langchain_core.agents import AgentAction, AgentFinish
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.socket.service import SocketIOService
|
||||
|
|
@ -33,9 +32,7 @@ class AsyncStreamingLLMCallbackHandleSIO(AsyncCallbackHandler):
|
|||
resp = ChatResponse(message=token, type="stream", intermediate_steps="")
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
|
||||
async def on_tool_start(
|
||||
self, serialized: Dict[str, Any], input_str: str, **kwargs: Any
|
||||
) -> Any:
|
||||
async def on_tool_start(self, serialized: Dict[str, Any], input_str: str, **kwargs: Any) -> Any:
|
||||
"""Run when tool starts running."""
|
||||
resp = ChatResponse(
|
||||
message="",
|
||||
|
|
@ -73,9 +70,7 @@ class AsyncStreamingLLMCallbackHandleSIO(AsyncCallbackHandler):
|
|||
try:
|
||||
# This is to emulate the stream of tokens
|
||||
for resp in resps:
|
||||
await self.socketio_service.emit_token(
|
||||
to=self.sid, data=resp.model_dump()
|
||||
)
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
except Exception as exc:
|
||||
logger.error(f"Error sending response: {exc}")
|
||||
|
||||
|
|
@ -101,9 +96,7 @@ class AsyncStreamingLLMCallbackHandleSIO(AsyncCallbackHandler):
|
|||
resp = PromptResponse(
|
||||
prompt=text,
|
||||
)
|
||||
await self.socketio_service.emit_message(
|
||||
to=self.sid, data=resp.model_dump()
|
||||
)
|
||||
await self.socketio_service.emit_message(to=self.sid, data=resp.model_dump())
|
||||
|
||||
async def on_agent_action(self, action: AgentAction, **kwargs: Any):
|
||||
log = f"Thought: {action.log}"
|
||||
|
|
@ -113,9 +106,7 @@ class AsyncStreamingLLMCallbackHandleSIO(AsyncCallbackHandler):
|
|||
logs = log.split("\n")
|
||||
for log in logs:
|
||||
resp = ChatResponse(message="", type="stream", intermediate_steps=log)
|
||||
await self.socketio_service.emit_token(
|
||||
to=self.sid, data=resp.model_dump()
|
||||
)
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
else:
|
||||
resp = ChatResponse(message="", type="stream", intermediate_steps=log)
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
338
src/backend/base/langflow/api/v1/chat.py
Normal file
338
src/backend/base/langflow/api/v1/chat.py
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
import time
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, Annotated, Optional
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from loguru import logger
|
||||
|
||||
from langflow.api.utils import (
|
||||
build_and_cache_graph_from_data,
|
||||
build_and_cache_graph_from_db,
|
||||
format_elapsed_time,
|
||||
format_exception_message,
|
||||
get_top_level_vertices,
|
||||
parse_exception,
|
||||
)
|
||||
from langflow.api.v1.schemas import (
|
||||
FlowDataRequest,
|
||||
InputValueRequest,
|
||||
ResultDataResponse,
|
||||
StreamData,
|
||||
VertexBuildResponse,
|
||||
VerticesOrderResponse,
|
||||
)
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.chat.service import ChatService
|
||||
from langflow.services.deps import get_chat_service, get_session, get_session_service
|
||||
from langflow.services.monitor.utils import log_vertex_build
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.vertex.types import InterfaceVertex
|
||||
from langflow.services.session.service import SessionService
|
||||
|
||||
router = APIRouter(tags=["Chat"])
|
||||
|
||||
|
||||
async def try_running_celery_task(vertex, user_id):
|
||||
# Try running the task in celery
|
||||
# and set the task_id to the local vertex
|
||||
# if it fails, run the task locally
|
||||
try:
|
||||
from langflow.worker import build_vertex
|
||||
|
||||
task = build_vertex.delay(vertex)
|
||||
vertex.task_id = task.id
|
||||
except Exception as exc:
|
||||
logger.debug(f"Error running task in celery: {exc}")
|
||||
vertex.task_id = None
|
||||
await vertex.build(user_id=user_id)
|
||||
return vertex
|
||||
|
||||
|
||||
@router.post("/build/{flow_id}/vertices", response_model=VerticesOrderResponse)
|
||||
async def retrieve_vertices_order(
|
||||
flow_id: uuid.UUID,
|
||||
data: Optional[Annotated[Optional[FlowDataRequest], Body(embed=True)]] = None,
|
||||
stop_component_id: Optional[str] = None,
|
||||
start_component_id: Optional[str] = None,
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
session=Depends(get_session),
|
||||
):
|
||||
"""
|
||||
Retrieve the vertices order for a given flow.
|
||||
|
||||
Args:
|
||||
flow_id (str): The ID of the flow.
|
||||
data (Optional[FlowDataRequest], optional): The flow data. Defaults to None.
|
||||
stop_component_id (str, optional): The ID of the stop component. Defaults to None.
|
||||
start_component_id (str, optional): The ID of the start component. Defaults to None.
|
||||
chat_service (ChatService, optional): The chat service dependency. Defaults to Depends(get_chat_service).
|
||||
session (Session, optional): The session dependency. Defaults to Depends(get_session).
|
||||
|
||||
Returns:
|
||||
VerticesOrderResponse: The response containing the ordered vertex IDs and the run ID.
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is an error checking the build status.
|
||||
"""
|
||||
try:
|
||||
flow_id_str = str(flow_id)
|
||||
# First, we need to check if the flow_id is in the cache
|
||||
if not data:
|
||||
graph = await build_and_cache_graph_from_db(flow_id=flow_id_str, session=session, chat_service=chat_service)
|
||||
else:
|
||||
graph = await build_and_cache_graph_from_data(
|
||||
flow_id=flow_id_str, graph_data=data.model_dump(), chat_service=chat_service
|
||||
)
|
||||
graph.validate_stream()
|
||||
if stop_component_id or start_component_id:
|
||||
try:
|
||||
first_layer = graph.sort_vertices(stop_component_id, start_component_id)
|
||||
except Exception as exc:
|
||||
logger.error(exc)
|
||||
first_layer = graph.sort_vertices()
|
||||
else:
|
||||
first_layer = graph.sort_vertices()
|
||||
# When we send vertices to the frontend
|
||||
# we need to remove them from the predecessors
|
||||
# so they are not considered for building again
|
||||
# which duplicates the results
|
||||
for vertex_id in first_layer:
|
||||
graph.remove_from_predecessors(vertex_id)
|
||||
|
||||
# Now vertices is a list of lists
|
||||
# We need to get the id of each vertex
|
||||
# and return the same structure but only with the ids
|
||||
run_id = uuid.uuid4()
|
||||
graph.set_run_id(run_id)
|
||||
vertices_to_run = list(graph.vertices_to_run) + get_top_level_vertices(graph, graph.vertices_to_run)
|
||||
return VerticesOrderResponse(ids=first_layer, run_id=run_id, vertices_to_run=vertices_to_run)
|
||||
|
||||
except Exception as exc:
|
||||
if "stream or streaming set to True" in str(exc):
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
logger.error(f"Error checking build status: {exc}")
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/build/{flow_id}/vertices/{vertex_id}")
|
||||
async def build_vertex(
|
||||
flow_id: uuid.UUID,
|
||||
vertex_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
inputs: Annotated[Optional[InputValueRequest], Body(embed=True)] = None,
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
current_user=Depends(get_current_active_user),
|
||||
):
|
||||
"""Build a vertex instead of the entire graph.
|
||||
|
||||
Args:
|
||||
flow_id (str): The ID of the flow.
|
||||
vertex_id (str): The ID of the vertex to build.
|
||||
background_tasks (BackgroundTasks): The background tasks object for logging.
|
||||
inputs (Optional[InputValueRequest], optional): The input values for the vertex. Defaults to None.
|
||||
chat_service (ChatService, optional): The chat service dependency. Defaults to Depends(get_chat_service).
|
||||
current_user (Any, optional): The current user dependency. Defaults to Depends(get_current_active_user).
|
||||
|
||||
Returns:
|
||||
VertexBuildResponse: The response containing the built vertex information.
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is an error building the vertex.
|
||||
|
||||
"""
|
||||
flow_id_str = str(flow_id)
|
||||
|
||||
next_runnable_vertices = []
|
||||
top_level_vertices = []
|
||||
try:
|
||||
start_time = time.perf_counter()
|
||||
cache = await chat_service.get_cache(flow_id_str)
|
||||
if not cache:
|
||||
# If there's no cache
|
||||
logger.warning(f"No cache found for {flow_id_str}. Building graph starting at {vertex_id}")
|
||||
graph = await build_and_cache_graph_from_db(
|
||||
flow_id=flow_id_str, session=next(get_session()), chat_service=chat_service
|
||||
)
|
||||
else:
|
||||
graph = cache.get("result")
|
||||
vertex = graph.get_vertex(vertex_id)
|
||||
try:
|
||||
lock = chat_service._cache_locks[flow_id_str]
|
||||
(
|
||||
next_runnable_vertices,
|
||||
top_level_vertices,
|
||||
result_dict,
|
||||
params,
|
||||
valid,
|
||||
artifacts,
|
||||
vertex,
|
||||
) = await graph.build_vertex(
|
||||
lock=lock,
|
||||
chat_service=chat_service,
|
||||
vertex_id=vertex_id,
|
||||
user_id=current_user.id,
|
||||
inputs_dict=inputs.model_dump() if inputs else {},
|
||||
)
|
||||
result_data_response = ResultDataResponse(**result_dict.model_dump())
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(f"Error building vertex: {exc}")
|
||||
params = format_exception_message(exc)
|
||||
valid = False
|
||||
result_data_response = ResultDataResponse(results={})
|
||||
artifacts = {}
|
||||
# If there's an error building the vertex
|
||||
# we need to clear the cache
|
||||
await chat_service.clear_cache(flow_id_str)
|
||||
|
||||
# Log the vertex build
|
||||
if not vertex.will_stream:
|
||||
background_tasks.add_task(
|
||||
log_vertex_build,
|
||||
flow_id=flow_id_str,
|
||||
vertex_id=vertex_id,
|
||||
valid=valid,
|
||||
params=params,
|
||||
data=result_data_response,
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
timedelta = time.perf_counter() - start_time
|
||||
duration = format_elapsed_time(timedelta)
|
||||
result_data_response.duration = duration
|
||||
result_data_response.timedelta = timedelta
|
||||
vertex.add_build_time(timedelta)
|
||||
inactivated_vertices = None
|
||||
inactivated_vertices = list(graph.inactivated_vertices)
|
||||
graph.reset_inactivated_vertices()
|
||||
graph.reset_activated_vertices()
|
||||
await chat_service.set_cache(flow_id_str, graph)
|
||||
|
||||
# graph.stop_vertex tells us if the user asked
|
||||
# to stop the build of the graph at a certain vertex
|
||||
# if it is in next_vertices_ids, we need to remove other
|
||||
# vertices from next_vertices_ids
|
||||
if graph.stop_vertex and graph.stop_vertex in next_runnable_vertices:
|
||||
next_runnable_vertices = [graph.stop_vertex]
|
||||
|
||||
build_response = VertexBuildResponse(
|
||||
inactivated_vertices=inactivated_vertices,
|
||||
next_vertices_ids=next_runnable_vertices,
|
||||
top_level_vertices=top_level_vertices,
|
||||
valid=valid,
|
||||
params=params,
|
||||
id=vertex.id,
|
||||
data=result_data_response,
|
||||
)
|
||||
return build_response
|
||||
except Exception as exc:
|
||||
logger.error(f"Error building vertex: {exc}")
|
||||
logger.exception(exc)
|
||||
message = parse_exception(exc)
|
||||
raise HTTPException(status_code=500, detail=message) from exc
|
||||
|
||||
|
||||
@router.get("/build/{flow_id}/{vertex_id}/stream", response_class=StreamingResponse)
|
||||
async def build_vertex_stream(
|
||||
flow_id: uuid.UUID,
|
||||
vertex_id: str,
|
||||
session_id: Optional[str] = None,
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
session_service: "SessionService" = Depends(get_session_service),
|
||||
):
|
||||
"""Build a vertex instead of the entire graph.
|
||||
|
||||
This function is responsible for building a single vertex instead of the entire graph.
|
||||
It takes the `flow_id` and `vertex_id` as required parameters, and an optional `session_id`.
|
||||
It also depends on the `ChatService` and `SessionService` services.
|
||||
|
||||
If `session_id` is not provided, it retrieves the graph from the cache using the `chat_service`.
|
||||
If `session_id` is provided, it loads the session data using the `session_service`.
|
||||
|
||||
Once the graph is obtained, it retrieves the specified vertex using the `vertex_id`.
|
||||
If the vertex does not support streaming, an error is raised.
|
||||
If the vertex has a built result, it sends the result as a chunk.
|
||||
If the vertex is not frozen or not built, it streams the vertex data.
|
||||
If the vertex has a result, it sends the result as a chunk.
|
||||
If none of the above conditions are met, an error is raised.
|
||||
|
||||
If any exception occurs during the process, an error message is sent.
|
||||
Finally, the stream is closed.
|
||||
|
||||
Returns:
|
||||
A `StreamingResponse` object with the streamed vertex data in text/event-stream format.
|
||||
|
||||
Raises:
|
||||
HTTPException: If an error occurs while building the vertex.
|
||||
"""
|
||||
try:
|
||||
flow_id_str = str(flow_id)
|
||||
|
||||
async def stream_vertex():
|
||||
try:
|
||||
if not session_id:
|
||||
cache = await chat_service.get_cache(flow_id_str)
|
||||
if not cache:
|
||||
# If there's no cache
|
||||
raise ValueError(f"No cache found for {flow_id_str}.")
|
||||
else:
|
||||
graph = cache.get("result")
|
||||
else:
|
||||
session_data = await session_service.load_session(session_id, flow_id=flow_id_str)
|
||||
graph, artifacts = session_data if session_data else (None, None)
|
||||
if not graph:
|
||||
raise ValueError(f"No graph found for {flow_id_str}.")
|
||||
|
||||
vertex: "InterfaceVertex" = graph.get_vertex(vertex_id)
|
||||
if not hasattr(vertex, "stream"):
|
||||
raise ValueError(f"Vertex {vertex_id} does not support streaming")
|
||||
if isinstance(vertex._built_result, str) and vertex._built_result:
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"message": f"Streaming vertex {vertex_id}"},
|
||||
)
|
||||
yield str(stream_data)
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"chunk": vertex._built_result},
|
||||
)
|
||||
yield str(stream_data)
|
||||
|
||||
elif not vertex.frozen or not vertex._built:
|
||||
logger.debug(f"Streaming vertex {vertex_id}")
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"message": f"Streaming vertex {vertex_id}"},
|
||||
)
|
||||
yield str(stream_data)
|
||||
async for chunk in vertex.stream():
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"chunk": chunk},
|
||||
)
|
||||
yield str(stream_data)
|
||||
elif vertex.result is not None:
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"chunk": vertex._built_result},
|
||||
)
|
||||
yield str(stream_data)
|
||||
else:
|
||||
raise ValueError(f"No result found for vertex {vertex_id}")
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(f"Error building vertex: {exc}")
|
||||
exc_message = parse_exception(exc)
|
||||
if exc_message == "The message must be an iterator or an async iterator.":
|
||||
exc_message = "This stream has already been closed."
|
||||
yield str(StreamData(event="error", data={"error": exc_message}))
|
||||
finally:
|
||||
logger.debug("Closing stream")
|
||||
yield str(StreamData(event="close", data={"message": "Stream closed"}))
|
||||
|
||||
return StreamingResponse(stream_vertex(), media_type="text/event-stream")
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail="Error building vertex") from exc
|
||||
457
src/backend/base/langflow/api/v1/endpoints.py
Normal file
457
src/backend/base/langflow/api/v1/endpoints.py
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING, Annotated, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
import sqlalchemy as sa
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, UploadFile, status
|
||||
from loguru import logger
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from langflow.api.utils import update_frontend_node_with_template_values
|
||||
from langflow.api.v1.schemas import (
|
||||
ConfigResponse,
|
||||
CustomComponentRequest,
|
||||
InputValueRequest,
|
||||
ProcessResponse,
|
||||
RunResponse,
|
||||
SimplifiedAPIRequest,
|
||||
TaskStatusResponse,
|
||||
UpdateCustomComponentRequest,
|
||||
UploadFileResponse,
|
||||
)
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.custom.utils import build_custom_component_template
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.processing.process import process_tweaks, run_graph_internal
|
||||
from langflow.schema.graph import Tweaks
|
||||
from langflow.services.auth.utils import api_key_security, get_current_active_user
|
||||
from langflow.services.cache.utils import save_uploaded_file
|
||||
from langflow.services.database.models.flow import Flow
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_session, get_session_service, get_settings_service, get_task_service
|
||||
from langflow.services.session.service import SessionService
|
||||
from langflow.services.task.service import TaskService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.settings.manager import SettingsService
|
||||
|
||||
router = APIRouter(tags=["Base"])
|
||||
|
||||
|
||||
@router.get("/all", dependencies=[Depends(get_current_active_user)])
|
||||
def get_all(
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
from langflow.interface.types import get_all_types_dict
|
||||
|
||||
logger.debug("Building langchain types dict")
|
||||
try:
|
||||
all_types_dict = get_all_types_dict(settings_service.settings.components_path)
|
||||
return all_types_dict
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True)
|
||||
async def simplified_run_flow(
|
||||
db: Annotated[Session, Depends(get_session)],
|
||||
flow_id: UUID,
|
||||
input_request: SimplifiedAPIRequest = SimplifiedAPIRequest(),
|
||||
stream: bool = False,
|
||||
api_key_user: User = Depends(api_key_security),
|
||||
session_service: SessionService = Depends(get_session_service),
|
||||
):
|
||||
"""
|
||||
Executes a specified flow by ID with input customization, performance enhancements through caching, and optional data streaming.
|
||||
|
||||
### Parameters:
|
||||
- `db` (Session): Database session for executing queries.
|
||||
- `flow_id` (str): Unique identifier of the flow to be executed.
|
||||
- `input_request` (SimplifiedAPIRequest): Request object containing input values, types, output selection, tweaks, and session ID.
|
||||
- `api_key_user` (User): User object derived from the provided API key, used for authentication.
|
||||
- `session_service` (SessionService): Service for managing flow sessions, essential for session reuse and caching.
|
||||
|
||||
### SimplifiedAPIRequest:
|
||||
- `input_value` (Optional[str], default=""): Input value to pass to the flow.
|
||||
- `input_type` (Optional[Literal["chat", "text", "any"]], default="chat"): Type of the input value, determining how the input is interpreted.
|
||||
- `output_type` (Optional[Literal["chat", "text", "any", "debug"]], default="chat"): Desired type of output, affecting which components' outputs are included in the response. If set to "debug", all outputs are returned.
|
||||
- `output_component` (Optional[str], default=None): Specific component output to retrieve. If provided, only the output of the specified component is returned. This overrides the `output_type` parameter.
|
||||
- `tweaks` (Optional[Tweaks], default=None): Adjustments to the flow's behavior, allowing for custom execution parameters.
|
||||
- `session_id` (Optional[str], default=None): An identifier for reusing session data, aiding in performance for subsequent requests.
|
||||
|
||||
|
||||
### Tweaks
|
||||
A dictionary of tweaks to customize the flow execution. The tweaks can be used to modify the flow's parameters and components. Tweaks can be overridden by the input values.
|
||||
You can use Component's `id` or Display Name as key to tweak a specific component (e.g., `{"Component Name": {"parameter_name": "value"}}`).
|
||||
You can also use the parameter name as key to tweak all components with that parameter (e.g., `{"parameter_name": "value"}`).
|
||||
|
||||
### Returns:
|
||||
- A `RunResponse` object containing the execution results, including selected (or all, based on `output_type`) outputs of the flow and the session ID, facilitating result retrieval and further interactions in a session context.
|
||||
|
||||
### Raises:
|
||||
- HTTPException: 404 if the specified flow ID curl -X 'POST' \
|
||||
|
||||
### Example:
|
||||
```bash
|
||||
curl -X 'POST' \
|
||||
'http://<your_server>/run/{flow_id}' \
|
||||
-H 'accept: application/json' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'x-api-key: YOU_API_KEY' \
|
||||
-H '
|
||||
-d '{
|
||||
"input_value": "Sample input",
|
||||
"input_type": "chat",
|
||||
"output_type": "chat",
|
||||
"tweaks": {},
|
||||
}'
|
||||
```
|
||||
|
||||
This endpoint provides a powerful interface for executing flows with enhanced flexibility and efficiency, supporting a wide range of applications by allowing for dynamic input and output configuration along with performance optimizations through session management and caching.
|
||||
"""
|
||||
session_id = input_request.session_id
|
||||
|
||||
try:
|
||||
flow_id_str = str(flow_id)
|
||||
artifacts = {}
|
||||
if input_request.session_id:
|
||||
session_data = await session_service.load_session(input_request.session_id, flow_id=flow_id_str)
|
||||
graph, artifacts = session_data if session_data else (None, None)
|
||||
if graph is None:
|
||||
raise ValueError(f"Session {input_request.session_id} not found")
|
||||
else:
|
||||
# Get the flow that matches the flow_id and belongs to the user
|
||||
# flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first()
|
||||
flow = db.exec(select(Flow).where(Flow.id == flow_id_str).where(Flow.user_id == api_key_user.id)).first()
|
||||
if flow is None:
|
||||
raise ValueError(f"Flow {flow_id_str} not found")
|
||||
|
||||
if flow.data is None:
|
||||
raise ValueError(f"Flow {flow_id_str} has no data")
|
||||
graph_data = flow.data
|
||||
|
||||
graph_data = process_tweaks(graph_data, input_request.tweaks or {}, stream=stream)
|
||||
graph = Graph.from_payload(graph_data, flow_id=flow_id_str, user_id=str(api_key_user.id))
|
||||
inputs = [
|
||||
InputValueRequest(components=[], input_value=input_request.input_value, type=input_request.input_type)
|
||||
]
|
||||
# outputs is a list of all components that should return output
|
||||
# we need to get them by checking their type
|
||||
# if the output type is debug, we return all outputs
|
||||
# if the output type is any, we return all outputs that are either chat or text
|
||||
# if the output type is chat or text, we return only the outputs that match the type
|
||||
if input_request.output_component:
|
||||
outputs = [input_request.output_component]
|
||||
else:
|
||||
outputs = [
|
||||
vertex.id
|
||||
for vertex in graph.vertices
|
||||
if input_request.output_type == "debug"
|
||||
or (
|
||||
vertex.is_output
|
||||
and (input_request.output_type == "any" or input_request.output_type in vertex.id.lower())
|
||||
)
|
||||
]
|
||||
task_result, session_id = await run_graph_internal(
|
||||
graph=graph,
|
||||
flow_id=flow_id_str,
|
||||
session_id=input_request.session_id,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
artifacts=artifacts,
|
||||
session_service=session_service,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
return RunResponse(outputs=task_result, session_id=session_id)
|
||||
except sa.exc.StatementError as exc:
|
||||
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
|
||||
if "badly formed hexadecimal UUID string" in str(exc):
|
||||
logger.error(f"Flow ID {flow_id_str} is not a valid UUID")
|
||||
# This means the Flow ID is not a valid UUID which means it can't find the flow
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
if f"Flow {flow_id_str} not found" in str(exc):
|
||||
logger.error(f"Flow {flow_id_str} not found")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
elif f"Session {session_id} not found" in str(exc):
|
||||
logger.error(f"Session {session_id} not found")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
else:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/run/advanced/{flow_id}", response_model=RunResponse, response_model_exclude_none=True)
|
||||
async def experimental_run_flow(
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
flow_id: UUID,
|
||||
inputs: Optional[List[InputValueRequest]] = [InputValueRequest(components=[], input_value="")],
|
||||
outputs: Optional[List[str]] = [],
|
||||
tweaks: Annotated[Optional[Tweaks], Body(embed=True)] = None, # noqa: F821
|
||||
stream: Annotated[bool, Body(embed=True)] = False, # noqa: F821
|
||||
session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
|
||||
api_key_user: User = Depends(api_key_security),
|
||||
session_service: SessionService = Depends(get_session_service),
|
||||
):
|
||||
"""
|
||||
Executes a specified flow by ID with optional input values, output selection, tweaks, and streaming capability.
|
||||
This endpoint supports running flows with caching to enhance performance and efficiency.
|
||||
|
||||
### Parameters:
|
||||
- `flow_id` (str): The unique identifier of the flow to be executed.
|
||||
- `inputs` (List[InputValueRequest], optional): A list of inputs specifying the input values and components for the flow. Each input can target specific components and provide custom values.
|
||||
- `outputs` (List[str], optional): A list of output names to retrieve from the executed flow. If not provided, all outputs are returned.
|
||||
- `tweaks` (Optional[Tweaks], optional): A dictionary of tweaks to customize the flow execution. The tweaks can be used to modify the flow's parameters and components. Tweaks can be overridden by the input values.
|
||||
- `stream` (bool, optional): Specifies whether the results should be streamed. Defaults to False.
|
||||
- `session_id` (Union[None, str], optional): An optional session ID to utilize existing session data for the flow execution.
|
||||
- `api_key_user` (User): The user associated with the current API key. Automatically resolved from the API key.
|
||||
- `session_service` (SessionService): The session service object for managing flow sessions.
|
||||
|
||||
### Returns:
|
||||
A `RunResponse` object containing the selected outputs (or all if not specified) of the executed flow and the session ID. The structure of the response accommodates multiple inputs, providing a nested list of outputs for each input.
|
||||
|
||||
### Raises:
|
||||
HTTPException: Indicates issues with finding the specified flow, invalid input formats, or internal errors during flow execution.
|
||||
|
||||
### Example usage:
|
||||
```json
|
||||
POST /run/{flow_id}
|
||||
x-api-key: YOUR_API_KEY
|
||||
Payload:
|
||||
{
|
||||
"inputs": [
|
||||
{"components": ["component1"], "input_value": "value1"},
|
||||
{"components": ["component3"], "input_value": "value2"}
|
||||
],
|
||||
"outputs": ["Component Name", "component_id"],
|
||||
"tweaks": {"parameter_name": "value", "Component Name": {"parameter_name": "value"}, "component_id": {"parameter_name": "value"}}
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
This endpoint facilitates complex flow executions with customized inputs, outputs, and configurations, catering to diverse application requirements.
|
||||
"""
|
||||
try:
|
||||
flow_id_str = str(flow_id)
|
||||
if outputs is None:
|
||||
outputs = []
|
||||
|
||||
artifacts = {}
|
||||
if session_id:
|
||||
session_data = await session_service.load_session(session_id, flow_id=flow_id_str)
|
||||
graph, artifacts = session_data if session_data else (None, None)
|
||||
if graph is None:
|
||||
raise ValueError(f"Session {session_id} not found")
|
||||
else:
|
||||
# Get the flow that matches the flow_id and belongs to the user
|
||||
# flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first()
|
||||
flow = session.exec(
|
||||
select(Flow).where(Flow.id == flow_id_str).where(Flow.user_id == api_key_user.id)
|
||||
).first()
|
||||
if flow is None:
|
||||
raise ValueError(f"Flow {flow_id_str} not found")
|
||||
|
||||
if flow.data is None:
|
||||
raise ValueError(f"Flow {flow_id_str} has no data")
|
||||
graph_data = flow.data
|
||||
graph_data = process_tweaks(graph_data, tweaks or {})
|
||||
graph = Graph.from_payload(graph_data, flow_id=flow_id_str)
|
||||
task_result, session_id = await run_graph_internal(
|
||||
graph=graph,
|
||||
flow_id=flow_id_str,
|
||||
session_id=session_id,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
artifacts=artifacts,
|
||||
session_service=session_service,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
return RunResponse(outputs=task_result, session_id=session_id)
|
||||
except sa.exc.StatementError as exc:
|
||||
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
|
||||
if "badly formed hexadecimal UUID string" in str(exc):
|
||||
logger.error(f"Flow ID {flow_id_str} is not a valid UUID")
|
||||
# This means the Flow ID is not a valid UUID which means it can't find the flow
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
if f"Flow {flow_id_str} not found" in str(exc):
|
||||
logger.error(f"Flow {flow_id_str} not found")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
elif f"Session {session_id} not found" in str(exc):
|
||||
logger.error(f"Session {session_id} not found")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
else:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/predict/{flow_id}",
|
||||
response_model=ProcessResponse,
|
||||
dependencies=[Depends(api_key_security)],
|
||||
)
|
||||
@router.post(
|
||||
"/process/{flow_id}",
|
||||
response_model=ProcessResponse,
|
||||
)
|
||||
async def process(
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
flow_id: str,
|
||||
inputs: Optional[Union[List[dict], dict]] = None,
|
||||
tweaks: Optional[dict] = None,
|
||||
clear_cache: Annotated[bool, Body(embed=True)] = False, # noqa: F821
|
||||
session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
|
||||
task_service: "TaskService" = Depends(get_task_service),
|
||||
api_key_user: User = Depends(api_key_security),
|
||||
sync: Annotated[bool, Body(embed=True)] = True, # noqa: F821
|
||||
session_service: SessionService = Depends(get_session_service),
|
||||
):
|
||||
"""
|
||||
Endpoint to process an input with a given flow_id.
|
||||
"""
|
||||
# Raise a depreciation warning
|
||||
logger.warning(
|
||||
"The /process endpoint is deprecated and will be removed in a future version. " "Please use /run instead."
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="The /process endpoint is deprecated and will be removed in a future version. "
|
||||
"Please use /run instead.",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/task/{task_id}", response_model=TaskStatusResponse)
|
||||
async def get_task_status(task_id: str):
|
||||
task_service = get_task_service()
|
||||
task = task_service.get_task(task_id)
|
||||
result = None
|
||||
if task is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
if task.ready():
|
||||
result = task.result
|
||||
# If result isinstance of Exception, can we get the traceback?
|
||||
if isinstance(result, Exception):
|
||||
logger.exception(task.traceback)
|
||||
|
||||
if isinstance(result, dict) and "result" in result:
|
||||
result = result["result"]
|
||||
elif hasattr(result, "result"):
|
||||
result = result.result
|
||||
|
||||
if task.status == "FAILURE":
|
||||
result = str(task.result)
|
||||
logger.error(f"Task {task_id} failed: {task.traceback}")
|
||||
|
||||
return TaskStatusResponse(status=task.status, result=result)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/upload/{flow_id}",
|
||||
response_model=UploadFileResponse,
|
||||
status_code=HTTPStatus.CREATED,
|
||||
)
|
||||
async def create_upload_file(
|
||||
file: UploadFile,
|
||||
flow_id: UUID,
|
||||
):
|
||||
try:
|
||||
flow_id_str = str(flow_id)
|
||||
file_path = save_uploaded_file(file, folder_name=flow_id_str)
|
||||
|
||||
return UploadFileResponse(
|
||||
flowId=flow_id_str,
|
||||
file_path=file_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(f"Error saving file: {exc}")
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
# get endpoint to return version of langflow
|
||||
@router.get("/version")
|
||||
def get_version():
|
||||
try:
|
||||
from langflow.version import __version__ # type: ignore
|
||||
|
||||
version = __version__
|
||||
package = "Langflow"
|
||||
except ImportError:
|
||||
from importlib import metadata
|
||||
|
||||
version = metadata.version("langflow-base")
|
||||
package = "Langflow Base"
|
||||
return {"version": version, "package": package}
|
||||
|
||||
|
||||
@router.post("/custom_component", status_code=HTTPStatus.OK)
|
||||
async def custom_component(
|
||||
raw_code: CustomComponentRequest,
|
||||
user: User = Depends(get_current_active_user),
|
||||
):
|
||||
component = CustomComponent(code=raw_code.code)
|
||||
|
||||
built_frontend_node, _ = build_custom_component_template(component, user_id=user.id)
|
||||
|
||||
built_frontend_node = update_frontend_node_with_template_values(built_frontend_node, raw_code.frontend_node)
|
||||
return built_frontend_node
|
||||
|
||||
|
||||
@router.post("/custom_component/update", status_code=HTTPStatus.OK)
|
||||
async def custom_component_update(
|
||||
code_request: UpdateCustomComponentRequest,
|
||||
user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""
|
||||
Update a custom component with the provided code request.
|
||||
|
||||
This endpoint generates the CustomComponentFrontendNode normally but then runs the `update_build_config` method
|
||||
on the latest version of the template. This ensures that every time it runs, it has the latest version of the template.
|
||||
|
||||
Args:
|
||||
code_request (CustomComponentRequest): The code request containing the updated code for the custom component.
|
||||
user (User, optional): The user making the request. Defaults to the current active user.
|
||||
|
||||
Returns:
|
||||
dict: The updated custom component node.
|
||||
|
||||
"""
|
||||
try:
|
||||
component = CustomComponent(code=code_request.code)
|
||||
|
||||
component_node, cc_instance = build_custom_component_template(
|
||||
component,
|
||||
user_id=user.id,
|
||||
)
|
||||
|
||||
updated_build_config = cc_instance.update_build_config(
|
||||
build_config=code_request.get_template(),
|
||||
field_value=code_request.field_value,
|
||||
field_name=code_request.field,
|
||||
)
|
||||
component_node["template"] = updated_build_config
|
||||
|
||||
return component_node
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/config", response_model=ConfigResponse)
|
||||
def get_config():
|
||||
try:
|
||||
from langflow.services.deps import get_settings_service
|
||||
|
||||
settings_service: "SettingsService" = get_settings_service()
|
||||
return settings_service.settings.model_dump()
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
import hashlib
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
|
||||
from langflow.api.v1.schemas import UploadFileResponse
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.flow import Flow
|
||||
|
|
@ -18,38 +21,41 @@ router = APIRouter(tags=["Files"], prefix="/files")
|
|||
# then finds it in the database and returns it while
|
||||
# using the current user as the owner
|
||||
def get_flow_id(
|
||||
flow_id: str,
|
||||
flow_id: UUID,
|
||||
current_user=Depends(get_current_active_user),
|
||||
session=Depends(get_session),
|
||||
):
|
||||
flow_id_str = str(flow_id)
|
||||
# AttributeError: 'SelectOfScalar' object has no attribute 'first'
|
||||
flow = session.get(Flow, flow_id)
|
||||
flow = session.get(Flow, flow_id_str)
|
||||
if not flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
if flow.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="You don't have access to this flow")
|
||||
return flow_id
|
||||
return flow_id_str
|
||||
|
||||
|
||||
@router.post("/upload/{flow_id}", status_code=HTTPStatus.CREATED)
|
||||
async def upload_file(
|
||||
file: UploadFile,
|
||||
flow_id: str = Depends(get_flow_id),
|
||||
flow_id: UUID = Depends(get_flow_id),
|
||||
storage_service: StorageService = Depends(get_storage_service),
|
||||
):
|
||||
try:
|
||||
flow_id_str = str(flow_id)
|
||||
file_content = await file.read()
|
||||
file_name = file.filename or hashlib.sha256(file_content).hexdigest()
|
||||
folder = flow_id
|
||||
folder = flow_id_str
|
||||
await storage_service.save_file(flow_id=folder, file_name=file_name, data=file_content)
|
||||
return UploadFileResponse(flowId=flow_id, file_path=f"{folder}/{file_name}")
|
||||
return UploadFileResponse(flowId=flow_id_str, file_path=f"{folder}/{file_name}")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/download/{flow_id}/{file_name}")
|
||||
async def download_file(file_name: str, flow_id: str, storage_service: StorageService = Depends(get_storage_service)):
|
||||
async def download_file(file_name: str, flow_id: UUID, storage_service: StorageService = Depends(get_storage_service)):
|
||||
try:
|
||||
flow_id_str = str(flow_id)
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
if not extension:
|
||||
|
|
@ -60,7 +66,7 @@ async def download_file(file_name: str, flow_id: str, storage_service: StorageSe
|
|||
if not content_type:
|
||||
raise HTTPException(status_code=500, detail=f"Content type not found for extension {extension}")
|
||||
|
||||
file_content = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
|
||||
file_content = await storage_service.get_file(flow_id=flow_id_str, file_name=file_name)
|
||||
headers = {
|
||||
"Content-Disposition": f"attachment; filename={file_name} filename*=UTF-8''{file_name}",
|
||||
"Content-Type": "application/octet-stream",
|
||||
|
|
@ -72,9 +78,10 @@ async def download_file(file_name: str, flow_id: str, storage_service: StorageSe
|
|||
|
||||
|
||||
@router.get("/images/{flow_id}/{file_name}")
|
||||
async def download_image(file_name: str, flow_id: str, storage_service: StorageService = Depends(get_storage_service)):
|
||||
async def download_image(file_name: str, flow_id: UUID, storage_service: StorageService = Depends(get_storage_service)):
|
||||
try:
|
||||
extension = file_name.split(".")[-1]
|
||||
flow_id_str = str(flow_id)
|
||||
|
||||
if not extension:
|
||||
raise HTTPException(status_code=500, detail=f"Extension not found for file {file_name}")
|
||||
|
|
@ -86,7 +93,7 @@ async def download_image(file_name: str, flow_id: str, storage_service: StorageS
|
|||
elif not content_type.startswith("image"):
|
||||
raise HTTPException(status_code=500, detail=f"Content type {content_type} is not an image")
|
||||
|
||||
file_content = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
|
||||
file_content = await storage_service.get_file(flow_id=flow_id_str, file_name=file_name)
|
||||
return StreamingResponse(BytesIO(file_content), media_type=content_type)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -94,10 +101,11 @@ async def download_image(file_name: str, flow_id: str, storage_service: StorageS
|
|||
|
||||
@router.get("/list/{flow_id}")
|
||||
async def list_files(
|
||||
flow_id: str = Depends(get_flow_id), storage_service: StorageService = Depends(get_storage_service)
|
||||
flow_id: UUID = Depends(get_flow_id), storage_service: StorageService = Depends(get_storage_service)
|
||||
):
|
||||
try:
|
||||
files = await storage_service.list_files(flow_id=flow_id)
|
||||
flow_id_str = str(flow_id)
|
||||
files = await storage_service.list_files(flow_id=flow_id_str)
|
||||
return {"files": files}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -105,10 +113,11 @@ async def list_files(
|
|||
|
||||
@router.delete("/delete/{flow_id}/{file_name}")
|
||||
async def delete_file(
|
||||
file_name: str, flow_id: str = Depends(get_flow_id), storage_service: StorageService = Depends(get_storage_service)
|
||||
file_name: str, flow_id: UUID = Depends(get_flow_id), storage_service: StorageService = Depends(get_storage_service)
|
||||
):
|
||||
try:
|
||||
await storage_service.delete_file(flow_id=flow_id, file_name=file_name)
|
||||
flow_id_str = str(flow_id)
|
||||
await storage_service.delete_file(flow_id=flow_id_str, file_name=file_name)
|
||||
return {"message": f"File {file_name} deleted successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -1,18 +1,23 @@
|
|||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
import orjson
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlmodel import Session, select
|
||||
from loguru import logger
|
||||
from sqlmodel import Session, col, select
|
||||
|
||||
from langflow.api.utils import remove_api_keys, validate_is_component
|
||||
from langflow.api.v1.schemas import FlowListCreate, FlowListRead
|
||||
from langflow.api.v1.schemas import FlowListCreate, FlowListIds, FlowListRead
|
||||
from langflow.initial_setup.setup import STARTER_FOLDER_NAME
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.flow import Flow, FlowCreate, FlowRead, FlowUpdate
|
||||
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
|
||||
from langflow.services.database.models.folder.model import Folder
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
from langflow.services.settings.service import SettingsService
|
||||
|
||||
# build router
|
||||
router = APIRouter(prefix="/flows", tags=["Flows"])
|
||||
|
|
@ -30,7 +35,15 @@ def create_flow(
|
|||
flow.user_id = current_user.id
|
||||
|
||||
db_flow = Flow.model_validate(flow, from_attributes=True)
|
||||
db_flow.updated_at = datetime.utcnow()
|
||||
db_flow.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
if db_flow.folder_id is None:
|
||||
# Make sure flows always have a folder
|
||||
default_folder = session.exec(
|
||||
select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME, Folder.user_id == current_user.id)
|
||||
).first()
|
||||
if default_folder:
|
||||
db_flow.folder_id = default_folder.id
|
||||
|
||||
session.add(db_flow)
|
||||
session.commit()
|
||||
|
|
@ -42,11 +55,33 @@ def create_flow(
|
|||
def read_flows(
|
||||
*,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: Session = Depends(get_session),
|
||||
settings_service: "SettingsService" = Depends(get_settings_service),
|
||||
):
|
||||
"""Read all flows."""
|
||||
try:
|
||||
flows = current_user.flows
|
||||
flows = validate_is_component(flows)
|
||||
auth_settings = settings_service.auth_settings
|
||||
if auth_settings.AUTO_LOGIN:
|
||||
flows = session.exec(
|
||||
select(Flow).where(
|
||||
(Flow.user_id == None) | (Flow.user_id == current_user.id) # noqa
|
||||
)
|
||||
).all()
|
||||
else:
|
||||
flows = current_user.flows
|
||||
|
||||
flows = validate_is_component(flows) # type: ignore
|
||||
flow_ids = [flow.id for flow in flows]
|
||||
# with the session get the flows that DO NOT have a user_id
|
||||
try:
|
||||
folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
|
||||
|
||||
example_flows = folder.flows if folder else []
|
||||
for example_flow in example_flows:
|
||||
if example_flow.id not in flow_ids:
|
||||
flows.append(example_flow) # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
return [jsonable_encoder(flow) for flow in flows]
|
||||
|
|
@ -58,9 +93,18 @@ def read_flow(
|
|||
session: Session = Depends(get_session),
|
||||
flow_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
settings_service: "SettingsService" = Depends(get_settings_service),
|
||||
):
|
||||
"""Read a flow."""
|
||||
if user_flow := (session.exec(select(Flow).where(Flow.id == flow_id, Flow.user_id == current_user.id)).first()):
|
||||
auth_settings = settings_service.auth_settings
|
||||
stmt = select(Flow).where(Flow.id == flow_id)
|
||||
if auth_settings.AUTO_LOGIN:
|
||||
# If auto login is enable user_id can be current_user.id or None
|
||||
# so write an OR
|
||||
stmt = stmt.where(
|
||||
(Flow.user_id == current_user.id) | (Flow.user_id == None) # noqa
|
||||
) # noqa
|
||||
if user_flow := session.exec(stmt).first():
|
||||
return user_flow
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
|
|
@ -77,16 +121,25 @@ def update_flow(
|
|||
):
|
||||
"""Update a flow."""
|
||||
|
||||
db_flow = read_flow(session=session, flow_id=flow_id, current_user=current_user)
|
||||
db_flow = read_flow(
|
||||
session=session,
|
||||
flow_id=flow_id,
|
||||
current_user=current_user,
|
||||
settings_service=settings_service,
|
||||
)
|
||||
if not db_flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
flow_data = flow.model_dump(exclude_unset=True)
|
||||
if settings_service.settings.REMOVE_API_KEYS:
|
||||
if settings_service.settings.remove_api_keys:
|
||||
flow_data = remove_api_keys(flow_data)
|
||||
for key, value in flow_data.items():
|
||||
if value is not None:
|
||||
setattr(db_flow, key, value)
|
||||
db_flow.updated_at = datetime.utcnow()
|
||||
db_flow.updated_at = datetime.now(timezone.utc)
|
||||
if db_flow.folder_id is None:
|
||||
default_folder = session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME)).first()
|
||||
if default_folder:
|
||||
db_flow.folder_id = default_folder.id
|
||||
session.add(db_flow)
|
||||
session.commit()
|
||||
session.refresh(db_flow)
|
||||
|
|
@ -99,9 +152,15 @@ def delete_flow(
|
|||
session: Session = Depends(get_session),
|
||||
flow_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
"""Delete a flow."""
|
||||
flow = read_flow(session=session, flow_id=flow_id, current_user=current_user)
|
||||
flow = read_flow(
|
||||
session=session,
|
||||
flow_id=flow_id,
|
||||
current_user=current_user,
|
||||
settings_service=settings_service,
|
||||
)
|
||||
if not flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
session.delete(flow)
|
||||
|
|
@ -109,9 +168,6 @@ def delete_flow(
|
|||
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(
|
||||
*,
|
||||
|
|
@ -157,8 +213,37 @@ async def upload_file(
|
|||
async def download_file(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
settings_service: "SettingsService" = Depends(get_settings_service),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Download all flows as a file."""
|
||||
flows = read_flows(current_user=current_user)
|
||||
flows = read_flows(current_user=current_user, session=session, settings_service=settings_service)
|
||||
return FlowListRead(flows=flows)
|
||||
|
||||
|
||||
@router.post("/multiple_delete/")
|
||||
async def delete_multiple_flows(
|
||||
flow_ids: FlowListIds, user: User = Depends(get_current_active_user), db: Session = Depends(get_session)
|
||||
):
|
||||
"""
|
||||
Delete multiple flows by their IDs.
|
||||
|
||||
Args:
|
||||
flow_ids (List[str]): The list of flow IDs to delete.
|
||||
user (User, optional): The user making the request. Defaults to the current active user.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the number of flows deleted.
|
||||
|
||||
"""
|
||||
try:
|
||||
deleted_flows = db.exec(
|
||||
select(Flow).where(col(Flow.id).in_(flow_ids.flow_ids)).where(Flow.user_id == user.id)
|
||||
).all()
|
||||
for flow in deleted_flows:
|
||||
db.delete(flow)
|
||||
db.commit()
|
||||
return {"deleted": len(deleted_flows)}
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
239
src/backend/base/langflow/api/v1/folders.py
Normal file
239
src/backend/base/langflow/api/v1/folders.py
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
import orjson
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile, status
|
||||
from sqlalchemy import or_, update
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from langflow.api.v1.flows import create_flows
|
||||
from langflow.api.v1.schemas import FlowListCreate, FlowListReadWithFolderName
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.flow.model import Flow, FlowCreate, FlowRead
|
||||
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
|
||||
from langflow.services.database.models.folder.model import (
|
||||
Folder,
|
||||
FolderCreate,
|
||||
FolderRead,
|
||||
FolderReadWithFlows,
|
||||
FolderUpdate,
|
||||
)
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_session
|
||||
|
||||
router = APIRouter(prefix="/folders", tags=["Folders"])
|
||||
|
||||
|
||||
@router.post("/", response_model=FolderRead, status_code=201)
|
||||
def create_folder(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
folder: FolderCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
try:
|
||||
new_folder = Folder.model_validate(folder, from_attributes=True)
|
||||
new_folder.user_id = current_user.id
|
||||
|
||||
folder_results = session.exec(
|
||||
select(Folder).where(
|
||||
Folder.name.like(f"{new_folder.name}%"), # type: ignore
|
||||
Folder.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
existing_folder_names = [folder.name for folder in folder_results]
|
||||
|
||||
if existing_folder_names:
|
||||
new_folder.name = f"{new_folder.name} ({len(existing_folder_names) + 1})"
|
||||
|
||||
session.add(new_folder)
|
||||
session.commit()
|
||||
session.refresh(new_folder)
|
||||
|
||||
if folder.components_list:
|
||||
update_statement_components = (
|
||||
update(Flow).where(Flow.id.in_(folder.components_list)).values(folder_id=new_folder.id) # type: ignore
|
||||
)
|
||||
session.exec(update_statement_components) # type: ignore
|
||||
session.commit()
|
||||
|
||||
if folder.flows_list:
|
||||
update_statement_flows = update(Flow).where(Flow.id.in_(folder.flows_list)).values(folder_id=new_folder.id) # type: ignore
|
||||
session.exec(update_statement_flows) # type: ignore
|
||||
session.commit()
|
||||
|
||||
return new_folder
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/", response_model=List[FolderRead], status_code=200)
|
||||
def read_folders(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
try:
|
||||
folders = session.exec(
|
||||
select(Folder).where(
|
||||
or_(Folder.user_id == current_user.id, Folder.user_id == None) # type: ignore # noqa: E711
|
||||
)
|
||||
).all()
|
||||
return folders
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{folder_id}", response_model=FolderReadWithFlows, status_code=200)
|
||||
def read_folder(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
folder_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
try:
|
||||
folder = session.exec(select(Folder).where(Folder.id == folder_id, Folder.user_id == current_user.id)).first()
|
||||
if not folder:
|
||||
raise HTTPException(status_code=404, detail="Folder not found")
|
||||
return folder
|
||||
except Exception as e:
|
||||
if "No result found" in str(e):
|
||||
raise HTTPException(status_code=404, detail="Folder not found")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.patch("/{folder_id}", response_model=FolderRead, status_code=200)
|
||||
def update_folder(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
folder_id: UUID,
|
||||
folder: FolderUpdate, # Assuming FolderUpdate is a Pydantic model defining updatable fields
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
try:
|
||||
existing_folder = session.exec(
|
||||
select(Folder).where(Folder.id == folder_id, Folder.user_id == current_user.id)
|
||||
).first()
|
||||
if not existing_folder:
|
||||
raise HTTPException(status_code=404, detail="Folder not found")
|
||||
folder_data = folder.model_dump(exclude_unset=True)
|
||||
for key, value in folder_data.items():
|
||||
if key != "components" and key != "flows":
|
||||
setattr(existing_folder, key, value)
|
||||
session.add(existing_folder)
|
||||
session.commit()
|
||||
session.refresh(existing_folder)
|
||||
|
||||
concat_folder_components = folder.components + folder.flows
|
||||
|
||||
flows_ids = session.exec(select(Flow.id).where(Flow.folder_id == existing_folder.id)).all()
|
||||
|
||||
excluded_flows = list(set(flows_ids) - set(concat_folder_components))
|
||||
|
||||
my_collection_folder = session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME)).first()
|
||||
if my_collection_folder:
|
||||
update_statement_my_collection = (
|
||||
update(Flow).where(Flow.id.in_(excluded_flows)).values(folder_id=my_collection_folder.id) # type: ignore
|
||||
)
|
||||
session.exec(update_statement_my_collection) # type: ignore
|
||||
session.commit()
|
||||
|
||||
if concat_folder_components:
|
||||
update_statement_components = (
|
||||
update(Flow).where(Flow.id.in_(concat_folder_components)).values(folder_id=existing_folder.id) # type: ignore
|
||||
)
|
||||
session.exec(update_statement_components) # type: ignore
|
||||
session.commit()
|
||||
|
||||
return existing_folder
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{folder_id}", status_code=204)
|
||||
def delete_folder(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
folder_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
try:
|
||||
folder = session.exec(select(Folder).where(Folder.id == folder_id, Folder.user_id == current_user.id)).first()
|
||||
if not folder:
|
||||
raise HTTPException(status_code=404, detail="Folder not found")
|
||||
session.delete(folder)
|
||||
session.commit()
|
||||
flows = session.exec(select(Flow).where(Flow.folder_id == folder_id, Folder.user_id == current_user.id)).all()
|
||||
for flow in flows:
|
||||
session.delete(flow)
|
||||
session.commit()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/download/{folder_id}", response_model=FlowListReadWithFolderName, status_code=200)
|
||||
async def download_file(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
folder_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Download all flows from folder."""
|
||||
try:
|
||||
folder = session.exec(select(Folder).where(Folder.id == folder_id, Folder.user_id == current_user.id)).first()
|
||||
return folder
|
||||
except Exception as e:
|
||||
if "No result found" in str(e):
|
||||
raise HTTPException(status_code=404, detail="Folder not found")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/upload/", response_model=List[FlowRead], status_code=201)
|
||||
async def upload_file(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
file: UploadFile = File(...),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Upload flows from a file."""
|
||||
contents = await file.read()
|
||||
data = orjson.loads(contents)
|
||||
|
||||
if not data:
|
||||
raise HTTPException(status_code=400, detail="No flows found in the file")
|
||||
|
||||
folder_results = session.exec(
|
||||
select(Folder).where(
|
||||
Folder.name == data["folder_name"],
|
||||
Folder.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
existing_folder_names = [folder.name for folder in folder_results]
|
||||
|
||||
if existing_folder_names:
|
||||
data["folder_name"] = f"{data['folder_name']} ({len(existing_folder_names) + 1})"
|
||||
|
||||
folder = FolderCreate(name=data["folder_name"], description=data["folder_description"])
|
||||
|
||||
new_folder = Folder.model_validate(folder, from_attributes=True)
|
||||
new_folder.id = None
|
||||
new_folder.user_id = current_user.id
|
||||
session.add(new_folder)
|
||||
session.commit()
|
||||
session.refresh(new_folder)
|
||||
|
||||
del data["folder_name"]
|
||||
del data["folder_description"]
|
||||
|
||||
if "flows" in data:
|
||||
flow_list = FlowListCreate(flows=[FlowCreate(**flow) for flow in data["flows"]])
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="No flows found in the data")
|
||||
# Now we set the user_id for all flows
|
||||
for flow in flow_list.flows:
|
||||
flow.user_id = current_user.id
|
||||
flow.folder_id = new_folder.id
|
||||
|
||||
return create_flows(session=session, flow_list=flow_list, current_user=current_user)
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel import Session
|
||||
|
||||
from langflow.api.v1.schemas import Token
|
||||
from langflow.services.auth.utils import (
|
||||
authenticate_user,
|
||||
|
|
@ -7,8 +9,10 @@ from langflow.services.auth.utils import (
|
|||
create_user_longterm_token,
|
||||
create_user_tokens,
|
||||
)
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
from sqlmodel import Session
|
||||
from langflow.services.database.models.folder.utils import create_default_folder_if_it_doesnt_exist
|
||||
from langflow.services.deps import get_session, get_settings_service, get_variable_service
|
||||
from langflow.services.settings.manager import SettingsService
|
||||
from langflow.services.variable.service import VariableService
|
||||
|
||||
router = APIRouter(tags=["Login"])
|
||||
|
||||
|
|
@ -20,6 +24,7 @@ async def login_to_get_access_token(
|
|||
db: Session = Depends(get_session),
|
||||
# _: Session = Depends(get_current_active_user)
|
||||
settings_service=Depends(get_settings_service),
|
||||
variable_service: VariableService = Depends(get_variable_service),
|
||||
):
|
||||
auth_settings = settings_service.auth_settings
|
||||
try:
|
||||
|
|
@ -40,6 +45,8 @@ async def login_to_get_access_token(
|
|||
httponly=auth_settings.REFRESH_HTTPONLY,
|
||||
samesite=auth_settings.REFRESH_SAME_SITE,
|
||||
secure=auth_settings.REFRESH_SECURE,
|
||||
expires=auth_settings.REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||
domain=auth_settings.COOKIE_DOMAIN,
|
||||
)
|
||||
response.set_cookie(
|
||||
"access_token_lf",
|
||||
|
|
@ -47,7 +54,12 @@ async def login_to_get_access_token(
|
|||
httponly=auth_settings.ACCESS_HTTPONLY,
|
||||
samesite=auth_settings.ACCESS_SAME_SITE,
|
||||
secure=auth_settings.ACCESS_SECURE,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||
domain=auth_settings.COOKIE_DOMAIN,
|
||||
)
|
||||
variable_service.initialize_user_variables(user.id, db)
|
||||
# Create default folder for user if it doesn't exist
|
||||
create_default_folder_if_it_doesnt_exist(db, user.id)
|
||||
return tokens
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -62,17 +74,22 @@ async def auto_login(
|
|||
response: Response,
|
||||
db: Session = Depends(get_session),
|
||||
settings_service=Depends(get_settings_service),
|
||||
variable_service: VariableService = Depends(get_variable_service),
|
||||
):
|
||||
auth_settings = settings_service.auth_settings
|
||||
if settings_service.auth_settings.AUTO_LOGIN:
|
||||
tokens = create_user_longterm_token(db)
|
||||
user_id, tokens = create_user_longterm_token(db)
|
||||
response.set_cookie(
|
||||
"access_token_lf",
|
||||
tokens["access_token"],
|
||||
httponly=auth_settings.ACCESS_HTTPONLY,
|
||||
samesite=auth_settings.ACCESS_SAME_SITE,
|
||||
secure=auth_settings.ACCESS_SECURE,
|
||||
expires=None, # Set to None to make it a session cookie
|
||||
domain=auth_settings.COOKIE_DOMAIN,
|
||||
)
|
||||
variable_service.initialize_user_variables(user_id, db)
|
||||
create_default_folder_if_it_doesnt_exist(db, user_id)
|
||||
return tokens
|
||||
|
||||
raise HTTPException(
|
||||
|
|
@ -85,7 +102,11 @@ async def auto_login(
|
|||
|
||||
|
||||
@router.post("/refresh")
|
||||
async def refresh_token(request: Request, response: Response, settings_service=Depends(get_settings_service)):
|
||||
async def refresh_token(
|
||||
request: Request,
|
||||
response: Response,
|
||||
settings_service: "SettingsService" = Depends(get_settings_service),
|
||||
):
|
||||
auth_settings = settings_service.auth_settings
|
||||
|
||||
token = request.cookies.get("refresh_token_lf")
|
||||
|
|
@ -95,9 +116,11 @@ async def refresh_token(request: Request, response: Response, settings_service=D
|
|||
response.set_cookie(
|
||||
"refresh_token_lf",
|
||||
tokens["refresh_token"],
|
||||
httponly=auth_settings.REFRESH_TOKEN_HTTPONLY,
|
||||
httponly=auth_settings.REFRESH_HTTPONLY,
|
||||
samesite=auth_settings.REFRESH_SAME_SITE,
|
||||
secure=auth_settings.REFRESH_SECURE,
|
||||
expires=auth_settings.REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||
domain=auth_settings.COOKIE_DOMAIN,
|
||||
)
|
||||
response.set_cookie(
|
||||
"access_token_lf",
|
||||
|
|
@ -105,6 +128,8 @@ async def refresh_token(request: Request, response: Response, settings_service=D
|
|||
httponly=auth_settings.ACCESS_HTTPONLY,
|
||||
samesite=auth_settings.ACCESS_SAME_SITE,
|
||||
secure=auth_settings.ACCESS_SECURE,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||
domain=auth_settings.COOKIE_DOMAIN,
|
||||
)
|
||||
return tokens
|
||||
else:
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from langflow.services.deps import get_monitor_service
|
||||
from langflow.services.monitor.schema import VertexBuildMapModel
|
||||
from langflow.services.monitor.schema import (
|
||||
MessageModelResponse,
|
||||
TransactionModelResponse,
|
||||
VertexBuildMapModel,
|
||||
)
|
||||
from langflow.services.monitor.service import MonitorService
|
||||
|
||||
router = APIRouter(prefix="/monitor", tags=["Monitor"])
|
||||
|
|
@ -38,8 +43,9 @@ async def delete_vertex_builds(
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/messages")
|
||||
@router.get("/messages", response_model=List[MessageModelResponse])
|
||||
async def get_messages(
|
||||
flow_id: Optional[str] = Query(None),
|
||||
session_id: Optional[str] = Query(None),
|
||||
sender: Optional[str] = Query(None),
|
||||
sender_name: Optional[str] = Query(None),
|
||||
|
|
@ -47,25 +53,32 @@ async def get_messages(
|
|||
monitor_service: MonitorService = Depends(get_monitor_service),
|
||||
):
|
||||
try:
|
||||
return monitor_service.get_messages(
|
||||
df = monitor_service.get_messages(
|
||||
flow_id=flow_id,
|
||||
sender=sender,
|
||||
sender_name=sender_name,
|
||||
session_id=session_id,
|
||||
order_by=order_by,
|
||||
)
|
||||
dicts = df.to_dict(orient="records")
|
||||
return [MessageModelResponse(**d) for d in dicts]
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/transactions")
|
||||
@router.get("/transactions", response_model=List[TransactionModelResponse])
|
||||
async def get_transactions(
|
||||
source: Optional[str] = Query(None),
|
||||
target: Optional[str] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
order_by: Optional[str] = Query("timestamp"),
|
||||
flow_id: Optional[str] = Query(None),
|
||||
monitor_service: MonitorService = Depends(get_monitor_service),
|
||||
):
|
||||
try:
|
||||
return monitor_service.get_transactions(source=source, target=target, status=status, order_by=order_by)
|
||||
dicts = monitor_service.get_transactions(
|
||||
source=source, target=target, status=status, order_by=order_by, flow_id=flow_id
|
||||
)
|
||||
return [TransactionModelResponse(**d) for d in dicts]
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_serializer
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_serializer
|
||||
|
||||
from langflow.graph.schema import RunOutputs
|
||||
from langflow.schema import dotdict
|
||||
from langflow.schema.graph import Tweaks
|
||||
from langflow.schema.schema import InputType, OutputType
|
||||
from langflow.services.database.models.api_key.model import ApiKeyRead
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
from langflow.services.database.models.flow import FlowCreate, FlowRead
|
||||
|
|
@ -22,7 +26,7 @@ class BuildStatus(Enum):
|
|||
|
||||
|
||||
class TweaksRequest(BaseModel):
|
||||
tweaks: Optional[Dict[str, Dict[str, str]]] = Field(default_factory=dict)
|
||||
tweaks: Optional[Dict[str, Dict[str, Any]]] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class UpdateTemplateRequest(BaseModel):
|
||||
|
|
@ -49,21 +53,22 @@ class ProcessResponse(BaseModel):
|
|||
class RunResponse(BaseModel):
|
||||
"""Run response schema."""
|
||||
|
||||
outputs: Optional[List[Any]] = None
|
||||
outputs: Optional[List[RunOutputs]] = []
|
||||
session_id: Optional[str] = None
|
||||
|
||||
@model_serializer(mode="wrap")
|
||||
def serialize(self, handler):
|
||||
@model_serializer(mode="plain")
|
||||
def serialize(self):
|
||||
# Serialize all the outputs if they are base models
|
||||
serialized = {"session_id": self.session_id, "outputs": []}
|
||||
if self.outputs:
|
||||
serialized_outputs = []
|
||||
for output in self.outputs:
|
||||
if isinstance(output, BaseModel):
|
||||
if isinstance(output, BaseModel) and not isinstance(output, RunOutputs):
|
||||
serialized_outputs.append(output.model_dump(exclude_none=True))
|
||||
else:
|
||||
serialized_outputs.append(output)
|
||||
self.outputs = serialized_outputs
|
||||
return handler(self)
|
||||
serialized["outputs"] = serialized_outputs
|
||||
return serialized
|
||||
|
||||
|
||||
class PreloadResponse(BaseModel):
|
||||
|
|
@ -134,10 +139,20 @@ class FlowListCreate(BaseModel):
|
|||
flows: List[FlowCreate]
|
||||
|
||||
|
||||
class FlowListIds(BaseModel):
|
||||
flow_ids: List[str]
|
||||
|
||||
|
||||
class FlowListRead(BaseModel):
|
||||
flows: List[FlowRead]
|
||||
|
||||
|
||||
class FlowListReadWithFolderName(BaseModel):
|
||||
flows: List[FlowRead]
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
class InitResponse(BaseModel):
|
||||
flowId: str
|
||||
|
||||
|
|
@ -161,12 +176,21 @@ class StreamData(BaseModel):
|
|||
return f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n"
|
||||
|
||||
|
||||
class CustomComponentCode(BaseModel):
|
||||
class CustomComponentRequest(BaseModel):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
code: str
|
||||
field: Optional[str] = None
|
||||
frontend_node: Optional[dict] = None
|
||||
|
||||
|
||||
class UpdateCustomComponentRequest(CustomComponentRequest):
|
||||
field: str
|
||||
field_value: Optional[Union[str, int, float, bool, dict, list]] = None
|
||||
template: dict
|
||||
|
||||
def get_template(self):
|
||||
return dotdict(self.template)
|
||||
|
||||
|
||||
class CustomComponentResponseError(BaseModel):
|
||||
detail: str
|
||||
traceback: str
|
||||
|
|
@ -214,8 +238,9 @@ class ApiKeyCreateRequest(BaseModel):
|
|||
|
||||
|
||||
class VerticesOrderResponse(BaseModel):
|
||||
ids: List[List[str]]
|
||||
ids: List[str]
|
||||
run_id: UUID
|
||||
vertices_to_run: List[str]
|
||||
|
||||
|
||||
class ResultDataResponse(BaseModel):
|
||||
|
|
@ -223,17 +248,20 @@ class ResultDataResponse(BaseModel):
|
|||
artifacts: Optional[Any] = Field(default_factory=dict)
|
||||
timedelta: Optional[float] = None
|
||||
duration: Optional[str] = None
|
||||
used_frozen_result: Optional[bool] = False
|
||||
|
||||
|
||||
class VertexBuildResponse(BaseModel):
|
||||
id: Optional[str] = None
|
||||
inactive_vertices: Optional[List[str]] = None
|
||||
inactivated_vertices: Optional[List[str]] = None
|
||||
next_vertices_ids: Optional[List[str]] = None
|
||||
top_level_vertices: Optional[List[str]] = None
|
||||
valid: bool
|
||||
params: Optional[str]
|
||||
params: Optional[Any] = Field(default_factory=dict)
|
||||
"""JSON string of the params."""
|
||||
data: ResultDataResponse
|
||||
"""Mapping of vertex ids to result dict containing the param name and result value."""
|
||||
timestamp: Optional[datetime] = Field(default_factory=datetime.utcnow)
|
||||
timestamp: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
"""Timestamp of the build."""
|
||||
|
||||
|
||||
|
|
@ -242,4 +270,54 @@ class VerticesBuiltResponse(BaseModel):
|
|||
|
||||
|
||||
class InputValueRequest(BaseModel):
|
||||
input_value: str
|
||||
components: Optional[List[str]] = []
|
||||
input_value: Optional[str] = None
|
||||
type: Optional[InputType] = Field(
|
||||
"any",
|
||||
description="Defines on which components the input value should be applied. 'any' applies to all input components.",
|
||||
)
|
||||
|
||||
# add an example
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"examples": [
|
||||
{
|
||||
"components": ["components_id", "Component Name"],
|
||||
"input_value": "input_value",
|
||||
},
|
||||
{"components": ["Component Name"], "input_value": "input_value"},
|
||||
{"input_value": "input_value"},
|
||||
{"type": "chat", "input_value": "input_value"},
|
||||
{"type": "json", "input_value": '{"key": "value"}'},
|
||||
]
|
||||
},
|
||||
extra="forbid",
|
||||
)
|
||||
|
||||
|
||||
class SimplifiedAPIRequest(BaseModel):
|
||||
input_value: Optional[str] = Field(default="", description="The input value")
|
||||
input_type: Optional[InputType] = Field(default="chat", description="The input type")
|
||||
output_type: Optional[OutputType] = Field(default="chat", description="The output type")
|
||||
output_component: Optional[str] = Field(
|
||||
default="",
|
||||
description="If there are multiple output components, you can specify the component to get the output from.",
|
||||
)
|
||||
tweaks: Optional[Tweaks] = Field(default=None, description="The tweaks")
|
||||
session_id: Optional[str] = Field(default=None, description="The session id")
|
||||
|
||||
|
||||
# (alias) type ReactFlowJsonObject<NodeData = any, EdgeData = any> = {
|
||||
# nodes: Node<NodeData>[];
|
||||
# edges: Edge<EdgeData>[];
|
||||
# viewport: Viewport;
|
||||
# }
|
||||
# import ReactFlowJsonObject
|
||||
class FlowDataRequest(BaseModel):
|
||||
nodes: List[dict]
|
||||
edges: List[dict]
|
||||
viewport: Optional[dict] = None
|
||||
|
||||
|
||||
class ConfigResponse(BaseModel):
|
||||
frontend_timeout: int
|
||||
|
|
@ -2,6 +2,7 @@ from typing import Annotated, List, Optional, Union
|
|||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from loguru import logger
|
||||
|
||||
from langflow.api.utils import check_langflow_version
|
||||
from langflow.services.auth import utils as auth_utils
|
||||
|
|
@ -27,8 +28,11 @@ def get_user_store_api_key(
|
|||
):
|
||||
if not user.store_api_key:
|
||||
raise HTTPException(status_code=400, detail="You must have a store API key set.")
|
||||
decrypted = auth_utils.decrypt_api_key(user.store_api_key, settings_service)
|
||||
return decrypted
|
||||
try:
|
||||
decrypted = auth_utils.decrypt_api_key(user.store_api_key, settings_service)
|
||||
return decrypted
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail="Failed to decrypt API key. Please set a new one.") from e
|
||||
|
||||
|
||||
def get_optional_user_store_api_key(
|
||||
|
|
@ -37,8 +41,12 @@ def get_optional_user_store_api_key(
|
|||
):
|
||||
if not user.store_api_key:
|
||||
return None
|
||||
decrypted = auth_utils.decrypt_api_key(user.store_api_key, settings_service)
|
||||
return decrypted
|
||||
try:
|
||||
decrypted = auth_utils.decrypt_api_key(user.store_api_key, settings_service)
|
||||
return decrypted
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decrypt API key: {e}")
|
||||
return user.store_api_key
|
||||
|
||||
|
||||
@router.get("/check/")
|
||||
|
|
@ -46,7 +54,7 @@ def check_if_store_is_enabled(
|
|||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
return {
|
||||
"enabled": settings_service.settings.STORE,
|
||||
"enabled": settings_service.settings.store,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -13,6 +13,7 @@ from langflow.services.auth.utils import (
|
|||
get_password_hash,
|
||||
verify_password,
|
||||
)
|
||||
from langflow.services.database.models.folder.utils import create_default_folder_if_it_doesnt_exist
|
||||
from langflow.services.database.models.user import User, UserCreate, UserRead, UserUpdate
|
||||
from langflow.services.database.models.user.crud import get_user_by_id, update_user
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
|
|
@ -36,6 +37,9 @@ def add_user(
|
|||
session.add(new_user)
|
||||
session.commit()
|
||||
session.refresh(new_user)
|
||||
folder = create_default_folder_if_it_doesnt_exist(session, new_user.id)
|
||||
if not folder:
|
||||
raise HTTPException(status_code=500, detail="Error creating default folder")
|
||||
except IntegrityError as e:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=400, detail="This username is unavailable.") from e
|
||||
81
src/backend/base/langflow/api/v1/validate.py
Normal file
81
src/backend/base/langflow/api/v1/validate.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
from collections import defaultdict
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from langflow.api.v1.base import Code, CodeValidationResponse, PromptValidationResponse, ValidatePromptRequest
|
||||
from langflow.base.prompts.api_utils import (
|
||||
add_new_variables_to_template,
|
||||
get_old_custom_fields,
|
||||
remove_old_variables_from_template,
|
||||
update_input_variables_field,
|
||||
validate_prompt,
|
||||
)
|
||||
from langflow.utils.validate import validate_code
|
||||
|
||||
# build router
|
||||
router = APIRouter(prefix="/validate", tags=["Validate"])
|
||||
|
||||
|
||||
@router.post("/code", status_code=200, response_model=CodeValidationResponse)
|
||||
def post_validate_code(code: Code):
|
||||
try:
|
||||
errors = validate_code(code.code)
|
||||
return CodeValidationResponse(
|
||||
imports=errors.get("imports", {}),
|
||||
function=errors.get("function", {}),
|
||||
)
|
||||
except Exception as e:
|
||||
return HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/prompt", status_code=200, response_model=PromptValidationResponse)
|
||||
def post_validate_prompt(prompt_request: ValidatePromptRequest):
|
||||
try:
|
||||
input_variables = validate_prompt(prompt_request.template)
|
||||
# Check if frontend_node is None before proceeding to avoid attempting to update a non-existent node.
|
||||
if prompt_request.frontend_node is None:
|
||||
return PromptValidationResponse(
|
||||
input_variables=input_variables,
|
||||
frontend_node=None,
|
||||
)
|
||||
if not prompt_request.frontend_node.custom_fields:
|
||||
prompt_request.frontend_node.custom_fields = defaultdict(list)
|
||||
old_custom_fields = get_old_custom_fields(prompt_request.frontend_node.custom_fields, prompt_request.name)
|
||||
|
||||
add_new_variables_to_template(
|
||||
input_variables,
|
||||
prompt_request.frontend_node.custom_fields,
|
||||
prompt_request.frontend_node.template,
|
||||
prompt_request.name,
|
||||
)
|
||||
|
||||
remove_old_variables_from_template(
|
||||
old_custom_fields,
|
||||
input_variables,
|
||||
prompt_request.frontend_node.custom_fields,
|
||||
prompt_request.frontend_node.template,
|
||||
prompt_request.name,
|
||||
)
|
||||
|
||||
update_input_variables_field(input_variables, prompt_request.frontend_node.template)
|
||||
|
||||
# If frontend_node.template contains only one field that is type == 'prompt', then we can remove all fields that are not
|
||||
# 'code', and not in the input_variables list.
|
||||
prompt_fields = [
|
||||
key
|
||||
for key, field in prompt_request.frontend_node.template.items()
|
||||
if isinstance(field, dict) and field["type"] == "prompt"
|
||||
]
|
||||
|
||||
if len(prompt_fields) == 1:
|
||||
for key, field in prompt_request.frontend_node.template.copy().items():
|
||||
if isinstance(field, dict) and field["type"] != "code" and key not in input_variables + prompt_fields:
|
||||
del prompt_request.frontend_node.template[key]
|
||||
return PromptValidationResponse(
|
||||
input_variables=input_variables,
|
||||
frontend_node=prompt_request.frontend_node,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
117
src/backend/base/langflow/api/v1/variable.py
Normal file
117
src/backend/base/langflow/api/v1/variable.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
from datetime import datetime, timezone
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from langflow.services.auth import utils as auth_utils
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.database.models.variable import Variable, VariableCreate, VariableRead, VariableUpdate
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
|
||||
router = APIRouter(prefix="/variables", tags=["Variables"])
|
||||
|
||||
|
||||
@router.post("/", response_model=VariableRead, status_code=201)
|
||||
def create_variable(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
variable: VariableCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
"""Create a new variable."""
|
||||
try:
|
||||
# check if variable name already exists
|
||||
variable_exists = session.exec(
|
||||
select(Variable).where(
|
||||
Variable.name == variable.name,
|
||||
Variable.user_id == current_user.id,
|
||||
)
|
||||
).first()
|
||||
if variable_exists:
|
||||
raise HTTPException(status_code=400, detail="Variable name already exists")
|
||||
|
||||
variable_dict = variable.model_dump()
|
||||
variable_dict["user_id"] = current_user.id
|
||||
|
||||
db_variable = Variable.model_validate(variable_dict)
|
||||
if not db_variable.name and not db_variable.value:
|
||||
raise HTTPException(status_code=400, detail="Variable name and value cannot be empty")
|
||||
elif not db_variable.name:
|
||||
raise HTTPException(status_code=400, detail="Variable name cannot be empty")
|
||||
elif not db_variable.value:
|
||||
raise HTTPException(status_code=400, detail="Variable value cannot be empty")
|
||||
encrypted = auth_utils.encrypt_api_key(db_variable.value, settings_service=settings_service)
|
||||
db_variable.value = encrypted
|
||||
db_variable.user_id = current_user.id
|
||||
session.add(db_variable)
|
||||
session.commit()
|
||||
session.refresh(db_variable)
|
||||
return db_variable
|
||||
except Exception as e:
|
||||
if isinstance(e, HTTPException):
|
||||
raise e
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.get("/", response_model=list[VariableRead], status_code=200)
|
||||
def read_variables(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Read all variables."""
|
||||
try:
|
||||
variables = session.exec(select(Variable).where(Variable.user_id == current_user.id)).all()
|
||||
return variables
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.patch("/{variable_id}", response_model=VariableRead, status_code=200)
|
||||
def update_variable(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
variable_id: UUID,
|
||||
variable: VariableUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Update a variable."""
|
||||
try:
|
||||
db_variable = session.exec(
|
||||
select(Variable).where(Variable.id == variable_id, Variable.user_id == current_user.id)
|
||||
).first()
|
||||
if not db_variable:
|
||||
raise HTTPException(status_code=404, detail="Variable not found")
|
||||
|
||||
variable_data = variable.model_dump(exclude_unset=True)
|
||||
for key, value in variable_data.items():
|
||||
setattr(db_variable, key, value)
|
||||
db_variable.updated_at = datetime.now(timezone.utc)
|
||||
session.commit()
|
||||
session.refresh(db_variable)
|
||||
return db_variable
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.delete("/{variable_id}", status_code=204)
|
||||
def delete_variable(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
variable_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Delete a variable."""
|
||||
try:
|
||||
db_variable = session.exec(
|
||||
select(Variable).where(Variable.id == variable_id, Variable.user_id == current_user.id)
|
||||
).first()
|
||||
if not db_variable:
|
||||
raise HTTPException(status_code=404, detail="Variable not found")
|
||||
session.delete(db_variable)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
78
src/backend/base/langflow/base/agents/agent.py
Normal file
78
src/backend/base/langflow/base/agents/agent.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
from typing import List, Optional, Union, cast
|
||||
|
||||
from langchain.agents import AgentExecutor, BaseMultiActionAgent, BaseSingleActionAgent
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_core.runnables import Runnable
|
||||
|
||||
from langflow.base.agents.utils import get_agents_list, records_to_messages
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import Text, Tool
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
|
||||
class LCAgentComponent(CustomComponent):
|
||||
def get_agents_list(self):
|
||||
return get_agents_list()
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"lc": {
|
||||
"display_name": "LangChain",
|
||||
"info": "The LangChain to interact with.",
|
||||
},
|
||||
"handle_parsing_errors": {
|
||||
"display_name": "Handle Parsing Errors",
|
||||
"info": "If True, the agent will handle parsing errors. If False, the agent will raise an error.",
|
||||
"advanced": True,
|
||||
},
|
||||
"output_key": {
|
||||
"display_name": "Output Key",
|
||||
"info": "The key to use to get the output from the agent.",
|
||||
"advanced": True,
|
||||
},
|
||||
"memory": {
|
||||
"display_name": "Memory",
|
||||
"info": "Memory to use for the agent.",
|
||||
},
|
||||
"tools": {
|
||||
"display_name": "Tools",
|
||||
"info": "Tools the agent can use.",
|
||||
},
|
||||
"input_value": {
|
||||
"display_name": "Input",
|
||||
"info": "Input text to pass to the agent.",
|
||||
},
|
||||
}
|
||||
|
||||
async def run_agent(
|
||||
self,
|
||||
agent: Union[Runnable, BaseSingleActionAgent, BaseMultiActionAgent, AgentExecutor],
|
||||
inputs: str,
|
||||
tools: List[Tool],
|
||||
message_history: Optional[List[Record]] = None,
|
||||
handle_parsing_errors: bool = True,
|
||||
output_key: str = "output",
|
||||
) -> Text:
|
||||
if isinstance(agent, AgentExecutor):
|
||||
runnable = agent
|
||||
else:
|
||||
runnable = AgentExecutor.from_agent_and_tools(
|
||||
agent=agent, # type: ignore
|
||||
tools=tools,
|
||||
verbose=True,
|
||||
handle_parsing_errors=handle_parsing_errors,
|
||||
)
|
||||
input_dict: dict[str, str | list[BaseMessage]] = {"input": inputs}
|
||||
if message_history:
|
||||
input_dict["chat_history"] = records_to_messages(message_history)
|
||||
result = await runnable.ainvoke(input_dict)
|
||||
self.status = result
|
||||
if output_key in result:
|
||||
return cast(str, result.get(output_key))
|
||||
elif "output" not in result:
|
||||
if output_key != "output":
|
||||
raise ValueError(f"Output key not found in result. Tried '{output_key}' and 'output'.")
|
||||
else:
|
||||
raise ValueError("Output key not found in result. Tried 'output'.")
|
||||
|
||||
return cast(str, result.get("output"))
|
||||
23
src/backend/base/langflow/base/agents/default_prompts.py
Normal file
23
src/backend/base/langflow/base/agents/default_prompts.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
XML_AGENT_PROMPT = """You are a helpful assistant. Help the user answer any questions.
|
||||
|
||||
You have access to the following tools:
|
||||
|
||||
{tools}
|
||||
|
||||
In order to use a tool, you can use <tool></tool> and <tool_input></tool_input> tags. You will then get back a response in the form <observation></observation>
|
||||
For example, if you have a tool called 'search' that could run a google search, in order to search for the weather in SF you would respond:
|
||||
|
||||
<tool>search</tool><tool_input>weather in SF</tool_input>
|
||||
<observation>64 degrees</observation>
|
||||
|
||||
When you are done, respond with a final answer between <final_answer></final_answer>. For example:
|
||||
|
||||
<final_answer>The weather in SF is 64 degrees</final_answer>
|
||||
|
||||
Begin!
|
||||
|
||||
Previous Conversation:
|
||||
{chat_history}
|
||||
|
||||
Question: {input}
|
||||
{agent_scratchpad}"""
|
||||
143
src/backend/base/langflow/base/agents/utils.py
Normal file
143
src/backend/base/langflow/base/agents/utils.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
|
||||
|
||||
from langchain.agents import (
|
||||
create_json_chat_agent,
|
||||
create_openai_tools_agent,
|
||||
create_tool_calling_agent,
|
||||
create_xml_agent,
|
||||
)
|
||||
from langchain.agents.xml.base import render_text_description
|
||||
from langchain_core.language_models import BaseLanguageModel
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_core.prompts import BasePromptTemplate, ChatPromptTemplate
|
||||
from langchain_core.tools import BaseTool
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
from .default_prompts import XML_AGENT_PROMPT
|
||||
|
||||
|
||||
class AgentSpec(BaseModel):
|
||||
func: Callable[
|
||||
[
|
||||
BaseLanguageModel,
|
||||
Sequence[BaseTool],
|
||||
BasePromptTemplate | ChatPromptTemplate,
|
||||
Optional[Callable[[List[BaseTool]], str]],
|
||||
Optional[Union[bool, List[str]]],
|
||||
],
|
||||
Any,
|
||||
]
|
||||
prompt: Optional[Any] = None
|
||||
fields: List[str]
|
||||
hub_repo: Optional[str] = None
|
||||
|
||||
|
||||
def records_to_messages(records: List[Record]) -> List[BaseMessage]:
|
||||
"""
|
||||
Convert a list of records to a list of messages.
|
||||
|
||||
Args:
|
||||
records (List[Record]): The records to convert.
|
||||
|
||||
Returns:
|
||||
List[Message]: The records as messages.
|
||||
"""
|
||||
return [record.to_lc_message() for record in records]
|
||||
|
||||
|
||||
def validate_and_create_xml_agent(
|
||||
llm: BaseLanguageModel,
|
||||
tools: Sequence[BaseTool],
|
||||
prompt: BasePromptTemplate,
|
||||
tools_renderer: Callable[[List[BaseTool]], str] = render_text_description,
|
||||
*,
|
||||
stop_sequence: Union[bool, List[str]] = True,
|
||||
):
|
||||
return create_xml_agent(
|
||||
llm=llm,
|
||||
tools=tools,
|
||||
prompt=prompt,
|
||||
tools_renderer=tools_renderer,
|
||||
stop_sequence=stop_sequence,
|
||||
)
|
||||
|
||||
|
||||
def validate_and_create_openai_tools_agent(
|
||||
llm: BaseLanguageModel,
|
||||
tools: Sequence[BaseTool],
|
||||
prompt: ChatPromptTemplate,
|
||||
tools_renderer: Callable[[List[BaseTool]], str] = render_text_description,
|
||||
*,
|
||||
stop_sequence: Union[bool, List[str]] = True,
|
||||
):
|
||||
return create_openai_tools_agent(
|
||||
llm=llm,
|
||||
tools=tools,
|
||||
prompt=prompt,
|
||||
)
|
||||
|
||||
|
||||
def validate_and_create_tool_calling_agent(
|
||||
llm: BaseLanguageModel,
|
||||
tools: Sequence[BaseTool],
|
||||
prompt: ChatPromptTemplate,
|
||||
tools_renderer: Callable[[List[BaseTool]], str] = render_text_description,
|
||||
*,
|
||||
stop_sequence: Union[bool, List[str]] = True,
|
||||
):
|
||||
return create_tool_calling_agent(
|
||||
llm=llm,
|
||||
tools=tools,
|
||||
prompt=prompt,
|
||||
)
|
||||
|
||||
|
||||
def validate_and_create_json_chat_agent(
|
||||
llm: BaseLanguageModel,
|
||||
tools: Sequence[BaseTool],
|
||||
prompt: ChatPromptTemplate,
|
||||
tools_renderer: Callable[[List[BaseTool]], str] = render_text_description,
|
||||
*,
|
||||
stop_sequence: Union[bool, List[str]] = True,
|
||||
):
|
||||
return create_json_chat_agent(
|
||||
llm=llm,
|
||||
tools=tools,
|
||||
prompt=prompt,
|
||||
tools_renderer=tools_renderer,
|
||||
stop_sequence=stop_sequence,
|
||||
)
|
||||
|
||||
|
||||
AGENTS: Dict[str, AgentSpec] = {
|
||||
"Tool Calling Agent": AgentSpec(
|
||||
func=validate_and_create_tool_calling_agent,
|
||||
prompt=None,
|
||||
fields=["llm", "tools", "prompt"],
|
||||
hub_repo=None,
|
||||
),
|
||||
"XML Agent": AgentSpec(
|
||||
func=validate_and_create_xml_agent,
|
||||
prompt=XML_AGENT_PROMPT, # Ensure XML_AGENT_PROMPT is properly defined and typed.
|
||||
fields=["llm", "tools", "prompt", "tools_renderer", "stop_sequence"],
|
||||
hub_repo="hwchase17/xml-agent-convo",
|
||||
),
|
||||
"OpenAI Tools Agent": AgentSpec(
|
||||
func=validate_and_create_openai_tools_agent,
|
||||
prompt=None,
|
||||
fields=["llm", "tools", "prompt"],
|
||||
hub_repo=None,
|
||||
),
|
||||
"JSON Chat Agent": AgentSpec(
|
||||
func=validate_and_create_json_chat_agent,
|
||||
prompt=None,
|
||||
fields=["llm", "tools", "prompt", "tools_renderer", "stop_sequence"],
|
||||
hub_repo="hwchase17/react-chat-json",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_agents_list():
|
||||
return list(AGENTS.keys())
|
||||
29
src/backend/base/langflow/base/constants.py
Normal file
29
src/backend/base/langflow/base/constants.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""
|
||||
This module contains constants used in the Langflow base module.
|
||||
|
||||
Constants:
|
||||
- STREAM_INFO_TEXT: A string representing the information about streaming the response from the model.
|
||||
- NODE_FORMAT_ATTRIBUTES: A list of attributes used for formatting nodes.
|
||||
- FIELD_FORMAT_ATTRIBUTES: A list of attributes used for formatting fields.
|
||||
"""
|
||||
|
||||
STREAM_INFO_TEXT = "Stream the response from the model. Streaming works only in Chat."
|
||||
|
||||
NODE_FORMAT_ATTRIBUTES = ["beta", "icon", "display_name", "description"]
|
||||
|
||||
|
||||
FIELD_FORMAT_ATTRIBUTES = [
|
||||
"info",
|
||||
"display_name",
|
||||
"required",
|
||||
"list",
|
||||
"multiline",
|
||||
"fileTypes",
|
||||
"password",
|
||||
"input_types",
|
||||
"title_case",
|
||||
"real_time_refresh",
|
||||
"refresh_button",
|
||||
"refresh_button_text",
|
||||
"options",
|
||||
]
|
||||
164
src/backend/base/langflow/base/data/utils.py
Normal file
164
src/backend/base/langflow/base/data/utils.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
from concurrent import futures
|
||||
from pathlib import Path
|
||||
from typing import Callable, List, Optional, Text
|
||||
|
||||
import yaml
|
||||
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
# Types of files that can be read simply by file.read()
|
||||
# and have 100% to be completely readable
|
||||
TEXT_FILE_TYPES = [
|
||||
"txt",
|
||||
"md",
|
||||
"mdx",
|
||||
"csv",
|
||||
"json",
|
||||
"yaml",
|
||||
"yml",
|
||||
"xml",
|
||||
"html",
|
||||
"htm",
|
||||
"pdf",
|
||||
"docx",
|
||||
"py",
|
||||
"sh",
|
||||
"sql",
|
||||
"js",
|
||||
"ts",
|
||||
"tsx",
|
||||
]
|
||||
|
||||
|
||||
def is_hidden(path: Path) -> bool:
|
||||
return path.name.startswith(".")
|
||||
|
||||
|
||||
def retrieve_file_paths(
|
||||
path: str,
|
||||
load_hidden: bool,
|
||||
recursive: bool,
|
||||
depth: int,
|
||||
types: List[str] = TEXT_FILE_TYPES,
|
||||
) -> List[str]:
|
||||
path_obj = Path(path)
|
||||
if not path_obj.exists() or not path_obj.is_dir():
|
||||
raise ValueError(f"Path {path} must exist and be a directory.")
|
||||
|
||||
def match_types(p: Path) -> bool:
|
||||
return any(p.suffix == f".{t}" for t in types) if types else True
|
||||
|
||||
def is_not_hidden(p: Path) -> bool:
|
||||
return not is_hidden(p) or load_hidden
|
||||
|
||||
def walk_level(directory: Path, max_depth: int):
|
||||
directory = directory.resolve()
|
||||
prefix_length = len(directory.parts)
|
||||
for p in directory.rglob("*" if recursive else "[!.]*"):
|
||||
if len(p.parts) - prefix_length <= max_depth:
|
||||
yield p
|
||||
|
||||
glob = "**/*" if recursive else "*"
|
||||
paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob)
|
||||
file_paths = [Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)]
|
||||
|
||||
return file_paths
|
||||
|
||||
|
||||
# ! Removing unstructured dependency until
|
||||
# ! 3.12 is supported
|
||||
# def partition_file_to_record(file_path: str, silent_errors: bool) -> Optional[Record]:
|
||||
# # Use the partition function to load the file
|
||||
# from unstructured.partition.auto import partition # type: ignore
|
||||
|
||||
# try:
|
||||
# elements = partition(file_path)
|
||||
# except Exception as e:
|
||||
# if not silent_errors:
|
||||
# raise ValueError(f"Error loading file {file_path}: {e}") from e
|
||||
# return None
|
||||
|
||||
# # Create a Record
|
||||
# text = "\n\n".join([Text(el) for el in elements])
|
||||
# metadata = elements.metadata if hasattr(elements, "metadata") else {}
|
||||
# metadata["file_path"] = file_path
|
||||
# record = Record(text=text, data=metadata)
|
||||
# return record
|
||||
|
||||
|
||||
def read_text_file(file_path: str) -> str:
|
||||
with open(file_path, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def read_docx_file(file_path: str) -> str:
|
||||
from docx import Document # type: ignore
|
||||
|
||||
doc = Document(file_path)
|
||||
return "\n\n".join([p.text for p in doc.paragraphs])
|
||||
|
||||
|
||||
def parse_pdf_to_text(file_path: str) -> str:
|
||||
from pypdf import PdfReader # type: ignore
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
reader = PdfReader(f)
|
||||
return "\n\n".join([page.extract_text() for page in reader.pages])
|
||||
|
||||
|
||||
def parse_text_file_to_record(file_path: str, silent_errors: bool) -> Optional[Record]:
|
||||
try:
|
||||
if file_path.endswith(".pdf"):
|
||||
text = parse_pdf_to_text(file_path)
|
||||
elif file_path.endswith(".docx"):
|
||||
text = read_docx_file(file_path)
|
||||
else:
|
||||
text = read_text_file(file_path)
|
||||
# if file is json, yaml, or xml, we can parse it
|
||||
if file_path.endswith(".json"):
|
||||
text = json.loads(text)
|
||||
elif file_path.endswith(".yaml") or file_path.endswith(".yml"):
|
||||
text = yaml.safe_load(text)
|
||||
elif file_path.endswith(".xml"):
|
||||
xml_element = ET.fromstring(text)
|
||||
text = ET.tostring(xml_element, encoding="unicode")
|
||||
except Exception as e:
|
||||
if not silent_errors:
|
||||
raise ValueError(f"Error loading file {file_path}: {e}") from e
|
||||
return None
|
||||
|
||||
record = Record(data={"file_path": file_path, "text": text})
|
||||
return record
|
||||
|
||||
|
||||
# ! Removing unstructured dependency until
|
||||
# ! 3.12 is supported
|
||||
# def get_elements(
|
||||
# file_paths: List[str],
|
||||
# silent_errors: bool,
|
||||
# max_concurrency: int,
|
||||
# use_multithreading: bool,
|
||||
# ) -> List[Optional[Record]]:
|
||||
# if use_multithreading:
|
||||
# records = parallel_load_records(file_paths, silent_errors, max_concurrency)
|
||||
# else:
|
||||
# records = [partition_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
||||
# records = list(filter(None, records))
|
||||
# return records
|
||||
|
||||
|
||||
def parallel_load_records(
|
||||
file_paths: List[str],
|
||||
silent_errors: bool,
|
||||
max_concurrency: int,
|
||||
load_function: Callable = parse_text_file_to_record,
|
||||
) -> List[Optional[Record]]:
|
||||
with futures.ThreadPoolExecutor(max_workers=max_concurrency) as executor:
|
||||
loaded_files = executor.map(
|
||||
lambda file_path: load_function(file_path, silent_errors),
|
||||
file_paths,
|
||||
)
|
||||
# loaded_files is an iterator, so we need to convert it to a list
|
||||
return list(loaded_files)
|
||||
67
src/backend/base/langflow/base/flow_processing/utils.py
Normal file
67
src/backend/base/langflow/base/flow_processing/utils.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
from typing import List
|
||||
|
||||
from langflow.graph.schema import ResultData, RunOutputs
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
|
||||
def build_records_from_run_outputs(run_outputs: RunOutputs) -> List[Record]:
|
||||
"""
|
||||
Build a list of records from the given RunOutputs.
|
||||
|
||||
Args:
|
||||
run_outputs (RunOutputs): The RunOutputs object containing the output data.
|
||||
|
||||
Returns:
|
||||
List[Record]: A list of records built from the RunOutputs.
|
||||
|
||||
"""
|
||||
if not run_outputs:
|
||||
return []
|
||||
records = []
|
||||
for result_data in run_outputs.outputs:
|
||||
if result_data:
|
||||
records.extend(build_records_from_result_data(result_data))
|
||||
return records
|
||||
|
||||
|
||||
def build_records_from_result_data(result_data: ResultData, get_final_results_only: bool = True) -> List[Record]:
|
||||
"""
|
||||
Build a list of records from the given ResultData.
|
||||
|
||||
Args:
|
||||
result_data (ResultData): The ResultData object containing the result data.
|
||||
get_final_results_only (bool, optional): Whether to include only final results. Defaults to True.
|
||||
|
||||
Returns:
|
||||
List[Record]: A list of records built from the ResultData.
|
||||
|
||||
"""
|
||||
messages = result_data.messages
|
||||
if not messages:
|
||||
return []
|
||||
records = []
|
||||
for message in messages:
|
||||
message_dict = message if isinstance(message, dict) else message.model_dump()
|
||||
if get_final_results_only:
|
||||
result_data_dict = result_data.model_dump()
|
||||
results = result_data_dict.get("results", {})
|
||||
inner_result = results.get("result", {})
|
||||
record = Record(data={"result": inner_result, "message": message_dict}, text_key="result")
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
|
||||
def format_flow_output_records(records: List[Record]) -> str:
|
||||
"""
|
||||
Format the flow output records into a string.
|
||||
|
||||
Args:
|
||||
records (List[Record]): The list of records to format.
|
||||
|
||||
Returns:
|
||||
str: The formatted flow output records.
|
||||
|
||||
"""
|
||||
result = "Flow run output:\n"
|
||||
results = "\n".join([record.result for record in records if record.data["message"]])
|
||||
return result + results
|
||||
137
src/backend/base/langflow/base/io/chat.py
Normal file
137
src/backend/base/langflow/base/io/chat.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
from typing import Optional, Union
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
from langflow.helpers.record import records_to_text
|
||||
from langflow.memory import store_message
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class ChatComponent(CustomComponent):
|
||||
display_name = "Chat Component"
|
||||
description = "Use as base for chat components."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"input_value": {
|
||||
"input_types": ["Text"],
|
||||
"display_name": "Message",
|
||||
"multiline": True,
|
||||
},
|
||||
"sender": {
|
||||
"options": ["Machine", "User"],
|
||||
"display_name": "Sender Type",
|
||||
"advanced": True,
|
||||
},
|
||||
"sender_name": {"display_name": "Sender Name"},
|
||||
"session_id": {
|
||||
"display_name": "Session ID",
|
||||
"info": "If provided, the message will be stored in the memory.",
|
||||
"advanced": True,
|
||||
},
|
||||
"return_record": {
|
||||
"display_name": "Return Record",
|
||||
"info": "Return the message as a record containing the sender, sender_name, and session_id.",
|
||||
"advanced": True,
|
||||
},
|
||||
"record_template": {
|
||||
"display_name": "Record Template",
|
||||
"multiline": True,
|
||||
"info": "In case of Message being a Record, this template will be used to convert it to text.",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def store_message(
|
||||
self,
|
||||
message: Union[str, Text, Record],
|
||||
session_id: Optional[str] = None,
|
||||
sender: Optional[str] = None,
|
||||
sender_name: Optional[str] = None,
|
||||
) -> list[Record]:
|
||||
records = store_message(
|
||||
message,
|
||||
session_id=session_id,
|
||||
sender=sender,
|
||||
sender_name=sender_name,
|
||||
flow_id=self.graph.flow_id,
|
||||
)
|
||||
|
||||
self.status = records
|
||||
return records
|
||||
|
||||
def build_with_record(
|
||||
self,
|
||||
sender: Optional[str] = "User",
|
||||
sender_name: Optional[str] = "User",
|
||||
input_value: Optional[Union[str, Record]] = None,
|
||||
session_id: Optional[str] = None,
|
||||
return_record: Optional[bool] = False,
|
||||
record_template: str = "Text: {text}\nData: {data}",
|
||||
) -> Union[Text, Record]:
|
||||
input_value_record: Optional[Record] = None
|
||||
if return_record:
|
||||
if isinstance(input_value, Record):
|
||||
# Update the data of the record
|
||||
input_value.data["sender"] = sender
|
||||
input_value.data["sender_name"] = sender_name
|
||||
input_value.data["session_id"] = session_id
|
||||
else:
|
||||
input_value_record = Record(
|
||||
text=input_value,
|
||||
data={
|
||||
"sender": sender,
|
||||
"sender_name": sender_name,
|
||||
"session_id": session_id,
|
||||
},
|
||||
)
|
||||
elif isinstance(input_value, Record):
|
||||
input_value = records_to_text(template=record_template, records=input_value)
|
||||
if not input_value:
|
||||
input_value = ""
|
||||
if return_record and input_value_record:
|
||||
result: Union[Text, Record] = input_value_record
|
||||
else:
|
||||
result = input_value
|
||||
self.status = result
|
||||
if session_id and isinstance(result, (Record, str)):
|
||||
self.store_message(result, session_id, sender, sender_name)
|
||||
return result
|
||||
|
||||
def build_no_record(
|
||||
self,
|
||||
sender: Optional[str] = "User",
|
||||
sender_name: Optional[str] = "User",
|
||||
input_value: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
return_record: Optional[bool] = False,
|
||||
record_template: str = "Text: {text}\nData: {data}",
|
||||
) -> Union[Text, Record]:
|
||||
input_value_record: Optional[Record] = None
|
||||
if return_record:
|
||||
if isinstance(input_value, Record):
|
||||
# Update the data of the record
|
||||
input_value.data["sender"] = sender
|
||||
input_value.data["sender_name"] = sender_name
|
||||
input_value.data["session_id"] = session_id
|
||||
else:
|
||||
input_value_record = Record(
|
||||
text=input_value,
|
||||
data={
|
||||
"sender": sender,
|
||||
"sender_name": sender_name,
|
||||
"session_id": session_id,
|
||||
},
|
||||
)
|
||||
elif isinstance(input_value, Record):
|
||||
input_value = records_to_text(template=record_template, records=input_value)
|
||||
if not input_value:
|
||||
input_value = ""
|
||||
if return_record and input_value_record:
|
||||
result: Union[Text, Record] = input_value_record
|
||||
else:
|
||||
result = input_value
|
||||
self.status = result
|
||||
if session_id and isinstance(result, (Record, str)):
|
||||
self.store_message(result, session_id, sender, sender_name)
|
||||
return result
|
||||
42
src/backend/base/langflow/base/io/text.py
Normal file
42
src/backend/base/langflow/base/io/text.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
from langflow.helpers.record import records_to_text
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
|
||||
class TextComponent(CustomComponent):
|
||||
display_name = "Text Component"
|
||||
description = "Used to pass text to the next component."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"input_value": {
|
||||
"display_name": "Value",
|
||||
"input_types": ["Text", "Record"],
|
||||
"info": "Text or Record to be passed.",
|
||||
},
|
||||
"record_template": {
|
||||
"display_name": "Record Template",
|
||||
"multiline": True,
|
||||
"info": "Template to convert Record to Text. If left empty, it will be dynamically set to the Record's text key.",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
input_value: Optional[Text] = "",
|
||||
record_template: Optional[str] = "{text}",
|
||||
) -> Text:
|
||||
if isinstance(input_value, Record):
|
||||
if record_template == "":
|
||||
# it should be dynamically set to the Record's .text_key value
|
||||
# meaning, if text_key = "bacon", then record_template = "{bacon}"
|
||||
record_template = "{" + input_value.text_key + "}"
|
||||
input_value = records_to_text(template=record_template, records=input_value)
|
||||
self.status = input_value
|
||||
if not input_value:
|
||||
input_value = ""
|
||||
return input_value
|
||||
49
src/backend/base/langflow/base/memory/memory.py
Normal file
49
src/backend/base/langflow/base/memory/memory.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
|
||||
class BaseMemoryComponent(CustomComponent):
|
||||
display_name = "Chat Memory"
|
||||
description = "Retrieves stored chat messages given a specific Session ID."
|
||||
beta: bool = True
|
||||
icon = "history"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"sender": {
|
||||
"options": ["Machine", "User", "Machine and User"],
|
||||
"display_name": "Sender Type",
|
||||
},
|
||||
"sender_name": {"display_name": "Sender Name", "advanced": True},
|
||||
"n_messages": {
|
||||
"display_name": "Number of Messages",
|
||||
"info": "Number of messages to retrieve.",
|
||||
},
|
||||
"session_id": {
|
||||
"display_name": "Session ID",
|
||||
"info": "Session ID of the chat history.",
|
||||
"input_types": ["Text"],
|
||||
},
|
||||
"order": {
|
||||
"options": ["Ascending", "Descending"],
|
||||
"display_name": "Order",
|
||||
"info": "Order of the messages.",
|
||||
"advanced": True,
|
||||
},
|
||||
"record_template": {
|
||||
"display_name": "Record Template",
|
||||
"multiline": True,
|
||||
"info": "Template to convert Record to Text. If left empty, it will be dynamically set to the Record's text key.",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def get_messages(self, **kwargs) -> list[Record]:
|
||||
raise NotImplementedError
|
||||
|
||||
def add_message(
|
||||
self, sender: str, sender_name: str, text: str, session_id: str, metadata: Optional[dict] = None, **kwargs
|
||||
):
|
||||
raise NotImplementedError
|
||||
3
src/backend/base/langflow/base/models/__init__.py
Normal file
3
src/backend/base/langflow/base/models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .model import LCModelComponent
|
||||
|
||||
__all__ = ["LCModelComponent"]
|
||||
1
src/backend/base/langflow/base/models/groq_constants.py
Normal file
1
src/backend/base/langflow/base/models/groq_constants.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
MODEL_NAMES = ["llama3-8b-8192", "llama3-70b-8192", "mixtral-8x7b-32768", "gemma-7b-it"]
|
||||
95
src/backend/base/langflow/base/models/model.py
Normal file
95
src/backend/base/langflow/base/models/model.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from typing import Optional, Union
|
||||
|
||||
from langchain_core.language_models.chat_models import BaseChatModel
|
||||
from langchain_core.language_models.llms import LLM
|
||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
|
||||
|
||||
class LCModelComponent(CustomComponent):
|
||||
display_name: str = "Model Name"
|
||||
description: str = "Model Description"
|
||||
|
||||
def get_result(self, runnable: LLM, stream: bool, input_value: str):
|
||||
"""
|
||||
Retrieves the result from the output of a Runnable object.
|
||||
|
||||
Args:
|
||||
output (Runnable): The output object to retrieve the result from.
|
||||
stream (bool): Indicates whether to use streaming or invocation mode.
|
||||
input_value (str): The input value to pass to the output object.
|
||||
|
||||
Returns:
|
||||
The result obtained from the output object.
|
||||
"""
|
||||
if stream:
|
||||
result = runnable.stream(input_value)
|
||||
else:
|
||||
message = runnable.invoke(input_value)
|
||||
result = message.content if hasattr(message, "content") else message
|
||||
self.status = result
|
||||
return result
|
||||
|
||||
def build_status_message(self, message: AIMessage):
|
||||
"""
|
||||
Builds a status message from an AIMessage object.
|
||||
|
||||
Args:
|
||||
message (AIMessage): The AIMessage object to build the status message from.
|
||||
|
||||
Returns:
|
||||
The status message.
|
||||
"""
|
||||
if message.response_metadata:
|
||||
# Build a well formatted status message
|
||||
content = message.content
|
||||
response_metadata = message.response_metadata
|
||||
openai_keys = ["token_usage", "model_name", "finish_reason"]
|
||||
inner_openai_keys = ["completion_tokens", "prompt_tokens", "total_tokens"]
|
||||
anthropic_keys = ["model", "usage", "stop_reason"]
|
||||
inner_anthropic_keys = ["input_tokens", "output_tokens"]
|
||||
if all(key in response_metadata for key in openai_keys) and all(
|
||||
key in response_metadata["token_usage"] for key in inner_openai_keys
|
||||
):
|
||||
token_usage = response_metadata["token_usage"]
|
||||
completion_tokens = token_usage["completion_tokens"]
|
||||
prompt_tokens = token_usage["prompt_tokens"]
|
||||
total_tokens = token_usage["total_tokens"]
|
||||
finish_reason = response_metadata["finish_reason"]
|
||||
status_message = f"Tokens:\nInput: {prompt_tokens}\nOutput: {completion_tokens}\nTotal Tokens: {total_tokens}\nStop Reason: {finish_reason}\nResponse: {content}"
|
||||
elif all(key in response_metadata for key in anthropic_keys) and all(
|
||||
key in response_metadata["usage"] for key in inner_anthropic_keys
|
||||
):
|
||||
usage = response_metadata["usage"]
|
||||
input_tokens = usage["input_tokens"]
|
||||
output_tokens = usage["output_tokens"]
|
||||
stop_reason = response_metadata["stop_reason"]
|
||||
status_message = f"Tokens:\nInput: {input_tokens}\nOutput: {output_tokens}\nStop Reason: {stop_reason}\nResponse: {content}"
|
||||
else:
|
||||
status_message = f"Response: {content}"
|
||||
else:
|
||||
status_message = f"Response: {message.content}"
|
||||
return status_message
|
||||
|
||||
def get_chat_result(
|
||||
self, runnable: BaseChatModel, stream: bool, input_value: str, system_message: Optional[str] = None
|
||||
):
|
||||
messages: list[Union[HumanMessage, SystemMessage]] = []
|
||||
if not input_value and not system_message:
|
||||
raise ValueError("The message you want to send to the model is empty.")
|
||||
if system_message:
|
||||
messages.append(SystemMessage(content=system_message))
|
||||
if input_value:
|
||||
messages.append(HumanMessage(content=input_value))
|
||||
if stream:
|
||||
return runnable.stream(messages)
|
||||
else:
|
||||
message = runnable.invoke(messages)
|
||||
result = message.content
|
||||
if isinstance(message, AIMessage):
|
||||
status_message = self.build_status_message(message)
|
||||
self.status = status_message
|
||||
else:
|
||||
self.status = result
|
||||
return result
|
||||
|
|
@ -0,0 +1 @@
|
|||
MODEL_NAMES = ["gpt-4o", "gpt-4-turbo", "gpt-4-turbo-preview", "gpt-3.5-turbo", "gpt-3.5-turbo-0125"]
|
||||
83
src/backend/base/langflow/base/prompts/api_utils.py
Normal file
83
src/backend/base/langflow/base/prompts/api_utils.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from langflow.api.v1.base import INVALID_NAMES, check_input_variables
|
||||
from langflow.interface.utils import extract_input_variables_from_prompt
|
||||
from langflow.template.field.prompt import DefaultPromptField
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
|
||||
def validate_prompt(prompt_template: str, silent_errors: bool = False) -> list[str]:
|
||||
input_variables = extract_input_variables_from_prompt(prompt_template)
|
||||
|
||||
# Check if there are invalid characters in the input_variables
|
||||
input_variables = check_input_variables(input_variables)
|
||||
if any(var in INVALID_NAMES for var in input_variables):
|
||||
raise ValueError(f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. ")
|
||||
|
||||
try:
|
||||
PromptTemplate(template=prompt_template, input_variables=input_variables)
|
||||
except Exception as exc:
|
||||
logger.error(f"Invalid prompt: {exc}")
|
||||
if not silent_errors:
|
||||
raise ValueError(f"Invalid prompt: {exc}") from exc
|
||||
|
||||
return input_variables
|
||||
|
||||
|
||||
def get_old_custom_fields(custom_fields, name):
|
||||
try:
|
||||
if len(custom_fields) == 1 and name == "":
|
||||
# If there is only one custom field and the name is empty string
|
||||
# then we are dealing with the first prompt request after the node was created
|
||||
name = list(custom_fields.keys())[0]
|
||||
|
||||
old_custom_fields = custom_fields[name]
|
||||
if not old_custom_fields:
|
||||
old_custom_fields = []
|
||||
|
||||
old_custom_fields = old_custom_fields.copy()
|
||||
except KeyError:
|
||||
old_custom_fields = []
|
||||
custom_fields[name] = []
|
||||
return old_custom_fields
|
||||
|
||||
|
||||
def add_new_variables_to_template(input_variables, custom_fields, template, name):
|
||||
for variable in input_variables:
|
||||
try:
|
||||
template_field = DefaultPromptField(name=variable, display_name=variable)
|
||||
if variable in template:
|
||||
# Set the new field with the old value
|
||||
template_field.value = template[variable]["value"]
|
||||
|
||||
template[variable] = template_field.to_dict()
|
||||
|
||||
# Check if variable is not already in the list before appending
|
||||
if variable not in custom_fields[name]:
|
||||
custom_fields[name].append(variable)
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
def remove_old_variables_from_template(old_custom_fields, input_variables, custom_fields, template, name):
|
||||
for variable in old_custom_fields:
|
||||
if variable not in input_variables:
|
||||
try:
|
||||
# Remove the variable from custom_fields associated with the given name
|
||||
if variable in custom_fields[name]:
|
||||
custom_fields[name].remove(variable)
|
||||
|
||||
# Remove the variable from the template
|
||||
template.pop(variable, None)
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
def update_input_variables_field(input_variables, template):
|
||||
if "input_variables" in template:
|
||||
template["input_variables"]["value"] = input_variables
|
||||
59
src/backend/base/langflow/base/prompts/utils.py
Normal file
59
src/backend/base/langflow/base/prompts/utils.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from copy import deepcopy
|
||||
|
||||
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
def record_to_string(record: Record) -> str:
|
||||
"""
|
||||
Convert a record to a string.
|
||||
|
||||
Args:
|
||||
record (Record): The record to convert.
|
||||
|
||||
Returns:
|
||||
str: The record as a string.
|
||||
"""
|
||||
return record.get_text()
|
||||
|
||||
|
||||
def dict_values_to_string(d: dict) -> dict:
|
||||
"""
|
||||
Converts the values of a dictionary to strings.
|
||||
|
||||
Args:
|
||||
d (dict): The dictionary whose values need to be converted.
|
||||
|
||||
Returns:
|
||||
dict: The dictionary with values converted to strings.
|
||||
"""
|
||||
# Do something similar to the above
|
||||
d_copy = deepcopy(d)
|
||||
for key, value in d_copy.items():
|
||||
# it could be a list of records or documents or strings
|
||||
if isinstance(value, list):
|
||||
for i, item in enumerate(value):
|
||||
if isinstance(item, Record):
|
||||
d_copy[key][i] = record_to_string(item)
|
||||
elif isinstance(item, Document):
|
||||
d_copy[key][i] = document_to_string(item)
|
||||
elif isinstance(value, Record):
|
||||
d_copy[key] = record_to_string(value)
|
||||
elif isinstance(value, Document):
|
||||
d_copy[key] = document_to_string(value)
|
||||
return d_copy
|
||||
|
||||
|
||||
def document_to_string(document: Document) -> str:
|
||||
"""
|
||||
Convert a document to a string.
|
||||
|
||||
Args:
|
||||
document (Document): The document to convert.
|
||||
|
||||
Returns:
|
||||
str: The document as a string.
|
||||
"""
|
||||
return document.page_content
|
||||
23
src/backend/base/langflow/base/tools/base.py
Normal file
23
src/backend/base/langflow/base/tools/base.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from langflow.field_typing import Tool
|
||||
|
||||
|
||||
def build_status_from_tool(tool: Tool):
|
||||
"""
|
||||
Builds a status string representation of a tool.
|
||||
|
||||
Args:
|
||||
tool (Tool): The tool object to build the status for.
|
||||
|
||||
Returns:
|
||||
str: The status string representation of the tool, including its name, description, and arguments (if any).
|
||||
"""
|
||||
description_repr = repr(tool.description).strip("'")
|
||||
args_str = "\n".join(
|
||||
[
|
||||
f"- {arg_name}: {arg_data['description']}"
|
||||
for arg_name, arg_data in tool.args.items()
|
||||
if "description" in arg_data
|
||||
]
|
||||
)
|
||||
status = f"Name: {tool.name}\nDescription: {description_repr}"
|
||||
return status + (f"\nArguments:\n{args_str}" if args_str else "")
|
||||
117
src/backend/base/langflow/base/tools/flow_tool.py
Normal file
117
src/backend/base/langflow/base/tools/flow_tool.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
from typing import Any, List, Optional, Type
|
||||
|
||||
from asyncer import syncify
|
||||
from langchain.tools import BaseTool
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.tools import ToolException
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from langflow.base.flow_processing.utils import build_records_from_result_data, format_flow_output_records
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.helpers.flow import build_schema_from_inputs, get_arg_names, get_flow_inputs, run_flow
|
||||
|
||||
|
||||
class FlowTool(BaseTool):
|
||||
name: str
|
||||
description: str
|
||||
graph: Optional[Graph] = None
|
||||
flow_id: Optional[str] = None
|
||||
user_id: Optional[str] = None
|
||||
inputs: List["Vertex"] = []
|
||||
get_final_results_only: bool = True
|
||||
|
||||
@property
|
||||
def args(self) -> dict:
|
||||
schema = self.get_input_schema()
|
||||
return schema.schema()["properties"]
|
||||
|
||||
def get_input_schema(self, config: Optional[RunnableConfig] = None) -> Type[BaseModel]:
|
||||
"""The tool's input schema."""
|
||||
if self.args_schema is not None:
|
||||
return self.args_schema
|
||||
elif self.graph is not None:
|
||||
return build_schema_from_inputs(self.name, get_flow_inputs(self.graph))
|
||||
else:
|
||||
raise ToolException("No input schema available.")
|
||||
|
||||
def _run(
|
||||
self,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""Use the tool."""
|
||||
args_names = get_arg_names(self.inputs)
|
||||
if len(args_names) == len(args):
|
||||
kwargs = {arg["arg_name"]: arg_value for arg, arg_value in zip(args_names, args)}
|
||||
elif len(args_names) != len(args) and len(args) != 0:
|
||||
raise ToolException(
|
||||
"Number of arguments does not match the number of inputs. Pass keyword arguments instead."
|
||||
)
|
||||
tweaks = {arg["component_name"]: kwargs[arg["arg_name"]] for arg in args_names}
|
||||
|
||||
run_outputs = syncify(run_flow, raise_sync_error=False)(
|
||||
tweaks={key: {"input_value": value} for key, value in tweaks.items()},
|
||||
flow_id=self.flow_id,
|
||||
user_id=self.user_id,
|
||||
)
|
||||
if not run_outputs:
|
||||
return "No output"
|
||||
run_output = run_outputs[0]
|
||||
|
||||
records = []
|
||||
if run_output is not None:
|
||||
for output in run_output.outputs:
|
||||
if output:
|
||||
records.extend(
|
||||
build_records_from_result_data(output, get_final_results_only=self.get_final_results_only)
|
||||
)
|
||||
return format_flow_output_records(records)
|
||||
|
||||
def validate_inputs(self, args_names: List[dict[str, str]], args: Any, kwargs: Any):
|
||||
"""Validate the inputs."""
|
||||
|
||||
if len(args) > 0 and len(args) != len(args_names):
|
||||
raise ToolException(
|
||||
"Number of positional arguments does not match the number of inputs. Pass keyword arguments instead."
|
||||
)
|
||||
|
||||
if len(args) == len(args_names):
|
||||
kwargs = {arg_name["arg_name"]: arg_value for arg_name, arg_value in zip(args_names, args)}
|
||||
|
||||
missing_args = [arg["arg_name"] for arg in args_names if arg["arg_name"] not in kwargs]
|
||||
if missing_args:
|
||||
raise ToolException(f"Missing required arguments: {', '.join(missing_args)}")
|
||||
|
||||
return kwargs
|
||||
|
||||
def build_tweaks_dict(self, args, kwargs):
|
||||
args_names = get_arg_names(self.inputs)
|
||||
kwargs = self.validate_inputs(args_names=args_names, args=args, kwargs=kwargs)
|
||||
tweaks = {arg["component_name"]: kwargs[arg["arg_name"]] for arg in args_names}
|
||||
return tweaks
|
||||
|
||||
async def _arun(
|
||||
self,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""Use the tool asynchronously."""
|
||||
tweaks = self.build_tweaks_dict(args, kwargs)
|
||||
run_outputs = await run_flow(
|
||||
tweaks={key: {"input_value": value} for key, value in tweaks.items()},
|
||||
flow_id=self.flow_id,
|
||||
user_id=self.user_id,
|
||||
)
|
||||
if not run_outputs:
|
||||
return "No output"
|
||||
run_output = run_outputs[0]
|
||||
|
||||
records = []
|
||||
if run_output is not None:
|
||||
for output in run_output.outputs:
|
||||
if output:
|
||||
records.extend(
|
||||
build_records_from_result_data(output, get_final_results_only=self.get_final_results_only)
|
||||
)
|
||||
return format_flow_output_records(records)
|
||||
17
src/backend/base/langflow/components/__init__.py
Normal file
17
src/backend/base/langflow/components/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
__all__ = [
|
||||
"agents",
|
||||
"chains",
|
||||
"documentloaders",
|
||||
"embeddings",
|
||||
"experimental",
|
||||
"inputs",
|
||||
"memories",
|
||||
"model_specs",
|
||||
"outputs",
|
||||
"retrievers",
|
||||
"textsplitters",
|
||||
"toolkits",
|
||||
"tools",
|
||||
"vectorsearch",
|
||||
"vectorstores",
|
||||
]
|
||||
34
src/backend/base/langflow/components/agents/CSVAgent.py
Normal file
34
src/backend/base/langflow/components/agents/CSVAgent.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from langchain_experimental.agents.agent_toolkits.csv.base import create_csv_agent
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import AgentExecutor, BaseLanguageModel
|
||||
|
||||
|
||||
class CSVAgentComponent(CustomComponent):
|
||||
display_name = "CSVAgent"
|
||||
description = "Construct a CSV agent from a CSV and tools."
|
||||
documentation = "https://python.langchain.com/docs/modules/agents/toolkits/csv"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"llm": {"display_name": "LLM", "type": BaseLanguageModel},
|
||||
"path": {"display_name": "Path", "field_type": "file", "suffixes": [".csv"], "file_types": [".csv"]},
|
||||
"handle_parsing_errors": {"display_name": "Handle Parse Errors", "advanced": True},
|
||||
"agent_type": {
|
||||
"display_name": "Agent Type",
|
||||
"options": ["zero-shot-react-description", "openai-functions", "openai-tools"],
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self, llm: BaseLanguageModel, path: str, handle_parsing_errors: bool = True, agent_type: str = "openai-tools"
|
||||
) -> AgentExecutor:
|
||||
# Instantiate and return the CSV agent class with the provided llm and path
|
||||
return create_csv_agent(
|
||||
llm=llm,
|
||||
path=path,
|
||||
agent_type=agent_type,
|
||||
verbose=True,
|
||||
agent_executor_kwargs=dict(handle_parsing_errors=handle_parsing_errors),
|
||||
)
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain.agents import AgentExecutor, create_json_agent
|
||||
from langflow.field_typing import (
|
||||
BaseLanguageModel,
|
||||
)
|
||||
from langchain.agents import AgentExecutor
|
||||
from langchain_community.agent_toolkits import create_json_agent
|
||||
from langchain_community.agent_toolkits.json.toolkit import JsonToolkit
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel
|
||||
|
||||
|
||||
class JsonAgentComponent(CustomComponent):
|
||||
display_name = "JsonAgent"
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
from langflow import CustomComponent
|
||||
from typing import Union, Callable
|
||||
from typing import Callable, Union
|
||||
|
||||
from langchain.agents import AgentExecutor
|
||||
from langflow.field_typing import BaseLanguageModel
|
||||
from langchain_community.agent_toolkits.sql.base import create_sql_agent
|
||||
from langchain.sql_database import SQLDatabase
|
||||
from langchain_community.agent_toolkits import SQLDatabaseToolkit
|
||||
from langchain_community.agent_toolkits.sql.base import create_sql_agent
|
||||
from langchain_community.utilities import SQLDatabase
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel
|
||||
|
||||
|
||||
class SQLAgentComponent(CustomComponent):
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from langchain.agents.tool_calling_agent.base import create_tool_calling_agent
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
|
||||
from langflow.base.agents.agent import LCAgentComponent
|
||||
from langflow.field_typing import BaseLanguageModel, Text, Tool
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
|
||||
class ToolCallingAgentComponent(LCAgentComponent):
|
||||
display_name: str = "Tool Calling Agent"
|
||||
description: str = "Agent that uses tools. Only models that are compatible with function calling are supported."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"llm": {"display_name": "LLM"},
|
||||
"tools": {"display_name": "Tools"},
|
||||
"user_prompt": {
|
||||
"display_name": "Prompt",
|
||||
"multiline": True,
|
||||
"info": "This prompt must contain 'input' key.",
|
||||
},
|
||||
"handle_parsing_errors": {
|
||||
"display_name": "Handle Parsing Errors",
|
||||
"info": "If True, the agent will handle parsing errors. If False, the agent will raise an error.",
|
||||
"advanced": True,
|
||||
},
|
||||
"memory": {
|
||||
"display_name": "Memory",
|
||||
"info": "Memory to use for the agent.",
|
||||
},
|
||||
"input_value": {
|
||||
"display_name": "Inputs",
|
||||
"info": "Input text to pass to the agent.",
|
||||
},
|
||||
}
|
||||
|
||||
async def build(
|
||||
self,
|
||||
input_value: str,
|
||||
llm: BaseLanguageModel,
|
||||
tools: List[Tool],
|
||||
user_prompt: str = "{input}",
|
||||
message_history: Optional[List[Record]] = None,
|
||||
system_message: str = "You are a helpful assistant",
|
||||
handle_parsing_errors: bool = True,
|
||||
) -> Text:
|
||||
if "input" not in user_prompt:
|
||||
raise ValueError("Prompt must contain 'input' key.")
|
||||
messages = [
|
||||
("system", system_message),
|
||||
(
|
||||
"placeholder",
|
||||
"{chat_history}",
|
||||
),
|
||||
("human", user_prompt),
|
||||
("placeholder", "{agent_scratchpad}"),
|
||||
]
|
||||
prompt = ChatPromptTemplate.from_messages(messages)
|
||||
agent = create_tool_calling_agent(llm, tools, prompt)
|
||||
result = await self.run_agent(agent, input_value, tools, message_history, handle_parsing_errors)
|
||||
self.status = result
|
||||
return result
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
from langflow import CustomComponent
|
||||
from typing import Callable, Union
|
||||
|
||||
from langchain.agents import AgentExecutor, create_vectorstore_agent
|
||||
from langchain.agents.agent_toolkits.vectorstore.toolkit import VectorStoreToolkit
|
||||
from typing import Union, Callable
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel
|
||||
|
||||
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain_core.language_models.base import BaseLanguageModel
|
||||
from langchain.agents.agent_toolkits.vectorstore.toolkit import VectorStoreRouterToolkit
|
||||
from langchain.agents import create_vectorstore_router_agent
|
||||
from typing import Callable
|
||||
|
||||
from langchain.agents import create_vectorstore_router_agent
|
||||
from langchain.agents.agent_toolkits.vectorstore.toolkit import VectorStoreRouterToolkit
|
||||
from langchain_core.language_models.base import BaseLanguageModel
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
|
||||
|
||||
class VectorStoreRouterAgentComponent(CustomComponent):
|
||||
display_name = "VectorStoreRouterAgent"
|
||||
111
src/backend/base/langflow/components/agents/XMLAgent.py
Normal file
111
src/backend/base/langflow/components/agents/XMLAgent.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from langchain.agents import create_xml_agent
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
|
||||
|
||||
from langflow.base.agents.agent import LCAgentComponent
|
||||
from langflow.field_typing import BaseLanguageModel, Text, Tool
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
|
||||
class XMLAgentComponent(LCAgentComponent):
|
||||
display_name = "XMLAgent"
|
||||
description = "Construct an XML agent from an LLM and tools."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"llm": {"display_name": "LLM"},
|
||||
"tools": {"display_name": "Tools"},
|
||||
"user_prompt": {
|
||||
"display_name": "Prompt",
|
||||
"multiline": True,
|
||||
"info": "This prompt must contain 'tools' and 'agent_scratchpad' keys.",
|
||||
"value": """You are a helpful assistant. Help the user answer any questions.
|
||||
|
||||
You have access to the following tools:
|
||||
|
||||
{tools}
|
||||
|
||||
In order to use a tool, you can use <tool></tool> and <tool_input></tool_input> tags. You will then get back a response in the form <observation></observation>
|
||||
For example, if you have a tool called 'search' that could run a google search, in order to search for the weather in SF you would respond:
|
||||
|
||||
<tool>search</tool><tool_input>weather in SF</tool_input>
|
||||
<observation>64 degrees</observation>
|
||||
|
||||
When you are done, respond with a final answer between <final_answer></final_answer>. For example:
|
||||
|
||||
<final_answer>The weather in SF is 64 degrees</final_answer>
|
||||
|
||||
Begin!
|
||||
|
||||
Previous Conversation:
|
||||
{chat_history}
|
||||
|
||||
Question: {input}
|
||||
{agent_scratchpad}""",
|
||||
},
|
||||
"system_message": {
|
||||
"display_name": "System Message",
|
||||
"info": "System message to be passed to the LLM.",
|
||||
"advanced": True,
|
||||
},
|
||||
"tool_template": {
|
||||
"display_name": "Tool Template",
|
||||
"info": "Template for rendering tools in the prompt. Tools have 'name' and 'description' keys.",
|
||||
"advanced": True,
|
||||
},
|
||||
"handle_parsing_errors": {
|
||||
"display_name": "Handle Parsing Errors",
|
||||
"info": "If True, the agent will handle parsing errors. If False, the agent will raise an error.",
|
||||
"advanced": True,
|
||||
},
|
||||
"message_history": {
|
||||
"display_name": "Message History",
|
||||
"info": "Message history to pass to the agent.",
|
||||
},
|
||||
"input_value": {
|
||||
"display_name": "Inputs",
|
||||
"info": "Input text to pass to the agent.",
|
||||
},
|
||||
}
|
||||
|
||||
async def build(
|
||||
self,
|
||||
input_value: str,
|
||||
llm: BaseLanguageModel,
|
||||
tools: List[Tool],
|
||||
user_prompt: str = "{input}",
|
||||
system_message: str = "You are a helpful assistant",
|
||||
message_history: Optional[List[Record]] = None,
|
||||
tool_template: str = "{name}: {description}",
|
||||
handle_parsing_errors: bool = True,
|
||||
) -> Text:
|
||||
if "input" not in user_prompt:
|
||||
raise ValueError("Prompt must contain 'input' key.")
|
||||
|
||||
def render_tool_description(tools):
|
||||
return "\n".join(
|
||||
[tool_template.format(name=tool.name, description=tool.description, args=tool.args) for tool in tools]
|
||||
)
|
||||
|
||||
messages = [
|
||||
("system", system_message),
|
||||
(
|
||||
"placeholder",
|
||||
"{chat_history}",
|
||||
),
|
||||
("human", user_prompt),
|
||||
("placeholder", "{agent_scratchpad}"),
|
||||
]
|
||||
prompt = ChatPromptTemplate.from_messages(messages)
|
||||
agent = create_xml_agent(llm, tools, prompt, tools_renderer=render_tool_description)
|
||||
result = await self.run_agent(
|
||||
agent=agent,
|
||||
inputs=input_value,
|
||||
tools=tools,
|
||||
message_history=message_history,
|
||||
handle_parsing_errors=handle_parsing_errors,
|
||||
)
|
||||
self.status = result
|
||||
return result
|
||||
15
src/backend/base/langflow/components/agents/__init__.py
Normal file
15
src/backend/base/langflow/components/agents/__init__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from .CSVAgent import CSVAgentComponent
|
||||
from .JsonAgent import JsonAgentComponent
|
||||
from .SQLAgent import SQLAgentComponent
|
||||
from .VectorStoreAgent import VectorStoreAgentComponent
|
||||
from .VectorStoreRouterAgent import VectorStoreRouterAgentComponent
|
||||
from .XMLAgent import XMLAgentComponent
|
||||
|
||||
__all__ = [
|
||||
"CSVAgentComponent",
|
||||
"JsonAgentComponent",
|
||||
"SQLAgentComponent",
|
||||
"VectorStoreAgentComponent",
|
||||
"VectorStoreRouterAgentComponent",
|
||||
"XMLAgentComponent",
|
||||
]
|
||||
|
|
@ -2,7 +2,7 @@ from typing import Optional
|
|||
|
||||
from langchain.chains import ConversationChain
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, Text
|
||||
|
||||
|
||||
|
|
@ -18,7 +18,10 @@ class ConversationChainComponent(CustomComponent):
|
|||
"display_name": "Memory",
|
||||
"info": "Memory to load context from. If none is provided, a ConversationBufferMemory will be used.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"info": "The input value to pass to the chain.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
|
|
@ -31,16 +34,13 @@ class ConversationChainComponent(CustomComponent):
|
|||
chain = ConversationChain(llm=llm)
|
||||
else:
|
||||
chain = ConversationChain(llm=llm, memory=memory)
|
||||
result = chain.invoke({chain.input_key: input_value})
|
||||
# result is an AIMessage which is a subclass of BaseMessage
|
||||
# We need to check if it is a string or a BaseMessage
|
||||
result_str: Text = ""
|
||||
if hasattr(result, "content") and isinstance(result.content, str):
|
||||
result_str = result.content
|
||||
result = chain.invoke({"input": input_value})
|
||||
if isinstance(result, dict):
|
||||
result = result.get(chain.output_key, "") # type: ignore
|
||||
|
||||
elif isinstance(result, str):
|
||||
result_str = result
|
||||
result = result
|
||||
else:
|
||||
# is dict
|
||||
result_str = Text(result.get("response"))
|
||||
self.status = result_str
|
||||
return result_str
|
||||
result = result.get("response")
|
||||
self.status = result
|
||||
return str(result)
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.chains import LLMChain
|
||||
from langchain.chains.llm import LLMChain
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import (
|
||||
BaseLanguageModel,
|
||||
BaseMemory,
|
||||
BasePromptTemplate,
|
||||
Text,
|
||||
)
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, Text
|
||||
|
||||
|
||||
class LLMChainComponent(CustomComponent):
|
||||
|
|
@ -20,15 +16,15 @@ class LLMChainComponent(CustomComponent):
|
|||
"prompt": {"display_name": "Prompt"},
|
||||
"llm": {"display_name": "LLM"},
|
||||
"memory": {"display_name": "Memory"},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
prompt: BasePromptTemplate,
|
||||
template: Text,
|
||||
llm: BaseLanguageModel,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
) -> Text:
|
||||
prompt = PromptTemplate.from_template(template)
|
||||
runnable = LLMChain(prompt=prompt, llm=llm, memory=memory)
|
||||
result_dict = runnable.invoke({})
|
||||
output_key = runnable.output_key
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from langchain.chains import LLMCheckerChain
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, Text
|
||||
|
||||
|
||||
|
|
@ -12,6 +12,10 @@ class LLMCheckerChainComponent(CustomComponent):
|
|||
def build_config(self):
|
||||
return {
|
||||
"llm": {"display_name": "LLM"},
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"info": "The input value to pass to the chain.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
|
|
@ -2,7 +2,7 @@ from typing import Optional
|
|||
|
||||
from langchain.chains import LLMChain, LLMMathChain
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, Text
|
||||
|
||||
|
||||
|
|
@ -18,6 +18,10 @@ class LLMMathChainComponent(CustomComponent):
|
|||
"memory": {"display_name": "Memory"},
|
||||
"input_key": {"display_name": "Input Key"},
|
||||
"output_key": {"display_name": "Output Key"},
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"info": "The input value to pass to the chain.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.chains.combine_documents.base import BaseCombineDocumentsChain
|
||||
from langchain.chains.retrieval_qa.base import RetrievalQA
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseMemory, BaseRetriever, Text
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, BaseRetriever, Text
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
|
||||
class RetrievalQAComponent(CustomComponent):
|
||||
|
|
@ -14,7 +14,8 @@ class RetrievalQAComponent(CustomComponent):
|
|||
|
||||
def build_config(self):
|
||||
return {
|
||||
"combine_documents_chain": {"display_name": "Combine Documents Chain"},
|
||||
"llm": {"display_name": "LLM"},
|
||||
"chain_type": {"display_name": "Chain Type", "options": ["Stuff", "Map Reduce", "Refine", "Map Rerank"]},
|
||||
"retriever": {"display_name": "Retriever"},
|
||||
"memory": {"display_name": "Memory", "required": False},
|
||||
"input_key": {"display_name": "Input Key", "advanced": True},
|
||||
|
|
@ -22,13 +23,14 @@ class RetrievalQAComponent(CustomComponent):
|
|||
"return_source_documents": {"display_name": "Return Source Documents"},
|
||||
"input_value": {
|
||||
"display_name": "Input",
|
||||
"input_types": ["Text", "Document"],
|
||||
"input_types": ["Record", "Document"],
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
combine_documents_chain: BaseCombineDocumentsChain,
|
||||
llm: BaseLanguageModel,
|
||||
chain_type: str,
|
||||
retriever: BaseRetriever,
|
||||
input_value: str = "",
|
||||
memory: Optional[BaseMemory] = None,
|
||||
|
|
@ -36,8 +38,10 @@ class RetrievalQAComponent(CustomComponent):
|
|||
output_key: str = "result",
|
||||
return_source_documents: bool = True,
|
||||
) -> Text:
|
||||
runnable = RetrievalQA(
|
||||
combine_documents_chain=combine_documents_chain,
|
||||
chain_type = chain_type.lower().replace(" ", "_")
|
||||
runnable = RetrievalQA.from_chain_type(
|
||||
llm=llm,
|
||||
chain_type=chain_type,
|
||||
retriever=retriever,
|
||||
memory=memory,
|
||||
input_key=input_key,
|
||||
|
|
@ -46,6 +50,8 @@ class RetrievalQAComponent(CustomComponent):
|
|||
)
|
||||
if isinstance(input_value, Document):
|
||||
input_value = input_value.page_content
|
||||
if isinstance(input_value, Record):
|
||||
input_value = input_value.get_text()
|
||||
self.status = runnable
|
||||
result = runnable.invoke({input_key: input_value})
|
||||
result = result.content if hasattr(result, "content") else result
|
||||
|
|
@ -3,7 +3,7 @@ from typing import Optional
|
|||
from langchain.chains import RetrievalQAWithSourcesChain
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, BaseRetriever, Text
|
||||
|
||||
|
||||
|
|
@ -16,12 +16,16 @@ class RetrievalQAWithSourcesChainComponent(CustomComponent):
|
|||
"llm": {"display_name": "LLM"},
|
||||
"chain_type": {
|
||||
"display_name": "Chain Type",
|
||||
"options": ["stuff", "map_reduce", "map_rerank", "refine"],
|
||||
"options": ["Stuff", "Map Reduce", "Refine", "Map Rerank"],
|
||||
"info": "The type of chain to use to combined Documents.",
|
||||
},
|
||||
"memory": {"display_name": "Memory"},
|
||||
"return_source_documents": {"display_name": "Return Source Documents"},
|
||||
"retriever": {"display_name": "Retriever"},
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"info": "The input value to pass to the chain.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
|
|
@ -33,6 +37,7 @@ class RetrievalQAWithSourcesChainComponent(CustomComponent):
|
|||
memory: Optional[BaseMemory] = None,
|
||||
return_source_documents: Optional[bool] = True,
|
||||
) -> Text:
|
||||
chain_type = chain_type.lower().replace(" ", "_")
|
||||
runnable = RetrievalQAWithSourcesChain.from_chain_type(
|
||||
llm=llm,
|
||||
chain_type=chain_type,
|
||||
|
|
@ -5,7 +5,7 @@ from langchain_community.utilities.sql_database import SQLDatabase
|
|||
from langchain_core.prompts import PromptTemplate
|
||||
from langchain_core.runnables import Runnable
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, Text
|
||||
|
||||
|
||||
|
|
@ -25,6 +25,10 @@ class SQLGeneratorComponent(CustomComponent):
|
|||
"display_name": "Top K",
|
||||
"info": "The number of results per select statement to return. If 0, no limit.",
|
||||
},
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"info": "The input value to pass to the chain.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
17
src/backend/base/langflow/components/chains/__init__.py
Normal file
17
src/backend/base/langflow/components/chains/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from .ConversationChain import ConversationChainComponent
|
||||
from .LLMChain import LLMChainComponent
|
||||
from .LLMCheckerChain import LLMCheckerChainComponent
|
||||
from .LLMMathChain import LLMMathChainComponent
|
||||
from .RetrievalQA import RetrievalQAComponent
|
||||
from .RetrievalQAWithSourcesChain import RetrievalQAWithSourcesChainComponent
|
||||
from .SQLGenerator import SQLGeneratorComponent
|
||||
|
||||
__all__ = [
|
||||
"ConversationChainComponent",
|
||||
"LLMChainComponent",
|
||||
"LLMCheckerChainComponent",
|
||||
"LLMMathChainComponent",
|
||||
"RetrievalQAComponent",
|
||||
"RetrievalQAWithSourcesChainComponent",
|
||||
"SQLGeneratorComponent",
|
||||
]
|
||||
121
src/backend/base/langflow/components/data/APIRequest.py
Normal file
121
src/backend/base/langflow/components/data/APIRequest.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import asyncio
|
||||
import json
|
||||
from typing import List, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class APIRequest(CustomComponent):
|
||||
display_name: str = "API Request"
|
||||
description: str = "Make HTTP requests given one or more URLs."
|
||||
output_types: list[str] = ["Record"]
|
||||
documentation: str = "https://docs.langflow.org/components/utilities#api-request"
|
||||
icon = "Globe"
|
||||
|
||||
field_config = {
|
||||
"urls": {"display_name": "URLs", "info": "URLs to make requests to."},
|
||||
"method": {
|
||||
"display_name": "Method",
|
||||
"info": "The HTTP method to use.",
|
||||
"field_type": "str",
|
||||
"options": ["GET", "POST", "PATCH", "PUT"],
|
||||
"value": "GET",
|
||||
},
|
||||
"headers": {
|
||||
"display_name": "Headers",
|
||||
"info": "The headers to send with the request.",
|
||||
"input_types": ["Record"],
|
||||
},
|
||||
"body": {
|
||||
"display_name": "Body",
|
||||
"info": "The body to send with the request (for POST, PATCH, PUT).",
|
||||
"input_types": ["Record"],
|
||||
},
|
||||
"timeout": {
|
||||
"display_name": "Timeout",
|
||||
"field_type": "int",
|
||||
"info": "The timeout to use for the request.",
|
||||
"value": 5,
|
||||
},
|
||||
}
|
||||
|
||||
async def make_request(
|
||||
self,
|
||||
client: httpx.AsyncClient,
|
||||
method: str,
|
||||
url: str,
|
||||
headers: Optional[dict] = None,
|
||||
body: Optional[dict] = None,
|
||||
timeout: int = 5,
|
||||
) -> Record:
|
||||
method = method.upper()
|
||||
if method not in ["GET", "POST", "PATCH", "PUT", "DELETE"]:
|
||||
raise ValueError(f"Unsupported method: {method}")
|
||||
|
||||
data = body if body else None
|
||||
payload = json.dumps(data)
|
||||
try:
|
||||
response = await client.request(method, url, headers=headers, content=payload, timeout=timeout)
|
||||
try:
|
||||
result = response.json()
|
||||
except Exception:
|
||||
result = response.text
|
||||
return Record(
|
||||
data={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": response.status_code,
|
||||
"result": result,
|
||||
},
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
return Record(
|
||||
data={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": 408,
|
||||
"error": "Request timed out",
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
return Record(
|
||||
data={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": 500,
|
||||
"error": str(exc),
|
||||
},
|
||||
)
|
||||
|
||||
async def build(
|
||||
self,
|
||||
method: str,
|
||||
urls: List[str],
|
||||
headers: Optional[Record] = None,
|
||||
body: Optional[Record] = None,
|
||||
timeout: int = 5,
|
||||
) -> List[Record]:
|
||||
if headers is None:
|
||||
headers_dict = {}
|
||||
else:
|
||||
headers_dict = headers.data
|
||||
|
||||
bodies = []
|
||||
if body:
|
||||
if isinstance(body, list):
|
||||
bodies = [b.data for b in body]
|
||||
else:
|
||||
bodies = [body.data]
|
||||
|
||||
if len(urls) != len(bodies):
|
||||
# add bodies with None
|
||||
bodies += [None] * (len(urls) - len(bodies)) # type: ignore
|
||||
async with httpx.AsyncClient() as client:
|
||||
results = await asyncio.gather(
|
||||
*[self.make_request(client, method, u, headers_dict, rec, timeout) for u, rec in zip(urls, bodies)]
|
||||
)
|
||||
self.status = results
|
||||
return results
|
||||
63
src/backend/base/langflow/components/data/Directory.py
Normal file
63
src/backend/base/langflow/components/data/Directory.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from langflow.base.data.utils import parallel_load_records, parse_text_file_to_record, retrieve_file_paths
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class DirectoryComponent(CustomComponent):
|
||||
display_name = "Directory"
|
||||
description = "Recursively load files from a directory."
|
||||
icon = "folder"
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"path": {"display_name": "Path"},
|
||||
"types": {
|
||||
"display_name": "Types",
|
||||
"info": "File types to load. Leave empty to load all types.",
|
||||
},
|
||||
"depth": {"display_name": "Depth", "info": "Depth to search for files."},
|
||||
"max_concurrency": {"display_name": "Max Concurrency", "advanced": True},
|
||||
"load_hidden": {
|
||||
"display_name": "Load Hidden",
|
||||
"advanced": True,
|
||||
"info": "If true, hidden files will be loaded.",
|
||||
},
|
||||
"recursive": {
|
||||
"display_name": "Recursive",
|
||||
"advanced": True,
|
||||
"info": "If true, the search will be recursive.",
|
||||
},
|
||||
"silent_errors": {
|
||||
"display_name": "Silent Errors",
|
||||
"advanced": True,
|
||||
"info": "If true, errors will not raise an exception.",
|
||||
},
|
||||
"use_multithreading": {
|
||||
"display_name": "Use Multithreading",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
path: str,
|
||||
depth: int = 0,
|
||||
max_concurrency: int = 2,
|
||||
load_hidden: bool = False,
|
||||
recursive: bool = True,
|
||||
silent_errors: bool = False,
|
||||
use_multithreading: bool = True,
|
||||
) -> List[Optional[Record]]:
|
||||
resolved_path = self.resolve_path(path)
|
||||
file_paths = retrieve_file_paths(resolved_path, load_hidden, recursive, depth)
|
||||
loaded_records = []
|
||||
|
||||
if use_multithreading:
|
||||
loaded_records = parallel_load_records(file_paths, silent_errors, max_concurrency)
|
||||
else:
|
||||
loaded_records = [parse_text_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
||||
loaded_records = list(filter(None, loaded_records))
|
||||
self.status = loaded_records
|
||||
return loaded_records
|
||||
48
src/backend/base/langflow/components/data/File.py
Normal file
48
src/backend/base/langflow/components/data/File.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from langflow.base.data.utils import TEXT_FILE_TYPES, parse_text_file_to_record
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class FileComponent(CustomComponent):
|
||||
display_name = "File"
|
||||
description = "A generic file loader."
|
||||
icon = "file-text"
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"path": {
|
||||
"display_name": "Path",
|
||||
"field_type": "file",
|
||||
"file_types": TEXT_FILE_TYPES,
|
||||
"info": f"Supported file types: {', '.join(TEXT_FILE_TYPES)}",
|
||||
},
|
||||
"silent_errors": {
|
||||
"display_name": "Silent Errors",
|
||||
"advanced": True,
|
||||
"info": "If true, errors will not raise an exception.",
|
||||
},
|
||||
}
|
||||
|
||||
def load_file(self, path: str, silent_errors: bool = False) -> Record:
|
||||
resolved_path = self.resolve_path(path)
|
||||
path_obj = Path(resolved_path)
|
||||
extension = path_obj.suffix[1:].lower()
|
||||
if extension == "doc":
|
||||
raise ValueError("doc files are not supported. Please save as .docx")
|
||||
if extension not in TEXT_FILE_TYPES:
|
||||
raise ValueError(f"Unsupported file type: {extension}")
|
||||
record = parse_text_file_to_record(resolved_path, silent_errors)
|
||||
self.status = record if record else "No data"
|
||||
return record or Record()
|
||||
|
||||
def build(
|
||||
self,
|
||||
path: str,
|
||||
silent_errors: bool = False,
|
||||
) -> Record:
|
||||
record = self.load_file(path, silent_errors)
|
||||
self.status = record
|
||||
return record
|
||||
27
src/backend/base/langflow/components/data/URL.py
Normal file
27
src/backend/base/langflow/components/data/URL.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
from langchain_community.document_loaders.web_base import WebBaseLoader
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class URLComponent(CustomComponent):
|
||||
display_name = "URL"
|
||||
description = "Fetch content from one or more URLs."
|
||||
icon = "layout-template"
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"urls": {"display_name": "URL"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
urls: list[str],
|
||||
) -> list[Record]:
|
||||
loader = WebBaseLoader(web_paths=urls)
|
||||
docs = loader.load()
|
||||
records = self.to_records(docs)
|
||||
self.status = records
|
||||
return records
|
||||
7
src/backend/base/langflow/components/data/__init__.py
Normal file
7
src/backend/base/langflow/components/data/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from .APIRequest import APIRequest
|
||||
from .Directory import DirectoryComponent
|
||||
from .File import FileComponent
|
||||
|
||||
from .URL import URLComponent
|
||||
|
||||
__all__ = ["APIRequest", "DirectoryComponent", "FileComponent", "URLComponent"]
|
||||
|
|
@ -1,21 +1,15 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.embeddings.base import Embeddings
|
||||
from langchain_community.embeddings import BedrockEmbeddings
|
||||
from langchain_core.embeddings import Embeddings
|
||||
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.custom import CustomComponent
|
||||
|
||||
|
||||
class AmazonBedrockEmeddingsComponent(CustomComponent):
|
||||
"""
|
||||
A custom component for implementing an Embeddings Model using Amazon Bedrock.
|
||||
"""
|
||||
|
||||
display_name: str = "Amazon Bedrock Embeddings"
|
||||
description: str = "Embeddings model from Amazon Bedrock."
|
||||
description: str = "Generate embeddings using Amazon Bedrock models."
|
||||
documentation = "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/bedrock"
|
||||
beta = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
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