Merge remote-tracking branch 'origin/dev' into fix_secret_key
This commit is contained in:
commit
53b9565ac6
134 changed files with 4732 additions and 1946 deletions
|
|
@ -1,10 +1,11 @@
|
|||
import sys
|
||||
import time
|
||||
import httpx
|
||||
from langflow.services.manager import initialize_settings_manager
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.utils.util import get_number_of_workers
|
||||
from multiprocess import Process # type: ignore
|
||||
from langflow.services.database.utils import session_getter
|
||||
from langflow.services.manager import initialize_services, initialize_settings_manager
|
||||
from langflow.services.utils import get_db_manager, get_settings_manager
|
||||
|
||||
from multiprocess import Process, cpu_count # type: ignore
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
|
@ -12,15 +13,46 @@ import socket
|
|||
from rich.panel import Panel
|
||||
from rich import box
|
||||
from rich import print as rprint
|
||||
from rich.table import Table
|
||||
import typer
|
||||
from langflow.main import setup_app
|
||||
from langflow.utils.logger import configure, logger
|
||||
import webbrowser
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
def get_number_of_workers(workers=None):
|
||||
if workers == -1 or workers is None:
|
||||
workers = (cpu_count() * 2) + 1
|
||||
logger.debug(f"Number of workers: {workers}")
|
||||
return workers
|
||||
|
||||
|
||||
def display_results(results):
|
||||
"""
|
||||
Display the results of the migration.
|
||||
"""
|
||||
for table_results in results:
|
||||
table = Table(title=f"Migration {table_results.table_name}")
|
||||
table.add_column("Name")
|
||||
table.add_column("Type")
|
||||
table.add_column("Status")
|
||||
|
||||
for result in table_results.results:
|
||||
status = "Success" if result.success else "Failure"
|
||||
color = "green" if result.success else "red"
|
||||
table.add_row(result.name, result.type, f"[{color}]{status}[/{color}]")
|
||||
|
||||
console.print(table)
|
||||
console.print() # Print a new line
|
||||
|
||||
|
||||
def update_settings(
|
||||
config: str,
|
||||
cache: str,
|
||||
|
|
@ -94,7 +126,7 @@ def serve_on_jcloud():
|
|||
|
||||
|
||||
@app.command()
|
||||
def serve(
|
||||
def run(
|
||||
host: str = typer.Option(
|
||||
"127.0.0.1", help="Host to bind the server to.", envvar="LANGFLOW_HOST"
|
||||
),
|
||||
|
|
@ -312,6 +344,43 @@ def run_langflow(host, port, log_level, options, app):
|
|||
sys.exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def superuser(
|
||||
username: str = typer.Option(..., prompt=True, help="Username for the superuser."),
|
||||
password: str = typer.Option(
|
||||
..., prompt=True, hide_input=True, help="Password for the superuser."
|
||||
),
|
||||
):
|
||||
initialize_services()
|
||||
db_manager = get_db_manager()
|
||||
with session_getter(db_manager) as session:
|
||||
from langflow.services.auth.utils import create_super_user
|
||||
|
||||
if create_super_user(session, username, password):
|
||||
# Verify that the superuser was created
|
||||
from langflow.services.database.models.user.user import User
|
||||
|
||||
user = session.query(User).filter(User.username == username).first()
|
||||
if user is None:
|
||||
typer.echo("Superuser creation failed.")
|
||||
return
|
||||
|
||||
typer.echo("Superuser created successfully.")
|
||||
|
||||
else:
|
||||
typer.echo("Superuser creation failed.")
|
||||
|
||||
|
||||
@app.command()
|
||||
def migration(test: bool = typer.Option(False, help="Run migrations in test mode.")):
|
||||
initialize_services()
|
||||
db_manager = get_db_manager()
|
||||
if not test:
|
||||
db_manager.run_migrations()
|
||||
results = db_manager.run_migrations_test()
|
||||
display_results(results)
|
||||
|
||||
|
||||
def main():
|
||||
app()
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ def run_migrations_offline() -> None:
|
|||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
render_as_batch=True,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
|
|
@ -66,7 +67,9 @@ def run_migrations_online() -> None:
|
|||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata, render_as_batch=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
"""Remove FlowStyles table
|
||||
|
||||
Revision ID: 0a534bdfd84b
|
||||
Revises: 4814b6f4abfd
|
||||
Create Date: 2023-08-07 14:09:06.844104
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "0a534bdfd84b"
|
||||
down_revision: Union[str, None] = "4814b6f4abfd"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("flowstyle")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"flowstyle",
|
||||
sa.Column("color", sa.VARCHAR(), nullable=False),
|
||||
sa.Column("emoji", sa.VARCHAR(), nullable=False),
|
||||
sa.Column("flow_id", sa.CHAR(length=32), nullable=True),
|
||||
sa.Column("id", sa.CHAR(length=32), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["flow_id"],
|
||||
["flow.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
"""Adds tables
|
||||
|
||||
Revision ID: 260dbcc8b680
|
||||
Revises:
|
||||
Create Date: 2023-08-27 19:49:02.681355
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "260dbcc8b680"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn)
|
||||
# List existing tables
|
||||
existing_tables = inspector.get_table_names()
|
||||
# Drop 'flowstyle' table if it exists
|
||||
# and other related indices
|
||||
if "flowstyle" in existing_tables:
|
||||
op.drop_table("flowstyle")
|
||||
if "ix_flowstyle_flow_id" in [
|
||||
index["name"] for index in inspector.get_indexes("flowstyle")
|
||||
]:
|
||||
op.drop_index("ix_flowstyle_flow_id", table_name="flowstyle")
|
||||
|
||||
existing_indices_flow = []
|
||||
existing_fks_flow = []
|
||||
if "flow" in existing_tables:
|
||||
existing_indices_flow = [
|
||||
index["name"] for index in inspector.get_indexes("flow")
|
||||
]
|
||||
# Existing foreign keys for the 'flow' table, if it exists
|
||||
existing_fks_flow = [
|
||||
fk["referred_table"] + "." + fk["referred_columns"][0]
|
||||
for fk in inspector.get_foreign_keys("flow")
|
||||
]
|
||||
# Now check if the columns user_id exists in the 'flow' table
|
||||
# If it does not exist, we need to create the foreign key
|
||||
|
||||
if "user" not in existing_tables:
|
||||
op.create_table(
|
||||
"user",
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("username", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("password", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||
sa.Column("is_superuser", sa.Boolean(), nullable=False),
|
||||
sa.Column("create_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("last_login_at", sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_user_username"), ["username"], unique=True
|
||||
)
|
||||
|
||||
if "apikey" not in existing_tables:
|
||||
op.create_table(
|
||||
"apikey",
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("last_used_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("total_uses", sa.Integer(), nullable=False, default=0),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, default=True),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("api_key", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["user.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_apikey_api_key"), ["api_key"], unique=True
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_apikey_name"), ["name"], unique=False)
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_apikey_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
if "flow" not in existing_tables:
|
||||
op.create_table(
|
||||
"flow",
|
||||
sa.Column("data", sa.JSON(), nullable=True),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["user.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
# Conditionally create indices for 'flow' table
|
||||
# if _alembic_tmp_flow exists, then we need to drop it first
|
||||
# This is to deal with SQLite not being able to ROLLBACK
|
||||
# for some unknown reason
|
||||
if "_alembic_tmp_flow" in existing_tables:
|
||||
op.drop_table("_alembic_tmp_flow")
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
flow_columns = [col["name"] for col in inspector.get_columns("flow")]
|
||||
if "user_id" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"user_id",
|
||||
sqlmodel.sql.sqltypes.GUID(),
|
||||
nullable=True, # This should be False, but we need to allow NULL values for now
|
||||
)
|
||||
)
|
||||
if "user.id" not in existing_fks_flow:
|
||||
batch_op.create_foreign_key("fk_flow_user_id", "user", ["user_id"], ["id"])
|
||||
if "ix_flow_description" not in existing_indices_flow:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_flow_description"), ["description"], unique=False
|
||||
)
|
||||
if "ix_flow_name" not in existing_indices_flow:
|
||||
batch_op.create_index(batch_op.f("ix_flow_name"), ["name"], unique=False)
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "ix_flow_user_id" not in existing_indices_flow:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_flow_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn)
|
||||
# List existing tables
|
||||
existing_tables = inspector.get_table_names()
|
||||
if "flow" in existing_tables:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_flow_user_id"))
|
||||
batch_op.drop_index(batch_op.f("ix_flow_name"))
|
||||
batch_op.drop_index(batch_op.f("ix_flow_description"))
|
||||
|
||||
op.drop_table("flow")
|
||||
if "apikey" in existing_tables:
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_apikey_user_id"))
|
||||
batch_op.drop_index(batch_op.f("ix_apikey_name"))
|
||||
batch_op.drop_index(batch_op.f("ix_apikey_api_key"))
|
||||
|
||||
op.drop_table("apikey")
|
||||
if "user" in existing_tables:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f("ix_user_username"))
|
||||
|
||||
op.drop_table("user")
|
||||
|
||||
if "flowstyle" in existing_tables:
|
||||
op.drop_table("flowstyle")
|
||||
|
||||
if "component" in existing_tables:
|
||||
op.drop_table("component")
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"""Add Flow table
|
||||
|
||||
Revision ID: 4814b6f4abfd
|
||||
Revises:
|
||||
Create Date: 2023-08-05 17:47:42.879824
|
||||
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "4814b6f4abfd"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
# This suppress is used to not break the migration if the table already exists.
|
||||
with contextlib.suppress(sa.exc.OperationalError):
|
||||
op.create_table(
|
||||
"flow",
|
||||
sa.Column("data", sa.JSON(), nullable=True),
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_flow_description"), "flow", ["description"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_flow_name"), "flow", ["name"], unique=False)
|
||||
with contextlib.suppress(sa.exc.OperationalError):
|
||||
op.create_table(
|
||||
"flowstyle",
|
||||
sa.Column("color", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("emoji", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("flow_id", sqlmodel.sql.sqltypes.GUID(), nullable=True),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["flow_id"],
|
||||
["flow.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("flowstyle")
|
||||
op.drop_index(op.f("ix_flow_name"), table_name="flow")
|
||||
op.drop_index(op.f("ix_flow_description"), table_name="flow")
|
||||
op.drop_table("flow")
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -6,6 +6,9 @@ from langflow.api.v1 import (
|
|||
validate_router,
|
||||
flows_router,
|
||||
component_router,
|
||||
users_router,
|
||||
api_key_router,
|
||||
login_router,
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
|
|
@ -16,3 +19,6 @@ router.include_router(endpoints_router)
|
|||
router.include_router(validate_router)
|
||||
router.include_router(component_router)
|
||||
router.include_router(flows_router)
|
||||
router.include_router(users_router)
|
||||
router.include_router(api_key_router)
|
||||
router.include_router(login_router)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ from langflow.api.v1.validate import router as validate_router
|
|||
from langflow.api.v1.chat import router as chat_router
|
||||
from langflow.api.v1.flows import router as flows_router
|
||||
from langflow.api.v1.components import router as component_router
|
||||
from langflow.api.v1.users import router as users_router
|
||||
from langflow.api.v1.api_key import router as api_key_router
|
||||
from langflow.api.v1.login import router as login_router
|
||||
|
||||
__all__ = [
|
||||
"chat_router",
|
||||
|
|
@ -10,4 +13,7 @@ __all__ = [
|
|||
"component_router",
|
||||
"validate_router",
|
||||
"flows_router",
|
||||
"users_router",
|
||||
"api_key_router",
|
||||
"login_router",
|
||||
]
|
||||
|
|
|
|||
61
src/backend/langflow/api/v1/api_key.py
Normal file
61
src/backend/langflow/api/v1/api_key.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from uuid import UUID
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from langflow.api.v1.schemas import ApiKeysResponse
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.api_key.api_key import (
|
||||
ApiKeyCreate,
|
||||
UnmaskedApiKeyRead,
|
||||
)
|
||||
|
||||
# Assuming you have these methods in your service layer
|
||||
from langflow.services.database.models.api_key.crud import (
|
||||
get_api_keys,
|
||||
create_api_key,
|
||||
delete_api_key,
|
||||
)
|
||||
from langflow.services.database.models.user.user import User
|
||||
from langflow.services.utils import get_session
|
||||
from sqlmodel import Session
|
||||
|
||||
|
||||
router = APIRouter(tags=["APIKey"], prefix="/api_key")
|
||||
|
||||
|
||||
@router.get("/", response_model=ApiKeysResponse)
|
||||
def get_api_keys_route(
|
||||
db: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
try:
|
||||
user_id = current_user.id
|
||||
keys = get_api_keys(db, user_id)
|
||||
|
||||
return ApiKeysResponse(total_count=len(keys), user_id=user_id, api_keys=keys)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/", response_model=UnmaskedApiKeyRead)
|
||||
def create_api_key_route(
|
||||
req: ApiKeyCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
user_id = current_user.id
|
||||
return create_api_key(db, req, user_id=user_id)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.delete("/{api_key_id}")
|
||||
def delete_api_key_route(
|
||||
api_key_id: UUID,
|
||||
current_user=Depends(get_current_active_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
try:
|
||||
delete_api_key(db, api_key_id)
|
||||
return {"detail": "API Key deleted"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
from typing import Optional
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
|
|
@ -20,7 +21,8 @@ class FrontendNodeRequest(FrontendNode):
|
|||
class ValidatePromptRequest(BaseModel):
|
||||
name: str
|
||||
template: str
|
||||
frontend_node: FrontendNodeRequest
|
||||
#optional for tweak call
|
||||
frontend_node: Optional[FrontendNodeRequest]
|
||||
|
||||
|
||||
# Build ValidationResponse class for {"imports": {"errors": []}, "function": {"errors": []}}
|
||||
|
|
@ -39,7 +41,8 @@ class CodeValidationResponse(BaseModel):
|
|||
|
||||
class PromptValidationResponse(BaseModel):
|
||||
input_variables: list
|
||||
frontend_node: FrontendNodeRequest
|
||||
#object return for tweak call
|
||||
frontend_node: FrontendNodeRequest | object
|
||||
|
||||
|
||||
INVALID_CHARACTERS = {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketException, status
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
HTTPException,
|
||||
Query,
|
||||
WebSocket,
|
||||
WebSocketException,
|
||||
status,
|
||||
)
|
||||
from fastapi.responses import StreamingResponse
|
||||
from langflow.api.utils import build_input_keys_response
|
||||
from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, StreamData
|
||||
|
||||
from langflow.services import service_manager, ServiceType
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.services.auth.utils import get_current_active_user, get_current_user
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.utils.logger import logger
|
||||
from cachetools import LRUCache
|
||||
from sqlmodel import Session
|
||||
|
||||
router = APIRouter(tags=["Chat"])
|
||||
|
||||
|
|
@ -14,9 +25,17 @@ flow_data_store: LRUCache = LRUCache(maxsize=10)
|
|||
|
||||
|
||||
@router.websocket("/chat/{client_id}")
|
||||
async def chat(client_id: str, websocket: WebSocket):
|
||||
async def chat(
|
||||
client_id: str,
|
||||
websocket: WebSocket,
|
||||
token: str = Query(...),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Websocket endpoint for chat."""
|
||||
try:
|
||||
user = await get_current_user(token, db)
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
chat_manager = service_manager.get(ServiceType.CHAT_MANAGER)
|
||||
if client_id in chat_manager.in_memory_cache:
|
||||
await chat_manager.handle_websocket(client_id, websocket)
|
||||
|
|
@ -32,7 +51,9 @@ async def chat(client_id: str, websocket: WebSocket):
|
|||
|
||||
|
||||
@router.post("/build/init/{flow_id}", response_model=InitResponse, status_code=201)
|
||||
async def init_build(graph_data: dict, flow_id: str):
|
||||
async def init_build(
|
||||
graph_data: dict, flow_id: str, current_user=Depends(get_current_active_user)
|
||||
):
|
||||
"""Initialize the build by storing graph data and returning a unique session ID."""
|
||||
|
||||
try:
|
||||
|
|
@ -54,6 +75,7 @@ async def init_build(graph_data: dict, flow_id: str):
|
|||
flow_data_store[flow_id] = {
|
||||
"graph_data": graph_data,
|
||||
"status": BuildStatus.STARTED,
|
||||
"user_id": current_user.id,
|
||||
}
|
||||
|
||||
return InitResponse(flowId=flow_id)
|
||||
|
|
@ -99,6 +121,7 @@ async def stream_build(flow_id: str):
|
|||
return
|
||||
|
||||
graph_data = flow_data_store[flow_id].get("graph_data")
|
||||
user_id = flow_data_store[flow_id]["user_id"]
|
||||
|
||||
if not graph_data:
|
||||
error_message = "No data provided"
|
||||
|
|
@ -119,7 +142,7 @@ async def stream_build(flow_id: str):
|
|||
"log": f"Building node {vertex.vertex_type}",
|
||||
}
|
||||
yield str(StreamData(event="log", data=log_dict))
|
||||
vertex.build()
|
||||
vertex.build(user_id)
|
||||
params = vertex._built_object_repr()
|
||||
valid = True
|
||||
logger.debug(f"Building node {str(vertex.vertex_type)}")
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
from http import HTTPStatus
|
||||
from typing import Annotated, Optional, Union
|
||||
from typing import Annotated, Any, Optional, Union
|
||||
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.processing.process import process_graph_cached, process_tweaks
|
||||
from langflow.services.database.models.user.user import User
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.utils.logger import logger
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body, status
|
||||
import sqlalchemy as sa
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
|
|
@ -33,12 +35,12 @@ router = APIRouter(tags=["Base"])
|
|||
|
||||
|
||||
@router.get("/all")
|
||||
def get_all():
|
||||
def get_all(current_user: User = Depends(get_current_active_user)):
|
||||
logger.debug("Building langchain types dict")
|
||||
native_components = build_langchain_types_dict()
|
||||
# custom_components is a list of dicts
|
||||
# need to merge all the keys into one dict
|
||||
custom_components_from_file = {}
|
||||
custom_components_from_file: dict[str, Any] = {}
|
||||
settings_manager = get_settings_manager()
|
||||
if settings_manager.settings.COMPONENTS_PATH:
|
||||
logger.info(
|
||||
|
|
@ -58,8 +60,12 @@ def get_all():
|
|||
|
||||
logger.info(f"Loading {len(custom_component_dicts)} category(ies)")
|
||||
for custom_component_dict in custom_component_dicts:
|
||||
logger.debug(
|
||||
{key: len(value) for key, value in custom_component_dict.items()}
|
||||
# custom_component_dict is a dict of dicts
|
||||
if not custom_component_dict:
|
||||
continue
|
||||
category = list(custom_component_dict.keys())[0]
|
||||
logger.info(
|
||||
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
|
||||
)
|
||||
custom_components_from_file = merge_nested_dicts_with_renaming(
|
||||
custom_components_from_file, custom_component_dict
|
||||
|
|
@ -71,22 +77,42 @@ def get_all():
|
|||
|
||||
|
||||
# For backwards compatibility we will keep the old endpoint
|
||||
@router.post("/predict/{flow_id}", response_model=ProcessResponse)
|
||||
@router.post("/process/{flow_id}", response_model=ProcessResponse)
|
||||
@router.post(
|
||||
"/predict/{flow_id}",
|
||||
response_model=ProcessResponse,
|
||||
dependencies=[Depends(api_key_security)],
|
||||
)
|
||||
@router.post(
|
||||
"/process/{flow_id}",
|
||||
response_model=ProcessResponse,
|
||||
)
|
||||
async def process_flow(
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
flow_id: str,
|
||||
inputs: Optional[dict] = None,
|
||||
tweaks: Optional[dict] = None,
|
||||
clear_cache: Annotated[bool, Body(embed=True)] = False, # noqa: F821
|
||||
session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
|
||||
session: Session = Depends(get_session),
|
||||
api_key_user: User = Depends(api_key_security),
|
||||
):
|
||||
"""
|
||||
Endpoint to process an input with a given flow_id.
|
||||
"""
|
||||
|
||||
try:
|
||||
flow = session.get(Flow, flow_id)
|
||||
if api_key_user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid API Key",
|
||||
)
|
||||
|
||||
# Get the flow that matches the flow_id and belongs to the user
|
||||
flow = (
|
||||
session.query(Flow)
|
||||
.filter(Flow.id == flow_id)
|
||||
.filter(Flow.user_id == api_key_user.id)
|
||||
.first()
|
||||
)
|
||||
if flow is None:
|
||||
raise ValueError(f"Flow {flow_id} not found")
|
||||
|
||||
|
|
@ -102,6 +128,22 @@ async def process_flow(
|
|||
graph_data, inputs, clear_cache, session_id
|
||||
)
|
||||
return ProcessResponse(result=response, session_id=session_id)
|
||||
except sa.exc.StatementError as exc:
|
||||
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
|
||||
if "badly formed hexadecimal UUID string" in str(exc):
|
||||
# This means the Flow ID is not a valid UUID which means it can't find the flow
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
||||
) from exc
|
||||
except ValueError as exc:
|
||||
if f"Flow {flow_id} not found" in str(exc):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
||||
) from exc
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||
) from exc
|
||||
except Exception as e:
|
||||
# Log stack trace
|
||||
logger.exception(e)
|
||||
|
|
|
|||
|
|
@ -4,16 +4,18 @@ from fastapi.encoders import jsonable_encoder
|
|||
|
||||
from langflow.api.utils import remove_api_keys
|
||||
from langflow.api.v1.schemas import FlowListCreate, FlowListRead
|
||||
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.user.user import User
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.services.utils import get_settings_manager
|
||||
import orjson
|
||||
from sqlmodel import Session, select
|
||||
from sqlmodel import Session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from fastapi import File, UploadFile
|
||||
|
|
@ -23,9 +25,18 @@ router = APIRouter(prefix="/flows", tags=["Flows"])
|
|||
|
||||
|
||||
@router.post("/", response_model=FlowRead, status_code=201)
|
||||
def create_flow(*, session: Session = Depends(get_session), flow: FlowCreate):
|
||||
def create_flow(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
flow: FlowCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Create a new flow."""
|
||||
if flow.user_id is None:
|
||||
flow.user_id = current_user.id
|
||||
|
||||
db_flow = Flow.from_orm(flow)
|
||||
|
||||
session.add(db_flow)
|
||||
session.commit()
|
||||
session.refresh(db_flow)
|
||||
|
|
@ -33,31 +44,49 @@ def create_flow(*, session: Session = Depends(get_session), flow: FlowCreate):
|
|||
|
||||
|
||||
@router.get("/", response_model=list[FlowRead], status_code=200)
|
||||
def read_flows(*, session: Session = Depends(get_session)):
|
||||
def read_flows(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Read all flows."""
|
||||
try:
|
||||
flows = session.exec(select(Flow)).all()
|
||||
flows = current_user.flows
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
return [jsonable_encoder(flow) for flow in flows]
|
||||
|
||||
|
||||
@router.get("/{flow_id}", response_model=FlowRead, status_code=200)
|
||||
def read_flow(*, session: Session = Depends(get_session), flow_id: UUID):
|
||||
def read_flow(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
flow_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Read a flow."""
|
||||
if flow := session.get(Flow, flow_id):
|
||||
return flow
|
||||
if user_flow := (
|
||||
session.query(Flow)
|
||||
.filter(Flow.id == flow_id)
|
||||
.filter(Flow.user_id == current_user.id)
|
||||
.first()
|
||||
):
|
||||
return user_flow
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
|
||||
|
||||
@router.patch("/{flow_id}", response_model=FlowRead, status_code=200)
|
||||
def update_flow(
|
||||
*, session: Session = Depends(get_session), flow_id: UUID, flow: FlowUpdate
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
flow_id: UUID,
|
||||
flow: FlowUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Update a flow."""
|
||||
|
||||
db_flow = session.get(Flow, flow_id)
|
||||
db_flow = read_flow(session=session, flow_id=flow_id, current_user=current_user)
|
||||
if not db_flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
flow_data = flow.dict(exclude_unset=True)
|
||||
|
|
@ -65,7 +94,8 @@ def update_flow(
|
|||
if settings_manager.settings.REMOVE_API_KEYS:
|
||||
flow_data = remove_api_keys(flow_data)
|
||||
for key, value in flow_data.items():
|
||||
setattr(db_flow, key, value)
|
||||
if value is not None:
|
||||
setattr(db_flow, key, value)
|
||||
session.add(db_flow)
|
||||
session.commit()
|
||||
session.refresh(db_flow)
|
||||
|
|
@ -73,9 +103,14 @@ def update_flow(
|
|||
|
||||
|
||||
@router.delete("/{flow_id}", status_code=200)
|
||||
def delete_flow(*, session: Session = Depends(get_session), flow_id: UUID):
|
||||
def delete_flow(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
flow_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Delete a flow."""
|
||||
flow = session.get(Flow, flow_id)
|
||||
flow = read_flow(session=session, flow_id=flow_id, current_user=current_user)
|
||||
if not flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
session.delete(flow)
|
||||
|
|
@ -87,10 +122,16 @@ def delete_flow(*, session: Session = Depends(get_session), flow_id: UUID):
|
|||
|
||||
|
||||
@router.post("/batch/", response_model=List[FlowRead], status_code=201)
|
||||
def create_flows(*, session: Session = Depends(get_session), flow_list: FlowListCreate):
|
||||
def create_flows(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
flow_list: FlowListCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Create multiple new flows."""
|
||||
db_flows = []
|
||||
for flow in flow_list.flows:
|
||||
flow.user_id = current_user.id
|
||||
db_flow = Flow.from_orm(flow)
|
||||
session.add(db_flow)
|
||||
db_flows.append(db_flow)
|
||||
|
|
@ -102,7 +143,10 @@ def create_flows(*, session: Session = Depends(get_session), flow_list: FlowList
|
|||
|
||||
@router.post("/upload/", response_model=List[FlowRead], status_code=201)
|
||||
async def upload_file(
|
||||
*, session: Session = Depends(get_session), file: UploadFile = File(...)
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
file: UploadFile = File(...),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Upload flows from a file."""
|
||||
contents = await file.read()
|
||||
|
|
@ -111,11 +155,19 @@ async def upload_file(
|
|||
flow_list = FlowListCreate(**data)
|
||||
else:
|
||||
flow_list = FlowListCreate(flows=[FlowCreate(**flow) for flow in data])
|
||||
return create_flows(session=session, flow_list=flow_list)
|
||||
# Now we set the user_id for all flows
|
||||
for flow in flow_list.flows:
|
||||
flow.user_id = current_user.id
|
||||
|
||||
return create_flows(session=session, flow_list=flow_list, current_user=current_user)
|
||||
|
||||
|
||||
@router.get("/download/", response_model=FlowListRead, status_code=200)
|
||||
async def download_file(*, session: Session = Depends(get_session)):
|
||||
async def download_file(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Download all flows as a file."""
|
||||
flows = read_flows(session=session)
|
||||
flows = read_flows(session=session, current_user=current_user)
|
||||
return FlowListRead(flows=flows)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
from uuid import UUID
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import Session
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.database.models.token import Token
|
||||
from langflow.auth.auth import (
|
||||
from langflow.api.v1.schemas import Token
|
||||
from langflow.services.auth.utils import (
|
||||
authenticate_user,
|
||||
create_user_tokens,
|
||||
create_refresh_token,
|
||||
create_user_longterm_token,
|
||||
get_current_active_user,
|
||||
)
|
||||
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
router = APIRouter()
|
||||
router = APIRouter(tags=["Login"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
|
|
@ -37,9 +37,8 @@ async def login_to_get_access_token(
|
|||
async def auto_login(db: Session = Depends(get_session)):
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
if settings_manager.settings.AUTO_LOGIN:
|
||||
user_id = UUID("3fa85f64-5717-4562-b3fc-2c963f66afa6")
|
||||
return create_user_longterm_token(user_id, db)
|
||||
if settings_manager.auth_settings.AUTO_LOGIN:
|
||||
return create_user_longterm_token(db)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -51,7 +50,9 @@ async def auto_login(db: Session = Depends(get_session)):
|
|||
|
||||
|
||||
@router.post("/refresh")
|
||||
async def refresh_token(token: str):
|
||||
async def refresh_token(
|
||||
token: str, current_user: Session = Depends(get_current_active_user)
|
||||
):
|
||||
if token:
|
||||
return create_refresh_token(token)
|
||||
else:
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from uuid import UUID
|
||||
from langflow.services.database.models.api_key.api_key import ApiKeyRead
|
||||
from langflow.services.database.models.flow import FlowCreate, FlowRead
|
||||
from langflow.services.database.models.user import UserRead
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
|
@ -137,3 +140,32 @@ class ComponentListCreate(BaseModel):
|
|||
|
||||
class ComponentListRead(BaseModel):
|
||||
flows: List[FlowRead]
|
||||
|
||||
|
||||
class UsersResponse(BaseModel):
|
||||
total_count: int
|
||||
users: List[UserRead]
|
||||
|
||||
|
||||
class ApiKeyResponse(BaseModel):
|
||||
id: str
|
||||
api_key: str
|
||||
name: str
|
||||
created_at: str
|
||||
last_used_at: str
|
||||
|
||||
|
||||
class ApiKeysResponse(BaseModel):
|
||||
total_count: int
|
||||
user_id: UUID
|
||||
api_keys: List[ApiKeyRead]
|
||||
|
||||
|
||||
class CreateApiKeyRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
from uuid import UUID
|
||||
from langflow.api.v1.schemas import UsersResponse
|
||||
from langflow.services.database.models.user import (
|
||||
User,
|
||||
UserCreate,
|
||||
UserRead,
|
||||
UserUpdate,
|
||||
)
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
|
@ -7,28 +14,27 @@ from sqlmodel import Session, select
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.auth.auth import get_current_active_user, get_password_hash
|
||||
from langflow.database.models.user import (
|
||||
User,
|
||||
UserAddModel,
|
||||
UserListModel,
|
||||
UserPatchModel,
|
||||
UsersResponse,
|
||||
from langflow.services.auth.utils import (
|
||||
get_current_active_superuser,
|
||||
get_current_active_user,
|
||||
get_password_hash,
|
||||
)
|
||||
from langflow.services.database.models.user.crud import (
|
||||
update_user,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["Login"])
|
||||
router = APIRouter(tags=["Users"])
|
||||
|
||||
|
||||
@router.post("/user", response_model=UserListModel)
|
||||
@router.post("/user", response_model=UserRead, status_code=201)
|
||||
def add_user(
|
||||
user: UserAddModel,
|
||||
user: UserCreate,
|
||||
db: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""
|
||||
Add a new user to the database.
|
||||
"""
|
||||
new_user = User(**user.dict())
|
||||
new_user = User.from_orm(user)
|
||||
try:
|
||||
new_user.password = get_password_hash(user.password)
|
||||
|
||||
|
|
@ -37,13 +43,15 @@ def add_user(
|
|||
db.refresh(new_user)
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail="User exists") from e
|
||||
raise HTTPException(status_code=400, detail="This username is unavailable.") from e
|
||||
|
||||
return new_user
|
||||
|
||||
|
||||
@router.get("/user", response_model=UserListModel)
|
||||
def read_current_user(current_user: User = Depends(get_current_active_user)) -> User:
|
||||
@router.get("/user", response_model=UserRead)
|
||||
def read_current_user(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
) -> User:
|
||||
"""
|
||||
Retrieve the current user's data.
|
||||
"""
|
||||
|
|
@ -54,7 +62,7 @@ def read_current_user(current_user: User = Depends(get_current_active_user)) ->
|
|||
def read_all_users(
|
||||
skip: int = 0,
|
||||
limit: int = 10,
|
||||
_: Session = Depends(get_current_active_user),
|
||||
current_user: Session = Depends(get_current_active_superuser),
|
||||
db: Session = Depends(get_session),
|
||||
) -> UsersResponse:
|
||||
"""
|
||||
|
|
@ -68,14 +76,14 @@ def read_all_users(
|
|||
|
||||
return UsersResponse(
|
||||
total_count=total_count, # type: ignore
|
||||
users=[UserListModel(**dict(user.User)) for user in users],
|
||||
users=[UserRead(**dict(user.User)) for user in users],
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/user/{user_id}", response_model=UserListModel)
|
||||
@router.patch("/user/{user_id}", response_model=UserRead)
|
||||
def patch_user(
|
||||
user_id: UUID,
|
||||
user: UserPatchModel,
|
||||
user: UserUpdate,
|
||||
_: Session = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_session),
|
||||
) -> User:
|
||||
|
|
@ -88,12 +96,21 @@ def patch_user(
|
|||
@router.delete("/user/{user_id}")
|
||||
def delete_user(
|
||||
user_id: UUID,
|
||||
_: Session = Depends(get_current_active_user),
|
||||
current_user: User = Depends(get_current_active_superuser),
|
||||
db: Session = Depends(get_session),
|
||||
) -> dict:
|
||||
"""
|
||||
Delete a user from the database.
|
||||
"""
|
||||
if current_user.id == user_id:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="You can't delete your own user account"
|
||||
)
|
||||
elif not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="You don't have the permission to delete this user"
|
||||
)
|
||||
|
||||
user_db = db.query(User).filter(User.id == user_id).first()
|
||||
if not user_db:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
|
@ -115,14 +132,13 @@ def add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(
|
|||
"""
|
||||
new_user = User(
|
||||
username="superuser",
|
||||
password="12345",
|
||||
password=get_password_hash("12345"),
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
last_login_at=None,
|
||||
)
|
||||
|
||||
try:
|
||||
new_user.password = get_password_hash(new_user.password)
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
|
|
@ -31,7 +31,12 @@ def post_validate_code(code: Code):
|
|||
def post_validate_prompt(prompt_request: ValidatePromptRequest):
|
||||
try:
|
||||
input_variables = validate_prompt(prompt_request.template)
|
||||
|
||||
# Check if frontend_node is None before proceeding to avoid attempting to update a non-existent node.
|
||||
if prompt_request.frontend_node is None:
|
||||
return PromptValidationResponse(
|
||||
input_variables=input_variables,
|
||||
frontend_node={},
|
||||
)
|
||||
old_custom_fields = get_old_custom_fields(prompt_request)
|
||||
|
||||
add_new_variables_to_template(input_variables, prompt_request)
|
||||
|
|
|
|||
|
|
@ -1,177 +0,0 @@
|
|||
from uuid import UUID
|
||||
from typing import Annotated
|
||||
from jose import JWTError, jwt
|
||||
from sqlalchemy.orm import Session
|
||||
from passlib.context import CryptContext
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.database.models.user import (
|
||||
User,
|
||||
get_user_by_id,
|
||||
get_user_by_username,
|
||||
update_user_last_login_at,
|
||||
)
|
||||
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_session)
|
||||
) -> User:
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings_manager.settings.SECRET_KEY,
|
||||
algorithms=[settings_manager.settings.ALGORITHM],
|
||||
)
|
||||
user_id: UUID = payload.get("sub") # type: ignore
|
||||
token_type: str = payload.get("type") # type: ignore
|
||||
|
||||
if user_id is None or token_type:
|
||||
raise credentials_exception
|
||||
except JWTError as e:
|
||||
raise credentials_exception from e
|
||||
|
||||
user = get_user_by_id(db, user_id) # type: ignore
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: Annotated[User, Depends(get_current_user)]
|
||||
):
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def create_token(data: dict, expires_delta: timedelta):
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
to_encode["exp"] = expire
|
||||
|
||||
return jwt.encode(
|
||||
to_encode,
|
||||
settings_manager.settings.SECRET_KEY,
|
||||
algorithm=settings_manager.settings.ALGORITHM,
|
||||
)
|
||||
|
||||
|
||||
def create_user_longterm_token(
|
||||
user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False
|
||||
) -> dict:
|
||||
access_token_expires_longterm = timedelta(days=365)
|
||||
access_token = create_token(
|
||||
data={"sub": str(user_id)},
|
||||
expires_delta=access_token_expires_longterm,
|
||||
)
|
||||
|
||||
# Update: last_login_at
|
||||
if update_last_login:
|
||||
update_user_last_login_at(user_id, db)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": None,
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
|
||||
def create_user_tokens(
|
||||
user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False
|
||||
) -> dict:
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
access_token_expires = timedelta(
|
||||
minutes=settings_manager.settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
access_token = create_token(
|
||||
data={"sub": str(user_id)},
|
||||
expires_delta=access_token_expires,
|
||||
)
|
||||
|
||||
refresh_token_expires = timedelta(
|
||||
minutes=settings_manager.settings.REFRESH_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
refresh_token = create_token(
|
||||
data={"sub": str(user_id), "type": "rf"},
|
||||
expires_delta=refresh_token_expires,
|
||||
)
|
||||
|
||||
# Update: last_login_at
|
||||
if update_last_login:
|
||||
update_user_last_login_at(user_id, db)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
|
||||
def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)):
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
refresh_token,
|
||||
settings_manager.settings.SECRET_KEY,
|
||||
algorithms=[settings_manager.settings.ALGORITHM],
|
||||
)
|
||||
user_id: UUID = payload.get("sub") # type: ignore
|
||||
token_type: str = payload.get("type") # type: ignore
|
||||
|
||||
if user_id is None or token_type is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
|
||||
)
|
||||
|
||||
return create_user_tokens(user_id, db)
|
||||
|
||||
except JWTError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token",
|
||||
) from e
|
||||
|
||||
|
||||
def authenticate_user(
|
||||
username: str, password: str, db: Session = Depends(get_session)
|
||||
) -> User | None:
|
||||
user = get_user_by_username(db, username)
|
||||
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if not user.is_active:
|
||||
if not user.last_login_at:
|
||||
raise HTTPException(status_code=400, detail="Waiting for approval")
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
|
||||
return user if verify_password(password, user.password) else None
|
||||
|
|
@ -6,7 +6,7 @@ from langchain.schema import Document
|
|||
from langchain.vectorstores.base import VectorStore
|
||||
from langchain.schema import BaseRetriever
|
||||
from langchain.embeddings.base import Embeddings
|
||||
import chromadb
|
||||
import chromadb # type: ignore
|
||||
|
||||
|
||||
class ChromaComponent(CustomComponent):
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
from sqlmodel import Field
|
||||
from uuid import UUID, uuid4
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import timezone, datetime
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException, Depends
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.services.database.models.base import SQLModelSerializable, SQLModel
|
||||
|
||||
|
||||
class User(SQLModelSerializable, table=True):
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||
username: str = Field(index=True, unique=True)
|
||||
password: str = Field()
|
||||
is_active: bool = Field(default=False)
|
||||
is_superuser: bool = Field(default=False)
|
||||
create_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
last_login_at: Optional[datetime] = Field()
|
||||
|
||||
|
||||
class UserAddModel(SQLModel):
|
||||
username: str = Field()
|
||||
password: str = Field()
|
||||
|
||||
|
||||
class UserListModel(SQLModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
username: str = Field()
|
||||
is_active: bool = Field()
|
||||
is_superuser: bool = Field()
|
||||
create_at: datetime = Field()
|
||||
updated_at: datetime = Field()
|
||||
last_login_at: Optional[datetime] = Field()
|
||||
|
||||
|
||||
class UserPatchModel(SQLModel):
|
||||
username: Optional[str] = Field()
|
||||
is_active: Optional[bool] = Field()
|
||||
is_superuser: Optional[bool] = Field()
|
||||
last_login_at: Optional[datetime] = Field()
|
||||
|
||||
|
||||
class UsersResponse(BaseModel):
|
||||
total_count: int
|
||||
users: List[UserListModel]
|
||||
|
||||
|
||||
def get_user_by_username(db: Session, username: str) -> User:
|
||||
db_user = db.query(User).filter(User.username == username).first()
|
||||
return User.from_orm(db_user) if db_user else None # type: ignore
|
||||
|
||||
|
||||
def get_user_by_id(db: Session, id: UUID) -> User:
|
||||
db_user = db.query(User).filter(User.id == id).first()
|
||||
return User.from_orm(db_user) if db_user else None # type: ignore
|
||||
|
||||
|
||||
def update_user(
|
||||
user_id: UUID, user: UserPatchModel, db: Session = Depends(get_session)
|
||||
) -> User:
|
||||
user_db = get_user_by_username(db, user.username) # type: ignore
|
||||
if user_db and user_db.id != user_id:
|
||||
raise HTTPException(status_code=409, detail="Username already exists")
|
||||
|
||||
user_db = get_user_by_id(db, user_id)
|
||||
if not user_db:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
try:
|
||||
user_data = user.dict(exclude_unset=True)
|
||||
for key, value in user_data.items():
|
||||
setattr(user_db, key, value)
|
||||
|
||||
user_db.updated_at = datetime.now(timezone.utc)
|
||||
user_db = db.merge(user_db)
|
||||
db.commit()
|
||||
if db.identity_key(instance=user_db) is not None:
|
||||
db.refresh(user_db)
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
||||
return user_db
|
||||
|
||||
|
||||
def update_user_last_login_at(user_id: UUID, db: Session = Depends(get_session)):
|
||||
user_data = UserPatchModel(last_login_at=datetime.now(timezone.utc)) # type: ignore
|
||||
|
||||
return update_user(user_id, user_data, db)
|
||||
|
|
@ -144,7 +144,7 @@ class Graph:
|
|||
|
||||
return list(reversed(sorted_vertices))
|
||||
|
||||
def generator_build(self) -> Generator:
|
||||
def generator_build(self) -> Generator[Vertex, None, None]:
|
||||
"""Builds each vertex in the graph and yields it."""
|
||||
sorted_vertices = self.topological_sort()
|
||||
logger.debug("Sorted vertices: %s", sorted_vertices)
|
||||
|
|
|
|||
|
|
@ -133,13 +133,13 @@ class Vertex:
|
|||
# Add _type to params
|
||||
self.params = params
|
||||
|
||||
def _build(self):
|
||||
def _build(self, user_id=None):
|
||||
"""
|
||||
Initiate the build process.
|
||||
"""
|
||||
logger.debug(f"Building {self.vertex_type}")
|
||||
self._build_each_node_in_params_dict()
|
||||
self._get_and_instantiate_class()
|
||||
self._get_and_instantiate_class(user_id)
|
||||
self._validate_built_object()
|
||||
|
||||
self._built = True
|
||||
|
|
@ -169,23 +169,25 @@ class Vertex:
|
|||
"""
|
||||
return all(self._is_node(node) for node in value)
|
||||
|
||||
def _build_node_and_update_params(self, key, node):
|
||||
def _build_node_and_update_params(self, key, node, user_id=None):
|
||||
"""
|
||||
Builds a given node and updates the params dictionary accordingly.
|
||||
"""
|
||||
result = node.build()
|
||||
result = node.build(user_id)
|
||||
self._handle_func(key, result)
|
||||
if isinstance(result, list):
|
||||
self._extend_params_list_with_result(key, result)
|
||||
self.params[key] = result
|
||||
|
||||
def _build_list_of_nodes_and_update_params(self, key, nodes):
|
||||
def _build_list_of_nodes_and_update_params(
|
||||
self, key, nodes: List["Vertex"], user_id=None
|
||||
):
|
||||
"""
|
||||
Iterates over a list of nodes, builds each and updates the params dictionary.
|
||||
"""
|
||||
self.params[key] = []
|
||||
for node in nodes:
|
||||
built = node.build()
|
||||
built = node.build(user_id)
|
||||
if isinstance(built, list):
|
||||
if key not in self.params:
|
||||
self.params[key] = []
|
||||
|
|
@ -215,7 +217,7 @@ class Vertex:
|
|||
if isinstance(self.params[key], list):
|
||||
self.params[key].extend(result)
|
||||
|
||||
def _get_and_instantiate_class(self):
|
||||
def _get_and_instantiate_class(self, user_id=None):
|
||||
"""
|
||||
Gets the class from a dictionary and instantiates it with the params.
|
||||
"""
|
||||
|
|
@ -226,6 +228,7 @@ class Vertex:
|
|||
node_type=self.vertex_type,
|
||||
base_type=self.base_type,
|
||||
params=self.params,
|
||||
user_id=user_id,
|
||||
)
|
||||
self._update_built_object_and_artifacts(result)
|
||||
except Exception as exc:
|
||||
|
|
@ -255,9 +258,9 @@ class Vertex:
|
|||
|
||||
raise ValueError(message)
|
||||
|
||||
def build(self, force: bool = False) -> Any:
|
||||
def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any:
|
||||
if not self._built or force:
|
||||
self._build()
|
||||
self._build(user_id, *args, **kwargs)
|
||||
|
||||
return self._built_object
|
||||
|
||||
|
|
|
|||
|
|
@ -21,18 +21,18 @@ class AgentVertex(Vertex):
|
|||
elif isinstance(source_node, ChainVertex):
|
||||
self.chains.append(source_node)
|
||||
|
||||
def build(self, force: bool = False) -> Any:
|
||||
def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any:
|
||||
if not self._built or force:
|
||||
self._set_tools_and_chains()
|
||||
# First, build the tools
|
||||
for tool_node in self.tools:
|
||||
tool_node.build()
|
||||
tool_node.build(user_id=user_id)
|
||||
|
||||
# Next, build the chains and the rest
|
||||
for chain_node in self.chains:
|
||||
chain_node.build(tools=self.tools)
|
||||
chain_node.build(tools=self.tools, user_id=user_id)
|
||||
|
||||
self._build()
|
||||
self._build(user_id=user_id)
|
||||
|
||||
return self._built_object
|
||||
|
||||
|
|
@ -49,13 +49,13 @@ class LLMVertex(Vertex):
|
|||
def __init__(self, data: Dict):
|
||||
super().__init__(data, base_type="llms")
|
||||
|
||||
def build(self, force: bool = False) -> Any:
|
||||
def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any:
|
||||
# LLM is different because some models might take up too much memory
|
||||
# or time to load. So we only load them when we need them.ß
|
||||
if self.vertex_type == self.built_node_type:
|
||||
return self.class_built_object
|
||||
if not self._built or force:
|
||||
self._build()
|
||||
self._build(user_id=user_id)
|
||||
self.built_node_type = self.vertex_type
|
||||
self.class_built_object = self._built_object
|
||||
# Avoid deepcopying the LLM
|
||||
|
|
@ -77,11 +77,11 @@ class WrapperVertex(Vertex):
|
|||
def __init__(self, data: Dict):
|
||||
super().__init__(data, base_type="wrappers")
|
||||
|
||||
def build(self, force: bool = False) -> Any:
|
||||
def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any:
|
||||
if not self._built or force:
|
||||
if "headers" in self.params:
|
||||
self.params["headers"] = ast.literal_eval(self.params["headers"])
|
||||
self._build()
|
||||
self._build(user_id=user_id)
|
||||
return self._built_object
|
||||
|
||||
|
||||
|
|
@ -148,16 +148,19 @@ class ChainVertex(Vertex):
|
|||
def build(
|
||||
self,
|
||||
force: bool = False,
|
||||
tools: Optional[List[Union[ToolkitVertex, ToolVertex]]] = None,
|
||||
user_id=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
if not self._built or force:
|
||||
# Check if the chain requires a PromptVertex
|
||||
for key, value in self.params.items():
|
||||
if isinstance(value, PromptVertex):
|
||||
# Build the PromptVertex, passing the tools if available
|
||||
tools = kwargs.get("tools", None)
|
||||
self.params[key] = value.build(tools=tools, force=force)
|
||||
|
||||
self._build()
|
||||
self._build(user_id=user_id)
|
||||
|
||||
return self._built_object
|
||||
|
||||
|
|
@ -169,7 +172,10 @@ class PromptVertex(Vertex):
|
|||
def build(
|
||||
self,
|
||||
force: bool = False,
|
||||
user_id=None,
|
||||
tools: Optional[List[Union[ToolkitVertex, ToolVertex]]] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
if not self._built or force:
|
||||
if (
|
||||
|
|
@ -180,7 +186,7 @@ class PromptVertex(Vertex):
|
|||
# Check if it is a ZeroShotPrompt and needs a tool
|
||||
if "ShotPrompt" in self.vertex_type:
|
||||
tools = (
|
||||
[tool_node.build() for tool_node in tools]
|
||||
[tool_node.build(user_id=user_id) for tool_node in tools]
|
||||
if tools is not None
|
||||
else []
|
||||
)
|
||||
|
|
@ -208,7 +214,7 @@ class PromptVertex(Vertex):
|
|||
else:
|
||||
self.params.pop("input_variables", None)
|
||||
|
||||
self._build()
|
||||
self._build(user_id=user_id)
|
||||
return self._built_object
|
||||
|
||||
def _built_object_repr(self):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import Any, Callable, List, Optional
|
||||
from typing import Any, Callable, List, Optional, Union
|
||||
from uuid import UUID
|
||||
from fastapi import HTTPException
|
||||
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
|
||||
from langflow.interface.custom.component import Component
|
||||
|
|
@ -22,6 +23,7 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
function: Optional[Callable] = None
|
||||
return_type_valid_list = list(CUSTOM_COMPONENT_SUPPORTED_TYPES.keys())
|
||||
repr_value: Optional[Any] = ""
|
||||
user_id: Optional[Union[UUID, str]] = None
|
||||
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
|
|
@ -187,11 +189,16 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
return build_sorted_vertices_with_caching(graph_data)
|
||||
|
||||
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]:
|
||||
get_session = get_session or session_getter
|
||||
db_manager = get_db_manager()
|
||||
with get_session(db_manager) as session:
|
||||
flows = session.query(Flow).all()
|
||||
return flows
|
||||
if not self.user_id:
|
||||
raise ValueError("Session is invalid")
|
||||
try:
|
||||
get_session = get_session or session_getter
|
||||
db_manager = get_db_manager()
|
||||
with get_session(db_manager) as session:
|
||||
flows = session.query(Flow).filter(Flow.user_id == self.user_id).all()
|
||||
return flows
|
||||
except Exception as e:
|
||||
raise ValueError("Session is invalid") from e
|
||||
|
||||
def get_flow(
|
||||
self,
|
||||
|
|
@ -207,7 +214,11 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
if flow_id:
|
||||
flow = session.query(Flow).get(flow_id)
|
||||
elif flow_name:
|
||||
flow = session.query(Flow).filter(Flow.name == flow_name).first()
|
||||
flow = (
|
||||
session.query(Flow)
|
||||
.filter(Flow.name == flow_name)
|
||||
.filter(Flow.user_id == self.user_id)
|
||||
).first()
|
||||
else:
|
||||
raise ValueError("Either flow_name or flow_id must be provided")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
import orjson
|
||||
from typing import Any, Callable, Dict, Sequence, Type
|
||||
from typing import Any, Callable, Dict, Sequence, Type, TYPE_CHECKING
|
||||
|
||||
from langchain.agents import agent as agent_module
|
||||
from langchain.agents.agent import AgentExecutor
|
||||
|
|
@ -36,8 +36,13 @@ from langchain.vectorstores.base import VectorStore
|
|||
from langchain.document_loaders.base import BaseLoader
|
||||
from langflow.utils.logger import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow import CustomComponent
|
||||
|
||||
def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any:
|
||||
|
||||
def instantiate_class(
|
||||
node_type: str, base_type: str, params: Dict, user_id=None
|
||||
) -> Any:
|
||||
"""Instantiate class from module type and key, and params"""
|
||||
params = convert_params_to_sets(params)
|
||||
params = convert_kwargs(params)
|
||||
|
|
@ -48,7 +53,9 @@ def instantiate_class(node_type: str, base_type: str, params: Dict) -> Any:
|
|||
return custom_node(**params)
|
||||
logger.debug(f"Instantiating {node_type} of type {base_type}")
|
||||
class_object = import_by_type(_type=base_type, name=node_type)
|
||||
return instantiate_based_on_type(class_object, base_type, node_type, params)
|
||||
return instantiate_based_on_type(
|
||||
class_object, base_type, node_type, params, user_id=user_id
|
||||
)
|
||||
|
||||
|
||||
def convert_params_to_sets(params):
|
||||
|
|
@ -75,7 +82,7 @@ def convert_kwargs(params):
|
|||
return params
|
||||
|
||||
|
||||
def instantiate_based_on_type(class_object, base_type, node_type, params):
|
||||
def instantiate_based_on_type(class_object, base_type, node_type, params, user_id):
|
||||
if base_type == "agents":
|
||||
return instantiate_agent(node_type, class_object, params)
|
||||
elif base_type == "prompts":
|
||||
|
|
@ -109,19 +116,19 @@ def instantiate_based_on_type(class_object, base_type, node_type, params):
|
|||
elif base_type == "memory":
|
||||
return instantiate_memory(node_type, class_object, params)
|
||||
elif base_type == "custom_components":
|
||||
return instantiate_custom_component(node_type, class_object, params)
|
||||
return instantiate_custom_component(node_type, class_object, params, user_id)
|
||||
elif base_type == "wrappers":
|
||||
return instantiate_wrapper(node_type, class_object, params)
|
||||
else:
|
||||
return class_object(**params)
|
||||
|
||||
|
||||
def instantiate_custom_component(node_type, class_object, params):
|
||||
def instantiate_custom_component(node_type, class_object, params, user_id):
|
||||
# we need to make a copy of the params because we will be
|
||||
# modifying it
|
||||
params_copy = params.copy()
|
||||
class_object = get_function_custom(params_copy.pop("code"))
|
||||
custom_component = class_object()
|
||||
class_object: "CustomComponent" = get_function_custom(params_copy.pop("code"))
|
||||
custom_component = class_object(user_id=user_id)
|
||||
built_object = custom_component.build(**params_copy)
|
||||
return built_object, {"repr": custom_component.custom_repr()}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import inspect
|
|||
from typing import Dict, Union
|
||||
|
||||
from langchain.agents.tools import Tool
|
||||
from langflow.utils.logger import logger
|
||||
|
||||
|
||||
def get_func_tool_params(func, **kwargs) -> Union[Dict, None]:
|
||||
|
|
@ -57,7 +58,13 @@ def get_func_tool_params(func, **kwargs) -> Union[Dict, None]:
|
|||
|
||||
|
||||
def get_class_tool_params(cls, **kwargs) -> Union[Dict, None]:
|
||||
tree = ast.parse(inspect.getsource(cls))
|
||||
try:
|
||||
tree = ast.parse(inspect.getsource(cls))
|
||||
except IndentationError:
|
||||
logger.error(
|
||||
f"Error parsing class {cls.__name__}. Make sure there are no tabs in the code."
|
||||
)
|
||||
return None
|
||||
|
||||
tool_params = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from fastapi.responses import FileResponse
|
|||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from langflow.api import router
|
||||
from langflow.routers import login, users, health
|
||||
|
||||
|
||||
from langflow.interface.utils import setup_llm_caching
|
||||
from langflow.services.database.utils import initialize_database
|
||||
|
|
@ -31,9 +31,9 @@ def create_app():
|
|||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(login.router)
|
||||
app.include_router(users.router)
|
||||
app.include_router(health.router)
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ def setup_app(
|
|||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
from langflow.utils.util import get_number_of_workers
|
||||
from langflow.__main__ import get_number_of_workers
|
||||
|
||||
configure()
|
||||
uvicorn.run(
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def get_health():
|
||||
return {"status": "OK"}
|
||||
12
src/backend/langflow/services/auth/factory.py
Normal file
12
src/backend/langflow/services/auth/factory.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from langflow.services.factory import ServiceFactory
|
||||
from langflow.services.auth.service import AuthManager
|
||||
|
||||
|
||||
class AuthManagerFactory(ServiceFactory):
|
||||
name = "auth_manager"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(AuthManager)
|
||||
|
||||
def create(self, settings_manager):
|
||||
return AuthManager(settings_manager)
|
||||
12
src/backend/langflow/services/auth/service.py
Normal file
12
src/backend/langflow/services/auth/service.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from langflow.services.base import Service
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.settings.manager import SettingsManager
|
||||
|
||||
|
||||
class AuthManager(Service):
|
||||
name = "auth_manager"
|
||||
|
||||
def __init__(self, settings_manager: "SettingsManager"):
|
||||
self.settings_manager = settings_manager
|
||||
283
src/backend/langflow/services/auth/utils.py
Normal file
283
src/backend/langflow/services/auth/utils.py
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import Depends, HTTPException, Security, status
|
||||
from fastapi.security import APIKeyHeader, APIKeyQuery, OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from typing import Annotated, Coroutine, Optional, Union
|
||||
from uuid import UUID
|
||||
from langflow.services.database.models.api_key.api_key import ApiKey
|
||||
from langflow.services.database.models.api_key.crud import check_key
|
||||
from langflow.services.database.models.user.user import User
|
||||
from langflow.services.database.models.user.crud import (
|
||||
get_user_by_id,
|
||||
get_user_by_username,
|
||||
update_user_last_login_at,
|
||||
)
|
||||
from langflow.services.utils import get_session, get_settings_manager
|
||||
from sqlmodel import Session
|
||||
|
||||
oauth2_login = OAuth2PasswordBearer(tokenUrl="api/v1/login")
|
||||
|
||||
API_KEY_NAME = "api-key"
|
||||
|
||||
api_key_query = APIKeyQuery(
|
||||
name=API_KEY_NAME, scheme_name="API key query", auto_error=False
|
||||
)
|
||||
api_key_header = APIKeyHeader(
|
||||
name=API_KEY_NAME, scheme_name="API key header", auto_error=False
|
||||
)
|
||||
|
||||
|
||||
# Source: https://github.com/mrtolkien/fastapi_simple_security/blob/master/fastapi_simple_security/security_api_key.py
|
||||
async def api_key_security(
|
||||
query_param: str = Security(api_key_query),
|
||||
header_param: str = Security(api_key_header),
|
||||
db: Session = Depends(get_session),
|
||||
) -> Optional[User]:
|
||||
settings_manager = get_settings_manager()
|
||||
result: Optional[Union[ApiKey, User]] = None
|
||||
if settings_manager.auth_settings.AUTO_LOGIN:
|
||||
# Get the first user
|
||||
settings_manager.auth_settings.FIRST_SUPERUSER
|
||||
result = get_user_by_username(
|
||||
db, settings_manager.auth_settings.FIRST_SUPERUSER
|
||||
)
|
||||
|
||||
elif not query_param and not header_param:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="An API key must be passed as query or header",
|
||||
)
|
||||
|
||||
elif query_param:
|
||||
result = check_key(db, query_param)
|
||||
|
||||
else:
|
||||
result = check_key(db, header_param)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid or missing API key",
|
||||
)
|
||||
if isinstance(result, ApiKey):
|
||||
return result.user
|
||||
elif isinstance(result, User):
|
||||
return result
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
token: Annotated[str, Depends(oauth2_login)],
|
||||
db: Session = Depends(get_session),
|
||||
) -> User:
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if isinstance(token, Coroutine):
|
||||
token = await token
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings_manager.auth_settings.SECRET_KEY,
|
||||
algorithms=[settings_manager.auth_settings.ALGORITHM],
|
||||
)
|
||||
user_id: UUID = payload.get("sub") # type: ignore
|
||||
token_type: str = payload.get("type") # type: ignore
|
||||
|
||||
if user_id is None or token_type:
|
||||
raise credentials_exception
|
||||
except JWTError as e:
|
||||
raise credentials_exception from e
|
||||
|
||||
user = get_user_by_id(db, user_id) # type: ignore
|
||||
if user is None or not user.is_active:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
|
||||
def get_current_active_user(current_user: Annotated[User, Depends(get_current_user)]):
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
def get_current_active_superuser(
|
||||
current_user: Annotated[User, Depends(get_current_user)]
|
||||
) -> User:
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=401, detail="Inactive user")
|
||||
if not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="The user doesn't have enough privileges"
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
settings_manager = get_settings_manager()
|
||||
return settings_manager.auth_settings.pwd_context.verify(
|
||||
plain_password, hashed_password
|
||||
)
|
||||
|
||||
|
||||
def get_password_hash(password):
|
||||
settings_manager = get_settings_manager()
|
||||
return settings_manager.auth_settings.pwd_context.hash(password)
|
||||
|
||||
|
||||
def create_token(data: dict, expires_delta: timedelta):
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
to_encode["exp"] = expire
|
||||
|
||||
return jwt.encode(
|
||||
to_encode,
|
||||
settings_manager.auth_settings.SECRET_KEY,
|
||||
algorithm=settings_manager.auth_settings.ALGORITHM,
|
||||
)
|
||||
|
||||
|
||||
def create_super_user(
|
||||
db: Session = Depends(get_session),
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
) -> User:
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
super_user = get_user_by_username(
|
||||
db, username or settings_manager.auth_settings.FIRST_SUPERUSER
|
||||
)
|
||||
|
||||
if not super_user:
|
||||
super_user = User(
|
||||
username=username or settings_manager.auth_settings.FIRST_SUPERUSER,
|
||||
password=get_password_hash(
|
||||
password or settings_manager.auth_settings.FIRST_SUPERUSER_PASSWORD
|
||||
),
|
||||
is_superuser=True,
|
||||
is_active=True,
|
||||
last_login_at=None,
|
||||
)
|
||||
|
||||
db.add(super_user)
|
||||
db.commit()
|
||||
db.refresh(super_user)
|
||||
|
||||
return super_user
|
||||
|
||||
|
||||
def create_user_longterm_token(db: Session = Depends(get_session)) -> dict:
|
||||
super_user = create_super_user(db)
|
||||
|
||||
access_token_expires_longterm = timedelta(days=365)
|
||||
access_token = create_token(
|
||||
data={"sub": str(super_user.id)},
|
||||
expires_delta=access_token_expires_longterm,
|
||||
)
|
||||
|
||||
# Update: last_login_at
|
||||
update_user_last_login_at(super_user.id, db)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": None,
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
|
||||
def create_user_api_key(user_id: UUID) -> dict:
|
||||
access_token = create_token(
|
||||
data={"sub": str(user_id), "role": "api_key"},
|
||||
expires_delta=timedelta(days=365 * 2),
|
||||
)
|
||||
|
||||
return {"api_key": access_token}
|
||||
|
||||
|
||||
def get_user_id_from_token(token: str) -> UUID:
|
||||
try:
|
||||
user_id = jwt.get_unverified_claims(token)["sub"]
|
||||
return UUID(user_id)
|
||||
except (KeyError, JWTError, ValueError):
|
||||
return UUID(int=0)
|
||||
|
||||
|
||||
def create_user_tokens(
|
||||
user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False
|
||||
) -> dict:
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
access_token_expires = timedelta(
|
||||
minutes=settings_manager.auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
access_token = create_token(
|
||||
data={"sub": str(user_id)},
|
||||
expires_delta=access_token_expires,
|
||||
)
|
||||
|
||||
refresh_token_expires = timedelta(
|
||||
minutes=settings_manager.auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
refresh_token = create_token(
|
||||
data={"sub": str(user_id), "type": "rf"},
|
||||
expires_delta=refresh_token_expires,
|
||||
)
|
||||
|
||||
# Update: last_login_at
|
||||
if update_last_login:
|
||||
update_user_last_login_at(user_id, db)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
|
||||
def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)):
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
refresh_token,
|
||||
settings_manager.auth_settings.SECRET_KEY,
|
||||
algorithms=[settings_manager.auth_settings.ALGORITHM],
|
||||
)
|
||||
user_id: UUID = payload.get("sub") # type: ignore
|
||||
token_type: str = payload.get("type") # type: ignore
|
||||
|
||||
if user_id is None or token_type is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
|
||||
)
|
||||
|
||||
return create_user_tokens(user_id, db)
|
||||
|
||||
except JWTError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token",
|
||||
) from e
|
||||
|
||||
|
||||
def authenticate_user(
|
||||
username: str, password: str, db: Session = Depends(get_session)
|
||||
) -> Optional[User]:
|
||||
user = get_user_by_username(db, username)
|
||||
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if not user.is_active:
|
||||
if not user.last_login_at:
|
||||
raise HTTPException(status_code=400, detail="Waiting for approval")
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
|
||||
return user if verify_password(password, user.password) else None
|
||||
|
|
@ -6,6 +6,6 @@ class CacheManagerFactory(ServiceFactory):
|
|||
def __init__(self):
|
||||
super().__init__(CacheManager)
|
||||
|
||||
def create(self, settings_service):
|
||||
def create(self):
|
||||
# Here you would have logic to create and configure a CacheManager
|
||||
return CacheManager()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ class ChatManagerFactory(ServiceFactory):
|
|||
def __init__(self):
|
||||
super().__init__(ChatManager)
|
||||
|
||||
def create(self, settings_service):
|
||||
def create(self):
|
||||
# Here you would have logic to create and configure a ChatManager
|
||||
return ChatManager()
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ class ChatManager(Service):
|
|||
json_payload = await websocket.receive_json()
|
||||
try:
|
||||
payload = orjson.loads(json_payload)
|
||||
except TypeError:
|
||||
except Exception:
|
||||
payload = json_payload
|
||||
if "clear_history" in payload:
|
||||
self.chat_history.history[client_id] = []
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ class DatabaseManagerFactory(ServiceFactory):
|
|||
def __init__(self):
|
||||
super().__init__(DatabaseManager)
|
||||
|
||||
def create(self, settings_service: "SettingsManager"):
|
||||
def create(self, settings_manager: "SettingsManager"):
|
||||
# Here you would have logic to create and configure a DatabaseManager
|
||||
if not settings_service.settings.DATABASE_URL:
|
||||
if not settings_manager.settings.DATABASE_URL:
|
||||
raise ValueError("No database URL provided")
|
||||
return DatabaseManager(settings_service.settings.DATABASE_URL)
|
||||
return DatabaseManager(settings_manager.settings.DATABASE_URL)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from langflow.services.base import Service
|
||||
from langflow.services.database.utils import Result, TableResults
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from sqlalchemy import inspect
|
||||
import sqlalchemy as sa
|
||||
from sqlmodel import SQLModel, Session, create_engine
|
||||
from langflow.utils.logger import logger
|
||||
from alembic.config import Config
|
||||
|
|
@ -54,6 +57,41 @@ class DatabaseManager(Service):
|
|||
with Session(self.engine) as session:
|
||||
yield session
|
||||
|
||||
def check_schema_health(self) -> bool:
|
||||
inspector = inspect(self.engine)
|
||||
|
||||
model_mapping = {
|
||||
"flow": models.Flow,
|
||||
"user": models.User,
|
||||
"apikey": models.ApiKey,
|
||||
# Add other SQLModel classes here
|
||||
}
|
||||
|
||||
# To account for tables that existed in older versions
|
||||
legacy_tables = ["flowstyle"]
|
||||
|
||||
for table, model in model_mapping.items():
|
||||
expected_columns = list(model.__fields__.keys())
|
||||
|
||||
try:
|
||||
available_columns = [
|
||||
col["name"] for col in inspector.get_columns(table)
|
||||
]
|
||||
except sa.exc.NoSuchTableError:
|
||||
logger.error(f"Missing table: {table}")
|
||||
return False
|
||||
|
||||
for column in expected_columns:
|
||||
if column not in available_columns:
|
||||
logger.error(f"Missing column: {column} in table {table}")
|
||||
return False
|
||||
|
||||
for table in legacy_tables:
|
||||
if table in inspector.get_table_names():
|
||||
logger.warn(f"Legacy table exists: {table}")
|
||||
|
||||
return True
|
||||
|
||||
def run_migrations(self):
|
||||
logger.info(
|
||||
f"Running DB migrations in {self.script_location} on {self.database_url}"
|
||||
|
|
@ -63,6 +101,40 @@ class DatabaseManager(Service):
|
|||
alembic_cfg.set_main_option("sqlalchemy.url", self.database_url)
|
||||
command.upgrade(alembic_cfg, "head")
|
||||
|
||||
def run_migrations_test(self):
|
||||
# This method is used for testing purposes only
|
||||
# We will check that all models are in the database
|
||||
# and that the database is up to date with all columns
|
||||
sql_models = [models.Flow, models.User, models.ApiKey]
|
||||
results = []
|
||||
for sql_model in sql_models:
|
||||
results.append(
|
||||
TableResults(sql_model.__tablename__, self.check_table(sql_model))
|
||||
)
|
||||
return results
|
||||
|
||||
def check_table(self, model):
|
||||
results = []
|
||||
inspector = inspect(self.engine)
|
||||
table_name = model.__tablename__
|
||||
expected_columns = list(model.__fields__.keys())
|
||||
try:
|
||||
available_columns = [
|
||||
col["name"] for col in inspector.get_columns(table_name)
|
||||
]
|
||||
results.append(Result(name=table_name, type="table", success=True))
|
||||
except sa.exc.NoSuchTableError:
|
||||
logger.error(f"Missing table: {table_name}")
|
||||
results.append(Result(name=table_name, type="table", success=False))
|
||||
|
||||
for column in expected_columns:
|
||||
if column not in available_columns:
|
||||
logger.error(f"Missing column: {column} in table {table_name}")
|
||||
results.append(Result(name=column, type="column", success=False))
|
||||
else:
|
||||
results.append(Result(name=column, type="column", success=True))
|
||||
return results
|
||||
|
||||
def create_db_and_tables(self):
|
||||
logger.debug("Creating database and tables")
|
||||
try:
|
||||
|
|
@ -76,9 +148,14 @@ class DatabaseManager(Service):
|
|||
from sqlalchemy import inspect
|
||||
|
||||
inspector = inspect(self.engine)
|
||||
if "flow" not in inspector.get_table_names():
|
||||
logger.error("Something went wrong creating the database and tables.")
|
||||
logger.error("Please check your database settings.")
|
||||
raise RuntimeError("Something went wrong creating the database and tables.")
|
||||
else:
|
||||
logger.debug("Database and tables created successfully")
|
||||
current_tables = ["flow", "user", "apikey"]
|
||||
table_names = inspector.get_table_names()
|
||||
for table in current_tables:
|
||||
if table not in table_names:
|
||||
logger.error("Something went wrong creating the database and tables.")
|
||||
logger.error("Please check your database settings.")
|
||||
raise RuntimeError(
|
||||
"Something went wrong creating the database and tables."
|
||||
)
|
||||
|
||||
logger.debug("Database and tables created successfully")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from .flow import Flow
|
||||
from .user import User
|
||||
from .api_key import ApiKey
|
||||
|
||||
|
||||
__all__ = ["Flow"]
|
||||
__all__ = ["Flow", "User", "ApiKey"]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
from .api_key import ApiKey, ApiKeyCreate, UnmaskedApiKeyRead, ApiKeyRead
|
||||
|
||||
__all__ = ["ApiKey", "ApiKeyCreate", "UnmaskedApiKeyRead", "ApiKeyRead"]
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
from pydantic import validator
|
||||
from sqlmodel import Field, Relationship
|
||||
from uuid import UUID, uuid4
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from datetime import datetime
|
||||
from langflow.services.database.models.base import SQLModelSerializable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.database.models.user import User
|
||||
|
||||
|
||||
class ApiKeyBase(SQLModelSerializable):
|
||||
name: Optional[str] = Field(index=True)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
last_used_at: Optional[datetime] = Field(default=None)
|
||||
total_uses: int = Field(default=0)
|
||||
is_active: bool = Field(default=True)
|
||||
|
||||
|
||||
class ApiKey(ApiKeyBase, table=True):
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||
|
||||
api_key: str = Field(index=True, unique=True)
|
||||
# User relationship
|
||||
user_id: UUID = Field(index=True, foreign_key="user.id")
|
||||
user: "User" = Relationship(back_populates="api_keys")
|
||||
|
||||
|
||||
class ApiKeyCreate(ApiKeyBase):
|
||||
api_key: Optional[str] = None
|
||||
user_id: Optional[UUID] = None
|
||||
|
||||
|
||||
class UnmaskedApiKeyRead(ApiKeyBase):
|
||||
id: UUID
|
||||
api_key: str = Field()
|
||||
user_id: UUID = Field()
|
||||
|
||||
|
||||
class ApiKeyRead(ApiKeyBase):
|
||||
id: UUID
|
||||
api_key: str = Field()
|
||||
user_id: UUID = Field()
|
||||
|
||||
@validator("api_key", always=True)
|
||||
def mask_api_key(cls, v):
|
||||
# This validator will always run, and will mask the API key
|
||||
return f"{v[:8]}{'*' * (len(v) - 8)}"
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import datetime
|
||||
import secrets
|
||||
import threading
|
||||
from uuid import UUID
|
||||
from typing import List, Optional
|
||||
from sqlmodel import Session, select
|
||||
from langflow.services.database.models.api_key import (
|
||||
ApiKey,
|
||||
ApiKeyCreate,
|
||||
UnmaskedApiKeyRead,
|
||||
ApiKeyRead,
|
||||
)
|
||||
|
||||
|
||||
def get_api_keys(session: Session, user_id: UUID) -> List[ApiKeyRead]:
|
||||
query = select(ApiKey).where(ApiKey.user_id == user_id)
|
||||
api_keys = session.exec(query).all()
|
||||
return [ApiKeyRead.from_orm(api_key) for api_key in api_keys]
|
||||
|
||||
|
||||
def create_api_key(
|
||||
session: Session, api_key_create: ApiKeyCreate, user_id: UUID
|
||||
) -> UnmaskedApiKeyRead:
|
||||
# Generate a random API key with 32 bytes of randomness
|
||||
generated_api_key = f"lf-{secrets.token_urlsafe(32)}"
|
||||
|
||||
api_key = ApiKey(
|
||||
api_key=generated_api_key,
|
||||
name=api_key_create.name,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
session.add(api_key)
|
||||
session.commit()
|
||||
session.refresh(api_key)
|
||||
unmasked = UnmaskedApiKeyRead.from_orm(api_key)
|
||||
unmasked.api_key = generated_api_key
|
||||
return unmasked
|
||||
|
||||
|
||||
def delete_api_key(session: Session, api_key_id: UUID) -> None:
|
||||
api_key = session.get(ApiKey, api_key_id)
|
||||
if api_key is None:
|
||||
raise ValueError("API Key not found")
|
||||
session.delete(api_key)
|
||||
session.commit()
|
||||
|
||||
|
||||
def check_key(session: Session, api_key: str) -> Optional[ApiKey]:
|
||||
"""Check if the API key is valid."""
|
||||
query = select(ApiKey).where(ApiKey.api_key == api_key)
|
||||
api_key_object: Optional[ApiKey] = session.exec(query).first()
|
||||
if api_key_object is not None:
|
||||
threading.Thread(
|
||||
target=update_total_uses,
|
||||
args=(
|
||||
session,
|
||||
api_key_object,
|
||||
),
|
||||
).start()
|
||||
return api_key_object
|
||||
|
||||
|
||||
def update_total_uses(session, api_key: ApiKey):
|
||||
"""Update the total uses and last used at."""
|
||||
api_key.total_uses += 1
|
||||
api_key.last_used_at = datetime.datetime.now(datetime.timezone.utc)
|
||||
session.add(api_key)
|
||||
session.commit()
|
||||
session.refresh(api_key)
|
||||
return api_key
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from .component import Component, ComponentModel
|
||||
|
||||
__all__ = ["Component", "ComponentModel"]
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from .flow import Flow, FlowCreate, FlowRead, FlowUpdate
|
||||
|
||||
__all__ = ["Flow", "FlowCreate", "FlowRead", "FlowUpdate"]
|
||||
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
from langflow.services.database.models.base import SQLModelSerializable
|
||||
from pydantic import validator
|
||||
from sqlmodel import Field, JSON, Column
|
||||
from sqlmodel import Field, JSON, Column, Relationship
|
||||
from uuid import UUID, uuid4
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
|
||||
# if TYPE_CHECKING:
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.database.models.user import User
|
||||
|
||||
|
||||
class FlowBase(SQLModelSerializable):
|
||||
|
|
@ -16,7 +17,6 @@ class FlowBase(SQLModelSerializable):
|
|||
|
||||
@validator("data")
|
||||
def validate_json(v):
|
||||
# dict_keys(['description', 'name', 'id', 'data'])
|
||||
if not v:
|
||||
return v
|
||||
if not isinstance(v, dict):
|
||||
|
|
@ -34,14 +34,17 @@ class FlowBase(SQLModelSerializable):
|
|||
class Flow(FlowBase, table=True):
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||
data: Optional[Dict] = Field(default=None, sa_column=Column(JSON))
|
||||
user_id: UUID = Field(index=True, foreign_key="user.id")
|
||||
user: "User" = Relationship(back_populates="flows")
|
||||
|
||||
|
||||
class FlowCreate(FlowBase):
|
||||
pass
|
||||
user_id: Optional[UUID] = None
|
||||
|
||||
|
||||
class FlowRead(FlowBase):
|
||||
id: UUID
|
||||
user_id: UUID = Field()
|
||||
|
||||
|
||||
class FlowUpdate(SQLModelSerializable):
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from .user import User, UserCreate, UserRead, UserUpdate
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
"UserCreate",
|
||||
"UserRead",
|
||||
"UserUpdate",
|
||||
]
|
||||
53
src/backend/langflow/services/database/models/user/crud.py
Normal file
53
src/backend/langflow/services/database/models/user/crud.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
from datetime import datetime, timezone
|
||||
from typing import Union
|
||||
from uuid import UUID
|
||||
from fastapi import Depends, HTTPException
|
||||
from langflow.services.database.models.user.user import User, UserUpdate
|
||||
from langflow.services.utils import get_session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel import Session
|
||||
|
||||
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
|
||||
def get_user_by_username(db: Session, username: str) -> Union[User, None]:
|
||||
return db.query(User).filter(User.username == username).first()
|
||||
|
||||
|
||||
def get_user_by_id(db: Session, id: UUID) -> Union[User, None]:
|
||||
return db.query(User).filter(User.id == id).first()
|
||||
|
||||
|
||||
def update_user(
|
||||
user_id: UUID, user: UserUpdate, db: Session = Depends(get_session)
|
||||
) -> User:
|
||||
user_db = get_user_by_id(db, user_id)
|
||||
if not user_db:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
user_db_by_username = get_user_by_username(db, user.username) # type: ignore
|
||||
if user_db_by_username and user_db_by_username.id != user_id:
|
||||
raise HTTPException(status_code=409, detail="Username already exists")
|
||||
|
||||
user_data = user.dict(exclude_unset=True)
|
||||
for attr, value in user_data.items():
|
||||
if hasattr(user_db, attr) and value is not None:
|
||||
setattr(user_db, attr, value)
|
||||
|
||||
user_db.updated_at = datetime.now(timezone.utc)
|
||||
flag_modified(user_db, "updated_at")
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
||||
return user_db
|
||||
|
||||
|
||||
def update_user_last_login_at(user_id: UUID, db: Session = Depends(get_session)):
|
||||
user_data = UserUpdate(last_login_at=datetime.now(timezone.utc)) # type: ignore
|
||||
|
||||
return update_user(user_id, user_data, db)
|
||||
46
src/backend/langflow/services/database/models/user/user.py
Normal file
46
src/backend/langflow/services/database/models/user/user.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
from langflow.services.database.models.base import SQLModel, SQLModelSerializable
|
||||
from sqlmodel import Field, Relationship
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.database.models.api_key import ApiKey
|
||||
from langflow.services.database.models.flow import Flow
|
||||
|
||||
|
||||
class User(SQLModelSerializable, table=True):
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||
username: str = Field(index=True, unique=True)
|
||||
password: str = Field()
|
||||
is_active: bool = Field(default=False)
|
||||
is_superuser: bool = Field(default=False)
|
||||
create_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
last_login_at: Optional[datetime] = Field()
|
||||
api_keys: list["ApiKey"] = Relationship(back_populates="user")
|
||||
flows: list["Flow"] = Relationship(back_populates="user")
|
||||
|
||||
|
||||
class UserCreate(SQLModel):
|
||||
username: str = Field()
|
||||
password: str = Field()
|
||||
|
||||
|
||||
class UserRead(SQLModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
username: str = Field()
|
||||
is_active: bool = Field()
|
||||
is_superuser: bool = Field()
|
||||
create_at: datetime = Field()
|
||||
updated_at: datetime = Field()
|
||||
last_login_at: Optional[datetime] = Field()
|
||||
|
||||
|
||||
class UserUpdate(SQLModel):
|
||||
username: Optional[str] = Field()
|
||||
is_active: Optional[bool] = Field()
|
||||
is_superuser: Optional[bool] = Field()
|
||||
last_login_at: Optional[datetime] = Field()
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
from langflow.utils.logger import logger
|
||||
from contextlib import contextmanager
|
||||
|
|
@ -13,6 +14,11 @@ def initialize_database():
|
|||
from langflow.services import service_manager, ServiceType
|
||||
|
||||
database_manager = service_manager.get(ServiceType.DATABASE_MANAGER)
|
||||
try:
|
||||
database_manager.check_schema_health()
|
||||
except Exception as exc:
|
||||
logger.error(f"Error checking schema health: {exc}")
|
||||
raise RuntimeError("Error checking schema health") from exc
|
||||
try:
|
||||
database_manager.run_migrations()
|
||||
except CommandError as exc:
|
||||
|
|
@ -28,8 +34,11 @@ def initialize_database():
|
|||
session.execute("DROP TABLE alembic_version")
|
||||
database_manager.run_migrations()
|
||||
except Exception as exc:
|
||||
logger.error(f"Error running migrations: {exc}")
|
||||
raise RuntimeError("Error running migrations") from exc
|
||||
# if the exception involves tables already existing
|
||||
# we can ignore it
|
||||
if "already exists" not in str(exc):
|
||||
logger.error(f"Error running migrations: {exc}")
|
||||
raise RuntimeError("Error running migrations") from exc
|
||||
database_manager.create_db_and_tables()
|
||||
logger.debug("Database initialized")
|
||||
|
||||
|
|
@ -45,3 +54,16 @@ def session_getter(db_manager: "DatabaseManager"):
|
|||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
name: str
|
||||
type: str
|
||||
success: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class TableResults:
|
||||
table_name: str
|
||||
results: list[Result]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from langflow.services.schema import ServiceType
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.factory import ServiceFactory
|
||||
|
|
@ -13,13 +13,21 @@ class ServiceManager:
|
|||
def __init__(self):
|
||||
self.services = {}
|
||||
self.factories = {}
|
||||
self.dependencies = {}
|
||||
|
||||
def register_factory(self, service_factory: "ServiceFactory"):
|
||||
def register_factory(
|
||||
self,
|
||||
service_factory: "ServiceFactory",
|
||||
dependencies: Optional[List[ServiceType]] = None,
|
||||
):
|
||||
"""
|
||||
Registers a new factory.
|
||||
Registers a new factory with dependencies.
|
||||
"""
|
||||
if service_factory.service_class.name not in self.factories:
|
||||
self.factories[service_factory.service_class.name] = service_factory
|
||||
if dependencies is None:
|
||||
dependencies = []
|
||||
service_name = service_factory.service_class.name
|
||||
self.factories[service_name] = service_factory
|
||||
self.dependencies[service_name] = dependencies
|
||||
|
||||
def get(self, service_name: ServiceType):
|
||||
"""
|
||||
|
|
@ -32,17 +40,25 @@ class ServiceManager:
|
|||
|
||||
def _create_service(self, service_name: ServiceType):
|
||||
"""
|
||||
Create a new service given its name.
|
||||
Create a new service given its name, handling dependencies.
|
||||
"""
|
||||
self._validate_service_creation(service_name)
|
||||
|
||||
if service_name == ServiceType.SETTINGS_MANAGER:
|
||||
self.services[service_name] = self.factories[service_name].create()
|
||||
else:
|
||||
settings_service = self.get(ServiceType.SETTINGS_MANAGER)
|
||||
self.services[service_name] = self.factories[service_name].create(
|
||||
settings_service
|
||||
)
|
||||
# Create dependencies first
|
||||
for dependency in self.dependencies.get(service_name, []):
|
||||
if dependency not in self.services:
|
||||
self._create_service(dependency)
|
||||
|
||||
# Collect the dependent services
|
||||
dependent_services = {
|
||||
dep.value: self.services[dep]
|
||||
for dep in self.dependencies.get(service_name, [])
|
||||
}
|
||||
|
||||
# Create the actual service
|
||||
self.services[service_name] = self.factories[service_name].create(
|
||||
**dependent_services
|
||||
)
|
||||
|
||||
def _validate_service_creation(self, service_name: ServiceType):
|
||||
"""
|
||||
|
|
@ -53,14 +69,6 @@ class ServiceManager:
|
|||
f"No factory registered for the service class '{service_name.name}'"
|
||||
)
|
||||
|
||||
if (
|
||||
ServiceType.SETTINGS_MANAGER not in self.factories
|
||||
and service_name != ServiceType.SETTINGS_MANAGER
|
||||
):
|
||||
raise ValueError(
|
||||
f"Cannot create service '{service_name.name}' before the settings service"
|
||||
)
|
||||
|
||||
def update(self, service_name: ServiceType):
|
||||
"""
|
||||
Update a service by its name.
|
||||
|
|
@ -81,12 +89,24 @@ def initialize_services():
|
|||
from langflow.services.cache import factory as cache_factory
|
||||
from langflow.services.chat import factory as chat_factory
|
||||
from langflow.services.settings import factory as settings_factory
|
||||
from langflow.services.auth import factory as auth_factory
|
||||
|
||||
service_manager.register_factory(settings_factory.SettingsManagerFactory())
|
||||
service_manager.register_factory(database_factory.DatabaseManagerFactory())
|
||||
service_manager.register_factory(
|
||||
auth_factory.AuthManagerFactory(), dependencies=[ServiceType.SETTINGS_MANAGER]
|
||||
)
|
||||
service_manager.register_factory(
|
||||
database_factory.DatabaseManagerFactory(),
|
||||
dependencies=[ServiceType.SETTINGS_MANAGER],
|
||||
)
|
||||
service_manager.register_factory(cache_factory.CacheManagerFactory())
|
||||
service_manager.register_factory(chat_factory.ChatManagerFactory())
|
||||
|
||||
# Test cache connection
|
||||
service_manager.get(ServiceType.CACHE_MANAGER)
|
||||
# Test database connection
|
||||
service_manager.get(ServiceType.DATABASE_MANAGER)
|
||||
|
||||
|
||||
def initialize_settings_manager():
|
||||
"""
|
||||
|
|
@ -95,3 +115,22 @@ def initialize_settings_manager():
|
|||
from langflow.services.settings import factory as settings_factory
|
||||
|
||||
service_manager.register_factory(settings_factory.SettingsManagerFactory())
|
||||
|
||||
|
||||
def initialize_session_manager():
|
||||
"""
|
||||
Initialize the session manager.
|
||||
"""
|
||||
from langflow.services.session import factory as session_manager_factory
|
||||
from langflow.services.cache import factory as cache_factory
|
||||
|
||||
initialize_settings_manager()
|
||||
|
||||
service_manager.register_factory(
|
||||
cache_factory.CacheManagerFactory(), dependencies=[ServiceType.SETTINGS_MANAGER]
|
||||
)
|
||||
|
||||
service_manager.register_factory(
|
||||
session_manager_factory.SessionManagerFactory(),
|
||||
dependencies=[ServiceType.CACHE_MANAGER],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ class ServiceType(str, Enum):
|
|||
registered with the service manager.
|
||||
"""
|
||||
|
||||
AUTH_MANAGER = "auth_manager"
|
||||
CACHE_MANAGER = "cache_manager"
|
||||
SETTINGS_MANAGER = "settings_manager"
|
||||
DATABASE_MANAGER = "database_manager"
|
||||
|
|
|
|||
33
src/backend/langflow/services/settings/auth.py
Normal file
33
src/backend/langflow/services/settings/auth.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from typing import Optional
|
||||
import secrets
|
||||
|
||||
from pydantic import BaseSettings
|
||||
from passlib.context import CryptContext
|
||||
|
||||
|
||||
class AuthSettings(BaseSettings):
|
||||
# Login settings
|
||||
SECRET_KEY: str = secrets.token_hex(32)
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
|
||||
REFRESH_TOKEN_EXPIRE_MINUTES: int = 70
|
||||
|
||||
# API Key to execute /process endpoint
|
||||
API_KEY_SECRET_KEY: Optional[
|
||||
str
|
||||
] = "b82818e0ad4ff76615c5721ee21004b07d84cd9b87ba4d9cb42374da134b841a"
|
||||
API_KEY_ALGORITHM: str = "HS256"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
|
||||
# If AUTO_LOGIN = True
|
||||
# > The application does not request login and logs in automatically as a super user.
|
||||
AUTO_LOGIN: bool = False
|
||||
FIRST_SUPERUSER: str = "langflow"
|
||||
FIRST_SUPERUSER_PASSWORD: str = "langflow"
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
class Config:
|
||||
validate_assignment = True
|
||||
extra = "ignore"
|
||||
env_prefix = "LANGFLOW_"
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from langflow.services.base import Service
|
||||
from langflow.services.settings.auth import AuthSettings
|
||||
from langflow.services.settings.base import Settings
|
||||
from langflow.utils.logger import logger
|
||||
import os
|
||||
|
|
@ -8,9 +9,10 @@ import yaml
|
|||
class SettingsManager(Service):
|
||||
name = "settings_manager"
|
||||
|
||||
def __init__(self, settings: Settings):
|
||||
def __init__(self, settings: Settings, auth_settings: AuthSettings):
|
||||
super().__init__()
|
||||
self.settings = settings
|
||||
self.auth_settings = auth_settings
|
||||
|
||||
@classmethod
|
||||
def load_settings_from_yaml(cls, file_path: str) -> "SettingsManager":
|
||||
|
|
@ -33,4 +35,5 @@ class SettingsManager(Service):
|
|||
)
|
||||
|
||||
settings = Settings(**settings_dict)
|
||||
return cls(settings)
|
||||
auth_settings = AuthSettings()
|
||||
return cls(settings, auth_settings)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from langflow.services import ServiceType, service_manager
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.database.manager import DatabaseManager
|
||||
from langflow.services.settings.manager import SettingsManager
|
||||
|
||||
|
||||
|
|
@ -9,7 +11,7 @@ def get_settings_manager() -> "SettingsManager":
|
|||
return service_manager.get(ServiceType.SETTINGS_MANAGER)
|
||||
|
||||
|
||||
def get_db_manager():
|
||||
def get_db_manager() -> "DatabaseManager":
|
||||
return service_manager.get(ServiceType.DATABASE_MANAGER)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@ import importlib
|
|||
from functools import wraps
|
||||
from typing import Optional, Dict, Any, Union
|
||||
|
||||
from docstring_parser import parse # type: ignore
|
||||
from docstring_parser import parse
|
||||
|
||||
from langflow.template.frontend_node.constants import FORCE_SHOW_FIELDS
|
||||
from langflow.utils import constants
|
||||
from langflow.utils.logger import logger
|
||||
from multiprocess import cpu_count # type: ignore
|
||||
|
||||
|
||||
def build_template_from_function(
|
||||
|
|
@ -265,6 +263,9 @@ def format_dict(
|
|||
|
||||
_type: Union[str, type] = get_type(value)
|
||||
|
||||
if "BaseModel" in str(_type):
|
||||
continue
|
||||
|
||||
_type = remove_optional_wrapper(_type)
|
||||
_type = check_list_type(_type, value)
|
||||
_type = replace_mapping_with_dict(_type)
|
||||
|
|
@ -455,10 +456,3 @@ def add_options_to_field(
|
|||
value["options"] = options_map[class_name]
|
||||
value["list"] = True
|
||||
value["value"] = options_map[class_name][0]
|
||||
|
||||
|
||||
def get_number_of_workers(workers=None):
|
||||
if workers == -1 or workers is None:
|
||||
workers = (cpu_count() * 2) + 1
|
||||
logger.debug(f"Number of workers: {workers}")
|
||||
return workers
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue