merge dev into feat-more
This commit is contained in:
commit
e4ba7364bb
345 changed files with 18856 additions and 8899 deletions
|
|
@ -11,4 +11,4 @@ RUN rm *.whl
|
|||
|
||||
EXPOSE 80
|
||||
|
||||
CMD [ "uvicorn", "--host", "0.0.0.0", "--port", "80", "langflow.backend.app:app" ]
|
||||
CMD [ "uvicorn", "--host", "0.0.0.0", "--port", "7860", "--factory", "langflow.main:create_app" ]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from importlib import metadata
|
||||
from langflow.cache import cache_manager
|
||||
|
||||
# Deactivate cache manager for now
|
||||
# from langflow.services.cache import cache_manager
|
||||
from langflow.processing.process import load_flow_from_json
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import sys
|
||||
import time
|
||||
import httpx
|
||||
from langflow.utils.util import get_number_of_workers
|
||||
from multiprocess import Process # type: ignore
|
||||
from langflow.services.database.utils import session_getter
|
||||
from langflow.services.manager import initialize_services, initialize_settings_manager
|
||||
from langflow.services.utils import get_db_manager, get_settings_manager
|
||||
|
||||
from multiprocess import Process, cpu_count # type: ignore
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
|
@ -10,19 +13,49 @@ import socket
|
|||
from rich.panel import Panel
|
||||
from rich import box
|
||||
from rich import print as rprint
|
||||
from rich.table import Table
|
||||
import typer
|
||||
from langflow.main import setup_app
|
||||
from langflow.settings import settings
|
||||
from langflow.utils.logger import configure, logger
|
||||
import webbrowser
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
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 update_settings(
|
||||
config: str,
|
||||
cache: str,
|
||||
cache: Optional[str] = None,
|
||||
dev: bool = False,
|
||||
remove_api_keys: bool = False,
|
||||
components_path: Optional[Path] = None,
|
||||
|
|
@ -30,19 +63,20 @@ def update_settings(
|
|||
"""Update the settings from a config file."""
|
||||
|
||||
# Check for database_url in the environment variables
|
||||
|
||||
initialize_settings_manager()
|
||||
settings_manager = get_settings_manager()
|
||||
if config:
|
||||
logger.debug(f"Loading settings from {config}")
|
||||
settings.update_from_yaml(config, dev=dev)
|
||||
settings_manager.settings.update_from_yaml(config, dev=dev)
|
||||
if remove_api_keys:
|
||||
logger.debug(f"Setting remove_api_keys to {remove_api_keys}")
|
||||
settings.update_settings(REMOVE_API_KEYS=remove_api_keys)
|
||||
settings_manager.settings.update_settings(REMOVE_API_KEYS=remove_api_keys)
|
||||
if cache:
|
||||
logger.debug(f"Setting cache to {cache}")
|
||||
settings.update_settings(CACHE=cache)
|
||||
settings_manager.settings.update_settings(CACHE=cache)
|
||||
if components_path:
|
||||
logger.debug(f"Adding component path {components_path}")
|
||||
settings.update_settings(COMPONENTS_PATH=components_path)
|
||||
settings_manager.settings.update_settings(COMPONENTS_PATH=components_path)
|
||||
|
||||
|
||||
def serve_on_jcloud():
|
||||
|
|
@ -92,7 +126,7 @@ def serve_on_jcloud():
|
|||
|
||||
|
||||
@app.command()
|
||||
def serve(
|
||||
def run(
|
||||
host: str = typer.Option(
|
||||
"127.0.0.1", help="Host to bind the server to.", envvar="LANGFLOW_HOST"
|
||||
),
|
||||
|
|
@ -106,7 +140,9 @@ def serve(
|
|||
help="Path to the directory containing custom components.",
|
||||
envvar="LANGFLOW_COMPONENTS_PATH",
|
||||
),
|
||||
config: str = typer.Option("config.yaml", help="Path to the configuration file."),
|
||||
config: str = typer.Option(
|
||||
Path(__file__).parent / "config.yaml", help="Path to the configuration file."
|
||||
),
|
||||
# .env file param
|
||||
env_file: Path = typer.Option(
|
||||
None, help="Path to the .env file containing environment variables."
|
||||
|
|
@ -117,10 +153,10 @@ def serve(
|
|||
log_file: Path = typer.Option(
|
||||
"logs/langflow.log", help="Path to the log file.", envvar="LANGFLOW_LOG_FILE"
|
||||
),
|
||||
cache: str = typer.Option(
|
||||
cache: Optional[str] = typer.Option(
|
||||
envvar="LANGFLOW_LANGCHAIN_CACHE",
|
||||
help="Type of cache to use. (InMemoryCache, SQLiteCache)",
|
||||
default="SQLiteCache",
|
||||
default=None,
|
||||
),
|
||||
jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"),
|
||||
dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"),
|
||||
|
|
@ -146,6 +182,11 @@ def serve(
|
|||
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",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Run the Langflow server.
|
||||
|
|
@ -167,7 +208,7 @@ def serve(
|
|||
)
|
||||
# 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)
|
||||
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)
|
||||
|
|
@ -179,6 +220,10 @@ def serve(
|
|||
"timeout": timeout,
|
||||
}
|
||||
|
||||
# Define an env variable to know if we are just testing the server
|
||||
if "pytest" in sys.modules:
|
||||
return
|
||||
|
||||
if platform.system() in ["Windows"]:
|
||||
# Run using uvicorn on MacOS and Windows
|
||||
# Windows doesn't support gunicorn
|
||||
|
|
@ -299,6 +344,43 @@ def run_langflow(host, port, log_level, options, app):
|
|||
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."
|
||||
),
|
||||
):
|
||||
initialize_services()
|
||||
db_manager = get_db_manager()
|
||||
with session_getter(db_manager) 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.user import User
|
||||
|
||||
user = session.query(User).filter(User.username == username).first()
|
||||
if user is None:
|
||||
typer.echo("Superuser creation failed.")
|
||||
return
|
||||
|
||||
typer.echo("Superuser created successfully.")
|
||||
|
||||
else:
|
||||
typer.echo("Superuser creation failed.")
|
||||
|
||||
|
||||
@app.command()
|
||||
def migration(test: bool = typer.Option(False, help="Run migrations in test mode.")):
|
||||
initialize_services()
|
||||
db_manager = get_db_manager()
|
||||
if not test:
|
||||
db_manager.run_migrations()
|
||||
results = db_manager.run_migrations_test()
|
||||
display_results(results)
|
||||
|
||||
|
||||
def main():
|
||||
app()
|
||||
|
||||
|
|
|
|||
113
src/backend/langflow/alembic.ini
Normal file
113
src/backend/langflow/alembic.ini
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = alembic
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python-dateutil library that can be
|
||||
# installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
# This is the path to the db in the root of the project.
|
||||
# When the user runs the Langflow the database url will
|
||||
# be set dinamically.
|
||||
sqlalchemy.url = sqlite:///../../../langflow.db
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
1
src/backend/langflow/alembic/README
Normal file
1
src/backend/langflow/alembic/README
Normal file
|
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration.
|
||||
81
src/backend/langflow/alembic/env.py
Normal file
81
src/backend/langflow/alembic/env.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
from langflow.services.database.manager import SQLModel
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = SQLModel.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
render_as_batch=True,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
27
src/backend/langflow/alembic/script.py.mako
Normal file
27
src/backend/langflow/alembic/script.py.mako
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
"""Adds tables
|
||||
|
||||
Revision ID: 260dbcc8b680
|
||||
Revises:
|
||||
Create Date: 2023-08-27 19:49:02.681355
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "260dbcc8b680"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn)
|
||||
# List existing tables
|
||||
existing_tables = inspector.get_table_names()
|
||||
# Drop 'flowstyle' table if it exists
|
||||
# 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")
|
||||
|
||||
existing_indices_flow = []
|
||||
existing_fks_flow = []
|
||||
if "flow" in existing_tables:
|
||||
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")
|
||||
]
|
||||
# Now check if the columns user_id exists in the 'flow' table
|
||||
# If it does not exist, we need to create the foreign key
|
||||
|
||||
if "user" not in existing_tables:
|
||||
op.create_table(
|
||||
"user",
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("username", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("password", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||
sa.Column("is_superuser", sa.Boolean(), nullable=False),
|
||||
sa.Column("create_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("last_login_at", sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("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
|
||||
)
|
||||
|
||||
if "apikey" not in existing_tables:
|
||||
op.create_table(
|
||||
"apikey",
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("last_used_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("total_uses", sa.Integer(), nullable=False, default=0),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, default=True),
|
||||
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"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("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_name"), ["name"], 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",
|
||||
sa.Column("data", sa.JSON(), nullable=True),
|
||||
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("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["user.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
# Conditionally create indices for 'flow' table
|
||||
# if _alembic_tmp_flow exists, then we need to drop it first
|
||||
# This is to deal with SQLite not being able to ROLLBACK
|
||||
# for some unknown reason
|
||||
if "_alembic_tmp_flow" in existing_tables:
|
||||
op.drop_table("_alembic_tmp_flow")
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
flow_columns = [col["name"] for col in inspector.get_columns("flow")]
|
||||
if "user_id" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"user_id",
|
||||
sqlmodel.sql.sqltypes.GUID(),
|
||||
nullable=True, # This should be False, but we need to allow NULL values for now
|
||||
)
|
||||
)
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn)
|
||||
# List existing tables
|
||||
existing_tables = inspector.get_table_names()
|
||||
if "flow" in existing_tables:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_flow_user_id"))
|
||||
batch_op.drop_index(batch_op.f("ix_flow_name"))
|
||||
batch_op.drop_index(batch_op.f("ix_flow_description"))
|
||||
|
||||
op.drop_table("flow")
|
||||
if "apikey" in existing_tables:
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_apikey_user_id"))
|
||||
batch_op.drop_index(batch_op.f("ix_apikey_name"))
|
||||
batch_op.drop_index(batch_op.f("ix_apikey_api_key"))
|
||||
|
||||
op.drop_table("apikey")
|
||||
if "user" in existing_tables:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_user_username"))
|
||||
|
||||
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,49 @@
|
|||
"""Add profile-image column
|
||||
|
||||
Revision ID: 67cc006d50bf
|
||||
Revises: 260dbcc8b680
|
||||
Create Date: 2023-09-08 07:36:13.387318
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "67cc006d50bf"
|
||||
down_revision: Union[str, None] = "260dbcc8b680"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn)
|
||||
if "user" in inspector.get_table_names() and "profile_image" not in [
|
||||
column["name"] for column in inspector.get_columns("user")
|
||||
]:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"profile_image", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
)
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn)
|
||||
if "user" in inspector.get_table_names() and "profile_image" in [
|
||||
column["name"] for column in inspector.get_columns("user")
|
||||
]:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.drop_column("profile_image")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -5,8 +5,10 @@ from langflow.api.v1 import (
|
|||
endpoints_router,
|
||||
validate_router,
|
||||
flows_router,
|
||||
flow_styles_router,
|
||||
component_router,
|
||||
users_router,
|
||||
api_key_router,
|
||||
login_router,
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
|
|
@ -17,4 +19,6 @@ router.include_router(endpoints_router)
|
|||
router.include_router(validate_router)
|
||||
router.include_router(component_router)
|
||||
router.include_router(flows_router)
|
||||
router.include_router(flow_styles_router)
|
||||
router.include_router(users_router)
|
||||
router.include_router(api_key_router)
|
||||
router.include_router(login_router)
|
||||
|
|
|
|||
|
|
@ -66,3 +66,30 @@ def merge_nested_dicts(dict1, dict2):
|
|||
else:
|
||||
dict1[key] = value
|
||||
return dict1
|
||||
|
||||
|
||||
def merge_nested_dicts_with_renaming(dict1, dict2):
|
||||
for key, value in dict2.items():
|
||||
if (
|
||||
key in dict1
|
||||
and isinstance(value, dict)
|
||||
and isinstance(dict1.get(key), dict)
|
||||
):
|
||||
for sub_key, sub_value in value.items():
|
||||
if sub_key in dict1[key]:
|
||||
new_key = get_new_key(dict1[key], sub_key)
|
||||
dict1[key][new_key] = sub_value
|
||||
else:
|
||||
dict1[key][sub_key] = sub_value
|
||||
else:
|
||||
dict1[key] = value
|
||||
return dict1
|
||||
|
||||
|
||||
def get_new_key(dictionary, original_key):
|
||||
counter = 1
|
||||
new_key = original_key + " (" + str(counter) + ")"
|
||||
while new_key in dictionary:
|
||||
counter += 1
|
||||
new_key = original_key + " (" + str(counter) + ")"
|
||||
return new_key
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ from langflow.api.v1.endpoints import router as endpoints_router
|
|||
from langflow.api.v1.validate import router as validate_router
|
||||
from langflow.api.v1.chat import router as chat_router
|
||||
from langflow.api.v1.flows import router as flows_router
|
||||
from langflow.api.v1.flow_styles import router as flow_styles_router
|
||||
from langflow.api.v1.components import router as component_router
|
||||
from langflow.api.v1.users import router as users_router
|
||||
from langflow.api.v1.api_key import router as api_key_router
|
||||
from langflow.api.v1.login import router as login_router
|
||||
|
||||
__all__ = [
|
||||
"chat_router",
|
||||
|
|
@ -11,5 +13,7 @@ __all__ = [
|
|||
"component_router",
|
||||
"validate_router",
|
||||
"flows_router",
|
||||
"flow_styles_router",
|
||||
"users_router",
|
||||
"api_key_router",
|
||||
"login_router",
|
||||
]
|
||||
|
|
|
|||
61
src/backend/langflow/api/v1/api_key.py
Normal file
61
src/backend/langflow/api/v1/api_key.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from uuid import UUID
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from langflow.api.v1.schemas import ApiKeysResponse
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.api_key.api_key import (
|
||||
ApiKeyCreate,
|
||||
UnmaskedApiKeyRead,
|
||||
)
|
||||
|
||||
# Assuming you have these methods in your service layer
|
||||
from langflow.services.database.models.api_key.crud import (
|
||||
get_api_keys,
|
||||
create_api_key,
|
||||
delete_api_key,
|
||||
)
|
||||
from langflow.services.database.models.user.user import User
|
||||
from langflow.services.utils import get_session
|
||||
from sqlmodel import Session
|
||||
|
||||
|
||||
router = APIRouter(tags=["APIKey"], prefix="/api_key")
|
||||
|
||||
|
||||
@router.get("/", response_model=ApiKeysResponse)
|
||||
def get_api_keys_route(
|
||||
db: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
try:
|
||||
user_id = current_user.id
|
||||
keys = get_api_keys(db, user_id)
|
||||
|
||||
return ApiKeysResponse(total_count=len(keys), user_id=user_id, api_keys=keys)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/", response_model=UnmaskedApiKeyRead)
|
||||
def create_api_key_route(
|
||||
req: ApiKeyCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
user_id = current_user.id
|
||||
return create_api_key(db, req, user_id=user_id)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.delete("/{api_key_id}")
|
||||
def delete_api_key_route(
|
||||
api_key_id: UUID,
|
||||
current_user=Depends(get_current_active_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
delete_api_key(db, api_key_id)
|
||||
return {"detail": "API Key deleted"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
from typing import Optional
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
|
|
@ -20,7 +21,8 @@ class FrontendNodeRequest(FrontendNode):
|
|||
class ValidatePromptRequest(BaseModel):
|
||||
name: str
|
||||
template: str
|
||||
frontend_node: FrontendNodeRequest
|
||||
# optional for tweak call
|
||||
frontend_node: Optional[FrontendNodeRequest] = None
|
||||
|
||||
|
||||
# Build ValidationResponse class for {"imports": {"errors": []}, "function": {"errors": []}}
|
||||
|
|
@ -39,7 +41,8 @@ class CodeValidationResponse(BaseModel):
|
|||
|
||||
class PromptValidationResponse(BaseModel):
|
||||
input_variables: list
|
||||
frontend_node: FrontendNodeRequest
|
||||
# object return for tweak call
|
||||
frontend_node: Optional[FrontendNodeRequest] = None
|
||||
|
||||
|
||||
INVALID_CHARACTERS = {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from fastapi import WebSocket
|
|||
|
||||
|
||||
from langchain.schema import AgentAction, LLMResult, AgentFinish
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
|
||||
|
||||
# https://github.com/hwchase17/chat-langchain/blob/master/callback.py
|
||||
|
|
|
|||
|
|
@ -1,37 +1,79 @@
|
|||
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketException, status
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
HTTPException,
|
||||
Query,
|
||||
WebSocket,
|
||||
WebSocketException,
|
||||
status,
|
||||
)
|
||||
from fastapi.responses import StreamingResponse
|
||||
from langflow.api.utils import build_input_keys_response
|
||||
from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, StreamData
|
||||
|
||||
from langflow.chat.manager import ChatManager
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.utils.logger import logger
|
||||
from langflow.services.auth.utils import get_current_active_user, get_current_user
|
||||
from loguru import logger
|
||||
from langflow.services.utils import get_chat_manager, get_session
|
||||
from cachetools import LRUCache
|
||||
from sqlmodel import Session
|
||||
from langflow.services.chat.manager import ChatManager
|
||||
|
||||
|
||||
router = APIRouter(tags=["Chat"])
|
||||
chat_manager = ChatManager()
|
||||
|
||||
flow_data_store: LRUCache = LRUCache(maxsize=10)
|
||||
|
||||
|
||||
@router.websocket("/chat/{client_id}")
|
||||
async def chat(client_id: str, websocket: WebSocket):
|
||||
async def chat(
|
||||
client_id: str,
|
||||
websocket: WebSocket,
|
||||
token: str = Query(...),
|
||||
db: Session = Depends(get_session),
|
||||
chat_manager: "ChatManager" = Depends(get_chat_manager),
|
||||
):
|
||||
"""Websocket endpoint for chat."""
|
||||
try:
|
||||
await websocket.accept()
|
||||
user = await get_current_user(token, db)
|
||||
if not user:
|
||||
await websocket.close(
|
||||
code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized"
|
||||
)
|
||||
if not user.is_active:
|
||||
await websocket.close(
|
||||
code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized"
|
||||
)
|
||||
|
||||
if client_id in chat_manager.in_memory_cache:
|
||||
await chat_manager.handle_websocket(client_id, websocket)
|
||||
else:
|
||||
# We accept the connection but close it immediately
|
||||
# if the flow is not built yet
|
||||
await websocket.accept()
|
||||
message = "Please, build the flow before sending messages"
|
||||
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=message)
|
||||
except WebSocketException as exc:
|
||||
logger.error(f"Websocket error: {exc}")
|
||||
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc))
|
||||
except Exception as exc:
|
||||
logger.error(f"Error in chat websocket: {exc}")
|
||||
messsage = exc.detail if isinstance(exc, HTTPException) else str(exc)
|
||||
if "Could not validate credentials" in str(exc):
|
||||
await websocket.close(
|
||||
code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized"
|
||||
)
|
||||
else:
|
||||
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=messsage)
|
||||
|
||||
|
||||
@router.post("/build/init/{flow_id}", response_model=InitResponse, status_code=201)
|
||||
async def init_build(graph_data: dict, flow_id: str):
|
||||
async def init_build(
|
||||
graph_data: dict,
|
||||
flow_id: str,
|
||||
current_user=Depends(get_current_active_user),
|
||||
chat_manager: "ChatManager" = Depends(get_chat_manager),
|
||||
):
|
||||
"""Initialize the build by storing graph data and returning a unique session ID."""
|
||||
|
||||
try:
|
||||
|
|
@ -52,6 +94,7 @@ async def init_build(graph_data: dict, flow_id: str):
|
|||
flow_data_store[flow_id] = {
|
||||
"graph_data": graph_data,
|
||||
"status": BuildStatus.STARTED,
|
||||
"user_id": current_user.id,
|
||||
}
|
||||
|
||||
return InitResponse(flowId=flow_id)
|
||||
|
|
@ -79,7 +122,9 @@ async def build_status(flow_id: str):
|
|||
|
||||
|
||||
@router.get("/build/stream/{flow_id}", response_class=StreamingResponse)
|
||||
async def stream_build(flow_id: str):
|
||||
async def stream_build(
|
||||
flow_id: str, chat_manager: "ChatManager" = Depends(get_chat_manager)
|
||||
):
|
||||
"""Stream the build process based on stored flow data."""
|
||||
|
||||
async def event_stream(flow_id):
|
||||
|
|
@ -97,6 +142,7 @@ async def stream_build(flow_id: str):
|
|||
return
|
||||
|
||||
graph_data = flow_data_store[flow_id].get("graph_data")
|
||||
user_id = flow_data_store[flow_id]["user_id"]
|
||||
|
||||
if not graph_data:
|
||||
error_message = "No data provided"
|
||||
|
|
@ -104,14 +150,9 @@ async def stream_build(flow_id: str):
|
|||
return
|
||||
|
||||
logger.debug("Building langchain object")
|
||||
try:
|
||||
# Some error could happen when building the graph
|
||||
graph = Graph.from_payload(graph_data)
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
error_message = str(exc)
|
||||
yield str(StreamData(event="error", data={"error": error_message}))
|
||||
return
|
||||
|
||||
# Some error could happen when building the graph
|
||||
graph = Graph.from_payload(graph_data)
|
||||
|
||||
number_of_nodes = len(graph.nodes)
|
||||
flow_data_store[flow_id]["status"] = BuildStatus.IN_PROGRESS
|
||||
|
|
@ -122,11 +163,13 @@ async def stream_build(flow_id: str):
|
|||
"log": f"Building node {vertex.vertex_type}",
|
||||
}
|
||||
yield str(StreamData(event="log", data=log_dict))
|
||||
vertex.build()
|
||||
vertex.build(user_id)
|
||||
params = vertex._built_object_repr()
|
||||
valid = True
|
||||
logger.debug(f"Building node {str(vertex.vertex_type)}")
|
||||
logger.debug(f"Output: {params}")
|
||||
logger.debug(
|
||||
f"Output: {params[:100]}{'...' if len(params) > 100 else ''}"
|
||||
)
|
||||
if vertex.artifacts:
|
||||
# The artifacts will be prompt variables
|
||||
# passed to build_input_keys_response
|
||||
|
|
@ -155,12 +198,11 @@ async def stream_build(flow_id: str):
|
|||
)
|
||||
else:
|
||||
input_keys_response = {
|
||||
"input_keys": {},
|
||||
"input_keys": None,
|
||||
"memory_keys": [],
|
||||
"handle_keys": [],
|
||||
}
|
||||
yield str(StreamData(event="message", data=input_keys_response))
|
||||
|
||||
chat_manager.set_cache(flow_id, langchain_object)
|
||||
# We need to reset the chat history
|
||||
chat_manager.chat_history.empty_history(flow_id)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
from datetime import timezone
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
from langflow.database.models.component import Component, ComponentModel
|
||||
from langflow.database.base import get_session
|
||||
from langflow.services.database.models.component import Component, ComponentModel
|
||||
from langflow.services.utils import get_session
|
||||
from sqlmodel import Session, select
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
from http import HTTPStatus
|
||||
from typing import Annotated, Optional
|
||||
from typing import Annotated, Any, Optional, Union
|
||||
from langflow.services.auth.utils import api_key_security, get_current_active_user
|
||||
|
||||
from langflow.cache.utils import save_uploaded_file
|
||||
from langflow.database.models.flow import Flow
|
||||
from langflow.services.cache.utils import save_uploaded_file
|
||||
from langflow.services.database.models.flow import Flow
|
||||
from langflow.processing.process import process_graph_cached, process_tweaks
|
||||
from langflow.utils.logger import logger
|
||||
from langflow.settings import settings
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body
|
||||
|
||||
from langflow.services.database.models.user.user import User
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from loguru import logger
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body, status
|
||||
import sqlalchemy as sa
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
|
|
@ -18,7 +19,7 @@ from langflow.api.v1.schemas import (
|
|||
CustomComponentCode,
|
||||
)
|
||||
|
||||
from langflow.api.utils import merge_nested_dicts
|
||||
from langflow.api.utils import merge_nested_dicts_with_renaming
|
||||
|
||||
from langflow.interface.types import (
|
||||
build_langchain_types_dict,
|
||||
|
|
@ -26,52 +27,93 @@ from langflow.interface.types import (
|
|||
build_langchain_custom_component_list_from_path,
|
||||
)
|
||||
|
||||
from langflow.database.base import get_session
|
||||
from langflow.services.utils import get_session
|
||||
from sqlmodel import Session
|
||||
|
||||
# build router
|
||||
router = APIRouter(tags=["Base"])
|
||||
|
||||
|
||||
@router.get("/all")
|
||||
def get_all():
|
||||
@router.get("/all", dependencies=[Depends(get_current_active_user)])
|
||||
def get_all(
|
||||
settings_manager=Depends(get_settings_manager),
|
||||
):
|
||||
logger.debug("Building langchain types dict")
|
||||
native_components = build_langchain_types_dict()
|
||||
# custom_components is a list of dicts
|
||||
# need to merge all the keys into one dict
|
||||
custom_components_from_file = {}
|
||||
if settings.COMPONENTS_PATH:
|
||||
logger.info(f"Building custom components from {settings.COMPONENTS_PATH}")
|
||||
custom_component_dicts = [
|
||||
build_langchain_custom_component_list_from_path(str(path))
|
||||
for path in settings.COMPONENTS_PATH
|
||||
]
|
||||
logger.info(f"Loading {len(custom_component_dicts)} custom components")
|
||||
custom_components_from_file: dict[str, Any] = {}
|
||||
if settings_manager.settings.COMPONENTS_PATH:
|
||||
logger.info(
|
||||
f"Building custom components from {settings_manager.settings.COMPONENTS_PATH}"
|
||||
)
|
||||
|
||||
custom_component_dicts = []
|
||||
processed_paths = []
|
||||
for path in settings_manager.settings.COMPONENTS_PATH:
|
||||
if str(path) in processed_paths:
|
||||
continue
|
||||
custom_component_dict = build_langchain_custom_component_list_from_path(
|
||||
str(path)
|
||||
)
|
||||
custom_component_dicts.append(custom_component_dict)
|
||||
processed_paths.append(str(path))
|
||||
|
||||
logger.info(f"Loading {len(custom_component_dicts)} category(ies)")
|
||||
for custom_component_dict in custom_component_dicts:
|
||||
custom_components_from_file = merge_nested_dicts(
|
||||
# custom_component_dict is a dict of dicts
|
||||
if not custom_component_dict:
|
||||
continue
|
||||
category = list(custom_component_dict.keys())[0]
|
||||
logger.info(
|
||||
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
|
||||
)
|
||||
custom_components_from_file = merge_nested_dicts_with_renaming(
|
||||
custom_components_from_file, custom_component_dict
|
||||
)
|
||||
logger.info(f"Loaded {custom_component_dict}")
|
||||
return merge_nested_dicts(native_components, custom_components_from_file)
|
||||
|
||||
return merge_nested_dicts_with_renaming(
|
||||
native_components, custom_components_from_file
|
||||
)
|
||||
|
||||
|
||||
# For backwards compatibility we will keep the old endpoint
|
||||
@router.post("/predict/{flow_id}", response_model=ProcessResponse)
|
||||
@router.post("/process/{flow_id}", response_model=ProcessResponse)
|
||||
@router.post(
|
||||
"/predict/{flow_id}",
|
||||
response_model=ProcessResponse,
|
||||
dependencies=[Depends(api_key_security)],
|
||||
)
|
||||
@router.post(
|
||||
"/process/{flow_id}",
|
||||
response_model=ProcessResponse,
|
||||
)
|
||||
async def process_flow(
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
flow_id: str,
|
||||
inputs: Optional[dict] = None,
|
||||
tweaks: Optional[dict] = None,
|
||||
clear_cache: Annotated[bool, Body(embed=True)] = False, # noqa: F821
|
||||
session: Session = Depends(get_session),
|
||||
session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
|
||||
api_key_user: User = Depends(api_key_security),
|
||||
):
|
||||
"""
|
||||
Endpoint to process an input with a given flow_id.
|
||||
"""
|
||||
|
||||
try:
|
||||
flow = session.get(Flow, flow_id)
|
||||
if api_key_user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid API Key",
|
||||
)
|
||||
|
||||
# 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()
|
||||
)
|
||||
if flow is None:
|
||||
raise ValueError(f"Flow {flow_id} not found")
|
||||
|
||||
|
|
@ -83,10 +125,26 @@ async def process_flow(
|
|||
graph_data = process_tweaks(graph_data, tweaks)
|
||||
except Exception as exc:
|
||||
logger.error(f"Error processing tweaks: {exc}")
|
||||
response = process_graph_cached(graph_data, inputs, clear_cache)
|
||||
return ProcessResponse(
|
||||
result=response,
|
||||
response, session_id = process_graph_cached(
|
||||
graph_data, inputs, clear_cache, session_id
|
||||
)
|
||||
return ProcessResponse(result=response, 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):
|
||||
# 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} not found" in str(exc):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
||||
) from exc
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||
) from exc
|
||||
except Exception as e:
|
||||
# Log stack trace
|
||||
logger.exception(e)
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
from uuid import UUID
|
||||
from langflow.database.models.flow_style import (
|
||||
FlowStyle,
|
||||
FlowStyleCreate,
|
||||
FlowStyleRead,
|
||||
FlowStyleUpdate,
|
||||
)
|
||||
from langflow.database.base import get_session
|
||||
from sqlmodel import Session, select
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
|
||||
# build router
|
||||
router = APIRouter(prefix="/flow_styles", tags=["FlowStyles"])
|
||||
|
||||
# FlowStyleCreate:
|
||||
# class FlowStyleBase(SQLModel):
|
||||
# color: str = Field(index=True)
|
||||
# emoji: str = Field(index=False)
|
||||
# flow_id: UUID = Field(default=None, foreign_key="flow.id")
|
||||
|
||||
|
||||
@router.post("/", response_model=FlowStyleRead)
|
||||
def create_flow_style(
|
||||
*, session: Session = Depends(get_session), flow_style: FlowStyleCreate
|
||||
):
|
||||
"""Create a new flow_style."""
|
||||
db_flow_style = FlowStyle.from_orm(flow_style)
|
||||
session.add(db_flow_style)
|
||||
session.commit()
|
||||
session.refresh(db_flow_style)
|
||||
return db_flow_style
|
||||
|
||||
|
||||
@router.get("/", response_model=list[FlowStyleRead])
|
||||
def read_flow_styles(*, session: Session = Depends(get_session)):
|
||||
"""Read all flows."""
|
||||
try:
|
||||
flows = session.exec(select(FlowStyle)).all()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
return flows
|
||||
|
||||
|
||||
@router.get("/{flow_styles_id}", response_model=FlowStyleRead)
|
||||
def read_flow_style(*, session: Session = Depends(get_session), flow_styles_id: UUID):
|
||||
"""Read a flow_style."""
|
||||
if flow_style := session.get(FlowStyle, flow_styles_id):
|
||||
return flow_style
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="FlowStyle not found")
|
||||
|
||||
|
||||
@router.patch("/{flow_style_id}", response_model=FlowStyleRead)
|
||||
def update_flow_style(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
flow_style_id: UUID,
|
||||
flow_style: FlowStyleUpdate,
|
||||
):
|
||||
"""Update a flow_style."""
|
||||
db_flow_style = session.get(FlowStyle, flow_style_id)
|
||||
if not db_flow_style:
|
||||
raise HTTPException(status_code=404, detail="FlowStyle not found")
|
||||
flow_data = flow_style.dict(exclude_unset=True)
|
||||
for key, value in flow_data.items():
|
||||
if hasattr(db_flow_style, key) and value is not None:
|
||||
setattr(db_flow_style, key, value)
|
||||
session.add(db_flow_style)
|
||||
session.commit()
|
||||
session.refresh(db_flow_style)
|
||||
return db_flow_style
|
||||
|
||||
|
||||
@router.delete("/{flow_id}")
|
||||
def delete_flow_style(*, session: Session = Depends(get_session), flow_id: UUID):
|
||||
"""Delete a flow_style."""
|
||||
flow_style = session.get(FlowStyle, flow_id)
|
||||
if not flow_style:
|
||||
raise HTTPException(status_code=404, detail="FlowStyle not found")
|
||||
session.delete(flow_style)
|
||||
session.commit()
|
||||
return {"message": "FlowStyle deleted successfully"}
|
||||
|
|
@ -1,70 +1,101 @@
|
|||
from typing import List
|
||||
from uuid import UUID
|
||||
from langflow.settings import settings
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from langflow.api.utils import remove_api_keys
|
||||
from langflow.api.v1.schemas import FlowListCreate, FlowListRead
|
||||
from langflow.database.models.flow import (
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.flow import (
|
||||
Flow,
|
||||
FlowCreate,
|
||||
FlowRead,
|
||||
FlowReadWithStyle,
|
||||
FlowUpdate,
|
||||
)
|
||||
from langflow.database.base import get_session
|
||||
from sqlmodel import Session, select
|
||||
from langflow.services.database.models.user.user import User
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.services.utils import get_settings_manager
|
||||
import orjson
|
||||
from sqlmodel import Session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from fastapi import File, UploadFile
|
||||
import json
|
||||
|
||||
# build router
|
||||
router = APIRouter(prefix="/flows", tags=["Flows"])
|
||||
|
||||
|
||||
@router.post("/", response_model=FlowRead, status_code=201)
|
||||
def create_flow(*, session: Session = Depends(get_session), flow: FlowCreate):
|
||||
def create_flow(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
flow: FlowCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Create a new flow."""
|
||||
if flow.user_id is None:
|
||||
flow.user_id = current_user.id
|
||||
|
||||
db_flow = Flow.from_orm(flow)
|
||||
|
||||
session.add(db_flow)
|
||||
session.commit()
|
||||
session.refresh(db_flow)
|
||||
return db_flow
|
||||
|
||||
|
||||
@router.get("/", response_model=list[FlowReadWithStyle], status_code=200)
|
||||
def read_flows(*, session: Session = Depends(get_session)):
|
||||
@router.get("/", response_model=list[FlowRead], status_code=200)
|
||||
def read_flows(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Read all flows."""
|
||||
try:
|
||||
flows = session.exec(select(Flow)).all()
|
||||
flows = current_user.flows
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
return [jsonable_encoder(flow) for flow in flows]
|
||||
|
||||
|
||||
@router.get("/{flow_id}", response_model=FlowReadWithStyle, status_code=200)
|
||||
def read_flow(*, session: Session = Depends(get_session), flow_id: UUID):
|
||||
@router.get("/{flow_id}", response_model=FlowRead, status_code=200)
|
||||
def read_flow(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
flow_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Read a flow."""
|
||||
if flow := session.get(Flow, flow_id):
|
||||
return flow
|
||||
if user_flow := (
|
||||
session.query(Flow)
|
||||
.filter(Flow.id == flow_id)
|
||||
.filter(Flow.user_id == current_user.id)
|
||||
.first()
|
||||
):
|
||||
return user_flow
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
|
||||
|
||||
@router.patch("/{flow_id}", response_model=FlowRead, status_code=200)
|
||||
def update_flow(
|
||||
*, session: Session = Depends(get_session), flow_id: UUID, flow: FlowUpdate
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
flow_id: UUID,
|
||||
flow: FlowUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
settings_manager=Depends(get_settings_manager),
|
||||
):
|
||||
"""Update a flow."""
|
||||
|
||||
db_flow = session.get(Flow, flow_id)
|
||||
db_flow = read_flow(session=session, flow_id=flow_id, current_user=current_user)
|
||||
if not db_flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
flow_data = flow.dict(exclude_unset=True)
|
||||
if settings.REMOVE_API_KEYS:
|
||||
if settings_manager.settings.REMOVE_API_KEYS:
|
||||
flow_data = remove_api_keys(flow_data)
|
||||
for key, value in flow_data.items():
|
||||
setattr(db_flow, key, value)
|
||||
if value is not None:
|
||||
setattr(db_flow, key, value)
|
||||
session.add(db_flow)
|
||||
session.commit()
|
||||
session.refresh(db_flow)
|
||||
|
|
@ -72,9 +103,14 @@ def update_flow(
|
|||
|
||||
|
||||
@router.delete("/{flow_id}", status_code=200)
|
||||
def delete_flow(*, session: Session = Depends(get_session), flow_id: UUID):
|
||||
def delete_flow(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
flow_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Delete a flow."""
|
||||
flow = session.get(Flow, flow_id)
|
||||
flow = read_flow(session=session, flow_id=flow_id, current_user=current_user)
|
||||
if not flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
session.delete(flow)
|
||||
|
|
@ -86,10 +122,16 @@ def delete_flow(*, session: Session = Depends(get_session), flow_id: UUID):
|
|||
|
||||
|
||||
@router.post("/batch/", response_model=List[FlowRead], status_code=201)
|
||||
def create_flows(*, session: Session = Depends(get_session), flow_list: FlowListCreate):
|
||||
def create_flows(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
flow_list: FlowListCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Create multiple new flows."""
|
||||
db_flows = []
|
||||
for flow in flow_list.flows:
|
||||
flow.user_id = current_user.id
|
||||
db_flow = Flow.from_orm(flow)
|
||||
session.add(db_flow)
|
||||
db_flows.append(db_flow)
|
||||
|
|
@ -101,20 +143,31 @@ def create_flows(*, session: Session = Depends(get_session), flow_list: FlowList
|
|||
|
||||
@router.post("/upload/", response_model=List[FlowRead], status_code=201)
|
||||
async def upload_file(
|
||||
*, session: Session = Depends(get_session), file: UploadFile = 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 = json.loads(contents)
|
||||
data = orjson.loads(contents)
|
||||
if "flows" in data:
|
||||
flow_list = FlowListCreate(**data)
|
||||
else:
|
||||
flow_list = FlowListCreate(flows=[FlowCreate(**flow) for flow in data])
|
||||
return create_flows(session=session, flow_list=flow_list)
|
||||
# Now we set the user_id for all flows
|
||||
for flow in flow_list.flows:
|
||||
flow.user_id = current_user.id
|
||||
|
||||
return create_flows(session=session, flow_list=flow_list, current_user=current_user)
|
||||
|
||||
|
||||
@router.get("/download/", response_model=FlowListRead, status_code=200)
|
||||
async def download_file(*, session: Session = Depends(get_session)):
|
||||
async def download_file(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Download all flows as a file."""
|
||||
flows = read_flows(session=session)
|
||||
flows = read_flows(session=session, current_user=current_user)
|
||||
return FlowListRead(flows=flows)
|
||||
|
|
|
|||
63
src/backend/langflow/api/v1/login.py
Normal file
63
src/backend/langflow/api/v1/login.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from sqlmodel import Session
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.api.v1.schemas import Token
|
||||
from langflow.services.auth.utils import (
|
||||
authenticate_user,
|
||||
create_user_tokens,
|
||||
create_refresh_token,
|
||||
create_user_longterm_token,
|
||||
get_current_active_user,
|
||||
)
|
||||
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
router = APIRouter(tags=["Login"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login_to_get_access_token(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: Session = Depends(get_session),
|
||||
# _: Session = Depends(get_current_active_user)
|
||||
):
|
||||
if user := authenticate_user(form_data.username, form_data.password, db):
|
||||
return create_user_tokens(user_id=user.id, db=db, update_last_login=True)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/auto_login")
|
||||
async def auto_login(
|
||||
db: Session = Depends(get_session), settings_manager=Depends(get_settings_manager)
|
||||
):
|
||||
if settings_manager.auth_settings.AUTO_LOGIN:
|
||||
return create_user_longterm_token(db)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"message": "Auto login is disabled. Please enable it in the settings",
|
||||
"auto_login": False,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
async def refresh_token(
|
||||
token: str, current_user: Session = Depends(get_current_active_user)
|
||||
):
|
||||
if token:
|
||||
return create_refresh_token(token)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from langflow.database.models.flow import FlowCreate, FlowRead
|
||||
from uuid import UUID
|
||||
from langflow.services.database.models.api_key.api_key import ApiKeyRead
|
||||
from langflow.services.database.models.flow import FlowCreate, FlowRead
|
||||
from langflow.services.database.models.user import UserRead
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
import json
|
||||
|
||||
|
||||
class BuildStatus(Enum):
|
||||
|
|
@ -47,6 +51,7 @@ class ProcessResponse(BaseModel):
|
|||
"""Process response schema."""
|
||||
|
||||
result: dict
|
||||
session_id: Optional[str] = None
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
|
|
@ -115,7 +120,9 @@ class StreamData(BaseModel):
|
|||
data: dict
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"event: {self.event}\ndata: {json.dumps(self.data)}\n\n"
|
||||
return (
|
||||
f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n"
|
||||
)
|
||||
|
||||
|
||||
class CustomComponentCode(BaseModel):
|
||||
|
|
@ -133,3 +140,32 @@ class ComponentListCreate(BaseModel):
|
|||
|
||||
class ComponentListRead(BaseModel):
|
||||
flows: List[FlowRead]
|
||||
|
||||
|
||||
class UsersResponse(BaseModel):
|
||||
total_count: int
|
||||
users: List[UserRead]
|
||||
|
||||
|
||||
class ApiKeyResponse(BaseModel):
|
||||
id: str
|
||||
api_key: str
|
||||
name: str
|
||||
created_at: str
|
||||
last_used_at: str
|
||||
|
||||
|
||||
class ApiKeysResponse(BaseModel):
|
||||
total_count: int
|
||||
user_id: UUID
|
||||
api_keys: List[ApiKeyRead]
|
||||
|
||||
|
||||
class CreateApiKeyRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str
|
||||
|
|
|
|||
194
src/backend/langflow/api/v1/users.py
Normal file
194
src/backend/langflow/api/v1/users.py
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
from uuid import UUID
|
||||
from langflow.api.v1.schemas import UsersResponse
|
||||
from langflow.services.database.models.user import (
|
||||
User,
|
||||
UserCreate,
|
||||
UserRead,
|
||||
UserUpdate,
|
||||
)
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from sqlmodel import Session, select
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.services.auth.utils import (
|
||||
get_current_active_superuser,
|
||||
get_current_active_user,
|
||||
get_password_hash,
|
||||
verify_password,
|
||||
)
|
||||
from langflow.services.database.models.user.crud import (
|
||||
get_user_by_id,
|
||||
update_user,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["Users"], prefix="/users")
|
||||
|
||||
|
||||
@router.post("/", response_model=UserRead, status_code=201)
|
||||
def add_user(
|
||||
user: UserCreate,
|
||||
session: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""
|
||||
Add a new user to the database.
|
||||
"""
|
||||
new_user = User.from_orm(user)
|
||||
try:
|
||||
new_user.password = get_password_hash(user.password)
|
||||
|
||||
session.add(new_user)
|
||||
session.commit()
|
||||
session.refresh(new_user)
|
||||
except IntegrityError as e:
|
||||
session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=400, detail="This username is unavailable."
|
||||
) from e
|
||||
|
||||
return new_user
|
||||
|
||||
|
||||
@router.get("/whoami", response_model=UserRead)
|
||||
def read_current_user(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
) -> User:
|
||||
"""
|
||||
Retrieve the current user's data.
|
||||
"""
|
||||
return current_user
|
||||
|
||||
|
||||
@router.get("/", response_model=UsersResponse)
|
||||
def read_all_users(
|
||||
skip: int = 0,
|
||||
limit: int = 10,
|
||||
current_user: Session = Depends(get_current_active_superuser),
|
||||
session: Session = Depends(get_session),
|
||||
) -> UsersResponse:
|
||||
"""
|
||||
Retrieve a list of users from the database with pagination.
|
||||
"""
|
||||
query = select(User).offset(skip).limit(limit)
|
||||
users = session.execute(query).fetchall()
|
||||
|
||||
count_query = select(func.count()).select_from(User) # type: ignore
|
||||
total_count = session.execute(count_query).scalar()
|
||||
|
||||
return UsersResponse(
|
||||
total_count=total_count, # type: ignore
|
||||
users=[UserRead(**dict(user.User)) for user in users],
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/{user_id}", response_model=UserRead)
|
||||
def patch_user(
|
||||
user_id: UUID,
|
||||
user_update: UserUpdate,
|
||||
user: User = Depends(get_current_active_user),
|
||||
session: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""
|
||||
Update an existing user's data.
|
||||
"""
|
||||
if not user.is_superuser and user.id != user_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="You don't have the permission to update this user"
|
||||
)
|
||||
if user_update.password:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="You can't change your password here"
|
||||
)
|
||||
|
||||
if user_db := get_user_by_id(session, user_id):
|
||||
return update_user(user_db, user_update, session)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
|
||||
@router.patch("/{user_id}/reset-password", response_model=UserRead)
|
||||
def reset_password(
|
||||
user_id: UUID,
|
||||
user_update: UserUpdate,
|
||||
user: User = Depends(get_current_active_user),
|
||||
session: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""
|
||||
Reset a user's password.
|
||||
"""
|
||||
if user_id != user.id:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="You can't change another user's password"
|
||||
)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if verify_password(user_update.password, user.password):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="You can't use your current password"
|
||||
)
|
||||
new_password = get_password_hash(user_update.password)
|
||||
user.password = new_password
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@router.delete("/{user_id}", response_model=dict)
|
||||
def delete_user(
|
||||
user_id: UUID,
|
||||
current_user: User = Depends(get_current_active_superuser),
|
||||
session: Session = Depends(get_session),
|
||||
) -> dict:
|
||||
"""
|
||||
Delete a user from the database.
|
||||
"""
|
||||
if current_user.id == user_id:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="You can't delete your own user account"
|
||||
)
|
||||
elif not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="You don't have the permission to delete this user"
|
||||
)
|
||||
|
||||
user_db = session.query(User).filter(User.id == user_id).first()
|
||||
if not user_db:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
session.delete(user_db)
|
||||
session.commit()
|
||||
|
||||
return {"detail": "User deleted"}
|
||||
|
||||
|
||||
# TODO: REMOVE - Just for testing purposes
|
||||
@router.post("/super_user", response_model=User)
|
||||
def add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(
|
||||
session: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""
|
||||
Add a superuser for testing purposes.
|
||||
(This should be removed in production)
|
||||
"""
|
||||
new_user = User(
|
||||
username="superuser",
|
||||
password=get_password_hash("12345"),
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
last_login_at=None,
|
||||
)
|
||||
|
||||
try:
|
||||
session.add(new_user)
|
||||
session.commit()
|
||||
session.refresh(new_user)
|
||||
except IntegrityError as e:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=400, detail="User exists") from e
|
||||
|
||||
return new_user
|
||||
|
|
@ -8,7 +8,7 @@ from langflow.api.v1.base import (
|
|||
validate_prompt,
|
||||
)
|
||||
from langflow.template.field.base import TemplateField
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.validate import validate_code
|
||||
|
||||
# build router
|
||||
|
|
@ -31,7 +31,12 @@ def post_validate_code(code: Code):
|
|||
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,
|
||||
)
|
||||
old_custom_fields = get_old_custom_fields(prompt_request)
|
||||
|
||||
add_new_variables_to_template(input_variables, prompt_request)
|
||||
|
|
@ -53,6 +58,16 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
|
|||
|
||||
def get_old_custom_fields(prompt_request):
|
||||
try:
|
||||
if (
|
||||
len(prompt_request.frontend_node.custom_fields) == 1
|
||||
and prompt_request.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
|
||||
prompt_request.name = list(
|
||||
prompt_request.frontend_node.custom_fields.keys()
|
||||
)[0]
|
||||
|
||||
old_custom_fields = prompt_request.frontend_node.custom_fields[
|
||||
prompt_request.name
|
||||
].copy()
|
||||
|
|
|
|||
7
src/backend/langflow/cache/__init__.py
vendored
7
src/backend/langflow/cache/__init__.py
vendored
|
|
@ -1,7 +0,0 @@
|
|||
from langflow.cache.manager import cache_manager
|
||||
from langflow.cache.flow import InMemoryCache
|
||||
|
||||
__all__ = [
|
||||
"cache_manager",
|
||||
"InMemoryCache",
|
||||
]
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
from langflow import CustomComponent
|
||||
from typing import Optional
|
||||
from langchain.prompts import SystemMessagePromptTemplate
|
||||
from langchain.tools import Tool
|
||||
from langchain.schema.memory import BaseMemory
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
|
||||
from langchain.agents.agent import AgentExecutor
|
||||
from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
|
||||
from langchain.memory.token_buffer import ConversationTokenBufferMemory
|
||||
from langchain.prompts.chat import MessagesPlaceholder
|
||||
from langchain.agents.agent_toolkits.conversational_retrieval.openai_functions import (
|
||||
_get_default_system_message,
|
||||
)
|
||||
|
||||
|
||||
class ConversationalAgent(CustomComponent):
|
||||
display_name: str = "OpenAI Conversational Agent"
|
||||
description: str = "Conversational Agent that can use OpenAI's function calling API"
|
||||
|
||||
def build_config(self):
|
||||
openai_function_models = [
|
||||
"gpt-3.5-turbo-0613",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"gpt-4-0613",
|
||||
"gpt-4-32k-0613",
|
||||
]
|
||||
return {
|
||||
"tools": {"is_list": True, "display_name": "Tools"},
|
||||
"memory": {"display_name": "Memory"},
|
||||
"system_message": {"display_name": "System Message"},
|
||||
"max_token_limit": {"display_name": "Max Token Limit"},
|
||||
"model_name": {
|
||||
"display_name": "Model Name",
|
||||
"options": openai_function_models,
|
||||
"value": openai_function_models[0],
|
||||
},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
model_name: str,
|
||||
openai_api_key: str,
|
||||
tools: Tool,
|
||||
openai_api_base: Optional[str] = None,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
system_message: Optional[SystemMessagePromptTemplate] = None,
|
||||
max_token_limit: int = 2000,
|
||||
) -> AgentExecutor:
|
||||
llm = ChatOpenAI(
|
||||
model=model_name,
|
||||
openai_api_key=openai_api_key,
|
||||
openai_api_base=openai_api_base,
|
||||
)
|
||||
if not memory:
|
||||
memory_key = "chat_history"
|
||||
memory = ConversationTokenBufferMemory(
|
||||
memory_key=memory_key,
|
||||
return_messages=True,
|
||||
output_key="output",
|
||||
llm=llm,
|
||||
max_token_limit=max_token_limit,
|
||||
)
|
||||
else:
|
||||
memory_key = memory.memory_key # type: ignore
|
||||
|
||||
_system_message = system_message or _get_default_system_message()
|
||||
prompt = OpenAIFunctionsAgent.create_prompt(
|
||||
system_message=_system_message, # type: ignore
|
||||
extra_prompt_messages=[MessagesPlaceholder(variable_name=memory_key)],
|
||||
)
|
||||
agent = OpenAIFunctionsAgent(
|
||||
llm=llm, tools=tools, prompt=prompt # type: ignore
|
||||
)
|
||||
return AgentExecutor(
|
||||
agent=agent,
|
||||
tools=tools, # type: ignore
|
||||
memory=memory,
|
||||
verbose=True,
|
||||
return_intermediate_steps=True,
|
||||
)
|
||||
|
|
@ -16,17 +16,14 @@ class PromptRunner(CustomComponent):
|
|||
"info": "Make sure the prompt has all variables filled.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
"inputs": {"field_type": "code"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
llm: BaseLLM,
|
||||
prompt: PromptTemplate,
|
||||
self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {}
|
||||
) -> Document:
|
||||
chain = prompt | llm
|
||||
# The input is an empty dict because the prompt is already filled
|
||||
result = chain.invoke({})
|
||||
result = chain.invoke(input=inputs)
|
||||
if hasattr(result, "content"):
|
||||
result = result.content
|
||||
self.repr_value = result
|
||||
42
src/backend/langflow/components/llms/HuggingFaceEndpoints.py
Normal file
42
src/backend/langflow/components/llms/HuggingFaceEndpoints.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.llms import HuggingFaceEndpoint
|
||||
from langchain.llms.base import BaseLLM
|
||||
|
||||
|
||||
class HuggingFaceEndpointsComponent(CustomComponent):
|
||||
display_name: str = "Hugging Face Inference API"
|
||||
description: str = "LLM model from Hugging Face Inference API."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"endpoint_url": {"display_name": "Endpoint URL", "password": True},
|
||||
"task": {
|
||||
"display_name": "Task",
|
||||
"type": "select",
|
||||
"options": ["text2text-generation", "text-generation", "summarization"],
|
||||
},
|
||||
"huggingfacehub_api_token": {"display_name": "API token", "password": True},
|
||||
"model_kwargs": {
|
||||
"display_name": "Model Keyword Arguments",
|
||||
"field_type": "code",
|
||||
},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
endpoint_url: str,
|
||||
task="text2text-generation",
|
||||
huggingfacehub_api_token: Optional[str] = None,
|
||||
model_kwargs: Optional[dict] = None,
|
||||
) -> BaseLLM:
|
||||
try:
|
||||
output = HuggingFaceEndpoint(
|
||||
endpoint_url=endpoint_url,
|
||||
task=task,
|
||||
huggingfacehub_api_token=huggingfacehub_api_token,
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValueError("Could not connect to HuggingFace Endpoints API.") from e
|
||||
return output
|
||||
28
src/backend/langflow/components/retrievers/MetalRetriever.py
Normal file
28
src/backend/langflow/components/retrievers/MetalRetriever.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.retrievers import MetalRetriever
|
||||
from langchain.schema import BaseRetriever
|
||||
from metal_sdk.metal import Metal # type: ignore
|
||||
|
||||
|
||||
class MetalRetrieverComponent(CustomComponent):
|
||||
display_name: str = "Metal Retriever"
|
||||
description: str = "Retriever that uses the Metal API."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"api_key": {"display_name": "API Key", "password": True},
|
||||
"client_id": {"display_name": "Client ID", "password": True},
|
||||
"index_id": {"display_name": "Index ID"},
|
||||
"params": {"display_name": "Parameters"},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(
|
||||
self, api_key: str, client_id: str, index_id: str, params: Optional[dict] = None
|
||||
) -> BaseRetriever:
|
||||
try:
|
||||
metal = Metal(api_key=api_key, client_id=client_id, index_id=index_id)
|
||||
except Exception as e:
|
||||
raise ValueError("Could not connect to Metal API.") from e
|
||||
return MetalRetriever(client=metal, params=params or {})
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.text_splitter import Language
|
||||
from langchain.schema import Document
|
||||
|
||||
|
||||
class LanguageRecursiveTextSplitterComponent(CustomComponent):
|
||||
display_name: str = "Language Recursive Text Splitter"
|
||||
description: str = "Split text into chunks of a specified length based on language."
|
||||
documentation: str = "https://docs.langflow.org/components/text-splitters#languagerecursivetextsplitter"
|
||||
|
||||
def build_config(self):
|
||||
options = [x.value for x in Language]
|
||||
return {
|
||||
"documents": {
|
||||
"display_name": "Documents",
|
||||
"info": "The documents to split.",
|
||||
},
|
||||
"separator_type": {
|
||||
"display_name": "Separator Type",
|
||||
"info": "The type of separator to use.",
|
||||
"field_type": "str",
|
||||
"options": options,
|
||||
"value": "Python",
|
||||
},
|
||||
"separators": {
|
||||
"display_name": "Separators",
|
||||
"info": "The characters to split on.",
|
||||
"is_list": True,
|
||||
},
|
||||
"chunk_size": {
|
||||
"display_name": "Chunk Size",
|
||||
"info": "The maximum length of each chunk.",
|
||||
"field_type": "int",
|
||||
"value": 1000,
|
||||
},
|
||||
"chunk_overlap": {
|
||||
"display_name": "Chunk Overlap",
|
||||
"info": "The amount of overlap between chunks.",
|
||||
"field_type": "int",
|
||||
"value": 200,
|
||||
},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
documents: list[Document],
|
||||
chunk_size: Optional[int] = 1000,
|
||||
chunk_overlap: Optional[int] = 200,
|
||||
separator_type: Optional[str] = "Python",
|
||||
) -> list[Document]:
|
||||
"""
|
||||
Split text into chunks of a specified length.
|
||||
|
||||
Args:
|
||||
separators (list[str]): The characters to split on.
|
||||
chunk_size (int): The maximum length of each chunk.
|
||||
chunk_overlap (int): The amount of overlap between chunks.
|
||||
length_function (function): The function to use to calculate the length of the text.
|
||||
|
||||
Returns:
|
||||
list[str]: The chunks of text.
|
||||
"""
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
# Make sure chunk_size and chunk_overlap are ints
|
||||
if isinstance(chunk_size, str):
|
||||
chunk_size = int(chunk_size)
|
||||
if isinstance(chunk_overlap, str):
|
||||
chunk_overlap = int(chunk_overlap)
|
||||
|
||||
splitter = RecursiveCharacterTextSplitter.from_language(
|
||||
language=Language(separator_type),
|
||||
chunk_size=chunk_size,
|
||||
chunk_overlap=chunk_overlap,
|
||||
)
|
||||
|
||||
docs = splitter.split_documents(documents)
|
||||
return docs
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.schema import Document
|
||||
from langflow.utils.util import build_loader_repr_from_documents
|
||||
|
||||
|
||||
class RecursiveCharacterTextSplitterComponent(CustomComponent):
|
||||
display_name: str = "Recursive Character Text Splitter"
|
||||
description: str = "Split text into chunks of a specified length."
|
||||
documentation: str = "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"documents": {
|
||||
"display_name": "Documents",
|
||||
"info": "The documents to split.",
|
||||
},
|
||||
"separators": {
|
||||
"display_name": "Separators",
|
||||
"info": 'The characters to split on.\nIf left empty defaults to ["\\n\\n", "\\n", " ", ""].',
|
||||
"is_list": True,
|
||||
},
|
||||
"chunk_size": {
|
||||
"display_name": "Chunk Size",
|
||||
"info": "The maximum length of each chunk.",
|
||||
"field_type": "int",
|
||||
"value": 1000,
|
||||
},
|
||||
"chunk_overlap": {
|
||||
"display_name": "Chunk Overlap",
|
||||
"info": "The amount of overlap between chunks.",
|
||||
"field_type": "int",
|
||||
"value": 200,
|
||||
},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
documents: list[Document],
|
||||
separators: Optional[list[str]] = None,
|
||||
chunk_size: Optional[int] = 1000,
|
||||
chunk_overlap: Optional[int] = 200,
|
||||
) -> list[Document]:
|
||||
"""
|
||||
Split text into chunks of a specified length.
|
||||
|
||||
Args:
|
||||
separators (list[str]): The characters to split on.
|
||||
chunk_size (int): The maximum length of each chunk.
|
||||
chunk_overlap (int): The amount of overlap between chunks.
|
||||
length_function (function): The function to use to calculate the length of the text.
|
||||
|
||||
Returns:
|
||||
list[str]: The chunks of text.
|
||||
"""
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
if separators == "":
|
||||
separators = None
|
||||
elif separators:
|
||||
# check if the separators list has escaped characters
|
||||
# if there are escaped characters, unescape them
|
||||
separators = [x.encode().decode("unicode-escape") for x in separators]
|
||||
|
||||
# Make sure chunk_size and chunk_overlap are ints
|
||||
if isinstance(chunk_size, str):
|
||||
chunk_size = int(chunk_size)
|
||||
if isinstance(chunk_overlap, str):
|
||||
chunk_overlap = int(chunk_overlap)
|
||||
splitter = RecursiveCharacterTextSplitter(
|
||||
separators=separators,
|
||||
chunk_size=chunk_size,
|
||||
chunk_overlap=chunk_overlap,
|
||||
)
|
||||
|
||||
docs = splitter.split_documents(documents)
|
||||
self.repr_value = build_loader_repr_from_documents(docs)
|
||||
return docs
|
||||
56
src/backend/langflow/components/toolkits/Metaphor.py
Normal file
56
src/backend/langflow/components/toolkits/Metaphor.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from typing import List, Union
|
||||
from langflow import CustomComponent
|
||||
|
||||
from metaphor_python import Metaphor # type: ignore
|
||||
from langchain.tools import Tool
|
||||
from langchain.agents import tool
|
||||
from langchain.agents.agent_toolkits.base import BaseToolkit
|
||||
|
||||
|
||||
class MetaphorToolkit(CustomComponent):
|
||||
display_name: str = "Metaphor"
|
||||
description: str = "Metaphor Toolkit"
|
||||
documentation = (
|
||||
"https://python.langchain.com/docs/integrations/tools/metaphor_search"
|
||||
)
|
||||
beta = True
|
||||
# api key should be password = True
|
||||
field_config = {
|
||||
"metaphor_api_key": {"display_name": "Metaphor API Key", "password": True},
|
||||
"code": {"advanced": True},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
metaphor_api_key: str,
|
||||
use_autoprompt: bool = True,
|
||||
search_num_results: int = 5,
|
||||
similar_num_results: int = 5,
|
||||
) -> Union[Tool, BaseToolkit]:
|
||||
# If documents, then we need to create a Vectara instance using .from_documents
|
||||
client = Metaphor(api_key=metaphor_api_key)
|
||||
|
||||
@tool
|
||||
def search(query: str):
|
||||
"""Call search engine with a query."""
|
||||
return client.search(
|
||||
query, use_autoprompt=use_autoprompt, num_results=search_num_results
|
||||
)
|
||||
|
||||
@tool
|
||||
def get_contents(ids: List[str]):
|
||||
"""Get contents of a webpage.
|
||||
|
||||
The ids passed in should be a list of ids as fetched from `search`.
|
||||
"""
|
||||
return client.get_contents(ids)
|
||||
|
||||
@tool
|
||||
def find_similar(url: str):
|
||||
"""Get search results similar to a given URL.
|
||||
|
||||
The url passed in should be a URL returned from `search`
|
||||
"""
|
||||
return client.find_similar(url, num_results=similar_num_results)
|
||||
|
||||
return [search, get_contents, find_similar] # type: ignore
|
||||
0
src/backend/langflow/components/toolkits/__init__.py
Normal file
0
src/backend/langflow/components/toolkits/__init__.py
Normal file
75
src/backend/langflow/components/utilities/GetRequest.py
Normal file
75
src/backend/langflow/components/utilities/GetRequest.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain.schema import Document
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
import requests
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class GetRequest(CustomComponent):
|
||||
display_name: str = "GET Request"
|
||||
description: str = "Make a GET request to the given URL."
|
||||
output_types: list[str] = ["Document"]
|
||||
documentation: str = "https://docs.langflow.org/components/utilities#get-request"
|
||||
beta = True
|
||||
field_config = {
|
||||
"url": {
|
||||
"display_name": "URL",
|
||||
"info": "The URL to make the request to",
|
||||
"is_list": True,
|
||||
},
|
||||
"headers": {
|
||||
"display_name": "Headers",
|
||||
"info": "The headers to send with the request.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
"timeout": {
|
||||
"display_name": "Timeout",
|
||||
"field_type": "int",
|
||||
"info": "The timeout to use for the request.",
|
||||
"value": 5,
|
||||
},
|
||||
}
|
||||
|
||||
def get_document(
|
||||
self, session: requests.Session, url: str, headers: Optional[dict], timeout: int
|
||||
) -> Document:
|
||||
try:
|
||||
response = session.get(url, headers=headers, timeout=int(timeout))
|
||||
try:
|
||||
response_json = response.json()
|
||||
result = orjson_dumps(response_json, indent_2=False)
|
||||
except Exception:
|
||||
result = response.text
|
||||
self.repr_value = result
|
||||
return Document(
|
||||
page_content=result,
|
||||
metadata={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": response.status_code,
|
||||
},
|
||||
)
|
||||
except requests.Timeout:
|
||||
return Document(
|
||||
page_content="Request Timed Out",
|
||||
metadata={"source": url, "headers": headers, "status_code": 408},
|
||||
)
|
||||
except Exception as exc:
|
||||
return Document(
|
||||
page_content=str(exc),
|
||||
metadata={"source": url, "headers": headers, "status_code": 500},
|
||||
)
|
||||
|
||||
def build(
|
||||
self,
|
||||
url: str,
|
||||
headers: Optional[dict] = None,
|
||||
timeout: int = 5,
|
||||
) -> list[Document]:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
urls = url if isinstance(url, list) else [url]
|
||||
with requests.Session() as session:
|
||||
documents = [self.get_document(session, u, headers, timeout) for u in urls]
|
||||
self.repr_value = documents
|
||||
return documents
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
### JSON Document Builder
|
||||
|
||||
# Build a Document containing a JSON object using a key and another Document page content.
|
||||
|
||||
# **Params**
|
||||
|
||||
# - **Key:** The key to use for the JSON object.
|
||||
# - **Document:** The Document page to use for the JSON object.
|
||||
|
||||
# **Output**
|
||||
|
||||
# - **Document:** The Document containing the JSON object.
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langchain.schema import Document
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
|
||||
|
||||
class JSONDocumentBuilder(CustomComponent):
|
||||
display_name: str = "JSON Document Builder"
|
||||
description: str = "Build a Document containing a JSON object using a key and another Document page content."
|
||||
output_types: list[str] = ["Document"]
|
||||
beta = True
|
||||
documentation: str = (
|
||||
"https://docs.langflow.org/components/utilities#json-document-builder"
|
||||
)
|
||||
|
||||
field_config = {
|
||||
"key": {"display_name": "Key"},
|
||||
"document": {"display_name": "Document"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
key: str,
|
||||
document: Document,
|
||||
) -> Document:
|
||||
documents = None
|
||||
if isinstance(document, list):
|
||||
documents = [
|
||||
Document(
|
||||
page_content=orjson_dumps({key: doc.page_content}, indent_2=False)
|
||||
)
|
||||
for doc in document
|
||||
]
|
||||
elif isinstance(document, Document):
|
||||
documents = Document(
|
||||
page_content=orjson_dumps({key: document.page_content}, indent_2=False)
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Expected Document or list of Documents, got {type(document)}"
|
||||
)
|
||||
self.repr_value = documents
|
||||
return documents
|
||||
80
src/backend/langflow/components/utilities/PostRequest.py
Normal file
80
src/backend/langflow/components/utilities/PostRequest.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain.schema import Document
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
import requests
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class PostRequest(CustomComponent):
|
||||
display_name: str = "POST Request"
|
||||
description: str = "Make a POST request to the given URL."
|
||||
output_types: list[str] = ["Document"]
|
||||
documentation: str = "https://docs.langflow.org/components/utilities#post-request"
|
||||
beta = True
|
||||
field_config = {
|
||||
"url": {"display_name": "URL", "info": "The URL to make the request to."},
|
||||
"headers": {
|
||||
"display_name": "Headers",
|
||||
"info": "The headers to send with the request.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
"document": {"display_name": "Document"},
|
||||
}
|
||||
|
||||
def post_document(
|
||||
self,
|
||||
session: requests.Session,
|
||||
document: Document,
|
||||
url: str,
|
||||
headers: Optional[dict] = None,
|
||||
) -> Document:
|
||||
try:
|
||||
response = session.post(url, headers=headers, data=document.page_content)
|
||||
try:
|
||||
response_json = response.json()
|
||||
result = orjson_dumps(response_json, indent_2=False)
|
||||
except Exception:
|
||||
result = response.text
|
||||
self.repr_value = result
|
||||
return Document(
|
||||
page_content=result,
|
||||
metadata={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": response,
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
return Document(
|
||||
page_content=str(exc),
|
||||
metadata={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": 500,
|
||||
},
|
||||
)
|
||||
|
||||
def build(
|
||||
self,
|
||||
document: Document,
|
||||
url: str,
|
||||
headers: Optional[dict] = None,
|
||||
) -> list[Document]:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
if not isinstance(document, list) and isinstance(document, Document):
|
||||
documents: list[Document] = [document]
|
||||
elif isinstance(document, list) and all(
|
||||
isinstance(doc, Document) for doc in document
|
||||
):
|
||||
documents = document
|
||||
else:
|
||||
raise ValueError("document must be a Document or a list of Documents")
|
||||
|
||||
with requests.Session() as session:
|
||||
documents = [
|
||||
self.post_document(session, doc, url, headers) for doc in documents
|
||||
]
|
||||
self.repr_value = documents
|
||||
return documents
|
||||
94
src/backend/langflow/components/utilities/UpdateRequest.py
Normal file
94
src/backend/langflow/components/utilities/UpdateRequest.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from typing import List, Optional
|
||||
import requests
|
||||
from langflow import CustomComponent
|
||||
from langchain.schema import Document
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
|
||||
|
||||
class UpdateRequest(CustomComponent):
|
||||
display_name: str = "Update Request"
|
||||
description: str = "Make a PATCH request to the given URL."
|
||||
output_types: list[str] = ["Document"]
|
||||
documentation: str = "https://docs.langflow.org/components/utilities#update-request"
|
||||
beta = True
|
||||
field_config = {
|
||||
"url": {"display_name": "URL", "info": "The URL to make the request to."},
|
||||
"headers": {
|
||||
"display_name": "Headers",
|
||||
"field_type": "NestedDict",
|
||||
"info": "The headers to send with the request.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
"document": {"display_name": "Document"},
|
||||
"method": {
|
||||
"display_name": "Method",
|
||||
"field_type": "str",
|
||||
"info": "The HTTP method to use.",
|
||||
"options": ["PATCH", "PUT"],
|
||||
"value": "PATCH",
|
||||
},
|
||||
}
|
||||
|
||||
def update_document(
|
||||
self,
|
||||
session: requests.Session,
|
||||
document: Document,
|
||||
url: str,
|
||||
headers: Optional[dict] = None,
|
||||
method: str = "PATCH",
|
||||
) -> Document:
|
||||
try:
|
||||
if method == "PATCH":
|
||||
response = session.patch(
|
||||
url, headers=headers, data=document.page_content
|
||||
)
|
||||
elif method == "PUT":
|
||||
response = session.put(url, headers=headers, data=document.page_content)
|
||||
else:
|
||||
raise ValueError(f"Unsupported method: {method}")
|
||||
try:
|
||||
response_json = response.json()
|
||||
result = orjson_dumps(response_json, indent_2=False)
|
||||
except Exception:
|
||||
result = response.text
|
||||
self.repr_value = result
|
||||
return Document(
|
||||
page_content=result,
|
||||
metadata={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": response.status_code,
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
return Document(
|
||||
page_content=str(exc),
|
||||
metadata={"source": url, "headers": headers, "status_code": 500},
|
||||
)
|
||||
|
||||
def build(
|
||||
self,
|
||||
method: str,
|
||||
document: Document,
|
||||
url: str,
|
||||
headers: Optional[dict] = None,
|
||||
) -> List[Document]:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
if not isinstance(document, list) and isinstance(document, Document):
|
||||
documents: list[Document] = [document]
|
||||
elif isinstance(document, list) and all(
|
||||
isinstance(doc, Document) for doc in document
|
||||
):
|
||||
documents = document
|
||||
else:
|
||||
raise ValueError("document must be a Document or a list of Documents")
|
||||
|
||||
with requests.Session() as session:
|
||||
documents = [
|
||||
self.update_document(session, doc, url, headers, method)
|
||||
for doc in documents
|
||||
]
|
||||
self.repr_value = documents
|
||||
return documents
|
||||
109
src/backend/langflow/components/vectorstores/Chroma.py
Normal file
109
src/backend/langflow/components/vectorstores/Chroma.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
from typing import Optional, Union
|
||||
from langflow import CustomComponent
|
||||
|
||||
from langchain.vectorstores import Chroma
|
||||
from langchain.schema import Document
|
||||
from langchain.vectorstores.base import VectorStore
|
||||
from langchain.schema import BaseRetriever
|
||||
from langchain.embeddings.base import Embeddings
|
||||
import chromadb # type: ignore
|
||||
|
||||
|
||||
class ChromaComponent(CustomComponent):
|
||||
"""
|
||||
A custom component for implementing a Vector Store using Chroma.
|
||||
"""
|
||||
|
||||
display_name: str = "Chroma (Custom Component)"
|
||||
description: str = "Implementation of Vector Store using Chroma"
|
||||
documentation = "https://python.langchain.com/docs/integrations/vectorstores/chroma"
|
||||
beta = True
|
||||
|
||||
def build_config(self):
|
||||
"""
|
||||
Builds the configuration for the component.
|
||||
|
||||
Returns:
|
||||
- dict: A dictionary containing the configuration options for the component.
|
||||
"""
|
||||
return {
|
||||
"collection_name": {"display_name": "Collection Name", "value": "langflow"},
|
||||
"persist": {"display_name": "Persist"},
|
||||
"persist_directory": {"display_name": "Persist Directory"},
|
||||
"code": {"show": False, "display_name": "Code"},
|
||||
"documents": {"display_name": "Documents", "is_list": True},
|
||||
"embedding": {"display_name": "Embedding"},
|
||||
"chroma_server_cors_allow_origins": {
|
||||
"display_name": "Server CORS Allow Origins",
|
||||
"advanced": True,
|
||||
},
|
||||
"chroma_server_host": {"display_name": "Server Host", "advanced": True},
|
||||
"chroma_server_port": {"display_name": "Server Port", "advanced": True},
|
||||
"chroma_server_grpc_port": {
|
||||
"display_name": "Server gRPC Port",
|
||||
"advanced": True,
|
||||
},
|
||||
"chroma_server_ssl_enabled": {
|
||||
"display_name": "Server SSL Enabled",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
collection_name: str,
|
||||
persist: bool,
|
||||
chroma_server_ssl_enabled: bool,
|
||||
persist_directory: Optional[str] = None,
|
||||
embedding: Optional[Embeddings] = None,
|
||||
documents: Optional[Document] = None,
|
||||
chroma_server_cors_allow_origins: Optional[str] = None,
|
||||
chroma_server_host: Optional[str] = None,
|
||||
chroma_server_port: Optional[int] = None,
|
||||
chroma_server_grpc_port: Optional[int] = None,
|
||||
) -> Union[VectorStore, BaseRetriever]:
|
||||
"""
|
||||
Builds the Vector Store or BaseRetriever object.
|
||||
|
||||
Args:
|
||||
- collection_name (str): The name of the collection.
|
||||
- persist_directory (Optional[str]): The directory to persist the Vector Store to.
|
||||
- chroma_server_ssl_enabled (bool): Whether to enable SSL for the Chroma server.
|
||||
- persist (bool): Whether to persist the Vector Store or not.
|
||||
- embedding (Optional[Embeddings]): The embeddings to use for the Vector Store.
|
||||
- documents (Optional[Document]): The documents to use for the Vector Store.
|
||||
- chroma_server_cors_allow_origins (Optional[str]): The CORS allow origins for the Chroma server.
|
||||
- chroma_server_host (Optional[str]): The host for the Chroma server.
|
||||
- chroma_server_port (Optional[int]): The port for the Chroma server.
|
||||
- chroma_server_grpc_port (Optional[int]): The gRPC port for the Chroma server.
|
||||
|
||||
Returns:
|
||||
- Union[VectorStore, BaseRetriever]: The Vector Store or BaseRetriever object.
|
||||
"""
|
||||
|
||||
# Chroma settings
|
||||
chroma_settings = None
|
||||
|
||||
if chroma_server_host is not None:
|
||||
chroma_settings = chromadb.config.Settings(
|
||||
chroma_server_cors_allow_origins=chroma_server_cors_allow_origins
|
||||
or None,
|
||||
chroma_server_host=chroma_server_host,
|
||||
chroma_server_port=chroma_server_port or None,
|
||||
chroma_server_grpc_port=chroma_server_grpc_port or None,
|
||||
chroma_server_ssl_enabled=chroma_server_ssl_enabled,
|
||||
)
|
||||
|
||||
# If documents, then we need to create a Chroma instance using .from_documents
|
||||
if documents is not None and embedding is not None:
|
||||
return Chroma.from_documents(
|
||||
documents=documents, # type: ignore
|
||||
persist_directory=persist_directory if persist else None,
|
||||
collection_name=collection_name,
|
||||
embedding=embedding,
|
||||
client_settings=chroma_settings,
|
||||
)
|
||||
|
||||
return Chroma(
|
||||
persist_directory=persist_directory, client_settings=chroma_settings
|
||||
)
|
||||
50
src/backend/langflow/components/vectorstores/Vectara.py
Normal file
50
src/backend/langflow/components/vectorstores/Vectara.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from typing import Optional, Union
|
||||
from langflow import CustomComponent
|
||||
|
||||
from langchain.vectorstores import Vectara
|
||||
from langchain.schema import Document
|
||||
from langchain.vectorstores.base import VectorStore
|
||||
from langchain.schema import BaseRetriever
|
||||
from langchain.embeddings.base import Embeddings
|
||||
|
||||
|
||||
class VectaraComponent(CustomComponent):
|
||||
display_name: str = "Vectara"
|
||||
description: str = "Implementation of Vector Store using Vectara"
|
||||
documentation = (
|
||||
"https://python.langchain.com/docs/integrations/vectorstores/vectara"
|
||||
)
|
||||
beta = True
|
||||
# api key should be password = True
|
||||
field_config = {
|
||||
"vectara_customer_id": {"display_name": "Vectara Customer ID"},
|
||||
"vectara_corpus_id": {"display_name": "Vectara Corpus ID"},
|
||||
"vectara_api_key": {"display_name": "Vectara API Key", "password": True},
|
||||
"code": {"show": False},
|
||||
"documents": {"display_name": "Documents"},
|
||||
"embedding": {"display_name": "Embedding"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
vectara_customer_id: str,
|
||||
vectara_corpus_id: str,
|
||||
vectara_api_key: str,
|
||||
embedding: Optional[Embeddings] = None,
|
||||
documents: Optional[Document] = None,
|
||||
) -> Union[VectorStore, BaseRetriever]:
|
||||
# If documents, then we need to create a Vectara instance using .from_documents
|
||||
if documents is not None and embedding is not None:
|
||||
return Vectara.from_documents(
|
||||
documents=documents, # type: ignore
|
||||
vectara_customer_id=vectara_customer_id,
|
||||
vectara_corpus_id=vectara_corpus_id,
|
||||
vectara_api_key=vectara_api_key,
|
||||
embedding=embedding,
|
||||
)
|
||||
|
||||
return Vectara(
|
||||
vectara_customer_id=vectara_customer_id,
|
||||
vectara_corpus_id=vectara_corpus_id,
|
||||
vectara_api_key=vectara_api_key,
|
||||
)
|
||||
0
src/backend/langflow/components/vectorstores/__init__.py
Normal file
0
src/backend/langflow/components/vectorstores/__init__.py
Normal file
|
|
@ -104,6 +104,8 @@ embeddings:
|
|||
documentation: "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/sentence_transformers"
|
||||
CohereEmbeddings:
|
||||
documentation: "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/cohere"
|
||||
VertexAIEmbeddings:
|
||||
documentation: "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/google_vertex_ai_palm"
|
||||
llms:
|
||||
OpenAI:
|
||||
documentation: "https://python.langchain.com/docs/modules/model_io/models/llms/integrations/openai"
|
||||
|
|
@ -127,8 +129,8 @@ llms:
|
|||
# There's a bug in this component deactivating until we get it sorted: _language_models.py", line 804, in send_message
|
||||
# is_blocked=safety_attributes.get("blocked", False),
|
||||
# AttributeError: 'list' object has no attribute 'get'
|
||||
# ChatVertexAI:
|
||||
# documentation: "https://python.langchain.com/docs/modules/model_io/models/chat/integrations/google_vertex_ai_palm"
|
||||
ChatVertexAI:
|
||||
documentation: "https://python.langchain.com/docs/modules/model_io/models/chat/integrations/google_vertex_ai_palm"
|
||||
###
|
||||
memories:
|
||||
# https://github.com/supabase-community/supabase-py/issues/482
|
||||
|
|
@ -169,8 +171,6 @@ prompts:
|
|||
textsplitters:
|
||||
CharacterTextSplitter:
|
||||
documentation: "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/character_text_splitter"
|
||||
RecursiveCharacterTextSplitter:
|
||||
documentation: "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/recursive_text_splitter"
|
||||
toolkits:
|
||||
OpenAPIToolkit:
|
||||
documentation: ""
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
from contextlib import contextmanager
|
||||
import os
|
||||
|
||||
from sqlmodel import SQLModel, Session, create_engine
|
||||
from langflow.utils.logger import logger
|
||||
|
||||
|
||||
class Engine:
|
||||
_instance = None
|
||||
|
||||
@classmethod
|
||||
def get(cls):
|
||||
logger.debug("Getting database engine")
|
||||
if cls._instance is None:
|
||||
cls.create()
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def create(cls):
|
||||
logger.debug("Creating database engine")
|
||||
from langflow.settings import settings
|
||||
|
||||
if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"):
|
||||
settings.DATABASE_URL = langflow_database_url
|
||||
logger.debug("Using LANGFLOW_DATABASE_URL")
|
||||
|
||||
if settings.DATABASE_URL and settings.DATABASE_URL.startswith("sqlite"):
|
||||
connect_args = {"check_same_thread": False}
|
||||
else:
|
||||
connect_args = {}
|
||||
if not settings.DATABASE_URL:
|
||||
raise RuntimeError("No database_url provided")
|
||||
cls._instance = create_engine(settings.DATABASE_URL, connect_args=connect_args)
|
||||
|
||||
@classmethod
|
||||
def update(cls):
|
||||
logger.debug("Updating database engine")
|
||||
cls._instance = None
|
||||
cls.create()
|
||||
|
||||
|
||||
def create_db_and_tables():
|
||||
logger.debug("Creating database and tables")
|
||||
try:
|
||||
SQLModel.metadata.create_all(Engine.get())
|
||||
except Exception as exc:
|
||||
logger.error(f"Error creating database and tables: {exc}")
|
||||
raise RuntimeError("Error creating database and tables") from exc
|
||||
# Now check if the table Flow exists, if not, something went wrong
|
||||
# and we need to create the tables again.
|
||||
from sqlalchemy import inspect
|
||||
|
||||
inspector = inspect(Engine.get())
|
||||
if "flow" not in inspector.get_table_names():
|
||||
logger.error("Something went wrong creating the database and tables.")
|
||||
logger.error("Please check your database settings.")
|
||||
|
||||
raise RuntimeError("Something went wrong creating the database and tables.")
|
||||
else:
|
||||
logger.debug("Database and tables created successfully")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def session_getter():
|
||||
try:
|
||||
session = Session(Engine.get())
|
||||
yield session
|
||||
except Exception as e:
|
||||
print("Session rollback because of exception:", e)
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def get_session():
|
||||
with session_getter() as session:
|
||||
yield session
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
from sqlmodel import SQLModel
|
||||
import orjson
|
||||
|
||||
|
||||
def orjson_dumps(v, *, default):
|
||||
# orjson.dumps returns bytes, to match standard json.dumps we need to decode
|
||||
return orjson.dumps(v, default=default).decode()
|
||||
|
||||
|
||||
class SQLModelSerializable(SQLModel):
|
||||
class Config:
|
||||
orm_mode = True
|
||||
json_loads = orjson.loads
|
||||
json_dumps = orjson_dumps
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
# Path: src/backend/langflow/database/models/flowstyle.py
|
||||
|
||||
from langflow.database.models.base import SQLModelSerializable
|
||||
from sqlmodel import Field, Relationship
|
||||
from uuid import UUID, uuid4
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.database.models.flow import Flow
|
||||
|
||||
|
||||
class FlowStyleBase(SQLModelSerializable):
|
||||
color: str
|
||||
emoji: str
|
||||
flow_id: UUID = Field(default=None, foreign_key="flow.id")
|
||||
|
||||
|
||||
class FlowStyle(FlowStyleBase, table=True):
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||
flow: "Flow" = Relationship(back_populates="style")
|
||||
|
||||
|
||||
class FlowStyleUpdate(SQLModelSerializable):
|
||||
color: Optional[str] = None
|
||||
emoji: Optional[str] = None
|
||||
|
||||
|
||||
class FlowStyleCreate(FlowStyleBase):
|
||||
pass
|
||||
|
||||
|
||||
class FlowStyleRead(FlowStyleBase):
|
||||
id: UUID
|
||||
3
src/backend/langflow/field_typing/__init__.py
Normal file
3
src/backend/langflow/field_typing/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .base import NestedDict
|
||||
|
||||
__all__ = ["NestedDict"]
|
||||
4
src/backend/langflow/field_typing/base.py
Normal file
4
src/backend/langflow/field_typing/base.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from typing import Union, Dict
|
||||
|
||||
# Type alias for more complex dicts
|
||||
NestedDict = Dict[str, Union[str, Dict]]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -40,7 +40,6 @@ class Edge:
|
|||
if no_matched_type:
|
||||
logger.debug(self.source_types)
|
||||
logger.debug(self.target_reqs)
|
||||
if no_matched_type:
|
||||
raise ValueError(
|
||||
f"Edge between {self.source.vertex_type} and {self.target.vertex_type} "
|
||||
f"has no matched type"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from typing import Dict, Generator, List, Type, Union
|
||||
|
||||
from langflow.graph.edge.base import Edge
|
||||
from langflow.graph.graph.constants import VERTEX_TYPE_MAP
|
||||
from langflow.graph.graph.constants import lazy_load_vertex_dict
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.graph.vertex.types import (
|
||||
FileToolVertex,
|
||||
|
|
@ -10,7 +10,7 @@ from langflow.graph.vertex.types import (
|
|||
)
|
||||
from langflow.interface.tools.constants import FILE_TOOLS
|
||||
from langflow.utils import payload
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langchain.chains.base import Chain
|
||||
|
||||
|
||||
|
|
@ -144,7 +144,7 @@ class Graph:
|
|||
|
||||
return list(reversed(sorted_vertices))
|
||||
|
||||
def generator_build(self) -> Generator:
|
||||
def generator_build(self) -> Generator[Vertex, None, None]:
|
||||
"""Builds each vertex in the graph and yields it."""
|
||||
sorted_vertices = self.topological_sort()
|
||||
logger.debug("Sorted vertices: %s", sorted_vertices)
|
||||
|
|
@ -187,10 +187,12 @@ class Graph:
|
|||
"""Returns the node class based on the node type."""
|
||||
if node_type in FILE_TOOLS:
|
||||
return FileToolVertex
|
||||
if node_type in VERTEX_TYPE_MAP:
|
||||
return VERTEX_TYPE_MAP[node_type]
|
||||
if node_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP:
|
||||
return lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_type]
|
||||
return (
|
||||
VERTEX_TYPE_MAP[node_lc_type] if node_lc_type in VERTEX_TYPE_MAP else Vertex
|
||||
lazy_load_vertex_dict.VERTEX_TYPE_MAP[node_lc_type]
|
||||
if node_lc_type in lazy_load_vertex_dict.VERTEX_TYPE_MAP
|
||||
else Vertex
|
||||
)
|
||||
|
||||
def _build_vertices(self) -> List[Vertex]:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.graph.vertex import types
|
||||
from langflow.interface.agents.base import agent_creator
|
||||
from langflow.interface.chains.base import chain_creator
|
||||
|
|
@ -15,23 +14,45 @@ from langflow.interface.wrappers.base import wrapper_creator
|
|||
from langflow.interface.output_parsers.base import output_parser_creator
|
||||
from langflow.interface.retrievers.base import retriever_creator
|
||||
from langflow.interface.custom.base import custom_component_creator
|
||||
from typing import Dict, Type
|
||||
from langflow.utils.lazy_load import LazyLoadDictBase
|
||||
|
||||
|
||||
VERTEX_TYPE_MAP: Dict[str, Type[Vertex]] = {
|
||||
**{t: types.PromptVertex for t in prompt_creator.to_list()},
|
||||
**{t: types.AgentVertex for t in agent_creator.to_list()},
|
||||
**{t: types.ChainVertex for t in chain_creator.to_list()},
|
||||
**{t: types.ToolVertex for t in tool_creator.to_list()},
|
||||
**{t: types.ToolkitVertex for t in toolkits_creator.to_list()},
|
||||
**{t: types.WrapperVertex for t in wrapper_creator.to_list()},
|
||||
**{t: types.LLMVertex for t in llm_creator.to_list()},
|
||||
**{t: types.MemoryVertex for t in memory_creator.to_list()},
|
||||
**{t: types.EmbeddingVertex for t in embedding_creator.to_list()},
|
||||
**{t: types.VectorStoreVertex for t in vectorstore_creator.to_list()},
|
||||
**{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()},
|
||||
**{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()},
|
||||
**{t: types.OutputParserVertex for t in output_parser_creator.to_list()},
|
||||
**{t: types.CustomComponentVertex for t in custom_component_creator.to_list()},
|
||||
**{t: types.RetrieverVertex for t in retriever_creator.to_list()},
|
||||
}
|
||||
class VertexTypesDict(LazyLoadDictBase):
|
||||
def __init__(self):
|
||||
self._all_types_dict = None
|
||||
|
||||
@property
|
||||
def VERTEX_TYPE_MAP(self):
|
||||
return self.all_types_dict
|
||||
|
||||
def _build_dict(self):
|
||||
langchain_types_dict = self.get_type_dict()
|
||||
return {
|
||||
**langchain_types_dict,
|
||||
"Custom": ["Custom Tool", "Python Function"],
|
||||
}
|
||||
|
||||
def get_type_dict(self):
|
||||
return {
|
||||
**{t: types.PromptVertex for t in prompt_creator.to_list()},
|
||||
**{t: types.AgentVertex for t in agent_creator.to_list()},
|
||||
**{t: types.ChainVertex for t in chain_creator.to_list()},
|
||||
**{t: types.ToolVertex for t in tool_creator.to_list()},
|
||||
**{t: types.ToolkitVertex for t in toolkits_creator.to_list()},
|
||||
**{t: types.WrapperVertex for t in wrapper_creator.to_list()},
|
||||
**{t: types.LLMVertex for t in llm_creator.to_list()},
|
||||
**{t: types.MemoryVertex for t in memory_creator.to_list()},
|
||||
**{t: types.EmbeddingVertex for t in embedding_creator.to_list()},
|
||||
**{t: types.VectorStoreVertex for t in vectorstore_creator.to_list()},
|
||||
**{t: types.DocumentLoaderVertex for t in documentloader_creator.to_list()},
|
||||
**{t: types.TextSplitterVertex for t in textsplitter_creator.to_list()},
|
||||
**{t: types.OutputParserVertex for t in output_parser_creator.to_list()},
|
||||
**{
|
||||
t: types.CustomComponentVertex
|
||||
for t in custom_component_creator.to_list()
|
||||
},
|
||||
**{t: types.RetrieverVertex for t in retriever_creator.to_list()},
|
||||
}
|
||||
|
||||
|
||||
lazy_load_vertex_dict = VertexTypesDict()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ from typing import Any, Union
|
|||
from langflow.interface.utils import extract_input_variables_from_prompt
|
||||
|
||||
|
||||
class UnbuiltObject:
|
||||
pass
|
||||
|
||||
|
||||
def validate_prompt(prompt: str):
|
||||
"""Validate prompt."""
|
||||
if extract_input_variables_from_prompt(prompt):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import ast
|
||||
from langflow.graph.utils import UnbuiltObject
|
||||
from langflow.interface.initialize import loading
|
||||
from langflow.interface.listing import ALL_TYPES_DICT
|
||||
from langflow.interface.listing import lazy_load_dict
|
||||
from langflow.utils.constants import DIRECT_TYPES
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import sync_to_async
|
||||
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ class Vertex:
|
|||
self.edges: List["Edge"] = []
|
||||
self.base_type: Optional[str] = base_type
|
||||
self._parse_data()
|
||||
self._built_object = None
|
||||
self._built_object = UnbuiltObject()
|
||||
self._built = False
|
||||
self.artifacts: Dict[str, Any] = {}
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ class Vertex:
|
|||
)
|
||||
|
||||
if self.base_type is None:
|
||||
for base_type, value in ALL_TYPES_DICT.items():
|
||||
for base_type, value in lazy_load_dict.ALL_TYPES_DICT.items():
|
||||
if self.vertex_type in value:
|
||||
self.base_type = base_type
|
||||
break
|
||||
|
|
@ -121,6 +122,19 @@ class Vertex:
|
|||
except Exception as exc:
|
||||
logger.debug(f"Error parsing code: {exc}")
|
||||
params[key] = value.get("value")
|
||||
elif value.get("type") in ["dict", "NestedDict"]:
|
||||
# When dict comes from the frontend it comes as a
|
||||
# list of dicts, so we need to convert it to a dict
|
||||
# before passing it to the build method
|
||||
_value = value.get("value")
|
||||
if isinstance(_value, list):
|
||||
params[key] = {
|
||||
k: v
|
||||
for item in value.get("value", [])
|
||||
for k, v in item.items()
|
||||
}
|
||||
elif isinstance(_value, dict):
|
||||
params[key] = _value
|
||||
else:
|
||||
params[key] = value.get("value")
|
||||
|
||||
|
|
@ -132,13 +146,13 @@ class Vertex:
|
|||
# Add _type to params
|
||||
self.params = params
|
||||
|
||||
def _build(self):
|
||||
def _build(self, user_id=None):
|
||||
"""
|
||||
Initiate the build process.
|
||||
"""
|
||||
logger.debug(f"Building {self.vertex_type}")
|
||||
self._build_each_node_in_params_dict()
|
||||
self._get_and_instantiate_class()
|
||||
self._get_and_instantiate_class(user_id)
|
||||
self._validate_built_object()
|
||||
|
||||
self._built = True
|
||||
|
|
@ -168,23 +182,25 @@ class Vertex:
|
|||
"""
|
||||
return all(self._is_node(node) for node in value)
|
||||
|
||||
def _build_node_and_update_params(self, key, node):
|
||||
def _build_node_and_update_params(self, key, node, user_id=None):
|
||||
"""
|
||||
Builds a given node and updates the params dictionary accordingly.
|
||||
"""
|
||||
result = node.build()
|
||||
result = node.build(user_id)
|
||||
self._handle_func(key, result)
|
||||
if isinstance(result, list):
|
||||
self._extend_params_list_with_result(key, result)
|
||||
self.params[key] = result
|
||||
|
||||
def _build_list_of_nodes_and_update_params(self, key, nodes):
|
||||
def _build_list_of_nodes_and_update_params(
|
||||
self, key, nodes: List["Vertex"], user_id=None
|
||||
):
|
||||
"""
|
||||
Iterates over a list of nodes, builds each and updates the params dictionary.
|
||||
"""
|
||||
self.params[key] = []
|
||||
for node in nodes:
|
||||
built = node.build()
|
||||
built = node.build(user_id)
|
||||
if isinstance(built, list):
|
||||
if key not in self.params:
|
||||
self.params[key] = []
|
||||
|
|
@ -214,7 +230,7 @@ class Vertex:
|
|||
if isinstance(self.params[key], list):
|
||||
self.params[key].extend(result)
|
||||
|
||||
def _get_and_instantiate_class(self):
|
||||
def _get_and_instantiate_class(self, user_id=None):
|
||||
"""
|
||||
Gets the class from a dictionary and instantiates it with the params.
|
||||
"""
|
||||
|
|
@ -225,6 +241,7 @@ class Vertex:
|
|||
node_type=self.vertex_type,
|
||||
base_type=self.base_type,
|
||||
params=self.params,
|
||||
user_id=user_id,
|
||||
)
|
||||
self._update_built_object_and_artifacts(result)
|
||||
except Exception as exc:
|
||||
|
|
@ -245,12 +262,18 @@ class Vertex:
|
|||
"""
|
||||
Checks if the built object is None and raises a ValueError if so.
|
||||
"""
|
||||
if self._built_object is None:
|
||||
raise ValueError(f"Node type {self.vertex_type} not found")
|
||||
if isinstance(self._built_object, UnbuiltObject):
|
||||
raise ValueError(f"{self.vertex_type}: {self._built_object_repr()}")
|
||||
elif self._built_object is None:
|
||||
message = f"{self.vertex_type} returned None."
|
||||
if self.base_type == "custom_components":
|
||||
message += " Make sure your build method returns a component."
|
||||
|
||||
def build(self, force: bool = False) -> Any:
|
||||
raise ValueError(message)
|
||||
|
||||
def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any:
|
||||
if not self._built or force:
|
||||
self._build()
|
||||
self._build(user_id, *args, **kwargs)
|
||||
|
||||
return self._built_object
|
||||
|
||||
|
|
|
|||
|
|
@ -21,18 +21,18 @@ class AgentVertex(Vertex):
|
|||
elif isinstance(source_node, ChainVertex):
|
||||
self.chains.append(source_node)
|
||||
|
||||
def build(self, force: bool = False) -> Any:
|
||||
def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any:
|
||||
if not self._built or force:
|
||||
self._set_tools_and_chains()
|
||||
# First, build the tools
|
||||
for tool_node in self.tools:
|
||||
tool_node.build()
|
||||
tool_node.build(user_id=user_id)
|
||||
|
||||
# Next, build the chains and the rest
|
||||
for chain_node in self.chains:
|
||||
chain_node.build(tools=self.tools)
|
||||
chain_node.build(tools=self.tools, user_id=user_id)
|
||||
|
||||
self._build()
|
||||
self._build(user_id=user_id)
|
||||
|
||||
return self._built_object
|
||||
|
||||
|
|
@ -49,13 +49,13 @@ class LLMVertex(Vertex):
|
|||
def __init__(self, data: Dict):
|
||||
super().__init__(data, base_type="llms")
|
||||
|
||||
def build(self, force: bool = False) -> Any:
|
||||
def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any:
|
||||
# LLM is different because some models might take up too much memory
|
||||
# or time to load. So we only load them when we need them.ß
|
||||
if self.vertex_type == self.built_node_type:
|
||||
return self.class_built_object
|
||||
if not self._built or force:
|
||||
self._build()
|
||||
self._build(user_id=user_id)
|
||||
self.built_node_type = self.vertex_type
|
||||
self.class_built_object = self._built_object
|
||||
# Avoid deepcopying the LLM
|
||||
|
|
@ -77,11 +77,11 @@ class WrapperVertex(Vertex):
|
|||
def __init__(self, data: Dict):
|
||||
super().__init__(data, base_type="wrappers")
|
||||
|
||||
def build(self, force: bool = False) -> Any:
|
||||
def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any:
|
||||
if not self._built or force:
|
||||
if "headers" in self.params:
|
||||
self.params["headers"] = ast.literal_eval(self.params["headers"])
|
||||
self._build()
|
||||
self._build(user_id=user_id)
|
||||
return self._built_object
|
||||
|
||||
|
||||
|
|
@ -148,16 +148,19 @@ class ChainVertex(Vertex):
|
|||
def build(
|
||||
self,
|
||||
force: bool = False,
|
||||
tools: Optional[List[Union[ToolkitVertex, ToolVertex]]] = None,
|
||||
user_id=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
if not self._built or force:
|
||||
# Check if the chain requires a PromptVertex
|
||||
for key, value in self.params.items():
|
||||
if isinstance(value, PromptVertex):
|
||||
# Build the PromptVertex, passing the tools if available
|
||||
tools = kwargs.get("tools", None)
|
||||
self.params[key] = value.build(tools=tools, force=force)
|
||||
|
||||
self._build()
|
||||
self._build(user_id=user_id)
|
||||
|
||||
return self._built_object
|
||||
|
||||
|
|
@ -169,7 +172,10 @@ class PromptVertex(Vertex):
|
|||
def build(
|
||||
self,
|
||||
force: bool = False,
|
||||
user_id=None,
|
||||
tools: Optional[List[Union[ToolkitVertex, ToolVertex]]] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
if not self._built or force:
|
||||
if (
|
||||
|
|
@ -180,7 +186,7 @@ class PromptVertex(Vertex):
|
|||
# Check if it is a ZeroShotPrompt and needs a tool
|
||||
if "ShotPrompt" in self.vertex_type:
|
||||
tools = (
|
||||
[tool_node.build() for tool_node in tools]
|
||||
[tool_node.build(user_id=user_id) for tool_node in tools]
|
||||
if tools is not None
|
||||
else []
|
||||
)
|
||||
|
|
@ -208,7 +214,7 @@ class PromptVertex(Vertex):
|
|||
else:
|
||||
self.params.pop("input_variables", None)
|
||||
|
||||
self._build()
|
||||
self._build(user_id=user_id)
|
||||
return self._built_object
|
||||
|
||||
def _built_object_repr(self):
|
||||
|
|
@ -226,7 +232,12 @@ class PromptVertex(Vertex):
|
|||
# so the prompt format doesn't break
|
||||
artifacts.pop("handle_keys", None)
|
||||
try:
|
||||
template = self._built_object.template
|
||||
if not hasattr(self._built_object, "template") and hasattr(
|
||||
self._built_object, "prompt"
|
||||
):
|
||||
template = self._built_object.prompt.template
|
||||
else:
|
||||
template = self._built_object.template
|
||||
for key, value in artifacts.items():
|
||||
if value:
|
||||
replace_key = "{" + key + "}"
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ from langchain.agents import types
|
|||
from langflow.custom.customs import get_custom_nodes
|
||||
from langflow.interface.agents.custom import CUSTOM_AGENTS
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.settings import settings
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.template.frontend_node.agents import AgentFrontendNode
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class, build_template_from_method
|
||||
|
||||
|
||||
|
|
@ -53,13 +54,17 @@ class AgentCreator(LangChainTypeCreator):
|
|||
# Now this is a generator
|
||||
def to_list(self) -> List[str]:
|
||||
names = []
|
||||
settings_manager = get_settings_manager()
|
||||
for _, agent in self.type_to_loader_dict.items():
|
||||
agent_name = (
|
||||
agent.function_name()
|
||||
if hasattr(agent, "function_name")
|
||||
else agent.__name__
|
||||
)
|
||||
if agent_name in settings.AGENTS or settings.DEV:
|
||||
if (
|
||||
agent_name in settings_manager.settings.AGENTS
|
||||
or settings_manager.settings.DEV
|
||||
):
|
||||
names.append(agent_name)
|
||||
return names
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ from abc import ABC, abstractmethod
|
|||
from typing import Any, Dict, List, Optional, Type, Union
|
||||
from langchain.chains.base import Chain
|
||||
from langchain.agents import AgentExecutor
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langflow.template.field.base import TemplateField
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
from langflow.template.template.base import Template
|
||||
from langflow.utils.logger import logger
|
||||
from langflow.settings import settings
|
||||
from loguru import logger
|
||||
|
||||
|
||||
# Assuming necessary imports for Field, Template, and FrontendNode classes
|
||||
|
||||
|
|
@ -26,9 +27,12 @@ class LangChainTypeCreator(BaseModel, ABC):
|
|||
@property
|
||||
def docs_map(self) -> Dict[str, str]:
|
||||
"""A dict with the name of the component as key and the documentation link as value."""
|
||||
settings_manager = get_settings_manager()
|
||||
if self.name_docs_dict is None:
|
||||
try:
|
||||
type_settings = getattr(settings, self.type_name.upper())
|
||||
type_settings = getattr(
|
||||
settings_manager.settings, self.type_name.upper()
|
||||
)
|
||||
self.name_docs_dict = {
|
||||
name: value_dict["documentation"]
|
||||
for name, value_dict in type_settings.items()
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ from typing import Any, Dict, List, Optional, Type
|
|||
from langflow.custom.customs import get_custom_nodes
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class
|
||||
from langflow.settings import settings
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.template.frontend_node.chains import ChainFrontendNode
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class, build_template_from_method
|
||||
from langchain import chains
|
||||
from langchain_experimental.sql import SQLDatabaseChain # type: ignore
|
||||
|
|
@ -30,6 +31,7 @@ class ChainCreator(LangChainTypeCreator):
|
|||
@property
|
||||
def type_to_loader_dict(self) -> Dict:
|
||||
if self.type_dict is None:
|
||||
settings_manager = get_settings_manager()
|
||||
self.type_dict: dict[str, Any] = {
|
||||
chain_name: import_class(f"langchain.chains.{chain_name}")
|
||||
for chain_name in chains.__all__
|
||||
|
|
@ -43,7 +45,8 @@ class ChainCreator(LangChainTypeCreator):
|
|||
self.type_dict = {
|
||||
name: chain
|
||||
for name, chain in self.type_dict.items()
|
||||
if name in settings.CHAINS or settings.DEV
|
||||
if name in settings_manager.settings.CHAINS
|
||||
or settings_manager.settings.DEV
|
||||
}
|
||||
return self.type_dict
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from langflow.interface.custom.custom_component import CustomComponent
|
|||
from langflow.template.frontend_node.custom_components import (
|
||||
CustomComponentFrontendNode,
|
||||
)
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
|
||||
# Assuming necessary imports for Field, Template, and FrontendNode classes
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ class Component(BaseModel):
|
|||
elif "beta" in item_name:
|
||||
template_config["beta"] = ast.literal_eval(item_value)
|
||||
|
||||
elif "documentation" in item_name:
|
||||
template_config["documentation"] = ast.literal_eval(item_value)
|
||||
|
||||
return template_config
|
||||
|
||||
def build(self, *args: Any, **kwargs: Any) -> Any:
|
||||
|
|
|
|||
|
|
@ -8,10 +8,13 @@ from langchain.text_splitter import TextSplitter
|
|||
from langchain.tools import Tool
|
||||
from langchain.vectorstores.base import VectorStore
|
||||
from langchain.schema import BaseOutputParser
|
||||
|
||||
from langchain.schema.memory import BaseMemory
|
||||
from langchain.memory.chat_memory import BaseChatMemory
|
||||
from langchain.agents.agent import AgentExecutor
|
||||
|
||||
LANGCHAIN_BASE_TYPES = {
|
||||
"Chain": Chain,
|
||||
"AgentExecutor": AgentExecutor,
|
||||
"Tool": Tool,
|
||||
"BaseLLM": BaseLLM,
|
||||
"PromptTemplate": PromptTemplate,
|
||||
|
|
@ -22,6 +25,8 @@ LANGCHAIN_BASE_TYPES = {
|
|||
"Embeddings": Embeddings,
|
||||
"BaseRetriever": BaseRetriever,
|
||||
"BaseOutputParser": BaseOutputParser,
|
||||
"BaseMemory": BaseMemory,
|
||||
"BaseChatMemory": BaseChatMemory,
|
||||
}
|
||||
|
||||
# Langchain base types plus Python base types
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
from typing import Any, Callable, List, Optional
|
||||
from typing import Any, Callable, List, Optional, Union
|
||||
from uuid import UUID
|
||||
from fastapi import HTTPException
|
||||
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
|
||||
from langflow.interface.custom.component import Component
|
||||
from langflow.interface.custom.directory_reader import DirectoryReader
|
||||
from langflow.services.utils import get_db_manager
|
||||
from langflow.interface.custom.utils import extract_inner_type
|
||||
|
||||
from langflow.utils import validate
|
||||
|
||||
from langflow.database.base import session_getter
|
||||
from langflow.database.models.flow import Flow
|
||||
from langflow.services.database.utils import session_getter
|
||||
from langflow.services.database.models.flow import Flow
|
||||
from pydantic import Extra
|
||||
import yaml
|
||||
|
||||
|
|
@ -19,7 +22,8 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
function_entrypoint_name = "build"
|
||||
function: Optional[Callable] = None
|
||||
return_type_valid_list = list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())
|
||||
repr_value: Optional[str] = ""
|
||||
repr_value: Optional[Any] = ""
|
||||
user_id: Optional[Union[UUID, str]] = None
|
||||
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
|
|
@ -49,7 +53,9 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
reader = DirectoryReader("", False)
|
||||
|
||||
for type_hint in TYPE_HINT_LIST:
|
||||
if reader.is_type_hint_used_but_not_imported(type_hint, code):
|
||||
if reader._is_type_hint_used_in_args(
|
||||
type_hint, code
|
||||
) and not reader._is_type_hint_imported(type_hint, code):
|
||||
error_detail = {
|
||||
"error": "Type hint Error",
|
||||
"traceback": f"Type hint '{type_hint}' is used but not imported in the code.",
|
||||
|
|
@ -92,9 +98,9 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
return build_method["args"]
|
||||
|
||||
@property
|
||||
def get_function_entrypoint_return_type(self) -> str:
|
||||
def get_function_entrypoint_return_type(self) -> List[str]:
|
||||
if not self.code:
|
||||
return ""
|
||||
return []
|
||||
tree = self.get_code_tree(self.code)
|
||||
|
||||
component_classes = [
|
||||
|
|
@ -103,7 +109,7 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
if self.code_class_base_inheritance in cls["bases"]
|
||||
]
|
||||
if not component_classes:
|
||||
return ""
|
||||
return []
|
||||
|
||||
# Assume the first Component class is the one we're interested in
|
||||
component_class = component_classes[0]
|
||||
|
|
@ -114,11 +120,25 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
]
|
||||
|
||||
if not build_methods:
|
||||
return ""
|
||||
return []
|
||||
|
||||
build_method = build_methods[0]
|
||||
return_type = build_method["return_type"]
|
||||
if not return_type:
|
||||
return []
|
||||
# If list or List is in the return type, then we remove it and return the inner type
|
||||
if return_type.startswith("list") or return_type.startswith("List"):
|
||||
return_type = extract_inner_type(return_type)
|
||||
|
||||
return build_method["return_type"]
|
||||
# If the return type is not a Union, then we just return it as a list
|
||||
if "Union" not in return_type:
|
||||
return [return_type] if return_type in self.return_type_valid_list else []
|
||||
|
||||
# If the return type is a Union, then we need to parse it
|
||||
return_type = return_type.replace("Union", "").replace("[", "").replace("]", "")
|
||||
return_type = return_type.split(",")
|
||||
return_type = [item.strip() for item in return_type]
|
||||
return [item for item in return_type if item in self.return_type_valid_list]
|
||||
|
||||
@property
|
||||
def get_main_class_name(self):
|
||||
|
|
@ -159,7 +179,8 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
from langflow.processing.process import build_sorted_vertices_with_caching
|
||||
from langflow.processing.process import process_tweaks
|
||||
|
||||
with session_getter() as session:
|
||||
db_manager = get_db_manager()
|
||||
with session_getter(db_manager) as session:
|
||||
graph_data = flow.data if (flow := session.get(Flow, flow_id)) else None
|
||||
if not graph_data:
|
||||
raise ValueError(f"Flow {flow_id} not found")
|
||||
|
|
@ -168,10 +189,16 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
return build_sorted_vertices_with_caching(graph_data)
|
||||
|
||||
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]:
|
||||
get_session = get_session or session_getter
|
||||
with get_session() as session:
|
||||
flows = session.query(Flow).all()
|
||||
return flows
|
||||
if not self.user_id:
|
||||
raise ValueError("Session is invalid")
|
||||
try:
|
||||
get_session = get_session or session_getter
|
||||
db_manager = get_db_manager()
|
||||
with get_session(db_manager) as session:
|
||||
flows = session.query(Flow).filter(Flow.user_id == self.user_id).all()
|
||||
return flows
|
||||
except Exception as e:
|
||||
raise ValueError("Session is invalid") from e
|
||||
|
||||
def get_flow(
|
||||
self,
|
||||
|
|
@ -182,12 +209,16 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
get_session: Optional[Callable] = None,
|
||||
) -> Flow:
|
||||
get_session = get_session or session_getter
|
||||
|
||||
with get_session() as session:
|
||||
db_manager = get_db_manager()
|
||||
with get_session(db_manager) as session:
|
||||
if flow_id:
|
||||
flow = session.query(Flow).get(flow_id)
|
||||
elif flow_name:
|
||||
flow = session.query(Flow).filter(Flow.name == flow_name).first()
|
||||
flow = (
|
||||
session.query(Flow)
|
||||
.filter(Flow.name == flow_name)
|
||||
.filter(Flow.user_id == self.user_id)
|
||||
).first()
|
||||
else:
|
||||
raise ValueError("Either flow_name or flow_id must be provided")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import ast
|
||||
import zlib
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class CustomComponentPathValueError(ValueError):
|
||||
|
|
@ -77,7 +77,7 @@ class DirectoryReader:
|
|||
]
|
||||
filtered = [menu for menu in items if menu["components"]]
|
||||
logger.debug(
|
||||
f'Filtered components {"with errors" if with_errors else ""}: {filtered}'
|
||||
f'Filtered components {"with errors" if with_errors else ""}: {len(filtered)}'
|
||||
)
|
||||
return {"menu": filtered}
|
||||
|
||||
|
|
@ -152,15 +152,19 @@ class DirectoryReader:
|
|||
Check if a specific type hint is used in the
|
||||
function definitions within the given code.
|
||||
"""
|
||||
module = ast.parse(code)
|
||||
try:
|
||||
module = ast.parse(code)
|
||||
|
||||
for node in ast.walk(module):
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
for arg in node.args.args:
|
||||
if self._is_type_hint_in_arg_annotation(
|
||||
arg.annotation, type_hint_name
|
||||
):
|
||||
return True
|
||||
for node in ast.walk(module):
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
for arg in node.args.args:
|
||||
if self._is_type_hint_in_arg_annotation(
|
||||
arg.annotation, type_hint_name
|
||||
):
|
||||
return True
|
||||
except SyntaxError:
|
||||
# Returns False if the code is not valid Python
|
||||
return False
|
||||
return False
|
||||
|
||||
def _is_type_hint_in_arg_annotation(self, annotation, type_hint_name: str) -> bool:
|
||||
|
|
@ -204,8 +208,13 @@ class DirectoryReader:
|
|||
return False, "Syntax error"
|
||||
elif not self.validate_build(file_content):
|
||||
return False, "Missing build function"
|
||||
elif self.is_type_hint_used_but_not_imported("Optional", file_content):
|
||||
return False, "Type hint 'Optional' is used but not imported in the code."
|
||||
elif self._is_type_hint_used_in_args(
|
||||
"Optional", file_content
|
||||
) and not self._is_type_hint_imported("Optional", file_content):
|
||||
return (
|
||||
False,
|
||||
"Type hint 'Optional' is used but not imported in the code.",
|
||||
)
|
||||
else:
|
||||
if self.compress_code_field:
|
||||
file_content = str(StringCompressor(file_content).compress_string())
|
||||
|
|
|
|||
10
src/backend/langflow/interface/custom/utils.py
Normal file
10
src/backend/langflow/interface/custom/utils.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import re
|
||||
|
||||
|
||||
def extract_inner_type(return_type: str) -> str:
|
||||
"""
|
||||
Extracts the inner type from a type hint that is a list.
|
||||
"""
|
||||
if match := re.match(r"list\[(.*)\]", return_type, re.IGNORECASE):
|
||||
return match[1]
|
||||
return return_type
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
from typing import Dict, List, Optional, Type
|
||||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.template.frontend_node.documentloaders import DocumentLoaderFrontNode
|
||||
from langflow.interface.custom_lists import documentloaders_type_to_cls_dict
|
||||
from langflow.settings import settings
|
||||
from langflow.utils.logger import logger
|
||||
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class
|
||||
|
||||
|
||||
|
|
@ -30,10 +31,12 @@ class DocumentLoaderCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
return [
|
||||
documentloader.__name__
|
||||
for documentloader in self.type_to_loader_dict.values()
|
||||
if documentloader.__name__ in settings.DOCUMENTLOADERS or settings.DEV
|
||||
if documentloader.__name__ in settings_manager.settings.DOCUMENTLOADERS
|
||||
or settings_manager.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ from typing import Dict, List, Optional, Type
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.custom_lists import embedding_type_to_cls_dict
|
||||
from langflow.settings import settings
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
from langflow.template.frontend_node.embeddings import EmbeddingFrontendNode
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class
|
||||
|
||||
|
||||
|
|
@ -32,10 +33,12 @@ class EmbeddingCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
return [
|
||||
embedding.__name__
|
||||
for embedding in self.type_to_loader_dict.values()
|
||||
if embedding.__name__ in settings.EMBEDDINGS or settings.DEV
|
||||
if embedding.__name__ in settings_manager.settings.EMBEDDINGS
|
||||
or settings_manager.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
from typing import Any, Callable, Dict, Sequence, Type
|
||||
import orjson
|
||||
from typing import Any, Callable, Dict, Sequence, Type, TYPE_CHECKING
|
||||
|
||||
from langchain.agents import agent as agent_module
|
||||
from langchain.agents.agent import AgentExecutor
|
||||
|
|
@ -33,10 +34,15 @@ from langflow.utils import validate
|
|||
from langchain.chains.base import Chain
|
||||
from langchain.vectorstores.base import VectorStore
|
||||
from langchain.document_loaders.base import BaseLoader
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any:
|
||||
def instantiate_class(
|
||||
node_type: str, base_type: str, params: Dict, user_id=None
|
||||
) -> Any:
|
||||
"""Instantiate class from module type and key, and params"""
|
||||
params = convert_params_to_sets(params)
|
||||
params = convert_kwargs(params)
|
||||
|
|
@ -47,7 +53,9 @@ def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any:
|
|||
return custom_node(**params)
|
||||
logger.debug(f"Instantiating {node_type} of type {base_type}")
|
||||
class_object = import_by_type(_type=base_type, name=node_type)
|
||||
return instantiate_based_on_type(class_object, base_type, node_type, params)
|
||||
return instantiate_based_on_type(
|
||||
class_object, base_type, node_type, params, user_id=user_id
|
||||
)
|
||||
|
||||
|
||||
def convert_params_to_sets(params):
|
||||
|
|
@ -66,7 +74,7 @@ def convert_kwargs(params):
|
|||
for key in kwargs_keys:
|
||||
if isinstance(params[key], str):
|
||||
try:
|
||||
params[key] = json.loads(params[key])
|
||||
params[key] = orjson.loads(params[key])
|
||||
except json.JSONDecodeError:
|
||||
# if the string is not a valid json string, we will
|
||||
# remove the key from the params
|
||||
|
|
@ -74,7 +82,7 @@ def convert_kwargs(params):
|
|||
return params
|
||||
|
||||
|
||||
def instantiate_based_on_type(class_object, base_type, node_type, params):
|
||||
def instantiate_based_on_type(class_object, base_type, node_type, params, user_id):
|
||||
if base_type == "agents":
|
||||
return instantiate_agent(node_type, class_object, params)
|
||||
elif base_type == "prompts":
|
||||
|
|
@ -88,7 +96,7 @@ def instantiate_based_on_type(class_object, base_type, node_type, params):
|
|||
elif base_type == "toolkits":
|
||||
return instantiate_toolkit(node_type, class_object, params)
|
||||
elif base_type == "embeddings":
|
||||
return instantiate_embedding(class_object, params)
|
||||
return instantiate_embedding(node_type, class_object, params)
|
||||
elif base_type == "vectorstores":
|
||||
return instantiate_vectorstore(class_object, params)
|
||||
elif base_type == "documentloaders":
|
||||
|
|
@ -108,17 +116,20 @@ def instantiate_based_on_type(class_object, base_type, node_type, params):
|
|||
elif base_type == "memory":
|
||||
return instantiate_memory(node_type, class_object, params)
|
||||
elif base_type == "custom_components":
|
||||
return instantiate_custom_component(node_type, class_object, params)
|
||||
return instantiate_custom_component(node_type, class_object, params, user_id)
|
||||
elif base_type == "wrappers":
|
||||
return instantiate_wrapper(node_type, class_object, params)
|
||||
else:
|
||||
return class_object(**params)
|
||||
|
||||
|
||||
def instantiate_custom_component(node_type, class_object, params):
|
||||
class_object = get_function_custom(params.pop("code"))
|
||||
custom_component = class_object()
|
||||
built_object = custom_component.build(**params)
|
||||
def instantiate_custom_component(node_type, class_object, params, user_id):
|
||||
# we need to make a copy of the params because we will be
|
||||
# modifying it
|
||||
params_copy = params.copy()
|
||||
class_object: "CustomComponent" = get_function_custom(params_copy.pop("code"))
|
||||
custom_component = class_object(user_id=user_id)
|
||||
built_object = custom_component.build(**params_copy)
|
||||
return built_object, {"repr": custom_component.custom_repr()}
|
||||
|
||||
|
||||
|
|
@ -144,7 +155,7 @@ def instantiate_llm(node_type, class_object, params: Dict):
|
|||
# This is a workaround so JinaChat works until streaming is implemented
|
||||
# if "openai_api_base" in params and "jina" in params["openai_api_base"]:
|
||||
# False if condition is True
|
||||
if node_type == "VertexAI":
|
||||
if "VertexAI" in node_type:
|
||||
return initialize_vertexai(class_object=class_object, params=params)
|
||||
# max_tokens sometimes is a string and should be an int
|
||||
if "max_tokens" in params:
|
||||
|
|
@ -258,9 +269,13 @@ def instantiate_toolkit(node_type, class_object: Type[BaseToolkit], params: Dict
|
|||
return loaded_toolkit
|
||||
|
||||
|
||||
def instantiate_embedding(class_object, params: Dict):
|
||||
def instantiate_embedding(node_type, class_object, params: Dict):
|
||||
params.pop("model", None)
|
||||
params.pop("headers", None)
|
||||
|
||||
if "VertexAI" in node_type:
|
||||
return initialize_vertexai(class_object=class_object, params=params)
|
||||
|
||||
try:
|
||||
return class_object(**params)
|
||||
except ValidationError:
|
||||
|
|
@ -303,7 +318,7 @@ def instantiate_documentloader(class_object: Type[BaseLoader], params: Dict):
|
|||
metadata = params.pop("metadata", None)
|
||||
if metadata and isinstance(metadata, str):
|
||||
try:
|
||||
metadata = json.loads(metadata)
|
||||
metadata = orjson.loads(metadata)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(
|
||||
"The metadata you provided is not a valid JSON string."
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import contextlib
|
||||
import json
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
import orjson
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from langchain.agents import ZeroShotAgent
|
||||
|
|
@ -51,7 +53,9 @@ def handle_partial_variables(prompt, format_kwargs: Dict):
|
|||
}
|
||||
# Remove handle_keys otherwise LangChain raises an error
|
||||
partial_variables.pop("handle_keys", None)
|
||||
return prompt.partial(**partial_variables)
|
||||
if partial_variables and hasattr(prompt, "partial"):
|
||||
return prompt.partial(**partial_variables)
|
||||
return prompt
|
||||
|
||||
|
||||
def handle_variable(params: Dict, input_variable: str, format_kwargs: Dict):
|
||||
|
|
@ -93,9 +97,11 @@ def format_content(variable):
|
|||
|
||||
def try_to_load_json(content):
|
||||
with contextlib.suppress(json.JSONDecodeError):
|
||||
content = json.loads(content)
|
||||
content = orjson.loads(content)
|
||||
if isinstance(content, list):
|
||||
content = ",".join([str(item) for item in content])
|
||||
else:
|
||||
content = orjson_dumps(content)
|
||||
return content
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import json
|
||||
from typing import Any, Callable, Dict, Type
|
||||
from langchain.vectorstores import (
|
||||
Pinecone,
|
||||
|
|
@ -12,6 +11,8 @@ from langchain.vectorstores import (
|
|||
|
||||
import os
|
||||
|
||||
import orjson
|
||||
|
||||
|
||||
def docs_in_params(params: dict) -> bool:
|
||||
"""Check if params has documents OR texts and one of them is not an empty list,
|
||||
|
|
@ -92,7 +93,7 @@ def initialize_weaviate(class_object: Type[Weaviate], params: dict):
|
|||
import weaviate # type: ignore
|
||||
|
||||
client_kwargs_json = params.get("client_kwargs", "{}")
|
||||
client_kwargs = json.loads(client_kwargs_json)
|
||||
client_kwargs = orjson.loads(client_kwargs_json)
|
||||
client_params = {
|
||||
"url": params.get("weaviate_url"),
|
||||
}
|
||||
|
|
@ -130,8 +131,8 @@ def initialize_pinecone(class_object: Type[Pinecone], params: dict):
|
|||
|
||||
import pinecone # type: ignore
|
||||
|
||||
pinecone_api_key = params.get("pinecone_api_key")
|
||||
pinecone_env = params.get("pinecone_env")
|
||||
pinecone_api_key = params.pop("pinecone_api_key")
|
||||
pinecone_env = params.pop("pinecone_env")
|
||||
|
||||
if pinecone_api_key is None or pinecone_env is None:
|
||||
if os.getenv("PINECONE_API_KEY") is not None:
|
||||
|
|
@ -170,6 +171,26 @@ def initialize_pinecone(class_object: Type[Pinecone], params: dict):
|
|||
|
||||
def initialize_chroma(class_object: Type[Chroma], params: dict):
|
||||
"""Initialize a ChromaDB object from the params"""
|
||||
if ( # type: ignore
|
||||
"chroma_server_host" in params or "chroma_server_http_port" in params
|
||||
):
|
||||
import chromadb # type: ignore
|
||||
|
||||
settings_params = {
|
||||
key: params[key]
|
||||
for key, value_ in params.items()
|
||||
if key.startswith("chroma_server_") and value_
|
||||
}
|
||||
chroma_settings = chromadb.config.Settings(**settings_params)
|
||||
params["client_settings"] = chroma_settings
|
||||
else:
|
||||
# remove all chroma_server_ keys from params
|
||||
params = {
|
||||
key: value
|
||||
for key, value in params.items()
|
||||
if not key.startswith("chroma_server_")
|
||||
}
|
||||
|
||||
persist = params.pop("persist", False)
|
||||
if not docs_in_params(params):
|
||||
params.pop("documents", None)
|
||||
|
|
|
|||
|
|
@ -14,34 +14,43 @@ from langflow.interface.wrappers.base import wrapper_creator
|
|||
from langflow.interface.output_parsers.base import output_parser_creator
|
||||
from langflow.interface.retrievers.base import retriever_creator
|
||||
from langflow.interface.custom.base import custom_component_creator
|
||||
from langflow.utils.lazy_load import LazyLoadDictBase
|
||||
|
||||
|
||||
def get_type_dict():
|
||||
return {
|
||||
"agents": agent_creator.to_list(),
|
||||
"prompts": prompt_creator.to_list(),
|
||||
"llms": llm_creator.to_list(),
|
||||
"tools": tool_creator.to_list(),
|
||||
"chains": chain_creator.to_list(),
|
||||
"memory": memory_creator.to_list(),
|
||||
"toolkits": toolkits_creator.to_list(),
|
||||
"wrappers": wrapper_creator.to_list(),
|
||||
"documentLoaders": documentloader_creator.to_list(),
|
||||
"vectorStore": vectorstore_creator.to_list(),
|
||||
"embeddings": embedding_creator.to_list(),
|
||||
"textSplitters": textsplitter_creator.to_list(),
|
||||
"utilities": utility_creator.to_list(),
|
||||
"outputParsers": output_parser_creator.to_list(),
|
||||
"retrievers": retriever_creator.to_list(),
|
||||
"custom_components": custom_component_creator.to_list(),
|
||||
}
|
||||
class AllTypesDict(LazyLoadDictBase):
|
||||
def __init__(self):
|
||||
self._all_types_dict = None
|
||||
|
||||
@property
|
||||
def ALL_TYPES_DICT(self):
|
||||
return self.all_types_dict
|
||||
|
||||
def _build_dict(self):
|
||||
langchain_types_dict = self.get_type_dict()
|
||||
return {
|
||||
**langchain_types_dict,
|
||||
"Custom": ["Custom Tool", "Python Function"],
|
||||
}
|
||||
|
||||
def get_type_dict(self):
|
||||
return {
|
||||
"agents": agent_creator.to_list(),
|
||||
"prompts": prompt_creator.to_list(),
|
||||
"llms": llm_creator.to_list(),
|
||||
"tools": tool_creator.to_list(),
|
||||
"chains": chain_creator.to_list(),
|
||||
"memory": memory_creator.to_list(),
|
||||
"toolkits": toolkits_creator.to_list(),
|
||||
"wrappers": wrapper_creator.to_list(),
|
||||
"documentLoaders": documentloader_creator.to_list(),
|
||||
"vectorStore": vectorstore_creator.to_list(),
|
||||
"embeddings": embedding_creator.to_list(),
|
||||
"textSplitters": textsplitter_creator.to_list(),
|
||||
"utilities": utility_creator.to_list(),
|
||||
"outputParsers": output_parser_creator.to_list(),
|
||||
"retrievers": retriever_creator.to_list(),
|
||||
"custom_components": custom_component_creator.to_list(),
|
||||
}
|
||||
|
||||
|
||||
LANGCHAIN_TYPES_DICT = get_type_dict()
|
||||
|
||||
# Now we'll build a dict with Langchain types and ours
|
||||
|
||||
ALL_TYPES_DICT = {
|
||||
**LANGCHAIN_TYPES_DICT,
|
||||
"Custom": ["Custom Tool", "Python Function"],
|
||||
}
|
||||
lazy_load_dict = AllTypesDict()
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ from typing import Dict, List, Optional, Type
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.custom_lists import llm_type_to_cls_dict
|
||||
from langflow.settings import settings
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.template.frontend_node.llms import LLMFrontendNode
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class
|
||||
|
||||
|
||||
|
|
@ -33,10 +34,12 @@ class LLMCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
return [
|
||||
llm.__name__
|
||||
for llm in self.type_to_loader_dict.values()
|
||||
if llm.__name__ in settings.LLMS or settings.DEV
|
||||
if llm.__name__ in settings_manager.settings.LLMS
|
||||
or settings_manager.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ from typing import Dict, List, Optional, Type
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.custom_lists import memory_type_to_cls_dict
|
||||
from langflow.settings import settings
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
from langflow.template.frontend_node.memories import MemoryFrontendNode
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class, build_template_from_method
|
||||
from langflow.custom.customs import get_custom_nodes
|
||||
|
||||
|
|
@ -48,10 +49,12 @@ class MemoryCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
return [
|
||||
memory.__name__
|
||||
for memory in self.type_to_loader_dict.values()
|
||||
if memory.__name__ in settings.MEMORIES or settings.DEV
|
||||
if memory.__name__ in settings_manager.settings.MEMORIES
|
||||
or settings_manager.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ from langchain import output_parsers
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class
|
||||
from langflow.settings import settings
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.template.frontend_node.output_parsers import OutputParserFrontendNode
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class, build_template_from_method
|
||||
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ class OutputParserCreator(LangChainTypeCreator):
|
|||
@property
|
||||
def type_to_loader_dict(self) -> Dict:
|
||||
if self.type_dict is None:
|
||||
settings_manager = get_settings_manager()
|
||||
self.type_dict = {
|
||||
output_parser_name: import_class(
|
||||
f"langchain.output_parsers.{output_parser_name}"
|
||||
|
|
@ -33,7 +35,8 @@ class OutputParserCreator(LangChainTypeCreator):
|
|||
self.type_dict = {
|
||||
name: output_parser
|
||||
for name, output_parser in self.type_dict.items()
|
||||
if name in settings.OUTPUT_PARSERS or settings.DEV
|
||||
if name in settings_manager.settings.OUTPUT_PARSERS
|
||||
or settings_manager.settings.DEV
|
||||
}
|
||||
return self.type_dict
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ from langchain import prompts
|
|||
from langflow.custom.customs import get_custom_nodes
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class
|
||||
from langflow.settings import settings
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.template.frontend_node.prompts import PromptFrontendNode
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class
|
||||
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ class PromptCreator(LangChainTypeCreator):
|
|||
|
||||
@property
|
||||
def type_to_loader_dict(self) -> Dict:
|
||||
settings_manager = get_settings_manager()
|
||||
if self.type_dict is None:
|
||||
self.type_dict = {
|
||||
prompt_name: import_class(f"langchain.prompts.{prompt_name}")
|
||||
|
|
@ -34,7 +36,8 @@ class PromptCreator(LangChainTypeCreator):
|
|||
self.type_dict = {
|
||||
name: prompt
|
||||
for name, prompt in self.type_dict.items()
|
||||
if name in settings.PROMPTS or settings.DEV
|
||||
if name in settings_manager.settings.PROMPTS
|
||||
or settings_manager.settings.DEV
|
||||
}
|
||||
return self.type_dict
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ from langchain import retrievers
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class
|
||||
from langflow.settings import settings
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.template.frontend_node.retrievers import RetrieverFrontendNode
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_method, build_template_from_class
|
||||
|
||||
|
||||
|
|
@ -48,10 +49,12 @@ class RetrieverCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
return [
|
||||
retriever
|
||||
for retriever in self.type_to_loader_dict.keys()
|
||||
if retriever in settings.RETRIEVERS or settings.DEV
|
||||
if retriever in settings_manager.settings.RETRIEVERS
|
||||
or settings_manager.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from langflow.cache.utils import memoize_dict
|
||||
from typing import Any, Dict, Tuple
|
||||
from langflow.services.cache.utils import memoize_dict
|
||||
from langflow.graph import Graph
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@memoize_dict(maxsize=10)
|
||||
|
|
@ -15,7 +16,7 @@ def build_langchain_object_with_caching(data_graph):
|
|||
|
||||
|
||||
@memoize_dict(maxsize=10)
|
||||
def build_sorted_vertices_with_caching(data_graph):
|
||||
def build_sorted_vertices_with_caching(data_graph) -> Tuple[Any, Dict]:
|
||||
"""
|
||||
Build langchain object from data_graph.
|
||||
"""
|
||||
|
|
@ -57,8 +58,12 @@ def get_memory_key(langchain_object):
|
|||
"chat_history": "history",
|
||||
"history": "chat_history",
|
||||
}
|
||||
memory_key = langchain_object.memory.memory_key
|
||||
return mem_key_dict.get(memory_key)
|
||||
# Check if memory_key attribute exists
|
||||
if hasattr(langchain_object.memory, "memory_key"):
|
||||
memory_key = langchain_object.memory.memory_key
|
||||
return mem_key_dict.get(memory_key)
|
||||
else:
|
||||
return None # or some other default value or action
|
||||
|
||||
|
||||
def update_memory_keys(langchain_object, possible_new_mem_key):
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
from typing import Dict, List, Optional, Type
|
||||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.template.frontend_node.textsplitters import TextSplittersFrontendNode
|
||||
from langflow.interface.custom_lists import textsplitter_type_to_cls_dict
|
||||
from langflow.settings import settings
|
||||
from langflow.utils.logger import logger
|
||||
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class
|
||||
|
||||
|
||||
|
|
@ -30,10 +31,12 @@ class TextSplitterCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
return [
|
||||
textsplitter.__name__
|
||||
for textsplitter in self.type_to_loader_dict.values()
|
||||
if textsplitter.__name__ in settings.TEXTSPLITTERS or settings.DEV
|
||||
if textsplitter.__name__ in settings_manager.settings.TEXTSPLITTERS
|
||||
or settings_manager.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ from langchain.agents import agent_toolkits
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class, import_module
|
||||
from langflow.settings import settings
|
||||
from langflow.utils.logger import logger
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class
|
||||
|
||||
|
||||
|
|
@ -29,13 +30,15 @@ class ToolkitCreator(LangChainTypeCreator):
|
|||
@property
|
||||
def type_to_loader_dict(self) -> Dict:
|
||||
if self.type_dict is None:
|
||||
settings_manager = get_settings_manager()
|
||||
self.type_dict = {
|
||||
toolkit_name: import_class(
|
||||
f"langchain.agents.agent_toolkits.{toolkit_name}"
|
||||
)
|
||||
# if toolkit_name is not lower case it is a class
|
||||
for toolkit_name in agent_toolkits.__all__
|
||||
if not toolkit_name.islower() and toolkit_name in settings.TOOLKITS
|
||||
if not toolkit_name.islower()
|
||||
and toolkit_name in settings_manager.settings.TOOLKITS
|
||||
}
|
||||
|
||||
return self.type_dict
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ from langflow.interface.tools.constants import (
|
|||
OTHER_TOOLS,
|
||||
)
|
||||
from langflow.interface.tools.util import get_tool_params
|
||||
from langflow.settings import settings
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.template.field.base import TemplateField
|
||||
from langflow.template.template.base import Template
|
||||
from langflow.utils import util
|
||||
|
|
@ -66,6 +67,7 @@ class ToolCreator(LangChainTypeCreator):
|
|||
|
||||
@property
|
||||
def type_to_loader_dict(self) -> Dict:
|
||||
settings_manager = get_settings_manager()
|
||||
if self.tools_dict is None:
|
||||
all_tools = {}
|
||||
|
||||
|
|
@ -74,7 +76,10 @@ class ToolCreator(LangChainTypeCreator):
|
|||
|
||||
tool_name = tool_params.get("name") or tool
|
||||
|
||||
if tool_name in settings.TOOLS or settings.DEV:
|
||||
if (
|
||||
tool_name in settings_manager.settings.TOOLS
|
||||
or settings_manager.settings.DEV
|
||||
):
|
||||
if tool_name == "JsonSpec":
|
||||
tool_params["path"] = tool_params.pop("dict_") # type: ignore
|
||||
all_tools[tool_name] = {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import inspect
|
|||
from typing import Dict, Union
|
||||
|
||||
from langchain.agents.tools import Tool
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def get_func_tool_params(func, **kwargs) -> Union[Dict, None]:
|
||||
|
|
@ -57,7 +58,13 @@ def get_func_tool_params(func, **kwargs) -> Union[Dict, None]:
|
|||
|
||||
|
||||
def get_class_tool_params(cls, **kwargs) -> Union[Dict, None]:
|
||||
tree = ast.parse(inspect.getsource(cls))
|
||||
try:
|
||||
tree = ast.parse(inspect.getsource(cls))
|
||||
except IndentationError:
|
||||
logger.error(
|
||||
f"Error parsing class {cls.__name__}. Make sure there are no tabs in the code."
|
||||
)
|
||||
return None
|
||||
|
||||
tool_params = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import ast
|
||||
import contextlib
|
||||
from typing import Any
|
||||
from typing import Any, List
|
||||
from langflow.api.utils import merge_nested_dicts_with_renaming
|
||||
from langflow.interface.agents.base import agent_creator
|
||||
from langflow.interface.chains.base import chain_creator
|
||||
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
|
||||
from langflow.interface.custom.utils import extract_inner_type
|
||||
from langflow.interface.document_loaders.base import documentloader_creator
|
||||
from langflow.interface.embeddings.base import embedding_creator
|
||||
from langflow.interface.importing.utils import get_function_custom
|
||||
|
|
@ -28,9 +30,8 @@ from langflow.template.frontend_node.custom_components import (
|
|||
from langflow.interface.retrievers.base import retriever_creator
|
||||
|
||||
from langflow.interface.custom.directory_reader import DirectoryReader
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import get_base_classes
|
||||
from langflow.api.utils import merge_nested_dicts
|
||||
|
||||
import re
|
||||
import warnings
|
||||
|
|
@ -84,6 +85,8 @@ def build_langchain_types_dict(): # sourcery skip: dict-assign-update-to-union
|
|||
|
||||
|
||||
def process_type(field_type: str):
|
||||
if field_type.startswith("list") or field_type.startswith("List"):
|
||||
return extract_inner_type(field_type)
|
||||
return "prompt" if field_type == "Prompt" else field_type
|
||||
|
||||
|
||||
|
|
@ -100,6 +103,7 @@ def add_new_custom_field(
|
|||
# if it is, update the value
|
||||
display_name = field_config.pop("display_name", field_name)
|
||||
field_type = field_config.pop("field_type", field_type)
|
||||
field_contains_list = "list" in field_type.lower()
|
||||
field_type = process_type(field_type)
|
||||
field_value = field_config.pop("value", field_value)
|
||||
field_advanced = field_config.pop("advanced", False)
|
||||
|
|
@ -110,7 +114,9 @@ def add_new_custom_field(
|
|||
# If options is a list, then it's a dropdown
|
||||
# If options is None, then it's a list of strings
|
||||
is_list = isinstance(field_config.get("options"), list)
|
||||
field_config["is_list"] = is_list or field_config.get("is_list", False)
|
||||
field_config["is_list"] = (
|
||||
is_list or field_config.get("is_list", False) or field_contains_list
|
||||
)
|
||||
|
||||
if "name" in field_config:
|
||||
warnings.warn(
|
||||
|
|
@ -172,7 +178,7 @@ def extract_type_from_optional(field_type):
|
|||
Returns:
|
||||
str: The extracted type, or an empty string if no type was found.
|
||||
"""
|
||||
match = re.search(r"\[(.*?)\]", field_type)
|
||||
match = re.search(r"\[(.*?)\]$", field_type)
|
||||
return match[1] if match else None
|
||||
|
||||
|
||||
|
|
@ -190,14 +196,16 @@ def build_frontend_node(custom_component: CustomComponent):
|
|||
|
||||
def update_attributes(frontend_node, template_config):
|
||||
"""Update the display name and description of a frontend node"""
|
||||
if "display_name" in template_config:
|
||||
frontend_node["display_name"] = template_config["display_name"]
|
||||
|
||||
if "description" in template_config:
|
||||
frontend_node["description"] = template_config["description"]
|
||||
|
||||
if "beta" in template_config:
|
||||
frontend_node["beta"] = template_config["beta"]
|
||||
attributes = [
|
||||
"display_name",
|
||||
"description",
|
||||
"beta",
|
||||
"documentation",
|
||||
"output_types",
|
||||
]
|
||||
for attribute in attributes:
|
||||
if attribute in template_config:
|
||||
frontend_node[attribute] = template_config[attribute]
|
||||
|
||||
|
||||
def build_field_config(custom_component: CustomComponent):
|
||||
|
|
@ -257,55 +265,67 @@ def get_field_properties(extra_field):
|
|||
return field_name, field_type, field_value, field_required
|
||||
|
||||
|
||||
def add_base_classes(frontend_node, return_type):
|
||||
def add_base_classes(frontend_node, return_types: List[str]):
|
||||
"""Add base classes to the frontend node"""
|
||||
if return_type not in CUSTOM_COMPONENT_SUPPORTED_TYPES or return_type is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": (
|
||||
"Invalid return type should be one of: "
|
||||
f"{list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())}"
|
||||
),
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
)
|
||||
for return_type in return_types:
|
||||
if return_type not in CUSTOM_COMPONENT_SUPPORTED_TYPES or return_type is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": (
|
||||
"Invalid return type should be one of: "
|
||||
f"{list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())}"
|
||||
),
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
)
|
||||
|
||||
return_type_instance = CUSTOM_COMPONENT_SUPPORTED_TYPES.get(return_type)
|
||||
base_classes = get_base_classes(return_type_instance)
|
||||
return_type_instance = CUSTOM_COMPONENT_SUPPORTED_TYPES.get(return_type)
|
||||
base_classes = get_base_classes(return_type_instance)
|
||||
|
||||
for base_class in base_classes:
|
||||
if base_class not in CLASSES_TO_REMOVE:
|
||||
frontend_node.get("base_classes").append(base_class)
|
||||
for base_class in base_classes:
|
||||
if base_class not in CLASSES_TO_REMOVE:
|
||||
frontend_node.get("base_classes").append(base_class)
|
||||
|
||||
|
||||
def build_langchain_template_custom_component(custom_component: CustomComponent):
|
||||
"""Build a custom component template for the langchain"""
|
||||
logger.debug("Building custom component template")
|
||||
frontend_node = build_frontend_node(custom_component)
|
||||
try:
|
||||
logger.debug("Building custom component template")
|
||||
frontend_node = build_frontend_node(custom_component)
|
||||
|
||||
if frontend_node is None:
|
||||
return None
|
||||
logger.debug("Built base frontend node")
|
||||
template_config = custom_component.build_template_config
|
||||
if frontend_node is None:
|
||||
return None
|
||||
logger.debug("Built base frontend node")
|
||||
template_config = custom_component.build_template_config
|
||||
|
||||
update_attributes(frontend_node, template_config)
|
||||
logger.debug("Updated attributes")
|
||||
field_config = build_field_config(custom_component)
|
||||
logger.debug("Built field config")
|
||||
add_extra_fields(
|
||||
frontend_node, field_config, custom_component.get_function_entrypoint_args
|
||||
)
|
||||
logger.debug("Added extra fields")
|
||||
frontend_node = add_code_field(
|
||||
frontend_node, custom_component.code, field_config.get("code", {})
|
||||
)
|
||||
logger.debug("Added code field")
|
||||
add_base_classes(
|
||||
frontend_node, custom_component.get_function_entrypoint_return_type
|
||||
)
|
||||
logger.debug("Added base classes")
|
||||
return frontend_node
|
||||
update_attributes(frontend_node, template_config)
|
||||
logger.debug("Updated attributes")
|
||||
field_config = build_field_config(custom_component)
|
||||
logger.debug("Built field config")
|
||||
add_extra_fields(
|
||||
frontend_node, field_config, custom_component.get_function_entrypoint_args
|
||||
)
|
||||
logger.debug("Added extra fields")
|
||||
frontend_node = add_code_field(
|
||||
frontend_node, custom_component.code, field_config.get("code", {})
|
||||
)
|
||||
logger.debug("Added code field")
|
||||
add_base_classes(
|
||||
frontend_node, custom_component.get_function_entrypoint_return_type
|
||||
)
|
||||
logger.debug("Added base classes")
|
||||
return frontend_node
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": (
|
||||
"Invalid type convertion. Please check your code and try again."
|
||||
),
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
) from exc
|
||||
|
||||
|
||||
def load_files_from_path(path: str):
|
||||
|
|
@ -334,7 +354,9 @@ def build_valid_menu(valid_components):
|
|||
valid_menu[menu_name] = {}
|
||||
|
||||
for component in menu_item["components"]:
|
||||
logger.debug(f"Building component: {component}")
|
||||
logger.debug(
|
||||
f"Building component: {component.get('name'), component.get('output_types')}"
|
||||
)
|
||||
try:
|
||||
component_name = component["name"]
|
||||
component_code = component["code"]
|
||||
|
|
@ -423,4 +445,4 @@ def build_langchain_custom_component_list_from_path(path: str):
|
|||
valid_menu = build_valid_menu(valid_components)
|
||||
invalid_menu = build_invalid_menu(invalid_components)
|
||||
|
||||
return merge_nested_dicts(valid_menu, invalid_menu)
|
||||
return merge_nested_dicts_with_renaming(valid_menu, invalid_menu)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ from langchain import SQLDatabase, utilities
|
|||
from langflow.custom.customs import get_custom_nodes
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class
|
||||
from langflow.settings import settings
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.template.frontend_node.utilities import UtilitiesFrontendNode
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class
|
||||
|
||||
|
||||
|
|
@ -26,6 +27,7 @@ class UtilityCreator(LangChainTypeCreator):
|
|||
from the langchain.chains module and filtering them according to the settings.utilities list.
|
||||
"""
|
||||
if self.type_dict is None:
|
||||
settings_manager = get_settings_manager()
|
||||
self.type_dict = {
|
||||
utility_name: import_class(f"langchain.utilities.{utility_name}")
|
||||
for utility_name in utilities.__all__
|
||||
|
|
@ -35,7 +37,8 @@ class UtilityCreator(LangChainTypeCreator):
|
|||
self.type_dict = {
|
||||
name: utility
|
||||
for name, utility in self.type_dict.items()
|
||||
if name in settings.UTILITIES or settings.DEV
|
||||
if name in settings_manager.settings.UTILITIES
|
||||
or settings_manager.settings.DEV
|
||||
}
|
||||
|
||||
return self.type_dict
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ import re
|
|||
import yaml
|
||||
from langchain.base_language import BaseLanguageModel
|
||||
from PIL.Image import Image
|
||||
from langflow.utils.logger import logger
|
||||
from langflow.chat.config import ChatConfig
|
||||
from loguru import logger
|
||||
from langflow.services.chat.config import ChatConfig
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
|
||||
def load_file_into_dict(file_path: str) -> dict:
|
||||
|
|
@ -63,13 +64,11 @@ def extract_input_variables_from_prompt(prompt: str) -> list[str]:
|
|||
|
||||
def setup_llm_caching():
|
||||
"""Setup LLM caching."""
|
||||
|
||||
from langflow.settings import settings
|
||||
|
||||
settings_manager = get_settings_manager()
|
||||
try:
|
||||
set_langchain_cache(settings)
|
||||
set_langchain_cache(settings_manager.settings)
|
||||
except ImportError:
|
||||
logger.warning(f"Could not import {settings.CACHE}. ")
|
||||
logger.warning(f"Could not import {settings_manager.settings.CACHE}. ")
|
||||
except Exception as exc:
|
||||
logger.warning(f"Could not setup LLM caching. Error: {exc}")
|
||||
|
||||
|
|
@ -78,9 +77,16 @@ def set_langchain_cache(settings):
|
|||
import langchain
|
||||
from langflow.interface.importing.utils import import_class
|
||||
|
||||
cache_type = os.getenv("LANGFLOW_LANGCHAIN_CACHE")
|
||||
cache_class = import_class(f"langchain.cache.{cache_type or settings.CACHE}")
|
||||
if cache_type := os.getenv("LANGFLOW_LANGCHAIN_CACHE"):
|
||||
try:
|
||||
cache_class = import_class(
|
||||
f"langchain.cache.{cache_type or settings.CACHE}"
|
||||
)
|
||||
|
||||
logger.debug(f"Setting up LLM caching with {cache_class.__name__}")
|
||||
langchain.llm_cache = cache_class()
|
||||
logger.info(f"LLM caching setup with {cache_class.__name__}")
|
||||
logger.debug(f"Setting up LLM caching with {cache_class.__name__}")
|
||||
langchain.llm_cache = cache_class()
|
||||
logger.info(f"LLM caching setup with {cache_class.__name__}")
|
||||
except ImportError:
|
||||
logger.warning(f"Could not import {cache_type}. ")
|
||||
else:
|
||||
logger.info("No LLM cache set.")
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ from langchain import vectorstores
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class
|
||||
from langflow.settings import settings
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.template.frontend_node.vectorstores import VectorStoreFrontendNode
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_method
|
||||
|
||||
|
||||
|
|
@ -43,10 +44,12 @@ class VectorstoreCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
return [
|
||||
vectorstore
|
||||
for vectorstore in self.type_to_loader_dict.keys()
|
||||
if vectorstore in settings.VECTORSTORES or settings.DEV
|
||||
if vectorstore in settings_manager.settings.VECTORSTORES
|
||||
or settings_manager.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from typing import Dict, List, Optional
|
|||
from langchain import requests, sql_database
|
||||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class, build_template_from_method
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,24 +6,23 @@ from fastapi.responses import FileResponse
|
|||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from langflow.api import router
|
||||
from langflow.database.base import create_db_and_tables, Engine
|
||||
|
||||
|
||||
from langflow.interface.utils import setup_llm_caching
|
||||
from langflow.services.database.utils import initialize_database
|
||||
from langflow.services.manager import initialize_services, teardown_services
|
||||
from langflow.services.plugins.langfuse import LangfuseInstance
|
||||
from langflow.utils.logger import configure
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Create the FastAPI app and include the router."""
|
||||
|
||||
configure()
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
origins = [
|
||||
"*",
|
||||
]
|
||||
|
||||
@app.get("/health")
|
||||
def get_health():
|
||||
return {"status": "OK"}
|
||||
origins = ["*"]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
|
@ -33,10 +32,18 @@ def create_app():
|
|||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
app.include_router(router)
|
||||
app.on_event("startup")(Engine.update)
|
||||
app.on_event("startup")(create_db_and_tables)
|
||||
|
||||
app.on_event("startup")(initialize_services)
|
||||
app.on_event("startup")(initialize_database)
|
||||
app.on_event("startup")(setup_llm_caching)
|
||||
app.on_event("shutdown")(teardown_services)
|
||||
app.on_event("startup")(LangfuseInstance.update)
|
||||
app.on_event("shutdown")(LangfuseInstance.teardown)
|
||||
return app
|
||||
|
||||
|
||||
|
|
@ -68,22 +75,25 @@ def get_static_files_dir():
|
|||
return frontend_path / "frontend"
|
||||
|
||||
|
||||
def setup_app(static_files_dir: Optional[Path] = None) -> FastAPI:
|
||||
def setup_app(
|
||||
static_files_dir: Optional[Path] = None, backend_only: bool = False
|
||||
) -> FastAPI:
|
||||
"""Setup the FastAPI app."""
|
||||
# get the directory of the current file
|
||||
if not static_files_dir:
|
||||
static_files_dir = get_static_files_dir()
|
||||
|
||||
if not static_files_dir or not static_files_dir.exists():
|
||||
if not backend_only and (not static_files_dir or not static_files_dir.exists()):
|
||||
raise RuntimeError(f"Static files directory {static_files_dir} does not exist.")
|
||||
app = create_app()
|
||||
setup_static_files(app, static_files_dir)
|
||||
if not backend_only and static_files_dir is not None:
|
||||
setup_static_files(app, static_files_dir)
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
from langflow.utils.util import get_number_of_workers
|
||||
from langflow.__main__ import get_number_of_workers
|
||||
|
||||
configure()
|
||||
uvicorn.run(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,57 @@
|
|||
from typing import Union
|
||||
from typing import List, Union, TYPE_CHECKING
|
||||
from langflow.api.v1.callback import (
|
||||
AsyncStreamingLLMCallbackHandler,
|
||||
StreamingLLMCallbackHandler,
|
||||
)
|
||||
from langflow.processing.process import fix_memory_inputs, format_actions
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langchain.agents.agent import AgentExecutor
|
||||
from langchain.callbacks.base import BaseCallbackHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langfuse.callback import CallbackHandler # type: ignore
|
||||
|
||||
|
||||
def setup_callbacks(sync, trace_id, **kwargs):
|
||||
"""Setup callbacks for langchain object"""
|
||||
callbacks = []
|
||||
if sync:
|
||||
callbacks.append(StreamingLLMCallbackHandler(**kwargs))
|
||||
else:
|
||||
callbacks.append(AsyncStreamingLLMCallbackHandler(**kwargs))
|
||||
|
||||
if langfuse_callback := get_langfuse_callback(trace_id=trace_id):
|
||||
logger.debug("Langfuse callback loaded")
|
||||
callbacks.append(langfuse_callback)
|
||||
return callbacks
|
||||
|
||||
|
||||
def get_langfuse_callback(trace_id):
|
||||
from langflow.services.plugins.langfuse import LangfuseInstance
|
||||
from langfuse.callback import CreateTrace
|
||||
|
||||
logger.debug("Initializing langfuse callback")
|
||||
if langfuse := LangfuseInstance.get():
|
||||
logger.debug("Langfuse credentials found")
|
||||
try:
|
||||
trace = langfuse.trace(CreateTrace(id=trace_id))
|
||||
return trace.getNewHandler()
|
||||
except Exception as exc:
|
||||
logger.error(f"Error initializing langfuse callback: {exc}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def flush_langfuse_callback_if_present(
|
||||
callbacks: List[Union[BaseCallbackHandler, "CallbackHandler"]]
|
||||
):
|
||||
"""
|
||||
If langfuse callback is present, run callback.langfuse.flush()
|
||||
"""
|
||||
for callback in callbacks:
|
||||
if hasattr(callback, "langfuse"):
|
||||
callback.langfuse.flush()
|
||||
break
|
||||
|
||||
|
||||
async def get_result_and_steps(langchain_object, inputs: Union[dict, str], **kwargs):
|
||||
|
|
@ -20,18 +67,24 @@ async def get_result_and_steps(langchain_object, inputs: Union[dict, str], **kwa
|
|||
# to display intermediate steps
|
||||
langchain_object.return_intermediate_steps = True
|
||||
try:
|
||||
fix_memory_inputs(langchain_object)
|
||||
if not isinstance(langchain_object, AgentExecutor):
|
||||
fix_memory_inputs(langchain_object)
|
||||
except Exception as exc:
|
||||
logger.error(f"Error fixing memory inputs: {exc}")
|
||||
|
||||
try:
|
||||
async_callbacks = [AsyncStreamingLLMCallbackHandler(**kwargs)]
|
||||
output = await langchain_object.acall(inputs, callbacks=async_callbacks)
|
||||
trace_id = kwargs.pop("session_id", None)
|
||||
callbacks = setup_callbacks(sync=False, trace_id=trace_id, **kwargs)
|
||||
output = await langchain_object.acall(inputs, callbacks=callbacks)
|
||||
except Exception as exc:
|
||||
# make the error message more informative
|
||||
logger.debug(f"Error: {str(exc)}")
|
||||
sync_callbacks = [StreamingLLMCallbackHandler(**kwargs)]
|
||||
output = langchain_object(inputs, callbacks=sync_callbacks)
|
||||
trace_id = kwargs.pop("session_id", None)
|
||||
callbacks = setup_callbacks(sync=True, trace_id=trace_id, **kwargs)
|
||||
output = langchain_object(inputs, callbacks=callbacks)
|
||||
|
||||
# if langfuse callback is present, run callback.langfuse.flush()
|
||||
flush_langfuse_callback_if_present(callbacks)
|
||||
|
||||
intermediate_steps = (
|
||||
output.get("intermediate_steps", []) if isinstance(output, dict) else []
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
from langchain.schema import AgentAction
|
||||
import json
|
||||
from langflow.interface.run import (
|
||||
build_sorted_vertices_with_caching,
|
||||
get_memory_key,
|
||||
update_memory_keys,
|
||||
)
|
||||
from langflow.utils.logger import logger
|
||||
from loguru import logger
|
||||
from langflow.graph import Graph
|
||||
from langchain.chains.base import Chain
|
||||
from langchain.vectorstores.base import VectorStore
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from langchain.schema import Document
|
||||
|
||||
|
||||
def fix_memory_inputs(langchain_object):
|
||||
|
|
@ -85,39 +86,55 @@ def get_input_str_if_only_one_input(inputs: dict) -> Optional[str]:
|
|||
return list(inputs.values())[0] if len(inputs) == 1 else None
|
||||
|
||||
|
||||
def process_graph_cached(
|
||||
data_graph: Dict[str, Any], inputs: Optional[dict] = None, clear_cache=False
|
||||
):
|
||||
"""
|
||||
Process graph by extracting input variables and replacing ZeroShotPrompt
|
||||
with PromptTemplate,then run the graph and return the result and thought.
|
||||
"""
|
||||
# Load langchain object
|
||||
def get_build_result(data_graph, session_id):
|
||||
# If session_id is provided, load the langchain_object from the session
|
||||
# using build_sorted_vertices_with_caching.get_result_by_session_id
|
||||
# if it returns something different than None, return it
|
||||
# otherwise, build the graph and return the result
|
||||
if session_id:
|
||||
logger.debug(f"Loading LangChain object from session {session_id}")
|
||||
result = build_sorted_vertices_with_caching.get_result_by_session_id(session_id)
|
||||
if result is not None:
|
||||
logger.debug("Loaded LangChain object")
|
||||
return result
|
||||
|
||||
logger.debug("Building langchain object")
|
||||
return build_sorted_vertices_with_caching(data_graph)
|
||||
|
||||
|
||||
def clear_caches_if_needed(clear_cache: bool):
|
||||
if clear_cache:
|
||||
build_sorted_vertices_with_caching.clear_cache()
|
||||
logger.debug("Cleared cache")
|
||||
langchain_object, artifacts = build_sorted_vertices_with_caching(data_graph)
|
||||
logger.debug("Loaded LangChain object")
|
||||
if inputs is None:
|
||||
inputs = {}
|
||||
|
||||
# Add artifacts to inputs
|
||||
# artifacts can be documents loaded when building
|
||||
# the flow
|
||||
for (
|
||||
key,
|
||||
value,
|
||||
) in artifacts.items():
|
||||
if key not in inputs or not inputs[key]:
|
||||
inputs[key] = value
|
||||
|
||||
def load_langchain_object(
|
||||
data_graph: Dict[str, Any], session_id: str
|
||||
) -> Tuple[Union[Chain, VectorStore], Dict[str, Any], str]:
|
||||
langchain_object, artifacts = get_build_result(data_graph, session_id)
|
||||
session_id = build_sorted_vertices_with_caching.hash
|
||||
logger.debug("Loaded LangChain object")
|
||||
|
||||
if langchain_object is None:
|
||||
# Raise user facing error
|
||||
raise ValueError(
|
||||
"There was an error loading the langchain_object. Please, check all the nodes and try again."
|
||||
)
|
||||
|
||||
# Generate result and thought
|
||||
return langchain_object, artifacts, session_id
|
||||
|
||||
|
||||
def process_inputs(inputs: Optional[dict], artifacts: Dict[str, Any]) -> dict:
|
||||
if inputs is None:
|
||||
inputs = {}
|
||||
|
||||
for key, value in artifacts.items():
|
||||
if key not in inputs or not inputs[key]:
|
||||
inputs[key] = value
|
||||
|
||||
return inputs
|
||||
|
||||
|
||||
def generate_result(langchain_object: Union[Chain, VectorStore], inputs: dict):
|
||||
if isinstance(langchain_object, Chain):
|
||||
if inputs is None:
|
||||
raise ValueError("Inputs must be provided for a Chain")
|
||||
|
|
@ -126,13 +143,33 @@ def process_graph_cached(
|
|||
logger.debug("Generated result and thought")
|
||||
elif isinstance(langchain_object, VectorStore):
|
||||
result = langchain_object.search(**inputs)
|
||||
elif isinstance(langchain_object, Document):
|
||||
result = langchain_object.dict()
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown langchain_object type: {type(langchain_object).__name__}"
|
||||
)
|
||||
logger.warning(f"Unknown langchain_object type: {type(langchain_object)}")
|
||||
result = langchain_object
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def process_graph_cached(
|
||||
data_graph: Dict[str, Any],
|
||||
inputs: Optional[dict] = None,
|
||||
clear_cache=False,
|
||||
session_id=None,
|
||||
) -> Tuple[Any, str]:
|
||||
clear_caches_if_needed(clear_cache)
|
||||
# If session_id is provided, load the langchain_object from the session
|
||||
# else build the graph and return the result and the new session_id
|
||||
langchain_object, artifacts, session_id = load_langchain_object(
|
||||
data_graph, session_id
|
||||
)
|
||||
processed_inputs = process_inputs(inputs, artifacts)
|
||||
result = generate_result(langchain_object, processed_inputs)
|
||||
|
||||
return result, session_id
|
||||
|
||||
|
||||
def load_flow_from_json(
|
||||
flow: Union[Path, str, dict], tweaks: Optional[dict] = None, build=True
|
||||
):
|
||||
|
|
|
|||
4
src/backend/langflow/services/__init__.py
Normal file
4
src/backend/langflow/services/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .manager import service_manager
|
||||
from .schema import ServiceType
|
||||
|
||||
__all__ = ["service_manager", "ServiceType"]
|
||||
0
src/backend/langflow/services/auth/__init__.py
Normal file
0
src/backend/langflow/services/auth/__init__.py
Normal file
12
src/backend/langflow/services/auth/factory.py
Normal file
12
src/backend/langflow/services/auth/factory.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from langflow.services.factory import ServiceFactory
|
||||
from langflow.services.auth.service import AuthManager
|
||||
|
||||
|
||||
class AuthManagerFactory(ServiceFactory):
|
||||
name = "auth_manager"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(AuthManager)
|
||||
|
||||
def create(self, settings_manager):
|
||||
return AuthManager(settings_manager)
|
||||
12
src/backend/langflow/services/auth/service.py
Normal file
12
src/backend/langflow/services/auth/service.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from langflow.services.base import Service
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.settings.manager import SettingsManager
|
||||
|
||||
|
||||
class AuthManager(Service):
|
||||
name = "auth_manager"
|
||||
|
||||
def __init__(self, settings_manager: "SettingsManager"):
|
||||
self.settings_manager = settings_manager
|
||||
298
src/backend/langflow/services/auth/utils.py
Normal file
298
src/backend/langflow/services/auth/utils.py
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import Depends, HTTPException, Security, status
|
||||
from fastapi.security import APIKeyHeader, APIKeyQuery, OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from typing import Annotated, Coroutine, Optional, Union
|
||||
from uuid import UUID
|
||||
from langflow.services.database.models.api_key.api_key import ApiKey
|
||||
from langflow.services.database.models.api_key.crud import check_key
|
||||
from langflow.services.database.models.user.user import User
|
||||
from langflow.services.database.models.user.crud import (
|
||||
get_user_by_id,
|
||||
get_user_by_username,
|
||||
update_user_last_login_at,
|
||||
)
|
||||
from langflow.services.utils import get_session, get_settings_manager
|
||||
from sqlmodel import Session
|
||||
|
||||
oauth2_login = OAuth2PasswordBearer(tokenUrl="api/v1/login")
|
||||
|
||||
API_KEY_NAME = "api-key"
|
||||
|
||||
api_key_query = APIKeyQuery(
|
||||
name=API_KEY_NAME, scheme_name="API key query", auto_error=False
|
||||
)
|
||||
api_key_header = APIKeyHeader(
|
||||
name=API_KEY_NAME, scheme_name="API key header", auto_error=False
|
||||
)
|
||||
|
||||
|
||||
# Source: https://github.com/mrtolkien/fastapi_simple_security/blob/master/fastapi_simple_security/security_api_key.py
|
||||
async def api_key_security(
|
||||
query_param: str = Security(api_key_query),
|
||||
header_param: str = Security(api_key_header),
|
||||
db: Session = Depends(get_session),
|
||||
) -> Optional[User]:
|
||||
settings_manager = get_settings_manager()
|
||||
result: Optional[Union[ApiKey, User]] = None
|
||||
if settings_manager.auth_settings.AUTO_LOGIN:
|
||||
# Get the first user
|
||||
if not settings_manager.auth_settings.FIRST_SUPERUSER:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Missing first superuser credentials",
|
||||
)
|
||||
|
||||
result = get_user_by_username(
|
||||
db, settings_manager.auth_settings.FIRST_SUPERUSER
|
||||
)
|
||||
|
||||
elif not query_param and not header_param:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="An API key must be passed as query or header",
|
||||
)
|
||||
|
||||
elif query_param:
|
||||
result = check_key(db, query_param)
|
||||
|
||||
else:
|
||||
result = check_key(db, header_param)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid or missing API key",
|
||||
)
|
||||
if isinstance(result, ApiKey):
|
||||
return result.user
|
||||
elif isinstance(result, User):
|
||||
return result
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
token: Annotated[str, Depends(oauth2_login)],
|
||||
db: Session = Depends(get_session),
|
||||
) -> User:
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if isinstance(token, Coroutine):
|
||||
token = await token
|
||||
|
||||
if settings_manager.auth_settings.SECRET_KEY is None:
|
||||
raise credentials_exception
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings_manager.auth_settings.SECRET_KEY,
|
||||
algorithms=[settings_manager.auth_settings.ALGORITHM],
|
||||
)
|
||||
user_id: UUID = payload.get("sub") # type: ignore
|
||||
token_type: str = payload.get("type") # type: ignore
|
||||
if expires := payload.get("exp", None):
|
||||
expires_datetime = datetime.fromtimestamp(expires, timezone.utc)
|
||||
# TypeError: can't compare offset-naive and offset-aware datetimes
|
||||
if datetime.now(timezone.utc) > expires_datetime:
|
||||
raise credentials_exception
|
||||
|
||||
if user_id is None or token_type:
|
||||
raise credentials_exception
|
||||
except JWTError as e:
|
||||
raise credentials_exception from e
|
||||
|
||||
user = get_user_by_id(db, user_id) # type: ignore
|
||||
if user is None or not user.is_active:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
|
||||
def get_current_active_user(current_user: Annotated[User, Depends(get_current_user)]):
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
def get_current_active_superuser(
|
||||
current_user: Annotated[User, Depends(get_current_user)]
|
||||
) -> User:
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=401, detail="Inactive user")
|
||||
if not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
settings_manager = get_settings_manager()
|
||||
return settings_manager.auth_settings.pwd_context.verify(
|
||||
plain_password, hashed_password
|
||||
)
|
||||
|
||||
|
||||
def get_password_hash(password):
|
||||
settings_manager = get_settings_manager()
|
||||
return settings_manager.auth_settings.pwd_context.hash(password)
|
||||
|
||||
|
||||
def create_token(data: dict, expires_delta: timedelta):
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
to_encode["exp"] = expire
|
||||
|
||||
return jwt.encode(
|
||||
to_encode,
|
||||
settings_manager.auth_settings.SECRET_KEY,
|
||||
algorithm=settings_manager.auth_settings.ALGORITHM,
|
||||
)
|
||||
|
||||
|
||||
def create_super_user(
|
||||
username: str,
|
||||
password: str,
|
||||
db: Session = Depends(get_session),
|
||||
) -> User:
|
||||
super_user = get_user_by_username(db, username)
|
||||
|
||||
if not super_user:
|
||||
super_user = User(
|
||||
username=username,
|
||||
password=get_password_hash(password),
|
||||
is_superuser=True,
|
||||
is_active=True,
|
||||
last_login_at=None,
|
||||
)
|
||||
|
||||
db.add(super_user)
|
||||
db.commit()
|
||||
db.refresh(super_user)
|
||||
|
||||
return super_user
|
||||
|
||||
|
||||
def create_user_longterm_token(db: Session = Depends(get_session)) -> dict:
|
||||
settings_manager = get_settings_manager()
|
||||
username = settings_manager.auth_settings.FIRST_SUPERUSER
|
||||
password = settings_manager.auth_settings.FIRST_SUPERUSER_PASSWORD
|
||||
if not username or not password:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Missing first superuser credentials",
|
||||
)
|
||||
super_user = create_super_user(db=db, username=username, password=password)
|
||||
|
||||
access_token_expires_longterm = timedelta(days=365)
|
||||
access_token = create_token(
|
||||
data={"sub": str(super_user.id)},
|
||||
expires_delta=access_token_expires_longterm,
|
||||
)
|
||||
|
||||
# Update: last_login_at
|
||||
update_user_last_login_at(super_user.id, db)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": None,
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
|
||||
def create_user_api_key(user_id: UUID) -> dict:
|
||||
access_token = create_token(
|
||||
data={"sub": str(user_id), "role": "api_key"},
|
||||
expires_delta=timedelta(days=365 * 2),
|
||||
)
|
||||
|
||||
return {"api_key": access_token}
|
||||
|
||||
|
||||
def get_user_id_from_token(token: str) -> UUID:
|
||||
try:
|
||||
user_id = jwt.get_unverified_claims(token)["sub"]
|
||||
return UUID(user_id)
|
||||
except (KeyError, JWTError, ValueError):
|
||||
return UUID(int=0)
|
||||
|
||||
|
||||
def create_user_tokens(
|
||||
user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False
|
||||
) -> dict:
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
access_token_expires = timedelta(
|
||||
minutes=settings_manager.auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
access_token = create_token(
|
||||
data={"sub": str(user_id)},
|
||||
expires_delta=access_token_expires,
|
||||
)
|
||||
|
||||
refresh_token_expires = timedelta(
|
||||
minutes=settings_manager.auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
refresh_token = create_token(
|
||||
data={"sub": str(user_id), "type": "rf"},
|
||||
expires_delta=refresh_token_expires,
|
||||
)
|
||||
|
||||
# Update: last_login_at
|
||||
if update_last_login:
|
||||
update_user_last_login_at(user_id, db)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
|
||||
def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)):
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
refresh_token,
|
||||
settings_manager.auth_settings.SECRET_KEY,
|
||||
algorithms=[settings_manager.auth_settings.ALGORITHM],
|
||||
)
|
||||
user_id: UUID = payload.get("sub") # type: ignore
|
||||
token_type: str = payload.get("type") # type: ignore
|
||||
|
||||
if user_id is None or token_type is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
|
||||
)
|
||||
|
||||
return create_user_tokens(user_id, db)
|
||||
|
||||
except JWTError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token",
|
||||
) from e
|
||||
|
||||
|
||||
def authenticate_user(
|
||||
username: str, password: str, db: Session = Depends(get_session)
|
||||
) -> Optional[User]:
|
||||
user = get_user_by_username(db, username)
|
||||
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if not user.is_active:
|
||||
if not user.last_login_at:
|
||||
raise HTTPException(status_code=400, detail="Waiting for approval")
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
|
||||
return user if verify_password(password, user.password) else None
|
||||
8
src/backend/langflow/services/base.py
Normal file
8
src/backend/langflow/services/base.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from abc import ABC
|
||||
|
||||
|
||||
class Service(ABC):
|
||||
name: str
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
11
src/backend/langflow/services/cache/__init__.py
vendored
Normal file
11
src/backend/langflow/services/cache/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from . import factory, manager
|
||||
from langflow.services.cache.manager import cache_manager
|
||||
from langflow.services.cache.flow import InMemoryCache
|
||||
|
||||
|
||||
__all__ = [
|
||||
"cache_manager",
|
||||
"factory",
|
||||
"manager",
|
||||
"InMemoryCache",
|
||||
]
|
||||
11
src/backend/langflow/services/cache/factory.py
vendored
Normal file
11
src/backend/langflow/services/cache/factory.py
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from langflow.services.cache.manager import CacheManager
|
||||
from langflow.services.factory import ServiceFactory
|
||||
|
||||
|
||||
class CacheManagerFactory(ServiceFactory):
|
||||
def __init__(self):
|
||||
super().__init__(CacheManager)
|
||||
|
||||
def create(self):
|
||||
# Here you would have logic to create and configure a CacheManager
|
||||
return CacheManager()
|
||||
|
|
@ -2,7 +2,7 @@ import threading
|
|||
import time
|
||||
from collections import OrderedDict
|
||||
|
||||
from langflow.cache.base import BaseCache
|
||||
from langflow.services.cache.base import BaseCache
|
||||
|
||||
|
||||
class InMemoryCache(BaseCache):
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
from contextlib import contextmanager
|
||||
from typing import Any, Awaitable, Callable, List, Optional
|
||||
from langflow.services.base import Service
|
||||
|
||||
import pandas as pd
|
||||
from PIL import Image
|
||||
|
|
@ -49,9 +50,11 @@ class AsyncSubject:
|
|||
await observer()
|
||||
|
||||
|
||||
class CacheManager(Subject):
|
||||
class CacheManager(Subject, Service):
|
||||
"""Manages cache for different clients and notifies observers on changes."""
|
||||
|
||||
name = "cache_manager"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._cache = {}
|
||||
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