Merge remote-tracking branch 'origin/dev' into fix_secret_key

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-08-30 16:48:04 -03:00
commit 53b9565ac6
134 changed files with 4732 additions and 1946 deletions

View file

@ -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()

View file

@ -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()

View file

@ -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 ###

View file

@ -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 ###

View file

@ -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 ###

View file

@ -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)

View file

@ -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",
]

View 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

View file

@ -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 = {

View file

@ -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)}")

View file

@ -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)

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -1,7 +0,0 @@
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
refresh_token: str
token_type: str

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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")

View file

@ -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()}

View file

@ -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 = {}

View file

@ -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(

View file

@ -1,8 +0,0 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("/health")
def get_health():
return {"status": "OK"}

View 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)

View 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

View 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

View file

@ -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()

View file

@ -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()

View file

@ -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] = []

View file

@ -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)

View file

@ -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")

View file

@ -1,4 +1,5 @@
from .flow import Flow
from .user import User
from .api_key import ApiKey
__all__ = ["Flow"]
__all__ = ["Flow", "User", "ApiKey"]

View file

@ -0,0 +1,3 @@
from .api_key import ApiKey, ApiKeyCreate, UnmaskedApiKeyRead, ApiKeyRead
__all__ = ["ApiKey", "ApiKeyCreate", "UnmaskedApiKeyRead", "ApiKeyRead"]

View file

@ -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)}"

View file

@ -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

View file

@ -0,0 +1,3 @@
from .component import Component, ComponentModel
__all__ = ["Component", "ComponentModel"]

View file

@ -0,0 +1,3 @@
from .flow import Flow, FlowCreate, FlowRead, FlowUpdate
__all__ = ["Flow", "FlowCreate", "FlowRead", "FlowUpdate"]

View file

@ -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):

View file

@ -0,0 +1,8 @@
from .user import User, UserCreate, UserRead, UserUpdate
__all__ = [
"User",
"UserCreate",
"UserRead",
"UserUpdate",
]

View 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)

View 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()

View file

@ -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]

View file

@ -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],
)

View file

@ -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"

View 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_"

View file

@ -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)

View file

@ -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)

View file

@ -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