|
|
|
|
|
|
|
|
import re |
|
|
from datetime import datetime, timezone |
|
|
from typing import TYPE_CHECKING, Optional |
|
|
from uuid import UUID, uuid4 |
|
|
|
|
|
import emoji |
|
|
from emoji import purely_emoji |
|
|
from fastapi import HTTPException, status |
|
|
from loguru import logger |
|
|
from pydantic import BaseModel, field_serializer, field_validator |
|
|
from sqlalchemy import Text, UniqueConstraint |
|
|
from sqlmodel import JSON, Column, Field, Relationship, SQLModel |
|
|
|
|
|
from langflow.schema import Data |
|
|
|
|
|
if TYPE_CHECKING: |
|
|
from langflow.services.database.models import TransactionTable |
|
|
from langflow.services.database.models.folder import Folder |
|
|
from langflow.services.database.models.message import MessageTable |
|
|
from langflow.services.database.models.user import User |
|
|
from langflow.services.database.models.vertex_builds.model import VertexBuildTable |
|
|
|
|
|
HEX_COLOR_LENGTH = 7 |
|
|
|
|
|
|
|
|
class FlowBase(SQLModel): |
|
|
name: str = Field(index=True) |
|
|
description: str | None = Field(default=None, sa_column=Column(Text, index=True, nullable=True)) |
|
|
icon: str | None = Field(default=None, nullable=True) |
|
|
icon_bg_color: str | None = Field(default=None, nullable=True) |
|
|
gradient: str | None = Field(default=None, nullable=True) |
|
|
data: dict | None = Field(default=None, nullable=True) |
|
|
is_component: bool | None = Field(default=False, nullable=True) |
|
|
updated_at: datetime | None = Field(default_factory=lambda: datetime.now(timezone.utc), nullable=True) |
|
|
webhook: bool | None = Field(default=False, nullable=True, description="Can be used on the webhook endpoint") |
|
|
endpoint_name: str | None = Field(default=None, nullable=True, index=True) |
|
|
tags: list[str] | None = None |
|
|
locked: bool | None = Field(default=False, nullable=True) |
|
|
|
|
|
@field_validator("endpoint_name") |
|
|
@classmethod |
|
|
def validate_endpoint_name(cls, v): |
|
|
|
|
|
if v is not None: |
|
|
if not isinstance(v, str): |
|
|
raise HTTPException( |
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, |
|
|
detail="Endpoint name must be a string", |
|
|
) |
|
|
if not re.match(r"^[a-zA-Z0-9_-]+$", v): |
|
|
raise HTTPException( |
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, |
|
|
detail="Endpoint name must contain only letters, numbers, hyphens, and underscores", |
|
|
) |
|
|
return v |
|
|
|
|
|
@field_validator("icon_bg_color") |
|
|
@classmethod |
|
|
def validate_icon_bg_color(cls, v): |
|
|
if v is not None and not isinstance(v, str): |
|
|
msg = "Icon background color must be a string" |
|
|
raise ValueError(msg) |
|
|
|
|
|
if v and not v.startswith("#"): |
|
|
msg = "Icon background color must start with #" |
|
|
raise ValueError(msg) |
|
|
|
|
|
|
|
|
if v and len(v) != HEX_COLOR_LENGTH: |
|
|
msg = "Icon background color must be 7 characters long" |
|
|
raise ValueError(msg) |
|
|
return v |
|
|
|
|
|
@field_validator("icon") |
|
|
@classmethod |
|
|
def validate_icon_atr(cls, v): |
|
|
|
|
|
|
|
|
|
|
|
if v is None: |
|
|
return v |
|
|
|
|
|
|
|
|
|
|
|
if not v.startswith(":") and not v.endswith(":"): |
|
|
return v |
|
|
if not v.startswith(":") or not v.endswith(":"): |
|
|
|
|
|
|
|
|
msg = f"Invalid emoji. {v} is not a valid emoji." |
|
|
raise ValueError(msg) |
|
|
|
|
|
emoji_value = emoji.emojize(v, variant="emoji_type") |
|
|
if v == emoji_value: |
|
|
logger.warning(f"Invalid emoji. {v} is not a valid emoji.") |
|
|
icon = emoji_value |
|
|
|
|
|
if purely_emoji(icon): |
|
|
|
|
|
return icon |
|
|
|
|
|
if v is not None and not isinstance(v, str): |
|
|
msg = "Icon must be a string" |
|
|
raise ValueError(msg) |
|
|
|
|
|
if v and not v.islower(): |
|
|
msg = "Icon must be lowercase" |
|
|
raise ValueError(msg) |
|
|
if v and not v.replace("-", "").isalpha(): |
|
|
msg = "Icon must contain only letters and hyphens" |
|
|
raise ValueError(msg) |
|
|
return v |
|
|
|
|
|
@field_validator("data") |
|
|
@classmethod |
|
|
def validate_json(cls, v): |
|
|
if not v: |
|
|
return v |
|
|
if not isinstance(v, dict): |
|
|
msg = "Flow must be a valid JSON" |
|
|
raise ValueError(msg) |
|
|
|
|
|
|
|
|
if "nodes" not in v: |
|
|
msg = "Flow must have nodes" |
|
|
raise ValueError(msg) |
|
|
if "edges" not in v: |
|
|
msg = "Flow must have edges" |
|
|
raise ValueError(msg) |
|
|
|
|
|
return v |
|
|
|
|
|
|
|
|
@field_serializer("updated_at") |
|
|
def serialize_datetime(self, value): |
|
|
if isinstance(value, datetime): |
|
|
|
|
|
|
|
|
value = value.replace(microsecond=0) |
|
|
if value.tzinfo is None: |
|
|
value = value.replace(tzinfo=timezone.utc) |
|
|
return value.isoformat() |
|
|
return value |
|
|
|
|
|
@field_validator("updated_at", mode="before") |
|
|
@classmethod |
|
|
def validate_dt(cls, v): |
|
|
if v is None: |
|
|
return v |
|
|
if isinstance(v, datetime): |
|
|
return v |
|
|
|
|
|
return datetime.fromisoformat(v) |
|
|
|
|
|
|
|
|
class Flow(FlowBase, table=True): |
|
|
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) |
|
|
data: dict | None = Field(default=None, sa_column=Column(JSON)) |
|
|
user_id: UUID | None = Field(index=True, foreign_key="user.id", nullable=True) |
|
|
user: "User" = Relationship(back_populates="flows") |
|
|
icon: str | None = Field(default=None, nullable=True) |
|
|
tags: list[str] | None = Field(sa_column=Column(JSON), default=[]) |
|
|
locked: bool | None = Field(default=False, nullable=True) |
|
|
folder_id: UUID | None = Field(default=None, foreign_key="folder.id", nullable=True, index=True) |
|
|
folder: Optional["Folder"] = Relationship(back_populates="flows") |
|
|
messages: list["MessageTable"] = Relationship(back_populates="flow") |
|
|
transactions: list["TransactionTable"] = Relationship(back_populates="flow") |
|
|
vertex_builds: list["VertexBuildTable"] = Relationship(back_populates="flow") |
|
|
|
|
|
def to_data(self): |
|
|
serialized = self.model_dump() |
|
|
data = { |
|
|
"id": serialized.pop("id"), |
|
|
"data": serialized.pop("data"), |
|
|
"name": serialized.pop("name"), |
|
|
"description": serialized.pop("description"), |
|
|
"updated_at": serialized.pop("updated_at"), |
|
|
} |
|
|
return Data(data=data) |
|
|
|
|
|
__table_args__ = ( |
|
|
UniqueConstraint("user_id", "name", name="unique_flow_name"), |
|
|
UniqueConstraint("user_id", "endpoint_name", name="unique_flow_endpoint_name"), |
|
|
) |
|
|
|
|
|
|
|
|
class FlowCreate(FlowBase): |
|
|
user_id: UUID | None = None |
|
|
folder_id: UUID | None = None |
|
|
|
|
|
|
|
|
class FlowRead(FlowBase): |
|
|
id: UUID |
|
|
user_id: UUID | None = Field() |
|
|
folder_id: UUID | None = Field() |
|
|
|
|
|
|
|
|
class FlowHeader(BaseModel): |
|
|
"""Model representing a header for a flow - Without the data. |
|
|
|
|
|
Attributes: |
|
|
----------- |
|
|
id : UUID |
|
|
Unique identifier for the flow. |
|
|
name : str |
|
|
The name of the flow. |
|
|
folder_id : UUID | None, optional |
|
|
The ID of the folder containing the flow. None if not associated with a folder. |
|
|
is_component : bool | None, optional |
|
|
Flag indicating whether the flow is a component. |
|
|
endpoint_name : str | None, optional |
|
|
The name of the endpoint associated with this flow. |
|
|
description : str | None, optional |
|
|
A description of the flow. |
|
|
""" |
|
|
|
|
|
id: UUID |
|
|
name: str |
|
|
folder_id: UUID | None = None |
|
|
is_component: bool | None = None |
|
|
endpoint_name: str | None = None |
|
|
description: str | None = None |
|
|
|
|
|
|
|
|
class FlowUpdate(SQLModel): |
|
|
name: str | None = None |
|
|
description: str | None = None |
|
|
data: dict | None = None |
|
|
folder_id: UUID | None = None |
|
|
endpoint_name: str | None = None |
|
|
locked: bool | None = None |
|
|
|
|
|
@field_validator("endpoint_name") |
|
|
@classmethod |
|
|
def validate_endpoint_name(cls, v): |
|
|
|
|
|
if v is not None: |
|
|
if not isinstance(v, str): |
|
|
raise HTTPException( |
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, |
|
|
detail="Endpoint name must be a string", |
|
|
) |
|
|
if not re.match(r"^[a-zA-Z0-9_-]+$", v): |
|
|
raise HTTPException( |
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, |
|
|
detail="Endpoint name must contain only letters, numbers, hyphens, and underscores", |
|
|
) |
|
|
return v |
|
|
|