Merge branch 'zustand/io/migration' into globalVariables
This commit is contained in:
commit
6c9a87bed7
529 changed files with 26323 additions and 10993 deletions
|
|
@ -109,7 +109,11 @@ def version_callback(value: bool):
|
|||
@app.callback()
|
||||
def main_entry_point(
|
||||
version: bool = typer.Option(
|
||||
None, "--version", callback=version_callback, is_eager=True, help="Show the version and exit."
|
||||
None,
|
||||
"--version",
|
||||
callback=version_callback,
|
||||
is_eager=True,
|
||||
help="Show the version and exit.",
|
||||
),
|
||||
):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
|
|||
# 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
|
||||
sqlalchemy.url = sqlite:///./langflow.db
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
|
|
@ -98,7 +98,7 @@ handlers =
|
|||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
level = DEBUG
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import os
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
from loguru import logger
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
from langflow.services.database.models import * # noqa
|
||||
from langflow.services.database.service import SQLModel
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
|
|
@ -40,7 +41,8 @@ def run_migrations_offline() -> None:
|
|||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
url = os.getenv("LANGFLOW_DATABASE_URL")
|
||||
url = url or config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
|
|
@ -60,12 +62,32 @@ def run_migrations_online() -> None:
|
|||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
try:
|
||||
from langflow.services.database.factory import DatabaseServiceFactory
|
||||
from langflow.services.deps import get_db_service
|
||||
from langflow.services.manager import (
|
||||
initialize_settings_service,
|
||||
service_manager,
|
||||
)
|
||||
from langflow.services.schema import ServiceType
|
||||
|
||||
initialize_settings_service()
|
||||
service_manager.register_factory(
|
||||
DatabaseServiceFactory(), [ServiceType.SETTINGS_SERVICE]
|
||||
)
|
||||
connectable = get_db_service().engine
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting database engine: {e}")
|
||||
url = os.getenv("LANGFLOW_DATABASE_URL")
|
||||
url = url or config.get_main_option("sqlalchemy.url")
|
||||
if url:
|
||||
config.set_main_option("sqlalchemy.url", url)
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata, render_as_batch=True
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from typing import Sequence, Union
|
|||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
|
@ -20,8 +21,14 @@ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
${downgrades if downgrades else "pass"}
|
||||
|
|
|
|||
|
|
@ -5,28 +5,43 @@ Revises: 1ef9c4f3765d
|
|||
Create Date: 2023-12-13 18:55:52.587360
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '006b3990db50'
|
||||
down_revision: Union[str, None] = '1ef9c4f3765d'
|
||||
revision: str = "006b3990db50"
|
||||
down_revision: Union[str, None] = "1ef9c4f3765d"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
api_key_constraints = inspector.get_unique_constraints("apikey")
|
||||
flow_constraints = inspector.get_unique_constraints("flow")
|
||||
user_constraints = inspector.get_unique_constraints("user")
|
||||
try:
|
||||
with op.batch_alter_table('apikey', schema=None) as batch_op:
|
||||
batch_op.create_unique_constraint('uq_apikey_id', ['id'])
|
||||
if not any(
|
||||
constraint["name"] == "uq_apikey_id" for constraint in api_key_constraints
|
||||
):
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
|
||||
with op.batch_alter_table('flow', schema=None) as batch_op:
|
||||
batch_op.create_unique_constraint('uq_flow_id', ['id'])
|
||||
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.create_unique_constraint('uq_user_id', ['id'])
|
||||
batch_op.create_unique_constraint("uq_apikey_id", ["id"])
|
||||
if not any(
|
||||
constraint["name"] == "uq_flow_id" for constraint in flow_constraints
|
||||
):
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.create_unique_constraint("uq_flow_id", ["id"])
|
||||
if not any(
|
||||
constraint["name"] == "uq_user_id" for constraint in user_constraints
|
||||
):
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.create_unique_constraint("uq_user_id", ["id"])
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
|
@ -36,15 +51,24 @@ def upgrade() -> None:
|
|||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
api_key_constraints = inspector.get_unique_constraints("apikey")
|
||||
flow_constraints = inspector.get_unique_constraints("flow")
|
||||
user_constraints = inspector.get_unique_constraints("user")
|
||||
try:
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.drop_constraint('uq_user_id', type_='unique')
|
||||
if any(
|
||||
constraint["name"] == "uq_apikey_id" for constraint in api_key_constraints
|
||||
):
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("uq_user_id", type_="unique")
|
||||
if any(constraint["name"] == "uq_flow_id" for constraint in flow_constraints):
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("uq_flow_id", type_="unique")
|
||||
if any(constraint["name"] == "uq_user_id" for constraint in user_constraints):
|
||||
|
||||
with op.batch_alter_table('flow', schema=None) as batch_op:
|
||||
batch_op.drop_constraint('uq_flow_id', type_='unique')
|
||||
|
||||
with op.batch_alter_table('apikey', schema=None) as batch_op:
|
||||
batch_op.drop_constraint('uq_apikey_id', type_='unique')
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("uq_apikey_id", type_="unique")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -5,67 +5,25 @@ Revises: 006b3990db50
|
|||
Create Date: 2024-01-17 10:32:56.686287
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '0b8757876a7c'
|
||||
down_revision: Union[str, None] = '006b3990db50'
|
||||
revision: str = "0b8757876a7c"
|
||||
down_revision: Union[str, None] = "006b3990db50"
|
||||
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! ###
|
||||
try:
|
||||
with op.batch_alter_table('apikey', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_apikey_api_key'), ['api_key'], unique=True)
|
||||
batch_op.create_index(batch_op.f('ix_apikey_name'), ['name'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_apikey_user_id'), ['user_id'], unique=False)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
try:
|
||||
with op.batch_alter_table('flow', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_flow_description'), ['description'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_flow_name'), ['name'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_flow_user_id'), ['user_id'], unique=False)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
pass
|
||||
|
||||
try:
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_user_username'))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
try:
|
||||
with op.batch_alter_table('flow', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_flow_user_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_flow_name'))
|
||||
batch_op.drop_index(batch_op.f('ix_flow_description'))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
try:
|
||||
with op.batch_alter_table('apikey', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_apikey_user_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_apikey_name'))
|
||||
batch_op.drop_index(batch_op.f('ix_apikey_api_key'))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Revises: fd531f8868b1
|
|||
Create Date: 2023-12-04 15:00:27.968998
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
|
@ -13,8 +14,8 @@ import sqlmodel
|
|||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '1ef9c4f3765d'
|
||||
down_revision: Union[str, None] = 'fd531f8868b1'
|
||||
revision: str = "1ef9c4f3765d"
|
||||
down_revision: Union[str, None] = "fd531f8868b1"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
|
@ -22,10 +23,10 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table('apikey', schema=None) as batch_op:
|
||||
batch_op.alter_column('name',
|
||||
existing_type=sqlmodel.sql.sqltypes.AutoString(),
|
||||
nullable=True)
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"name", existing_type=sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -34,10 +35,8 @@ def upgrade() -> None:
|
|||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table('apikey', schema=None) as batch_op:
|
||||
batch_op.alter_column('name',
|
||||
existing_type=sa.VARCHAR(),
|
||||
nullable=False)
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=False)
|
||||
except Exception as e:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Revises:
|
|||
Create Date: 2023-08-27 19:49:02.681355
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
|
@ -33,7 +34,9 @@ def upgrade() -> None:
|
|||
if "ix_flowstyle_flow_id" in [
|
||||
index["name"] for index in inspector.get_indexes("flowstyle")
|
||||
]:
|
||||
op.drop_index("ix_flowstyle_flow_id", table_name="flowstyle")
|
||||
op.drop_index(
|
||||
"ix_flowstyle_flow_id", table_name="flowstyle", if_exists=True
|
||||
)
|
||||
|
||||
existing_indices_flow = []
|
||||
existing_fks_flow = []
|
||||
|
|
@ -80,8 +83,7 @@ def upgrade() -> None:
|
|||
sa.Column("api_key", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["user.id"],
|
||||
["user_id"], ["user.id"], name="fk_apikey_user_id_user"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name="pk_apikey"),
|
||||
sa.UniqueConstraint("id", name="uq_apikey_id"),
|
||||
|
|
@ -103,8 +105,7 @@ def upgrade() -> None:
|
|||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["user.id"],
|
||||
["user_id"], ["user.id"], name="fk_flow_user_id_user"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name="pk_flow"),
|
||||
sa.UniqueConstraint("id", name="uq_flow_id"),
|
||||
|
|
@ -151,21 +152,21 @@ def downgrade() -> None:
|
|||
existing_tables = inspector.get_table_names()
|
||||
if "flow" in existing_tables:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_flow_user_id"))
|
||||
batch_op.drop_index(batch_op.f("ix_flow_name"))
|
||||
batch_op.drop_index(batch_op.f("ix_flow_description"))
|
||||
batch_op.drop_index(batch_op.f("ix_flow_user_id"), if_exists=True)
|
||||
batch_op.drop_index(batch_op.f("ix_flow_name"), if_exists=True)
|
||||
batch_op.drop_index(batch_op.f("ix_flow_description"), if_exists=True)
|
||||
|
||||
op.drop_table("flow")
|
||||
if "apikey" in existing_tables:
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_apikey_user_id"))
|
||||
batch_op.drop_index(batch_op.f("ix_apikey_name"))
|
||||
batch_op.drop_index(batch_op.f("ix_apikey_api_key"))
|
||||
batch_op.drop_index(batch_op.f("ix_apikey_user_id"), if_exists=True)
|
||||
batch_op.drop_index(batch_op.f("ix_apikey_name"), if_exists=True)
|
||||
batch_op.drop_index(batch_op.f("ix_apikey_api_key"), if_exists=True)
|
||||
|
||||
op.drop_table("apikey")
|
||||
if "user" in existing_tables:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_user_username"))
|
||||
batch_op.drop_index(batch_op.f("ix_user_username"), if_exists=True)
|
||||
|
||||
op.drop_table("user")
|
||||
|
||||
|
|
|
|||
|
|
@ -5,34 +5,44 @@ Revises: 7d2162acc8b2
|
|||
Create Date: 2023-11-24 10:45:38.465302
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '2ac71eb9c3ae'
|
||||
down_revision: Union[str, None] = '7d2162acc8b2'
|
||||
revision: str = "2ac71eb9c3ae"
|
||||
down_revision: Union[str, None] = "7d2162acc8b2"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
tables = inspector.get_table_names()
|
||||
try:
|
||||
op.create_table('credential',
|
||||
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column('value', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column('provider', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column('user_id', sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column('id', sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
if "credential" not in tables:
|
||||
op.create_table(
|
||||
"credential",
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("value", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column(
|
||||
"provider", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
|
@ -40,7 +50,7 @@ def upgrade() -> None:
|
|||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
op.drop_table('credential')
|
||||
op.drop_table("credential")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
"""Add icon and icon_bg_color to Flow
|
||||
|
||||
Revision ID: 63b9c451fd30
|
||||
Revises: bc2f01c40e4a
|
||||
Create Date: 2024-03-06 10:53:47.148658
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "63b9c451fd30"
|
||||
down_revision: Union[str, None] = "bc2f01c40e4a"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
column_names = [column["name"] for column in inspector.get_columns("flow")]
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "icon" not in column_names:
|
||||
batch_op.add_column(
|
||||
sa.Column("icon", sqlmodel.sql.sqltypes.AutoString(), nullable=True)
|
||||
)
|
||||
if "icon_bg_color" not in column_names:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"icon_bg_color", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
)
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
column_names = [column["name"] for column in inspector.get_columns("flow")]
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "icon" in column_names:
|
||||
batch_op.drop_column("icon")
|
||||
if "icon_bg_color" in column_names:
|
||||
batch_op.drop_column("icon_bg_color")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -5,6 +5,7 @@ Revises: 260dbcc8b680
|
|||
Create Date: 2023-09-08 07:36:13.387318
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
|
@ -21,29 +22,36 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
if "user" in inspector.get_table_names() and "profile_image" not in [
|
||||
column["name"] for column in inspector.get_columns("user")
|
||||
]:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"profile_image", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
try:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
if "user" in inspector.get_table_names() and "profile_image" not in [
|
||||
column["name"] for column in inspector.get_columns("user")
|
||||
]:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"profile_image",
|
||||
sqlmodel.sql.sqltypes.AutoString(),
|
||||
nullable=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
if "user" in inspector.get_table_names() and "profile_image" in [
|
||||
column["name"] for column in inspector.get_columns("user")
|
||||
]:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.drop_column("profile_image")
|
||||
try:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
if "user" in inspector.get_table_names() and "profile_image" in [
|
||||
column["name"] for column in inspector.get_columns("user")
|
||||
]:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.drop_column("profile_image")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ Revises: eb5866d51fd2
|
|||
Create Date: 2023-10-18 23:08:57.744906
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
from loguru import logger
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "7843803a87b5"
|
||||
|
|
@ -21,19 +22,26 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
flow_columns = [column["name"] for column in inspector.get_columns("flow")]
|
||||
user_columns = [column["name"] for column in inspector.get_columns("user")]
|
||||
try:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("is_component", sa.Boolean(), nullable=True))
|
||||
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"store_api_key", sqlmodel.AutoString(), nullable=True
|
||||
if "is_component" not in flow_columns:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("is_component", sa.Boolean(), nullable=True)
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
pass
|
||||
try:
|
||||
if "store_api_key" not in user_columns:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("store_api_key", sqlmodel.AutoString(), nullable=True)
|
||||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,88 +5,74 @@ Revises: f5ee9749d1a6
|
|||
Create Date: 2023-11-21 20:56:53.998781
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '7d2162acc8b2'
|
||||
down_revision: Union[str, None] = 'f5ee9749d1a6'
|
||||
revision: str = "7d2162acc8b2"
|
||||
down_revision: Union[str, None] = "f5ee9749d1a6"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
api_key_columns = [column["name"] for column in inspector.get_columns("apikey")]
|
||||
flow_columns = [column["name"] for column in inspector.get_columns("flow")]
|
||||
|
||||
try:
|
||||
with op.batch_alter_table('component', schema=None) as batch_op:
|
||||
batch_op.drop_index('ix_component_frontend_node_id')
|
||||
batch_op.drop_index('ix_component_name')
|
||||
op.drop_table('component')
|
||||
op.drop_table('flowstyle')
|
||||
if "name" in api_key_columns:
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"name", existing_type=sa.VARCHAR(), nullable=False
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
with op.batch_alter_table('apikey', schema=None) as batch_op:
|
||||
batch_op.alter_column('name',
|
||||
existing_type=sa.VARCHAR(),
|
||||
nullable=False)
|
||||
|
||||
with op.batch_alter_table('flow', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('updated_at', sa.DateTime(), nullable=True))
|
||||
batch_op.add_column(sa.Column('folder', sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
pass
|
||||
try:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "updated_at" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True)
|
||||
)
|
||||
if "folder" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
pass
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table('flow', schema=None) as batch_op:
|
||||
batch_op.drop_column('folder')
|
||||
batch_op.drop_column('updated_at')
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.drop_column("folder")
|
||||
batch_op.drop_column("updated_at")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
try:
|
||||
|
||||
with op.batch_alter_table('apikey', schema=None) as batch_op:
|
||||
batch_op.alter_column('name',
|
||||
existing_type=sa.VARCHAR(),
|
||||
nullable=True)
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=True)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
try:
|
||||
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')
|
||||
)
|
||||
|
||||
with op.batch_alter_table('component', schema=None) as batch_op:
|
||||
batch_op.create_index('ix_component_name', ['name'], unique=False)
|
||||
batch_op.create_index('ix_component_frontend_node_id', ['frontend_node_id'], unique=False)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
|
|
|||
|
|
@ -5,55 +5,105 @@ Revises: 0b8757876a7c
|
|||
Create Date: 2024-01-26 13:31:14.797548
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b2fa308044b5'
|
||||
down_revision: Union[str, None] = '0b8757876a7c'
|
||||
revision: str = "b2fa308044b5"
|
||||
down_revision: Union[str, None] = "0b8757876a7c"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
tables = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
op.drop_table('flowstyle')
|
||||
with op.batch_alter_table('flow', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('is_component', sa.Boolean(), nullable=True))
|
||||
batch_op.add_column(sa.Column('updated_at', sa.DateTime(), nullable=True))
|
||||
batch_op.add_column(sa.Column('folder', sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
batch_op.add_column(sa.Column('user_id', sqlmodel.sql.sqltypes.GUID(), nullable=True))
|
||||
batch_op.create_index(batch_op.f('ix_flow_user_id'), ['user_id'], unique=False)
|
||||
batch_op.create_foreign_key('fk_flow_user_id_user', 'user', ['user_id'], ['id'])
|
||||
if "flowstyle" in tables:
|
||||
op.drop_table("flowstyle")
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
flow_columns = [column["name"] for column in inspector.get_columns("flow")]
|
||||
if "is_component" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("is_component", sa.Boolean(), nullable=True)
|
||||
)
|
||||
if "updated_at" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True)
|
||||
)
|
||||
if "folder" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
)
|
||||
)
|
||||
if "user_id" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=True)
|
||||
)
|
||||
indices = inspector.get_indexes("flow")
|
||||
indices_names = [index["name"] for index in indices]
|
||||
if "ix_flow_user_id" not in indices_names:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_flow_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
if "fk_flow_user_id_user" not in indices_names:
|
||||
batch_op.create_foreign_key(
|
||||
"fk_flow_user_id_user", "user", ["user_id"], ["id"]
|
||||
)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
try:
|
||||
with op.batch_alter_table('flow', schema=None) as batch_op:
|
||||
batch_op.drop_constraint('fk_flow_user_id_user', type_='foreignkey')
|
||||
batch_op.drop_index(batch_op.f('ix_flow_user_id'))
|
||||
batch_op.drop_column('user_id')
|
||||
batch_op.drop_column('folder')
|
||||
batch_op.drop_column('updated_at')
|
||||
batch_op.drop_column('is_component')
|
||||
# Re-create the dropped table 'flowstyle' if it was previously dropped in upgrade
|
||||
if "flowstyle" not in inspector.get_table_names():
|
||||
op.create_table(
|
||||
"flowstyle",
|
||||
sa.Column("color", sa.String(), nullable=False),
|
||||
sa.Column("emoji", sa.String(), 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"),
|
||||
)
|
||||
|
||||
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')
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
# Check and remove newly added columns and constraints in upgrade
|
||||
flow_columns = [column["name"] for column in inspector.get_columns("flow")]
|
||||
if "user_id" in flow_columns:
|
||||
batch_op.drop_column("user_id")
|
||||
if "folder" in flow_columns:
|
||||
batch_op.drop_column("folder")
|
||||
if "updated_at" in flow_columns:
|
||||
batch_op.drop_column("updated_at")
|
||||
if "is_component" in flow_columns:
|
||||
batch_op.drop_column("is_component")
|
||||
|
||||
indices = inspector.get_indexes("flow")
|
||||
indices_names = [index["name"] for index in indices]
|
||||
if "ix_flow_user_id" in indices_names:
|
||||
batch_op.drop_index("ix_flow_user_id")
|
||||
# Assuming fk_flow_user_id_user is a foreign key constraint's name, not an index
|
||||
constraints = inspector.get_foreign_keys("flow")
|
||||
constraint_names = [constraint["name"] for constraint in constraints]
|
||||
if "fk_flow_user_id_user" in constraint_names:
|
||||
batch_op.drop_constraint("fk_flow_user_id_user", type_="foreignkey")
|
||||
|
||||
except Exception as e:
|
||||
# It's generally a good idea to log the exception or handle it in a way other than a bare pass
|
||||
print(f"Error during downgrade: {e}")
|
||||
|
|
|
|||
|
|
@ -5,46 +5,68 @@ Revises: b2fa308044b5
|
|||
Create Date: 2024-01-26 13:34:14.496769
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'bc2f01c40e4a'
|
||||
down_revision: Union[str, None] = 'b2fa308044b5'
|
||||
revision: str = "bc2f01c40e4a"
|
||||
down_revision: Union[str, None] = "b2fa308044b5"
|
||||
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! ###
|
||||
try:
|
||||
with op.batch_alter_table('flow', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('is_component', sa.Boolean(), nullable=True))
|
||||
batch_op.add_column(sa.Column('updated_at', sa.DateTime(), nullable=True))
|
||||
batch_op.add_column(sa.Column('folder', sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
batch_op.add_column(sa.Column('user_id', sqlmodel.sql.sqltypes.GUID(), nullable=True))
|
||||
batch_op.create_index(batch_op.f('ix_flow_user_id'), ['user_id'], unique=False)
|
||||
batch_op.create_foreign_key('flow_user_id_fkey'
|
||||
, 'user', ['user_id'], ['id'])
|
||||
except Exception:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
flow_columns = {column["name"] for column in inspector.get_columns("flow")}
|
||||
flow_indexes = {index["name"] for index in inspector.get_indexes("flow")}
|
||||
flow_fks = {fk["name"] for fk in inspector.get_foreign_keys("flow")}
|
||||
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "is_component" not in flow_columns:
|
||||
batch_op.add_column(sa.Column("is_component", sa.Boolean(), nullable=True))
|
||||
if "updated_at" not in flow_columns:
|
||||
batch_op.add_column(sa.Column("updated_at", sa.DateTime(), nullable=True))
|
||||
if "folder" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True)
|
||||
)
|
||||
if "user_id" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=True)
|
||||
)
|
||||
if "ix_flow_user_id" not in flow_indexes:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_flow_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
if "flow_user_id_fkey" not in flow_fks:
|
||||
batch_op.create_foreign_key(
|
||||
"flow_user_id_fkey", "user", ["user_id"], ["id"]
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table('flow', schema=None) as batch_op:
|
||||
batch_op.drop_constraint('flow_user_id_fkey', type_='foreignkey')
|
||||
batch_op.drop_index(batch_op.f('ix_flow_user_id'))
|
||||
batch_op.drop_column('user_id')
|
||||
batch_op.drop_column('folder')
|
||||
batch_op.drop_column('updated_at')
|
||||
batch_op.drop_column('is_component')
|
||||
except Exception:
|
||||
pass
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
flow_columns = {column["name"] for column in inspector.get_columns("flow")}
|
||||
flow_indexes = {index["name"] for index in inspector.get_indexes("flow")}
|
||||
flow_fks = {fk["name"] for fk in inspector.get_foreign_keys("flow")}
|
||||
|
||||
# ### end Alembic commands ###
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "flow_user_id_fkey" in flow_fks:
|
||||
batch_op.drop_constraint("flow_user_id_fkey", type_="foreignkey")
|
||||
if "ix_flow_user_id" in flow_indexes:
|
||||
batch_op.drop_index(batch_op.f("ix_flow_user_id"))
|
||||
if "user_id" in flow_columns:
|
||||
batch_op.drop_column("user_id")
|
||||
if "folder" in flow_columns:
|
||||
batch_op.drop_column("folder")
|
||||
if "updated_at" in flow_columns:
|
||||
batch_op.drop_column("updated_at")
|
||||
if "is_component" in flow_columns:
|
||||
batch_op.drop_column("is_component")
|
||||
|
|
|
|||
|
|
@ -5,11 +5,10 @@ Revises: 67cc006d50bf
|
|||
Create Date: 2023-10-04 10:18:25.640458
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy import exc
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "eb5866d51fd2"
|
||||
|
|
@ -21,70 +20,12 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
connection = op.get_bind()
|
||||
try:
|
||||
op.drop_table("flowstyle")
|
||||
with op.batch_alter_table("component", schema=None) as batch_op:
|
||||
batch_op.drop_index("ix_component_frontend_node_id")
|
||||
batch_op.drop_index("ix_component_name")
|
||||
except exc.SQLAlchemyError:
|
||||
# connection.execute(text("ROLLBACK"))
|
||||
pass
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
try:
|
||||
op.drop_table("component")
|
||||
except exc.SQLAlchemyError:
|
||||
# connection.execute(text("ROLLBACK"))
|
||||
pass
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
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", name="pk_component"),
|
||||
)
|
||||
with op.batch_alter_table("component", schema=None) as batch_op:
|
||||
batch_op.create_index("ix_component_name", ["name"], unique=False)
|
||||
batch_op.create_index(
|
||||
"ix_component_frontend_node_id", ["frontend_node_id"], unique=False
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
try:
|
||||
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", name="pk_flowstyle"),
|
||||
sa.UniqueConstraint("id", name="uq_flowstyle_id"),
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Revises: 7843803a87b5
|
|||
Create Date: 2023-10-18 23:12:27.297016
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
|
|
|||
|
|
@ -5,22 +5,35 @@ Revises: 2ac71eb9c3ae
|
|||
Create Date: 2023-11-24 15:07:37.566516
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from typing import Optional, Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'fd531f8868b1'
|
||||
down_revision: Union[str, None] = '2ac71eb9c3ae'
|
||||
revision: str = "fd531f8868b1"
|
||||
down_revision: Union[str, None] = "2ac71eb9c3ae"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
tables = inspector.get_table_names()
|
||||
foreign_keys_names = []
|
||||
if "credential" in tables:
|
||||
foreign_keys = inspector.get_foreign_keys("credential")
|
||||
foreign_keys_names = [fk["name"] for fk in foreign_keys]
|
||||
|
||||
try:
|
||||
with op.batch_alter_table('credential', schema=None) as batch_op:
|
||||
batch_op.create_foreign_key("fk_credential_user_id", 'user', ['user_id'], ['id'])
|
||||
if "credential" in tables and "fk_credential_user_id" not in foreign_keys_names:
|
||||
with op.batch_alter_table("credential", schema=None) as batch_op:
|
||||
batch_op.create_foreign_key(
|
||||
"fk_credential_user_id", "user", ["user_id"], ["id"]
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
|
@ -30,9 +43,17 @@ def upgrade() -> None:
|
|||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
tables = inspector.get_table_names()
|
||||
foreign_keys_names: list[Optional[str]] = []
|
||||
if "credential" in tables:
|
||||
foreign_keys = inspector.get_foreign_keys("credential")
|
||||
foreign_keys_names = [fk["name"] for fk in foreign_keys]
|
||||
try:
|
||||
with op.batch_alter_table('credential', schema=None) as batch_op:
|
||||
batch_op.drop_constraint("fk_credential_user_id", type_='foreignkey')
|
||||
if "credential" in tables and "fk_credential_user_id" in foreign_keys_names:
|
||||
with op.batch_alter_table("credential", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("fk_credential_user_id", type_="foreignkey")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ from langflow.api.v1 import (
|
|||
chat_router,
|
||||
credentials_router,
|
||||
endpoints_router,
|
||||
files_router,
|
||||
flows_router,
|
||||
login_router,
|
||||
monitor_router,
|
||||
store_router,
|
||||
users_router,
|
||||
validate_router,
|
||||
|
|
@ -25,3 +27,5 @@ router.include_router(users_router)
|
|||
router.include_router(api_key_router)
|
||||
router.include_router(login_router)
|
||||
router.include_router(credentials_router)
|
||||
router.include_router(files_router)
|
||||
router.include_router(monitor_router)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, List
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from platformdirs import user_cache_dir
|
||||
from sqlmodel import Session
|
||||
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.services.chat.service import ChatService
|
||||
from langflow.services.database.models.flow import Flow
|
||||
from langflow.services.store.schema import StoreComponentCreate
|
||||
from langflow.services.store.utils import get_lf_version_from_pypi
|
||||
|
||||
|
|
@ -137,7 +141,7 @@ def get_file_path_value(file_path):
|
|||
return file_path
|
||||
|
||||
|
||||
def validate_is_component(flows: List["Flow"]):
|
||||
def validate_is_component(flows: list["Flow"]):
|
||||
for flow in flows:
|
||||
if not flow.data or flow.is_component is not None:
|
||||
continue
|
||||
|
|
@ -171,19 +175,66 @@ async def check_langflow_version(component: StoreComponentCreate):
|
|||
)
|
||||
|
||||
|
||||
def format_elapsed_time(elapsed_time) -> str:
|
||||
# Format elapsed time to human readable format coming from
|
||||
# perf_counter()
|
||||
# If the elapsed time is less than 1 second, return ms
|
||||
# If the elapsed time is less than 1 minute, return seconds rounded to 2 decimals
|
||||
time_str = ""
|
||||
def format_elapsed_time(elapsed_time: float) -> str:
|
||||
"""Format elapsed time to a human-readable format coming from perf_counter().
|
||||
|
||||
- Less than 1 second: returns milliseconds
|
||||
- Less than 1 minute: returns seconds rounded to 2 decimals
|
||||
- 1 minute or more: returns minutes and seconds
|
||||
"""
|
||||
if elapsed_time < 1:
|
||||
elapsed_time = int(round(elapsed_time * 1000))
|
||||
time_str = f"{elapsed_time} ms"
|
||||
milliseconds = int(round(elapsed_time * 1000))
|
||||
return f"{milliseconds} ms"
|
||||
elif elapsed_time < 60:
|
||||
elapsed_time = round(elapsed_time, 2)
|
||||
time_str = f"{elapsed_time} seconds"
|
||||
seconds = round(elapsed_time, 2)
|
||||
unit = "second" if seconds == 1 else "seconds"
|
||||
return f"{seconds} {unit}"
|
||||
else:
|
||||
elapsed_time = round(elapsed_time / 60, 2)
|
||||
time_str = f"{elapsed_time} minutes"
|
||||
return time_str
|
||||
minutes = int(elapsed_time // 60)
|
||||
seconds = round(elapsed_time % 60, 2)
|
||||
minutes_unit = "minute" if minutes == 1 else "minutes"
|
||||
seconds_unit = "second" if seconds == 1 else "seconds"
|
||||
return f"{minutes} {minutes_unit}, {seconds} {seconds_unit}"
|
||||
|
||||
|
||||
async def build_and_cache_graph(
|
||||
flow_id: str,
|
||||
session: Session,
|
||||
chat_service: "ChatService",
|
||||
graph: Optional[Graph] = None,
|
||||
):
|
||||
"""Build and cache the graph."""
|
||||
flow: Optional[Flow] = session.get(Flow, flow_id)
|
||||
if not flow or not flow.data:
|
||||
raise ValueError("Invalid flow ID")
|
||||
other_graph = Graph.from_payload(flow.data, flow_id)
|
||||
if graph is None:
|
||||
graph = other_graph
|
||||
else:
|
||||
graph = graph.update(other_graph)
|
||||
await chat_service.set_cache(flow_id, graph)
|
||||
return graph
|
||||
|
||||
|
||||
def format_syntax_error_message(exc: SyntaxError) -> str:
|
||||
"""Format a SyntaxError message for returning to the frontend."""
|
||||
if exc.text is None:
|
||||
return f"Syntax error in code. Error on line {exc.lineno}"
|
||||
return f"Syntax error in code. Error on line {exc.lineno}: {exc.text.strip()}"
|
||||
|
||||
|
||||
def get_causing_exception(exc: BaseException) -> BaseException:
|
||||
"""Get the causing exception from an exception."""
|
||||
if hasattr(exc, "__cause__") and exc.__cause__:
|
||||
return get_causing_exception(exc.__cause__)
|
||||
return exc
|
||||
|
||||
|
||||
def format_exception_message(exc: Exception) -> str:
|
||||
"""Format an exception message for returning to the frontend."""
|
||||
# We need to check if the __cause__ is a SyntaxError
|
||||
# If it is, we need to return the message of the SyntaxError
|
||||
causing_exception = get_causing_exception(exc)
|
||||
if isinstance(causing_exception, SyntaxError):
|
||||
return format_syntax_error_message(causing_exception)
|
||||
return str(exc)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ from langflow.api.v1.api_key import router as api_key_router
|
|||
from langflow.api.v1.chat import router as chat_router
|
||||
from langflow.api.v1.credential import router as credentials_router
|
||||
from langflow.api.v1.endpoints import router as endpoints_router
|
||||
from langflow.api.v1.files import router as files_router
|
||||
from langflow.api.v1.flows import router as flows_router
|
||||
from langflow.api.v1.login import router as login_router
|
||||
from langflow.api.v1.monitor import router as monitor_router
|
||||
from langflow.api.v1.store import router as store_router
|
||||
from langflow.api.v1.users import router as users_router
|
||||
from langflow.api.v1.validate import router as validate_router
|
||||
|
|
@ -18,4 +20,6 @@ __all__ = [
|
|||
"api_key_router",
|
||||
"login_router",
|
||||
"credentials_router",
|
||||
"monitor_router",
|
||||
"files_router",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.prompts import PromptTemplate
|
||||
from pydantic import BaseModel, field_validator, model_serializer
|
||||
|
||||
from langflow.interface.utils import extract_input_variables_from_prompt
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
|
||||
|
||||
|
|
@ -28,7 +26,7 @@ class FrontendNodeRequest(FrontendNode):
|
|||
class ValidatePromptRequest(BaseModel):
|
||||
name: str
|
||||
template: str
|
||||
# optional for tweak call
|
||||
custom_fields: Optional[dict] = None
|
||||
frontend_node: Optional[FrontendNodeRequest] = None
|
||||
|
||||
|
||||
|
|
@ -68,8 +66,6 @@ INVALID_CHARACTERS = {
|
|||
")",
|
||||
"[",
|
||||
"]",
|
||||
"{",
|
||||
"}",
|
||||
}
|
||||
|
||||
INVALID_NAMES = {
|
||||
|
|
@ -82,79 +78,88 @@ INVALID_NAMES = {
|
|||
}
|
||||
|
||||
|
||||
def validate_prompt(template: str):
|
||||
input_variables = extract_input_variables_from_prompt(template)
|
||||
|
||||
# Check if there are invalid characters in the input_variables
|
||||
input_variables = check_input_variables(input_variables)
|
||||
if any(var in INVALID_NAMES for var in input_variables):
|
||||
raise ValueError(f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. ")
|
||||
|
||||
try:
|
||||
PromptTemplate(template=template, input_variables=input_variables)
|
||||
except Exception as exc:
|
||||
raise ValueError(str(exc)) from exc
|
||||
|
||||
return input_variables
|
||||
def is_json_like(var):
|
||||
if var.startswith("{{") and var.endswith("}}"):
|
||||
# If it is a double brance variable
|
||||
# we don't want to validate any of its content
|
||||
return True
|
||||
# the above doesn't work on all cases because the json string can be multiline
|
||||
# or indented which can add \n or spaces at the start or end of the string
|
||||
# test_case_3 new_var == '\n{{\n "test": "hello",\n "text": "world"\n}}\n'
|
||||
# what we can do is to remove the \n and spaces from the start and end of the string
|
||||
# and then check if the string starts with {{ and ends with }}
|
||||
var = var.strip()
|
||||
var = var.replace("\n", "")
|
||||
var = var.replace(" ", "")
|
||||
# Now it should be a valid json string
|
||||
return var.startswith("{{") and var.endswith("}}")
|
||||
|
||||
|
||||
def check_input_variables(input_variables: list):
|
||||
def fix_variable(var, invalid_chars, wrong_variables):
|
||||
if not var:
|
||||
return var, invalid_chars, wrong_variables
|
||||
new_var = var
|
||||
|
||||
# Handle variables starting with a number
|
||||
if var[0].isdigit():
|
||||
invalid_chars.append(var[0])
|
||||
new_var, invalid_chars, wrong_variables = fix_variable(var[1:], invalid_chars, wrong_variables)
|
||||
|
||||
# Temporarily replace {{ and }} to avoid treating them as invalid
|
||||
new_var = new_var.replace("{{", "ᴛᴇᴍᴘᴏᴘᴇɴ").replace("}}", "ᴛᴇᴍᴘᴄʟᴏsᴇ")
|
||||
|
||||
# Remove invalid characters
|
||||
for char in new_var:
|
||||
if char in INVALID_CHARACTERS:
|
||||
invalid_chars.append(char)
|
||||
new_var = new_var.replace(char, "")
|
||||
if var not in wrong_variables: # Avoid duplicating entries
|
||||
wrong_variables.append(var)
|
||||
|
||||
# Restore {{ and }}
|
||||
new_var = new_var.replace("ᴛᴇᴍᴘᴏᴘᴇɴ", "{{").replace("ᴛᴇᴍᴘᴄʟᴏsᴇ", "}}")
|
||||
|
||||
return new_var, invalid_chars, wrong_variables
|
||||
|
||||
|
||||
def check_variable(var, invalid_chars, wrong_variables, empty_variables):
|
||||
if any(char in invalid_chars for char in var):
|
||||
wrong_variables.append(var)
|
||||
elif var == "":
|
||||
empty_variables.append(var)
|
||||
return wrong_variables, empty_variables
|
||||
|
||||
|
||||
def check_for_errors(input_variables, fixed_variables, wrong_variables, empty_variables):
|
||||
if any(var for var in input_variables if var not in fixed_variables):
|
||||
error_message = (
|
||||
f"Error: Input variables contain invalid characters or formats. \n"
|
||||
f"Invalid variables: {', '.join(wrong_variables)}.\n"
|
||||
f"Empty variables: {', '.join(empty_variables)}. \n"
|
||||
f"Fixed variables: {', '.join(fixed_variables)}."
|
||||
)
|
||||
raise ValueError(error_message)
|
||||
|
||||
|
||||
def check_input_variables(input_variables):
|
||||
invalid_chars = []
|
||||
fixed_variables = []
|
||||
wrong_variables = []
|
||||
empty_variables = []
|
||||
for variable in input_variables:
|
||||
new_var = variable
|
||||
variables_to_check = []
|
||||
|
||||
# if variable is empty, then we should add that to the wrong variables
|
||||
if not variable:
|
||||
empty_variables.append(variable)
|
||||
for var in input_variables:
|
||||
# First, let's check if the variable is a JSON string
|
||||
# because if it is, it won't be considered a variable
|
||||
# and we don't need to validate it
|
||||
if is_json_like(var):
|
||||
continue
|
||||
|
||||
# if variable starts with a number we should add that to the invalid chars
|
||||
# and wrong variables
|
||||
if variable[0].isdigit():
|
||||
invalid_chars.append(variable[0])
|
||||
new_var = new_var.replace(variable[0], "")
|
||||
wrong_variables.append(variable)
|
||||
else:
|
||||
for char in INVALID_CHARACTERS:
|
||||
if char in variable:
|
||||
invalid_chars.append(char)
|
||||
new_var = new_var.replace(char, "")
|
||||
wrong_variables.append(variable)
|
||||
new_var, wrong_variables, empty_variables = fix_variable(var, invalid_chars, wrong_variables)
|
||||
wrong_variables, empty_variables = check_variable(var, INVALID_CHARACTERS, wrong_variables, empty_variables)
|
||||
fixed_variables.append(new_var)
|
||||
# If any of the input_variables is not in the fixed_variables, then it means that
|
||||
# there are invalid characters in the input_variables
|
||||
variables_to_check.append(var)
|
||||
|
||||
if any(var not in fixed_variables for var in input_variables):
|
||||
error_message = build_error_message(
|
||||
input_variables,
|
||||
invalid_chars,
|
||||
wrong_variables,
|
||||
fixed_variables,
|
||||
empty_variables,
|
||||
)
|
||||
raise ValueError(error_message)
|
||||
return input_variables
|
||||
check_for_errors(variables_to_check, fixed_variables, wrong_variables, empty_variables)
|
||||
|
||||
|
||||
def build_error_message(input_variables, invalid_chars, wrong_variables, fixed_variables, empty_variables):
|
||||
input_variables_str = ", ".join([f"'{var}'" for var in input_variables])
|
||||
error_string = f"Invalid input variables: {input_variables_str}. "
|
||||
|
||||
if wrong_variables and invalid_chars:
|
||||
# fix the wrong variables replacing invalid chars and find them in the fixed variables
|
||||
error_string_vars = "You can fix them by replacing the invalid characters: "
|
||||
wvars = wrong_variables.copy()
|
||||
for i, wrong_var in enumerate(wvars):
|
||||
for char in invalid_chars:
|
||||
wrong_var = wrong_var.replace(char, "")
|
||||
if wrong_var in fixed_variables:
|
||||
error_string_vars += f"'{wrong_variables[i]}' -> '{wrong_var}'"
|
||||
error_string += error_string_vars
|
||||
elif empty_variables:
|
||||
error_string += f" There are {len(empty_variables)} empty variable{'s' if len(empty_variables) > 1 else ''}."
|
||||
elif len(set(fixed_variables)) != len(fixed_variables):
|
||||
error_string += "There are duplicate variables."
|
||||
return error_string
|
||||
return fixed_variables
|
||||
|
|
|
|||
|
|
@ -1,28 +1,37 @@
|
|||
import asyncio
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from langchain.callbacks.base import AsyncCallbackHandler, BaseCallbackHandler
|
||||
from langchain.schema import AgentAction, AgentFinish
|
||||
from langchain_core.callbacks.base import AsyncCallbackHandler
|
||||
from loguru import logger
|
||||
|
||||
from langflow.api.v1.schemas import ChatResponse, PromptResponse
|
||||
from langflow.services.deps import get_chat_service
|
||||
from langflow.services.deps import get_chat_service, get_socket_service
|
||||
from langflow.utils.util import remove_ansi_escape_codes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.socket.service import SocketIOService
|
||||
|
||||
|
||||
# https://github.com/hwchase17/chat-langchain/blob/master/callback.py
|
||||
class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
||||
class AsyncStreamingLLMCallbackHandleSIO(AsyncCallbackHandler):
|
||||
"""Callback handler for streaming LLM responses."""
|
||||
|
||||
def __init__(self, client_id: str):
|
||||
@property
|
||||
def ignore_chain(self) -> bool:
|
||||
"""Whether to ignore chain callbacks."""
|
||||
return False
|
||||
|
||||
def __init__(self, session_id: str):
|
||||
self.chat_service = get_chat_service()
|
||||
self.client_id = client_id
|
||||
self.websocket = self.chat_service.active_connections[self.client_id]
|
||||
self.client_id = session_id
|
||||
self.socketio_service: "SocketIOService" = get_socket_service()
|
||||
self.sid = session_id
|
||||
# self.socketio_service = self.chat_service.active_connections[self.client_id]
|
||||
|
||||
async def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
|
||||
resp = ChatResponse(message=token, type="stream", intermediate_steps="")
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
|
||||
async def on_tool_start(self, serialized: Dict[str, Any], input_str: str, **kwargs: Any) -> Any:
|
||||
"""Run when tool starts running."""
|
||||
|
|
@ -31,7 +40,7 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
type="stream",
|
||||
intermediate_steps=f"Tool input: {input_str}",
|
||||
)
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
|
||||
async def on_tool_end(self, output: str, **kwargs: Any) -> Any:
|
||||
"""Run when tool ends running."""
|
||||
|
|
@ -62,7 +71,7 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
try:
|
||||
# This is to emulate the stream of tokens
|
||||
for resp in resps:
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
except Exception as exc:
|
||||
logger.error(f"Error sending response: {exc}")
|
||||
|
||||
|
|
@ -88,8 +97,7 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
resp = PromptResponse(
|
||||
prompt=text,
|
||||
)
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
self.chat_service.chat_history.add_message(self.client_id, resp)
|
||||
await self.socketio_service.emit_message(to=self.sid, data=resp.model_dump())
|
||||
|
||||
async def on_agent_action(self, action: AgentAction, **kwargs: Any):
|
||||
log = f"Thought: {action.log}"
|
||||
|
|
@ -99,10 +107,10 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
logs = log.split("\n")
|
||||
for log in logs:
|
||||
resp = ChatResponse(message="", type="stream", intermediate_steps=log)
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
else:
|
||||
resp = ChatResponse(message="", type="stream", intermediate_steps=log)
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
|
||||
async def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> Any:
|
||||
"""Run on agent end."""
|
||||
|
|
@ -111,20 +119,4 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
type="stream",
|
||||
intermediate_steps=finish.log,
|
||||
)
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
|
||||
|
||||
class StreamingLLMCallbackHandler(BaseCallbackHandler):
|
||||
"""Callback handler for streaming LLM responses."""
|
||||
|
||||
def __init__(self, client_id: str):
|
||||
self.chat_service = get_chat_service()
|
||||
self.client_id = client_id
|
||||
self.websocket = self.chat_service.active_connections[self.client_id]
|
||||
|
||||
def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
|
||||
resp = ChatResponse(message=token, type="stream", intermediate_steps="")
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
coroutine = self.websocket.send_json(resp.model_dump())
|
||||
asyncio.run_coroutine_threadsafe(coroutine, loop)
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
|
|
|
|||
|
|
@ -1,223 +1,35 @@
|
|||
import time
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, Annotated, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketException, status
|
||||
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from langflow.api.utils import build_input_keys_response, format_elapsed_time
|
||||
from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, StreamData
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.services.auth.utils import get_current_active_user, get_current_user_for_websocket
|
||||
from langflow.services.cache.service import BaseCacheService
|
||||
from langflow.services.cache.utils import update_build_status
|
||||
from langflow.services.chat.service import ChatService
|
||||
from langflow.services.deps import get_cache_service, get_chat_service, get_session
|
||||
from loguru import logger
|
||||
from sqlmodel import Session
|
||||
|
||||
from langflow.api.utils import (
|
||||
build_and_cache_graph,
|
||||
format_elapsed_time,
|
||||
format_exception_message,
|
||||
)
|
||||
from langflow.api.v1.schemas import (
|
||||
InputValueRequest,
|
||||
ResultDataResponse,
|
||||
StreamData,
|
||||
VertexBuildResponse,
|
||||
VerticesOrderResponse,
|
||||
)
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.chat.service import ChatService
|
||||
from langflow.services.deps import get_chat_service, get_session, get_session_service
|
||||
from langflow.services.monitor.utils import log_vertex_build
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.vertex.types import ChatVertex
|
||||
from langflow.services.session.service import SessionService
|
||||
|
||||
router = APIRouter(tags=["Chat"])
|
||||
|
||||
|
||||
@router.websocket("/chat/{client_id}")
|
||||
async def chat(
|
||||
client_id: str,
|
||||
websocket: WebSocket,
|
||||
db: Session = Depends(get_session),
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
):
|
||||
"""Websocket endpoint for chat."""
|
||||
try:
|
||||
user = await get_current_user_for_websocket(websocket, db)
|
||||
await websocket.accept()
|
||||
if not user:
|
||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized")
|
||||
elif not user.is_active:
|
||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized")
|
||||
|
||||
if client_id in chat_service.cache_service:
|
||||
await chat_service.handle_websocket(client_id, websocket)
|
||||
else:
|
||||
# We accept the connection but close it immediately
|
||||
# if the flow is not built yet
|
||||
message = "Please, build the flow before sending messages"
|
||||
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=message)
|
||||
except WebSocketException as exc:
|
||||
logger.error(f"Websocket exrror: {exc}")
|
||||
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc))
|
||||
except Exception as exc:
|
||||
logger.error(f"Error in chat websocket: {exc}")
|
||||
messsage = exc.detail if isinstance(exc, HTTPException) else str(exc)
|
||||
if "Could not validate credentials" in str(exc):
|
||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized")
|
||||
else:
|
||||
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=messsage)
|
||||
|
||||
|
||||
@router.post("/build/init/{flow_id}", response_model=InitResponse, status_code=201)
|
||||
async def init_build(
|
||||
graph_data: dict,
|
||||
flow_id: str,
|
||||
current_user=Depends(get_current_active_user),
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
cache_service: "BaseCacheService" = Depends(get_cache_service),
|
||||
):
|
||||
"""Initialize the build by storing graph data and returning a unique session ID."""
|
||||
try:
|
||||
if flow_id is None:
|
||||
raise ValueError("No ID provided")
|
||||
# Check if already building
|
||||
if (
|
||||
flow_id in cache_service
|
||||
and isinstance(cache_service[flow_id], dict)
|
||||
and cache_service[flow_id].get("status") == BuildStatus.IN_PROGRESS
|
||||
):
|
||||
return InitResponse(flowId=flow_id)
|
||||
|
||||
# Delete from cache if already exists
|
||||
if flow_id in chat_service.cache_service:
|
||||
chat_service.cache_service.delete(flow_id)
|
||||
logger.debug(f"Deleted flow {flow_id} from cache")
|
||||
cache_service[flow_id] = {
|
||||
"graph_data": graph_data,
|
||||
"status": BuildStatus.STARTED,
|
||||
"user_id": current_user.id,
|
||||
}
|
||||
|
||||
return InitResponse(flowId=flow_id)
|
||||
except Exception as exc:
|
||||
logger.error(f"Error initializing build: {exc}")
|
||||
return HTTPException(status_code=500, detail=str(exc))
|
||||
|
||||
|
||||
@router.get("/build/{flow_id}/status", response_model=BuiltResponse)
|
||||
async def build_status(flow_id: str, cache_service: "BaseCacheService" = Depends(get_cache_service)):
|
||||
"""Check the flow_id is in the cache_service."""
|
||||
try:
|
||||
built = flow_id in cache_service and cache_service[flow_id]["status"] == BuildStatus.SUCCESS
|
||||
|
||||
return BuiltResponse(
|
||||
built=built,
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Error checking build status: {exc}")
|
||||
return HTTPException(status_code=500, detail=str(exc))
|
||||
|
||||
|
||||
@router.get("/build/stream/{flow_id}", response_class=StreamingResponse)
|
||||
async def stream_build(
|
||||
flow_id: str,
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
cache_service: "BaseCacheService" = Depends(get_cache_service),
|
||||
):
|
||||
"""Stream the build process based on stored flow data."""
|
||||
|
||||
async def event_stream(flow_id):
|
||||
final_response = {"end_of_stream": True}
|
||||
artifacts = {}
|
||||
flow_cache = cache_service[flow_id]
|
||||
flow_cache = flow_cache if isinstance(flow_cache, dict) else {}
|
||||
try:
|
||||
if flow_id not in cache_service:
|
||||
error_message = "Invalid session ID"
|
||||
yield str(StreamData(event="error", data={"error": error_message}))
|
||||
return
|
||||
|
||||
if flow_cache.get("status") == BuildStatus.IN_PROGRESS:
|
||||
error_message = "Already building"
|
||||
yield str(StreamData(event="error", data={"error": error_message}))
|
||||
return
|
||||
|
||||
graph_data = flow_cache.get("graph_data")
|
||||
|
||||
if not graph_data:
|
||||
error_message = "No data provided"
|
||||
yield str(StreamData(event="error", data={"error": error_message}))
|
||||
return
|
||||
|
||||
logger.debug("Building langchain object")
|
||||
|
||||
# Some error could happen when building the graph
|
||||
graph = Graph.from_payload(graph_data)
|
||||
|
||||
number_of_nodes = len(graph.vertices)
|
||||
update_build_status(cache_service, flow_id, BuildStatus.IN_PROGRESS)
|
||||
time_elapsed = ""
|
||||
try:
|
||||
user_id = flow_cache["user_id"]
|
||||
except KeyError:
|
||||
logger.debug("No user_id found in cache_service")
|
||||
user_id = None
|
||||
for i, vertex in enumerate(graph.generator_build(), 1):
|
||||
start_time = time.perf_counter()
|
||||
try:
|
||||
log_dict = {
|
||||
"log": f"Building node {vertex.vertex_type}",
|
||||
}
|
||||
yield str(StreamData(event="log", data=log_dict))
|
||||
if vertex.is_task:
|
||||
vertex = await try_running_celery_task(vertex, user_id)
|
||||
else:
|
||||
await vertex.build(user_id=user_id)
|
||||
time_elapsed = format_elapsed_time(time.perf_counter() - start_time)
|
||||
params = vertex._built_object_repr()
|
||||
valid = True
|
||||
|
||||
logger.debug(f"Building node {str(vertex.vertex_type)}")
|
||||
logger.debug(f"Output: {params[:100]}{'...' if len(params) > 100 else ''}")
|
||||
if vertex.artifacts:
|
||||
# The artifacts will be prompt variables
|
||||
# passed to build_input_keys_response
|
||||
# to set the input_keys values
|
||||
artifacts.update(vertex.artifacts)
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
params = str(exc)
|
||||
valid = False
|
||||
time_elapsed = format_elapsed_time(time.perf_counter() - start_time)
|
||||
update_build_status(cache_service, flow_id, BuildStatus.FAILURE)
|
||||
|
||||
vertex_id = vertex.parent_node_id if vertex.parent_is_top_level else vertex.id
|
||||
if vertex_id in graph.top_level_vertices:
|
||||
response = {
|
||||
"valid": valid,
|
||||
"params": params,
|
||||
"id": vertex_id,
|
||||
"progress": round(i / number_of_nodes, 2),
|
||||
"duration": time_elapsed,
|
||||
}
|
||||
|
||||
yield str(StreamData(event="message", data=response))
|
||||
|
||||
langchain_object = await graph.build()
|
||||
# Now we need to check the input_keys to send them to the client
|
||||
if hasattr(langchain_object, "input_keys"):
|
||||
input_keys_response = build_input_keys_response(langchain_object, artifacts)
|
||||
else:
|
||||
input_keys_response = {
|
||||
"input_keys": None,
|
||||
"memory_keys": [],
|
||||
"handle_keys": [],
|
||||
}
|
||||
yield str(StreamData(event="message", data=input_keys_response))
|
||||
chat_service.set_cache(flow_id, langchain_object)
|
||||
# We need to reset the chat history
|
||||
chat_service.chat_history.empty_history(flow_id)
|
||||
update_build_status(cache_service, flow_id, BuildStatus.SUCCESS)
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
logger.error("Error while building the flow: %s", exc)
|
||||
|
||||
update_build_status(cache_service, flow_id, BuildStatus.FAILURE)
|
||||
yield str(StreamData(event="error", data={"error": str(exc)}))
|
||||
finally:
|
||||
yield str(StreamData(event="message", data=final_response))
|
||||
|
||||
try:
|
||||
return StreamingResponse(event_stream(flow_id), media_type="text/event-stream")
|
||||
except Exception as exc:
|
||||
logger.error(f"Error streaming build: {exc}")
|
||||
raise HTTPException(status_code=500, detail=str(exc))
|
||||
|
||||
|
||||
async def try_running_celery_task(vertex, user_id):
|
||||
# Try running the task in celery
|
||||
# and set the task_id to the local vertex
|
||||
|
|
@ -232,3 +44,219 @@ async def try_running_celery_task(vertex, user_id):
|
|||
vertex.task_id = None
|
||||
await vertex.build(user_id=user_id)
|
||||
return vertex
|
||||
|
||||
|
||||
@router.get("/build/{flow_id}/vertices", response_model=VerticesOrderResponse)
|
||||
async def get_vertices(
|
||||
flow_id: str,
|
||||
stop_component_id: Optional[str] = None,
|
||||
start_component_id: Optional[str] = None,
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
session=Depends(get_session),
|
||||
):
|
||||
"""Check the flow_id is in the flow_data_store."""
|
||||
try:
|
||||
# First, we need to check if the flow_id is in the cache
|
||||
graph = None
|
||||
if cache := await chat_service.get_cache(flow_id):
|
||||
graph = cache.get("result")
|
||||
graph = await build_and_cache_graph(flow_id, session, chat_service, graph)
|
||||
if stop_component_id or start_component_id:
|
||||
try:
|
||||
vertices = graph.sort_vertices(stop_component_id, start_component_id)
|
||||
except Exception as exc:
|
||||
logger.error(exc)
|
||||
vertices = graph.sort_vertices()
|
||||
else:
|
||||
vertices = graph.sort_vertices()
|
||||
|
||||
# Now vertices is a list of lists
|
||||
# We need to get the id of each vertex
|
||||
# and return the same structure but only with the ids
|
||||
run_id = uuid.uuid4()
|
||||
graph.set_run_id(run_id)
|
||||
return VerticesOrderResponse(ids=vertices, run_id=run_id)
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Error checking build status: {exc}")
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/build/{flow_id}/vertices/{vertex_id}")
|
||||
async def build_vertex(
|
||||
flow_id: str,
|
||||
vertex_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
inputs: Annotated[Optional[InputValueRequest], Body(embed=True)] = None,
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
current_user=Depends(get_current_active_user),
|
||||
):
|
||||
"""Build a vertex instead of the entire graph."""
|
||||
|
||||
start_time = time.perf_counter()
|
||||
next_vertices_ids = []
|
||||
try:
|
||||
start_time = time.perf_counter()
|
||||
cache = await chat_service.get_cache(flow_id)
|
||||
if not cache:
|
||||
# If there's no cache
|
||||
logger.warning(f"No cache found for {flow_id}. Building graph starting at {vertex_id}")
|
||||
graph = await build_and_cache_graph(flow_id=flow_id, session=next(get_session()), chat_service=chat_service)
|
||||
else:
|
||||
graph = cache.get("result")
|
||||
result_data_response = ResultDataResponse(results={})
|
||||
duration = ""
|
||||
|
||||
vertex = graph.get_vertex(vertex_id)
|
||||
try:
|
||||
if not vertex.frozen or not vertex._built:
|
||||
inputs_dict = inputs.model_dump() if inputs else {}
|
||||
await vertex.build(user_id=current_user.id, inputs=inputs_dict)
|
||||
|
||||
if vertex.result is not None:
|
||||
params = vertex._built_object_repr()
|
||||
valid = True
|
||||
result_dict = vertex.result
|
||||
artifacts = vertex.artifacts
|
||||
else:
|
||||
raise ValueError(f"No result found for vertex {vertex_id}")
|
||||
async with chat_service._cache_locks[flow_id] as lock:
|
||||
graph.remove_from_predecessors(vertex_id)
|
||||
next_vertices_ids = vertex.successors_ids
|
||||
next_vertices_ids = [v for v in next_vertices_ids if graph.should_run_vertex(v)]
|
||||
await chat_service.set_cache(flow_id=flow_id, data=graph, lock=lock)
|
||||
|
||||
result_data_response = ResultDataResponse(**result_dict.model_dump())
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(f"Error building vertex: {exc}")
|
||||
params = format_exception_message(exc)
|
||||
valid = False
|
||||
result_data_response = ResultDataResponse(results={})
|
||||
artifacts = {}
|
||||
# If there's an error building the vertex
|
||||
# we need to clear the cache
|
||||
await chat_service.clear_cache(flow_id)
|
||||
|
||||
# Log the vertex build
|
||||
if not vertex.will_stream:
|
||||
background_tasks.add_task(
|
||||
log_vertex_build,
|
||||
flow_id=flow_id,
|
||||
vertex_id=vertex_id,
|
||||
valid=valid,
|
||||
params=params,
|
||||
data=result_data_response,
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
timedelta = time.perf_counter() - start_time
|
||||
duration = format_elapsed_time(timedelta)
|
||||
result_data_response.duration = duration
|
||||
result_data_response.timedelta = timedelta
|
||||
vertex.add_build_time(timedelta)
|
||||
inactivated_vertices = None
|
||||
inactivated_vertices = list(graph.inactivated_vertices)
|
||||
graph.reset_inactivated_vertices()
|
||||
graph.reset_activated_vertices()
|
||||
await chat_service.set_cache(flow_id, graph)
|
||||
|
||||
# graph.stop_vertex tells us if the user asked
|
||||
# to stop the build of the graph at a certain vertex
|
||||
# if it is in next_vertices_ids, we need to remove other
|
||||
# vertices from next_vertices_ids
|
||||
if graph.stop_vertex and graph.stop_vertex in next_vertices_ids:
|
||||
next_vertices_ids = [graph.stop_vertex]
|
||||
|
||||
build_response = VertexBuildResponse(
|
||||
inactivated_vertices=inactivated_vertices,
|
||||
next_vertices_ids=next_vertices_ids,
|
||||
valid=valid,
|
||||
params=params,
|
||||
id=vertex.id,
|
||||
data=result_data_response,
|
||||
)
|
||||
return build_response
|
||||
except Exception as exc:
|
||||
logger.error(f"Error building vertex: {exc}")
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
# Now onto an endpoint that is an SSE endpoint
|
||||
# it will receive a component_id and a flow_id
|
||||
#
|
||||
@router.get("/build/{flow_id}/{vertex_id}/stream", response_class=StreamingResponse)
|
||||
async def build_vertex_stream(
|
||||
flow_id: str,
|
||||
vertex_id: str,
|
||||
session_id: Optional[str] = None,
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
session_service: "SessionService" = Depends(get_session_service),
|
||||
):
|
||||
"""Build a vertex instead of the entire graph."""
|
||||
try:
|
||||
|
||||
async def stream_vertex():
|
||||
try:
|
||||
if not session_id:
|
||||
cache = chat_service.get_cache(flow_id)
|
||||
if not cache:
|
||||
# If there's no cache
|
||||
raise ValueError(f"No cache found for {flow_id}.")
|
||||
else:
|
||||
graph = cache.get("result")
|
||||
else:
|
||||
session_data = await session_service.load_session(session_id, flow_id=flow_id)
|
||||
graph, artifacts = session_data if session_data else (None, None)
|
||||
if not graph:
|
||||
raise ValueError(f"No graph found for {flow_id}.")
|
||||
|
||||
vertex: "ChatVertex" = graph.get_vertex(vertex_id)
|
||||
if not hasattr(vertex, "stream"):
|
||||
raise ValueError(f"Vertex {vertex_id} does not support streaming")
|
||||
if isinstance(vertex._built_result, str) and vertex._built_result:
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"message": f"Streaming vertex {vertex_id}"},
|
||||
)
|
||||
yield str(stream_data)
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"chunk": vertex._built_result},
|
||||
)
|
||||
yield str(stream_data)
|
||||
|
||||
elif not vertex.frozen or not vertex._built:
|
||||
logger.debug(f"Streaming vertex {vertex_id}")
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"message": f"Streaming vertex {vertex_id}"},
|
||||
)
|
||||
yield str(stream_data)
|
||||
async for chunk in vertex.stream():
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"chunk": chunk},
|
||||
)
|
||||
yield str(stream_data)
|
||||
elif vertex.result is not None:
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"chunk": vertex._built_result},
|
||||
)
|
||||
yield str(stream_data)
|
||||
else:
|
||||
raise ValueError(f"No result found for vertex {vertex_id}")
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Error building vertex: {exc}")
|
||||
yield str(StreamData(event="error", data={"error": str(exc)}))
|
||||
finally:
|
||||
logger.debug("Closing stream")
|
||||
yield str(StreamData(event="close", data={"message": "Stream closed"}))
|
||||
|
||||
return StreamingResponse(stream_vertex(), media_type="text/event-stream")
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail="Error building vertex") from exc
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ from datetime import datetime
|
|||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from langflow.services.auth import utils as auth_utils
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.credential import (
|
||||
|
|
@ -12,7 +14,6 @@ from langflow.services.database.models.credential import (
|
|||
)
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
from sqlmodel import Session, select
|
||||
|
||||
router = APIRouter(prefix="/credentials", tags=["Credentials"])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,114 +1,39 @@
|
|||
from http import HTTPStatus
|
||||
from typing import Annotated, Any, List, Optional, Union
|
||||
from typing import Annotated, List, Optional, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, UploadFile, status
|
||||
from loguru import logger
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from langflow.api.utils import update_frontend_node_with_template_values
|
||||
from langflow.api.v1.schemas import (
|
||||
CustomComponentCode,
|
||||
PreloadResponse,
|
||||
CustomComponentRequest,
|
||||
InputValueRequest,
|
||||
ProcessResponse,
|
||||
TaskResponse,
|
||||
RunResponse,
|
||||
TaskStatusResponse,
|
||||
Tweaks,
|
||||
UpdateCustomComponentRequest,
|
||||
UploadFileResponse,
|
||||
)
|
||||
from langflow.graph.schema import RunOutputs
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.interface.custom.directory_reader import DirectoryReader
|
||||
from langflow.interface.custom.utils import build_custom_component_template
|
||||
from langflow.processing.process import build_graph_and_generate_result, process_graph_cached, process_tweaks
|
||||
from langflow.processing.process import process_tweaks, run_graph
|
||||
from langflow.services.auth.utils import api_key_security, get_current_active_user
|
||||
from langflow.services.cache.utils import save_uploaded_file
|
||||
from langflow.services.database.models.flow import Flow
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_session, get_session_service, get_settings_service, get_task_service
|
||||
from langflow.services.session.service import SessionService
|
||||
from loguru import logger
|
||||
from sqlmodel import select
|
||||
|
||||
try:
|
||||
from langflow.worker import process_graph_cached_task
|
||||
except ImportError:
|
||||
|
||||
def process_graph_cached_task(*args, **kwargs):
|
||||
raise NotImplementedError("Celery is not installed")
|
||||
|
||||
|
||||
from langflow.services.task.service import TaskService
|
||||
from sqlmodel import Session
|
||||
|
||||
# build router
|
||||
router = APIRouter(tags=["Base"])
|
||||
|
||||
|
||||
async def process_graph_data(
|
||||
graph_data: dict,
|
||||
inputs: Optional[Union[List[dict], dict]] = None,
|
||||
tweaks: Optional[dict] = None,
|
||||
clear_cache: bool = False,
|
||||
session_id: Optional[str] = None,
|
||||
task_service: "TaskService" = Depends(get_task_service),
|
||||
sync: bool = True,
|
||||
):
|
||||
task_result: Any = None
|
||||
task_status = None
|
||||
if tweaks:
|
||||
try:
|
||||
graph_data = process_tweaks(graph_data, tweaks)
|
||||
except Exception as exc:
|
||||
logger.error(f"Error processing tweaks: {exc}")
|
||||
if sync:
|
||||
result = await process_graph_cached(
|
||||
graph_data,
|
||||
inputs,
|
||||
clear_cache,
|
||||
session_id,
|
||||
)
|
||||
task_id = str(id(result))
|
||||
if isinstance(result, dict) and "result" in result:
|
||||
task_result = result["result"]
|
||||
session_id = result["session_id"]
|
||||
elif hasattr(result, "result") and hasattr(result, "session_id"):
|
||||
task_result = result.result
|
||||
|
||||
session_id = result.session_id
|
||||
else:
|
||||
task_result = result
|
||||
else:
|
||||
logger.warning(
|
||||
"This is an experimental feature and may not work as expected."
|
||||
"Please report any issues to our GitHub repository."
|
||||
)
|
||||
if session_id is None:
|
||||
# Generate a session ID
|
||||
session_id = get_session_service().generate_key(session_id=session_id, data_graph=graph_data)
|
||||
task_id, task = await task_service.launch_task(
|
||||
process_graph_cached_task if task_service.use_celery else process_graph_cached,
|
||||
graph_data,
|
||||
inputs,
|
||||
clear_cache,
|
||||
session_id,
|
||||
)
|
||||
task_status = task.status
|
||||
if task.status == "FAILURE":
|
||||
logger.error(f"Task {task_id} failed: {task.traceback}")
|
||||
task_result = str(task._exception)
|
||||
else:
|
||||
task_result = task.result
|
||||
|
||||
if task_id:
|
||||
task_response = TaskResponse(id=task_id, href=f"api/v1/task/{task_id}")
|
||||
else:
|
||||
task_response = None
|
||||
|
||||
return ProcessResponse(
|
||||
result=task_result,
|
||||
status=task_status,
|
||||
task=task_response,
|
||||
session_id=session_id,
|
||||
backend=task_service.backend_name,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/all", dependencies=[Depends(get_current_active_user)])
|
||||
def get_all(
|
||||
settings_service=Depends(get_settings_service),
|
||||
|
|
@ -117,65 +42,86 @@ def get_all(
|
|||
|
||||
logger.debug("Building langchain types dict")
|
||||
try:
|
||||
return get_all_types_dict(settings_service)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/process/json", response_model=ProcessResponse)
|
||||
async def process_json(
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
data: dict,
|
||||
inputs: Optional[dict] = None,
|
||||
tweaks: Optional[dict] = None,
|
||||
clear_cache: Annotated[bool, Body(embed=True)] = False, # noqa: F821
|
||||
session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
|
||||
task_service: "TaskService" = Depends(get_task_service),
|
||||
sync: Annotated[bool, Body(embed=True)] = True, # noqa: F821
|
||||
):
|
||||
try:
|
||||
return await process_graph_data(
|
||||
graph_data=data,
|
||||
inputs=inputs,
|
||||
tweaks=tweaks,
|
||||
clear_cache=clear_cache,
|
||||
session_id=session_id,
|
||||
task_service=task_service,
|
||||
sync=sync,
|
||||
)
|
||||
all_types_dict = get_all_types_dict(settings_service.settings.COMPONENTS_PATH)
|
||||
return all_types_dict
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
# Endpoint to preload a graph
|
||||
@router.post("/process/preload/{flow_id}", response_model=PreloadResponse)
|
||||
async def preload_flow(
|
||||
@router.post("/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True)
|
||||
async def run_flow_with_caching(
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
flow_id: str,
|
||||
session_id: Optional[str] = None,
|
||||
session_service: SessionService = Depends(get_session_service),
|
||||
inputs: Optional[List[InputValueRequest]] = [],
|
||||
outputs: Optional[List[str]] = [],
|
||||
tweaks: Annotated[Optional[Tweaks], Body(embed=True)] = None, # noqa: F821
|
||||
stream: Annotated[bool, Body(embed=True)] = False, # noqa: F821
|
||||
session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
|
||||
api_key_user: User = Depends(api_key_security),
|
||||
clear_session: Annotated[bool, Body(embed=True)] = False, # noqa: F821
|
||||
session_service: SessionService = Depends(get_session_service),
|
||||
):
|
||||
"""
|
||||
Executes a specified flow by ID with optional input values, output selection, tweaks, and streaming capability.
|
||||
This endpoint supports running flows with caching to enhance performance and efficiency.
|
||||
|
||||
### Parameters:
|
||||
- `flow_id` (str): The unique identifier of the flow to be executed.
|
||||
- `inputs` (List[InputValueRequest], optional): A list of inputs specifying the input values and components for the flow. Each input can target specific components and provide custom values.
|
||||
- `outputs` (List[str], optional): A list of output names to retrieve from the executed flow. If not provided, all outputs are returned.
|
||||
- `tweaks` (Optional[Tweaks], optional): A dictionary of tweaks to customize the flow execution. The tweaks can be used to modify the flow's parameters and components. Tweaks can be overridden by the input values.
|
||||
- `stream` (bool, optional): Specifies whether the results should be streamed. Defaults to False.
|
||||
- `session_id` (Union[None, str], optional): An optional session ID to utilize existing session data for the flow execution.
|
||||
- `api_key_user` (User): The user associated with the current API key. Automatically resolved from the API key.
|
||||
- `session_service` (SessionService): The session service object for managing flow sessions.
|
||||
|
||||
### Returns:
|
||||
A `RunResponse` object containing the selected outputs (or all if not specified) of the executed flow and the session ID. The structure of the response accommodates multiple inputs, providing a nested list of outputs for each input.
|
||||
|
||||
### Raises:
|
||||
HTTPException: Indicates issues with finding the specified flow, invalid input formats, or internal errors during flow execution.
|
||||
|
||||
### Example usage:
|
||||
```json
|
||||
POST /run/{flow_id}
|
||||
Payload:
|
||||
{
|
||||
"inputs": [
|
||||
{"components": ["component1"], "input_value": "value1"},
|
||||
{"components": ["component3"], "input_value": "value2"}
|
||||
],
|
||||
"outputs": ["Component Name", "component_id"],
|
||||
"tweaks": {"parameter_name": "value", "Component Name": {"parameter_name": "value"}, "component_id": {"parameter_name": "value"}}
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
This endpoint facilitates complex flow executions with customized inputs, outputs, and configurations, catering to diverse application requirements.
|
||||
"""
|
||||
try:
|
||||
# Get the flow that matches the flow_id and belongs to the user
|
||||
# flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first()
|
||||
if clear_session:
|
||||
session_service.clear_session(session_id)
|
||||
# Check if the session exists
|
||||
session_data = await session_service.load_session(session_id)
|
||||
# Session data is a tuple of (graph, artifacts)
|
||||
# or (None, None) if the session is empty
|
||||
if isinstance(session_data, tuple):
|
||||
graph, artifacts = session_data
|
||||
is_clear = graph is None and artifacts is None
|
||||
else:
|
||||
is_clear = session_data is None
|
||||
return PreloadResponse(session_id=session_id, is_clear=is_clear)
|
||||
if outputs is None:
|
||||
outputs = []
|
||||
|
||||
if session_id:
|
||||
session_data = await session_service.load_session(session_id, flow_id=flow_id)
|
||||
graph, artifacts = session_data if session_data else (None, None)
|
||||
task_result: List[RunOutputs] = []
|
||||
if not graph:
|
||||
raise ValueError("Graph not found in the session")
|
||||
task_result, session_id = await run_graph(
|
||||
graph=graph,
|
||||
flow_id=flow_id,
|
||||
session_id=session_id,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
artifacts=artifacts,
|
||||
session_service=session_service,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
else:
|
||||
if session_id is None:
|
||||
session_id = flow_id
|
||||
# Get the flow that matches the flow_id and belongs to the user
|
||||
# flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first()
|
||||
flow = session.exec(select(Flow).where(Flow.id == flow_id).where(Flow.user_id == api_key_user.id)).first()
|
||||
if flow is None:
|
||||
raise ValueError(f"Flow {flow_id} not found")
|
||||
|
|
@ -183,18 +129,29 @@ async def preload_flow(
|
|||
if flow.data is None:
|
||||
raise ValueError(f"Flow {flow_id} has no data")
|
||||
graph_data = flow.data
|
||||
session_service.clear_session(session_id)
|
||||
# Load the graph using SessionService
|
||||
session_data = await session_service.load_session(session_id, graph_data)
|
||||
graph, artifacts = session_data if session_data else (None, None)
|
||||
if not graph:
|
||||
raise ValueError("Graph not found in the session")
|
||||
_ = await graph.build()
|
||||
session_service.update_session(session_id, (graph, artifacts))
|
||||
return PreloadResponse(session_id=session_id)
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
graph_data = process_tweaks(graph_data, tweaks or {})
|
||||
task_result, session_id = await run_graph(
|
||||
graph=graph_data,
|
||||
flow_id=flow_id,
|
||||
session_id=session_id,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
artifacts={},
|
||||
session_service=session_service,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
return RunResponse(outputs=task_result, session_id=session_id)
|
||||
except sa.exc.StatementError as exc:
|
||||
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
|
||||
if "badly formed hexadecimal UUID string" in str(exc):
|
||||
# This means the Flow ID is not a valid UUID which means it can't find the flow
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
if f"Flow {flow_id} not found" in str(exc):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
else:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
|
|
@ -221,84 +178,15 @@ async def process(
|
|||
"""
|
||||
Endpoint to process an input with a given flow_id.
|
||||
"""
|
||||
|
||||
try:
|
||||
if session_id:
|
||||
session_data = await session_service.load_session(session_id)
|
||||
graph, artifacts = session_data if session_data else (None, None)
|
||||
task_result: Any = None
|
||||
task_status = None
|
||||
task_id = None
|
||||
if not graph:
|
||||
raise ValueError("Graph not found in the session")
|
||||
result = await build_graph_and_generate_result(
|
||||
graph=graph,
|
||||
inputs=inputs,
|
||||
artifacts=artifacts,
|
||||
session_id=session_id,
|
||||
session_service=session_service,
|
||||
)
|
||||
task_id = str(id(result))
|
||||
if isinstance(result, dict) and "result" in result:
|
||||
task_result = result["result"]
|
||||
session_id = result["session_id"]
|
||||
elif hasattr(result, "result") and hasattr(result, "session_id"):
|
||||
task_result = result.result
|
||||
|
||||
session_id = result.session_id
|
||||
else:
|
||||
task_result = result
|
||||
if task_id:
|
||||
task_response = TaskResponse(id=task_id, href=f"api/v1/task/{task_id}")
|
||||
else:
|
||||
task_response = None
|
||||
return ProcessResponse(
|
||||
result=task_result,
|
||||
status=task_status,
|
||||
task=task_response,
|
||||
session_id=session_id,
|
||||
backend=task_service.backend_name,
|
||||
)
|
||||
|
||||
else:
|
||||
if api_key_user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid API Key",
|
||||
)
|
||||
|
||||
# Get the flow that matches the flow_id and belongs to the user
|
||||
# flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first()
|
||||
flow = session.exec(select(Flow).where(Flow.id == flow_id).where(Flow.user_id == api_key_user.id)).first()
|
||||
if flow is None:
|
||||
raise ValueError(f"Flow {flow_id} not found")
|
||||
|
||||
if flow.data is None:
|
||||
raise ValueError(f"Flow {flow_id} has no data")
|
||||
graph_data = flow.data
|
||||
return await process_graph_data(
|
||||
graph_data=graph_data,
|
||||
inputs=inputs,
|
||||
tweaks=tweaks,
|
||||
clear_cache=clear_cache,
|
||||
session_id=session_id,
|
||||
task_service=task_service,
|
||||
sync=sync,
|
||||
)
|
||||
except sa.exc.StatementError as exc:
|
||||
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
|
||||
if "badly formed hexadecimal UUID string" in str(exc):
|
||||
# This means the Flow ID is not a valid UUID which means it can't find the flow
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
if f"Flow {flow_id} not found" in str(exc):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
else:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
||||
except Exception as e:
|
||||
# Log stack trace
|
||||
logger.exception(e)
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
# Raise a depreciation warning
|
||||
logger.warning(
|
||||
"The /process endpoint is deprecated and will be removed in a future version. " "Please use /run instead."
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="The /process endpoint is deprecated and will be removed in a future version. "
|
||||
"Please use /run instead.",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/task/{task_id}", response_model=TaskStatusResponse)
|
||||
|
|
@ -331,8 +219,10 @@ async def get_task_status(task_id: str):
|
|||
response_model=UploadFileResponse,
|
||||
status_code=HTTPStatus.CREATED,
|
||||
)
|
||||
async def create_upload_file(file: UploadFile, flow_id: str):
|
||||
# Cache file
|
||||
async def create_upload_file(
|
||||
file: UploadFile,
|
||||
flow_id: str,
|
||||
):
|
||||
try:
|
||||
file_path = save_uploaded_file(file, folder_name=flow_id)
|
||||
|
||||
|
|
@ -355,12 +245,12 @@ def get_version():
|
|||
|
||||
@router.post("/custom_component", status_code=HTTPStatus.OK)
|
||||
async def custom_component(
|
||||
raw_code: CustomComponentCode,
|
||||
raw_code: CustomComponentRequest,
|
||||
user: User = Depends(get_current_active_user),
|
||||
):
|
||||
component = CustomComponent(code=raw_code.code)
|
||||
|
||||
built_frontend_node = build_custom_component_template(component, user_id=user.id)
|
||||
built_frontend_node, _ = build_custom_component_template(component, user_id=user.id)
|
||||
|
||||
built_frontend_node = update_frontend_node_with_template_values(built_frontend_node, raw_code.frontend_node)
|
||||
return built_frontend_node
|
||||
|
|
@ -377,18 +267,46 @@ async def reload_custom_component(path: str, user: User = Depends(get_current_ac
|
|||
raise ValueError(content)
|
||||
|
||||
extractor = CustomComponent(code=content)
|
||||
return build_custom_component_template(extractor, user_id=user.id)
|
||||
frontend_node, _ = build_custom_component_template(extractor, user_id=user.id)
|
||||
return frontend_node
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.post("/custom_component/update", status_code=HTTPStatus.OK)
|
||||
async def custom_component_update(
|
||||
raw_code: CustomComponentCode,
|
||||
code_request: UpdateCustomComponentRequest,
|
||||
user: User = Depends(get_current_active_user),
|
||||
):
|
||||
component = CustomComponent(code=raw_code.code)
|
||||
"""
|
||||
Update a custom component with the provided code request.
|
||||
|
||||
component_node = build_custom_component_template(component, user_id=user.id, update_field=raw_code.field)
|
||||
# Update the field
|
||||
return component_node
|
||||
This endpoint generates the CustomComponentFrontendNode normally but then runs the `update_build_config` method
|
||||
on the latest version of the template. This ensures that every time it runs, it has the latest version of the template.
|
||||
|
||||
Args:
|
||||
code_request (CustomComponentRequest): The code request containing the updated code for the custom component.
|
||||
user (User, optional): The user making the request. Defaults to the current active user.
|
||||
|
||||
Returns:
|
||||
dict: The updated custom component node.
|
||||
|
||||
"""
|
||||
try:
|
||||
component = CustomComponent(code=code_request.code)
|
||||
|
||||
component_node, cc_instance = build_custom_component_template(
|
||||
component,
|
||||
user_id=user.id,
|
||||
)
|
||||
|
||||
updated_build_config = cc_instance.update_build_config(
|
||||
build_config=code_request.get_template(),
|
||||
field_value=code_request.field_value,
|
||||
field_name=code_request.field,
|
||||
)
|
||||
component_node["template"] = updated_build_config
|
||||
|
||||
return component_node
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
|
|
|||
114
src/backend/langflow/api/v1/files.py
Normal file
114
src/backend/langflow/api/v1/files.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import hashlib
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile
|
||||
from fastapi.responses import StreamingResponse
|
||||
from langflow.api.v1.schemas import UploadFileResponse
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.flow import Flow
|
||||
from langflow.services.deps import get_session, get_storage_service
|
||||
from langflow.services.storage.service import StorageService
|
||||
from langflow.services.storage.utils import build_content_type_from_extension
|
||||
|
||||
router = APIRouter(tags=["Files"], prefix="/files")
|
||||
|
||||
|
||||
# Create dep that gets the flow_id from the request
|
||||
# then finds it in the database and returns it while
|
||||
# using the current user as the owner
|
||||
def get_flow_id(
|
||||
flow_id: str,
|
||||
current_user=Depends(get_current_active_user),
|
||||
session=Depends(get_session),
|
||||
):
|
||||
# AttributeError: 'SelectOfScalar' object has no attribute 'first'
|
||||
flow = session.get(Flow, flow_id)
|
||||
if not flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
if flow.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="You don't have access to this flow")
|
||||
return flow_id
|
||||
|
||||
|
||||
@router.post("/upload/{flow_id}", status_code=HTTPStatus.CREATED)
|
||||
async def upload_file(
|
||||
file: UploadFile,
|
||||
flow_id: str = Depends(get_flow_id),
|
||||
storage_service: StorageService = Depends(get_storage_service),
|
||||
):
|
||||
try:
|
||||
file_content = await file.read()
|
||||
file_name = file.filename or hashlib.sha256(file_content).hexdigest()
|
||||
folder = flow_id
|
||||
await storage_service.save_file(flow_id=folder, file_name=file_name, data=file_content)
|
||||
return UploadFileResponse(flowId=flow_id, file_path=f"{folder}/{file_name}")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/download/{flow_id}/{file_name}")
|
||||
async def download_file(file_name: str, flow_id: str, storage_service: StorageService = Depends(get_storage_service)):
|
||||
try:
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
if not extension:
|
||||
raise HTTPException(status_code=500, detail=f"Extension not found for file {file_name}")
|
||||
|
||||
content_type = build_content_type_from_extension(extension)
|
||||
|
||||
if not content_type:
|
||||
raise HTTPException(status_code=500, detail=f"Content type not found for extension {extension}")
|
||||
|
||||
file_content = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
|
||||
headers = {
|
||||
"Content-Disposition": f"attachment; filename={file_name} filename*=UTF-8''{file_name}",
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": str(len(file_content)),
|
||||
}
|
||||
return StreamingResponse(BytesIO(file_content), media_type=content_type, headers=headers)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/images/{flow_id}/{file_name}")
|
||||
async def download_image(file_name: str, flow_id: str, storage_service: StorageService = Depends(get_storage_service)):
|
||||
try:
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
if not extension:
|
||||
raise HTTPException(status_code=500, detail=f"Extension not found for file {file_name}")
|
||||
|
||||
content_type = build_content_type_from_extension(extension)
|
||||
|
||||
if not content_type:
|
||||
raise HTTPException(status_code=500, detail=f"Content type not found for extension {extension}")
|
||||
elif not content_type.startswith("image"):
|
||||
raise HTTPException(status_code=500, detail=f"Content type {content_type} is not an image")
|
||||
|
||||
file_content = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
|
||||
return StreamingResponse(BytesIO(file_content), media_type=content_type)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/list/{flow_id}")
|
||||
async def list_files(
|
||||
flow_id: str = Depends(get_flow_id), storage_service: StorageService = Depends(get_storage_service)
|
||||
):
|
||||
try:
|
||||
files = await storage_service.list_files(flow_id=flow_id)
|
||||
return {"files": files}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/delete/{flow_id}/{file_name}")
|
||||
async def delete_file(
|
||||
file_name: str, flow_id: str = Depends(get_flow_id), storage_service: StorageService = Depends(get_storage_service)
|
||||
):
|
||||
try:
|
||||
await storage_service.delete_file(flow_id=flow_id, file_name=file_name)
|
||||
return {"message": f"File {file_name} deleted successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -5,14 +5,22 @@ from uuid import UUID
|
|||
import orjson
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from loguru import logger
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from langflow.api.utils import remove_api_keys, validate_is_component
|
||||
from langflow.api.v1.schemas import FlowListCreate, FlowListRead
|
||||
from langflow.initial_setup.setup import STARTER_FOLDER_NAME
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.flow import Flow, FlowCreate, FlowRead, FlowUpdate
|
||||
from langflow.services.database.models.flow import (
|
||||
Flow,
|
||||
FlowCreate,
|
||||
FlowRead,
|
||||
FlowUpdate,
|
||||
)
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
from langflow.services.settings.service import SettingsService
|
||||
|
||||
# build router
|
||||
router = APIRouter(prefix="/flows", tags=["Flows"])
|
||||
|
|
@ -42,11 +50,36 @@ def create_flow(
|
|||
def read_flows(
|
||||
*,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: Session = Depends(get_session),
|
||||
settings_service: "SettingsService" = Depends(get_settings_service),
|
||||
):
|
||||
"""Read all flows."""
|
||||
try:
|
||||
flows = current_user.flows
|
||||
flows = validate_is_component(flows)
|
||||
auth_settings = settings_service.auth_settings
|
||||
if auth_settings.AUTO_LOGIN:
|
||||
flows = session.exec(
|
||||
select(Flow).where(
|
||||
(Flow.user_id == None) | (Flow.user_id == current_user.id) # noqa
|
||||
)
|
||||
).all()
|
||||
else:
|
||||
flows = current_user.flows
|
||||
|
||||
flows = validate_is_component(flows) # type: ignore
|
||||
flow_ids = [flow.id for flow in flows]
|
||||
# with the session get the flows that DO NOT have a user_id
|
||||
try:
|
||||
example_flows = session.exec(
|
||||
select(Flow).where(
|
||||
Flow.user_id == None, # noqa
|
||||
Flow.folder == STARTER_FOLDER_NAME,
|
||||
)
|
||||
).all()
|
||||
for example_flow in example_flows:
|
||||
if example_flow.id not in flow_ids:
|
||||
flows.append(example_flow) # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
return [jsonable_encoder(flow) for flow in flows]
|
||||
|
|
@ -58,9 +91,18 @@ def read_flow(
|
|||
session: Session = Depends(get_session),
|
||||
flow_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
settings_service: "SettingsService" = Depends(get_settings_service),
|
||||
):
|
||||
"""Read a flow."""
|
||||
if user_flow := (session.exec(select(Flow).where(Flow.id == flow_id, Flow.user_id == current_user.id)).first()):
|
||||
auth_settings = settings_service.auth_settings
|
||||
stmt = select(Flow).where(Flow.id == flow_id)
|
||||
if auth_settings.AUTO_LOGIN:
|
||||
# If auto login is enable user_id can be current_user.id or None
|
||||
# so write an OR
|
||||
stmt = stmt.where(
|
||||
(Flow.user_id == current_user.id) | (Flow.user_id == None) # noqa
|
||||
) # noqa
|
||||
if user_flow := session.exec(stmt).first():
|
||||
return user_flow
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
|
|
@ -77,7 +119,12 @@ def update_flow(
|
|||
):
|
||||
"""Update a flow."""
|
||||
|
||||
db_flow = read_flow(session=session, flow_id=flow_id, current_user=current_user)
|
||||
db_flow = read_flow(
|
||||
session=session,
|
||||
flow_id=flow_id,
|
||||
current_user=current_user,
|
||||
settings_service=settings_service,
|
||||
)
|
||||
if not db_flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
flow_data = flow.model_dump(exclude_unset=True)
|
||||
|
|
@ -99,9 +146,15 @@ def delete_flow(
|
|||
session: Session = Depends(get_session),
|
||||
flow_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
"""Delete a flow."""
|
||||
flow = read_flow(session=session, flow_id=flow_id, current_user=current_user)
|
||||
flow = read_flow(
|
||||
session=session,
|
||||
flow_id=flow_id,
|
||||
current_user=current_user,
|
||||
settings_service=settings_service,
|
||||
)
|
||||
if not flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
session.delete(flow)
|
||||
|
|
@ -109,9 +162,6 @@ def delete_flow(
|
|||
return {"message": "Flow deleted successfully"}
|
||||
|
||||
|
||||
# Define a new model to handle multiple flows
|
||||
|
||||
|
||||
@router.post("/batch/", response_model=List[FlowRead], status_code=201)
|
||||
def create_flows(
|
||||
*,
|
||||
|
|
@ -157,8 +207,9 @@ async def upload_file(
|
|||
async def download_file(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
settings_service: "SettingsService" = Depends(get_settings_service),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Download all flows as a file."""
|
||||
flows = read_flows(current_user=current_user)
|
||||
flows = read_flows(current_user=current_user, session=session, settings_service=settings_service)
|
||||
return FlowListRead(flows=flows)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ async def login_to_get_access_token(
|
|||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: Session = Depends(get_session),
|
||||
# _: Session = Depends(get_current_active_user)
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
auth_settings = settings_service.auth_settings
|
||||
try:
|
||||
user = authenticate_user(form_data.username, form_data.password, db)
|
||||
except Exception as exc:
|
||||
|
|
@ -33,8 +35,22 @@ async def login_to_get_access_token(
|
|||
|
||||
if user:
|
||||
tokens = create_user_tokens(user_id=user.id, db=db, update_last_login=True)
|
||||
response.set_cookie("refresh_token_lf", tokens["refresh_token"], httponly=True)
|
||||
response.set_cookie("access_token_lf", tokens["access_token"], httponly=False)
|
||||
response.set_cookie(
|
||||
"refresh_token_lf",
|
||||
tokens["refresh_token"],
|
||||
httponly=auth_settings.REFRESH_HTTPONLY,
|
||||
samesite=auth_settings.REFRESH_SAME_SITE,
|
||||
secure=auth_settings.REFRESH_SECURE,
|
||||
expires=auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES * 60,
|
||||
)
|
||||
response.set_cookie(
|
||||
"access_token_lf",
|
||||
tokens["access_token"],
|
||||
httponly=auth_settings.ACCESS_HTTPONLY,
|
||||
samesite=auth_settings.ACCESS_SAME_SITE,
|
||||
secure=auth_settings.ACCESS_SECURE,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
)
|
||||
return tokens
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -46,11 +62,21 @@ async def login_to_get_access_token(
|
|||
|
||||
@router.get("/auto_login")
|
||||
async def auto_login(
|
||||
response: Response, db: Session = Depends(get_session), settings_service=Depends(get_settings_service)
|
||||
response: Response,
|
||||
db: Session = Depends(get_session),
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
auth_settings = settings_service.auth_settings
|
||||
if settings_service.auth_settings.AUTO_LOGIN:
|
||||
tokens = create_user_longterm_token(db)
|
||||
response.set_cookie("access_token_lf", tokens["access_token"], httponly=False)
|
||||
response.set_cookie(
|
||||
"access_token_lf",
|
||||
tokens["access_token"],
|
||||
httponly=auth_settings.ACCESS_HTTPONLY,
|
||||
samesite=auth_settings.ACCESS_SAME_SITE,
|
||||
secure=auth_settings.ACCESS_SECURE,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
)
|
||||
return tokens
|
||||
|
||||
raise HTTPException(
|
||||
|
|
@ -63,12 +89,29 @@ async def auto_login(
|
|||
|
||||
|
||||
@router.post("/refresh")
|
||||
async def refresh_token(request: Request, response: Response):
|
||||
async def refresh_token(request: Request, response: Response, settings_service=Depends(get_settings_service)):
|
||||
auth_settings = settings_service.auth_settings
|
||||
|
||||
token = request.cookies.get("refresh_token_lf")
|
||||
|
||||
if token:
|
||||
tokens = create_refresh_token(token)
|
||||
response.set_cookie("refresh_token_lf", tokens["refresh_token"], httponly=True)
|
||||
response.set_cookie("access_token_lf", tokens["access_token"], httponly=False)
|
||||
response.set_cookie(
|
||||
"refresh_token_lf",
|
||||
tokens["refresh_token"],
|
||||
httponly=auth_settings.REFRESH_HTTPONLY,
|
||||
samesite=auth_settings.REFRESH_SAME_SITE,
|
||||
secure=auth_settings.REFRESH_SECURE,
|
||||
expires=auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES * 60,
|
||||
)
|
||||
response.set_cookie(
|
||||
"access_token_lf",
|
||||
tokens["access_token"],
|
||||
httponly=auth_settings.ACCESS_HTTPONLY,
|
||||
samesite=auth_settings.ACCESS_SAME_SITE,
|
||||
secure=auth_settings.ACCESS_SECURE,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
)
|
||||
return tokens
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
71
src/backend/langflow/api/v1/monitor.py
Normal file
71
src/backend/langflow/api/v1/monitor.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from langflow.services.deps import get_monitor_service
|
||||
from langflow.services.monitor.schema import VertexBuildMapModel
|
||||
from langflow.services.monitor.service import MonitorService
|
||||
|
||||
router = APIRouter(prefix="/monitor", tags=["Monitor"])
|
||||
|
||||
|
||||
# Get vertex_builds data from the monitor service
|
||||
@router.get("/builds", response_model=VertexBuildMapModel)
|
||||
async def get_vertex_builds(
|
||||
flow_id: Optional[str] = Query(None),
|
||||
vertex_id: Optional[str] = Query(None),
|
||||
valid: Optional[bool] = Query(None),
|
||||
order_by: Optional[str] = Query("timestamp"),
|
||||
monitor_service: MonitorService = Depends(get_monitor_service),
|
||||
):
|
||||
try:
|
||||
vertex_build_dicts = monitor_service.get_vertex_builds(
|
||||
flow_id=flow_id, vertex_id=vertex_id, valid=valid, order_by=order_by
|
||||
)
|
||||
vertex_build_map = VertexBuildMapModel.from_list_of_dicts(vertex_build_dicts)
|
||||
return vertex_build_map
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/builds", status_code=204)
|
||||
async def delete_vertex_builds(
|
||||
flow_id: Optional[str] = Query(None),
|
||||
monitor_service: MonitorService = Depends(get_monitor_service),
|
||||
):
|
||||
try:
|
||||
monitor_service.delete_vertex_builds(flow_id=flow_id)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/messages")
|
||||
async def get_messages(
|
||||
session_id: Optional[str] = Query(None),
|
||||
sender: Optional[str] = Query(None),
|
||||
sender_name: Optional[str] = Query(None),
|
||||
order_by: Optional[str] = Query("timestamp"),
|
||||
monitor_service: MonitorService = Depends(get_monitor_service),
|
||||
):
|
||||
try:
|
||||
return monitor_service.get_messages(
|
||||
sender=sender,
|
||||
sender_name=sender_name,
|
||||
session_id=session_id,
|
||||
order_by=order_by,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/transactions")
|
||||
async def get_transactions(
|
||||
source: Optional[str] = Query(None),
|
||||
target: Optional[str] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
order_by: Optional[str] = Query("timestamp"),
|
||||
monitor_service: MonitorService = Depends(get_monitor_service),
|
||||
):
|
||||
try:
|
||||
return monitor_service.get_transactions(source=source, target=target, status=status, order_by=order_by)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -1,13 +1,24 @@
|
|||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
RootModel,
|
||||
field_validator,
|
||||
model_serializer,
|
||||
)
|
||||
|
||||
from langflow.graph.schema import RunOutputs
|
||||
from langflow.schema import dotdict
|
||||
from langflow.services.database.models.api_key.model import ApiKeyRead
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
from langflow.services.database.models.flow import FlowCreate, FlowRead
|
||||
from langflow.services.database.models.user import UserRead
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class BuildStatus(Enum):
|
||||
|
|
@ -19,26 +30,6 @@ class BuildStatus(Enum):
|
|||
IN_PROGRESS = "in_progress"
|
||||
|
||||
|
||||
class GraphData(BaseModel):
|
||||
"""Data inside the exported flow."""
|
||||
|
||||
nodes: List[Dict[str, Any]]
|
||||
edges: List[Dict[str, Any]]
|
||||
|
||||
|
||||
class ExportedFlow(BaseModel):
|
||||
"""Exported flow from Langflow."""
|
||||
|
||||
description: str
|
||||
name: str
|
||||
id: str
|
||||
data: GraphData
|
||||
|
||||
|
||||
class InputRequest(BaseModel):
|
||||
input: dict
|
||||
|
||||
|
||||
class TweaksRequest(BaseModel):
|
||||
tweaks: Optional[Dict[str, Dict[str, str]]] = Field(default_factory=dict)
|
||||
|
||||
|
|
@ -64,6 +55,26 @@ class ProcessResponse(BaseModel):
|
|||
backend: Optional[str] = None
|
||||
|
||||
|
||||
class RunResponse(BaseModel):
|
||||
"""Run response schema."""
|
||||
|
||||
outputs: Optional[List[RunOutputs]] = []
|
||||
session_id: Optional[str] = None
|
||||
|
||||
@model_serializer(mode="wrap")
|
||||
def serialize(self, handler):
|
||||
# Serialize all the outputs if they are base models
|
||||
if self.outputs:
|
||||
serialized_outputs = []
|
||||
for output in self.outputs:
|
||||
if isinstance(output, BaseModel):
|
||||
serialized_outputs.append(output.model_dump(exclude_none=True))
|
||||
else:
|
||||
serialized_outputs.append(output)
|
||||
self.outputs = serialized_outputs
|
||||
return handler(self)
|
||||
|
||||
|
||||
class PreloadResponse(BaseModel):
|
||||
"""Preload response schema."""
|
||||
|
||||
|
|
@ -71,9 +82,6 @@ class PreloadResponse(BaseModel):
|
|||
is_clear: Optional[bool] = None
|
||||
|
||||
|
||||
# TaskStatusResponse(
|
||||
# status=task.status, result=task.result if task.ready() else None
|
||||
# )
|
||||
class TaskStatusResponse(BaseModel):
|
||||
"""Task status response schema."""
|
||||
|
||||
|
|
@ -162,12 +170,21 @@ class StreamData(BaseModel):
|
|||
return f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n"
|
||||
|
||||
|
||||
class CustomComponentCode(BaseModel):
|
||||
class CustomComponentRequest(BaseModel):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
code: str
|
||||
field: Optional[str] = None
|
||||
frontend_node: Optional[dict] = None
|
||||
|
||||
|
||||
class UpdateCustomComponentRequest(CustomComponentRequest):
|
||||
field: str
|
||||
field_value: Optional[Union[str, int, float, bool, dict, list]] = None
|
||||
template: dict
|
||||
|
||||
def get_template(self):
|
||||
return dotdict(self.template)
|
||||
|
||||
|
||||
class CustomComponentResponseError(BaseModel):
|
||||
detail: str
|
||||
traceback: str
|
||||
|
|
@ -212,3 +229,81 @@ class Token(BaseModel):
|
|||
|
||||
class ApiKeyCreateRequest(BaseModel):
|
||||
api_key: str
|
||||
|
||||
|
||||
class VerticesOrderResponse(BaseModel):
|
||||
ids: List[str]
|
||||
run_id: UUID
|
||||
|
||||
|
||||
class ResultDataResponse(BaseModel):
|
||||
results: Optional[Any] = Field(default_factory=dict)
|
||||
artifacts: Optional[Any] = Field(default_factory=dict)
|
||||
timedelta: Optional[float] = None
|
||||
duration: Optional[str] = None
|
||||
|
||||
|
||||
class VertexBuildResponse(BaseModel):
|
||||
id: Optional[str] = None
|
||||
inactivated_vertices: Optional[List[str]] = None
|
||||
next_vertices_ids: Optional[List[str]] = None
|
||||
valid: bool
|
||||
params: Optional[Any] = Field(default_factory=dict)
|
||||
"""JSON string of the params."""
|
||||
data: ResultDataResponse
|
||||
"""Mapping of vertex ids to result dict containing the param name and result value."""
|
||||
timestamp: Optional[datetime] = Field(default_factory=datetime.utcnow)
|
||||
"""Timestamp of the build."""
|
||||
|
||||
|
||||
class VerticesBuiltResponse(BaseModel):
|
||||
vertices: List[VertexBuildResponse]
|
||||
|
||||
|
||||
class InputValueRequest(BaseModel):
|
||||
components: Optional[List[str]] = []
|
||||
input_value: Optional[str] = None
|
||||
|
||||
# add an example
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"examples": [
|
||||
{
|
||||
"components": ["components_id", "Component Name"],
|
||||
"input_value": "input_value",
|
||||
},
|
||||
{"components": ["Component Name"], "input_value": "input_value"},
|
||||
{"input_value": "input_value"},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Tweaks(RootModel):
|
||||
root: dict[str, Union[str, dict[str, str]]] = Field(
|
||||
description="A dictionary of tweaks to adjust the flow's execution. Allows customizing flow behavior dynamically. All tweaks are overridden by the input values.",
|
||||
)
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"examples": [
|
||||
{
|
||||
"parameter_name": "value",
|
||||
"Component Name": {"parameter_name": "value"},
|
||||
"component_id": {"parameter_name": "value"},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# This should behave like a dict
|
||||
def __getitem__(self, key):
|
||||
return self.root[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.root[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.root[key]
|
||||
|
||||
def items(self):
|
||||
return self.root.items()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
from collections import defaultdict
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from langflow.api.v1.base import (
|
||||
Code,
|
||||
CodeValidationResponse,
|
||||
PromptValidationResponse,
|
||||
ValidatePromptRequest,
|
||||
)
|
||||
from langflow.base.prompts.utils import (
|
||||
add_new_variables_to_template,
|
||||
get_old_custom_fields,
|
||||
remove_old_variables_from_template,
|
||||
update_input_variables_field,
|
||||
validate_prompt,
|
||||
)
|
||||
from langflow.template.field.base import TemplateField
|
||||
from langflow.utils.validate import validate_code
|
||||
from loguru import logger
|
||||
|
||||
# build router
|
||||
router = APIRouter(prefix="/validate", tags=["Validate"])
|
||||
|
|
@ -36,13 +44,26 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
|
|||
input_variables=input_variables,
|
||||
frontend_node=None,
|
||||
)
|
||||
old_custom_fields = get_old_custom_fields(prompt_request)
|
||||
if not prompt_request.custom_fields:
|
||||
prompt_request.custom_fields = defaultdict(list)
|
||||
old_custom_fields = get_old_custom_fields(prompt_request.custom_fields, prompt_request.name)
|
||||
|
||||
add_new_variables_to_template(input_variables, prompt_request)
|
||||
add_new_variables_to_template(
|
||||
input_variables,
|
||||
prompt_request.custom_fields,
|
||||
prompt_request.frontend_node.template,
|
||||
prompt_request.name,
|
||||
)
|
||||
|
||||
remove_old_variables_from_template(old_custom_fields, input_variables, prompt_request)
|
||||
remove_old_variables_from_template(
|
||||
old_custom_fields,
|
||||
input_variables,
|
||||
prompt_request.custom_fields,
|
||||
prompt_request.frontend_node.template,
|
||||
prompt_request.name,
|
||||
)
|
||||
|
||||
update_input_variables_field(input_variables, prompt_request)
|
||||
update_input_variables_field(input_variables, prompt_request.frontend_node.template)
|
||||
|
||||
return PromptValidationResponse(
|
||||
input_variables=input_variables,
|
||||
|
|
@ -51,70 +72,3 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
|
|||
except Exception as e:
|
||||
logger.exception(e)
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
def get_old_custom_fields(prompt_request):
|
||||
try:
|
||||
if len(prompt_request.frontend_node.custom_fields) == 1 and prompt_request.name == "":
|
||||
# If there is only one custom field and the name is empty string
|
||||
# then we are dealing with the first prompt request after the node was created
|
||||
prompt_request.name = list(prompt_request.frontend_node.custom_fields.keys())[0]
|
||||
|
||||
old_custom_fields = prompt_request.frontend_node.custom_fields[prompt_request.name]
|
||||
if old_custom_fields is None:
|
||||
old_custom_fields = []
|
||||
|
||||
old_custom_fields = old_custom_fields.copy()
|
||||
except KeyError:
|
||||
old_custom_fields = []
|
||||
prompt_request.frontend_node.custom_fields[prompt_request.name] = []
|
||||
return old_custom_fields
|
||||
|
||||
|
||||
def add_new_variables_to_template(input_variables, prompt_request):
|
||||
for variable in input_variables:
|
||||
try:
|
||||
template_field = TemplateField(
|
||||
name=variable,
|
||||
display_name=variable,
|
||||
field_type="str",
|
||||
show=True,
|
||||
advanced=False,
|
||||
multiline=True,
|
||||
input_types=["Document", "BaseOutputParser"],
|
||||
value="", # Set the value to empty string
|
||||
)
|
||||
if variable in prompt_request.frontend_node.template:
|
||||
# Set the new field with the old value
|
||||
template_field.value = prompt_request.frontend_node.template[variable]["value"]
|
||||
|
||||
prompt_request.frontend_node.template[variable] = template_field.to_dict()
|
||||
|
||||
# Check if variable is not already in the list before appending
|
||||
if variable not in prompt_request.frontend_node.custom_fields[prompt_request.name]:
|
||||
prompt_request.frontend_node.custom_fields[prompt_request.name].append(variable)
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
def remove_old_variables_from_template(old_custom_fields, input_variables, prompt_request):
|
||||
for variable in old_custom_fields:
|
||||
if variable not in input_variables:
|
||||
try:
|
||||
# Remove the variable from custom_fields associated with the given name
|
||||
if variable in prompt_request.frontend_node.custom_fields[prompt_request.name]:
|
||||
prompt_request.frontend_node.custom_fields[prompt_request.name].remove(variable)
|
||||
|
||||
# Remove the variable from the template
|
||||
prompt_request.frontend_node.template.pop(variable, None)
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
def update_input_variables_field(input_variables, prompt_request):
|
||||
if "input_variables" in prompt_request.frontend_node.template:
|
||||
prompt_request.frontend_node.template["input_variables"]["value"] = input_variables
|
||||
|
|
|
|||
144
src/backend/langflow/base/data/utils.py
Normal file
144
src/backend/langflow/base/data/utils.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
from concurrent import futures
|
||||
from pathlib import Path
|
||||
from typing import Callable, List, Optional, Text
|
||||
|
||||
import yaml
|
||||
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
# Types of files that can be read simply by file.read()
|
||||
# and have 100% to be completely readable
|
||||
TEXT_FILE_TYPES = [
|
||||
"txt",
|
||||
"md",
|
||||
"mdx",
|
||||
"csv",
|
||||
"json",
|
||||
"yaml",
|
||||
"yml",
|
||||
"xml",
|
||||
"html",
|
||||
"htm",
|
||||
"pdf",
|
||||
]
|
||||
|
||||
|
||||
def is_hidden(path: Path) -> bool:
|
||||
return path.name.startswith(".")
|
||||
|
||||
|
||||
def retrieve_file_paths(
|
||||
path: str,
|
||||
load_hidden: bool,
|
||||
recursive: bool,
|
||||
depth: int,
|
||||
types: List[str] = TEXT_FILE_TYPES,
|
||||
) -> List[str]:
|
||||
path_obj = Path(path)
|
||||
if not path_obj.exists() or not path_obj.is_dir():
|
||||
raise ValueError(f"Path {path} must exist and be a directory.")
|
||||
|
||||
def match_types(p: Path) -> bool:
|
||||
return any(p.suffix == f".{t}" for t in types) if types else True
|
||||
|
||||
def is_not_hidden(p: Path) -> bool:
|
||||
return not is_hidden(p) or load_hidden
|
||||
|
||||
def walk_level(directory: Path, max_depth: int):
|
||||
directory = directory.resolve()
|
||||
prefix_length = len(directory.parts)
|
||||
for p in directory.rglob("*" if recursive else "[!.]*"):
|
||||
if len(p.parts) - prefix_length <= max_depth:
|
||||
yield p
|
||||
|
||||
glob = "**/*" if recursive else "*"
|
||||
paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob)
|
||||
file_paths = [Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)]
|
||||
|
||||
return file_paths
|
||||
|
||||
|
||||
def partition_file_to_record(file_path: str, silent_errors: bool) -> Optional[Record]:
|
||||
# Use the partition function to load the file
|
||||
from unstructured.partition.auto import partition # type: ignore
|
||||
|
||||
try:
|
||||
elements = partition(file_path)
|
||||
except Exception as e:
|
||||
if not silent_errors:
|
||||
raise ValueError(f"Error loading file {file_path}: {e}") from e
|
||||
return None
|
||||
|
||||
# Create a Record
|
||||
text = "\n\n".join([Text(el) for el in elements])
|
||||
metadata = elements.metadata if hasattr(elements, "metadata") else {}
|
||||
metadata["file_path"] = file_path
|
||||
record = Record(text=text, data=metadata)
|
||||
return record
|
||||
|
||||
|
||||
def read_text_file(file_path: str) -> str:
|
||||
with open(file_path, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def parse_pdf_to_text(file_path: str) -> str:
|
||||
from pypdf import PdfReader # type: ignore
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
reader = PdfReader(f)
|
||||
return "\n\n".join([page.extract_text() for page in reader.pages])
|
||||
|
||||
|
||||
def parse_text_file_to_record(file_path: str, silent_errors: bool) -> Optional[Record]:
|
||||
try:
|
||||
if file_path.endswith(".pdf"):
|
||||
text = parse_pdf_to_text(file_path)
|
||||
else:
|
||||
text = read_text_file(file_path)
|
||||
# if file is json, yaml, or xml, we can parse it
|
||||
if file_path.endswith(".json"):
|
||||
text = json.loads(text)
|
||||
elif file_path.endswith(".yaml") or file_path.endswith(".yml"):
|
||||
text = yaml.safe_load(text)
|
||||
elif file_path.endswith(".xml"):
|
||||
xml_element = ET.fromstring(text)
|
||||
text = ET.tostring(xml_element, encoding="unicode")
|
||||
except Exception as e:
|
||||
if not silent_errors:
|
||||
raise ValueError(f"Error loading file {file_path}: {e}") from e
|
||||
return None
|
||||
|
||||
record = Record(data={"file_path": file_path, "text": text})
|
||||
return record
|
||||
|
||||
|
||||
def get_elements(
|
||||
file_paths: List[str],
|
||||
silent_errors: bool,
|
||||
max_concurrency: int,
|
||||
use_multithreading: bool,
|
||||
) -> List[Optional[Record]]:
|
||||
if use_multithreading:
|
||||
records = parallel_load_records(file_paths, silent_errors, max_concurrency)
|
||||
else:
|
||||
records = [partition_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
||||
records = list(filter(None, records))
|
||||
return records
|
||||
|
||||
|
||||
def parallel_load_records(
|
||||
file_paths: List[str],
|
||||
silent_errors: bool,
|
||||
max_concurrency: int,
|
||||
load_function: Callable = parse_text_file_to_record,
|
||||
) -> List[Optional[Record]]:
|
||||
with futures.ThreadPoolExecutor(max_workers=max_concurrency) as executor:
|
||||
loaded_files = executor.map(
|
||||
lambda file_path: load_function(file_path, silent_errors),
|
||||
file_paths,
|
||||
)
|
||||
# loaded_files is an iterator, so we need to convert it to a list
|
||||
return list(loaded_files)
|
||||
0
src/backend/langflow/base/io/__init__.py
Normal file
0
src/backend/langflow/base/io/__init__.py
Normal file
106
src/backend/langflow/base/io/chat.py
Normal file
106
src/backend/langflow/base/io/chat.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import warnings
|
||||
from typing import Optional, Union
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
from langflow.memory import add_messages
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class ChatComponent(CustomComponent):
|
||||
display_name = "Chat Component"
|
||||
description = "Use as base for chat components."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"input_value": {
|
||||
"input_types": ["Text"],
|
||||
"display_name": "Message",
|
||||
"multiline": True,
|
||||
},
|
||||
"sender": {
|
||||
"options": ["Machine", "User"],
|
||||
"display_name": "Sender Type",
|
||||
},
|
||||
"sender_name": {"display_name": "Sender Name"},
|
||||
"session_id": {
|
||||
"display_name": "Session ID",
|
||||
"info": "If provided, the message will be stored in the memory.",
|
||||
"advanced": True,
|
||||
},
|
||||
"return_record": {
|
||||
"display_name": "Return Record",
|
||||
"info": "Return the message as a record containing the sender, sender_name, and session_id.",
|
||||
},
|
||||
}
|
||||
|
||||
def store_message(
|
||||
self,
|
||||
message: Union[str, Text, Record],
|
||||
session_id: Optional[str] = None,
|
||||
sender: Optional[str] = None,
|
||||
sender_name: Optional[str] = None,
|
||||
) -> list[Record]:
|
||||
if not message:
|
||||
warnings.warn("No message provided.")
|
||||
return []
|
||||
|
||||
if not session_id or not sender or not sender_name:
|
||||
raise ValueError("All of session_id, sender, and sender_name must be provided.")
|
||||
if isinstance(message, Record):
|
||||
record = message
|
||||
record.data.update(
|
||||
{
|
||||
"session_id": session_id,
|
||||
"sender": sender,
|
||||
"sender_name": sender_name,
|
||||
}
|
||||
)
|
||||
else:
|
||||
record = Record(
|
||||
data={
|
||||
"text": message,
|
||||
"session_id": session_id,
|
||||
"sender": sender,
|
||||
"sender_name": sender_name,
|
||||
},
|
||||
)
|
||||
|
||||
self.status = record
|
||||
records = add_messages([record])
|
||||
return records[0]
|
||||
|
||||
def build(
|
||||
self,
|
||||
sender: Optional[str] = "User",
|
||||
sender_name: Optional[str] = "User",
|
||||
input_value: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
return_record: Optional[bool] = False,
|
||||
) -> Union[Text, Record]:
|
||||
input_value_record: Optional[Record] = None
|
||||
if return_record:
|
||||
if isinstance(input_value, Record):
|
||||
# Update the data of the record
|
||||
input_value.data["sender"] = sender
|
||||
input_value.data["sender_name"] = sender_name
|
||||
input_value.data["session_id"] = session_id
|
||||
else:
|
||||
input_value_record = Record(
|
||||
text=input_value,
|
||||
data={
|
||||
"sender": sender,
|
||||
"sender_name": sender_name,
|
||||
"session_id": session_id,
|
||||
},
|
||||
)
|
||||
if not input_value:
|
||||
input_value = ""
|
||||
if return_record and input_value_record:
|
||||
result: Union[Text, Record] = input_value_record
|
||||
else:
|
||||
result = input_value
|
||||
self.status = result
|
||||
if session_id:
|
||||
self.store_message(result, session_id, sender, sender_name)
|
||||
return result
|
||||
19
src/backend/langflow/base/io/text.py
Normal file
19
src/backend/langflow/base/io/text.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class TextComponent(CustomComponent):
|
||||
display_name = "Text Component"
|
||||
description = "Used to pass text to the next component."
|
||||
|
||||
field_config = {
|
||||
"input_value": {"display_name": "Value", "multiline": True},
|
||||
}
|
||||
|
||||
def build(self, input_value: Optional[str] = "") -> Text:
|
||||
self.status = input_value
|
||||
if not input_value:
|
||||
input_value = ""
|
||||
return input_value
|
||||
0
src/backend/langflow/base/prompts/__init__.py
Normal file
0
src/backend/langflow/base/prompts/__init__.py
Normal file
137
src/backend/langflow/base/prompts/utils.py
Normal file
137
src/backend/langflow/base/prompts/utils.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
from fastapi import HTTPException
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain_core.documents import Document
|
||||
from loguru import logger
|
||||
|
||||
from langflow.api.v1.base import INVALID_NAMES, check_input_variables
|
||||
from langflow.interface.utils import extract_input_variables_from_prompt
|
||||
from langflow.schema import Record
|
||||
from langflow.template.field.prompt import DefaultPromptField
|
||||
|
||||
|
||||
def dict_values_to_string(d: dict) -> dict:
|
||||
"""
|
||||
Converts the values of a dictionary to strings.
|
||||
|
||||
Args:
|
||||
d (dict): The dictionary whose values need to be converted.
|
||||
|
||||
Returns:
|
||||
dict: The dictionary with values converted to strings.
|
||||
"""
|
||||
# Do something similar to the above
|
||||
for key, value in d.items():
|
||||
# it could be a list of records or documents or strings
|
||||
if isinstance(value, list):
|
||||
for i, item in enumerate(value):
|
||||
if isinstance(item, Record):
|
||||
d[key][i] = record_to_string(item)
|
||||
elif isinstance(item, Document):
|
||||
d[key][i] = document_to_string(item)
|
||||
elif isinstance(value, Record):
|
||||
d[key] = record_to_string(value)
|
||||
elif isinstance(value, Document):
|
||||
d[key] = document_to_string(value)
|
||||
return d
|
||||
|
||||
|
||||
def record_to_string(record: Record) -> str:
|
||||
"""
|
||||
Convert a record to a string.
|
||||
|
||||
Args:
|
||||
record (Record): The record to convert.
|
||||
|
||||
Returns:
|
||||
str: The record as a string.
|
||||
"""
|
||||
return record.text
|
||||
|
||||
|
||||
def document_to_string(document: Document) -> str:
|
||||
"""
|
||||
Convert a document to a string.
|
||||
|
||||
Args:
|
||||
document (Document): The document to convert.
|
||||
|
||||
Returns:
|
||||
str: The document as a string.
|
||||
"""
|
||||
return document.page_content
|
||||
|
||||
|
||||
def validate_prompt(prompt_template: str, silent_errors: bool = False) -> list[str]:
|
||||
input_variables = extract_input_variables_from_prompt(prompt_template)
|
||||
|
||||
# Check if there are invalid characters in the input_variables
|
||||
input_variables = check_input_variables(input_variables)
|
||||
if any(var in INVALID_NAMES for var in input_variables):
|
||||
raise ValueError(f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. ")
|
||||
|
||||
try:
|
||||
PromptTemplate(template=prompt_template, input_variables=input_variables)
|
||||
except Exception as exc:
|
||||
logger.error(f"Invalid prompt: {exc}")
|
||||
if not silent_errors:
|
||||
raise ValueError(f"Invalid prompt: {exc}") from exc
|
||||
|
||||
return input_variables
|
||||
|
||||
|
||||
def get_old_custom_fields(custom_fields, name):
|
||||
try:
|
||||
if len(custom_fields) == 1 and name == "":
|
||||
# If there is only one custom field and the name is empty string
|
||||
# then we are dealing with the first prompt request after the node was created
|
||||
name = list(custom_fields.keys())[0]
|
||||
|
||||
old_custom_fields = custom_fields[name]
|
||||
if not old_custom_fields:
|
||||
old_custom_fields = []
|
||||
|
||||
old_custom_fields = old_custom_fields.copy()
|
||||
except KeyError:
|
||||
old_custom_fields = []
|
||||
custom_fields[name] = []
|
||||
return old_custom_fields
|
||||
|
||||
|
||||
def add_new_variables_to_template(input_variables, custom_fields, template, name):
|
||||
for variable in input_variables:
|
||||
try:
|
||||
template_field = DefaultPromptField(name=variable, display_name=variable)
|
||||
if variable in template:
|
||||
# Set the new field with the old value
|
||||
template_field.value = template[variable]["value"]
|
||||
|
||||
template[variable] = template_field.to_dict()
|
||||
|
||||
# Check if variable is not already in the list before appending
|
||||
if variable not in custom_fields[name]:
|
||||
custom_fields[name].append(variable)
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
def remove_old_variables_from_template(old_custom_fields, input_variables, custom_fields, template, name):
|
||||
for variable in old_custom_fields:
|
||||
if variable not in input_variables:
|
||||
try:
|
||||
# Remove the variable from custom_fields associated with the given name
|
||||
if variable in custom_fields[name]:
|
||||
custom_fields[name].remove(variable)
|
||||
|
||||
# Remove the variable from the template
|
||||
template.pop(variable, None)
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
def update_input_variables_field(input_variables, template):
|
||||
if "input_variables" in template:
|
||||
template["input_variables"]["value"] = input_variables
|
||||
|
|
@ -1,4 +1,18 @@
|
|||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
__all__ = ["CustomComponent"]
|
||||
__all__ = [
|
||||
"agents",
|
||||
"chains",
|
||||
"data",
|
||||
"documentloaders",
|
||||
"embeddings",
|
||||
"experimental",
|
||||
"helpers",
|
||||
"inputs",
|
||||
"memories",
|
||||
"model_specs",
|
||||
"models",
|
||||
"outputs",
|
||||
"retrievers",
|
||||
"textsplitters",
|
||||
"toolkits",
|
||||
"vectorstores",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ from typing import List, Optional
|
|||
from langchain.agents.agent import AgentExecutor
|
||||
from langchain.agents.agent_toolkits.conversational_retrieval.openai_functions import _get_default_system_message
|
||||
from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
|
||||
from langchain_community.chat_models import ChatOpenAI
|
||||
from langchain.memory.token_buffer import ConversationTokenBufferMemory
|
||||
from langchain.prompts import SystemMessagePromptTemplate
|
||||
from langchain.prompts.chat import MessagesPlaceholder
|
||||
from langchain.schema.memory import BaseMemory
|
||||
from langchain.tools import Tool
|
||||
|
||||
from langchain_community.chat_models import ChatOpenAI
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing.range_spec import RangeSpec
|
||||
|
||||
|
|
@ -17,14 +16,16 @@ from langflow.field_typing.range_spec import RangeSpec
|
|||
class ConversationalAgent(CustomComponent):
|
||||
display_name: str = "OpenAI Conversational Agent"
|
||||
description: str = "Conversational Agent that can use OpenAI's function calling API"
|
||||
icon = "OpenAI"
|
||||
|
||||
def build_config(self):
|
||||
openai_function_models = [
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-0125-preview",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-4",
|
||||
"gpt-4-32k",
|
||||
"gpt-4-vision-preview",
|
||||
"gpt-3.5-turbo-0125",
|
||||
"gpt-3.5-turbo-1106",
|
||||
]
|
||||
return {
|
||||
"tools": {"display_name": "Tools"},
|
||||
|
|
|
|||
70
src/backend/langflow/components/agents/ReActAgent.py
Normal file
70
src/backend/langflow/components/agents/ReActAgent.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# from typing import Dict, List
|
||||
|
||||
# import dspy
|
||||
|
||||
# from langflow import CustomComponent
|
||||
# from langflow.field_typing import Text
|
||||
|
||||
|
||||
# class ReActAgentComponent(CustomComponent):
|
||||
# display_name = "ReAct Agent"
|
||||
# description = "A component to create a ReAct Agent."
|
||||
# icon = "user-secret"
|
||||
|
||||
# def build_config(self):
|
||||
# return {
|
||||
# "input_value": {
|
||||
# "display_name": "Input",
|
||||
# "input_types": ["Text"],
|
||||
# "info": "The input value for the ReAct Agent.",
|
||||
# },
|
||||
# "instructions": {
|
||||
# "display_name": "Instructions",
|
||||
# "info": "The Prompt.",
|
||||
# },
|
||||
# "inputs": {
|
||||
# "display_name": "Inputs",
|
||||
# "info": "The Name and Description of the Input Fields.",
|
||||
# },
|
||||
# "outputs": {
|
||||
# "display_name": "Outputs",
|
||||
# "info": "The Name and Description of the Output Fields.",
|
||||
# },
|
||||
# }
|
||||
|
||||
# def build(
|
||||
# self,
|
||||
# input_value: List[dict],
|
||||
# instructions: Text,
|
||||
# inputs: List[dict],
|
||||
# outputs: List[Dict],
|
||||
# ) -> Text:
|
||||
# # inputs is a list of dictionaries where the key is the name of the input
|
||||
# # and the value is the description of the input
|
||||
# input_fields = (
|
||||
# {}
|
||||
# ) # dict[str, FieldInfo] InputField and OutputField are subclasses of pydantic.Field
|
||||
# for input_dict in inputs:
|
||||
# for name, description in input_dict.items():
|
||||
# prefix = name if ":" in name else f"{name}:"
|
||||
# input_fields[name] = dspy.InputField(
|
||||
# prefix=prefix, description=description
|
||||
# )
|
||||
|
||||
# output_fields = {} # dict[str, FieldInfo]
|
||||
# for output_dict in outputs:
|
||||
# for name, description in output_dict.items():
|
||||
# prefix = name if ":" in name else f"{name}:"
|
||||
# output_fields[name] = dspy.OutputField(
|
||||
# prefix=prefix, description=description
|
||||
# )
|
||||
|
||||
# signature = dspy.make_signature(inputs, instructions=instructions)
|
||||
# agent = dspy.ReAct(
|
||||
# signature=signature,
|
||||
# )
|
||||
# inputs_dict = {}
|
||||
# for input_dict in input_value:
|
||||
# inputs_dict.update(input_dict)
|
||||
|
||||
# result = agent(inputs_dict)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from .AgentInitializer import AgentInitializerComponent
|
||||
from .CSVAgent import CSVAgentComponent
|
||||
from .JsonAgent import JsonAgentComponent
|
||||
from .OpenAIConversationalAgent import ConversationalAgent
|
||||
from .SQLAgent import SQLAgentComponent
|
||||
from .VectorStoreAgent import VectorStoreAgentComponent
|
||||
from .VectorStoreRouterAgent import VectorStoreRouterAgentComponent
|
||||
|
||||
__all__ = [
|
||||
"AgentInitializerComponent",
|
||||
"CSVAgentComponent",
|
||||
"JsonAgentComponent",
|
||||
"ConversationalAgent",
|
||||
"SQLAgentComponent",
|
||||
"VectorStoreAgentComponent",
|
||||
"VectorStoreRouterAgentComponent",
|
||||
]
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
from langflow import CustomComponent
|
||||
from typing import Optional
|
||||
|
||||
from langchain.chains import ConversationChain
|
||||
from typing import Optional, Union, Callable
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, Chain
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, Text
|
||||
|
||||
|
||||
class ConversationChainComponent(CustomComponent):
|
||||
|
|
@ -21,9 +23,21 @@ class ConversationChainComponent(CustomComponent):
|
|||
|
||||
def build(
|
||||
self,
|
||||
input_value: Text,
|
||||
llm: BaseLanguageModel,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
) -> Union[Chain, Callable]:
|
||||
) -> Text:
|
||||
if memory is None:
|
||||
return ConversationChain(llm=llm)
|
||||
return ConversationChain(llm=llm, memory=memory)
|
||||
chain = ConversationChain(llm=llm)
|
||||
else:
|
||||
chain = ConversationChain(llm=llm, memory=memory)
|
||||
result = chain.invoke({"input": input_value})
|
||||
if isinstance(result, dict):
|
||||
result = result.get(chain.output_key, "") # type: ignore
|
||||
|
||||
elif isinstance(result, str):
|
||||
result = result
|
||||
else:
|
||||
result = result.get("response")
|
||||
self.status = result
|
||||
return str(result)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Callable, Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
from langchain.chains import LLMChain
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ from langflow.field_typing import (
|
|||
BaseLanguageModel,
|
||||
BaseMemory,
|
||||
BasePromptTemplate,
|
||||
Chain,
|
||||
Text,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -28,5 +28,10 @@ class LLMChainComponent(CustomComponent):
|
|||
prompt: BasePromptTemplate,
|
||||
llm: BaseLanguageModel,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
) -> Union[Chain, Callable, LLMChain]:
|
||||
return LLMChain(prompt=prompt, llm=llm, memory=memory)
|
||||
) -> Text:
|
||||
runnable = LLMChain(prompt=prompt, llm=llm, memory=memory)
|
||||
result_dict = runnable.invoke({})
|
||||
output_key = runnable.output_key
|
||||
result = result_dict[output_key]
|
||||
self.status = result
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain.chains import LLMCheckerChain
|
||||
from typing import Union, Callable
|
||||
from langflow.field_typing import (
|
||||
BaseLanguageModel,
|
||||
Chain,
|
||||
)
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, Text
|
||||
|
||||
|
||||
class LLMCheckerChainComponent(CustomComponent):
|
||||
|
|
@ -19,6 +16,12 @@ class LLMCheckerChainComponent(CustomComponent):
|
|||
|
||||
def build(
|
||||
self,
|
||||
input_value: Text,
|
||||
llm: BaseLanguageModel,
|
||||
) -> Union[Chain, Callable]:
|
||||
return LLMCheckerChain(llm=llm)
|
||||
) -> Text:
|
||||
chain = LLMCheckerChain.from_llm(llm=llm)
|
||||
response = chain.invoke({chain.input_key: input_value})
|
||||
result = response.get(chain.output_key, "")
|
||||
result_str = Text(result)
|
||||
self.status = result_str
|
||||
return result_str
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from typing import Callable, Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
from langchain.chains import LLMChain, LLMMathChain
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, Chain
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, Text
|
||||
|
||||
|
||||
class LLMMathChainComponent(CustomComponent):
|
||||
|
|
@ -22,10 +22,22 @@ class LLMMathChainComponent(CustomComponent):
|
|||
|
||||
def build(
|
||||
self,
|
||||
input_value: Text,
|
||||
llm: BaseLanguageModel,
|
||||
llm_chain: LLMChain,
|
||||
input_key: str = "question",
|
||||
output_key: str = "answer",
|
||||
memory: Optional[BaseMemory] = None,
|
||||
) -> Union[LLMMathChain, Callable, Chain]:
|
||||
return LLMMathChain(llm=llm, llm_chain=llm_chain, input_key=input_key, output_key=output_key, memory=memory)
|
||||
) -> Text:
|
||||
chain = LLMMathChain(
|
||||
llm=llm,
|
||||
llm_chain=llm_chain,
|
||||
input_key=input_key,
|
||||
output_key=output_key,
|
||||
memory=memory,
|
||||
)
|
||||
response = chain.invoke({input_key: input_value})
|
||||
result = response.get(output_key)
|
||||
result_str = Text(result)
|
||||
self.status = result_str
|
||||
return result_str
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
from langchain.llms.base import BaseLLM
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class PromptRunner(CustomComponent):
|
||||
display_name: str = "Prompt Runner"
|
||||
description: str = "Run a Chain with the given PromptTemplate"
|
||||
beta: bool = True
|
||||
field_config = {
|
||||
"llm": {"display_name": "LLM"},
|
||||
"prompt": {
|
||||
"display_name": "Prompt Template",
|
||||
"info": "Make sure the prompt has all variables filled.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {}) -> Document:
|
||||
chain = prompt | llm
|
||||
# The input is an empty dict because the prompt is already filled
|
||||
result = chain.invoke(input=inputs)
|
||||
if hasattr(result, "content"):
|
||||
result = result.content
|
||||
self.repr_value = result
|
||||
return Document(page_content=str(result))
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
from typing import Callable, Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
from langchain.chains.combine_documents.base import BaseCombineDocumentsChain
|
||||
from langchain.chains.retrieval_qa.base import BaseRetrievalQA, RetrievalQA
|
||||
from langchain.chains.retrieval_qa.base import RetrievalQA
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseMemory, BaseRetriever
|
||||
from langflow.field_typing import BaseMemory, BaseRetriever, Text
|
||||
|
||||
|
||||
class RetrievalQAComponent(CustomComponent):
|
||||
display_name = "RetrievalQA"
|
||||
display_name = "Retrieval QA"
|
||||
description = "Chain for question-answering against an index."
|
||||
|
||||
def build_config(self):
|
||||
|
|
@ -18,18 +20,23 @@ class RetrievalQAComponent(CustomComponent):
|
|||
"input_key": {"display_name": "Input Key", "advanced": True},
|
||||
"output_key": {"display_name": "Output Key", "advanced": True},
|
||||
"return_source_documents": {"display_name": "Return Source Documents"},
|
||||
"input_value": {
|
||||
"display_name": "Input",
|
||||
"input_types": ["Text", "Document"],
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
combine_documents_chain: BaseCombineDocumentsChain,
|
||||
retriever: BaseRetriever,
|
||||
input_value: str = "",
|
||||
memory: Optional[BaseMemory] = None,
|
||||
input_key: str = "query",
|
||||
output_key: str = "result",
|
||||
return_source_documents: bool = True,
|
||||
) -> Union[BaseRetrievalQA, Callable]:
|
||||
return RetrievalQA(
|
||||
) -> Text:
|
||||
runnable = RetrievalQA(
|
||||
combine_documents_chain=combine_documents_chain,
|
||||
retriever=retriever,
|
||||
memory=memory,
|
||||
|
|
@ -37,3 +44,19 @@ class RetrievalQAComponent(CustomComponent):
|
|||
output_key=output_key,
|
||||
return_source_documents=return_source_documents,
|
||||
)
|
||||
if isinstance(input_value, Document):
|
||||
input_value = input_value.page_content
|
||||
self.status = runnable
|
||||
result = runnable.invoke({input_key: input_value})
|
||||
result = result.content if hasattr(result, "content") else result
|
||||
# Result is a dict with keys "query", "result" and "source_documents"
|
||||
# for now we just return the result
|
||||
records = self.to_records(result.get("source_documents"))
|
||||
references_str = ""
|
||||
if return_source_documents:
|
||||
references_str = self.create_references_from_records(records)
|
||||
result_str = result.get("result", "")
|
||||
|
||||
final_result = "\n".join([Text(result_str), references_str])
|
||||
self.status = final_result
|
||||
return final_result # OK
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.chains import RetrievalQAWithSourcesChain
|
||||
from langchain.chains.qa_with_sources.base import BaseQAWithSourcesChain
|
||||
from langchain.chains.combine_documents.base import BaseCombineDocumentsChain
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, BaseRetriever
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, BaseRetriever, Text
|
||||
|
||||
|
||||
class RetrievalQAWithSourcesChainComponent(CustomComponent):
|
||||
|
|
@ -18,25 +17,42 @@ class RetrievalQAWithSourcesChainComponent(CustomComponent):
|
|||
"chain_type": {
|
||||
"display_name": "Chain Type",
|
||||
"options": ["stuff", "map_reduce", "map_rerank", "refine"],
|
||||
"info": "The type of chain to use to combined Documents.",
|
||||
},
|
||||
"memory": {"display_name": "Memory"},
|
||||
"return_source_documents": {"display_name": "Return Source Documents"},
|
||||
"retriever": {"display_name": "Retriever"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
input_value: Text,
|
||||
retriever: BaseRetriever,
|
||||
llm: BaseLanguageModel,
|
||||
combine_documents_chain: BaseCombineDocumentsChain,
|
||||
chain_type: str,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
return_source_documents: Optional[bool] = True,
|
||||
) -> BaseQAWithSourcesChain:
|
||||
return RetrievalQAWithSourcesChain.from_chain_type(
|
||||
) -> Text:
|
||||
runnable = RetrievalQAWithSourcesChain.from_chain_type(
|
||||
llm=llm,
|
||||
chain_type=chain_type,
|
||||
combine_documents_chain=combine_documents_chain,
|
||||
memory=memory,
|
||||
return_source_documents=return_source_documents,
|
||||
retriever=retriever,
|
||||
)
|
||||
if isinstance(input_value, Document):
|
||||
input_value = input_value.page_content
|
||||
self.status = runnable
|
||||
input_key = runnable.input_keys[0]
|
||||
result = runnable.invoke({input_key: input_value})
|
||||
result = result.content if hasattr(result, "content") else result
|
||||
# Result is a dict with keys "query", "result" and "source_documents"
|
||||
# for now we just return the result
|
||||
records = self.to_records(result.get("source_documents"))
|
||||
references_str = ""
|
||||
if return_source_documents:
|
||||
references_str = self.create_references_from_records(records)
|
||||
result_str = Text(result.get("answer", ""))
|
||||
final_result = "\n".join([result_str, references_str])
|
||||
self.status = final_result
|
||||
return final_result
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
from langflow import CustomComponent
|
||||
from typing import Callable, Union
|
||||
from langflow.field_typing import BasePromptTemplate, BaseLanguageModel, Chain
|
||||
from langchain_community.utilities.sql_database import SQLDatabase
|
||||
from langchain_experimental.sql.base import SQLDatabaseChain
|
||||
|
||||
|
||||
class SQLDatabaseChainComponent(CustomComponent):
|
||||
display_name = "SQLDatabaseChain"
|
||||
description = ""
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"db": {"display_name": "Database"},
|
||||
"llm": {"display_name": "LLM"},
|
||||
"prompt": {"display_name": "Prompt"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
db: SQLDatabase,
|
||||
llm: BaseLanguageModel,
|
||||
prompt: BasePromptTemplate,
|
||||
) -> Union[Chain, Callable, SQLDatabaseChain]:
|
||||
return SQLDatabaseChain.from_llm(llm=llm, db=db, prompt=prompt)
|
||||
57
src/backend/langflow/components/chains/SQLGenerator.py
Normal file
57
src/backend/langflow/components/chains/SQLGenerator.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.chains import create_sql_query_chain
|
||||
from langchain_community.utilities.sql_database import SQLDatabase
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
from langchain_core.runnables import Runnable
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, Text
|
||||
|
||||
|
||||
class SQLGeneratorComponent(CustomComponent):
|
||||
display_name = "Natural Language to SQL"
|
||||
description = "Generate SQL from natural language."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"db": {"display_name": "Database"},
|
||||
"llm": {"display_name": "LLM"},
|
||||
"prompt": {
|
||||
"display_name": "Prompt",
|
||||
"info": "The prompt must contain `{question}`.",
|
||||
},
|
||||
"top_k": {
|
||||
"display_name": "Top K",
|
||||
"info": "The number of results per select statement to return. If 0, no limit.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
input_value: Text,
|
||||
db: SQLDatabase,
|
||||
llm: BaseLanguageModel,
|
||||
top_k: int = 5,
|
||||
prompt: Optional[Text] = None,
|
||||
) -> Text:
|
||||
if prompt:
|
||||
prompt_template = PromptTemplate.from_template(template=prompt)
|
||||
else:
|
||||
prompt_template = None
|
||||
|
||||
if top_k < 1:
|
||||
raise ValueError("Top K must be greater than 0.")
|
||||
|
||||
if not prompt_template:
|
||||
sql_query_chain = create_sql_query_chain(llm=llm, db=db, k=top_k)
|
||||
else:
|
||||
# Check if {question} is in the prompt
|
||||
if "{question}" not in prompt_template.template or "question" not in prompt_template.input_variables:
|
||||
raise ValueError("Prompt must contain `{question}` to be used with Natural Language to SQL.")
|
||||
sql_query_chain = create_sql_query_chain(llm=llm, db=db, prompt=prompt_template, k=top_k)
|
||||
query_writer: Runnable = sql_query_chain | {"query": lambda x: x.replace("SQLQuery:", "").strip()}
|
||||
response = query_writer.invoke({"question": input_value})
|
||||
query = response.get("query")
|
||||
self.status = query
|
||||
return query
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from .ConversationChain import ConversationChainComponent
|
||||
from .LLMChain import LLMChainComponent
|
||||
from .LLMCheckerChain import LLMCheckerChainComponent
|
||||
from .LLMMathChain import LLMMathChainComponent
|
||||
from .RetrievalQA import RetrievalQAComponent
|
||||
from .RetrievalQAWithSourcesChain import RetrievalQAWithSourcesChainComponent
|
||||
from .SQLGenerator import SQLGeneratorComponent
|
||||
|
||||
__all__ = [
|
||||
"ConversationChainComponent",
|
||||
"LLMChainComponent",
|
||||
"LLMCheckerChainComponent",
|
||||
"LLMMathChainComponent",
|
||||
"RetrievalQAComponent",
|
||||
"RetrievalQAWithSourcesChainComponent",
|
||||
"SQLGeneratorComponent",
|
||||
]
|
||||
115
src/backend/langflow/components/data/APIRequest.py
Normal file
115
src/backend/langflow/components/data/APIRequest.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import asyncio
|
||||
import json
|
||||
from typing import List, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class APIRequest(CustomComponent):
|
||||
display_name: str = "API Request"
|
||||
description: str = "Make an HTTP request to the given URL."
|
||||
output_types: list[str] = ["Record"]
|
||||
documentation: str = "https://docs.langflow.org/components/utilities#api-request"
|
||||
field_config = {
|
||||
"urls": {"display_name": "URLs", "info": "The URLs to make the request to."},
|
||||
"method": {
|
||||
"display_name": "Method",
|
||||
"info": "The HTTP method to use.",
|
||||
"field_type": "str",
|
||||
"options": ["GET", "POST", "PATCH", "PUT"],
|
||||
"value": "GET",
|
||||
},
|
||||
"headers": {
|
||||
"display_name": "Headers",
|
||||
"info": "The headers to send with the request.",
|
||||
"input_types": ["dict"],
|
||||
},
|
||||
"body": {
|
||||
"display_name": "Body",
|
||||
"info": "The body to send with the request (for POST, PATCH, PUT).",
|
||||
"input_types": ["dict"],
|
||||
},
|
||||
"timeout": {
|
||||
"display_name": "Timeout",
|
||||
"field_type": "int",
|
||||
"info": "The timeout to use for the request.",
|
||||
"value": 5,
|
||||
},
|
||||
}
|
||||
|
||||
async def make_request(
|
||||
self,
|
||||
client: httpx.AsyncClient,
|
||||
method: str,
|
||||
url: str,
|
||||
headers: Optional[dict] = None,
|
||||
body: Optional[dict] = None,
|
||||
timeout: int = 5,
|
||||
) -> Record:
|
||||
method = method.upper()
|
||||
if method not in ["GET", "POST", "PATCH", "PUT", "DELETE"]:
|
||||
raise ValueError(f"Unsupported method: {method}")
|
||||
|
||||
data = body if body else None
|
||||
payload = json.dumps(data)
|
||||
try:
|
||||
response = await client.request(method, url, headers=headers, content=payload, timeout=timeout)
|
||||
try:
|
||||
result = response.json()
|
||||
except Exception:
|
||||
result = response.text
|
||||
return Record(
|
||||
data={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": response.status_code,
|
||||
"result": result,
|
||||
},
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
return Record(
|
||||
data={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": 408,
|
||||
"error": "Request timed out",
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
return Record(
|
||||
data={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": 500,
|
||||
"error": str(exc),
|
||||
},
|
||||
)
|
||||
|
||||
async def build(
|
||||
self,
|
||||
method: str,
|
||||
urls: List[str],
|
||||
headers: Optional[dict] = None,
|
||||
body: Optional[List[Record]] = None,
|
||||
timeout: int = 5,
|
||||
) -> List[Record]:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
bodies = []
|
||||
if body:
|
||||
if isinstance(body, list):
|
||||
bodies = [b.data for b in body]
|
||||
else:
|
||||
bodies = [body.data]
|
||||
if len(urls) != len(bodies):
|
||||
# add bodies with None
|
||||
bodies += [None] * (len(urls) - len(bodies)) # type: ignore
|
||||
async with httpx.AsyncClient() as client:
|
||||
results = await asyncio.gather(
|
||||
*[self.make_request(client, method, u, headers, rec, timeout) for u, rec in zip(urls, bodies)]
|
||||
)
|
||||
self.status = results
|
||||
return results
|
||||
66
src/backend/langflow/components/data/Directory.py
Normal file
66
src/backend/langflow/components/data/Directory.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.base.data.utils import (
|
||||
parallel_load_records,
|
||||
parse_text_file_to_record,
|
||||
retrieve_file_paths,
|
||||
)
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class DirectoryComponent(CustomComponent):
|
||||
display_name = "Directory"
|
||||
description = "Load Text Files from a Directory and Convert Them to Records."
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"path": {"display_name": "Path"},
|
||||
"types": {
|
||||
"display_name": "Types",
|
||||
"info": "File types to load. Leave empty to load all types.",
|
||||
},
|
||||
"depth": {"display_name": "Depth", "info": "Depth to search for files."},
|
||||
"max_concurrency": {"display_name": "Max Concurrency", "advanced": True},
|
||||
"load_hidden": {
|
||||
"display_name": "Load Hidden",
|
||||
"advanced": True,
|
||||
"info": "If true, hidden files will be loaded.",
|
||||
},
|
||||
"recursive": {
|
||||
"display_name": "Recursive",
|
||||
"advanced": True,
|
||||
"info": "If true, the search will be recursive.",
|
||||
},
|
||||
"silent_errors": {
|
||||
"display_name": "Silent Errors",
|
||||
"advanced": True,
|
||||
"info": "If true, errors will not raise an exception.",
|
||||
},
|
||||
"use_multithreading": {
|
||||
"display_name": "Use Multithreading",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
path: str,
|
||||
depth: int = 0,
|
||||
max_concurrency: int = 2,
|
||||
load_hidden: bool = False,
|
||||
recursive: bool = True,
|
||||
silent_errors: bool = False,
|
||||
use_multithreading: bool = True,
|
||||
) -> List[Optional[Record]]:
|
||||
resolved_path = self.resolve_path(path)
|
||||
file_paths = retrieve_file_paths(resolved_path, load_hidden, recursive, depth)
|
||||
loaded_records = []
|
||||
|
||||
if use_multithreading:
|
||||
loaded_records = parallel_load_records(file_paths, silent_errors, max_concurrency)
|
||||
else:
|
||||
loaded_records = [parse_text_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
||||
loaded_records = list(filter(None, loaded_records))
|
||||
self.status = loaded_records
|
||||
return loaded_records
|
||||
31
src/backend/langflow/components/data/File.py
Normal file
31
src/backend/langflow/components/data/File.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from typing import Any, Dict, Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.base.data.utils import TEXT_FILE_TYPES, parse_text_file_to_record
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class FileComponent(CustomComponent):
|
||||
display_name = "File"
|
||||
description = "Load a file."
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"path": {"display_name": "Path"},
|
||||
"silent_errors": {
|
||||
"display_name": "Silent Errors",
|
||||
"advanced": True,
|
||||
"info": "If true, errors will not raise an exception.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
path: str,
|
||||
silent_errors: bool = False,
|
||||
) -> Optional[Record]:
|
||||
resolved_path = self.resolve_path(path)
|
||||
extension = resolved_path.split(".")[-1]
|
||||
if extension not in TEXT_FILE_TYPES:
|
||||
raise ValueError(f"Unsupported file type: {extension}")
|
||||
return parse_text_file_to_record(resolved_path, silent_errors)
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
from langchain_core.documents import Document
|
||||
from typing import List
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
from langflow.utils.constants import LOADERS_INFO
|
||||
|
||||
|
||||
class FileLoaderComponent(CustomComponent):
|
||||
display_name: str = "File Loader"
|
||||
description: str = "Generic File Loader"
|
||||
beta = True
|
||||
|
||||
def build_config(self):
|
||||
loader_options = ["Automatic"] + [loader_info["name"] for loader_info in LOADERS_INFO]
|
||||
|
|
@ -38,6 +39,7 @@ class FileLoaderComponent(CustomComponent):
|
|||
"srt",
|
||||
"eml",
|
||||
"md",
|
||||
"mdx",
|
||||
"pptx",
|
||||
"docx",
|
||||
],
|
||||
|
|
@ -55,6 +57,7 @@ class FileLoaderComponent(CustomComponent):
|
|||
".srt",
|
||||
".eml",
|
||||
".md",
|
||||
".mdx",
|
||||
".pptx",
|
||||
".docx",
|
||||
],
|
||||
|
|
@ -71,10 +74,10 @@ class FileLoaderComponent(CustomComponent):
|
|||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(self, file_path: str, loader: str) -> Document:
|
||||
def build(self, file_path: str, loader: str) -> List[Record]:
|
||||
file_type = file_path.split(".")[-1]
|
||||
|
||||
# Mapeie o nome do loader selecionado para suas informações
|
||||
# Map the loader to the correct loader class
|
||||
selected_loader_info = None
|
||||
for loader_info in LOADERS_INFO:
|
||||
if loader_info["name"] == loader:
|
||||
|
|
@ -85,7 +88,7 @@ class FileLoaderComponent(CustomComponent):
|
|||
raise ValueError(f"Loader {loader} not found in the loader info list")
|
||||
|
||||
if loader == "Automatic":
|
||||
# Determine o loader automaticamente com base na extensão do arquivo
|
||||
# Determine the loader based on the file type
|
||||
default_loader_info = None
|
||||
for info in LOADERS_INFO:
|
||||
if "defaultFor" in info and file_type in info["defaultFor"]:
|
||||
|
|
@ -103,11 +106,12 @@ class FileLoaderComponent(CustomComponent):
|
|||
module_name, class_name = loader_import.rsplit(".", 1)
|
||||
|
||||
try:
|
||||
# Importe o loader dinamicamente
|
||||
# Import the loader class
|
||||
loader_module = __import__(module_name, fromlist=[class_name])
|
||||
loader_instance = getattr(loader_module, class_name)
|
||||
except ImportError as e:
|
||||
raise ValueError(f"Loader {loader} could not be imported\nLoader info:\n{selected_loader_info}") from e
|
||||
|
||||
result = loader_instance(file_path=file_path)
|
||||
return result.load()
|
||||
docs = result.load()
|
||||
return self.to_records(docs)
|
||||
25
src/backend/langflow/components/data/URL.py
Normal file
25
src/backend/langflow/components/data/URL.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
from langchain_community.document_loaders.web_base import WebBaseLoader
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class URLComponent(CustomComponent):
|
||||
display_name = "URL"
|
||||
description = "Load URLs and convert them to records."
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"urls": {"display_name": "URL"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
urls: list[str],
|
||||
) -> list[Record]:
|
||||
loader = WebBaseLoader(web_paths=urls)
|
||||
docs = loader.load()
|
||||
records = self.to_records(docs)
|
||||
return records
|
||||
7
src/backend/langflow/components/data/__init__.py
Normal file
7
src/backend/langflow/components/data/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from .APIRequest import APIRequest
|
||||
from .Directory import DirectoryComponent
|
||||
from .File import FileComponent
|
||||
from .FileLoader import FileLoaderComponent
|
||||
from .URL import URLComponent
|
||||
|
||||
__all__ = ["APIRequest", "DirectoryComponent", "FileComponent", "FileLoaderComponent", "URLComponent"]
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain.docstore.document import Document
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
class DirectoryLoaderComponent(CustomComponent):
|
||||
display_name = "DirectoryLoader"
|
||||
description = "Load from a directory."
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"glob": {"display_name": "Glob Pattern", "value": "**/*.txt"},
|
||||
"load_hidden": {"display_name": "Load Hidden Files", "value": False, "advanced": True},
|
||||
"max_concurrency": {"display_name": "Max Concurrency", "value": 10, "advanced": True},
|
||||
"metadata": {"display_name": "Metadata", "value": {}},
|
||||
"path": {"display_name": "Local Directory"},
|
||||
"recursive": {"display_name": "Recursive", "value": True, "advanced": True},
|
||||
"silent_errors": {"display_name": "Silent Errors", "value": False, "advanced": True},
|
||||
"use_multithreading": {"display_name": "Use Multithreading", "value": True, "advanced": True},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
glob: str,
|
||||
path: str,
|
||||
load_hidden: Optional[bool] = False,
|
||||
max_concurrency: Optional[int] = 10,
|
||||
metadata: Optional[dict] = {},
|
||||
recursive: Optional[bool] = True,
|
||||
silent_errors: Optional[bool] = False,
|
||||
use_multithreading: Optional[bool] = True,
|
||||
) -> Document:
|
||||
return Document(
|
||||
glob=glob,
|
||||
path=path,
|
||||
load_hidden=load_hidden,
|
||||
max_concurrency=max_concurrency,
|
||||
metadata=metadata,
|
||||
recursive=recursive,
|
||||
silent_errors=silent_errors,
|
||||
use_multithreading=use_multithreading,
|
||||
)
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
from typing import List
|
||||
|
||||
from langchain import document_loaders
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class UrlLoaderComponent(CustomComponent):
|
||||
display_name: str = "Url Loader"
|
||||
description: str = "Generic Url Loader Component"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"web_path": {
|
||||
"display_name": "Url",
|
||||
"required": True,
|
||||
},
|
||||
"loader": {
|
||||
"display_name": "Loader",
|
||||
"is_list": True,
|
||||
"required": True,
|
||||
"options": [
|
||||
"AZLyricsLoader",
|
||||
"CollegeConfidentialLoader",
|
||||
"GitbookLoader",
|
||||
"HNLoader",
|
||||
"IFixitLoader",
|
||||
"IMSDbLoader",
|
||||
"WebBaseLoader",
|
||||
],
|
||||
"value": "WebBaseLoader",
|
||||
},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(self, web_path: str, loader: str) -> List[Document]:
|
||||
try:
|
||||
loader_instance = getattr(document_loaders, loader)(web_path=web_path)
|
||||
except Exception as e:
|
||||
raise ValueError(f"No loader found for: {web_path}") from e
|
||||
docs = loader_instance.load()
|
||||
avg_length = sum(len(doc.page_content) for doc in docs if hasattr(doc, "page_content")) / len(docs)
|
||||
self.status = f"""{len(docs)} documents)
|
||||
\nAvg. Document Length (characters): {int(avg_length)}
|
||||
Documents: {docs[:3]}..."""
|
||||
return docs
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.embeddings import BedrockEmbeddings
|
||||
from langchain.embeddings.base import Embeddings
|
||||
from langchain_community.embeddings import BedrockEmbeddings
|
||||
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
|
|
@ -13,7 +15,6 @@ class AmazonBedrockEmeddingsComponent(CustomComponent):
|
|||
display_name: str = "Amazon Bedrock Embeddings"
|
||||
description: str = "Embeddings model from Amazon Bedrock."
|
||||
documentation = "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/bedrock"
|
||||
beta = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class AzureOpenAIEmbeddingsComponent(CustomComponent):
|
|||
description: str = "Embeddings model from Azure OpenAI."
|
||||
documentation: str = "https://python.langchain.com/docs/integrations/text_embedding/azureopenai"
|
||||
beta = False
|
||||
icon = "Azure"
|
||||
|
||||
API_VERSION_OPTIONS = [
|
||||
"2022-12-01",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class HuggingFaceEmbeddingsComponent(CustomComponent):
|
|||
documentation = (
|
||||
"https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/sentence_transformers"
|
||||
)
|
||||
icon = "HuggingFace"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
from typing import Dict, Optional
|
||||
|
||||
from langchain_community.embeddings.huggingface import HuggingFaceInferenceAPIEmbeddings
|
||||
from langflow import CustomComponent
|
||||
from pydantic.v1.types import SecretStr
|
||||
|
||||
|
||||
class HuggingFaceInferenceAPIEmbeddingsComponent(CustomComponent):
|
||||
display_name = "HuggingFaceInferenceAPIEmbeddings"
|
||||
description = "HuggingFace sentence_transformers embedding models, API version."
|
||||
documentation = "https://github.com/huggingface/text-embeddings-inference"
|
||||
icon = "HuggingFace"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"api_key": {"display_name": "API Key", "password": True, "advanced": True},
|
||||
"api_url": {"display_name": "API URL", "advanced": True},
|
||||
"model_name": {"display_name": "Model Name"},
|
||||
"cache_folder": {"display_name": "Cache Folder", "advanced": True},
|
||||
"encode_kwargs": {"display_name": "Encode Kwargs", "advanced": True, "field_type": "dict"},
|
||||
"model_kwargs": {"display_name": "Model Kwargs", "field_type": "dict", "advanced": True},
|
||||
"multi_process": {"display_name": "Multi Process", "advanced": True},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
api_key: Optional[str] = "",
|
||||
api_url: str = "http://localhost:8080",
|
||||
model_name: str = "BAAI/bge-large-en-v1.5",
|
||||
cache_folder: Optional[str] = None,
|
||||
encode_kwargs: Optional[Dict] = {},
|
||||
model_kwargs: Optional[Dict] = {},
|
||||
multi_process: bool = False,
|
||||
) -> HuggingFaceInferenceAPIEmbeddings:
|
||||
if api_key:
|
||||
secret_api_key = SecretStr(api_key)
|
||||
else:
|
||||
raise ValueError("API Key is required")
|
||||
return HuggingFaceInferenceAPIEmbeddings(
|
||||
api_key=secret_api_key,
|
||||
api_url=api_url,
|
||||
model_name=model_name,
|
||||
)
|
||||
|
|
@ -13,7 +13,6 @@ class OllamaEmbeddingsComponent(CustomComponent):
|
|||
display_name: str = "Ollama Embeddings"
|
||||
description: str = "Embeddings model from Ollama."
|
||||
documentation = "https://python.langchain.com/docs/integrations/text_embedding/ollama"
|
||||
beta = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
from langchain_openai.embeddings.base import OpenAIEmbeddings
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import NestedDict
|
||||
from pydantic.v1.types import SecretStr
|
||||
|
||||
|
||||
class OpenAIEmbeddingsComponent(CustomComponent):
|
||||
|
|
@ -42,7 +42,11 @@ class OpenAIEmbeddingsComponent(CustomComponent):
|
|||
"advanced": True,
|
||||
},
|
||||
"max_retries": {"display_name": "Max Retries", "advanced": True},
|
||||
"model": {"display_name": "Model", "advanced": True},
|
||||
"model": {
|
||||
"display_name": "Model",
|
||||
"advanced": False,
|
||||
"options": ["text-embedding-3-small", "text-embedding-3-large", "text-embedding-ada-002"],
|
||||
},
|
||||
"model_kwargs": {"display_name": "Model Kwargs", "advanced": True},
|
||||
"openai_api_base": {"display_name": "OpenAI API Base", "password": True, "advanced": True},
|
||||
"openai_api_key": {"display_name": "OpenAI API Key", "password": True},
|
||||
|
|
@ -63,7 +67,7 @@ class OpenAIEmbeddingsComponent(CustomComponent):
|
|||
},
|
||||
"skip_empty": {"display_name": "Skip Empty", "advanced": True},
|
||||
"tiktoken_model_name": {"display_name": "TikToken Model Name"},
|
||||
"tikToken_enable": {"display_name": "TikToken Enable"},
|
||||
"tikToken_enable": {"display_name": "TikToken Enable", "advanced": True},
|
||||
}
|
||||
|
||||
def build(
|
||||
|
|
@ -74,10 +78,10 @@ class OpenAIEmbeddingsComponent(CustomComponent):
|
|||
disallowed_special: List[str] = ["all"],
|
||||
chunk_size: int = 1000,
|
||||
client: Optional[Any] = None,
|
||||
deployment: str = "text-embedding-ada-002",
|
||||
deployment: str = "text-embedding-3-small",
|
||||
embedding_ctx_length: int = 8191,
|
||||
max_retries: int = 6,
|
||||
model: str = "text-embedding-ada-002",
|
||||
model: str = "text-embedding-3-small",
|
||||
model_kwargs: NestedDict = {},
|
||||
openai_api_base: Optional[str] = None,
|
||||
openai_api_key: Optional[str] = "",
|
||||
|
|
@ -88,15 +92,21 @@ class OpenAIEmbeddingsComponent(CustomComponent):
|
|||
request_timeout: Optional[float] = None,
|
||||
show_progress_bar: bool = False,
|
||||
skip_empty: bool = False,
|
||||
tikToken_enable: bool = True,
|
||||
tiktoken_enable: bool = True,
|
||||
tiktoken_model_name: Optional[str] = None,
|
||||
) -> Union[OpenAIEmbeddings, Callable]:
|
||||
# This is to avoid errors with Vector Stores (e.g Chroma)
|
||||
if disallowed_special == ["all"]:
|
||||
disallowed_special = "all" # type: ignore
|
||||
|
||||
api_key = SecretStr(openai_api_key) if openai_api_key else None
|
||||
|
||||
return OpenAIEmbeddings(
|
||||
tiktoken_enabled=tikToken_enable,
|
||||
tiktoken_enabled=tiktoken_enable,
|
||||
default_headers=default_headers,
|
||||
default_query=default_query,
|
||||
allowed_special=set(allowed_special),
|
||||
disallowed_special=set(disallowed_special),
|
||||
disallowed_special="all",
|
||||
chunk_size=chunk_size,
|
||||
client=client,
|
||||
deployment=deployment,
|
||||
|
|
@ -105,7 +115,7 @@ class OpenAIEmbeddingsComponent(CustomComponent):
|
|||
model=model,
|
||||
model_kwargs=model_kwargs,
|
||||
base_url=openai_api_base,
|
||||
api_key=openai_api_key,
|
||||
api_key=api_key,
|
||||
openai_api_type=openai_api_type,
|
||||
api_version=openai_api_version,
|
||||
organization=openai_organization,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain.embeddings import VertexAIEmbeddings
|
||||
from langchain_community.embeddings import VertexAIEmbeddings
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
|
|
@ -9,17 +9,45 @@ class VertexAIEmbeddingsComponent(CustomComponent):
|
|||
|
||||
def build_config(self):
|
||||
return {
|
||||
"credentials": {"display_name": "Credentials", "value": "", "file_types": [".json"], "field_type": "file"},
|
||||
"instance": {"display_name": "instance", "advanced": True, "field_type": "dict"},
|
||||
"location": {"display_name": "Location", "value": "us-central1", "advanced": True},
|
||||
"credentials": {
|
||||
"display_name": "Credentials",
|
||||
"value": "",
|
||||
"file_types": [".json"],
|
||||
"field_type": "file",
|
||||
},
|
||||
"instance": {
|
||||
"display_name": "instance",
|
||||
"advanced": True,
|
||||
"field_type": "dict",
|
||||
},
|
||||
"location": {
|
||||
"display_name": "Location",
|
||||
"value": "us-central1",
|
||||
"advanced": True,
|
||||
},
|
||||
"max_output_tokens": {"display_name": "Max Output Tokens", "value": 128},
|
||||
"max_retries": {"display_name": "Max Retries", "value": 6, "advanced": True},
|
||||
"model_name": {"display_name": "Model Name", "value": "textembedding-gecko"},
|
||||
"max_retries": {
|
||||
"display_name": "Max Retries",
|
||||
"value": 6,
|
||||
"advanced": True,
|
||||
},
|
||||
"model_name": {
|
||||
"display_name": "Model Name",
|
||||
"value": "textembedding-gecko",
|
||||
},
|
||||
"n": {"display_name": "N", "value": 1, "advanced": True},
|
||||
"project": {"display_name": "Project", "advanced": True},
|
||||
"request_parallelism": {"display_name": "Request Parallelism", "value": 5, "advanced": True},
|
||||
"request_parallelism": {
|
||||
"display_name": "Request Parallelism",
|
||||
"value": 5,
|
||||
"advanced": True,
|
||||
},
|
||||
"stop": {"display_name": "Stop", "advanced": True},
|
||||
"streaming": {"display_name": "Streaming", "value": False, "advanced": True},
|
||||
"streaming": {
|
||||
"display_name": "Streaming",
|
||||
"value": False,
|
||||
"advanced": True,
|
||||
},
|
||||
"temperature": {"display_name": "Temperature", "value": 0.0},
|
||||
"top_k": {"display_name": "Top K", "value": 40, "advanced": True},
|
||||
"top_p": {"display_name": "Top P", "value": 0.95, "advanced": True},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
from .AmazonBedrockEmbeddings import AmazonBedrockEmeddingsComponent
|
||||
from .AzureOpenAIEmbeddings import AzureOpenAIEmbeddingsComponent
|
||||
from .CohereEmbeddings import CohereEmbeddingsComponent
|
||||
from .HuggingFaceEmbeddings import HuggingFaceEmbeddingsComponent
|
||||
from .HuggingFaceInferenceAPIEmbeddings import HuggingFaceInferenceAPIEmbeddingsComponent
|
||||
from .OllamaEmbeddings import OllamaEmbeddingsComponent
|
||||
from .OpenAIEmbeddings import OpenAIEmbeddingsComponent
|
||||
from .VertexAIEmbeddings import VertexAIEmbeddingsComponent
|
||||
|
||||
__all__ = [
|
||||
"AmazonBedrockEmeddingsComponent",
|
||||
"AzureOpenAIEmbeddingsComponent",
|
||||
"CohereEmbeddingsComponent",
|
||||
"HuggingFaceEmbeddingsComponent",
|
||||
"HuggingFaceInferenceAPIEmbeddingsComponent",
|
||||
"OllamaEmbeddingsComponent",
|
||||
"OpenAIEmbeddingsComponent",
|
||||
"VertexAIEmbeddingsComponent",
|
||||
]
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
from langflow import CustomComponent
|
||||
from langflow.memory import delete_messages, get_messages
|
||||
|
||||
|
||||
class ClearMessageHistoryComponent(CustomComponent):
|
||||
display_name = "Clear Message History"
|
||||
description = "A component to clear the message history."
|
||||
icon = "ClearMessageHistory"
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"session_id": {
|
||||
"display_name": "Session ID",
|
||||
"info": "The session ID to clear the message history.",
|
||||
}
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
session_id: str,
|
||||
) -> None:
|
||||
delete_messages(session_id=session_id)
|
||||
records = get_messages(session_id=session_id)
|
||||
self.records = records
|
||||
return records
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class ExtractKeyFromRecordComponent(CustomComponent):
|
||||
display_name = "Extract Key From Record"
|
||||
description = "Extracts a key from a record."
|
||||
beta: bool = True
|
||||
|
||||
field_config = {
|
||||
"record": {"display_name": "Record"},
|
||||
"keys": {
|
||||
"display_name": "Keys",
|
||||
"info": "The keys to extract from the record.",
|
||||
"input_types": [],
|
||||
},
|
||||
"silent_error": {
|
||||
"display_name": "Silent Errors",
|
||||
"info": "If True, errors will not be raised.",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(self, record: Record, keys: list[str], silent_error: bool = True) -> Record:
|
||||
"""
|
||||
Extracts the keys from a record.
|
||||
|
||||
Args:
|
||||
record (Record): The record from which to extract the keys.
|
||||
keys (list[str]): The keys to extract from the record.
|
||||
silent_error (bool): If True, errors will not be raised.
|
||||
|
||||
Returns:
|
||||
dict: The extracted keys.
|
||||
"""
|
||||
extracted_keys = {}
|
||||
for key in keys:
|
||||
try:
|
||||
extracted_keys[key] = getattr(record, key)
|
||||
except AttributeError:
|
||||
if not silent_error:
|
||||
raise KeyError(f"The key '{key}' does not exist in the record.")
|
||||
return_record = Record(data=extracted_keys)
|
||||
self.status = return_record
|
||||
return return_record
|
||||
21
src/backend/langflow/components/experimental/GetNotified.py
Normal file
21
src/backend/langflow/components/experimental/GetNotified.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class GetNotifiedComponent(CustomComponent):
|
||||
display_name = "Get Notified"
|
||||
description = "A component to get notified by Notify component."
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"name": {
|
||||
"display_name": "Name",
|
||||
"info": "The name of the notification to listen for.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(self, name: str) -> Record:
|
||||
state = self.get_state(name)
|
||||
self.status = state
|
||||
return state
|
||||
21
src/backend/langflow/components/experimental/ListFlows.py
Normal file
21
src/backend/langflow/components/experimental/ListFlows.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from typing import List
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class ListFlowsComponent(CustomComponent):
|
||||
display_name = "List Flows"
|
||||
description = "A component to list all available flows."
|
||||
icon = "ListFlows"
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {}
|
||||
|
||||
def build(
|
||||
self,
|
||||
) -> List[Record]:
|
||||
flows = self.list_flows()
|
||||
self.status = flows
|
||||
return flows
|
||||
36
src/backend/langflow/components/experimental/MergeRecords.py
Normal file
36
src/backend/langflow/components/experimental/MergeRecords.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class MergeRecordsComponent(CustomComponent):
|
||||
display_name = "Merge Records"
|
||||
description = "Merges records."
|
||||
beta: bool = True
|
||||
|
||||
field_config = {
|
||||
"records": {"display_name": "Records"},
|
||||
}
|
||||
|
||||
def build(self, records: list[Record]) -> Record:
|
||||
if not records:
|
||||
return Record()
|
||||
if len(records) == 1:
|
||||
return records[0]
|
||||
merged_record = Record()
|
||||
for record in records:
|
||||
if merged_record is None:
|
||||
merged_record = record
|
||||
else:
|
||||
merged_record += record
|
||||
self.status = merged_record
|
||||
return merged_record
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
records = [
|
||||
Record(data={"key1": "value1"}),
|
||||
Record(data={"key2": "value2"}),
|
||||
]
|
||||
component = MergeRecordsComponent()
|
||||
result = component.build(records)
|
||||
print(result)
|
||||
41
src/backend/langflow/components/experimental/Notify.py
Normal file
41
src/backend/langflow/components/experimental/Notify.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class NotifyComponent(CustomComponent):
|
||||
display_name = "Notify"
|
||||
description = "A component to generate a notification to Get Notified component."
|
||||
icon = "Notify"
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"name": {"display_name": "Name", "info": "The name of the notification."},
|
||||
"record": {"display_name": "Record", "info": "The record to store."},
|
||||
"append": {
|
||||
"display_name": "Append",
|
||||
"info": "If True, the record will be appended to the notification.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(self, name: str, record: Optional[Record] = None, append: bool = False) -> Record:
|
||||
if record and not isinstance(record, Record):
|
||||
if isinstance(record, str):
|
||||
record = Record(text=record)
|
||||
elif isinstance(record, dict):
|
||||
record = Record(data=record)
|
||||
else:
|
||||
record = Record(text=str(record))
|
||||
elif not record:
|
||||
record = Record(text="")
|
||||
if record:
|
||||
if append:
|
||||
self.append_state(name, record)
|
||||
else:
|
||||
self.update_state(name, record)
|
||||
else:
|
||||
self.status = "No record provided."
|
||||
self.status = record
|
||||
return record
|
||||
59
src/backend/langflow/components/experimental/RunFlow.py
Normal file
59
src/backend/langflow/components/experimental/RunFlow.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import NestedDict, Text
|
||||
from langflow.graph.schema import ResultData
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class RunFlowComponent(CustomComponent):
|
||||
display_name = "Run Flow"
|
||||
description = "A component to run a flow."
|
||||
beta: bool = True
|
||||
|
||||
def get_flow_names(self) -> List[str]:
|
||||
flow_records = self.list_flows()
|
||||
return [flow_record.data["name"] for flow_record in flow_records]
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"multiline": True,
|
||||
},
|
||||
"flow_name": {
|
||||
"display_name": "Flow Name",
|
||||
"info": "The name of the flow to run.",
|
||||
"options": self.get_flow_names,
|
||||
},
|
||||
"tweaks": {
|
||||
"display_name": "Tweaks",
|
||||
"info": "Tweaks to apply to the flow.",
|
||||
},
|
||||
}
|
||||
|
||||
def build_records_from_result_data(self, result_data: ResultData) -> List[Record]:
|
||||
messages = result_data.messages
|
||||
if not messages:
|
||||
return []
|
||||
records = []
|
||||
for message in messages:
|
||||
message_dict = message if isinstance(message, dict) else message.model_dump()
|
||||
record = Record(text=message_dict.get("text", ""), data={"result": result_data})
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
async def build(self, input_value: Text, flow_name: str, tweaks: NestedDict) -> List[Record]:
|
||||
results: List[Optional[ResultData]] = await self.run_flow(
|
||||
input_value=input_value, flow_name=flow_name, tweaks=tweaks
|
||||
)
|
||||
if isinstance(results, list):
|
||||
records = []
|
||||
for result in results:
|
||||
if result:
|
||||
records.extend(self.build_records_from_result_data(result))
|
||||
else:
|
||||
records = self.build_records_from_result_data(results)
|
||||
|
||||
self.status = records
|
||||
return records
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
from langchain_core.runnables import Runnable
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class RunnableExecComponent(CustomComponent):
|
||||
documentation: str = "http://docs.langflow.org/components/custom"
|
||||
display_name = "Runnable Executor"
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"input_key": {
|
||||
"display_name": "Input Key",
|
||||
"info": "The key to use for the input.",
|
||||
},
|
||||
"input_value": {
|
||||
"display_name": "Inputs",
|
||||
"info": "The inputs to pass to the runnable.",
|
||||
},
|
||||
"runnable": {
|
||||
"display_name": "Runnable",
|
||||
"info": "The runnable to execute.",
|
||||
},
|
||||
"output_key": {
|
||||
"display_name": "Output Key",
|
||||
"info": "The key to use for the output.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
input_key: str,
|
||||
input_value: Text,
|
||||
runnable: Runnable,
|
||||
output_key: str = "output",
|
||||
) -> Text:
|
||||
result = runnable.invoke({input_key: input_value})
|
||||
result = result.get(output_key)
|
||||
self.status = result
|
||||
return result
|
||||
69
src/backend/langflow/components/experimental/SQLExecutor.py
Normal file
69
src/backend/langflow/components/experimental/SQLExecutor.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
from langchain_community.tools.sql_database.tool import QuerySQLDataBaseTool
|
||||
from langchain_experimental.sql.base import SQLDatabase
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class SQLExecutorComponent(CustomComponent):
|
||||
display_name = "SQL Executor"
|
||||
description = "Execute SQL query."
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"database_url": {
|
||||
"display_name": "Database URL",
|
||||
"info": "The URL of the database.",
|
||||
},
|
||||
"include_columns": {
|
||||
"display_name": "Include Columns",
|
||||
"info": "Include columns in the result.",
|
||||
},
|
||||
"passthrough": {
|
||||
"display_name": "Passthrough",
|
||||
"info": "If an error occurs, return the query instead of raising an exception.",
|
||||
},
|
||||
"add_error": {
|
||||
"display_name": "Add Error",
|
||||
"info": "Add the error to the result.",
|
||||
},
|
||||
}
|
||||
|
||||
def clean_up_uri(self, uri: str) -> str:
|
||||
if uri.startswith("postgresql://"):
|
||||
uri = uri.replace("postgresql://", "postgres://")
|
||||
return uri.strip()
|
||||
|
||||
def build(
|
||||
self,
|
||||
query: str,
|
||||
database_url: str,
|
||||
include_columns: bool = False,
|
||||
passthrough: bool = False,
|
||||
add_error: bool = False,
|
||||
) -> Text:
|
||||
error = None
|
||||
try:
|
||||
database = SQLDatabase.from_uri(database_url)
|
||||
except Exception as e:
|
||||
raise ValueError(f"An error occurred while connecting to the database: {e}")
|
||||
try:
|
||||
tool = QuerySQLDataBaseTool(db=database)
|
||||
result = tool.run(query, include_columns=include_columns)
|
||||
self.status = result
|
||||
except Exception as e:
|
||||
result = Text(e)
|
||||
self.status = result
|
||||
if not passthrough:
|
||||
raise e
|
||||
error = repr(e)
|
||||
|
||||
if add_error and error is not None:
|
||||
result = f"{result}\n\nError: {error}\n\nQuery: {query}"
|
||||
elif error is not None:
|
||||
# Then we won't add the error to the result
|
||||
# but since we are in passthrough mode, we will return the query
|
||||
result = query
|
||||
|
||||
return result
|
||||
23
src/backend/langflow/components/experimental/__init__.py
Normal file
23
src/backend/langflow/components/experimental/__init__.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from .ClearMessageHistory import ClearMessageHistoryComponent
|
||||
from .ExtractDataFromRecord import ExtractKeyFromRecordComponent
|
||||
from .GetNotified import GetNotifiedComponent
|
||||
from .ListFlows import ListFlowsComponent
|
||||
from .MergeRecords import MergeRecordsComponent
|
||||
from .Notify import NotifyComponent
|
||||
from .RunFlow import RunFlowComponent
|
||||
from .RunnableExecutor import RunnableExecComponent
|
||||
from .SQLExecutor import SQLExecutorComponent
|
||||
|
||||
__all__ = [
|
||||
"ClearMessageHistoryComponent",
|
||||
"ExtractKeyFromRecordComponent",
|
||||
"GetNotifiedComponent",
|
||||
"ListFlowsComponent",
|
||||
"MergeRecordsComponent",
|
||||
"MessageHistoryComponent",
|
||||
"NotifyComponent",
|
||||
"RunFlowComponent",
|
||||
"RunnableExecComponent",
|
||||
"SQLExecutorComponent",
|
||||
"TextToRecordComponent",
|
||||
]
|
||||
|
|
@ -3,7 +3,10 @@ from langflow.field_typing import Data
|
|||
|
||||
|
||||
class Component(CustomComponent):
|
||||
display_name = "Custom Component"
|
||||
description = "Use as a template to create your own component."
|
||||
documentation: str = "http://docs.langflow.org/components/custom"
|
||||
icon = "custom_components"
|
||||
|
||||
def build_config(self):
|
||||
return {"param": {"display_name": "Parameter"}}
|
||||
22
src/backend/langflow/components/helpers/DocumentToRecord.py
Normal file
22
src/backend/langflow/components/helpers/DocumentToRecord.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from typing import List
|
||||
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class DocumentToRecordComponent(CustomComponent):
|
||||
display_name = "Documents to Records"
|
||||
description = "Convert documents to records."
|
||||
|
||||
field_config = {
|
||||
"documents": {"display_name": "Documents"},
|
||||
}
|
||||
|
||||
def build(self, documents: List[Document]) -> List[Record]:
|
||||
if isinstance(documents, Document):
|
||||
documents = [documents]
|
||||
records = [Record.from_document(document) for document in documents]
|
||||
self.status = records
|
||||
return records
|
||||
31
src/backend/langflow/components/helpers/IDGenerator.py
Normal file
31
src/backend/langflow/components/helpers/IDGenerator.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import uuid
|
||||
from typing import Any, Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class UUIDGeneratorComponent(CustomComponent):
|
||||
documentation: str = "http://docs.langflow.org/components/custom"
|
||||
display_name = "Unique ID Generator"
|
||||
description = "Generates a unique ID."
|
||||
|
||||
def update_build_config(
|
||||
self,
|
||||
build_config: dict,
|
||||
field_value: Any,
|
||||
field_name: Optional[str] = None,
|
||||
):
|
||||
if field_name == "unique_id":
|
||||
build_config[field_name]["value"] = str(uuid.uuid4())
|
||||
return build_config
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"unique_id": {
|
||||
"display_name": "Value",
|
||||
"real_time_refresh": True,
|
||||
}
|
||||
}
|
||||
|
||||
def build(self, unique_id: str) -> str:
|
||||
return unique_id
|
||||
47
src/backend/langflow/components/helpers/MessageHistory.py
Normal file
47
src/backend/langflow/components/helpers/MessageHistory.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.memory import get_messages
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class MessageHistoryComponent(CustomComponent):
|
||||
display_name = "Message History"
|
||||
description = "Used to retrieve stored messages."
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"sender": {
|
||||
"options": ["Machine", "User", "Machine and User"],
|
||||
"display_name": "Sender Type",
|
||||
},
|
||||
"sender_name": {"display_name": "Sender Name"},
|
||||
"n_messages": {
|
||||
"display_name": "Number of Messages",
|
||||
"info": "Number of messages to retrieve.",
|
||||
},
|
||||
"session_id": {
|
||||
"display_name": "Session ID",
|
||||
"info": "Session ID of the chat history.",
|
||||
"input_types": ["Text"],
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
sender: Optional[str] = None,
|
||||
sender_name: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
n_messages: int = 5,
|
||||
) -> List[Record]:
|
||||
if sender == "Machine and User":
|
||||
sender = None
|
||||
messages = get_messages(
|
||||
sender=sender,
|
||||
sender_name=sender_name,
|
||||
session_id=session_id,
|
||||
limit=n_messages,
|
||||
)
|
||||
self.status = messages
|
||||
return messages
|
||||
25
src/backend/langflow/components/helpers/PythonFunction.py
Normal file
25
src/backend/langflow/components/helpers/PythonFunction.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from typing import Callable
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Code
|
||||
from langflow.interface.custom.utils import get_function
|
||||
|
||||
|
||||
class PythonFunctionComponent(CustomComponent):
|
||||
display_name = "Python Function"
|
||||
description = "Define a Python function."
|
||||
icon = "Python"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"function_code": {
|
||||
"display_name": "Code",
|
||||
"info": "The code for the function.",
|
||||
"show": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(self, function_code: Code) -> Callable:
|
||||
self.status = function_code
|
||||
func = get_function(function_code)
|
||||
return func
|
||||
35
src/backend/langflow/components/helpers/RecordsAsText.py
Normal file
35
src/backend/langflow/components/helpers/RecordsAsText.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
from langflow.helpers.record import records_to_text
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class RecordsAsTextComponent(CustomComponent):
|
||||
display_name = "Records to Text"
|
||||
description = "Converts Records into single piece of text using a template."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"records": {
|
||||
"display_name": "Records",
|
||||
"info": "The records to convert to text.",
|
||||
},
|
||||
"template": {
|
||||
"display_name": "Template",
|
||||
"info": "The template to use for formatting the records. It can contain the keys {text}, {data} or any other key in the Record.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
records: list[Record],
|
||||
template: str = "Text: {text}\nData: {data}",
|
||||
) -> Text:
|
||||
if not records:
|
||||
return ""
|
||||
if isinstance(records, Record):
|
||||
records = [records]
|
||||
|
||||
result_string = records_to_text(template, records)
|
||||
self.status = result_string
|
||||
return result_string
|
||||
25
src/backend/langflow/components/helpers/TextToRecord.py
Normal file
25
src/backend/langflow/components/helpers/TextToRecord.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class TextToRecordComponent(CustomComponent):
|
||||
display_name = "Text to Record"
|
||||
description = "A component to create a record from Text."
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"data": {
|
||||
"display_name": "Data",
|
||||
"info": "The data to convert to a record.",
|
||||
"input_types": ["Text"],
|
||||
}
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
data: dict,
|
||||
) -> Record:
|
||||
return_record = Record(data=data)
|
||||
self.status = return_record
|
||||
return return_record
|
||||
39
src/backend/langflow/components/helpers/UpdateRecord.py
Normal file
39
src/backend/langflow/components/helpers/UpdateRecord.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from langflow import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class UpdateRecordComponent(CustomComponent):
|
||||
display_name = "Update Record"
|
||||
description = "Updates a record with new data."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"record": {
|
||||
"display_name": "Record",
|
||||
"info": "The record to update.",
|
||||
},
|
||||
"new_data": {
|
||||
"display_name": "New Data",
|
||||
"info": "The new data to update the record with.",
|
||||
"input_types": ["Text"],
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
record: Record,
|
||||
new_data: dict,
|
||||
) -> Record:
|
||||
"""
|
||||
Updates a record with new data.
|
||||
|
||||
Args:
|
||||
record (Record): The record to update.
|
||||
new_data (dict): The new data to update the record with.
|
||||
|
||||
Returns:
|
||||
Record: The updated record.
|
||||
"""
|
||||
record.data.update(new_data)
|
||||
self.status = record
|
||||
return record
|
||||
19
src/backend/langflow/components/helpers/__init__.py
Normal file
19
src/backend/langflow/components/helpers/__init__.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from .CustomComponent import Component
|
||||
from .DocumentToRecord import DocumentToRecordComponent
|
||||
from .IDGenerator import UUIDGeneratorComponent
|
||||
from .MessageHistory import MessageHistoryComponent
|
||||
from .PythonFunction import PythonFunctionComponent
|
||||
from .RecordsAsText import RecordsAsTextComponent
|
||||
from .TextToRecord import TextToRecordComponent
|
||||
from .UpdateRecord import UpdateRecordComponent
|
||||
|
||||
__all__ = [
|
||||
"Component",
|
||||
"UpdateRecordComponent",
|
||||
"DocumentToRecordComponent",
|
||||
"UUIDGeneratorComponent",
|
||||
"PythonFunctionComponent",
|
||||
"RecordsAsTextComponent",
|
||||
"TextToRecordComponent",
|
||||
"MessageHistoryComponent",
|
||||
]
|
||||
27
src/backend/langflow/components/inputs/ChatInput.py
Normal file
27
src/backend/langflow/components/inputs/ChatInput.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
from typing import Optional, Union
|
||||
|
||||
from langflow.base.io.chat import ChatComponent
|
||||
from langflow.field_typing import Text
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class ChatInput(ChatComponent):
|
||||
display_name = "Chat Input"
|
||||
description = "Used to get user input from the chat."
|
||||
icon = "ChatInput"
|
||||
|
||||
def build(
|
||||
self,
|
||||
sender: Optional[str] = "User",
|
||||
sender_name: Optional[str] = "User",
|
||||
input_value: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
return_record: Optional[bool] = False,
|
||||
) -> Union[Text, Record]:
|
||||
return super().build(
|
||||
sender=sender,
|
||||
sender_name=sender_name,
|
||||
input_value=input_value,
|
||||
session_id=session_id,
|
||||
return_record=return_record,
|
||||
)
|
||||
33
src/backend/langflow/components/inputs/Prompt.py
Normal file
33
src/backend/langflow/components/inputs/Prompt.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import Prompt, TemplateField, Text
|
||||
|
||||
|
||||
class PromptComponent(CustomComponent):
|
||||
display_name: str = "Prompt"
|
||||
description: str = "A component for creating prompts using templates"
|
||||
icon = "terminal-square"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"template": TemplateField(display_name="Template"),
|
||||
"code": TemplateField(advanced=True),
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
template: Prompt,
|
||||
**kwargs,
|
||||
) -> Text:
|
||||
from langflow.base.prompts.utils import dict_values_to_string
|
||||
|
||||
prompt_template = PromptTemplate.from_template(Text(template))
|
||||
kwargs = dict_values_to_string(kwargs)
|
||||
kwargs = {k: "\n".join(v) if isinstance(v, list) else v for k, v in kwargs.items()}
|
||||
try:
|
||||
formated_prompt = prompt_template.format(**kwargs)
|
||||
except Exception as exc:
|
||||
raise ValueError(f"Error formatting prompt: {exc}") from exc
|
||||
self.status = f'Prompt:\n"{formated_prompt}"'
|
||||
return formated_prompt
|
||||
12
src/backend/langflow/components/inputs/TextInput.py
Normal file
12
src/backend/langflow/components/inputs/TextInput.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow.base.io.text import TextComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class TextInput(TextComponent):
|
||||
display_name = "Text Input"
|
||||
description = "Used to pass text input to the next component."
|
||||
|
||||
def build(self, input_value: Optional[str] = "") -> Text:
|
||||
return super().build(input_value=input_value)
|
||||
5
src/backend/langflow/components/inputs/__init__.py
Normal file
5
src/backend/langflow/components/inputs/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .ChatInput import ChatInput
|
||||
from .Prompt import PromptComponent
|
||||
from .TextInput import TextInput
|
||||
|
||||
__all__ = ["ChatInput", "TextInput", "PromptComponent"]
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from langflow import CustomComponent
|
||||
from typing import Union, Callable
|
||||
from typing import Callable, Union
|
||||
|
||||
from langchain_community.utilities.google_search import GoogleSearchAPIWrapper
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class GoogleSearchAPIWrapperComponent(CustomComponent):
|
||||
|
|
@ -18,4 +19,4 @@ class GoogleSearchAPIWrapperComponent(CustomComponent):
|
|||
google_api_key: str,
|
||||
google_cse_id: str,
|
||||
) -> Union[GoogleSearchAPIWrapper, Callable]:
|
||||
return GoogleSearchAPIWrapper(google_api_key=google_api_key, google_cse_id=google_cse_id)
|
||||
return GoogleSearchAPIWrapper(google_api_key=google_api_key, google_cse_id=google_cse_id) # type: ignore
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
from langflow import CustomComponent
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict
|
||||
|
||||
# Assuming the existence of GoogleSerperAPIWrapper class in the serper module
|
||||
# If this class does not exist, you would need to create it or import the appropriate class from another module
|
||||
from langchain_community.utilities.google_serper import GoogleSerperAPIWrapper
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class GoogleSerperAPIWrapperComponent(CustomComponent):
|
||||
display_name = "GoogleSerperAPIWrapper"
|
||||
|
|
@ -17,20 +18,23 @@ class GoogleSerperAPIWrapperComponent(CustomComponent):
|
|||
"show": True,
|
||||
"multiline": False,
|
||||
"password": False,
|
||||
"name": "result_key_for_type",
|
||||
"advanced": False,
|
||||
"dynamic": False,
|
||||
"info": "",
|
||||
"field_type": "dict",
|
||||
"list": False,
|
||||
"value": {"news": "news", "places": "places", "images": "images", "search": "organic"},
|
||||
"value": {
|
||||
"news": "news",
|
||||
"places": "places",
|
||||
"images": "images",
|
||||
"search": "organic",
|
||||
},
|
||||
},
|
||||
"serper_api_key": {
|
||||
"display_name": "Serper API Key",
|
||||
"show": True,
|
||||
"multiline": False,
|
||||
"password": True,
|
||||
"name": "serper_api_key",
|
||||
"advanced": False,
|
||||
"dynamic": False,
|
||||
"info": "",
|
||||
|
|
@ -42,6 +46,5 @@ class GoogleSerperAPIWrapperComponent(CustomComponent):
|
|||
def build(
|
||||
self,
|
||||
serper_api_key: str,
|
||||
result_key_for_type: Optional[Dict[str, str]] = None,
|
||||
) -> GoogleSerperAPIWrapper:
|
||||
return GoogleSerperAPIWrapper(result_key_for_type=result_key_for_type, serper_api_key=serper_api_key)
|
||||
return GoogleSerperAPIWrapper(serper_api_key=serper_api_key)
|
||||
|
|
@ -20,7 +20,6 @@ class JSONDocumentBuilder(CustomComponent):
|
|||
display_name: str = "JSON Document Builder"
|
||||
description: str = "Build a Document containing a JSON object using a key and another Document page content."
|
||||
output_types: list[str] = ["Document"]
|
||||
beta = True
|
||||
documentation: str = "https://docs.langflow.org/components/utilities#json-document-builder"
|
||||
|
||||
field_config = {
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
from langchain_experimental.sql.base import SQLDatabase
|
||||
|
||||
from langflow import CustomComponent
|
||||
|
||||
|
||||
class SQLDatabaseComponent(CustomComponent):
|
||||
display_name = "SQLDatabase"
|
||||
description = "SQL Database"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"uri": {"display_name": "URI", "info": "URI to the database."},
|
||||
}
|
||||
|
||||
def clean_up_uri(self, uri: str) -> str:
|
||||
if uri.startswith("postgresql://"):
|
||||
uri = uri.replace("postgresql://", "postgres://")
|
||||
return uri.strip()
|
||||
|
||||
def build(self, uri: str) -> SQLDatabase:
|
||||
uri = self.clean_up_uri(uri)
|
||||
return SQLDatabase.from_uri(uri)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue