From 8d662ad9b4410e275404294dea38331d9d07cd34 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 18:18:26 -0300 Subject: [PATCH 01/45] =?UTF-8?q?=F0=9F=94=A7=20chore(pyproject.toml):=20a?= =?UTF-8?q?dd=20alembic=20as=20a=20development=20dependency=20to=20manage?= =?UTF-8?q?=20database=20migrations=20=E2=AC=86=EF=B8=8F=20feat(pyproject.?= =?UTF-8?q?toml):=20upgrade=20alembic=20to=20version=201.11.2=20to=20ensur?= =?UTF-8?q?e=20compatibility=20with=20other=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 42 ++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 809dab71e..fc7748707 100644 --- a/poetry.lock +++ b/poetry.lock @@ -144,6 +144,25 @@ files = [ {file = "aiostream-0.4.5.tar.gz", hash = "sha256:3ecbf87085230fbcd9605c32ca20c4fb41af02c71d076eab246ea22e35947d88"}, ] +[[package]] +name = "alembic" +version = "1.11.2" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.7" +files = [ + {file = "alembic-1.11.2-py3-none-any.whl", hash = "sha256:7981ab0c4fad4fe1be0cf183aae17689fe394ff874fd2464adb774396faf0796"}, + {file = "alembic-1.11.2.tar.gz", hash = "sha256:678f662130dc540dac12de0ea73de9f89caea9dbea138f60ef6263149bf84657"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" +typing-extensions = ">=4" + +[package.extras] +tz = ["python-dateutil"] + [[package]] name = "anthropic" version = "0.3.8" @@ -3147,6 +3166,25 @@ docs = ["sphinx (>=1.6.0)", "sphinx-bootstrap-theme"] flake8 = ["flake8"] tests = ["psutil", "pytest (!=3.3.0)", "pytest-cov"] +[[package]] +name = "mako" +version = "1.2.4" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, + {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + [[package]] name = "markdown" version = "3.4.4" @@ -3195,7 +3233,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, @@ -7580,4 +7618,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.11" -content-hash = "57abce2ebcdc3cd7e359c36805822b9398d3bfb500c175b173a6d784d1276df6" +content-hash = "51cb6dada892cc1b5d2800130a3bd95a475f471dc0538ebfb73d96f4dd1f1dc4" diff --git a/pyproject.toml b/pyproject.toml index a29ae46ca..81df39c7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ psycopg = "^3.1.9" psycopg-binary = "^3.1.9" fastavro = "^1.8.0" langchain-experimental = "^0.0.8" +alembic = "^1.11.2" [tool.poetry.group.dev.dependencies] black = "^23.1.0" From 46f289b5d0dbe099349ed971b3f7fc47f8a4098c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 18:20:37 -0300 Subject: [PATCH 02/45] =?UTF-8?q?=F0=9F=93=9D=20chore(alembic.ini):=20add?= =?UTF-8?q?=20Alembic=20configuration=20file=20for=20database=20migrations?= =?UTF-8?q?=20=F0=9F=93=9D=20chore(alembic/README):=20add=20README=20file?= =?UTF-8?q?=20for=20Alembic=20migrations=20=F0=9F=93=9D=20chore(alembic/en?= =?UTF-8?q?v.py):=20add=20Alembic=20environment=20configuration=20file=20?= =?UTF-8?q?=F0=9F=93=9D=20chore(alembic/script.py.mako):=20add=20Alembic?= =?UTF-8?q?=20migration=20script=20template=20=F0=9F=93=9D=20chore(main.py?= =?UTF-8?q?):=20refactor=20database=20initialization=20and=20migration=20l?= =?UTF-8?q?ogic=20to=20use=20DatabaseManager=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/alembic.ini | 113 ++++++++++++++++++++ src/backend/langflow/alembic/README | 1 + src/backend/langflow/alembic/env.py | 78 ++++++++++++++ src/backend/langflow/alembic/script.py.mako | 27 +++++ src/backend/langflow/main.py | 11 +- 5 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 src/backend/langflow/alembic.ini create mode 100644 src/backend/langflow/alembic/README create mode 100644 src/backend/langflow/alembic/env.py create mode 100644 src/backend/langflow/alembic/script.py.mako diff --git a/src/backend/langflow/alembic.ini b/src/backend/langflow/alembic.ini new file mode 100644 index 000000000..0227ea4f2 --- /dev/null +++ b/src/backend/langflow/alembic.ini @@ -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 a placeholder to run the first migration +# 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 diff --git a/src/backend/langflow/alembic/README b/src/backend/langflow/alembic/README new file mode 100644 index 000000000..98e4f9c44 --- /dev/null +++ b/src/backend/langflow/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/src/backend/langflow/alembic/env.py b/src/backend/langflow/alembic/env.py new file mode 100644 index 000000000..ea4fe9c43 --- /dev/null +++ b/src/backend/langflow/alembic/env.py @@ -0,0 +1,78 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +from langflow.database.base 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"}, + ) + + 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) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/src/backend/langflow/alembic/script.py.mako b/src/backend/langflow/alembic/script.py.mako new file mode 100644 index 000000000..6ce335109 --- /dev/null +++ b/src/backend/langflow/alembic/script.py.mako @@ -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"} diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index 5b3341693..deef1c914 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -6,13 +6,15 @@ 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.database.base import DatabaseManager from langflow.interface.utils import setup_llm_caching from langflow.utils.logger import configure def create_app(): """Create the FastAPI app and include the router.""" + from langflow.settings import settings + configure() app = FastAPI() @@ -32,10 +34,11 @@ def create_app(): allow_methods=["*"], allow_headers=["*"], ) - + database_manager = DatabaseManager(settings.DATABASE_URL) app.include_router(router) - app.on_event("startup")(Engine.update) - app.on_event("startup")(create_db_and_tables) + # app.on_event("startup")(Engine.update) + app.on_event("startup")(database_manager.run_migrations) + app.on_event("startup")(database_manager.create_db_and_tables) app.on_event("startup")(setup_llm_caching) return app From cd67aa212cd635fe20c1f8c5f4bc207e4dd46579 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 18:33:17 -0300 Subject: [PATCH 03/45] =?UTF-8?q?=F0=9F=93=A6=20chore(alembic):=20add=20mi?= =?UTF-8?q?gration=20script=20to=20create=20Flow=20table=20and=20FlowStyle?= =?UTF-8?q?=20table=20=F0=9F=94=A7=20refactor(base.py):=20refactor=20Datab?= =?UTF-8?q?aseManager=20class=20to=20handle=20database=20operations=20and?= =?UTF-8?q?=20migrations=20=F0=9F=94=A7=20refactor(base.py):=20refactor=20?= =?UTF-8?q?session=5Fgetter=20function=20to=20use=20DatabaseManager=20inst?= =?UTF-8?q?ance=20=F0=9F=94=A7=20refactor(base.py):=20refactor=20get=5Fses?= =?UTF-8?q?sion=20function=20to=20use=20DatabaseManager=20instance=20?= =?UTF-8?q?=F0=9F=94=A7=20refactor(models/=5F=5Finit=5F=5F.py):=20add=20Fl?= =?UTF-8?q?ow=20model=20to=20=5F=5Fall=5F=5F=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../versions/4814b6f4abfd_add_flow_table.py | 65 +++++++++++++++++ src/backend/langflow/database/base.py | 71 ++++++++++++++++--- .../langflow/database/models/__init__.py | 4 ++ 3 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 src/backend/langflow/alembic/versions/4814b6f4abfd_add_flow_table.py diff --git a/src/backend/langflow/alembic/versions/4814b6f4abfd_add_flow_table.py b/src/backend/langflow/alembic/versions/4814b6f4abfd_add_flow_table.py new file mode 100644 index 000000000..0b2f32657 --- /dev/null +++ b/src/backend/langflow/alembic/versions/4814b6f4abfd_add_flow_table.py @@ -0,0 +1,65 @@ +"""Add Flow table + +Revision ID: 4814b6f4abfd +Revises: +Create Date: 2023-08-05 17:47:42.879824 + +""" + +import contextlib +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = "4814b6f4abfd" +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! ### + + # This suppress is used to not break the migration if the table already exists. + with contextlib.suppress(sa.exc.OperationalError): + 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.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + ) + op.create_index( + op.f("ix_flow_description"), "flow", ["description"], unique=False + ) + op.create_index(op.f("ix_flow_name"), "flow", ["name"], unique=False) + with contextlib.suppress(sa.exc.OperationalError): + op.create_table( + "flowstyle", + sa.Column("color", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("emoji", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("flow_id", sqlmodel.sql.sqltypes.GUID(), nullable=True), + sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False), + sa.ForeignKeyConstraint( + ["flow_id"], + ["flow.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("flowstyle") + op.drop_index(op.f("ix_flow_name"), table_name="flow") + op.drop_index(op.f("ix_flow_description"), table_name="flow") + op.drop_table("flow") + # ### end Alembic commands ### diff --git a/src/backend/langflow/database/base.py b/src/backend/langflow/database/base.py index 546c341c1..518b95d15 100644 --- a/src/backend/langflow/database/base.py +++ b/src/backend/langflow/database/base.py @@ -1,8 +1,11 @@ from contextlib import contextmanager import os - +from pathlib import Path +from langflow.database import models # noqa from sqlmodel import SQLModel, Session, create_engine from langflow.utils.logger import logger +from alembic.config import Config +from alembic import command class Engine: @@ -60,10 +63,67 @@ def create_db_and_tables(): logger.debug("Database and tables created successfully") +class DatabaseManager: + def __init__(self, database_url: str): + self.database_url = database_url + # This file is in langflow.database.base.py + # the ini is in langflow + self.script_location = Path(__file__).parent.parent / "alembic" + self.alembic_cfg_path = Path(__file__).parent.parent / "alembic.ini" + self.engine = create_engine(database_url) + + def __enter__(self): + self._session = Session(self.engine) + return self._session + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is not None: # If an exception has been raised + logger.error( + f"Session rollback because of exception: {exc_type.__name__} {exc_value}" + ) + self._session.rollback() + else: + self._session.commit() + self._session.close() + + def get_session(self): + with Session(self.engine) as session: + yield session + + def run_migrations(self): + logger.info( + f"Running DB migrations in {self.script_location} on {self.database_url}" + ) + alembic_cfg = Config() + alembic_cfg.set_main_option("script_location", str(self.script_location)) + alembic_cfg.set_main_option("sqlalchemy.url", self.database_url) + command.upgrade(alembic_cfg, "head") + + def create_db_and_tables(self): + logger.debug("Creating database and tables") + try: + SQLModel.metadata.create_all(self.engine) + 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(self.engine) + 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(): +def session_getter(db_manager: DatabaseManager): try: - session = Session(Engine.get()) + session = Session(DatabaseManager.engine) yield session except Exception as e: print("Session rollback because of exception:", e) @@ -71,8 +131,3 @@ def session_getter(): raise finally: session.close() - - -def get_session(): - with session_getter() as session: - yield session diff --git a/src/backend/langflow/database/models/__init__.py b/src/backend/langflow/database/models/__init__.py index e69de29bb..da47bc5fe 100644 --- a/src/backend/langflow/database/models/__init__.py +++ b/src/backend/langflow/database/models/__init__.py @@ -0,0 +1,4 @@ +from .flow import Flow + + +__all__ = ["Flow"] From d566a86ed07b8802a83d9b33c2378fc1b1821bed Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 22:03:56 -0300 Subject: [PATCH 04/45] =?UTF-8?q?=F0=9F=94=A5=20refactor(cache):=20remove?= =?UTF-8?q?=20unused=20cache=20files=20and=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The following files and classes were removed: - `src/backend/langflow/cache/__init__.py`: Removed unused import statements and `__all__` variable. - `src/backend/langflow/cache/base.py`: Removed unused `BaseCache` class. - `src/backend/langflow/cache/flow.py`: Removed unused `InMemoryCache` class. - `src/backend/langflow/cache/manager.py`: Removed unused `Subject`, `AsyncSubject`, and `CacheManager` classes. These files and classes were removed to clean up the codebase and remove unused functionality. πŸ”₯ refactor(utils.py): remove unused code and dependencies in utils.py module πŸ”₯ refactor(chat): remove unused chat module and its configuration class πŸ”₯ refactor(chat/manager.py): remove unused imports and classes from chat manager module πŸ”₯ refactor(chat/utils.py): remove unused imports and function from chat utils module πŸ”₯ refactor(database/__init__.py): remove empty file πŸ”₯ refactor(database): remove unused database files and models πŸ”₯ refactor(database): remove unused database files and models to improve code organization and reduce clutter --- src/backend/langflow/cache/__init__.py | 7 - src/backend/langflow/cache/base.py | 84 ------- src/backend/langflow/cache/flow.py | 146 ------------ src/backend/langflow/cache/manager.py | 150 ------------ src/backend/langflow/cache/utils.py | 179 --------------- src/backend/langflow/chat/__init__.py | 0 src/backend/langflow/chat/config.py | 2 - src/backend/langflow/chat/manager.py | 217 ------------------ src/backend/langflow/chat/utils.py | 37 --- src/backend/langflow/database/__init__.py | 0 src/backend/langflow/database/base.py | 133 ----------- .../langflow/database/models/__init__.py | 4 - src/backend/langflow/database/models/base.py | 14 -- .../langflow/database/models/component.py | 29 --- src/backend/langflow/database/models/flow.py | 60 ----- .../langflow/database/models/flow_style.py | 33 --- 16 files changed, 1095 deletions(-) delete mode 100644 src/backend/langflow/cache/__init__.py delete mode 100644 src/backend/langflow/cache/base.py delete mode 100644 src/backend/langflow/cache/flow.py delete mode 100644 src/backend/langflow/cache/manager.py delete mode 100644 src/backend/langflow/cache/utils.py delete mode 100644 src/backend/langflow/chat/__init__.py delete mode 100644 src/backend/langflow/chat/config.py delete mode 100644 src/backend/langflow/chat/manager.py delete mode 100644 src/backend/langflow/chat/utils.py delete mode 100644 src/backend/langflow/database/__init__.py delete mode 100644 src/backend/langflow/database/base.py delete mode 100644 src/backend/langflow/database/models/__init__.py delete mode 100644 src/backend/langflow/database/models/base.py delete mode 100644 src/backend/langflow/database/models/component.py delete mode 100644 src/backend/langflow/database/models/flow.py delete mode 100644 src/backend/langflow/database/models/flow_style.py diff --git a/src/backend/langflow/cache/__init__.py b/src/backend/langflow/cache/__init__.py deleted file mode 100644 index 723aa9e18..000000000 --- a/src/backend/langflow/cache/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from langflow.cache.manager import cache_manager -from langflow.cache.flow import InMemoryCache - -__all__ = [ - "cache_manager", - "InMemoryCache", -] diff --git a/src/backend/langflow/cache/base.py b/src/backend/langflow/cache/base.py deleted file mode 100644 index 88cb3a1da..000000000 --- a/src/backend/langflow/cache/base.py +++ /dev/null @@ -1,84 +0,0 @@ -import abc - - -class BaseCache(abc.ABC): - """ - Abstract base class for a cache. - """ - - @abc.abstractmethod - def get(self, key): - """ - Retrieve an item from the cache. - - Args: - key: The key of the item to retrieve. - - Returns: - The value associated with the key, or None if the key is not found. - """ - - @abc.abstractmethod - def set(self, key, value): - """ - Add an item to the cache. - - Args: - key: The key of the item. - value: The value to cache. - """ - - @abc.abstractmethod - def delete(self, key): - """ - Remove an item from the cache. - - Args: - key: The key of the item to remove. - """ - - @abc.abstractmethod - def clear(self): - """ - Clear all items from the cache. - """ - - @abc.abstractmethod - def __contains__(self, key): - """ - Check if the key is in the cache. - - Args: - key: The key of the item to check. - - Returns: - True if the key is in the cache, False otherwise. - """ - - @abc.abstractmethod - def __getitem__(self, key): - """ - Retrieve an item from the cache using the square bracket notation. - - Args: - key: The key of the item to retrieve. - """ - - @abc.abstractmethod - def __setitem__(self, key, value): - """ - Add an item to the cache using the square bracket notation. - - Args: - key: The key of the item. - value: The value to cache. - """ - - @abc.abstractmethod - def __delitem__(self, key): - """ - Remove an item from the cache using the square bracket notation. - - Args: - key: The key of the item to remove. - """ diff --git a/src/backend/langflow/cache/flow.py b/src/backend/langflow/cache/flow.py deleted file mode 100644 index 6d8fee977..000000000 --- a/src/backend/langflow/cache/flow.py +++ /dev/null @@ -1,146 +0,0 @@ -import threading -import time -from collections import OrderedDict - -from langflow.cache.base import BaseCache - - -class InMemoryCache(BaseCache): - """ - A simple in-memory cache using an OrderedDict. - - This cache supports setting a maximum size and expiration time for cached items. - When the cache is full, it uses a Least Recently Used (LRU) eviction policy. - Thread-safe using a threading Lock. - - Attributes: - max_size (int, optional): Maximum number of items to store in the cache. - expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour. - - Example: - - cache = InMemoryCache(max_size=3, expiration_time=5) - - # setting cache values - cache.set("a", 1) - cache.set("b", 2) - cache["c"] = 3 - - # getting cache values - a = cache.get("a") - b = cache["b"] - """ - - def __init__(self, max_size=None, expiration_time=60 * 60): - """ - Initialize a new InMemoryCache instance. - - Args: - max_size (int, optional): Maximum number of items to store in the cache. - expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour. - """ - self._cache = OrderedDict() - self._lock = threading.Lock() - self.max_size = max_size - self.expiration_time = expiration_time - - def get(self, key): - """ - Retrieve an item from the cache. - - Args: - key: The key of the item to retrieve. - - Returns: - The value associated with the key, or None if the key is not found or the item has expired. - """ - with self._lock: - if key in self._cache: - item = self._cache.pop(key) - if ( - self.expiration_time is None - or time.time() - item["time"] < self.expiration_time - ): - # Move the key to the end to make it recently used - self._cache[key] = item - return item["value"] - else: - self.delete(key) - return None - - def set(self, key, value): - """ - Add an item to the cache. - - If the cache is full, the least recently used item is evicted. - - Args: - key: The key of the item. - value: The value to cache. - """ - with self._lock: - if key in self._cache: - # Remove existing key before re-inserting to update order - self.delete(key) - elif self.max_size and len(self._cache) >= self.max_size: - # Remove least recently used item - self._cache.popitem(last=False) - self._cache[key] = {"value": value, "time": time.time()} - - def get_or_set(self, key, value): - """ - Retrieve an item from the cache. If the item does not exist, set it with the provided value. - - Args: - key: The key of the item. - value: The value to cache if the item doesn't exist. - - Returns: - The cached value associated with the key. - """ - with self._lock: - if key in self._cache: - return self.get(key) - self.set(key, value) - return value - - def delete(self, key): - """ - Remove an item from the cache. - - Args: - key: The key of the item to remove. - """ - # with self._lock: - self._cache.pop(key, None) - - def clear(self): - """ - Clear all items from the cache. - """ - with self._lock: - self._cache.clear() - - def __contains__(self, key): - """Check if the key is in the cache.""" - return key in self._cache - - def __getitem__(self, key): - """Retrieve an item from the cache using the square bracket notation.""" - return self.get(key) - - def __setitem__(self, key, value): - """Add an item to the cache using the square bracket notation.""" - self.set(key, value) - - def __delitem__(self, key): - """Remove an item from the cache using the square bracket notation.""" - self.delete(key) - - def __len__(self): - """Return the number of items in the cache.""" - return len(self._cache) - - def __repr__(self): - """Return a string representation of the InMemoryCache instance.""" - return f"InMemoryCache(max_size={self.max_size}, expiration_time={self.expiration_time})" diff --git a/src/backend/langflow/cache/manager.py b/src/backend/langflow/cache/manager.py deleted file mode 100644 index 13b281008..000000000 --- a/src/backend/langflow/cache/manager.py +++ /dev/null @@ -1,150 +0,0 @@ -from contextlib import contextmanager -from typing import Any, Awaitable, Callable, List, Optional - -import pandas as pd -from PIL import Image - - -class Subject: - """Base class for implementing the observer pattern.""" - - def __init__(self): - self.observers: List[Callable[[], None]] = [] - - def attach(self, observer: Callable[[], None]): - """Attach an observer to the subject.""" - self.observers.append(observer) - - def detach(self, observer: Callable[[], None]): - """Detach an observer from the subject.""" - self.observers.remove(observer) - - def notify(self): - """Notify all observers about an event.""" - for observer in self.observers: - if observer is None: - continue - observer() - - -class AsyncSubject: - """Base class for implementing the async observer pattern.""" - - def __init__(self): - self.observers: List[Callable[[], Awaitable]] = [] - - def attach(self, observer: Callable[[], Awaitable]): - """Attach an observer to the subject.""" - self.observers.append(observer) - - def detach(self, observer: Callable[[], Awaitable]): - """Detach an observer from the subject.""" - self.observers.remove(observer) - - async def notify(self): - """Notify all observers about an event.""" - for observer in self.observers: - if observer is None: - continue - await observer() - - -class CacheManager(Subject): - """Manages cache for different clients and notifies observers on changes.""" - - def __init__(self): - super().__init__() - self._cache = {} - self.current_client_id = None - self.current_cache = {} - - @contextmanager - def set_client_id(self, client_id: str): - """ - Context manager to set the current client_id and associated cache. - - Args: - client_id (str): The client identifier. - """ - previous_client_id = self.current_client_id - self.current_client_id = client_id - self.current_cache = self._cache.setdefault(client_id, {}) - try: - yield - finally: - self.current_client_id = previous_client_id - self.current_cache = self._cache.get(self.current_client_id, {}) - - def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None): - """ - Add an object to the current client's cache. - - Args: - name (str): The cache key. - obj (Any): The object to cache. - obj_type (str): The type of the object. - """ - object_extensions = { - "image": "png", - "pandas": "csv", - } - if obj_type in object_extensions: - _extension = object_extensions[obj_type] - else: - _extension = type(obj).__name__.lower() - self.current_cache[name] = { - "obj": obj, - "type": obj_type, - "extension": extension or _extension, - } - self.notify() - - def add_pandas(self, name: str, obj: Any): - """ - Add a pandas DataFrame or Series to the current client's cache. - - Args: - name (str): The cache key. - obj (Any): The pandas DataFrame or Series object. - """ - if isinstance(obj, (pd.DataFrame, pd.Series)): - self.add(name, obj.to_csv(), "pandas", extension="csv") - else: - raise ValueError("Object is not a pandas DataFrame or Series") - - def add_image(self, name: str, obj: Any, extension: str = "png"): - """ - Add a PIL Image to the current client's cache. - - Args: - name (str): The cache key. - obj (Any): The PIL Image object. - """ - if isinstance(obj, Image.Image): - self.add(name, obj, "image", extension=extension) - else: - raise ValueError("Object is not a PIL Image") - - def get(self, name: str): - """ - Get an object from the current client's cache. - - Args: - name (str): The cache key. - - Returns: - The cached object associated with the given cache key. - """ - return self.current_cache[name] - - def get_last(self): - """ - Get the last added item in the current client's cache. - - Returns: - The last added item in the cache. - """ - return list(self.current_cache.values())[-1] - - -cache_manager = CacheManager() diff --git a/src/backend/langflow/cache/utils.py b/src/backend/langflow/cache/utils.py deleted file mode 100644 index 3deabe9f4..000000000 --- a/src/backend/langflow/cache/utils.py +++ /dev/null @@ -1,179 +0,0 @@ -import base64 -import contextlib -import functools -import hashlib -import json -import os -import tempfile -from collections import OrderedDict -from pathlib import Path -from typing import Any, Dict -from appdirs import user_cache_dir - -CACHE: Dict[str, Any] = {} - -CACHE_DIR = user_cache_dir("langflow", "langflow") - - -def create_cache_folder(func): - def wrapper(*args, **kwargs): - # Get the destination folder - cache_path = Path(CACHE_DIR) / PREFIX - - # Create the destination folder if it doesn't exist - os.makedirs(cache_path, exist_ok=True) - - return func(*args, **kwargs) - - return wrapper - - -def memoize_dict(maxsize=128): - cache = OrderedDict() - - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - hashed = compute_dict_hash(args[0]) - key = (func.__name__, hashed, frozenset(kwargs.items())) - if key not in cache: - result = func(*args, **kwargs) - cache[key] = result - if len(cache) > maxsize: - cache.popitem(last=False) - else: - result = cache[key] - return result - - def clear_cache(): - cache.clear() - - wrapper.clear_cache = clear_cache # type: ignore - wrapper.cache = cache # type: ignore - return wrapper - - return decorator - - -PREFIX = "langflow_cache" - - -@create_cache_folder -def clear_old_cache_files(max_cache_size: int = 3): - cache_dir = Path(tempfile.gettempdir()) / PREFIX - cache_files = list(cache_dir.glob("*.dill")) - - if len(cache_files) > max_cache_size: - cache_files_sorted_by_mtime = sorted( - cache_files, key=lambda x: x.stat().st_mtime, reverse=True - ) - - for cache_file in cache_files_sorted_by_mtime[max_cache_size:]: - with contextlib.suppress(OSError): - os.remove(cache_file) - - -def compute_dict_hash(graph_data): - graph_data = filter_json(graph_data) - - cleaned_graph_json = json.dumps(graph_data, sort_keys=True) - return hashlib.sha256(cleaned_graph_json.encode("utf-8")).hexdigest() - - -def filter_json(json_data): - filtered_data = json_data.copy() - - # Remove 'viewport' and 'chatHistory' keys - if "viewport" in filtered_data: - del filtered_data["viewport"] - if "chatHistory" in filtered_data: - del filtered_data["chatHistory"] - - # Filter nodes - if "nodes" in filtered_data: - for node in filtered_data["nodes"]: - if "position" in node: - del node["position"] - if "positionAbsolute" in node: - del node["positionAbsolute"] - if "selected" in node: - del node["selected"] - if "dragging" in node: - del node["dragging"] - - return filtered_data - - -@create_cache_folder -def save_binary_file(content: str, file_name: str, accepted_types: list[str]) -> str: - """ - Save a binary file to the specified folder. - - Args: - content: The content of the file as a bytes object. - file_name: The name of the file, including its extension. - - Returns: - The path to the saved file. - """ - if not any(file_name.endswith(suffix) for suffix in accepted_types): - raise ValueError(f"File {file_name} is not accepted") - - # Get the destination folder - cache_path = Path(CACHE_DIR) / PREFIX - if not content: - raise ValueError("Please, reload the file in the loader.") - data = content.split(",")[1] - decoded_bytes = base64.b64decode(data) - - # Create the full file path - file_path = os.path.join(cache_path, file_name) - - # Save the binary content to the file - with open(file_path, "wb") as file: - file.write(decoded_bytes) - - return file_path - - -@create_cache_folder -def save_uploaded_file(file, folder_name): - """ - Save an uploaded file to the specified folder with a hash of its content as the file name. - - Args: - file: The uploaded file object. - folder_name: The name of the folder to save the file in. - - Returns: - The path to the saved file. - """ - cache_path = Path(CACHE_DIR) - folder_path = cache_path / folder_name - - # Create the folder if it doesn't exist - if not folder_path.exists(): - folder_path.mkdir() - - # Create a hash of the file content - sha256_hash = hashlib.sha256() - # Reset the file cursor to the beginning of the file - file.seek(0) - # Iterate over the uploaded file in small chunks to conserve memory - while chunk := file.read(8192): # Read 8KB at a time (adjust as needed) - sha256_hash.update(chunk) - - # Use the hex digest of the hash as the file name - hex_dig = sha256_hash.hexdigest() - file_name = hex_dig - - # Reset the file cursor to the beginning of the file - file.seek(0) - - # Save the file with the hash as its name - file_path = folder_path / file_name - with open(file_path, "wb") as new_file: - while chunk := file.read(8192): - new_file.write(chunk) - - return file_path diff --git a/src/backend/langflow/chat/__init__.py b/src/backend/langflow/chat/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/backend/langflow/chat/config.py b/src/backend/langflow/chat/config.py deleted file mode 100644 index 274f4d5bd..000000000 --- a/src/backend/langflow/chat/config.py +++ /dev/null @@ -1,2 +0,0 @@ -class ChatConfig: - streaming: bool = True diff --git a/src/backend/langflow/chat/manager.py b/src/backend/langflow/chat/manager.py deleted file mode 100644 index 1e93174e2..000000000 --- a/src/backend/langflow/chat/manager.py +++ /dev/null @@ -1,217 +0,0 @@ -from collections import defaultdict -from fastapi import WebSocket, status -from langflow.api.v1.schemas import ChatMessage, ChatResponse, FileResponse -from langflow.cache import cache_manager -from langflow.cache.manager import Subject -from langflow.chat.utils import process_graph -from langflow.interface.utils import pil_to_base64 -from langflow.utils.logger import logger - - -import asyncio -import json -from typing import Any, Dict, List - -from langflow.cache.flow import InMemoryCache - - -class ChatHistory(Subject): - def __init__(self): - super().__init__() - self.history: Dict[str, List[ChatMessage]] = defaultdict(list) - - def add_message(self, client_id: str, message: ChatMessage): - """Add a message to the chat history.""" - - self.history[client_id].append(message) - - if not isinstance(message, FileResponse): - self.notify() - - def get_history(self, client_id: str, filter_messages=True) -> List[ChatMessage]: - """Get the chat history for a client.""" - if history := self.history.get(client_id, []): - if filter_messages: - return [msg for msg in history if msg.type not in ["start", "stream"]] - return history - else: - return [] - - def empty_history(self, client_id: str): - """Empty the chat history for a client.""" - self.history[client_id] = [] - - -class ChatManager: - def __init__(self): - self.active_connections: Dict[str, WebSocket] = {} - self.chat_history = ChatHistory() - self.cache_manager = cache_manager - self.cache_manager.attach(self.update) - self.in_memory_cache = InMemoryCache() - - def on_chat_history_update(self): - """Send the last chat message to the client.""" - client_id = self.cache_manager.current_client_id - if client_id in self.active_connections: - chat_response = self.chat_history.get_history( - client_id, filter_messages=False - )[-1] - if chat_response.is_bot: - # Process FileResponse - if isinstance(chat_response, FileResponse): - # If data_type is pandas, convert to csv - if chat_response.data_type == "pandas": - chat_response.data = chat_response.data.to_csv() - elif chat_response.data_type == "image": - # Base64 encode the image - chat_response.data = pil_to_base64(chat_response.data) - # get event loop - loop = asyncio.get_event_loop() - - coroutine = self.send_json(client_id, chat_response) - asyncio.run_coroutine_threadsafe(coroutine, loop) - - def update(self): - if self.cache_manager.current_client_id in self.active_connections: - self.last_cached_object_dict = self.cache_manager.get_last() - # Add a new ChatResponse with the data - chat_response = FileResponse( - message=None, - type="file", - data=self.last_cached_object_dict["obj"], - data_type=self.last_cached_object_dict["type"], - ) - - self.chat_history.add_message( - self.cache_manager.current_client_id, chat_response - ) - - async def connect(self, client_id: str, websocket: WebSocket): - await websocket.accept() - self.active_connections[client_id] = websocket - - def disconnect(self, client_id: str): - self.active_connections.pop(client_id, None) - - async def send_message(self, client_id: str, message: str): - websocket = self.active_connections[client_id] - await websocket.send_text(message) - - async def send_json(self, client_id: str, message: ChatMessage): - websocket = self.active_connections[client_id] - await websocket.send_json(message.dict()) - - async def close_connection(self, client_id: str, code: int, reason: str): - if websocket := self.active_connections[client_id]: - try: - await websocket.close(code=code, reason=reason) - self.disconnect(client_id) - except RuntimeError as exc: - # This is to catch the following error: - # Unexpected ASGI message 'websocket.close', after sending 'websocket.close' - if "after sending" in str(exc): - logger.error(f"Error closing connection: {exc}") - - async def process_message( - self, client_id: str, payload: Dict, langchain_object: Any - ): - # Process the graph data and chat message - chat_inputs = payload.pop("inputs", "") - chat_inputs = ChatMessage(message=chat_inputs) - self.chat_history.add_message(client_id, chat_inputs) - - # graph_data = payload - start_resp = ChatResponse(message=None, type="start", intermediate_steps="") - await self.send_json(client_id, start_resp) - - # is_first_message = len(self.chat_history.get_history(client_id=client_id)) <= 1 - # Generate result and thought - try: - logger.debug("Generating result and thought") - - result, intermediate_steps = await process_graph( - langchain_object=langchain_object, - chat_inputs=chat_inputs, - websocket=self.active_connections[client_id], - ) - except Exception as e: - # Log stack trace - logger.exception(e) - self.chat_history.empty_history(client_id) - raise e - # Send a response back to the frontend, if needed - intermediate_steps = intermediate_steps or "" - history = self.chat_history.get_history(client_id, filter_messages=False) - file_responses = [] - if history: - # Iterate backwards through the history - for msg in reversed(history): - if isinstance(msg, FileResponse): - if msg.data_type == "image": - # Base64 encode the image - if isinstance(msg.data, str): - continue - msg.data = pil_to_base64(msg.data) - file_responses.append(msg) - if msg.type == "start": - break - - response = ChatResponse( - message=result, - intermediate_steps=intermediate_steps.strip(), - type="end", - files=file_responses, - ) - await self.send_json(client_id, response) - self.chat_history.add_message(client_id, response) - - def set_cache(self, client_id: str, langchain_object: Any) -> bool: - """ - Set the cache for a client. - """ - - self.in_memory_cache.set(client_id, langchain_object) - return client_id in self.in_memory_cache - - async def handle_websocket(self, client_id: str, websocket: WebSocket): - await self.connect(client_id, websocket) - - try: - chat_history = self.chat_history.get_history(client_id) - # iterate and make BaseModel into dict - chat_history = [chat.dict() for chat in chat_history] - await websocket.send_json(chat_history) - - while True: - json_payload = await websocket.receive_json() - try: - payload = json.loads(json_payload) - except TypeError: - payload = json_payload - if "clear_history" in payload: - self.chat_history.history[client_id] = [] - continue - - with self.cache_manager.set_client_id(client_id): - langchain_object = self.in_memory_cache.get(client_id) - await self.process_message(client_id, payload, langchain_object) - - except Exception as exc: - # Handle any exceptions that might occur - logger.error(f"Error handling websocket: {exc}") - await self.close_connection( - client_id=client_id, - code=status.WS_1011_INTERNAL_ERROR, - reason=str(exc)[:120], - ) - finally: - try: - await self.close_connection( - client_id=client_id, - code=status.WS_1000_NORMAL_CLOSURE, - reason="Client disconnected", - ) - except Exception as exc: - logger.error(f"Error closing connection: {exc}") - self.disconnect(client_id) diff --git a/src/backend/langflow/chat/utils.py b/src/backend/langflow/chat/utils.py deleted file mode 100644 index 7db65b8e3..000000000 --- a/src/backend/langflow/chat/utils.py +++ /dev/null @@ -1,37 +0,0 @@ -from fastapi import WebSocket -from langflow.api.v1.schemas import ChatMessage -from langflow.processing.base import get_result_and_steps -from langflow.interface.utils import try_setting_streaming_options -from langflow.utils.logger import logger - - -async def process_graph( - langchain_object, - chat_inputs: ChatMessage, - websocket: WebSocket, -): - langchain_object = try_setting_streaming_options(langchain_object, websocket) - logger.debug("Loaded langchain object") - - if langchain_object is None: - # Raise user facing error - raise ValueError( - "There was an error loading the langchain_object. Please, check all the nodes and try again." - ) - - # Generate result and thought - try: - if not chat_inputs.message: - logger.debug("No message provided") - raise ValueError("No message provided") - - logger.debug("Generating result and thought") - result, intermediate_steps = await get_result_and_steps( - langchain_object, chat_inputs.message, websocket=websocket - ) - logger.debug("Generated result and intermediate_steps") - return result, intermediate_steps - except Exception as e: - # Log stack trace - logger.exception(e) - raise e diff --git a/src/backend/langflow/database/__init__.py b/src/backend/langflow/database/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/backend/langflow/database/base.py b/src/backend/langflow/database/base.py deleted file mode 100644 index 518b95d15..000000000 --- a/src/backend/langflow/database/base.py +++ /dev/null @@ -1,133 +0,0 @@ -from contextlib import contextmanager -import os -from pathlib import Path -from langflow.database import models # noqa -from sqlmodel import SQLModel, Session, create_engine -from langflow.utils.logger import logger -from alembic.config import Config -from alembic import command - - -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") - - -class DatabaseManager: - def __init__(self, database_url: str): - self.database_url = database_url - # This file is in langflow.database.base.py - # the ini is in langflow - self.script_location = Path(__file__).parent.parent / "alembic" - self.alembic_cfg_path = Path(__file__).parent.parent / "alembic.ini" - self.engine = create_engine(database_url) - - def __enter__(self): - self._session = Session(self.engine) - return self._session - - def __exit__(self, exc_type, exc_value, traceback): - if exc_type is not None: # If an exception has been raised - logger.error( - f"Session rollback because of exception: {exc_type.__name__} {exc_value}" - ) - self._session.rollback() - else: - self._session.commit() - self._session.close() - - def get_session(self): - with Session(self.engine) as session: - yield session - - def run_migrations(self): - logger.info( - f"Running DB migrations in {self.script_location} on {self.database_url}" - ) - alembic_cfg = Config() - alembic_cfg.set_main_option("script_location", str(self.script_location)) - alembic_cfg.set_main_option("sqlalchemy.url", self.database_url) - command.upgrade(alembic_cfg, "head") - - def create_db_and_tables(self): - logger.debug("Creating database and tables") - try: - SQLModel.metadata.create_all(self.engine) - 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(self.engine) - 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(db_manager: DatabaseManager): - try: - session = Session(DatabaseManager.engine) - yield session - except Exception as e: - print("Session rollback because of exception:", e) - session.rollback() - raise - finally: - session.close() diff --git a/src/backend/langflow/database/models/__init__.py b/src/backend/langflow/database/models/__init__.py deleted file mode 100644 index da47bc5fe..000000000 --- a/src/backend/langflow/database/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .flow import Flow - - -__all__ = ["Flow"] diff --git a/src/backend/langflow/database/models/base.py b/src/backend/langflow/database/models/base.py deleted file mode 100644 index e20895b93..000000000 --- a/src/backend/langflow/database/models/base.py +++ /dev/null @@ -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 diff --git a/src/backend/langflow/database/models/component.py b/src/backend/langflow/database/models/component.py deleted file mode 100644 index bb2408cdb..000000000 --- a/src/backend/langflow/database/models/component.py +++ /dev/null @@ -1,29 +0,0 @@ -from langflow.database.models.base import SQLModelSerializable, SQLModel -from sqlmodel import Field -from typing import Optional -from datetime import datetime -import uuid - - -class Component(SQLModelSerializable, table=True): - id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) - frontend_node_id: uuid.UUID = Field(index=True) - name: str = Field(index=True) - description: Optional[str] = Field(default=None) - python_code: Optional[str] = Field(default=None) - return_type: Optional[str] = Field(default=None) - is_disabled: bool = Field(default=False) - is_read_only: bool = Field(default=False) - create_at: datetime = Field(default_factory=datetime.utcnow) - update_at: datetime = Field(default_factory=datetime.utcnow) - - -class ComponentModel(SQLModel): - id: uuid.UUID = Field(default_factory=uuid.uuid4) - frontend_node_id: uuid.UUID = Field(default=uuid.uuid4()) - name: str = Field(default="") - description: Optional[str] = None - python_code: Optional[str] = None - return_type: Optional[str] = None - is_disabled: bool = False - is_read_only: bool = False diff --git a/src/backend/langflow/database/models/flow.py b/src/backend/langflow/database/models/flow.py deleted file mode 100644 index f9e3aa249..000000000 --- a/src/backend/langflow/database/models/flow.py +++ /dev/null @@ -1,60 +0,0 @@ -# Path: src/backend/langflow/database/models/flow.py - -from langflow.database.models.base import SQLModelSerializable -from pydantic import validator -from sqlmodel import Field, Relationship, JSON, Column -from uuid import UUID, uuid4 -from typing import Dict, Optional - -# if TYPE_CHECKING: -from langflow.database.models.flow_style import FlowStyle, FlowStyleRead - - -class FlowBase(SQLModelSerializable): - name: str = Field(index=True) - description: Optional[str] = Field(index=True) - data: Optional[Dict] = Field(default=None) - - @validator("data") - def validate_json(v): - # dict_keys(['description', 'name', 'id', 'data']) - if not v: - return v - if not isinstance(v, dict): - raise ValueError("Flow must be a valid JSON") - - # data must contain nodes and edges - if "nodes" not in v.keys(): - raise ValueError("Flow must have nodes") - if "edges" not in v.keys(): - raise ValueError("Flow must have edges") - - return v - - -class Flow(FlowBase, table=True): - id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) - data: Optional[Dict] = Field(default=None, sa_column=Column(JSON)) - style: Optional["FlowStyle"] = Relationship( - back_populates="flow", - # use "uselist=False" to make it a one-to-one relationship - sa_relationship_kwargs={"uselist": False}, - ) - - -class FlowCreate(FlowBase): - pass - - -class FlowRead(FlowBase): - id: UUID - - -class FlowReadWithStyle(FlowRead): - style: Optional["FlowStyleRead"] = None - - -class FlowUpdate(SQLModelSerializable): - name: Optional[str] = None - description: Optional[str] = None - data: Optional[Dict] = None diff --git a/src/backend/langflow/database/models/flow_style.py b/src/backend/langflow/database/models/flow_style.py deleted file mode 100644 index fe53799fe..000000000 --- a/src/backend/langflow/database/models/flow_style.py +++ /dev/null @@ -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 From 63a9b01bbc6aee5f957eaca92c4ff267aef15658 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 22:10:24 -0300 Subject: [PATCH 05/45] =?UTF-8?q?=E2=9C=A8=20feat(services):=20add=20suppo?= =?UTF-8?q?rt=20for=20service=20manager=20and=20service=20schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added `__init__.py` file to the `services` directory to make it a package. - Created `Service` class in `base.py` to serve as a base class for different services. - Implemented `ServiceFactory` class in `factory.py` to create instances of services. - Implemented `ServiceManager` class in `manager.py` to manage the creation and retrieval of services. - Created `ServiceType` enum in `schema.py` to define the different types of services that can be registered with the service manager. - Added `initialize_services` function in `manager.py` to initialize all the services needed. The purpose of these changes is to provide a modular and extensible architecture for managing different services in the application. The `ServiceManager` allows for easy creation and retrieval of services, while the `ServiceType` enum provides a standardized way to refer to different types of services. The `Service` base class and `ServiceFactory` class provide a foundation for creating and managing specific services. --- src/backend/langflow/services/__init__.py | 4 ++ src/backend/langflow/services/base.py | 2 + src/backend/langflow/services/factory.py | 6 ++ src/backend/langflow/services/manager.py | 87 +++++++++++++++++++++++ src/backend/langflow/services/schema.py | 13 ++++ 5 files changed, 112 insertions(+) create mode 100644 src/backend/langflow/services/__init__.py create mode 100644 src/backend/langflow/services/base.py create mode 100644 src/backend/langflow/services/factory.py create mode 100644 src/backend/langflow/services/manager.py create mode 100644 src/backend/langflow/services/schema.py diff --git a/src/backend/langflow/services/__init__.py b/src/backend/langflow/services/__init__.py new file mode 100644 index 000000000..8ac74b5b9 --- /dev/null +++ b/src/backend/langflow/services/__init__.py @@ -0,0 +1,4 @@ +from .manager import service_manager +from .schema import ServiceType + +__all__ = ["service_manager", "ServiceType"] diff --git a/src/backend/langflow/services/base.py b/src/backend/langflow/services/base.py new file mode 100644 index 000000000..6bca6c4e2 --- /dev/null +++ b/src/backend/langflow/services/base.py @@ -0,0 +1,2 @@ +class Service: + name: str diff --git a/src/backend/langflow/services/factory.py b/src/backend/langflow/services/factory.py new file mode 100644 index 000000000..c37f4e9c2 --- /dev/null +++ b/src/backend/langflow/services/factory.py @@ -0,0 +1,6 @@ +class ServiceFactory: + def __init__(self, service_class): + self.service_class = service_class + + def create(self, *args, **kwargs): + raise NotImplementedError diff --git a/src/backend/langflow/services/manager.py b/src/backend/langflow/services/manager.py new file mode 100644 index 000000000..1606b3a82 --- /dev/null +++ b/src/backend/langflow/services/manager.py @@ -0,0 +1,87 @@ +from langflow.services.schema import ServiceType +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from langflow.services.factory import ServiceFactory + + +class ServiceManager: + """ + Manages the creation of different services. + """ + + def __init__(self): + self.services = {} + self.factories = {} + + def register_factory(self, service_factory: "ServiceFactory"): + """ + Registers a new factory. + """ + self.factories[service_factory.service_class.name] = service_factory + + def get(self, service_name: ServiceType): + """ + Get (or create) a service by its name. + """ + if service_name not in self.services: + self._create_service(service_name) + + return self.services[service_name] + + def _create_service(self, service_name: ServiceType): + """ + Create a new service given its name. + """ + self._validate_service_creation(service_name) + + if service_name == ServiceType.SETTINGS_MANAGER: + self.services[service_name] = self.factories[service_name].create() + else: + settings_service = self.get(ServiceType.SETTINGS_MANAGER) + self.services[service_name] = self.factories[service_name].create( + settings_service + ) + + def _validate_service_creation(self, service_name: ServiceType): + """ + Validate whether the service can be created. + """ + if service_name not in self.factories: + raise ValueError( + f"No factory registered for the service class '{service_name.name}'" + ) + + if ( + ServiceType.SETTINGS_MANAGER not in self.factories + and service_name != ServiceType.SETTINGS_MANAGER + ): + raise ValueError( + f"Cannot create service '{service_name.name}' before the settings service" + ) + + def update(self, service_name: ServiceType): + """ + Update a service by its name. + """ + if service_name in self.services: + self.services.pop(service_name, None) + self.get(service_name) + + +service_manager = ServiceManager() + + +def initialize_services(): + """ + Initialize all the services needed. + """ + from langflow.services.database import factory as database_factory + from langflow.services.cache import factory as cache_factory + from langflow.services.chat import factory as chat_factory + from langflow.services.settings import factory as settings_factory + + service_manager.register_factory(settings_factory.SettingsManagerFactory()) + service_manager.register_factory(database_factory.DatabaseManagerFactory()) + service_manager.register_factory(cache_factory.CacheManagerFactory()) + service_manager.register_factory(chat_factory.ChatManagerFactory()) diff --git a/src/backend/langflow/services/schema.py b/src/backend/langflow/services/schema.py new file mode 100644 index 000000000..695763afc --- /dev/null +++ b/src/backend/langflow/services/schema.py @@ -0,0 +1,13 @@ +from enum import Enum + + +class ServiceType(str, Enum): + """ + Enum for the different types of services that can be + registered with the service manager. + """ + + CACHE_MANAGER = "cache_manager" + SETTINGS_MANAGER = "settings_manager" + DATABASE_MANAGER = "database_manager" + CHAT_MANAGER = "chat_manager" From d5ad1522500a2095d1647a88a43cfee9bf0c0e36 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 22:12:27 -0300 Subject: [PATCH 06/45] =?UTF-8?q?=F0=9F=93=A6=20chore(cache):=20add=20cach?= =?UTF-8?q?e=20module=20with=20cache=20manager,=20factory,=20base=20cache,?= =?UTF-8?q?=20and=20in-memory=20cache=20implementations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ feat(utils.py): add cache utility functions for memoization and file saving πŸ› fix(utils.py): fix cache folder creation to use the correct cache directory πŸ”§ chore(utils.py): refactor code to improve readability and remove unused imports --- .../langflow/services/cache/__init__.py | 11 ++ src/backend/langflow/services/cache/base.py | 84 ++++++++ .../langflow/services/cache/factory.py | 11 ++ src/backend/langflow/services/cache/flow.py | 146 ++++++++++++++ .../langflow/services/cache/manager.py | 153 +++++++++++++++ src/backend/langflow/services/cache/utils.py | 179 ++++++++++++++++++ 6 files changed, 584 insertions(+) create mode 100644 src/backend/langflow/services/cache/__init__.py create mode 100644 src/backend/langflow/services/cache/base.py create mode 100644 src/backend/langflow/services/cache/factory.py create mode 100644 src/backend/langflow/services/cache/flow.py create mode 100644 src/backend/langflow/services/cache/manager.py create mode 100644 src/backend/langflow/services/cache/utils.py diff --git a/src/backend/langflow/services/cache/__init__.py b/src/backend/langflow/services/cache/__init__.py new file mode 100644 index 000000000..79e143807 --- /dev/null +++ b/src/backend/langflow/services/cache/__init__.py @@ -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", +] diff --git a/src/backend/langflow/services/cache/base.py b/src/backend/langflow/services/cache/base.py new file mode 100644 index 000000000..88cb3a1da --- /dev/null +++ b/src/backend/langflow/services/cache/base.py @@ -0,0 +1,84 @@ +import abc + + +class BaseCache(abc.ABC): + """ + Abstract base class for a cache. + """ + + @abc.abstractmethod + def get(self, key): + """ + Retrieve an item from the cache. + + Args: + key: The key of the item to retrieve. + + Returns: + The value associated with the key, or None if the key is not found. + """ + + @abc.abstractmethod + def set(self, key, value): + """ + Add an item to the cache. + + Args: + key: The key of the item. + value: The value to cache. + """ + + @abc.abstractmethod + def delete(self, key): + """ + Remove an item from the cache. + + Args: + key: The key of the item to remove. + """ + + @abc.abstractmethod + def clear(self): + """ + Clear all items from the cache. + """ + + @abc.abstractmethod + def __contains__(self, key): + """ + Check if the key is in the cache. + + Args: + key: The key of the item to check. + + Returns: + True if the key is in the cache, False otherwise. + """ + + @abc.abstractmethod + def __getitem__(self, key): + """ + Retrieve an item from the cache using the square bracket notation. + + Args: + key: The key of the item to retrieve. + """ + + @abc.abstractmethod + def __setitem__(self, key, value): + """ + Add an item to the cache using the square bracket notation. + + Args: + key: The key of the item. + value: The value to cache. + """ + + @abc.abstractmethod + def __delitem__(self, key): + """ + Remove an item from the cache using the square bracket notation. + + Args: + key: The key of the item to remove. + """ diff --git a/src/backend/langflow/services/cache/factory.py b/src/backend/langflow/services/cache/factory.py new file mode 100644 index 000000000..77f8d58d1 --- /dev/null +++ b/src/backend/langflow/services/cache/factory.py @@ -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, settings_service): + # Here you would have logic to create and configure a CacheManager + return CacheManager() diff --git a/src/backend/langflow/services/cache/flow.py b/src/backend/langflow/services/cache/flow.py new file mode 100644 index 000000000..0c10c51e1 --- /dev/null +++ b/src/backend/langflow/services/cache/flow.py @@ -0,0 +1,146 @@ +import threading +import time +from collections import OrderedDict + +from langflow.services.cache.base import BaseCache + + +class InMemoryCache(BaseCache): + """ + A simple in-memory cache using an OrderedDict. + + This cache supports setting a maximum size and expiration time for cached items. + When the cache is full, it uses a Least Recently Used (LRU) eviction policy. + Thread-safe using a threading Lock. + + Attributes: + max_size (int, optional): Maximum number of items to store in the cache. + expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour. + + Example: + + cache = InMemoryCache(max_size=3, expiration_time=5) + + # setting cache values + cache.set("a", 1) + cache.set("b", 2) + cache["c"] = 3 + + # getting cache values + a = cache.get("a") + b = cache["b"] + """ + + def __init__(self, max_size=None, expiration_time=60 * 60): + """ + Initialize a new InMemoryCache instance. + + Args: + max_size (int, optional): Maximum number of items to store in the cache. + expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour. + """ + self._cache = OrderedDict() + self._lock = threading.Lock() + self.max_size = max_size + self.expiration_time = expiration_time + + def get(self, key): + """ + Retrieve an item from the cache. + + Args: + key: The key of the item to retrieve. + + Returns: + The value associated with the key, or None if the key is not found or the item has expired. + """ + with self._lock: + if key in self._cache: + item = self._cache.pop(key) + if ( + self.expiration_time is None + or time.time() - item["time"] < self.expiration_time + ): + # Move the key to the end to make it recently used + self._cache[key] = item + return item["value"] + else: + self.delete(key) + return None + + def set(self, key, value): + """ + Add an item to the cache. + + If the cache is full, the least recently used item is evicted. + + Args: + key: The key of the item. + value: The value to cache. + """ + with self._lock: + if key in self._cache: + # Remove existing key before re-inserting to update order + self.delete(key) + elif self.max_size and len(self._cache) >= self.max_size: + # Remove least recently used item + self._cache.popitem(last=False) + self._cache[key] = {"value": value, "time": time.time()} + + def get_or_set(self, key, value): + """ + Retrieve an item from the cache. If the item does not exist, set it with the provided value. + + Args: + key: The key of the item. + value: The value to cache if the item doesn't exist. + + Returns: + The cached value associated with the key. + """ + with self._lock: + if key in self._cache: + return self.get(key) + self.set(key, value) + return value + + def delete(self, key): + """ + Remove an item from the cache. + + Args: + key: The key of the item to remove. + """ + # with self._lock: + self._cache.pop(key, None) + + def clear(self): + """ + Clear all items from the cache. + """ + with self._lock: + self._cache.clear() + + def __contains__(self, key): + """Check if the key is in the cache.""" + return key in self._cache + + def __getitem__(self, key): + """Retrieve an item from the cache using the square bracket notation.""" + return self.get(key) + + def __setitem__(self, key, value): + """Add an item to the cache using the square bracket notation.""" + self.set(key, value) + + def __delitem__(self, key): + """Remove an item from the cache using the square bracket notation.""" + self.delete(key) + + def __len__(self): + """Return the number of items in the cache.""" + return len(self._cache) + + def __repr__(self): + """Return a string representation of the InMemoryCache instance.""" + return f"InMemoryCache(max_size={self.max_size}, expiration_time={self.expiration_time})" diff --git a/src/backend/langflow/services/cache/manager.py b/src/backend/langflow/services/cache/manager.py new file mode 100644 index 000000000..ce9a338ef --- /dev/null +++ b/src/backend/langflow/services/cache/manager.py @@ -0,0 +1,153 @@ +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 + + +class Subject: + """Base class for implementing the observer pattern.""" + + def __init__(self): + self.observers: List[Callable[[], None]] = [] + + def attach(self, observer: Callable[[], None]): + """Attach an observer to the subject.""" + self.observers.append(observer) + + def detach(self, observer: Callable[[], None]): + """Detach an observer from the subject.""" + self.observers.remove(observer) + + def notify(self): + """Notify all observers about an event.""" + for observer in self.observers: + if observer is None: + continue + observer() + + +class AsyncSubject: + """Base class for implementing the async observer pattern.""" + + def __init__(self): + self.observers: List[Callable[[], Awaitable]] = [] + + def attach(self, observer: Callable[[], Awaitable]): + """Attach an observer to the subject.""" + self.observers.append(observer) + + def detach(self, observer: Callable[[], Awaitable]): + """Detach an observer from the subject.""" + self.observers.remove(observer) + + async def notify(self): + """Notify all observers about an event.""" + for observer in self.observers: + if observer is None: + continue + await observer() + + +class CacheManager(Subject, Service): + """Manages cache for different clients and notifies observers on changes.""" + + name = "cache_manager" + + def __init__(self): + super().__init__() + self._cache = {} + self.current_client_id = None + self.current_cache = {} + + @contextmanager + def set_client_id(self, client_id: str): + """ + Context manager to set the current client_id and associated cache. + + Args: + client_id (str): The client identifier. + """ + previous_client_id = self.current_client_id + self.current_client_id = client_id + self.current_cache = self._cache.setdefault(client_id, {}) + try: + yield + finally: + self.current_client_id = previous_client_id + self.current_cache = self._cache.get(self.current_client_id, {}) + + def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None): + """ + Add an object to the current client's cache. + + Args: + name (str): The cache key. + obj (Any): The object to cache. + obj_type (str): The type of the object. + """ + object_extensions = { + "image": "png", + "pandas": "csv", + } + if obj_type in object_extensions: + _extension = object_extensions[obj_type] + else: + _extension = type(obj).__name__.lower() + self.current_cache[name] = { + "obj": obj, + "type": obj_type, + "extension": extension or _extension, + } + self.notify() + + def add_pandas(self, name: str, obj: Any): + """ + Add a pandas DataFrame or Series to the current client's cache. + + Args: + name (str): The cache key. + obj (Any): The pandas DataFrame or Series object. + """ + if isinstance(obj, (pd.DataFrame, pd.Series)): + self.add(name, obj.to_csv(), "pandas", extension="csv") + else: + raise ValueError("Object is not a pandas DataFrame or Series") + + def add_image(self, name: str, obj: Any, extension: str = "png"): + """ + Add a PIL Image to the current client's cache. + + Args: + name (str): The cache key. + obj (Any): The PIL Image object. + """ + if isinstance(obj, Image.Image): + self.add(name, obj, "image", extension=extension) + else: + raise ValueError("Object is not a PIL Image") + + def get(self, name: str): + """ + Get an object from the current client's cache. + + Args: + name (str): The cache key. + + Returns: + The cached object associated with the given cache key. + """ + return self.current_cache[name] + + def get_last(self): + """ + Get the last added item in the current client's cache. + + Returns: + The last added item in the cache. + """ + return list(self.current_cache.values())[-1] + + +cache_manager = CacheManager() diff --git a/src/backend/langflow/services/cache/utils.py b/src/backend/langflow/services/cache/utils.py new file mode 100644 index 000000000..3deabe9f4 --- /dev/null +++ b/src/backend/langflow/services/cache/utils.py @@ -0,0 +1,179 @@ +import base64 +import contextlib +import functools +import hashlib +import json +import os +import tempfile +from collections import OrderedDict +from pathlib import Path +from typing import Any, Dict +from appdirs import user_cache_dir + +CACHE: Dict[str, Any] = {} + +CACHE_DIR = user_cache_dir("langflow", "langflow") + + +def create_cache_folder(func): + def wrapper(*args, **kwargs): + # Get the destination folder + cache_path = Path(CACHE_DIR) / PREFIX + + # Create the destination folder if it doesn't exist + os.makedirs(cache_path, exist_ok=True) + + return func(*args, **kwargs) + + return wrapper + + +def memoize_dict(maxsize=128): + cache = OrderedDict() + + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + hashed = compute_dict_hash(args[0]) + key = (func.__name__, hashed, frozenset(kwargs.items())) + if key not in cache: + result = func(*args, **kwargs) + cache[key] = result + if len(cache) > maxsize: + cache.popitem(last=False) + else: + result = cache[key] + return result + + def clear_cache(): + cache.clear() + + wrapper.clear_cache = clear_cache # type: ignore + wrapper.cache = cache # type: ignore + return wrapper + + return decorator + + +PREFIX = "langflow_cache" + + +@create_cache_folder +def clear_old_cache_files(max_cache_size: int = 3): + cache_dir = Path(tempfile.gettempdir()) / PREFIX + cache_files = list(cache_dir.glob("*.dill")) + + if len(cache_files) > max_cache_size: + cache_files_sorted_by_mtime = sorted( + cache_files, key=lambda x: x.stat().st_mtime, reverse=True + ) + + for cache_file in cache_files_sorted_by_mtime[max_cache_size:]: + with contextlib.suppress(OSError): + os.remove(cache_file) + + +def compute_dict_hash(graph_data): + graph_data = filter_json(graph_data) + + cleaned_graph_json = json.dumps(graph_data, sort_keys=True) + return hashlib.sha256(cleaned_graph_json.encode("utf-8")).hexdigest() + + +def filter_json(json_data): + filtered_data = json_data.copy() + + # Remove 'viewport' and 'chatHistory' keys + if "viewport" in filtered_data: + del filtered_data["viewport"] + if "chatHistory" in filtered_data: + del filtered_data["chatHistory"] + + # Filter nodes + if "nodes" in filtered_data: + for node in filtered_data["nodes"]: + if "position" in node: + del node["position"] + if "positionAbsolute" in node: + del node["positionAbsolute"] + if "selected" in node: + del node["selected"] + if "dragging" in node: + del node["dragging"] + + return filtered_data + + +@create_cache_folder +def save_binary_file(content: str, file_name: str, accepted_types: list[str]) -> str: + """ + Save a binary file to the specified folder. + + Args: + content: The content of the file as a bytes object. + file_name: The name of the file, including its extension. + + Returns: + The path to the saved file. + """ + if not any(file_name.endswith(suffix) for suffix in accepted_types): + raise ValueError(f"File {file_name} is not accepted") + + # Get the destination folder + cache_path = Path(CACHE_DIR) / PREFIX + if not content: + raise ValueError("Please, reload the file in the loader.") + data = content.split(",")[1] + decoded_bytes = base64.b64decode(data) + + # Create the full file path + file_path = os.path.join(cache_path, file_name) + + # Save the binary content to the file + with open(file_path, "wb") as file: + file.write(decoded_bytes) + + return file_path + + +@create_cache_folder +def save_uploaded_file(file, folder_name): + """ + Save an uploaded file to the specified folder with a hash of its content as the file name. + + Args: + file: The uploaded file object. + folder_name: The name of the folder to save the file in. + + Returns: + The path to the saved file. + """ + cache_path = Path(CACHE_DIR) + folder_path = cache_path / folder_name + + # Create the folder if it doesn't exist + if not folder_path.exists(): + folder_path.mkdir() + + # Create a hash of the file content + sha256_hash = hashlib.sha256() + # Reset the file cursor to the beginning of the file + file.seek(0) + # Iterate over the uploaded file in small chunks to conserve memory + while chunk := file.read(8192): # Read 8KB at a time (adjust as needed) + sha256_hash.update(chunk) + + # Use the hex digest of the hash as the file name + hex_dig = sha256_hash.hexdigest() + file_name = hex_dig + + # Reset the file cursor to the beginning of the file + file.seek(0) + + # Save the file with the hash as its name + file_path = folder_path / file_name + with open(file_path, "wb") as new_file: + while chunk := file.read(8192): + new_file.write(chunk) + + return file_path From 7b1f99b1e0da4c088d6029ea3535a122319dfe5f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 22:13:10 -0300 Subject: [PATCH 07/45] =?UTF-8?q?=F0=9F=93=9D=20chore(chat):=20add=20chat?= =?UTF-8?q?=20module=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ“ chore(chat): add ChatConfig class to handle chat configuration πŸ“ chore(chat): add ChatManagerFactory class to create and configure ChatManager πŸ“ chore(chat): add ChatManager class to handle chat functionality πŸ“ chore(chat): add ChatHistory class to manage chat history πŸ“ chore(chat): add process_graph function to process chat inputs and generate result and thought --- .../langflow/services/chat/__init__.py | 0 src/backend/langflow/services/chat/config.py | 2 + src/backend/langflow/services/chat/factory.py | 11 + src/backend/langflow/services/chat/manager.py | 221 ++++++++++++++++++ src/backend/langflow/services/chat/utils.py | 37 +++ 5 files changed, 271 insertions(+) create mode 100644 src/backend/langflow/services/chat/__init__.py create mode 100644 src/backend/langflow/services/chat/config.py create mode 100644 src/backend/langflow/services/chat/factory.py create mode 100644 src/backend/langflow/services/chat/manager.py create mode 100644 src/backend/langflow/services/chat/utils.py diff --git a/src/backend/langflow/services/chat/__init__.py b/src/backend/langflow/services/chat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/services/chat/config.py b/src/backend/langflow/services/chat/config.py new file mode 100644 index 000000000..274f4d5bd --- /dev/null +++ b/src/backend/langflow/services/chat/config.py @@ -0,0 +1,2 @@ +class ChatConfig: + streaming: bool = True diff --git a/src/backend/langflow/services/chat/factory.py b/src/backend/langflow/services/chat/factory.py new file mode 100644 index 000000000..03597ed11 --- /dev/null +++ b/src/backend/langflow/services/chat/factory.py @@ -0,0 +1,11 @@ +from langflow.services.chat.manager import ChatManager +from langflow.services.factory import ServiceFactory + + +class ChatManagerFactory(ServiceFactory): + def __init__(self): + super().__init__(ChatManager) + + def create(self, settings_service): + # Here you would have logic to create and configure a ChatManager + return ChatManager() diff --git a/src/backend/langflow/services/chat/manager.py b/src/backend/langflow/services/chat/manager.py new file mode 100644 index 000000000..82fb77572 --- /dev/null +++ b/src/backend/langflow/services/chat/manager.py @@ -0,0 +1,221 @@ +from collections import defaultdict +from fastapi import WebSocket, status +from langflow.api.v1.schemas import ChatMessage, ChatResponse, FileResponse +from langflow.services.base import Service +from langflow.services import service_manager +from langflow.services.cache.manager import Subject +from langflow.services.chat.utils import process_graph +from langflow.interface.utils import pil_to_base64 +from langflow.services.schema import ServiceType +from langflow.utils.logger import logger + + +import asyncio +import json +from typing import Any, Dict, List + +from langflow.services.cache.flow import InMemoryCache + + +class ChatHistory(Subject): + def __init__(self): + super().__init__() + self.history: Dict[str, List[ChatMessage]] = defaultdict(list) + + def add_message(self, client_id: str, message: ChatMessage): + """Add a message to the chat history.""" + + self.history[client_id].append(message) + + if not isinstance(message, FileResponse): + self.notify() + + def get_history(self, client_id: str, filter_messages=True) -> List[ChatMessage]: + """Get the chat history for a client.""" + if history := self.history.get(client_id, []): + if filter_messages: + return [msg for msg in history if msg.type not in ["start", "stream"]] + return history + else: + return [] + + def empty_history(self, client_id: str): + """Empty the chat history for a client.""" + self.history[client_id] = [] + + +class ChatManager(Service): + name = "chat_manager" + + def __init__(self): + self.active_connections: Dict[str, WebSocket] = {} + self.chat_history = ChatHistory() + self.cache_manager = service_manager.get(ServiceType.CACHE_MANAGER) + self.cache_manager.attach(self.update) + self.in_memory_cache = InMemoryCache() + + def on_chat_history_update(self): + """Send the last chat message to the client.""" + client_id = self.cache_manager.current_client_id + if client_id in self.active_connections: + chat_response = self.chat_history.get_history( + client_id, filter_messages=False + )[-1] + if chat_response.is_bot: + # Process FileResponse + if isinstance(chat_response, FileResponse): + # If data_type is pandas, convert to csv + if chat_response.data_type == "pandas": + chat_response.data = chat_response.data.to_csv() + elif chat_response.data_type == "image": + # Base64 encode the image + chat_response.data = pil_to_base64(chat_response.data) + # get event loop + loop = asyncio.get_event_loop() + + coroutine = self.send_json(client_id, chat_response) + asyncio.run_coroutine_threadsafe(coroutine, loop) + + def update(self): + if self.cache_manager.current_client_id in self.active_connections: + self.last_cached_object_dict = self.cache_manager.get_last() + # Add a new ChatResponse with the data + chat_response = FileResponse( + message=None, + type="file", + data=self.last_cached_object_dict["obj"], + data_type=self.last_cached_object_dict["type"], + ) + + self.chat_history.add_message( + self.cache_manager.current_client_id, chat_response + ) + + async def connect(self, client_id: str, websocket: WebSocket): + await websocket.accept() + self.active_connections[client_id] = websocket + + def disconnect(self, client_id: str): + self.active_connections.pop(client_id, None) + + async def send_message(self, client_id: str, message: str): + websocket = self.active_connections[client_id] + await websocket.send_text(message) + + async def send_json(self, client_id: str, message: ChatMessage): + websocket = self.active_connections[client_id] + await websocket.send_json(message.dict()) + + async def close_connection(self, client_id: str, code: int, reason: str): + if websocket := self.active_connections[client_id]: + try: + await websocket.close(code=code, reason=reason) + self.disconnect(client_id) + except RuntimeError as exc: + # This is to catch the following error: + # Unexpected ASGI message 'websocket.close', after sending 'websocket.close' + if "after sending" in str(exc): + logger.error(f"Error closing connection: {exc}") + + async def process_message( + self, client_id: str, payload: Dict, langchain_object: Any + ): + # Process the graph data and chat message + chat_inputs = payload.pop("inputs", "") + chat_inputs = ChatMessage(message=chat_inputs) + self.chat_history.add_message(client_id, chat_inputs) + + # graph_data = payload + start_resp = ChatResponse(message=None, type="start", intermediate_steps="") + await self.send_json(client_id, start_resp) + + # is_first_message = len(self.chat_history.get_history(client_id=client_id)) <= 1 + # Generate result and thought + try: + logger.debug("Generating result and thought") + + result, intermediate_steps = await process_graph( + langchain_object=langchain_object, + chat_inputs=chat_inputs, + websocket=self.active_connections[client_id], + ) + except Exception as e: + # Log stack trace + logger.exception(e) + self.chat_history.empty_history(client_id) + raise e + # Send a response back to the frontend, if needed + intermediate_steps = intermediate_steps or "" + history = self.chat_history.get_history(client_id, filter_messages=False) + file_responses = [] + if history: + # Iterate backwards through the history + for msg in reversed(history): + if isinstance(msg, FileResponse): + if msg.data_type == "image": + # Base64 encode the image + if isinstance(msg.data, str): + continue + msg.data = pil_to_base64(msg.data) + file_responses.append(msg) + if msg.type == "start": + break + + response = ChatResponse( + message=result, + intermediate_steps=intermediate_steps.strip(), + type="end", + files=file_responses, + ) + await self.send_json(client_id, response) + self.chat_history.add_message(client_id, response) + + def set_cache(self, client_id: str, langchain_object: Any) -> bool: + """ + Set the cache for a client. + """ + + self.in_memory_cache.set(client_id, langchain_object) + return client_id in self.in_memory_cache + + async def handle_websocket(self, client_id: str, websocket: WebSocket): + await self.connect(client_id, websocket) + + try: + chat_history = self.chat_history.get_history(client_id) + # iterate and make BaseModel into dict + chat_history = [chat.dict() for chat in chat_history] + await websocket.send_json(chat_history) + + while True: + json_payload = await websocket.receive_json() + try: + payload = json.loads(json_payload) + except TypeError: + payload = json_payload + if "clear_history" in payload: + self.chat_history.history[client_id] = [] + continue + + with self.cache_manager.set_client_id(client_id): + langchain_object = self.in_memory_cache.get(client_id) + await self.process_message(client_id, payload, langchain_object) + + except Exception as exc: + # Handle any exceptions that might occur + logger.error(f"Error handling websocket: {exc}") + await self.close_connection( + client_id=client_id, + code=status.WS_1011_INTERNAL_ERROR, + reason=str(exc)[:120], + ) + finally: + try: + await self.close_connection( + client_id=client_id, + code=status.WS_1000_NORMAL_CLOSURE, + reason="Client disconnected", + ) + except Exception as exc: + logger.error(f"Error closing connection: {exc}") + self.disconnect(client_id) diff --git a/src/backend/langflow/services/chat/utils.py b/src/backend/langflow/services/chat/utils.py new file mode 100644 index 000000000..7db65b8e3 --- /dev/null +++ b/src/backend/langflow/services/chat/utils.py @@ -0,0 +1,37 @@ +from fastapi import WebSocket +from langflow.api.v1.schemas import ChatMessage +from langflow.processing.base import get_result_and_steps +from langflow.interface.utils import try_setting_streaming_options +from langflow.utils.logger import logger + + +async def process_graph( + langchain_object, + chat_inputs: ChatMessage, + websocket: WebSocket, +): + langchain_object = try_setting_streaming_options(langchain_object, websocket) + logger.debug("Loaded langchain object") + + if langchain_object is None: + # Raise user facing error + raise ValueError( + "There was an error loading the langchain_object. Please, check all the nodes and try again." + ) + + # Generate result and thought + try: + if not chat_inputs.message: + logger.debug("No message provided") + raise ValueError("No message provided") + + logger.debug("Generating result and thought") + result, intermediate_steps = await get_result_and_steps( + langchain_object, chat_inputs.message, websocket=websocket + ) + logger.debug("Generated result and intermediate_steps") + return result, intermediate_steps + except Exception as e: + # Log stack trace + logger.exception(e) + raise e From b451ad9bdd91bf978d3b7897ca49e9d98ce44f21 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 22:13:48 -0300 Subject: [PATCH 08/45] =?UTF-8?q?=F0=9F=93=A6=20chore(settings):=20add=20n?= =?UTF-8?q?ew=20files=20for=20managing=20settings=20in=20the=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ“„ feat(settings/__init__.py): add __init__.py file to expose factory and manager modules πŸ“„ feat(settings/base.py): add base settings class with default values and validators πŸ“„ feat(settings/factory.py): add factory class for creating and configuring a SettingsManager πŸ“„ feat(settings/manager.py): add manager class for loading settings from YAML file and initializing SettingsManager πŸ“¦ chore(settings.py): add settings.py file to the backend/langflow/services/settings directory ✨ feat(settings.py): add support for loading settings from a YAML file and updating settings from keyword arguments πŸ”’ chore(settings.py): add validation and logging to the settings module for improved reliability and debugging πŸ”§ chore(settings.py): add utility functions for saving and loading settings from a YAML file --- .../langflow/services/settings/__init__.py | 3 + .../langflow/services/settings/base.py | 172 ++++++++++++++++++ .../langflow/services/settings/factory.py | 15 ++ .../langflow/services/settings/manager.py | 36 ++++ .../langflow/services/settings/settings.py | 171 +++++++++++++++++ 5 files changed, 397 insertions(+) create mode 100644 src/backend/langflow/services/settings/__init__.py create mode 100644 src/backend/langflow/services/settings/base.py create mode 100644 src/backend/langflow/services/settings/factory.py create mode 100644 src/backend/langflow/services/settings/manager.py create mode 100644 src/backend/langflow/services/settings/settings.py diff --git a/src/backend/langflow/services/settings/__init__.py b/src/backend/langflow/services/settings/__init__.py new file mode 100644 index 000000000..2191bf2cc --- /dev/null +++ b/src/backend/langflow/services/settings/__init__.py @@ -0,0 +1,3 @@ +from . import factory, manager + +__all__ = ["factory", "manager"] diff --git a/src/backend/langflow/services/settings/base.py b/src/backend/langflow/services/settings/base.py new file mode 100644 index 000000000..9843339a5 --- /dev/null +++ b/src/backend/langflow/services/settings/base.py @@ -0,0 +1,172 @@ +import contextlib +import json +import os +from typing import Optional, List +from pathlib import Path + +import yaml +from pydantic import BaseSettings, root_validator, validator +from langflow.utils.logger import logger + +BASE_COMPONENTS_PATH = str(Path(__file__).parent / "components") + + +class Settings(BaseSettings): + CHAINS: dict = {} + AGENTS: dict = {} + PROMPTS: dict = {} + LLMS: dict = {} + TOOLS: dict = {} + MEMORIES: dict = {} + EMBEDDINGS: dict = {} + VECTORSTORES: dict = {} + DOCUMENTLOADERS: dict = {} + WRAPPERS: dict = {} + RETRIEVERS: dict = {} + TOOLKITS: dict = {} + TEXTSPLITTERS: dict = {} + UTILITIES: dict = {} + OUTPUT_PARSERS: dict = {} + CUSTOM_COMPONENTS: dict = {} + + DEV: bool = False + DATABASE_URL: Optional[str] = None + CACHE: str = "InMemoryCache" + REMOVE_API_KEYS: bool = False + COMPONENTS_PATH: List[str] = [] + + @validator("DATABASE_URL", pre=True) + def set_database_url(cls, value): + if not value: + logger.debug( + "No database_url provided, trying LANGFLOW_DATABASE_URL env variable" + ) + if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"): + value = langflow_database_url + logger.debug("Using LANGFLOW_DATABASE_URL env variable.") + else: + logger.debug("No DATABASE_URL env variable, using sqlite database") + value = "sqlite:///./langflow.db" + + return value + + @validator("COMPONENTS_PATH", pre=True) + def set_components_path(cls, value): + if os.getenv("LANGFLOW_COMPONENTS_PATH"): + logger.debug("Adding LANGFLOW_COMPONENTS_PATH to components_path") + langflow_component_path = os.getenv("LANGFLOW_COMPONENTS_PATH") + if ( + Path(langflow_component_path).exists() + and langflow_component_path not in value + ): + if isinstance(langflow_component_path, list): + for path in langflow_component_path: + if path not in value: + value.append(path) + logger.debug( + f"Extending {langflow_component_path} to components_path" + ) + elif langflow_component_path not in value: + value.append(langflow_component_path) + logger.debug( + f"Appending {langflow_component_path} to components_path" + ) + + if not value: + value = [BASE_COMPONENTS_PATH] + logger.debug("Setting default components path to components_path") + elif BASE_COMPONENTS_PATH not in value: + value.append(BASE_COMPONENTS_PATH) + logger.debug("Adding default components path to components_path") + + logger.debug(f"Components path: {value}") + return value + + class Config: + validate_assignment = True + extra = "ignore" + env_prefix = "LANGFLOW_" + + @root_validator(allow_reuse=True) + def validate_lists(cls, values): + for key, value in values.items(): + if key != "dev" and not value: + values[key] = [] + return values + + def update_from_yaml(self, file_path: str, dev: bool = False): + new_settings = load_settings_from_yaml(file_path) + self.CHAINS = new_settings.CHAINS or {} + self.AGENTS = new_settings.AGENTS or {} + self.PROMPTS = new_settings.PROMPTS or {} + self.LLMS = new_settings.LLMS or {} + self.TOOLS = new_settings.TOOLS or {} + self.MEMORIES = new_settings.MEMORIES or {} + self.WRAPPERS = new_settings.WRAPPERS or {} + self.TOOLKITS = new_settings.TOOLKITS or {} + self.TEXTSPLITTERS = new_settings.TEXTSPLITTERS or {} + self.UTILITIES = new_settings.UTILITIES or {} + self.EMBEDDINGS = new_settings.EMBEDDINGS or {} + self.VECTORSTORES = new_settings.VECTORSTORES or {} + self.DOCUMENTLOADERS = new_settings.DOCUMENTLOADERS or {} + self.RETRIEVERS = new_settings.RETRIEVERS or {} + self.OUTPUT_PARSERS = new_settings.OUTPUT_PARSERS or {} + self.CUSTOM_COMPONENTS = new_settings.CUSTOM_COMPONENTS or {} + self.COMPONENTS_PATH = new_settings.COMPONENTS_PATH or [] + self.DEV = dev + + def update_settings(self, **kwargs): + logger.debug("Updating settings") + for key, value in kwargs.items(): + # value may contain sensitive information, so we don't want to log it + if not hasattr(self, key): + logger.debug(f"Key {key} not found in settings") + continue + logger.debug(f"Updating {key}") + if isinstance(getattr(self, key), list): + # value might be a '[something]' string + with contextlib.suppress(json.decoder.JSONDecodeError): + value = json.loads(str(value)) + if isinstance(value, list): + for item in value: + if item not in getattr(self, key): + getattr(self, key).append(item) + logger.debug(f"Extended {key}") + else: + getattr(self, key).append(value) + logger.debug(f"Appended {key}") + + else: + setattr(self, key, value) + logger.debug(f"Updated {key}") + logger.debug(f"{key}: {getattr(self, key)}") + + +def save_settings_to_yaml(settings: Settings, file_path: str): + with open(file_path, "w") as f: + settings_dict = settings.dict() + yaml.dump(settings_dict, f) + + +def load_settings_from_yaml(file_path: str) -> Settings: + # Check if a string is a valid path or a file name + if "/" not in file_path: + # Get current path + current_path = os.path.dirname(os.path.abspath(__file__)) + + file_path = os.path.join(current_path, file_path) + + with open(file_path, "r") as f: + settings_dict = yaml.safe_load(f) + settings_dict = {k.upper(): v for k, v in settings_dict.items()} + + for key in settings_dict: + if key not in Settings.__fields__.keys(): + raise KeyError(f"Key {key} not found in settings") + logger.debug(f"Loading {len(settings_dict[key])} {key} from {file_path}") + + return Settings(**settings_dict) + + +langflow_dir = Path(__file__).parent.parent.parent +settings = load_settings_from_yaml(str(langflow_dir / "config.yaml")) diff --git a/src/backend/langflow/services/settings/factory.py b/src/backend/langflow/services/settings/factory.py new file mode 100644 index 000000000..ab22e22b8 --- /dev/null +++ b/src/backend/langflow/services/settings/factory.py @@ -0,0 +1,15 @@ +from pathlib import Path +from langflow.services.settings.manager import SettingsManager +from langflow.services.factory import ServiceFactory + + +class SettingsManagerFactory(ServiceFactory): + def __init__(self): + super().__init__(SettingsManager) + + def create(self): + # Here you would have logic to create and configure a SettingsManager + langflow_dir = Path(__file__).parent.parent.parent + return SettingsManager.load_settings_from_yaml( + str(langflow_dir / "config.yaml") + ) diff --git a/src/backend/langflow/services/settings/manager.py b/src/backend/langflow/services/settings/manager.py new file mode 100644 index 000000000..598efe2d8 --- /dev/null +++ b/src/backend/langflow/services/settings/manager.py @@ -0,0 +1,36 @@ +from langflow.services.base import Service +from langflow.services.settings.base import Settings +from langflow.utils.logger import logger +import os +import yaml + + +class SettingsManager(Service): + name = "settings_manager" + + def __init__(self, settings: Settings): + super().__init__() + self.settings = settings + + @classmethod + def load_settings_from_yaml(cls, file_path: str) -> Settings: + # Check if a string is a valid path or a file name + if "/" not in file_path: + # Get current path + current_path = os.path.dirname(os.path.abspath(__file__)) + + file_path = os.path.join(current_path, file_path) + + with open(file_path, "r") as f: + settings_dict = yaml.safe_load(f) + settings_dict = {k.upper(): v for k, v in settings_dict.items()} + + for key in settings_dict: + if key not in Settings.__fields__.keys(): + raise KeyError(f"Key {key} not found in settings") + logger.debug( + f"Loading {len(settings_dict[key])} {key} from {file_path}" + ) + + settings = Settings(**settings_dict) + return cls(settings) diff --git a/src/backend/langflow/services/settings/settings.py b/src/backend/langflow/services/settings/settings.py new file mode 100644 index 000000000..439b3a1e4 --- /dev/null +++ b/src/backend/langflow/services/settings/settings.py @@ -0,0 +1,171 @@ +import contextlib +import json +import os +from typing import Optional, List +from pathlib import Path + +import yaml +from pydantic import BaseSettings, root_validator, validator +from langflow.utils.logger import logger + +BASE_COMPONENTS_PATH = str(Path(__file__).parent / "components") + + +class Settings(BaseSettings): + CHAINS: dict = {} + AGENTS: dict = {} + PROMPTS: dict = {} + LLMS: dict = {} + TOOLS: dict = {} + MEMORIES: dict = {} + EMBEDDINGS: dict = {} + VECTORSTORES: dict = {} + DOCUMENTLOADERS: dict = {} + WRAPPERS: dict = {} + RETRIEVERS: dict = {} + TOOLKITS: dict = {} + TEXTSPLITTERS: dict = {} + UTILITIES: dict = {} + OUTPUT_PARSERS: dict = {} + CUSTOM_COMPONENTS: dict = {} + + DEV: bool = False + DATABASE_URL: Optional[str] = None + CACHE: str = "InMemoryCache" + REMOVE_API_KEYS: bool = False + COMPONENTS_PATH: List[str] = [] + + @validator("DATABASE_URL", pre=True) + def set_database_url(cls, value): + if not value: + logger.debug( + "No database_url provided, trying LANGFLOW_DATABASE_URL env variable" + ) + if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"): + value = langflow_database_url + logger.debug("Using LANGFLOW_DATABASE_URL env variable.") + else: + logger.debug("No DATABASE_URL env variable, using sqlite database") + value = "sqlite:///./langflow.db" + + return value + + @validator("COMPONENTS_PATH", pre=True) + def set_components_path(cls, value): + if os.getenv("LANGFLOW_COMPONENTS_PATH"): + logger.debug("Adding LANGFLOW_COMPONENTS_PATH to components_path") + langflow_component_path = os.getenv("LANGFLOW_COMPONENTS_PATH") + if ( + Path(langflow_component_path).exists() + and langflow_component_path not in value + ): + if isinstance(langflow_component_path, list): + for path in langflow_component_path: + if path not in value: + value.append(path) + logger.debug( + f"Extending {langflow_component_path} to components_path" + ) + elif langflow_component_path not in value: + value.append(langflow_component_path) + logger.debug( + f"Appending {langflow_component_path} to components_path" + ) + + if not value: + value = [BASE_COMPONENTS_PATH] + logger.debug("Setting default components path to components_path") + elif BASE_COMPONENTS_PATH not in value: + value.append(BASE_COMPONENTS_PATH) + logger.debug("Adding default components path to components_path") + + logger.debug(f"Components path: {value}") + return value + + class Config: + validate_assignment = True + extra = "ignore" + env_prefix = "LANGFLOW_" + + @root_validator(allow_reuse=True) + def validate_lists(cls, values): + for key, value in values.items(): + if key != "dev" and not value: + values[key] = [] + return values + + def update_from_yaml(self, file_path: str, dev: bool = False): + new_settings = load_settings_from_yaml(file_path) + self.CHAINS = new_settings.CHAINS or {} + self.AGENTS = new_settings.AGENTS or {} + self.PROMPTS = new_settings.PROMPTS or {} + self.LLMS = new_settings.LLMS or {} + self.TOOLS = new_settings.TOOLS or {} + self.MEMORIES = new_settings.MEMORIES or {} + self.WRAPPERS = new_settings.WRAPPERS or {} + self.TOOLKITS = new_settings.TOOLKITS or {} + self.TEXTSPLITTERS = new_settings.TEXTSPLITTERS or {} + self.UTILITIES = new_settings.UTILITIES or {} + self.EMBEDDINGS = new_settings.EMBEDDINGS or {} + self.VECTORSTORES = new_settings.VECTORSTORES or {} + self.DOCUMENTLOADERS = new_settings.DOCUMENTLOADERS or {} + self.RETRIEVERS = new_settings.RETRIEVERS or {} + self.OUTPUT_PARSERS = new_settings.OUTPUT_PARSERS or {} + self.CUSTOM_COMPONENTS = new_settings.CUSTOM_COMPONENTS or {} + self.COMPONENTS_PATH = new_settings.COMPONENTS_PATH or [] + self.DEV = dev + + def update_settings(self, **kwargs): + logger.debug("Updating settings") + for key, value in kwargs.items(): + # value may contain sensitive information, so we don't want to log it + if not hasattr(self, key): + logger.debug(f"Key {key} not found in settings") + continue + logger.debug(f"Updating {key}") + if isinstance(getattr(self, key), list): + # value might be a '[something]' string + with contextlib.suppress(json.decoder.JSONDecodeError): + value = json.loads(str(value)) + if isinstance(value, list): + for item in value: + if item not in getattr(self, key): + getattr(self, key).append(item) + logger.debug(f"Extended {key}") + else: + getattr(self, key).append(value) + logger.debug(f"Appended {key}") + + else: + setattr(self, key, value) + logger.debug(f"Updated {key}") + logger.debug(f"{key}: {getattr(self, key)}") + + +def save_settings_to_yaml(settings: Settings, file_path: str): + with open(file_path, "w") as f: + settings_dict = settings.dict() + yaml.dump(settings_dict, f) + + +def load_settings_from_yaml(file_path: str) -> Settings: + # Check if a string is a valid path or a file name + if "/" not in file_path: + # Get current path + current_path = os.path.dirname(os.path.abspath(__file__)) + + file_path = os.path.join(current_path, file_path) + + with open(file_path, "r") as f: + settings_dict = yaml.safe_load(f) + settings_dict = {k.upper(): v for k, v in settings_dict.items()} + + for key in settings_dict: + if key not in Settings.__fields__.keys(): + raise KeyError(f"Key {key} not found in settings") + logger.debug(f"Loading {len(settings_dict[key])} {key} from {file_path}") + + return Settings(**settings_dict) + + +settings = load_settings_from_yaml("config.yaml") From d51aa7ecb23a4163aed48545f595ca2cca4653ee Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 22:14:22 -0300 Subject: [PATCH 09/45] =?UTF-8?q?=F0=9F=93=A6=20chore(database):=20add=20d?= =?UTF-8?q?atabase=20related=20files=20and=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ“¦ chore(database): add database manager and factory πŸ“¦ chore(database): add flow and flow style models --- .../langflow/services/database/__init__.py | 0 .../langflow/services/database/base.py | 151 ++++++++++++++++++ .../langflow/services/database/factory.py | 15 ++ .../services/database/models/__init__.py | 4 + .../langflow/services/database/models/base.py | 14 ++ .../services/database/models/component.py | 29 ++++ .../langflow/services/database/models/flow.py | 60 +++++++ .../services/database/models/flow_style.py | 33 ++++ 8 files changed, 306 insertions(+) create mode 100644 src/backend/langflow/services/database/__init__.py create mode 100644 src/backend/langflow/services/database/base.py create mode 100644 src/backend/langflow/services/database/factory.py create mode 100644 src/backend/langflow/services/database/models/__init__.py create mode 100644 src/backend/langflow/services/database/models/base.py create mode 100644 src/backend/langflow/services/database/models/component.py create mode 100644 src/backend/langflow/services/database/models/flow.py create mode 100644 src/backend/langflow/services/database/models/flow_style.py diff --git a/src/backend/langflow/services/database/__init__.py b/src/backend/langflow/services/database/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/services/database/base.py b/src/backend/langflow/services/database/base.py new file mode 100644 index 000000000..fffb39096 --- /dev/null +++ b/src/backend/langflow/services/database/base.py @@ -0,0 +1,151 @@ +from contextlib import contextmanager +import os +from pathlib import Path +from langflow.services.base import Service +from sqlmodel import SQLModel, Session, create_engine +from langflow.utils.logger import logger +from alembic.config import Config +from alembic import command + + +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") + + +class DatabaseManager(Service): + name = "database_manager" + + def __init__(self, database_url: str): + self.database_url = database_url + # This file is in langflow.services.database.base.py + # the ini is in langflow + langflow_dir = Path(__file__).parent.parent.parent + self.script_location = langflow_dir / "alembic" + self.alembic_cfg_path = langflow_dir / "alembic.ini" + self.engine = create_engine(database_url) + + def __enter__(self): + self._session = Session(self.engine) + return self._session + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is not None: # If an exception has been raised + logger.error( + f"Session rollback because of exception: {exc_type.__name__} {exc_value}" + ) + self._session.rollback() + else: + self._session.commit() + self._session.close() + + def get_session(self): + with Session(self.engine) as session: + yield session + + def run_migrations(self): + logger.info( + f"Running DB migrations in {self.script_location} on {self.database_url}" + ) + alembic_cfg = Config() + alembic_cfg.set_main_option("script_location", str(self.script_location)) + alembic_cfg.set_main_option("sqlalchemy.url", self.database_url) + command.upgrade(alembic_cfg, "head") + + def create_db_and_tables(self): + logger.debug("Creating database and tables") + try: + SQLModel.metadata.create_all(self.engine) + 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(self.engine) + 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(db_manager: DatabaseManager): + try: + session = Session(DatabaseManager.engine) + yield session + except Exception as e: + print("Session rollback because of exception:", e) + session.rollback() + raise + finally: + session.close() + + +def get_session(): + with Session(Engine.get()) as session: + yield session + + +def initialize_database(): + logger.debug("Initializing database") + from langflow.services import service_manager, ServiceType + + database_manager = service_manager.get(ServiceType.DATABASE_MANAGER) + database_manager.run_migrations() + database_manager.create_db_and_tables() + logger.debug("Database initialized") diff --git a/src/backend/langflow/services/database/factory.py b/src/backend/langflow/services/database/factory.py new file mode 100644 index 000000000..187a29fdd --- /dev/null +++ b/src/backend/langflow/services/database/factory.py @@ -0,0 +1,15 @@ +from typing import TYPE_CHECKING +from langflow.services.database.base import DatabaseManager +from langflow.services.factory import ServiceFactory + +if TYPE_CHECKING: + from langflow.services.settings.manager import SettingsManager + + +class DatabaseManagerFactory(ServiceFactory): + def __init__(self): + super().__init__(DatabaseManager) + + def create(self, settings_service: "SettingsManager"): + # Here you would have logic to create and configure a DatabaseManager + return DatabaseManager(settings_service.settings.DATABASE_URL) diff --git a/src/backend/langflow/services/database/models/__init__.py b/src/backend/langflow/services/database/models/__init__.py new file mode 100644 index 000000000..da47bc5fe --- /dev/null +++ b/src/backend/langflow/services/database/models/__init__.py @@ -0,0 +1,4 @@ +from .flow import Flow + + +__all__ = ["Flow"] diff --git a/src/backend/langflow/services/database/models/base.py b/src/backend/langflow/services/database/models/base.py new file mode 100644 index 000000000..e20895b93 --- /dev/null +++ b/src/backend/langflow/services/database/models/base.py @@ -0,0 +1,14 @@ +from sqlmodel import SQLModel +import orjson + + +def orjson_dumps(v, *, default): + # orjson.dumps returns bytes, to match standard json.dumps we need to decode + return orjson.dumps(v, default=default).decode() + + +class SQLModelSerializable(SQLModel): + class Config: + orm_mode = True + json_loads = orjson.loads + json_dumps = orjson_dumps diff --git a/src/backend/langflow/services/database/models/component.py b/src/backend/langflow/services/database/models/component.py new file mode 100644 index 000000000..5c4e6c13a --- /dev/null +++ b/src/backend/langflow/services/database/models/component.py @@ -0,0 +1,29 @@ +from langflow.services.database.models.base import SQLModelSerializable, SQLModel +from sqlmodel import Field +from typing import Optional +from datetime import datetime +import uuid + + +class Component(SQLModelSerializable, table=True): + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + frontend_node_id: uuid.UUID = Field(index=True) + name: str = Field(index=True) + description: Optional[str] = Field(default=None) + python_code: Optional[str] = Field(default=None) + return_type: Optional[str] = Field(default=None) + is_disabled: bool = Field(default=False) + is_read_only: bool = Field(default=False) + create_at: datetime = Field(default_factory=datetime.utcnow) + update_at: datetime = Field(default_factory=datetime.utcnow) + + +class ComponentModel(SQLModel): + id: uuid.UUID = Field(default_factory=uuid.uuid4) + frontend_node_id: uuid.UUID = Field(default=uuid.uuid4()) + name: str = Field(default="") + description: Optional[str] = None + python_code: Optional[str] = None + return_type: Optional[str] = None + is_disabled: bool = False + is_read_only: bool = False diff --git a/src/backend/langflow/services/database/models/flow.py b/src/backend/langflow/services/database/models/flow.py new file mode 100644 index 000000000..2b6c6879c --- /dev/null +++ b/src/backend/langflow/services/database/models/flow.py @@ -0,0 +1,60 @@ +# Path: src/backend/langflow/database/models/flow.py + +from langflow.services.database.models.base import SQLModelSerializable +from pydantic import validator +from sqlmodel import Field, Relationship, JSON, Column +from uuid import UUID, uuid4 +from typing import Dict, Optional + +# if TYPE_CHECKING: +from langflow.services.database.models.flow_style import FlowStyle, FlowStyleRead + + +class FlowBase(SQLModelSerializable): + name: str = Field(index=True) + description: Optional[str] = Field(index=True) + data: Optional[Dict] = Field(default=None) + + @validator("data") + def validate_json(v): + # dict_keys(['description', 'name', 'id', 'data']) + if not v: + return v + if not isinstance(v, dict): + raise ValueError("Flow must be a valid JSON") + + # data must contain nodes and edges + if "nodes" not in v.keys(): + raise ValueError("Flow must have nodes") + if "edges" not in v.keys(): + raise ValueError("Flow must have edges") + + return v + + +class Flow(FlowBase, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) + data: Optional[Dict] = Field(default=None, sa_column=Column(JSON)) + style: Optional["FlowStyle"] = Relationship( + back_populates="flow", + # use "uselist=False" to make it a one-to-one relationship + sa_relationship_kwargs={"uselist": False}, + ) + + +class FlowCreate(FlowBase): + pass + + +class FlowRead(FlowBase): + id: UUID + + +class FlowReadWithStyle(FlowRead): + style: Optional["FlowStyleRead"] = None + + +class FlowUpdate(SQLModelSerializable): + name: Optional[str] = None + description: Optional[str] = None + data: Optional[Dict] = None diff --git a/src/backend/langflow/services/database/models/flow_style.py b/src/backend/langflow/services/database/models/flow_style.py new file mode 100644 index 000000000..3810c7cea --- /dev/null +++ b/src/backend/langflow/services/database/models/flow_style.py @@ -0,0 +1,33 @@ +# Path: src/backend/langflow/database/models/flowstyle.py + +from langflow.services.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.services.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 From be8be07a6241e945f96d7b7760512905ecba46df Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 22:16:14 -0300 Subject: [PATCH 10/45] =?UTF-8?q?=F0=9F=94=A5=20refactor(=5F=5Finit=5F=5F.?= =?UTF-8?q?py):=20deactivate=20cache=20manager=20for=20now=20to=20improve?= =?UTF-8?q?=20performance=20and=20simplify=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/__init__.py b/src/backend/langflow/__init__.py index 5920369e2..f6eb836cc 100644 --- a/src/backend/langflow/__init__.py +++ b/src/backend/langflow/__init__.py @@ -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 From c9ae251f8538d46ce448280e36ec6c0ddd705d5f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 22:16:41 -0300 Subject: [PATCH 11/45] =?UTF-8?q?=F0=9F=94=A7=20chore(main.py):=20refactor?= =?UTF-8?q?=20imports=20and=20remove=20unused=20code=20for=20better=20orga?= =?UTF-8?q?nization=20and=20readability=20=E2=9C=A8=20feat(main.py):=20add?= =?UTF-8?q?=20initialization=20functions=20for=20services=20and=20database?= =?UTF-8?q?=20on=20app=20startup=20to=20ensure=20proper=20setup=20and=20co?= =?UTF-8?q?nfiguration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/main.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index deef1c914..a2da92c93 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -6,14 +6,14 @@ from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from langflow.api import router -from langflow.database.base import DatabaseManager from langflow.interface.utils import setup_llm_caching +from langflow.services.database.base import initialize_database +from langflow.services.manager import initialize_services from langflow.utils.logger import configure def create_app(): """Create the FastAPI app and include the router.""" - from langflow.settings import settings configure() @@ -34,11 +34,10 @@ def create_app(): allow_methods=["*"], allow_headers=["*"], ) - database_manager = DatabaseManager(settings.DATABASE_URL) app.include_router(router) - # app.on_event("startup")(Engine.update) - app.on_event("startup")(database_manager.run_migrations) - app.on_event("startup")(database_manager.create_db_and_tables) + + app.on_event("startup")(initialize_services) + app.on_event("startup")(initialize_database) app.on_event("startup")(setup_llm_caching) return app From 029e06c03352272c48d8dd5cbc769578f352abaf Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 22:17:10 -0300 Subject: [PATCH 12/45] =?UTF-8?q?=F0=9F=94=80=20chore(alembic/env.py):=20u?= =?UTF-8?q?pdate=20import=20statement=20for=20SQLModel=20to=20reflect=20ne?= =?UTF-8?q?w=20file=20location?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/alembic/env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/alembic/env.py b/src/backend/langflow/alembic/env.py index ea4fe9c43..a3babba6d 100644 --- a/src/backend/langflow/alembic/env.py +++ b/src/backend/langflow/alembic/env.py @@ -5,7 +5,7 @@ from sqlalchemy import pool from alembic import context -from langflow.database.base import SQLModel +from langflow.services.database.base import SQLModel # this is the Alembic Config object, which provides # access to the values within the .ini file in use. From 0a8ca3b90849a30d177a2c76ec6b7ab4a1d50cd9 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 22:17:41 -0300 Subject: [PATCH 13/45] =?UTF-8?q?=F0=9F=94=A7=20fix(chat.py):=20update=20i?= =?UTF-8?q?mport=20statements=20to=20reflect=20changes=20in=20module=20str?= =?UTF-8?q?ucture=20=F0=9F=94=A7=20fix(components.py):=20update=20import?= =?UTF-8?q?=20statements=20to=20reflect=20changes=20in=20module=20structur?= =?UTF-8?q?e=20=F0=9F=94=A7=20fix(endpoints.py):=20update=20import=20state?= =?UTF-8?q?ments=20to=20reflect=20changes=20in=20module=20structure=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(flow=5Fstyles.py):=20update=20import=20state?= =?UTF-8?q?ments=20to=20reflect=20changes=20in=20module=20structure=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(flows.py):=20update=20import=20statements=20?= =?UTF-8?q?to=20reflect=20changes=20in=20module=20structure=20=F0=9F=94=A7?= =?UTF-8?q?=20fix(schemas.py):=20update=20import=20statements=20to=20refle?= =?UTF-8?q?ct=20changes=20in=20module=20structure=20=F0=9F=94=A7=20fix(run?= =?UTF-8?q?.py):=20update=20import=20statements=20to=20reflect=20changes?= =?UTF-8?q?=20in=20module=20structure=20=F0=9F=94=A7=20fix(utils.py):=20up?= =?UTF-8?q?date=20import=20statements=20to=20reflect=20changes=20in=20modu?= =?UTF-8?q?le=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/api/v1/chat.py | 8 +++++--- src/backend/langflow/api/v1/components.py | 4 ++-- src/backend/langflow/api/v1/endpoints.py | 6 +++--- src/backend/langflow/api/v1/flow_styles.py | 4 ++-- src/backend/langflow/api/v1/flows.py | 4 ++-- src/backend/langflow/api/v1/schemas.py | 2 +- src/backend/langflow/interface/run.py | 2 +- src/backend/langflow/interface/utils.py | 2 +- 8 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index dd3407d1b..5711b0c33 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -3,13 +3,13 @@ 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.services import service_manager, ServiceType from langflow.graph.graph.base import Graph from langflow.utils.logger import logger from cachetools import LRUCache router = APIRouter(tags=["Chat"]) -chat_manager = ChatManager() + flow_data_store: LRUCache = LRUCache(maxsize=10) @@ -17,6 +17,7 @@ flow_data_store: LRUCache = LRUCache(maxsize=10) async def chat(client_id: str, websocket: WebSocket): """Websocket endpoint for chat.""" try: + chat_manager = service_manager.get(ServiceType.CHAT_MANAGER) if client_id in chat_manager.in_memory_cache: await chat_manager.handle_websocket(client_id, websocket) else: @@ -45,6 +46,7 @@ async def init_build(graph_data: dict, flow_id: str): return InitResponse(flowId=flow_id) # Delete from cache if already exists + chat_manager = service_manager.get(ServiceType.CHAT_MANAGER) if flow_id in chat_manager.in_memory_cache: with chat_manager.in_memory_cache._lock: chat_manager.in_memory_cache.delete(flow_id) @@ -160,7 +162,7 @@ async def stream_build(flow_id: str): "handle_keys": [], } yield str(StreamData(event="message", data=input_keys_response)) - + chat_manager = service_manager.get(ServiceType.CHAT_MANAGER) chat_manager.set_cache(flow_id, langchain_object) # We need to reset the chat history chat_manager.chat_history.empty_history(flow_id) diff --git a/src/backend/langflow/api/v1/components.py b/src/backend/langflow/api/v1/components.py index 1e34da2aa..7f3572111 100644 --- a/src/backend/langflow/api/v1/components.py +++ b/src/backend/langflow/api/v1/components.py @@ -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.database.base import get_session from sqlmodel import Session, select from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.exc import IntegrityError diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index f4817d12a..58021cab7 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -1,8 +1,8 @@ from http import HTTPStatus from typing import Annotated, Optional -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 @@ -26,7 +26,7 @@ from langflow.interface.types import ( build_langchain_custom_component_list_from_path, ) -from langflow.database.base import get_session +from langflow.services.database.base import get_session from sqlmodel import Session # build router diff --git a/src/backend/langflow/api/v1/flow_styles.py b/src/backend/langflow/api/v1/flow_styles.py index 40e292eb3..6b0759df2 100644 --- a/src/backend/langflow/api/v1/flow_styles.py +++ b/src/backend/langflow/api/v1/flow_styles.py @@ -1,11 +1,11 @@ from uuid import UUID -from langflow.database.models.flow_style import ( +from langflow.services.database.models.flow_style import ( FlowStyle, FlowStyleCreate, FlowStyleRead, FlowStyleUpdate, ) -from langflow.database.base import get_session +from langflow.services.database.base import get_session from sqlmodel import Session, select from fastapi import APIRouter, Depends, HTTPException diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index 9f5042fcb..c7f1134d5 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -3,14 +3,14 @@ from uuid import UUID from langflow.settings import settings from langflow.api.utils import remove_api_keys from langflow.api.v1.schemas import FlowListCreate, FlowListRead -from langflow.database.models.flow import ( +from langflow.services.database.models.flow import ( Flow, FlowCreate, FlowRead, FlowReadWithStyle, FlowUpdate, ) -from langflow.database.base import get_session +from langflow.services.database.base import get_session from sqlmodel import Session, select from fastapi import APIRouter, Depends, HTTPException from fastapi.encoders import jsonable_encoder diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 0148dac6d..776e90034 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -1,7 +1,7 @@ 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 langflow.services.database.models.flow import FlowCreate, FlowRead from pydantic import BaseModel, Field, validator import json diff --git a/src/backend/langflow/interface/run.py b/src/backend/langflow/interface/run.py index 97f47334e..cb0573bf7 100644 --- a/src/backend/langflow/interface/run.py +++ b/src/backend/langflow/interface/run.py @@ -1,4 +1,4 @@ -from langflow.cache.utils import memoize_dict +from langflow.services.cache.utils import memoize_dict from langflow.graph import Graph from langflow.utils.logger import logger diff --git a/src/backend/langflow/interface/utils.py b/src/backend/langflow/interface/utils.py index d6c7b9023..f6b8a5488 100644 --- a/src/backend/langflow/interface/utils.py +++ b/src/backend/langflow/interface/utils.py @@ -9,7 +9,7 @@ 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 langflow.services.chat.config import ChatConfig def load_file_into_dict(file_path: str) -> dict: From 8a2358dae0f371586546a6aeef1f3d932bff9532 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 22:18:20 -0300 Subject: [PATCH 14/45] =?UTF-8?q?=F0=9F=94=A7=20fix(custom=5Fcomponent.py)?= =?UTF-8?q?:=20fix=20import=20paths=20for=20session=5Fgetter=20and=20Flow?= =?UTF-8?q?=20models=20in=20custom=5Fcomponent.py=20file=20=F0=9F=94=A7=20?= =?UTF-8?q?fix(test=5Fcache=5Fmanager.py):=20fix=20import=20path=20for=20C?= =?UTF-8?q?acheManager=20in=20test=5Fcache=5Fmanager.py=20file=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(test=5Fcustom=5Fcomponent.py):=20fix=20impor?= =?UTF-8?q?t=20path=20for=20Flow=20and=20FlowCreate=20models=20in=20test?= =?UTF-8?q?=5Fcustom=5Fcomponent.py=20file=20=F0=9F=94=A7=20fix(test=5Fdat?= =?UTF-8?q?abase.py):=20fix=20import=20path=20for=20Flow,=20FlowCreate,=20?= =?UTF-8?q?and=20FlowUpdate=20models=20in=20test=5Fdatabase.py=20file=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(test=5Fwebsocket.py):=20fix=20import=20path?= =?UTF-8?q?=20for=20WebSocketDisconnect=20in=20test=5Fwebsocket.py=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/interface/custom/custom_component.py | 4 ++-- tests/test_cache_manager.py | 2 +- tests/test_custom_component.py | 2 +- tests/test_database.py | 4 ++-- tests/test_websocket.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py index ce8956660..0d93f8d75 100644 --- a/src/backend/langflow/interface/custom/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component.py @@ -6,8 +6,8 @@ from langflow.interface.custom.directory_reader import DirectoryReader from langflow.utils import validate -from langflow.database.base import session_getter -from langflow.database.models.flow import Flow +from langflow.services.database.base import session_getter +from langflow.services.database.models.flow import Flow from pydantic import Extra import yaml diff --git a/tests/test_cache_manager.py b/tests/test_cache_manager.py index f3e65481e..660512634 100644 --- a/tests/test_cache_manager.py +++ b/tests/test_cache_manager.py @@ -2,7 +2,7 @@ from io import StringIO import pandas as pd import pytest -from langflow.cache.manager import CacheManager +from langflow.services.cache.manager import CacheManager from PIL import Image diff --git a/tests/test_custom_component.py b/tests/test_custom_component.py index 199906dda..f20311cec 100644 --- a/tests/test_custom_component.py +++ b/tests/test_custom_component.py @@ -5,7 +5,7 @@ from uuid import uuid4 from fastapi import HTTPException -from langflow.database.models.flow import Flow, FlowCreate +from langflow.services.database.models.flow import Flow, FlowCreate from langflow.interface.custom.base import CustomComponent from langflow.interface.custom.component import ( Component, diff --git a/tests/test_database.py b/tests/test_database.py index bc512b6b0..6ebae5396 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -8,9 +8,9 @@ from fastapi.testclient import TestClient from fastapi.encoders import jsonable_encoder from langflow.api.v1.schemas import FlowListCreate -from langflow.database.models.flow import Flow, FlowCreate, FlowUpdate +from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate -from langflow.database.models.flow_style import ( +from langflow.services.database.models.flow_style import ( FlowStyleCreate, FlowStyleRead, FlowStyleUpdate, diff --git a/tests/test_websocket.py b/tests/test_websocket.py index 57a0e95f6..dd668c287 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -1,6 +1,6 @@ from fastapi import WebSocketDisconnect -# from langflow.chat.manager import ChatManager +# from langflow.services.chat.manager import ChatManager import pytest From 7b2827f198a1a505558bb1a9b6636092f3e8654d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 22:18:45 -0300 Subject: [PATCH 15/45] =?UTF-8?q?=F0=9F=90=9B=20fix(conftest.py):=20fix=20?= =?UTF-8?q?client=5Ffixture=20to=20properly=20yield=20the=20TestClient=20i?= =?UTF-8?q?nstance=20and=20clear=20dependency=20overrides=20after=20usage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 45a8f8f1f..e6cc2a855 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -114,8 +114,8 @@ def client_fixture(session: Session): app = create_app() app.dependency_overrides[get_session] = get_session_override - - yield TestClient(app) + with TestClient(app) as client: + yield client app.dependency_overrides.clear() From 3442521f646a38e2fa8830288671eb9e0a3e197c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 23:13:51 -0300 Subject: [PATCH 16/45] =?UTF-8?q?=F0=9F=8E=89=20feat(lazy=5Fload.py):=20ad?= =?UTF-8?q?d=20LazyLoadDictBase=20class=20to=20provide=20lazy=20loading=20?= =?UTF-8?q?of=20a=20dictionary=20of=20all=20types=20=F0=9F=90=9B=20fix(laz?= =?UTF-8?q?y=5Fload.py):=20implement=20=5Fbuild=5Fdict()=20and=20get=5Ftyp?= =?UTF-8?q?e=5Fdict()=20methods=20to=20avoid=20NotImplementedError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/utils/lazy_load.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/backend/langflow/utils/lazy_load.py diff --git a/src/backend/langflow/utils/lazy_load.py b/src/backend/langflow/utils/lazy_load.py new file mode 100644 index 000000000..df0130acc --- /dev/null +++ b/src/backend/langflow/utils/lazy_load.py @@ -0,0 +1,15 @@ +class LazyLoadDictBase: + def __init__(self): + self._all_types_dict = None + + @property + def all_types_dict(self): + if self._all_types_dict is None: + self._all_types_dict = self._build_dict() + return self._all_types_dict + + def _build_dict(self): + raise NotImplementedError + + def get_type_dict(self): + raise NotImplementedError From f178e29ef45a36a67712f1ca12560aa275b43279 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 23:17:27 -0300 Subject: [PATCH 17/45] =?UTF-8?q?=F0=9F=94=A7=20fix(test=5Fllms=5Ftemplate?= =?UTF-8?q?.py):=20update=20import=20statement=20to=20use=20get=5Fsettings?= =?UTF-8?q?=5Fmanager=20function=20from=20langflow.services.utils=20module?= =?UTF-8?q?=20=F0=9F=94=A7=20fix(test=5Fprompts=5Ftemplate.py):=20update?= =?UTF-8?q?=20import=20statement=20to=20use=20get=5Fsettings=5Fmanager=20f?= =?UTF-8?q?unction=20from=20langflow.services.utils=20module=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(test=5Fvectorstore=5Ftemplate.py):=20update?= =?UTF-8?q?=20import=20statement=20to=20use=20get=5Fsettings=5Fmanager=20f?= =?UTF-8?q?unction=20from=20langflow.services.utils=20module=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(test=5Fllms=5Ftemplate.py):=20update=20asser?= =?UTF-8?q?tion=20to=20use=20settings=20from=20settings=5Fmanager=20instea?= =?UTF-8?q?d=20of=20settings=20module=20=F0=9F=94=A7=20fix(test=5Fprompts?= =?UTF-8?q?=5Ftemplate.py):=20update=20assertion=20to=20use=20settings=20f?= =?UTF-8?q?rom=20settings=5Fmanager=20instead=20of=20settings=20module=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(test=5Fvectorstore=5Ftemplate.py):=20update?= =?UTF-8?q?=20assertion=20to=20use=20settings=20from=20settings=5Fmanager?= =?UTF-8?q?=20instead=20of=20settings=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_llms_template.py | 5 +++-- tests/test_prompts_template.py | 5 +++-- tests/test_vectorstore_template.py | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_llms_template.py b/tests/test_llms_template.py index d8f9e96f3..f1b76e18e 100644 --- a/tests/test_llms_template.py +++ b/tests/test_llms_template.py @@ -1,13 +1,14 @@ from fastapi.testclient import TestClient -from langflow.settings import settings +from langflow.services.utils import get_settings_manager def test_llms_settings(client: TestClient): + settings_manager = get_settings_manager() response = client.get("api/v1/all") assert response.status_code == 200 json_response = response.json() llms = json_response["llms"] - assert set(llms.keys()) == set(settings.LLMS) + assert set(llms.keys()) == set(settings_manager.settings.LLMS) # def test_hugging_face_hub(client: TestClient): diff --git a/tests/test_prompts_template.py b/tests/test_prompts_template.py index fa7a683bd..dde313c20 100644 --- a/tests/test_prompts_template.py +++ b/tests/test_prompts_template.py @@ -1,13 +1,14 @@ from fastapi.testclient import TestClient -from langflow.settings import settings +from langflow.services.utils import get_settings_manager def test_prompts_settings(client: TestClient): + settings_manager = get_settings_manager() response = client.get("api/v1/all") assert response.status_code == 200 json_response = response.json() prompts = json_response["prompts"] - assert set(prompts.keys()) == set(settings.PROMPTS) + assert set(prompts.keys()) == set(settings_manager.settings.PROMPTS) def test_prompt_template(client: TestClient): diff --git a/tests/test_vectorstore_template.py b/tests/test_vectorstore_template.py index bac950ee1..6ae4843ac 100644 --- a/tests/test_vectorstore_template.py +++ b/tests/test_vectorstore_template.py @@ -1,12 +1,13 @@ from fastapi.testclient import TestClient -from langflow.settings import settings +from langflow.services.utils import get_settings_manager # check that all agents are in settings.agents # are in json_response["agents"] def test_vectorstores_settings(client: TestClient): + settings_manager = get_settings_manager() response = client.get("api/v1/all") assert response.status_code == 200 json_response = response.json() vectorstores = json_response["vectorstores"] - assert set(vectorstores.keys()) == set(settings.VECTORSTORES) + assert set(vectorstores.keys()) == set(settings_manager.settings.VECTORSTORES) From d43236fb98648843c47560af288f96755e4e2111 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 23:18:00 -0300 Subject: [PATCH 18/45] =?UTF-8?q?=F0=9F=94=A7=20chore(=5F=5Fmain=5F=5F.py)?= =?UTF-8?q?:=20import=20get=5Fsettings=5Fmanager=20function=20from=20langf?= =?UTF-8?q?low.services.utils=20module=20to=20improve=20code=20organizatio?= =?UTF-8?q?n=20and=20readability=20=F0=9F=94=A7=20chore(=5F=5Fmain=5F=5F.p?= =?UTF-8?q?y):=20remove=20unused=20import=20of=20settings=20module=20from?= =?UTF-8?q?=20langflow.settings=20package=20to=20clean=20up=20code=20?= =?UTF-8?q?=F0=9F=94=A7=20chore(=5F=5Fmain=5F=5F.py):=20update=20reference?= =?UTF-8?q?s=20to=20settings=20object=20to=20use=20settings=5Fmanager.sett?= =?UTF-8?q?ings=20for=20better=20encapsulation=20and=20modularity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/__main__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/backend/langflow/__main__.py b/src/backend/langflow/__main__.py index fa167f188..82d8bacb8 100644 --- a/src/backend/langflow/__main__.py +++ b/src/backend/langflow/__main__.py @@ -1,6 +1,7 @@ import sys import time import httpx +from langflow.services.utils import get_settings_manager from langflow.utils.util import get_number_of_workers from multiprocess import Process # type: ignore import platform @@ -12,7 +13,6 @@ from rich import box from rich import print as rprint 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 @@ -30,19 +30,19 @@ def update_settings( """Update the settings from a config file.""" # Check for database_url in the environment variables - + 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(): From 2fcbfa25a5fcf6f6708bb6bb556f3a6de2497d52 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 23:18:31 -0300 Subject: [PATCH 19/45] =?UTF-8?q?=F0=9F=8E=89=20feat(utils.py):=20add=20ne?= =?UTF-8?q?w=20utility=20function=20`get=5Fsettings=5Fmanager()`=20to=20re?= =?UTF-8?q?trieve=20the=20settings=20manager=20from=20the=20service=20mana?= =?UTF-8?q?ger=20=F0=9F=8E=89=20feat(utils.py):=20add=20new=20utility=20fu?= =?UTF-8?q?nction=20`get=5Fsession()`=20to=20retrieve=20a=20session=20from?= =?UTF-8?q?=20the=20database=20manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/services/utils.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/backend/langflow/services/utils.py diff --git a/src/backend/langflow/services/utils.py b/src/backend/langflow/services/utils.py new file mode 100644 index 000000000..07c67dfbe --- /dev/null +++ b/src/backend/langflow/services/utils.py @@ -0,0 +1,14 @@ +from langflow.services import ServiceType, service_manager +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from langflow.services.settings.manager import SettingsManager + + +def get_settings_manager() -> "SettingsManager": + return service_manager.get(ServiceType.SETTINGS_MANAGER) + + +def get_session(): + db_manager = service_manager.get(ServiceType.DATABASE_MANAGER) + yield from db_manager.get_session() From f72a42213c3783c520bb5a2e0f20e2f1bc6d8c94 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 23:19:14 -0300 Subject: [PATCH 20/45] =?UTF-8?q?=F0=9F=94=A5=20refactor(base.py):=20remov?= =?UTF-8?q?e=20unused=20imports=20and=20code=20related=20to=20database=20e?= =?UTF-8?q?ngine=20creation=20and=20session=20handling=20=F0=9F=94=A5=20re?= =?UTF-8?q?factor(base.py):=20remove=20unused=20code=20related=20to=20load?= =?UTF-8?q?ing=20settings=20from=20YAML=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../langflow/services/database/base.py | 61 ------------------- .../langflow/services/settings/base.py | 4 -- 2 files changed, 65 deletions(-) diff --git a/src/backend/langflow/services/database/base.py b/src/backend/langflow/services/database/base.py index fffb39096..cfc434f25 100644 --- a/src/backend/langflow/services/database/base.py +++ b/src/backend/langflow/services/database/base.py @@ -1,5 +1,4 @@ from contextlib import contextmanager -import os from pathlib import Path from langflow.services.base import Service from sqlmodel import SQLModel, Session, create_engine @@ -8,61 +7,6 @@ from alembic.config import Config from alembic import command -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") - - class DatabaseManager(Service): name = "database_manager" @@ -136,11 +80,6 @@ def session_getter(db_manager: DatabaseManager): session.close() -def get_session(): - with Session(Engine.get()) as session: - yield session - - def initialize_database(): logger.debug("Initializing database") from langflow.services import service_manager, ServiceType diff --git a/src/backend/langflow/services/settings/base.py b/src/backend/langflow/services/settings/base.py index 9843339a5..1eb2793b3 100644 --- a/src/backend/langflow/services/settings/base.py +++ b/src/backend/langflow/services/settings/base.py @@ -166,7 +166,3 @@ def load_settings_from_yaml(file_path: str) -> Settings: logger.debug(f"Loading {len(settings_dict[key])} {key} from {file_path}") return Settings(**settings_dict) - - -langflow_dir = Path(__file__).parent.parent.parent -settings = load_settings_from_yaml(str(langflow_dir / "config.yaml")) From 6ca7308e3c368362bdfaa6018af78efb2e80953d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 23:20:35 -0300 Subject: [PATCH 21/45] updates imports to use settings_manager --- src/backend/langflow/api/v1/components.py | 2 +- src/backend/langflow/api/v1/endpoints.py | 14 +- src/backend/langflow/api/v1/flow_styles.py | 2 +- src/backend/langflow/api/v1/flows.py | 7 +- src/backend/langflow/graph/graph/base.py | 10 +- src/backend/langflow/graph/graph/constants.py | 59 ++++-- src/backend/langflow/graph/vertex/base.py | 4 +- src/backend/langflow/interface/agents/base.py | 9 +- src/backend/langflow/interface/base.py | 8 +- src/backend/langflow/interface/chains/base.py | 7 +- .../interface/document_loaders/base.py | 7 +- .../langflow/interface/embeddings/base.py | 7 +- src/backend/langflow/interface/listing.py | 63 ++++--- src/backend/langflow/interface/llms/base.py | 7 +- .../langflow/interface/memories/base.py | 7 +- .../langflow/interface/output_parsers/base.py | 7 +- .../langflow/interface/prompts/base.py | 7 +- .../langflow/interface/retrievers/base.py | 7 +- .../langflow/interface/text_splitters/base.py | 7 +- .../langflow/interface/toolkits/base.py | 7 +- src/backend/langflow/interface/tools/base.py | 9 +- .../langflow/interface/utilities/base.py | 7 +- src/backend/langflow/interface/utils.py | 9 +- .../langflow/interface/vector_store/base.py | 7 +- src/backend/langflow/settings.py | 171 ------------------ 25 files changed, 182 insertions(+), 269 deletions(-) delete mode 100644 src/backend/langflow/settings.py diff --git a/src/backend/langflow/api/v1/components.py b/src/backend/langflow/api/v1/components.py index 7f3572111..4071461fb 100644 --- a/src/backend/langflow/api/v1/components.py +++ b/src/backend/langflow/api/v1/components.py @@ -2,7 +2,7 @@ from datetime import timezone from typing import List from uuid import UUID from langflow.services.database.models.component import Component, ComponentModel -from langflow.services.database.base import get_session +from langflow.services.utils import get_session from sqlmodel import Session, select from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.exc import IntegrityError diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index 58021cab7..92ccaffc8 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -4,9 +4,8 @@ from typing import Annotated, Optional 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.services.utils import get_settings_manager from langflow.utils.logger import logger -from langflow.settings import settings - from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body from langflow.interface.custom.custom_component import CustomComponent @@ -26,7 +25,7 @@ from langflow.interface.types import ( build_langchain_custom_component_list_from_path, ) -from langflow.services.database.base import get_session +from langflow.services.utils import get_session from sqlmodel import Session # build router @@ -40,11 +39,14 @@ def get_all(): # 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}") + settings_manager = get_settings_manager() + if settings_manager.settings.COMPONENTS_PATH: + logger.info( + f"Building custom components from {settings_manager.settings.COMPONENTS_PATH}" + ) custom_component_dicts = [ build_langchain_custom_component_list_from_path(str(path)) - for path in settings.COMPONENTS_PATH + for path in settings_manager.settings.COMPONENTS_PATH ] logger.info(f"Loading {len(custom_component_dicts)} custom components") diff --git a/src/backend/langflow/api/v1/flow_styles.py b/src/backend/langflow/api/v1/flow_styles.py index 6b0759df2..6eacf8d86 100644 --- a/src/backend/langflow/api/v1/flow_styles.py +++ b/src/backend/langflow/api/v1/flow_styles.py @@ -5,7 +5,7 @@ from langflow.services.database.models.flow_style import ( FlowStyleRead, FlowStyleUpdate, ) -from langflow.services.database.base import get_session +from langflow.services.utils import get_session from sqlmodel import Session, select from fastapi import APIRouter, Depends, HTTPException diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index c7f1134d5..1ecbc85f4 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -1,6 +1,5 @@ from typing import List from uuid import UUID -from langflow.settings import settings from langflow.api.utils import remove_api_keys from langflow.api.v1.schemas import FlowListCreate, FlowListRead from langflow.services.database.models.flow import ( @@ -10,7 +9,8 @@ from langflow.services.database.models.flow import ( FlowReadWithStyle, FlowUpdate, ) -from langflow.services.database.base import get_session +from langflow.services.utils import get_session +from langflow.services.utils import get_settings_manager from sqlmodel import Session, select from fastapi import APIRouter, Depends, HTTPException from fastapi.encoders import jsonable_encoder @@ -61,7 +61,8 @@ def update_flow( 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: + settings_manager = get_settings_manager() + 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) diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py index 99b4e2b3d..f0d3986cf 100644 --- a/src/backend/langflow/graph/graph/base.py +++ b/src/backend/langflow/graph/graph/base.py @@ -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, @@ -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]: diff --git a/src/backend/langflow/graph/graph/constants.py b/src/backend/langflow/graph/graph/constants.py index 5e5c3b709..c9fea48b5 100644 --- a/src/backend/langflow/graph/graph/constants.py +++ b/src/backend/langflow/graph/graph/constants.py @@ -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() diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py index cb7dc4905..ac7f72b4d 100644 --- a/src/backend/langflow/graph/vertex/base.py +++ b/src/backend/langflow/graph/vertex/base.py @@ -1,6 +1,6 @@ import ast 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 langflow.utils.util import sync_to_async @@ -62,7 +62,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 diff --git a/src/backend/langflow/interface/agents/base.py b/src/backend/langflow/interface/agents/base.py index cc5214c0c..ec8c42aba 100644 --- a/src/backend/langflow/interface/agents/base.py +++ b/src/backend/langflow/interface/agents/base.py @@ -5,7 +5,8 @@ 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 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 diff --git a/src/backend/langflow/interface/base.py b/src/backend/langflow/interface/base.py index 76d859b1f..d1ed83b5a 100644 --- a/src/backend/langflow/interface/base.py +++ b/src/backend/langflow/interface/base.py @@ -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 + # 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() diff --git a/src/backend/langflow/interface/chains/base.py b/src/backend/langflow/interface/chains/base.py index fe58397b2..b906dbd25 100644 --- a/src/backend/langflow/interface/chains/base.py +++ b/src/backend/langflow/interface/chains/base.py @@ -3,7 +3,8 @@ 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 langflow.utils.util import build_template_from_class, build_template_from_method @@ -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 diff --git a/src/backend/langflow/interface/document_loaders/base.py b/src/backend/langflow/interface/document_loaders/base.py index ebae1e5a4..db0832ff3 100644 --- a/src/backend/langflow/interface/document_loaders/base.py +++ b/src/backend/langflow/interface/document_loaders/base.py @@ -1,9 +1,10 @@ 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 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 ] diff --git a/src/backend/langflow/interface/embeddings/base.py b/src/backend/langflow/interface/embeddings/base.py index 7572a06cc..169985d37 100644 --- a/src/backend/langflow/interface/embeddings/base.py +++ b/src/backend/langflow/interface/embeddings/base.py @@ -2,7 +2,8 @@ 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 @@ -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 ] diff --git a/src/backend/langflow/interface/listing.py b/src/backend/langflow/interface/listing.py index fe3090f65..1cab1efbc 100644 --- a/src/backend/langflow/interface/listing.py +++ b/src/backend/langflow/interface/listing.py @@ -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() diff --git a/src/backend/langflow/interface/llms/base.py b/src/backend/langflow/interface/llms/base.py index 06aedd3cb..f562b99ed 100644 --- a/src/backend/langflow/interface/llms/base.py +++ b/src/backend/langflow/interface/llms/base.py @@ -2,7 +2,8 @@ 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 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 ] diff --git a/src/backend/langflow/interface/memories/base.py b/src/backend/langflow/interface/memories/base.py index 9cd25381c..70665602c 100644 --- a/src/backend/langflow/interface/memories/base.py +++ b/src/backend/langflow/interface/memories/base.py @@ -2,7 +2,8 @@ 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 @@ -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 ] diff --git a/src/backend/langflow/interface/output_parsers/base.py b/src/backend/langflow/interface/output_parsers/base.py index b5235ad58..256b521e1 100644 --- a/src/backend/langflow/interface/output_parsers/base.py +++ b/src/backend/langflow/interface/output_parsers/base.py @@ -4,7 +4,8 @@ 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 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 diff --git a/src/backend/langflow/interface/prompts/base.py b/src/backend/langflow/interface/prompts/base.py index c062a4a35..5aa41dfb2 100644 --- a/src/backend/langflow/interface/prompts/base.py +++ b/src/backend/langflow/interface/prompts/base.py @@ -5,7 +5,8 @@ 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 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 diff --git a/src/backend/langflow/interface/retrievers/base.py b/src/backend/langflow/interface/retrievers/base.py index 759cd5916..db1cfd165 100644 --- a/src/backend/langflow/interface/retrievers/base.py +++ b/src/backend/langflow/interface/retrievers/base.py @@ -4,7 +4,8 @@ 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 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 ] diff --git a/src/backend/langflow/interface/text_splitters/base.py b/src/backend/langflow/interface/text_splitters/base.py index 787f20d82..87b778c4c 100644 --- a/src/backend/langflow/interface/text_splitters/base.py +++ b/src/backend/langflow/interface/text_splitters/base.py @@ -1,9 +1,10 @@ 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 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 ] diff --git a/src/backend/langflow/interface/toolkits/base.py b/src/backend/langflow/interface/toolkits/base.py index b7c165a4d..c13ffdbd9 100644 --- a/src/backend/langflow/interface/toolkits/base.py +++ b/src/backend/langflow/interface/toolkits/base.py @@ -4,7 +4,8 @@ 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.services.utils import get_settings_manager + from langflow.utils.logger 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 diff --git a/src/backend/langflow/interface/tools/base.py b/src/backend/langflow/interface/tools/base.py index 8c9158c05..1dbc9a6ed 100644 --- a/src/backend/langflow/interface/tools/base.py +++ b/src/backend/langflow/interface/tools/base.py @@ -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] = { diff --git a/src/backend/langflow/interface/utilities/base.py b/src/backend/langflow/interface/utilities/base.py index b0ee4d4be..eb8cd60af 100644 --- a/src/backend/langflow/interface/utilities/base.py +++ b/src/backend/langflow/interface/utilities/base.py @@ -5,7 +5,8 @@ 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 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 diff --git a/src/backend/langflow/interface/utils.py b/src/backend/langflow/interface/utils.py index f6b8a5488..1fddbf80f 100644 --- a/src/backend/langflow/interface/utils.py +++ b/src/backend/langflow/interface/utils.py @@ -10,6 +10,7 @@ from langchain.base_language import BaseLanguageModel from PIL.Image import Image from langflow.utils.logger 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}") diff --git a/src/backend/langflow/interface/vector_store/base.py b/src/backend/langflow/interface/vector_store/base.py index 4a937ba89..4b8ca2b64 100644 --- a/src/backend/langflow/interface/vector_store/base.py +++ b/src/backend/langflow/interface/vector_store/base.py @@ -4,7 +4,8 @@ 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 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 ] diff --git a/src/backend/langflow/settings.py b/src/backend/langflow/settings.py deleted file mode 100644 index 439b3a1e4..000000000 --- a/src/backend/langflow/settings.py +++ /dev/null @@ -1,171 +0,0 @@ -import contextlib -import json -import os -from typing import Optional, List -from pathlib import Path - -import yaml -from pydantic import BaseSettings, root_validator, validator -from langflow.utils.logger import logger - -BASE_COMPONENTS_PATH = str(Path(__file__).parent / "components") - - -class Settings(BaseSettings): - CHAINS: dict = {} - AGENTS: dict = {} - PROMPTS: dict = {} - LLMS: dict = {} - TOOLS: dict = {} - MEMORIES: dict = {} - EMBEDDINGS: dict = {} - VECTORSTORES: dict = {} - DOCUMENTLOADERS: dict = {} - WRAPPERS: dict = {} - RETRIEVERS: dict = {} - TOOLKITS: dict = {} - TEXTSPLITTERS: dict = {} - UTILITIES: dict = {} - OUTPUT_PARSERS: dict = {} - CUSTOM_COMPONENTS: dict = {} - - DEV: bool = False - DATABASE_URL: Optional[str] = None - CACHE: str = "InMemoryCache" - REMOVE_API_KEYS: bool = False - COMPONENTS_PATH: List[str] = [] - - @validator("DATABASE_URL", pre=True) - def set_database_url(cls, value): - if not value: - logger.debug( - "No database_url provided, trying LANGFLOW_DATABASE_URL env variable" - ) - if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"): - value = langflow_database_url - logger.debug("Using LANGFLOW_DATABASE_URL env variable.") - else: - logger.debug("No DATABASE_URL env variable, using sqlite database") - value = "sqlite:///./langflow.db" - - return value - - @validator("COMPONENTS_PATH", pre=True) - def set_components_path(cls, value): - if os.getenv("LANGFLOW_COMPONENTS_PATH"): - logger.debug("Adding LANGFLOW_COMPONENTS_PATH to components_path") - langflow_component_path = os.getenv("LANGFLOW_COMPONENTS_PATH") - if ( - Path(langflow_component_path).exists() - and langflow_component_path not in value - ): - if isinstance(langflow_component_path, list): - for path in langflow_component_path: - if path not in value: - value.append(path) - logger.debug( - f"Extending {langflow_component_path} to components_path" - ) - elif langflow_component_path not in value: - value.append(langflow_component_path) - logger.debug( - f"Appending {langflow_component_path} to components_path" - ) - - if not value: - value = [BASE_COMPONENTS_PATH] - logger.debug("Setting default components path to components_path") - elif BASE_COMPONENTS_PATH not in value: - value.append(BASE_COMPONENTS_PATH) - logger.debug("Adding default components path to components_path") - - logger.debug(f"Components path: {value}") - return value - - class Config: - validate_assignment = True - extra = "ignore" - env_prefix = "LANGFLOW_" - - @root_validator(allow_reuse=True) - def validate_lists(cls, values): - for key, value in values.items(): - if key != "dev" and not value: - values[key] = [] - return values - - def update_from_yaml(self, file_path: str, dev: bool = False): - new_settings = load_settings_from_yaml(file_path) - self.CHAINS = new_settings.CHAINS or {} - self.AGENTS = new_settings.AGENTS or {} - self.PROMPTS = new_settings.PROMPTS or {} - self.LLMS = new_settings.LLMS or {} - self.TOOLS = new_settings.TOOLS or {} - self.MEMORIES = new_settings.MEMORIES or {} - self.WRAPPERS = new_settings.WRAPPERS or {} - self.TOOLKITS = new_settings.TOOLKITS or {} - self.TEXTSPLITTERS = new_settings.TEXTSPLITTERS or {} - self.UTILITIES = new_settings.UTILITIES or {} - self.EMBEDDINGS = new_settings.EMBEDDINGS or {} - self.VECTORSTORES = new_settings.VECTORSTORES or {} - self.DOCUMENTLOADERS = new_settings.DOCUMENTLOADERS or {} - self.RETRIEVERS = new_settings.RETRIEVERS or {} - self.OUTPUT_PARSERS = new_settings.OUTPUT_PARSERS or {} - self.CUSTOM_COMPONENTS = new_settings.CUSTOM_COMPONENTS or {} - self.COMPONENTS_PATH = new_settings.COMPONENTS_PATH or [] - self.DEV = dev - - def update_settings(self, **kwargs): - logger.debug("Updating settings") - for key, value in kwargs.items(): - # value may contain sensitive information, so we don't want to log it - if not hasattr(self, key): - logger.debug(f"Key {key} not found in settings") - continue - logger.debug(f"Updating {key}") - if isinstance(getattr(self, key), list): - # value might be a '[something]' string - with contextlib.suppress(json.decoder.JSONDecodeError): - value = json.loads(str(value)) - if isinstance(value, list): - for item in value: - if item not in getattr(self, key): - getattr(self, key).append(item) - logger.debug(f"Extended {key}") - else: - getattr(self, key).append(value) - logger.debug(f"Appended {key}") - - else: - setattr(self, key, value) - logger.debug(f"Updated {key}") - logger.debug(f"{key}: {getattr(self, key)}") - - -def save_settings_to_yaml(settings: Settings, file_path: str): - with open(file_path, "w") as f: - settings_dict = settings.dict() - yaml.dump(settings_dict, f) - - -def load_settings_from_yaml(file_path: str) -> Settings: - # Check if a string is a valid path or a file name - if "/" not in file_path: - # Get current path - current_path = os.path.dirname(os.path.abspath(__file__)) - - file_path = os.path.join(current_path, file_path) - - with open(file_path, "r") as f: - settings_dict = yaml.safe_load(f) - settings_dict = {k.upper(): v for k, v in settings_dict.items()} - - for key in settings_dict: - if key not in Settings.__fields__.keys(): - raise KeyError(f"Key {key} not found in settings") - logger.debug(f"Loading {len(settings_dict[key])} {key} from {file_path}") - - return Settings(**settings_dict) - - -settings = load_settings_from_yaml("config.yaml") From f9112facdac115c8b1dca6ad040aae8f0858a87e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sat, 5 Aug 2023 23:27:38 -0300 Subject: [PATCH 22/45] =?UTF-8?q?=F0=9F=90=9B=20fix(custom=5Fcomponent.py)?= =?UTF-8?q?:=20import=20get=5Fdb=5Fmanager=20function=20from=20langflow.se?= =?UTF-8?q?rvices.utils=20to=20fix=20NameError=20=F0=9F=90=9B=20fix(factor?= =?UTF-8?q?y.py):=20raise=20ValueError=20if=20no=20database=20URL=20provid?= =?UTF-8?q?ed=20in=20settings=20=F0=9F=90=9B=20fix(manager.py):=20change?= =?UTF-8?q?=20return=20type=20annotation=20of=20load=5Fsettings=5Ffrom=5Fy?= =?UTF-8?q?aml=20method=20to=20"SettingsManager"=20instead=20of=20"Setting?= =?UTF-8?q?s"=20=F0=9F=90=9B=20fix(utils.py):=20import=20service=5Fmanager?= =?UTF-8?q?=20and=20ServiceType=20to=20fix=20NameError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../langflow/interface/custom/custom_component.py | 11 +++++++---- src/backend/langflow/services/database/factory.py | 2 ++ src/backend/langflow/services/settings/manager.py | 2 +- src/backend/langflow/services/utils.py | 4 ++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py index 0d93f8d75..fdfef52f8 100644 --- a/src/backend/langflow/interface/custom/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component.py @@ -3,6 +3,7 @@ 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.utils import validate @@ -159,7 +160,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") @@ -169,7 +171,8 @@ class CustomComponent(Component, extra=Extra.allow): def list_flows(self, *, get_session: Optional[Callable] = None) -> List[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: flows = session.query(Flow).all() return flows @@ -182,8 +185,8 @@ 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: diff --git a/src/backend/langflow/services/database/factory.py b/src/backend/langflow/services/database/factory.py index 187a29fdd..d98414382 100644 --- a/src/backend/langflow/services/database/factory.py +++ b/src/backend/langflow/services/database/factory.py @@ -12,4 +12,6 @@ class DatabaseManagerFactory(ServiceFactory): def create(self, settings_service: "SettingsManager"): # Here you would have logic to create and configure a DatabaseManager + if not settings_service.settings.DATABASE_URL: + raise ValueError("No database URL provided") return DatabaseManager(settings_service.settings.DATABASE_URL) diff --git a/src/backend/langflow/services/settings/manager.py b/src/backend/langflow/services/settings/manager.py index 598efe2d8..a357c4804 100644 --- a/src/backend/langflow/services/settings/manager.py +++ b/src/backend/langflow/services/settings/manager.py @@ -13,7 +13,7 @@ class SettingsManager(Service): self.settings = settings @classmethod - def load_settings_from_yaml(cls, file_path: str) -> Settings: + def load_settings_from_yaml(cls, file_path: str) -> "SettingsManager": # Check if a string is a valid path or a file name if "/" not in file_path: # Get current path diff --git a/src/backend/langflow/services/utils.py b/src/backend/langflow/services/utils.py index 07c67dfbe..049e82c0f 100644 --- a/src/backend/langflow/services/utils.py +++ b/src/backend/langflow/services/utils.py @@ -9,6 +9,10 @@ def get_settings_manager() -> "SettingsManager": return service_manager.get(ServiceType.SETTINGS_MANAGER) +def get_db_manager(): + return service_manager.get(ServiceType.DATABASE_MANAGER) + + def get_session(): db_manager = service_manager.get(ServiceType.DATABASE_MANAGER) yield from db_manager.get_session() From df51f7879ce2d759767dd80da0a7ddf4ddb906eb Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sun, 6 Aug 2023 12:09:44 -0300 Subject: [PATCH 23/45] =?UTF-8?q?=F0=9F=94=A7=20chore(frontend):=20update?= =?UTF-8?q?=20.dockerignore=20to=20exclude=20the=20'build'=20directory=20f?= =?UTF-8?q?rom=20Docker=20build=20context=20=F0=9F=90=9B=20fix(frontend):?= =?UTF-8?q?=20add=20'build'=20directory=20to=20.dockerignore=20to=20preven?= =?UTF-8?q?t=20it=20from=20being=20included=20in=20Docker=20build=20contex?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/frontend/.dockerignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/frontend/.dockerignore b/src/frontend/.dockerignore index 600e365ec..ca5762007 100644 --- a/src/frontend/.dockerignore +++ b/src/frontend/.dockerignore @@ -1 +1,2 @@ -**/node_modules \ No newline at end of file +**/node_modules +**/build \ No newline at end of file From a9db2da6bfb71225e835122414076278fccc7a90 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Sun, 6 Aug 2023 12:15:29 -0300 Subject: [PATCH 24/45] =?UTF-8?q?=F0=9F=90=9B=20fix(base.py):=20use=20db?= =?UTF-8?q?=5Fmanager.engine=20instead=20of=20DatabaseManager.engine=20to?= =?UTF-8?q?=20access=20the=20database=20engine=20=F0=9F=90=9B=20fix(confte?= =?UTF-8?q?st.py):=20add=20TYPE=5FCHECKING=20import=20to=20fix=20type=20hi?= =?UTF-8?q?nting=20error=20=F0=9F=90=9B=20fix(conftest.py):=20pass=20db=5F?= =?UTF-8?q?manager=20to=20blank=5Fsession=5Fgetter=20fixture=20to=20fix=20?= =?UTF-8?q?session=20creation=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/services/database/base.py | 2 +- tests/conftest.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/backend/langflow/services/database/base.py b/src/backend/langflow/services/database/base.py index cfc434f25..9f92c6c25 100644 --- a/src/backend/langflow/services/database/base.py +++ b/src/backend/langflow/services/database/base.py @@ -70,7 +70,7 @@ class DatabaseManager(Service): @contextmanager def session_getter(db_manager: DatabaseManager): try: - session = Session(DatabaseManager.engine) + session = Session(db_manager.engine) yield session except Exception as e: print("Session rollback because of exception:", e) diff --git a/tests/conftest.py b/tests/conftest.py index e6cc2a855..a97270c7c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ from contextlib import contextmanager import json from pathlib import Path -from typing import AsyncGenerator +from typing import AsyncGenerator, TYPE_CHECKING from langflow.api.v1.flows import get_session from langflow.graph.graph.base import Graph @@ -11,6 +11,9 @@ from httpx import AsyncClient from sqlmodel import SQLModel, Session, create_engine from sqlmodel.pool import StaticPool +if TYPE_CHECKING: + from langflow.services.database.base import DatabaseManager + def pytest_configure(): pytest.BASIC_EXAMPLE_PATH = ( @@ -134,15 +137,15 @@ def client_fixture(session: Session): # create a fixture for session_getter above @pytest.fixture(name="session_getter") -def session_getter_fixture(): +def session_getter_fixture(client): engine = create_engine( "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool ) SQLModel.metadata.create_all(engine) @contextmanager - def blank_session_getter(): - with Session(engine) as session: + def blank_session_getter(db_manager: "DatabaseManager"): + with Session(db_manager.engine) as session: yield session yield blank_session_getter From 7776977378de920205b8356d0143073b176edc70 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 09:43:37 -0300 Subject: [PATCH 25/45] =?UTF-8?q?=F0=9F=9A=A7=20chore(conftest.py):=20add?= =?UTF-8?q?=20runner=20fixture=20to=20enable=20CLI=20testing=20?= =?UTF-8?q?=F0=9F=94=A7=20chore(conftest.py):=20import=20CliRunner=20from?= =?UTF-8?q?=20typer.testing=20to=20enable=20CLI=20testing=20=F0=9F=94=A7?= =?UTF-8?q?=20chore(conftest.py):=20remove=20unused=20imports=20and=20blan?= =?UTF-8?q?k=20lines=20for=20code=20cleanliness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index a97270c7c..2eae791cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ from fastapi.testclient import TestClient from httpx import AsyncClient from sqlmodel import SQLModel, Session, create_engine from sqlmodel.pool import StaticPool +from typer.testing import CliRunner if TYPE_CHECKING: from langflow.services.database.base import DatabaseManager @@ -149,3 +150,8 @@ def session_getter_fixture(client): yield session yield blank_session_getter + + +@pytest.fixture +def runner(): + return CliRunner() From 709c4a17496d4f9843ae98454c6196396bbba07b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 09:44:11 -0300 Subject: [PATCH 26/45] =?UTF-8?q?=F0=9F=94=A7=20chore(=5F=5Fmain=5F=5F.py)?= =?UTF-8?q?:=20refactor=20serve=20function=20to=20improve=20readability=20?= =?UTF-8?q?and=20maintainability=20=E2=9C=A8=20feat(=5F=5Fmain=5F=5F.py):?= =?UTF-8?q?=20add=20support=20for=20custom=20components=20directory=20path?= =?UTF-8?q?=20as=20an=20environment=20variable=20=E2=9C=A8=20feat(=5F=5Fma?= =?UTF-8?q?in=5F=5F.py):=20set=20default=20value=20for=20config=20option?= =?UTF-8?q?=20to=20be=20the=20config.yaml=20file=20in=20the=20same=20direc?= =?UTF-8?q?tory=20as=20the=20script=20=E2=9C=A8=20feat(=5F=5Fmain=5F=5F.py?= =?UTF-8?q?):=20add=20support=20for=20specifying=20an=20.env=20file=20cont?= =?UTF-8?q?aining=20environment=20variables=20=E2=9C=A8=20feat(=5F=5Fmain?= =?UTF-8?q?=5F=5F.py):=20add=20backend=5Fonly=20option=20to=20run=20only?= =?UTF-8?q?=20the=20backend=20server=20without=20the=20frontend=20?= =?UTF-8?q?=F0=9F=94=A7=20chore(=5F=5Fmain=5F=5F.py):=20refactor=20setup?= =?UTF-8?q?=5Fapp=20function=20to=20pass=20backend=5Fonly=20option=20to=20?= =?UTF-8?q?the=20app=20setup=20=E2=9C=A8=20feat(=5F=5Fmain=5F=5F.py):=20ad?= =?UTF-8?q?d=20check=20to=20skip=20server=20startup=20if=20running=20in=20?= =?UTF-8?q?pytest=20environment=20=F0=9F=94=A7=20chore(=5F=5Fmain=5F=5F.py?= =?UTF-8?q?):=20refactor=20serve=20function=20to=20improve=20readability?= =?UTF-8?q?=20and=20maintainability=20=E2=9C=A8=20feat(=5F=5Fmain=5F=5F.py?= =?UTF-8?q?):=20add=20support=20for=20running=20the=20server=20using=20uvi?= =?UTF-8?q?corn=20on=20Windows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/__main__.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/__main__.py b/src/backend/langflow/__main__.py index 82d8bacb8..43247b10f 100644 --- a/src/backend/langflow/__main__.py +++ b/src/backend/langflow/__main__.py @@ -106,7 +106,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." @@ -146,6 +148,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 +174,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 +186,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 From 76362da42bd395d39828506ab7300c94f3b3f605 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 09:44:45 -0300 Subject: [PATCH 27/45] =?UTF-8?q?=F0=9F=94=A7=20chore(main.py):=20refactor?= =?UTF-8?q?=20setup=5Fapp=20function=20to=20add=20support=20for=20backend?= =?UTF-8?q?=5Fonly=20flag=20=E2=9C=A8=20feat(main.py):=20add=20backend=5Fo?= =?UTF-8?q?nly=20flag=20to=20setup=5Fapp=20function=20to=20allow=20running?= =?UTF-8?q?=20the=20app=20without=20serving=20static=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/main.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index a2da92c93..222873275 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -70,16 +70,19 @@ 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: + setup_static_files(app, static_files_dir) return app From 36a7ba4ad5a589c75beec7cfa583b7cadea47545 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 09:45:09 -0300 Subject: [PATCH 28/45] =?UTF-8?q?=F0=9F=9A=80=20feat(test=5Fcli.py):=20add?= =?UTF-8?q?=20tests=20for=20server=20functionality=20and=20command=20line?= =?UTF-8?q?=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸš€ feat(test_cli.py): add test for checking database URL option πŸš€ feat(test_cli.py): add test for checking components path option --- tests/test_cli.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/test_cli.py diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 000000000..f1d5f193c --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,49 @@ +from pathlib import Path +from langflow.__main__ import app +import pytest + +import requests +import multiprocessing +import time +from langflow.services import utils + + +@pytest.fixture(scope="module") +def default_settings(): + return [ + "--backend-only", + "--no-open-browser", + ] + + +def test_server(default_settings): + p = multiprocessing.Process( + target=app, + args=(["--host", "localhost", "--port", "8982", *default_settings],), + ) + p.start() + time.sleep(5) # allow some time for the server to start + + response = requests.get( + "http://localhost:8982/health" + ) # assuming a /health endpoint exists + assert response.status_code == 200 + + p.terminate() + + +def test_database_url(runner): + result = runner.invoke(app, ["--database-url", "sqlite:///test.db"]) + assert result.exit_code == 2, result.stdout + assert "No such option: --database-url" in result.output + + +def test_components_path(runner, client, default_settings): + result = runner.invoke( + app, + ["--components-path", "./", *default_settings], + ) + assert result.exit_code == 0, result.stdout + settings_manager = utils.get_settings_manager() + path = Path("./") + assert path in settings_manager.settings.COMPONENTS_PATH From 4547edef0f452255b1eabedf24b145aa8b8d0900 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 10:13:40 -0300 Subject: [PATCH 29/45] =?UTF-8?q?=F0=9F=94=A5=20refactor(api):=20remove=20?= =?UTF-8?q?unused=20flow=5Fstyles=20module=20and=20related=20code=20?= =?UTF-8?q?=F0=9F=94=A5=20refactor(api):=20remove=20unused=20flow=5Fstyles?= =?UTF-8?q?=20router=20and=20related=20endpoints=20=F0=9F=94=A5=20refactor?= =?UTF-8?q?(api):=20remove=20unused=20FlowStyle=20model=20and=20related=20?= =?UTF-8?q?code=20=F0=9F=94=A5=20refactor(api):=20remove=20unused=20FlowSt?= =?UTF-8?q?yleCreate,=20FlowStyleRead,=20and=20FlowStyleUpdate=20models=20?= =?UTF-8?q?=F0=9F=94=A5=20refactor(api):=20remove=20unused=20style=20relat?= =?UTF-8?q?ionship=20from=20Flow=20model=20=F0=9F=94=A5=20refactor(api):?= =?UTF-8?q?=20remove=20unused=20FlowReadWithStyle=20model=20=F0=9F=94=A5?= =?UTF-8?q?=20refactor(api):=20remove=20unused=20flow=5Fstyles=5Fid=20para?= =?UTF-8?q?meter=20from=20read=5Fflow=5Fstyle=20endpoint=20=F0=9F=94=A5=20?= =?UTF-8?q?refactor(api):=20remove=20unused=20update=5Fflow=5Fstyle=20endp?= =?UTF-8?q?oint=20=F0=9F=94=A5=20refactor(api):=20remove=20unused=20delete?= =?UTF-8?q?=5Fflow=5Fstyle=20endpoint=20=F0=9F=94=A5=20refactor(api):=20re?= =?UTF-8?q?move=20unused=20flow=5Fid=20parameter=20from=20delete=5Fflow=5F?= =?UTF-8?q?style=20endpoint=20=F0=9F=94=A5=20refactor(api):=20remove=20unu?= =?UTF-8?q?sed=20style=20parameter=20from=20create=5Fflow=5Fstyle=20endpoi?= =?UTF-8?q?nt=20=F0=9F=94=A5=20refactor(api):=20remove=20unused=20read=5Ff?= =?UTF-8?q?low=5Fstyles=20endpoint=20=F0=9F=94=A5=20refactor(api):=20remov?= =?UTF-8?q?e=20unused=20flow=5Fstyles=5Fid=20parameter=20from=20read=5Fflo?= =?UTF-8?q?w=5Fstyles=20endpoint=20=F0=9F=94=A5=20refactor(api):=20remove?= =?UTF-8?q?=20unused=20flow=5Fstyle=5Fid=20parameter=20from=20update=5Fflo?= =?UTF-8?q?w=5Fstyle=20endpoint=20=F0=9F=94=A5=20refactor(api):=20remove?= =?UTF-8?q?=20unused=20flow=5Fstyle=20parameter=20from=20update=5Fflow=5Fs?= =?UTF-8?q?tyle=20endpoint=20=F0=9F=94=A5=20refactor(api):=20remove=20unus?= =?UTF-8?q?ed=20flow=5Fstyle=20parameter=20from=20create=5Fflow=5Fstyle=20?= =?UTF-8?q?endpoint=20=F0=9F=94=A5=20refactor(api):=20remove=20unused=20fl?= =?UTF-8?q?ow=5Fstyles=5Frouter=20import=20=F0=9F=94=A5=20refactor(api):?= =?UTF-8?q?=20remove=20unused=20flow=5Fstyles=5Frouter=20variable=20?= =?UTF-8?q?=F0=9F=94=A5=20refactor(api):=20remove=20unused=20flow=5Fstyles?= =?UTF-8?q?=5Frouter=20prefix=20and=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/api/v1/__init__.py | 2 - src/backend/langflow/api/v1/flow_styles.py | 83 ------------------- src/backend/langflow/api/v1/flows.py | 5 +- .../langflow/services/database/models/flow.py | 12 +-- .../services/database/models/flow_style.py | 33 -------- 5 files changed, 3 insertions(+), 132 deletions(-) delete mode 100644 src/backend/langflow/api/v1/flow_styles.py delete mode 100644 src/backend/langflow/services/database/models/flow_style.py diff --git a/src/backend/langflow/api/v1/__init__.py b/src/backend/langflow/api/v1/__init__.py index f001152a9..b6e7b36d8 100644 --- a/src/backend/langflow/api/v1/__init__.py +++ b/src/backend/langflow/api/v1/__init__.py @@ -2,7 +2,6 @@ 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 __all__ = [ @@ -11,5 +10,4 @@ __all__ = [ "component_router", "validate_router", "flows_router", - "flow_styles_router", ] diff --git a/src/backend/langflow/api/v1/flow_styles.py b/src/backend/langflow/api/v1/flow_styles.py deleted file mode 100644 index 6eacf8d86..000000000 --- a/src/backend/langflow/api/v1/flow_styles.py +++ /dev/null @@ -1,83 +0,0 @@ -from uuid import UUID -from langflow.services.database.models.flow_style import ( - FlowStyle, - FlowStyleCreate, - FlowStyleRead, - FlowStyleUpdate, -) -from langflow.services.utils 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"} diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py index 1ecbc85f4..3145ced3c 100644 --- a/src/backend/langflow/api/v1/flows.py +++ b/src/backend/langflow/api/v1/flows.py @@ -6,7 +6,6 @@ from langflow.services.database.models.flow import ( Flow, FlowCreate, FlowRead, - FlowReadWithStyle, FlowUpdate, ) from langflow.services.utils import get_session @@ -32,7 +31,7 @@ def create_flow(*, session: Session = Depends(get_session), flow: FlowCreate): return db_flow -@router.get("/", response_model=list[FlowReadWithStyle], status_code=200) +@router.get("/", response_model=list[FlowRead], status_code=200) def read_flows(*, session: Session = Depends(get_session)): """Read all flows.""" try: @@ -42,7 +41,7 @@ def read_flows(*, session: Session = Depends(get_session)): return [jsonable_encoder(flow) for flow in flows] -@router.get("/{flow_id}", response_model=FlowReadWithStyle, status_code=200) +@router.get("/{flow_id}", response_model=FlowRead, status_code=200) def read_flow(*, session: Session = Depends(get_session), flow_id: UUID): """Read a flow.""" if flow := session.get(Flow, flow_id): diff --git a/src/backend/langflow/services/database/models/flow.py b/src/backend/langflow/services/database/models/flow.py index 2b6c6879c..2bc83f9dc 100644 --- a/src/backend/langflow/services/database/models/flow.py +++ b/src/backend/langflow/services/database/models/flow.py @@ -2,12 +2,11 @@ from langflow.services.database.models.base import SQLModelSerializable from pydantic import validator -from sqlmodel import Field, Relationship, JSON, Column +from sqlmodel import Field, JSON, Column from uuid import UUID, uuid4 from typing import Dict, Optional # if TYPE_CHECKING: -from langflow.services.database.models.flow_style import FlowStyle, FlowStyleRead class FlowBase(SQLModelSerializable): @@ -35,11 +34,6 @@ class FlowBase(SQLModelSerializable): class Flow(FlowBase, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) data: Optional[Dict] = Field(default=None, sa_column=Column(JSON)) - style: Optional["FlowStyle"] = Relationship( - back_populates="flow", - # use "uselist=False" to make it a one-to-one relationship - sa_relationship_kwargs={"uselist": False}, - ) class FlowCreate(FlowBase): @@ -50,10 +44,6 @@ class FlowRead(FlowBase): id: UUID -class FlowReadWithStyle(FlowRead): - style: Optional["FlowStyleRead"] = None - - class FlowUpdate(SQLModelSerializable): name: Optional[str] = None description: Optional[str] = None diff --git a/src/backend/langflow/services/database/models/flow_style.py b/src/backend/langflow/services/database/models/flow_style.py deleted file mode 100644 index 3810c7cea..000000000 --- a/src/backend/langflow/services/database/models/flow_style.py +++ /dev/null @@ -1,33 +0,0 @@ -# Path: src/backend/langflow/database/models/flowstyle.py - -from langflow.services.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.services.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 From e2e14d8c9355a9ce653c8d4d4a2259a326ead37f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 10:24:02 -0300 Subject: [PATCH 30/45] =?UTF-8?q?=F0=9F=94=A5=20refactor(router.py):=20rem?= =?UTF-8?q?ove=20unused=20flow=5Fstyles=5Frouter=20import=20to=20improve?= =?UTF-8?q?=20code=20cleanliness=20and=20reduce=20unused=20code=20?= =?UTF-8?q?=F0=9F=94=A5=20refactor(test=5Fdatabase.py):=20remove=20unused?= =?UTF-8?q?=20flow=5Fstyle=20related=20tests=20to=20improve=20code=20clean?= =?UTF-8?q?liness=20and=20reduce=20unused=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/api/router.py | 2 - tests/test_database.py | 102 ----------------------------- 2 files changed, 104 deletions(-) diff --git a/src/backend/langflow/api/router.py b/src/backend/langflow/api/router.py index b9c51c11e..ea1938a75 100644 --- a/src/backend/langflow/api/router.py +++ b/src/backend/langflow/api/router.py @@ -5,7 +5,6 @@ from langflow.api.v1 import ( endpoints_router, validate_router, flows_router, - flow_styles_router, component_router, ) @@ -17,4 +16,3 @@ 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) diff --git a/tests/test_database.py b/tests/test_database.py index 6ebae5396..52a5daa4c 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -5,17 +5,10 @@ from uuid import UUID, uuid4 from sqlalchemy.orm import Session from fastapi.testclient import TestClient -from fastapi.encoders import jsonable_encoder from langflow.api.v1.schemas import FlowListCreate from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate -from langflow.services.database.models.flow_style import ( - FlowStyleCreate, - FlowStyleRead, - FlowStyleUpdate, -) - @pytest.fixture(scope="module") def json_style(): @@ -56,33 +49,12 @@ def test_read_flows(client: TestClient, json_flow: str): assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data - flow_style = FlowStyleCreate(color="red", emoji="πŸ‘", flow_id=response.json()["id"]) - response = client.post( - "api/v1/flow_styles/", json=jsonable_encoder(flow_style.dict()) - ) - assert response.status_code == 200 - assert response.json()["color"] == flow_style.color - assert response.json()["emoji"] == flow_style.emoji - assert response.json()["flow_id"] == str(flow_style.flow_id) - flow = FlowCreate(name="Test Flow", description="description", data=data) response = client.post("api/v1/flows/", json=flow.dict()) assert response.status_code == 201 assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data - # Now we need to create FlowStyle objects for each Flow - flow_style = FlowStyleCreate( - color="green", emoji="πŸ‘", flow_id=response.json()["id"] - ) - response = client.post( - "api/v1/flow_styles/", json=jsonable_encoder(flow_style.dict()) - ) - assert response.status_code == 200 - assert response.json()["color"] == flow_style.color - assert response.json()["emoji"] == flow_style.emoji - assert response.json()["flow_id"] == str(flow_style.flow_id) - response = client.get("api/v1/flows/") assert response.status_code == 200 assert len(response.json()) > 0 @@ -97,21 +69,10 @@ def test_read_flow(client: TestClient, json_flow: str): # turn it into a UUID flow_id = UUID(flow_id) - flow_style = FlowStyleCreate(color="green", emoji="πŸ‘", flow_id=flow_id) - response = client.post( - "api/v1/flow_styles/", json=jsonable_encoder(flow_style.dict()) - ) - assert response.status_code == 200 - response_json = response.json() - assert response_json["color"] == flow_style.color - assert response_json["emoji"] == flow_style.emoji - assert response_json["flow_id"] == str(flow_style.flow_id) - response = client.get(f"api/v1/flows/{flow_id}") assert response.status_code == 200 assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data - assert response.json()["style"]["color"] == flow_style.color def test_update_flow(client: TestClient, json_flow: str): @@ -275,66 +236,3 @@ def test_read_empty_flows(client: TestClient): response = client.get("api/v1/flows/") assert response.status_code == 200 assert len(response.json()) == 0 - - -def test_create_flow_style(client: TestClient): - flow_style = FlowStyleCreate(color="red", emoji="πŸ”΄") - response = client.post("api/v1/flow_styles/", json=flow_style.dict()) - assert response.status_code == 200 - created_flow_style = FlowStyleRead(**response.json()) - assert created_flow_style.color == flow_style.color - assert created_flow_style.emoji == flow_style.emoji - - -def test_read_flow_styles(client: TestClient): - response = client.get("api/v1/flow_styles/") - assert response.status_code == 200 - flow_styles = [FlowStyleRead(**flow_style) for flow_style in response.json()] - assert not flow_styles - # Create test data - flow_style = FlowStyleCreate(color="red", emoji="πŸ”΄") - response = client.post("api/v1/flow_styles/", json=flow_style.dict()) - assert response.status_code == 200 - # Check response data - response = client.get("api/v1/flow_styles/") - assert response.status_code == 200 - flow_styles = [FlowStyleRead(**flow_style) for flow_style in response.json()] - assert len(flow_styles) == 1 - assert flow_styles[0].color == flow_style.color - assert flow_styles[0].emoji == flow_style.emoji - - -def test_read_flow_style(client: TestClient): - flow_style = FlowStyleCreate(color="red", emoji="πŸ”΄") - response = client.post("api/v1/flow_styles/", json=flow_style.dict()) - created_flow_style = FlowStyleRead(**response.json()) - response = client.get(f"api/v1/flow_styles/{created_flow_style.id}") - assert response.status_code == 200 - read_flow_style = FlowStyleRead(**response.json()) - assert read_flow_style == created_flow_style - - -def test_update_flow_style(client: TestClient): - flow_style = FlowStyleCreate(color="red", emoji="πŸ”΄") - response = client.post("api/v1/flow_styles/", json=flow_style.dict()) - created_flow_style = FlowStyleRead(**response.json()) - to_update_flow_style = FlowStyleUpdate(color="blue") - response = client.patch( - f"api/v1/flow_styles/{created_flow_style.id}", json=to_update_flow_style.dict() - ) - assert response.status_code == 200 - updated_flow_style = FlowStyleRead(**response.json()) - assert updated_flow_style.color == "blue" - assert updated_flow_style.emoji == flow_style.emoji - - -def test_delete_flow_style(client: TestClient): - flow_style = FlowStyleCreate(color="red", emoji="πŸ”΄") - response = client.post("api/v1/flow_styles/", json=flow_style.dict()) - created_flow_style = FlowStyleRead(**response.json()) - response = client.delete(f"api/v1/flow_styles/{created_flow_style.id}") - assert response.status_code == 200 - assert response.json() == {"message": "FlowStyle deleted successfully"} - - response = client.get(f"api/v1/flow_styles/{created_flow_style.id}") - assert response.status_code == 404 From b7861ac77f3d1cfaae00c409791d998563bfe38f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 10:24:28 -0300 Subject: [PATCH 31/45] =?UTF-8?q?=F0=9F=94=A7=20chore(alembic.ini):=20upda?= =?UTF-8?q?te=20sqlalchemy.url=20to=20dynamically=20set=20the=20path=20to?= =?UTF-8?q?=20the=20database=20in=20the=20root=20of=20the=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/alembic.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/alembic.ini b/src/backend/langflow/alembic.ini index 0227ea4f2..379661422 100644 --- a/src/backend/langflow/alembic.ini +++ b/src/backend/langflow/alembic.ini @@ -60,10 +60,10 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne # are written from script.py.mako # output_encoding = utf-8 -# This is a placeholder to run the first migration +# 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 +# be set dinamically. +sqlalchemy.url = sqlite:///../../../langflow.db [post_write_hooks] From d75fb49bf2adcf1cdab25a4b8281893ba713c00b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 10:25:46 -0300 Subject: [PATCH 32/45] =?UTF-8?q?=F0=9F=94=A5=20chore(alembic):=20remove?= =?UTF-8?q?=20FlowStyles=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit removes the FlowStyles table from the database. The table was no longer needed and has been dropped. The corresponding indexes and foreign key constraints have also been removed. Downgrade functionality has been implemented to recreate the FlowStyles table and its associated indexes and foreign key constraints if needed. --- .../921920b95d3a_remove_flowstyles_table.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/backend/langflow/alembic/versions/921920b95d3a_remove_flowstyles_table.py diff --git a/src/backend/langflow/alembic/versions/921920b95d3a_remove_flowstyles_table.py b/src/backend/langflow/alembic/versions/921920b95d3a_remove_flowstyles_table.py new file mode 100644 index 000000000..7bb550fdf --- /dev/null +++ b/src/backend/langflow/alembic/versions/921920b95d3a_remove_flowstyles_table.py @@ -0,0 +1,63 @@ +"""Remove FlowStyles table + +Revision ID: 921920b95d3a +Revises: 4814b6f4abfd +Create Date: 2023-08-07 10:22:54.503716 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "921920b95d3a" +down_revision: Union[str, None] = "4814b6f4abfd" +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! ### + op.drop_index("ix_component_frontend_node_id", table_name="component") + op.drop_index("ix_component_name", table_name="component") + op.drop_table("component") + op.drop_table("flowstyle") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "flowstyle", + sa.Column("color", sa.VARCHAR(), nullable=False), + sa.Column("emoji", sa.VARCHAR(), nullable=False), + sa.Column("flow_id", sa.CHAR(length=32), nullable=True), + sa.Column("id", sa.CHAR(length=32), nullable=False), + sa.ForeignKeyConstraint( + ["flow_id"], + ["flow.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + ) + op.create_table( + "component", + sa.Column("id", sa.CHAR(length=32), nullable=False), + sa.Column("frontend_node_id", sa.CHAR(length=32), nullable=False), + sa.Column("name", sa.VARCHAR(), nullable=False), + sa.Column("description", sa.VARCHAR(), nullable=True), + sa.Column("python_code", sa.VARCHAR(), nullable=True), + sa.Column("return_type", sa.VARCHAR(), nullable=True), + sa.Column("is_disabled", sa.BOOLEAN(), nullable=False), + sa.Column("is_read_only", sa.BOOLEAN(), nullable=False), + sa.Column("create_at", sa.DATETIME(), nullable=False), + sa.Column("update_at", sa.DATETIME(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_component_name", "component", ["name"], unique=False) + op.create_index( + "ix_component_frontend_node_id", "component", ["frontend_node_id"], unique=False + ) + # ### end Alembic commands ### From 41707c1eac506910e378563a16a92fef1e8b0b7c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 11:59:23 -0300 Subject: [PATCH 33/45] =?UTF-8?q?=F0=9F=94=A7=20chore(test=5Fcli.py):=20re?= =?UTF-8?q?factor=20test=5Fcomponents=5Fpath=20to=20use=20a=20temporary=20?= =?UTF-8?q?directory=20for=20components=20path=20=F0=9F=94=A7=20chore(test?= =?UTF-8?q?=5Fcli.py):=20refactor=20test=5Fcomponents=5Fpath=20to=20use=20?= =?UTF-8?q?a=20temporary=20directory=20for=20components=20path=20to=20impr?= =?UTF-8?q?ove=20test=20isolation=20and=20avoid=20side=20effects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_cli.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index f1d5f193c..cd2dcdbb7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,5 @@ from pathlib import Path +from tempfile import tempdir from langflow.__main__ import app import pytest @@ -39,11 +40,16 @@ def test_database_url(runner): def test_components_path(runner, client, default_settings): + # Create a foldr in the tmp directory + temp_dir = Path(tempdir) + # create a "components" folder + temp_dir = temp_dir / "components" + temp_dir.mkdir(exist_ok=True) + result = runner.invoke( app, - ["--components-path", "./", *default_settings], + ["--components-path", str(temp_dir), *default_settings], ) assert result.exit_code == 0, result.stdout settings_manager = utils.get_settings_manager() - path = Path("./") - assert path in settings_manager.settings.COMPONENTS_PATH + assert temp_dir in settings_manager.settings.COMPONENTS_PATH From 8036f74d8b4f66eae435fe07abd02a261ca702f4 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 11:59:57 -0300 Subject: [PATCH 34/45] =?UTF-8?q?=F0=9F=94=80=20chore(conftest.py):=20upda?= =?UTF-8?q?te=20import=20statement=20for=20DatabaseManager=20to=20reflect?= =?UTF-8?q?=20file=20name=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2eae791cd..e90d03d0a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,7 @@ from sqlmodel.pool import StaticPool from typer.testing import CliRunner if TYPE_CHECKING: - from langflow.services.database.base import DatabaseManager + from langflow.services.database.manager import DatabaseManager def pytest_configure(): From 8436c66aa767eab3ac78f7080c78a0dab35a0feb Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 12:00:27 -0300 Subject: [PATCH 35/45] =?UTF-8?q?=F0=9F=93=A6=20chore(manager.py):=20add?= =?UTF-8?q?=20DatabaseManager=20class=20to=20handle=20database=20operation?= =?UTF-8?q?s=20=F0=9F=93=A6=20chore(utils.py):=20add=20initialize=5Fdataba?= =?UTF-8?q?se=20function=20and=20session=5Fgetter=20context=20manager=20to?= =?UTF-8?q?=20handle=20database=20initialization=20and=20session=20managem?= =?UTF-8?q?ent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/database/{base.py => manager.py} | 26 +--------------- .../langflow/services/database/utils.py | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 25 deletions(-) rename src/backend/langflow/services/database/{base.py => manager.py} (77%) create mode 100644 src/backend/langflow/services/database/utils.py diff --git a/src/backend/langflow/services/database/base.py b/src/backend/langflow/services/database/manager.py similarity index 77% rename from src/backend/langflow/services/database/base.py rename to src/backend/langflow/services/database/manager.py index 9f92c6c25..20db90f66 100644 --- a/src/backend/langflow/services/database/base.py +++ b/src/backend/langflow/services/database/manager.py @@ -1,4 +1,3 @@ -from contextlib import contextmanager from pathlib import Path from langflow.services.base import Service from sqlmodel import SQLModel, Session, create_engine @@ -12,7 +11,7 @@ class DatabaseManager(Service): def __init__(self, database_url: str): self.database_url = database_url - # This file is in langflow.services.database.base.py + # This file is in langflow.services.database.manager.py # the ini is in langflow langflow_dir = Path(__file__).parent.parent.parent self.script_location = langflow_dir / "alembic" @@ -65,26 +64,3 @@ class DatabaseManager(Service): raise RuntimeError("Something went wrong creating the database and tables.") else: logger.debug("Database and tables created successfully") - - -@contextmanager -def session_getter(db_manager: DatabaseManager): - try: - session = Session(db_manager.engine) - yield session - except Exception as e: - print("Session rollback because of exception:", e) - session.rollback() - raise - finally: - session.close() - - -def initialize_database(): - logger.debug("Initializing database") - from langflow.services import service_manager, ServiceType - - database_manager = service_manager.get(ServiceType.DATABASE_MANAGER) - database_manager.run_migrations() - database_manager.create_db_and_tables() - logger.debug("Database initialized") diff --git a/src/backend/langflow/services/database/utils.py b/src/backend/langflow/services/database/utils.py new file mode 100644 index 000000000..20b2bbbb4 --- /dev/null +++ b/src/backend/langflow/services/database/utils.py @@ -0,0 +1,31 @@ +from typing import TYPE_CHECKING +from langflow.utils.logger import logger +from contextlib import contextmanager + +from sqlmodel import Session + +if TYPE_CHECKING: + from langflow.services.database.manager import DatabaseManager + + +def initialize_database(): + logger.debug("Initializing database") + from langflow.services import service_manager, ServiceType + + database_manager = service_manager.get(ServiceType.DATABASE_MANAGER) + database_manager.run_migrations() + database_manager.create_db_and_tables() + logger.debug("Database initialized") + + +@contextmanager +def session_getter(db_manager: "DatabaseManager"): + try: + session = Session(db_manager.engine) + yield session + except Exception as e: + print("Session rollback because of exception:", e) + session.rollback() + raise + finally: + session.close() From 1601745ed909789a6360c68489c0c6008212dd68 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 12:00:58 -0300 Subject: [PATCH 36/45] =?UTF-8?q?=F0=9F=94=80=20refactor(alembic/env.py):?= =?UTF-8?q?=20update=20import=20statement=20for=20SQLModel=20in=20env.py?= =?UTF-8?q?=20to=20reflect=20new=20location=20in=20manager=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ”€ refactor(interface/custom/custom_component.py): update import statement for session_getter in custom_component.py to reflect new location in utils module πŸ”€ refactor(main.py): update import statement for initialize_database in main.py to reflect new location in utils module πŸ”€ refactor(database/factory.py): update import statement for DatabaseManager in factory.py to reflect new location in manager module --- src/backend/langflow/alembic/env.py | 2 +- src/backend/langflow/interface/custom/custom_component.py | 2 +- src/backend/langflow/main.py | 2 +- src/backend/langflow/services/database/factory.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/langflow/alembic/env.py b/src/backend/langflow/alembic/env.py index a3babba6d..310894431 100644 --- a/src/backend/langflow/alembic/env.py +++ b/src/backend/langflow/alembic/env.py @@ -5,7 +5,7 @@ from sqlalchemy import pool from alembic import context -from langflow.services.database.base import SQLModel +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. diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py index fdfef52f8..5388f40d8 100644 --- a/src/backend/langflow/interface/custom/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component.py @@ -7,7 +7,7 @@ from langflow.services.utils import get_db_manager from langflow.utils import validate -from langflow.services.database.base import session_getter +from langflow.services.database.utils import session_getter from langflow.services.database.models.flow import Flow from pydantic import Extra import yaml diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index 222873275..734483317 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -7,7 +7,7 @@ from fastapi.staticfiles import StaticFiles from langflow.api import router from langflow.interface.utils import setup_llm_caching -from langflow.services.database.base import initialize_database +from langflow.services.database.utils import initialize_database from langflow.services.manager import initialize_services from langflow.utils.logger import configure diff --git a/src/backend/langflow/services/database/factory.py b/src/backend/langflow/services/database/factory.py index d98414382..fecf24543 100644 --- a/src/backend/langflow/services/database/factory.py +++ b/src/backend/langflow/services/database/factory.py @@ -1,5 +1,5 @@ from typing import TYPE_CHECKING -from langflow.services.database.base import DatabaseManager +from langflow.services.database.manager import DatabaseManager from langflow.services.factory import ServiceFactory if TYPE_CHECKING: From 9dba69cffc8ce0ea9ed76eb42bbd753dc25a49d9 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 12:07:45 -0300 Subject: [PATCH 37/45] =?UTF-8?q?=F0=9F=93=9D=20docs(CONTRIBUTING.md):=20a?= =?UTF-8?q?dd=20branch=20structure=20information=20to=20CONTRIBUTING.md=20?= =?UTF-8?q?file=20for=20better=20understanding=20of=20the=20repository=20s?= =?UTF-8?q?tructure=20=F0=9F=93=9D=20docs(README.md):=20add=20a=20section?= =?UTF-8?q?=20about=20joining=20the=20Discord=20server=20to=20encourage=20?= =?UTF-8?q?community=20engagement=20and=20collaboration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 5 +++++ README.md | 2 ++ 2 files changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da7ec1977..c58bb92f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,11 @@ to contributions, whether it be in the form of a new feature, improved infra, or To contribute to this project, please follow a ["fork and pull request"](https://docs.github.com/en/get-started/quickstart/contributing-to-projects) workflow. Please do not try to push directly to this repo unless you are a maintainer. +The branch structure is as follows: + +- `main`: The stable version of Langflow +- `dev`: The development version of Langflow. This branch is used to test new features before they are merged into `main` and, as such, may be unstable. + ## πŸ—ΊοΈContributing Guidelines ## 🚩GitHub Issues diff --git a/README.md b/README.md index 3d795015d..9137ea714 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,8 @@ flow("Hey, have you heard of Langflow?") We welcome contributions from developers of all levels to our open-source project on GitHub. If you'd like to contribute, please check our [contributing guidelines](./CONTRIBUTING.md) and help make Langflow more accessible. +--- + Join our [Discord](https://discord.com/invite/EqksyE2EX9) server to ask questions, make suggestions and showcase your projects! 🦾

From 96dfb9b324bb77bd5ff69e7b4f8f6c0af381ec5c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 12:08:16 -0300 Subject: [PATCH 38/45] =?UTF-8?q?=F0=9F=90=9B=20fix(main.py):=20fix=20cond?= =?UTF-8?q?ition=20to=20setup=20static=20files=20only=20if=20static=5Ffile?= =?UTF-8?q?s=5Fdir=20is=20not=20None?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index 734483317..1702fb8f9 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -81,7 +81,7 @@ def setup_app( 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() - if not backend_only: + if not backend_only and static_files_dir is not None: setup_static_files(app, static_files_dir) return app From 71012ac47b3c936291a1784640534fdfcce4e8be Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 14:09:27 -0300 Subject: [PATCH 39/45] =?UTF-8?q?=F0=9F=94=A5=20chore(alembic):=20remove?= =?UTF-8?q?=20flowstyles=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ feat(alembic): add migration to remove flowstyles table πŸ”₯ chore(alembic): remove old migration file for removing flowstyles table πŸ› fix(database): import Flow model to avoid unused import warning --- .../0a534bdfd84b_remove_flowstyles_table.py | 42 +++++++++++++ .../921920b95d3a_remove_flowstyles_table.py | 63 ------------------- .../langflow/services/database/manager.py | 1 + 3 files changed, 43 insertions(+), 63 deletions(-) create mode 100644 src/backend/langflow/alembic/versions/0a534bdfd84b_remove_flowstyles_table.py delete mode 100644 src/backend/langflow/alembic/versions/921920b95d3a_remove_flowstyles_table.py diff --git a/src/backend/langflow/alembic/versions/0a534bdfd84b_remove_flowstyles_table.py b/src/backend/langflow/alembic/versions/0a534bdfd84b_remove_flowstyles_table.py new file mode 100644 index 000000000..0100df44d --- /dev/null +++ b/src/backend/langflow/alembic/versions/0a534bdfd84b_remove_flowstyles_table.py @@ -0,0 +1,42 @@ +"""Remove FlowStyles table + +Revision ID: 0a534bdfd84b +Revises: 4814b6f4abfd +Create Date: 2023-08-07 14:09:06.844104 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "0a534bdfd84b" +down_revision: Union[str, None] = "4814b6f4abfd" +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! ### + op.drop_table("flowstyle") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "flowstyle", + sa.Column("color", sa.VARCHAR(), nullable=False), + sa.Column("emoji", sa.VARCHAR(), nullable=False), + sa.Column("flow_id", sa.CHAR(length=32), nullable=True), + sa.Column("id", sa.CHAR(length=32), nullable=False), + sa.ForeignKeyConstraint( + ["flow_id"], + ["flow.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + ) + # ### end Alembic commands ### diff --git a/src/backend/langflow/alembic/versions/921920b95d3a_remove_flowstyles_table.py b/src/backend/langflow/alembic/versions/921920b95d3a_remove_flowstyles_table.py deleted file mode 100644 index 7bb550fdf..000000000 --- a/src/backend/langflow/alembic/versions/921920b95d3a_remove_flowstyles_table.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Remove FlowStyles table - -Revision ID: 921920b95d3a -Revises: 4814b6f4abfd -Create Date: 2023-08-07 10:22:54.503716 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = "921920b95d3a" -down_revision: Union[str, None] = "4814b6f4abfd" -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! ### - op.drop_index("ix_component_frontend_node_id", table_name="component") - op.drop_index("ix_component_name", table_name="component") - op.drop_table("component") - op.drop_table("flowstyle") - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "flowstyle", - sa.Column("color", sa.VARCHAR(), nullable=False), - sa.Column("emoji", sa.VARCHAR(), nullable=False), - sa.Column("flow_id", sa.CHAR(length=32), nullable=True), - sa.Column("id", sa.CHAR(length=32), nullable=False), - sa.ForeignKeyConstraint( - ["flow_id"], - ["flow.id"], - ), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("id"), - ) - op.create_table( - "component", - sa.Column("id", sa.CHAR(length=32), nullable=False), - sa.Column("frontend_node_id", sa.CHAR(length=32), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=False), - sa.Column("description", sa.VARCHAR(), nullable=True), - sa.Column("python_code", sa.VARCHAR(), nullable=True), - sa.Column("return_type", sa.VARCHAR(), nullable=True), - sa.Column("is_disabled", sa.BOOLEAN(), nullable=False), - sa.Column("is_read_only", sa.BOOLEAN(), nullable=False), - sa.Column("create_at", sa.DATETIME(), nullable=False), - sa.Column("update_at", sa.DATETIME(), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index("ix_component_name", "component", ["name"], unique=False) - op.create_index( - "ix_component_frontend_node_id", "component", ["frontend_node_id"], unique=False - ) - # ### end Alembic commands ### diff --git a/src/backend/langflow/services/database/manager.py b/src/backend/langflow/services/database/manager.py index 20db90f66..60a4de74d 100644 --- a/src/backend/langflow/services/database/manager.py +++ b/src/backend/langflow/services/database/manager.py @@ -4,6 +4,7 @@ from sqlmodel import SQLModel, Session, create_engine from langflow.utils.logger import logger from alembic.config import Config from alembic import command +from .models import Flow # noqa: F401 class DatabaseManager(Service): From 4e1fc2202ab97a6113117cb123012a223984e609 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 14:16:37 -0300 Subject: [PATCH 40/45] =?UTF-8?q?=F0=9F=94=A5=20refactor(test=5Fcli.py):?= =?UTF-8?q?=20remove=20unused=20imports=20and=20test=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test_cli.py file had some unused imports and test cases that were not being used. This commit removes those unused imports and test cases to improve code cleanliness and maintainability. --- tests/test_cli.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index cd2dcdbb7..408500d7a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,9 +3,6 @@ from tempfile import tempdir from langflow.__main__ import app import pytest -import requests -import multiprocessing -import time from langflow.services import utils @@ -17,28 +14,6 @@ def default_settings(): ] -def test_server(default_settings): - p = multiprocessing.Process( - target=app, - args=(["--host", "localhost", "--port", "8982", *default_settings],), - ) - p.start() - time.sleep(5) # allow some time for the server to start - - response = requests.get( - "http://localhost:8982/health" - ) # assuming a /health endpoint exists - assert response.status_code == 200 - - p.terminate() - - -def test_database_url(runner): - result = runner.invoke(app, ["--database-url", "sqlite:///test.db"]) - assert result.exit_code == 2, result.stdout - assert "No such option: --database-url" in result.output - - def test_components_path(runner, client, default_settings): # Create a foldr in the tmp directory temp_dir = Path(tempdir) From 9fcc96e76760813b4d86ff43c8983f313ff624e7 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 14:18:07 -0300 Subject: [PATCH 41/45] =?UTF-8?q?=F0=9F=94=80=20refactor(manager.py):=20up?= =?UTF-8?q?date=20import=20statement=20for=20models=20in=20database=20mana?= =?UTF-8?q?ger=20to=20improve=20readability=20and=20maintainability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/services/database/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/services/database/manager.py b/src/backend/langflow/services/database/manager.py index 60a4de74d..92385a457 100644 --- a/src/backend/langflow/services/database/manager.py +++ b/src/backend/langflow/services/database/manager.py @@ -4,7 +4,7 @@ from sqlmodel import SQLModel, Session, create_engine from langflow.utils.logger import logger from alembic.config import Config from alembic import command -from .models import Flow # noqa: F401 +from langflow.services.database import models # noqa class DatabaseManager(Service): From 68c4799ab0b648fbe95f58b21683d00a56934b4e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 7 Aug 2023 14:47:04 -0300 Subject: [PATCH 42/45] =?UTF-8?q?=F0=9F=94=A7=20chore(launch.json):=20upda?= =?UTF-8?q?te=20launch.json=20to=20include=20a=20new=20configuration=20for?= =?UTF-8?q?=20debugging=20Python=20tests=20=E2=9C=A8=20feat(launch.json):?= =?UTF-8?q?=20add=20a=20new=20configuration=20for=20debugging=20Python=20t?= =?UTF-8?q?ests=20with=20the=20purpose=20of=20"debug-test"=20and=20console?= =?UTF-8?q?=20set=20to=20"integratedTerminal"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index e09e76cc8..bb61b0b9e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,4 +1,5 @@ { + "version": "0.2.0", "configurations": [ { "name": "Debug Backend", @@ -38,6 +39,15 @@ "request": "launch", "url": "http://localhost:3000/", "webRoot": "${workspaceRoot}/src/frontend" + }, + { + "name": "Python: Debug Tests", + "type": "python", + "request": "launch", + "program": "${file}", + "purpose": ["debug-test"], + "console": "integratedTerminal", + "justMyCode": false } ] } From 3170ac665a33a0c14bf50ff35ee2568d9f6bd814 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 8 Aug 2023 16:15:30 -0300 Subject: [PATCH 43/45] =?UTF-8?q?=F0=9F=93=9D=20docs(pyproject.toml):=20ad?= =?UTF-8?q?d=20documentation=20link=20to=20the=20project=20configuration?= =?UTF-8?q?=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0bf39ba76..2ff3cfeb3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -7578,4 +7578,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.11" -content-hash = "1329d94d3cb37062393d79da99fb3fa7d214ebdcdab6402c411561f960c6689f" +content-hash = "b571961fa0603990e0ba3347fc5000255ce703b5ea7ed167b92792c123e54e4d" diff --git a/pyproject.toml b/pyproject.toml index 31bce703d..5a22d6dc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ readme = "README.md" keywords = ["nlp", "langchain", "openai", "gpt", "gui"] packages = [{ include = "langflow", from = "src/backend" }] include = ["src/backend/langflow/*", "src/backend/langflow/**/*"] - +documentation = "https://docs.langflow.org" [tool.poetry.scripts] langflow = "langflow.__main__:main" From 4b47f9f196713df14c3a80eb51d6d780cd433f0c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 8 Aug 2023 16:35:15 -0300 Subject: [PATCH 44/45] =?UTF-8?q?=F0=9F=90=9B=20fix(endpoints.py):=20add?= =?UTF-8?q?=20check=20to=20skip=20empty=20custom=5Fcomponent=5Fdict=20to?= =?UTF-8?q?=20prevent=20potential=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/api/v1/endpoints.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index ff8717ec0..5d0c9a900 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -51,6 +51,8 @@ def get_all(): logger.info(f"Loading {len(custom_component_dicts)} category(ies)") for custom_component_dict in custom_component_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}" From 67f0ca7549ca8d0013ca241e79102b857a76d178 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 8 Aug 2023 16:50:04 -0300 Subject: [PATCH 45/45] =?UTF-8?q?=F0=9F=93=A6=20chore(pyproject.toml):=20u?= =?UTF-8?q?pdate=20orjson=20dependency=20from=20version=203.9.1=20to=203.9?= =?UTF-8?q?.3=20for=20bug=20fixes=20and=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 77 +++++++++++++++++++++++++++++++++++++------------- pyproject.toml | 2 +- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2ff3cfeb3..aecc8ce57 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4074,28 +4074,67 @@ files = [ [[package]] name = "orjson" -version = "3.9.4" +version = "3.9.3" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.7" files = [ - {file = "orjson-3.9.4-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2e83ec1ee66d83b558a6d273d8a01b86563daa60bea9bc040e2c1cb8008de61f"}, - {file = "orjson-3.9.4-cp310-none-win32.whl", hash = "sha256:04cd7f4a4f4cd2fe43d104eb70e7435c6fcbdde7aa0cde4230e444fbc66924d3"}, - {file = "orjson-3.9.4-cp310-none-win_amd64.whl", hash = "sha256:4fdb59cfa00e10c82e09d1c32a9ce08a38bd29496ba20a73cd7f498e3a0a5024"}, - {file = "orjson-3.9.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:daeed2502ddf1f2b29ec8da2fe2ea82807a5c4acf869608ce6c476db8171d070"}, - {file = "orjson-3.9.4-cp311-none-win32.whl", hash = "sha256:e12492ce65cb10f385e70a88badc6046bc720fa7d468db27b7429d85d41beaeb"}, - {file = "orjson-3.9.4-cp311-none-win_amd64.whl", hash = "sha256:3b9f8bf43a5367d5522f80e7d533c98d880868cd0b640b9088c9237306eca6e8"}, - {file = "orjson-3.9.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:0b400cf89c15958cd829c8a4ade8f5dd73588e63d2fb71a00483e7a74e9f92da"}, - {file = "orjson-3.9.4-cp312-none-win_amd64.whl", hash = "sha256:a533e664a0e3904307d662c5d45775544dc2b38df6e39e213ff6a86ceaa3d53c"}, - {file = "orjson-3.9.4-cp37-cp37m-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:149d1b7630771222f73ecb024ab5dd8e7f41502402b02015494d429bacc4d5c1"}, - {file = "orjson-3.9.4-cp37-none-win32.whl", hash = "sha256:bcda6179eb863c295eb5ea832676d33ef12c04d227b4c98267876c8322e5a96e"}, - {file = "orjson-3.9.4-cp37-none-win_amd64.whl", hash = "sha256:3d947366127abef192419257eb7db7fcee0841ced2b49ccceba43b65e9ce5e3f"}, - {file = "orjson-3.9.4-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a7d029fc34a516f7eae29b778b30371fcb621134b2acfe4c51c785102aefc6cf"}, - {file = "orjson-3.9.4-cp38-none-win32.whl", hash = "sha256:94d15ee45c2aaed334688e511aa73b4681f7c08a0810884c6b3ae5824dea1222"}, - {file = "orjson-3.9.4-cp38-none-win_amd64.whl", hash = "sha256:336ec8471102851f0699198031924617b7a77baadea889df3ffda6000bd59f4c"}, - {file = "orjson-3.9.4-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2f57ccb50e9e123709e9f2d7b1a9e09e694e49d1fa5c5585e34b8e3f01929dc3"}, - {file = "orjson-3.9.4-cp39-none-win32.whl", hash = "sha256:b5b5038187b74e2d33e5caee8a7e83ddeb6a21da86837fa2aac95c69aeb366e6"}, - {file = "orjson-3.9.4-cp39-none-win_amd64.whl", hash = "sha256:915da36bc93ef0c659fa50fe7939d4f208804ad252fc4fc8d55adbbb82293c48"}, + {file = "orjson-3.9.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:082714b5554fcced092c45272f22a93400389733083c43f5043c4316e86f57a2"}, + {file = "orjson-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97ddec69ca4fa1b66d512cf4f4a3fe6a57c4bf21209295ab2f4ada415996e08a"}, + {file = "orjson-3.9.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab7501722ec2172b1c6ea333bc47bba3bbb9b5fc0e3e891191e8447f43d3187d"}, + {file = "orjson-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ae680163ab09f04683d35fbd63eee858019f0066640f7cbad4dba3e7422a4bc"}, + {file = "orjson-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e5abca1e0a9d110bab7346fab0acd3b7848d2ee13318bc24a31bbfbdad974b8"}, + {file = "orjson-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c55f42a8b07cdb7d514cfaeb56f6e9029eef1cbc8e670ac31fc377c46b993cd1"}, + {file = "orjson-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:303f1324f5ea516f8e874ea0f8d15c581caabdca59fc990705fc76f3bd9f3bdf"}, + {file = "orjson-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c444e3931ea4fe7dec26d195486a681fedc0233230c9b84848f8e60affd4a4"}, + {file = "orjson-3.9.3-cp310-none-win32.whl", hash = "sha256:63333de96d83091023c9c99cc579973a2977b15feb5cdc8d9660104c886e9ab8"}, + {file = "orjson-3.9.3-cp310-none-win_amd64.whl", hash = "sha256:7bce6ff507a83c6a4b6b00726f3a7d7aed0b1f0884aac0440e95b55cac0b113e"}, + {file = "orjson-3.9.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec4421f377cce51decd6ea3869a8b41e9f05c50bf6acef8284f8906e642992c4"}, + {file = "orjson-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b3177bd67756e53bdbd72c79fae3507796a67b67c32a16f4b55cad48ef25c13"}, + {file = "orjson-3.9.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b21908252c8a13b8f48d4cccdb7fabb592824cf39c9fa4e9076015dd65eabeba"}, + {file = "orjson-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7b795c6ac344b0c49776b7e135a9bed0cd15b1ade2a4c7b3a19e3913247702e"}, + {file = "orjson-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac43842f5ba26e6f21b4e63312bd1137111a9b9821d7f7dfe189a4015c6c6bc"}, + {file = "orjson-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8def4f6560c7b6dbc4b356dfd8e6624a018d920ce5a2864291a2bf1052cd6b68"}, + {file = "orjson-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bbc0dafd1de42c8dbfd6e5d1fe4deab15d2de474e11475921286bebefd109ec8"}, + {file = "orjson-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:85b1870d5420292419b34002659082d77f31b13d4d8cbd67bed9d717c775a0fb"}, + {file = "orjson-3.9.3-cp311-none-win32.whl", hash = "sha256:d6ece3f48f14a06c325181f2b9bd9a9827aac2ecdcad11eb12f561fb697eaaaa"}, + {file = "orjson-3.9.3-cp311-none-win_amd64.whl", hash = "sha256:448feda092c681c0a5b8eec62dd4f625ad5d316dafd56c81fb3f05b5221827ff"}, + {file = "orjson-3.9.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:413d7cf731f1222373360128a3d5232d52630a7355f446bf2659fc3445ec0b76"}, + {file = "orjson-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009a0f79804c604998b068f5f942e40546913ed45ee2f0a3d0e75695bf7543fa"}, + {file = "orjson-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ce062844255cce4d6a8a150e8e78b9fcd6c5a3f1ff3f8792922de25827c25b9c"}, + {file = "orjson-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:776659e18debe5de73c30b0957cd6454fcc61d87377fcb276441fca1b9f1305d"}, + {file = "orjson-3.9.3-cp312-none-win_amd64.whl", hash = "sha256:47b237da3818c8e546df4d2162f0a5cfd50b7b58528907919a27244141e0e48e"}, + {file = "orjson-3.9.3-cp37-cp37m-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f954115d8496d4ab5975438e3ce07780c1644ea0a66c78a943ef79f33769b61a"}, + {file = "orjson-3.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05c57100517b6dbfe34181ed2248bebfab03bd2a7aafb6fbf849c6fd3bb2fbda"}, + {file = "orjson-3.9.3-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa6017140fe487ab8fae605a2890c94c6fbe7a8e763ff33bbdb00e27ce078cfd"}, + {file = "orjson-3.9.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fe77af2ff33c370fb06c9fdf004a66d85ea19c77f0273bbf70c70f98f832725"}, + {file = "orjson-3.9.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2fa8c385b27bab886caa098fa3ae114d56571ae6e7a5610cb624d7b0a66faed"}, + {file = "orjson-3.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8323739e7905ae4ec4dbdebb31067d28be981f30c11b6ae88ddec2671c0b3194"}, + {file = "orjson-3.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ad43fd5b1ededb54fe01e67468710fcfec8a5830e4ce131f85e741ea151a18e9"}, + {file = "orjson-3.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:42cb645780f732c829bc351346a54157d57f2bc409e671ee36b9fc1037bb77fe"}, + {file = "orjson-3.9.3-cp37-none-win32.whl", hash = "sha256:b84542669d1b0175dc2870025b73cbd4f4a3beb17796de6ec82683663e0400f3"}, + {file = "orjson-3.9.3-cp37-none-win_amd64.whl", hash = "sha256:1440a404ce84f43e2f8e97d8b5fe6f271458e0ffd37290dc3a9f6aa067c69930"}, + {file = "orjson-3.9.3-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1da8edaefb75f25b449ed4e22d00b9b49211b97dcefd44b742bdd8721d572788"}, + {file = "orjson-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47210746acda49febe3bb07253eb5d63d7c7511beec5fa702aad3ce64e15664f"}, + {file = "orjson-3.9.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:893c62afd5b26f04e2814dffa4d9d4060583ac43dc3e79ed3eadf62a5ac37b2c"}, + {file = "orjson-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32aef33ae33901c327fd5679f91fa37199834d122dffd234416a6fe4193d1982"}, + {file = "orjson-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd2761384ddb9de63b20795845d5cedadf052255a34c3ff1750cfc77b29d9926"}, + {file = "orjson-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e2502b4af2055050dcc74718f2647b65102087c6f5b3f939e2e1a3e3099602"}, + {file = "orjson-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fa7c7a39eeb8dd171f59d96fd4610f908ac14b2f2eb268f4498e5f310bda8da7"}, + {file = "orjson-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc3fe0c0ae7acf00d827efe2506131f1b19af3c87e3d76b0e081748984e51c26"}, + {file = "orjson-3.9.3-cp38-none-win32.whl", hash = "sha256:5b1ff8e920518753b310034e5796f0116f7732b0b27531012d46f0b54f3c8c85"}, + {file = "orjson-3.9.3-cp38-none-win_amd64.whl", hash = "sha256:9f2b1007174c93dd838f52e623c972df33057e3cb7ad9341b7d9bbd66b8d8fb4"}, + {file = "orjson-3.9.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cddc5b8bd7b0d1dfd36637eedbd83726b8b8a5969d3ecee70a9b54a94b8a0258"}, + {file = "orjson-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c3bbf4b6f94fad2fd73c81293da8b343fbd07ce48d7836c07d0d54b58c8e93"}, + {file = "orjson-3.9.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5cc22ef6973992db18952f8b978781e19a0c62c098f475db936284df9311df7"}, + {file = "orjson-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dcea93630986209c690f27f32398956b04ccbba8f1fa7c3d1bb88a01d9ab87a"}, + {file = "orjson-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:526cb34e63faaad908c34597294507b7a4b999a436b4f206bc4e60ff4e911c20"}, + {file = "orjson-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f5ac6e30ee10af57f52e72f9c8b9bc4846a9343449d10ca2ae9760615da3042"}, + {file = "orjson-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b6c37ab097c062bdf535105c7156839c4e370065c476bb2393149ad31a2cdf6e"}, + {file = "orjson-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:27d69628f449c52a7a34836b15ec948804254f7954457f88de53f2f4de99512f"}, + {file = "orjson-3.9.3-cp39-none-win32.whl", hash = "sha256:5297463d8831c2327ed22bf92eb6d50347071ff1c73fb4702d50b8bc514aeac9"}, + {file = "orjson-3.9.3-cp39-none-win_amd64.whl", hash = "sha256:69a33486b5b6e5a99939fdb13c1c0d8bcc7c89fe6083e7b9ce3c70931ca9fb71"}, + {file = "orjson-3.9.3.tar.gz", hash = "sha256:d3da4faf6398154c1e75d32778035fa7dc284814809f76e8f8d50c4f54859399"}, ] [[package]] @@ -7578,4 +7617,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.11" -content-hash = "b571961fa0603990e0ba3347fc5000255ce703b5ea7ed167b92792c123e54e4d" +content-hash = "7c6d7dc33a9b0ae9da053fb78b9f2eabbe78df38c4763e5a8719df6249d6f657" diff --git a/pyproject.toml b/pyproject.toml index 5a22d6dc7..5b7054b65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ python-multipart = "^0.0.6" sqlmodel = "^0.0.8" faiss-cpu = "^1.7.4" anthropic = "^0.3.0" -orjson = "^3.9.1" +orjson = "3.9.3" multiprocess = "^0.70.14" cachetools = "^5.3.1" types-cachetools = "^5.3.0.5"