📦 chore(database): add database related files and models
📦 chore(database): add database manager and factory 📦 chore(database): add flow and flow style models
This commit is contained in:
parent
b451ad9bdd
commit
d51aa7ecb2
8 changed files with 306 additions and 0 deletions
0
src/backend/langflow/services/database/__init__.py
Normal file
0
src/backend/langflow/services/database/__init__.py
Normal file
151
src/backend/langflow/services/database/base.py
Normal file
151
src/backend/langflow/services/database/base.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
from contextlib import contextmanager
|
||||
import os
|
||||
from pathlib import Path
|
||||
from langflow.services.base import Service
|
||||
from sqlmodel import SQLModel, Session, create_engine
|
||||
from langflow.utils.logger import logger
|
||||
from alembic.config import Config
|
||||
from alembic import command
|
||||
|
||||
|
||||
class Engine:
|
||||
_instance = None
|
||||
|
||||
@classmethod
|
||||
def get(cls):
|
||||
logger.debug("Getting database engine")
|
||||
if cls._instance is None:
|
||||
cls.create()
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def create(cls):
|
||||
logger.debug("Creating database engine")
|
||||
from langflow.settings import settings
|
||||
|
||||
if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"):
|
||||
settings.DATABASE_URL = langflow_database_url
|
||||
logger.debug("Using LANGFLOW_DATABASE_URL")
|
||||
|
||||
if settings.DATABASE_URL and settings.DATABASE_URL.startswith("sqlite"):
|
||||
connect_args = {"check_same_thread": False}
|
||||
else:
|
||||
connect_args = {}
|
||||
if not settings.DATABASE_URL:
|
||||
raise RuntimeError("No database_url provided")
|
||||
cls._instance = create_engine(settings.DATABASE_URL, connect_args=connect_args)
|
||||
|
||||
@classmethod
|
||||
def update(cls):
|
||||
logger.debug("Updating database engine")
|
||||
cls._instance = None
|
||||
cls.create()
|
||||
|
||||
|
||||
def create_db_and_tables():
|
||||
logger.debug("Creating database and tables")
|
||||
try:
|
||||
SQLModel.metadata.create_all(Engine.get())
|
||||
except Exception as exc:
|
||||
logger.error(f"Error creating database and tables: {exc}")
|
||||
raise RuntimeError("Error creating database and tables") from exc
|
||||
# Now check if the table Flow exists, if not, something went wrong
|
||||
# and we need to create the tables again.
|
||||
from sqlalchemy import inspect
|
||||
|
||||
inspector = inspect(Engine.get())
|
||||
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")
|
||||
|
||||
|
||||
class DatabaseManager(Service):
|
||||
name = "database_manager"
|
||||
|
||||
def __init__(self, database_url: str):
|
||||
self.database_url = database_url
|
||||
# This file is in langflow.services.database.base.py
|
||||
# the ini is in langflow
|
||||
langflow_dir = Path(__file__).parent.parent.parent
|
||||
self.script_location = langflow_dir / "alembic"
|
||||
self.alembic_cfg_path = langflow_dir / "alembic.ini"
|
||||
self.engine = create_engine(database_url)
|
||||
|
||||
def __enter__(self):
|
||||
self._session = Session(self.engine)
|
||||
return self._session
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if exc_type is not None: # If an exception has been raised
|
||||
logger.error(
|
||||
f"Session rollback because of exception: {exc_type.__name__} {exc_value}"
|
||||
)
|
||||
self._session.rollback()
|
||||
else:
|
||||
self._session.commit()
|
||||
self._session.close()
|
||||
|
||||
def get_session(self):
|
||||
with Session(self.engine) as session:
|
||||
yield session
|
||||
|
||||
def run_migrations(self):
|
||||
logger.info(
|
||||
f"Running DB migrations in {self.script_location} on {self.database_url}"
|
||||
)
|
||||
alembic_cfg = Config()
|
||||
alembic_cfg.set_main_option("script_location", str(self.script_location))
|
||||
alembic_cfg.set_main_option("sqlalchemy.url", self.database_url)
|
||||
command.upgrade(alembic_cfg, "head")
|
||||
|
||||
def create_db_and_tables(self):
|
||||
logger.debug("Creating database and tables")
|
||||
try:
|
||||
SQLModel.metadata.create_all(self.engine)
|
||||
except Exception as exc:
|
||||
logger.error(f"Error creating database and tables: {exc}")
|
||||
raise RuntimeError("Error creating database and tables") from exc
|
||||
|
||||
# Now check if the table "flow" exists, if not, something went wrong
|
||||
# and we need to create the tables again.
|
||||
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")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def session_getter(db_manager: DatabaseManager):
|
||||
try:
|
||||
session = Session(DatabaseManager.engine)
|
||||
yield session
|
||||
except Exception as e:
|
||||
print("Session rollback because of exception:", e)
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def get_session():
|
||||
with Session(Engine.get()) as session:
|
||||
yield session
|
||||
|
||||
|
||||
def initialize_database():
|
||||
logger.debug("Initializing database")
|
||||
from langflow.services import service_manager, ServiceType
|
||||
|
||||
database_manager = service_manager.get(ServiceType.DATABASE_MANAGER)
|
||||
database_manager.run_migrations()
|
||||
database_manager.create_db_and_tables()
|
||||
logger.debug("Database initialized")
|
||||
15
src/backend/langflow/services/database/factory.py
Normal file
15
src/backend/langflow/services/database/factory.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from typing import TYPE_CHECKING
|
||||
from langflow.services.database.base import DatabaseManager
|
||||
from langflow.services.factory import ServiceFactory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.settings.manager import SettingsManager
|
||||
|
||||
|
||||
class DatabaseManagerFactory(ServiceFactory):
|
||||
def __init__(self):
|
||||
super().__init__(DatabaseManager)
|
||||
|
||||
def create(self, settings_service: "SettingsManager"):
|
||||
# Here you would have logic to create and configure a DatabaseManager
|
||||
return DatabaseManager(settings_service.settings.DATABASE_URL)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from .flow import Flow
|
||||
|
||||
|
||||
__all__ = ["Flow"]
|
||||
14
src/backend/langflow/services/database/models/base.py
Normal file
14
src/backend/langflow/services/database/models/base.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from sqlmodel import SQLModel
|
||||
import orjson
|
||||
|
||||
|
||||
def orjson_dumps(v, *, default):
|
||||
# orjson.dumps returns bytes, to match standard json.dumps we need to decode
|
||||
return orjson.dumps(v, default=default).decode()
|
||||
|
||||
|
||||
class SQLModelSerializable(SQLModel):
|
||||
class Config:
|
||||
orm_mode = True
|
||||
json_loads = orjson.loads
|
||||
json_dumps = orjson_dumps
|
||||
29
src/backend/langflow/services/database/models/component.py
Normal file
29
src/backend/langflow/services/database/models/component.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from langflow.services.database.models.base import SQLModelSerializable, SQLModel
|
||||
from sqlmodel import Field
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
|
||||
class Component(SQLModelSerializable, table=True):
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
frontend_node_id: uuid.UUID = Field(index=True)
|
||||
name: str = Field(index=True)
|
||||
description: Optional[str] = Field(default=None)
|
||||
python_code: Optional[str] = Field(default=None)
|
||||
return_type: Optional[str] = Field(default=None)
|
||||
is_disabled: bool = Field(default=False)
|
||||
is_read_only: bool = Field(default=False)
|
||||
create_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
update_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
|
||||
class ComponentModel(SQLModel):
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
||||
frontend_node_id: uuid.UUID = Field(default=uuid.uuid4())
|
||||
name: str = Field(default="")
|
||||
description: Optional[str] = None
|
||||
python_code: Optional[str] = None
|
||||
return_type: Optional[str] = None
|
||||
is_disabled: bool = False
|
||||
is_read_only: bool = False
|
||||
60
src/backend/langflow/services/database/models/flow.py
Normal file
60
src/backend/langflow/services/database/models/flow.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Path: src/backend/langflow/database/models/flow.py
|
||||
|
||||
from langflow.services.database.models.base import SQLModelSerializable
|
||||
from pydantic import validator
|
||||
from sqlmodel import Field, Relationship, JSON, Column
|
||||
from uuid import UUID, uuid4
|
||||
from typing import Dict, Optional
|
||||
|
||||
# if TYPE_CHECKING:
|
||||
from langflow.services.database.models.flow_style import FlowStyle, FlowStyleRead
|
||||
|
||||
|
||||
class FlowBase(SQLModelSerializable):
|
||||
name: str = Field(index=True)
|
||||
description: Optional[str] = Field(index=True)
|
||||
data: Optional[Dict] = Field(default=None)
|
||||
|
||||
@validator("data")
|
||||
def validate_json(v):
|
||||
# dict_keys(['description', 'name', 'id', 'data'])
|
||||
if not v:
|
||||
return v
|
||||
if not isinstance(v, dict):
|
||||
raise ValueError("Flow must be a valid JSON")
|
||||
|
||||
# data must contain nodes and edges
|
||||
if "nodes" not in v.keys():
|
||||
raise ValueError("Flow must have nodes")
|
||||
if "edges" not in v.keys():
|
||||
raise ValueError("Flow must have edges")
|
||||
|
||||
return v
|
||||
|
||||
|
||||
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))
|
||||
style: Optional["FlowStyle"] = Relationship(
|
||||
back_populates="flow",
|
||||
# use "uselist=False" to make it a one-to-one relationship
|
||||
sa_relationship_kwargs={"uselist": False},
|
||||
)
|
||||
|
||||
|
||||
class FlowCreate(FlowBase):
|
||||
pass
|
||||
|
||||
|
||||
class FlowRead(FlowBase):
|
||||
id: UUID
|
||||
|
||||
|
||||
class FlowReadWithStyle(FlowRead):
|
||||
style: Optional["FlowStyleRead"] = None
|
||||
|
||||
|
||||
class FlowUpdate(SQLModelSerializable):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
data: Optional[Dict] = None
|
||||
33
src/backend/langflow/services/database/models/flow_style.py
Normal file
33
src/backend/langflow/services/database/models/flow_style.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Path: src/backend/langflow/database/models/flowstyle.py
|
||||
|
||||
from langflow.services.database.models.base import SQLModelSerializable
|
||||
from sqlmodel import Field, Relationship
|
||||
from uuid import UUID, uuid4
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.database.models.flow import Flow
|
||||
|
||||
|
||||
class FlowStyleBase(SQLModelSerializable):
|
||||
color: str
|
||||
emoji: str
|
||||
flow_id: UUID = Field(default=None, foreign_key="flow.id")
|
||||
|
||||
|
||||
class FlowStyle(FlowStyleBase, table=True):
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||
flow: "Flow" = Relationship(back_populates="style")
|
||||
|
||||
|
||||
class FlowStyleUpdate(SQLModelSerializable):
|
||||
color: Optional[str] = None
|
||||
emoji: Optional[str] = None
|
||||
|
||||
|
||||
class FlowStyleCreate(FlowStyleBase):
|
||||
pass
|
||||
|
||||
|
||||
class FlowStyleRead(FlowStyleBase):
|
||||
id: UUID
|
||||
Loading…
Add table
Add a link
Reference in a new issue