Tai Truong
fix readme
d202ada
from __future__ import annotations
import io
import json
import re
import zipfile
from datetime import datetime, timezone
from typing import Annotated
from uuid import UUID
import orjson
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from fastapi_pagination import Page, Params, add_pagination
from fastapi_pagination.ext.sqlalchemy import paginate
from sqlmodel import and_, col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from langflow.api.utils import (
CurrentActiveUser,
DbSession,
cascade_delete_flow,
remove_api_keys,
validate_is_component,
)
from langflow.api.v1.schemas import FlowListCreate
from langflow.initial_setup.constants import STARTER_FOLDER_NAME
from langflow.services.database.models.flow import Flow, FlowCreate, FlowRead, FlowUpdate
from langflow.services.database.models.flow.model import FlowHeader
from langflow.services.database.models.flow.utils import get_webhook_component_in_flow
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
from langflow.services.database.models.folder.model import Folder
from langflow.services.database.models.transactions.crud import get_transactions_by_flow_id
from langflow.services.database.models.vertex_builds.crud import get_vertex_builds_by_flow_id
from langflow.services.deps import get_settings_service
from langflow.services.settings.service import SettingsService
# build router
router = APIRouter(prefix="/flows", tags=["Flows"])
async def _new_flow(
*,
session: AsyncSession,
flow: FlowCreate,
user_id: UUID,
):
try:
"""Create a new flow."""
if flow.user_id is None:
flow.user_id = user_id
# First check if the flow.name is unique
# there might be flows with name like: "MyFlow", "MyFlow (1)", "MyFlow (2)"
# so we need to check if the name is unique with `like` operator
# if we find a flow with the same name, we add a number to the end of the name
# based on the highest number found
if (await session.exec(select(Flow).where(Flow.name == flow.name).where(Flow.user_id == user_id))).first():
flows = (
await session.exec(
select(Flow).where(Flow.name.like(f"{flow.name} (%")).where(Flow.user_id == user_id) # type: ignore[attr-defined]
)
).all()
if flows:
extract_number = re.compile(r"\((\d+)\)$")
numbers = []
for _flow in flows:
result = extract_number.search(_flow.name)
if result:
numbers.append(int(result.groups(1)[0]))
if numbers:
flow.name = f"{flow.name} ({max(numbers) + 1})"
else:
flow.name = f"{flow.name} (1)"
# Now check if the endpoint is unique
if (
flow.endpoint_name
and (
await session.exec(
select(Flow).where(Flow.endpoint_name == flow.endpoint_name).where(Flow.user_id == user_id)
)
).first()
):
flows = (
await session.exec(
select(Flow)
.where(Flow.endpoint_name.like(f"{flow.endpoint_name}-%")) # type: ignore[union-attr]
.where(Flow.user_id == user_id)
)
).all()
if flows:
# The endpoint name is like "my-endpoint","my-endpoint-1", "my-endpoint-2"
# so we need to get the highest number and add 1
# we need to get the last part of the endpoint name
numbers = [int(flow.endpoint_name.split("-")[-1]) for flow in flows]
flow.endpoint_name = f"{flow.endpoint_name}-{max(numbers) + 1}"
else:
flow.endpoint_name = f"{flow.endpoint_name}-1"
db_flow = Flow.model_validate(flow, from_attributes=True)
db_flow.updated_at = datetime.now(timezone.utc)
if db_flow.folder_id is None:
# Make sure flows always have a folder
default_folder = (
await session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME, Folder.user_id == user_id))
).first()
if default_folder:
db_flow.folder_id = default_folder.id
session.add(db_flow)
except Exception as e:
# If it is a validation error, return the error message
if hasattr(e, "errors"):
raise HTTPException(status_code=400, detail=str(e)) from e
if isinstance(e, HTTPException):
raise
raise HTTPException(status_code=500, detail=str(e)) from e
return db_flow
@router.post("/", response_model=FlowRead, status_code=201)
async def create_flow(
*,
session: DbSession,
flow: FlowCreate,
current_user: CurrentActiveUser,
):
try:
db_flow = await _new_flow(session=session, flow=flow, user_id=current_user.id)
await session.commit()
await session.refresh(db_flow)
except Exception as e:
if "UNIQUE constraint failed" in str(e):
# Get the name of the column that failed
columns = str(e).split("UNIQUE constraint failed: ")[1].split(".")[1].split("\n")[0]
# UNIQUE constraint failed: flow.user_id, flow.name
# or UNIQUE constraint failed: flow.name
# if the column has id in it, we want the other column
column = columns.split(",")[1] if "id" in columns.split(",")[0] else columns.split(",")[0]
raise HTTPException(
status_code=400, detail=f"{column.capitalize().replace('_', ' ')} must be unique"
) from e
if isinstance(e, HTTPException):
raise
raise HTTPException(status_code=500, detail=str(e)) from e
return db_flow
@router.get("/", response_model=list[FlowRead] | Page[FlowRead] | list[FlowHeader], status_code=200)
async def read_flows(
*,
current_user: CurrentActiveUser,
session: DbSession,
remove_example_flows: bool = False,
components_only: bool = False,
get_all: bool = True,
folder_id: UUID | None = None,
params: Annotated[Params, Depends()],
header_flows: bool = False,
):
"""Retrieve a list of flows with pagination support.
Args:
current_user (User): The current authenticated user.
session (Session): The database session.
settings_service (SettingsService): The settings service.
components_only (bool, optional): Whether to return only components. Defaults to False.
get_all (bool, optional): Whether to return all flows without pagination. Defaults to True.
**This field must be True because of backward compatibility with the frontend - Release: 1.0.20**
folder_id (UUID, optional): The folder ID. Defaults to None.
params (Params): Pagination parameters.
remove_example_flows (bool, optional): Whether to remove example flows. Defaults to False.
header_flows (bool, optional): Whether to return only specific headers of the flows. Defaults to False.
Returns:
list[FlowRead] | Page[FlowRead] | list[FlowHeader]
A list of flows or a paginated response containing the list of flows or a list of flow headers.
"""
try:
auth_settings = get_settings_service().auth_settings
default_folder = (await session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME))).first()
default_folder_id = default_folder.id if default_folder else None
starter_folder = (await session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME))).first()
starter_folder_id = starter_folder.id if starter_folder else None
if not starter_folder and not default_folder:
raise HTTPException(
status_code=404,
detail="Starter folder and default folder not found. Please create a folder and add flows to it.",
)
if not folder_id:
folder_id = default_folder_id
if auth_settings.AUTO_LOGIN:
stmt = select(Flow).where(
(Flow.user_id == None) | (Flow.user_id == current_user.id) # noqa: E711
)
else:
stmt = select(Flow).where(Flow.user_id == current_user.id)
if remove_example_flows:
stmt = stmt.where(Flow.folder_id != starter_folder_id)
if components_only:
stmt = stmt.where(Flow.is_component == True) # noqa: E712
if get_all:
flows = (await session.exec(stmt)).all()
flows = validate_is_component(flows)
if components_only:
flows = [flow for flow in flows if flow.is_component]
if remove_example_flows and starter_folder_id:
flows = [flow for flow in flows if flow.folder_id != starter_folder_id]
if header_flows:
return [
{
"id": flow.id,
"name": flow.name,
"folder_id": flow.folder_id,
"is_component": flow.is_component,
"description": flow.description,
}
for flow in flows
]
return flows
stmt = stmt.where(Flow.folder_id == folder_id)
return await paginate(session, stmt, params=params)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
async def _read_flow(
session: AsyncSession,
flow_id: UUID,
user_id: UUID,
settings_service: SettingsService,
):
"""Read a flow."""
auth_settings = settings_service.auth_settings
stmt = select(Flow).where(Flow.id == flow_id)
if auth_settings.AUTO_LOGIN:
# If auto login is enable user_id can be current_user.id or None
# so write an OR
stmt = stmt.where(
(Flow.user_id == user_id) | (Flow.user_id == None) # noqa: E711
)
return (await session.exec(stmt)).first()
@router.get("/{flow_id}", response_model=FlowRead, status_code=200)
async def read_flow(
*,
session: DbSession,
flow_id: UUID,
current_user: CurrentActiveUser,
):
"""Read a flow."""
if user_flow := await _read_flow(session, flow_id, current_user.id, get_settings_service()):
return user_flow
raise HTTPException(status_code=404, detail="Flow not found")
@router.patch("/{flow_id}", response_model=FlowRead, status_code=200)
async def update_flow(
*,
session: DbSession,
flow_id: UUID,
flow: FlowUpdate,
current_user: CurrentActiveUser,
):
"""Update a flow."""
settings_service = get_settings_service()
try:
db_flow = await _read_flow(
session=session,
flow_id=flow_id,
user_id=current_user.id,
settings_service=settings_service,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
if not db_flow:
raise HTTPException(status_code=404, detail="Flow not found")
try:
flow_data = flow.model_dump(exclude_unset=True)
if settings_service.settings.remove_api_keys:
flow_data = remove_api_keys(flow_data)
for key, value in flow_data.items():
setattr(db_flow, key, value)
webhook_component = get_webhook_component_in_flow(db_flow.data)
db_flow.webhook = webhook_component is not None
db_flow.updated_at = datetime.now(timezone.utc)
if db_flow.folder_id is None:
default_folder = (await session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME))).first()
if default_folder:
db_flow.folder_id = default_folder.id
session.add(db_flow)
await session.commit()
await session.refresh(db_flow)
except Exception as e:
# If it is a validation error, return the error message
if hasattr(e, "errors"):
raise HTTPException(status_code=400, detail=str(e)) from e
if "UNIQUE constraint failed" in str(e):
# Get the name of the column that failed
columns = str(e).split("UNIQUE constraint failed: ")[1].split(".")[1].split("\n")[0]
# UNIQUE constraint failed: flow.user_id, flow.name
# or UNIQUE constraint failed: flow.name
# if the column has id in it, we want the other column
column = columns.split(",")[1] if "id" in columns.split(",")[0] else columns.split(",")[0]
raise HTTPException(
status_code=400, detail=f"{column.capitalize().replace('_', ' ')} must be unique"
) from e
raise HTTPException(status_code=500, detail=str(e)) from e
return db_flow
@router.delete("/{flow_id}", status_code=200)
async def delete_flow(
*,
session: DbSession,
flow_id: UUID,
current_user: CurrentActiveUser,
):
"""Delete a flow."""
flow = await _read_flow(
session=session,
flow_id=flow_id,
user_id=current_user.id,
settings_service=get_settings_service(),
)
if not flow:
raise HTTPException(status_code=404, detail="Flow not found")
await cascade_delete_flow(session, flow.id)
await session.commit()
return {"message": "Flow deleted successfully"}
@router.post("/batch/", response_model=list[FlowRead], status_code=201)
async def create_flows(
*,
session: DbSession,
flow_list: FlowListCreate,
current_user: CurrentActiveUser,
):
"""Create multiple new flows."""
db_flows = []
for flow in flow_list.flows:
flow.user_id = current_user.id
db_flow = Flow.model_validate(flow, from_attributes=True)
session.add(db_flow)
db_flows.append(db_flow)
await session.commit()
for db_flow in db_flows:
await session.refresh(db_flow)
return db_flows
@router.post("/upload/", response_model=list[FlowRead], status_code=201)
async def upload_file(
*,
session: DbSession,
file: Annotated[UploadFile, File(...)],
current_user: CurrentActiveUser,
folder_id: UUID | None = None,
):
"""Upload flows from a file."""
contents = await file.read()
data = orjson.loads(contents)
response_list = []
flow_list = FlowListCreate(**data) if "flows" in data else FlowListCreate(flows=[FlowCreate(**data)])
# Now we set the user_id for all flows
for flow in flow_list.flows:
flow.user_id = current_user.id
if folder_id:
flow.folder_id = folder_id
response = await _new_flow(session=session, flow=flow, user_id=current_user.id)
response_list.append(response)
try:
await session.commit()
for db_flow in response_list:
await session.refresh(db_flow)
except Exception as e:
if "UNIQUE constraint failed" in str(e):
# Get the name of the column that failed
columns = str(e).split("UNIQUE constraint failed: ")[1].split(".")[1].split("\n")[0]
# UNIQUE constraint failed: flow.user_id, flow.name
# or UNIQUE constraint failed: flow.name
# if the column has id in it, we want the other column
column = columns.split(",")[1] if "id" in columns.split(",")[0] else columns.split(",")[0]
raise HTTPException(
status_code=400, detail=f"{column.capitalize().replace('_', ' ')} must be unique"
) from e
if isinstance(e, HTTPException):
raise
raise HTTPException(status_code=500, detail=str(e)) from e
return response_list
@router.delete("/")
async def delete_multiple_flows(
flow_ids: list[UUID],
user: CurrentActiveUser,
db: DbSession,
):
"""Delete multiple flows by their IDs.
Args:
flow_ids (List[str]): The list of flow IDs to delete.
user (User, optional): The user making the request. Defaults to the current active user.
db (Session, optional): The database session.
Returns:
dict: A dictionary containing the number of flows deleted.
"""
try:
flows_to_delete = (
await db.exec(select(Flow).where(col(Flow.id).in_(flow_ids)).where(Flow.user_id == user.id))
).all()
for flow in flows_to_delete:
transactions_to_delete = await get_transactions_by_flow_id(db, flow.id)
for transaction in transactions_to_delete:
await db.delete(transaction)
builds_to_delete = await get_vertex_builds_by_flow_id(db, flow.id)
for build in builds_to_delete:
await db.delete(build)
await db.delete(flow)
await db.commit()
return {"deleted": len(flows_to_delete)}
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
@router.post("/download/", status_code=200)
async def download_multiple_file(
flow_ids: list[UUID],
user: CurrentActiveUser,
db: DbSession,
):
"""Download all flows as a zip file."""
flows = (await db.exec(select(Flow).where(and_(Flow.user_id == user.id, Flow.id.in_(flow_ids))))).all() # type: ignore[attr-defined]
if not flows:
raise HTTPException(status_code=404, detail="No flows found.")
flows_without_api_keys = [remove_api_keys(flow.model_dump()) for flow in flows]
if len(flows_without_api_keys) > 1:
# Create a byte stream to hold the ZIP file
zip_stream = io.BytesIO()
# Create a ZIP file
with zipfile.ZipFile(zip_stream, "w") as zip_file:
for flow in flows_without_api_keys:
# Convert the flow object to JSON
flow_json = json.dumps(jsonable_encoder(flow))
# Write the JSON to the ZIP file
zip_file.writestr(f"{flow['name']}.json", flow_json)
# Seek to the beginning of the byte stream
zip_stream.seek(0)
# Generate the filename with the current datetime
current_time = datetime.now(tz=timezone.utc).astimezone().strftime("%Y%m%d_%H%M%S")
filename = f"{current_time}_langflow_flows.zip"
return StreamingResponse(
zip_stream,
media_type="application/x-zip-compressed",
headers={"Content-Disposition": f"attachment; filename={filename}"},
)
return flows_without_api_keys[0]
@router.get("/basic_examples/", response_model=list[FlowRead], status_code=200)
async def read_basic_examples(
*,
session: DbSession,
):
"""Retrieve a list of basic example flows.
Args:
session (Session): The database session.
Returns:
list[FlowRead]: A list of basic example flows.
"""
try:
# Get the starter folder
starter_folder = (await session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME))).first()
if not starter_folder:
return []
# Get all flows in the starter folder
return (await session.exec(select(Flow).where(Flow.folder_id == starter_folder.id))).all()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
add_pagination(router)