feat: Migrate messages from monitor service to database
This commit migrates messages from the monitor service to the database. It adds a new function `migrate_messages_from_monitor_service_to_database` in the `utils.py` file, which retrieves messages from the monitor service, adds them to the database, and deletes them from the monitor service. This migration ensures that messages are stored in the database for better reliability and retrieval.
This commit is contained in:
parent
55a569c1c0
commit
10361dd3a9
5 changed files with 158 additions and 50 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
import warnings
|
import warnings
|
||||||
from typing import List, Optional
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
@ -8,17 +7,18 @@ from sqlmodel import Session, col, select
|
||||||
|
|
||||||
from langflow.schema.message import Message
|
from langflow.schema.message import Message
|
||||||
from langflow.services.database.models.message.model import MessageRead, MessageTable
|
from langflow.services.database.models.message.model import MessageRead, MessageTable
|
||||||
|
from langflow.services.database.utils import migrate_messages_from_monitor_service_to_database
|
||||||
from langflow.services.deps import session_scope
|
from langflow.services.deps import session_scope
|
||||||
|
|
||||||
|
|
||||||
def get_messages(
|
def get_messages(
|
||||||
sender: Optional[str] = None,
|
sender: str | None = None,
|
||||||
sender_name: Optional[str] = None,
|
sender_name: str | None = None,
|
||||||
session_id: Optional[str] = None,
|
session_id: str | None = None,
|
||||||
order_by: Optional[str] = "timestamp",
|
order_by: str | None = "timestamp",
|
||||||
order: Optional[str] = "DESC",
|
order: str | None = "DESC",
|
||||||
flow_id: Optional[UUID] = None,
|
flow_id: UUID | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Retrieves messages from the monitor service based on the provided filters.
|
Retrieves messages from the monitor service based on the provided filters.
|
||||||
|
|
@ -33,6 +33,7 @@ def get_messages(
|
||||||
Returns:
|
Returns:
|
||||||
List[Data]: A list of Data objects representing the retrieved messages.
|
List[Data]: A list of Data objects representing the retrieved messages.
|
||||||
"""
|
"""
|
||||||
|
migrate_messages_from_monitor_service_to_database()
|
||||||
messages_read: list[Message] = []
|
messages_read: list[Message] = []
|
||||||
with session_scope() as session:
|
with session_scope() as session:
|
||||||
stmt = select(MessageTable)
|
stmt = select(MessageTable)
|
||||||
|
|
@ -58,7 +59,7 @@ def get_messages(
|
||||||
return messages_read
|
return messages_read
|
||||||
|
|
||||||
|
|
||||||
def add_messages(messages: Message | list[Message], flow_id: Optional[str] = None):
|
def add_messages(messages: Message | list[Message], flow_id: str | None = None):
|
||||||
"""
|
"""
|
||||||
Add a message to the monitor service.
|
Add a message to the monitor service.
|
||||||
"""
|
"""
|
||||||
|
|
@ -111,8 +112,8 @@ def delete_messages(session_id: str):
|
||||||
|
|
||||||
def store_message(
|
def store_message(
|
||||||
message: Message,
|
message: Message,
|
||||||
flow_id: Optional[str] = None,
|
flow_id: str | None = None,
|
||||||
) -> List[Message]:
|
) -> list[Message]:
|
||||||
"""
|
"""
|
||||||
Stores a message in the memory.
|
Stores a message in the memory.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,24 @@ from typing import TYPE_CHECKING
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from alembic import command, util
|
from alembic import command, util
|
||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
from langflow.services.base import Service
|
|
||||||
from langflow.services.database import models # noqa
|
|
||||||
from langflow.services.database.models.user.crud import get_user_by_username
|
|
||||||
from langflow.services.database.utils import Result, TableResults
|
|
||||||
from langflow.services.deps import get_settings_service
|
|
||||||
from langflow.services.utils import teardown_superuser
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from sqlalchemy import event, inspect
|
from sqlalchemy import event, inspect
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.engine import Engine
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
from sqlmodel import Session, SQLModel, create_engine, select, text
|
from sqlmodel import Session, SQLModel, create_engine, select, text
|
||||||
|
|
||||||
|
from langflow.services.base import Service
|
||||||
|
from langflow.services.database import models # noqa
|
||||||
|
from langflow.services.database.models.user.crud import get_user_by_username
|
||||||
|
from langflow.services.database.utils import Result, TableResults, migrate_messages_from_monitor_service_to_database
|
||||||
|
from langflow.services.deps import get_settings_service
|
||||||
|
from langflow.services.utils import teardown_superuser
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from langflow.services.settings.service import SettingsService
|
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.engine import Engine
|
||||||
|
|
||||||
|
from langflow.services.settings.service import SettingsService
|
||||||
|
|
||||||
|
|
||||||
class DatabaseService(Service):
|
class DatabaseService(Service):
|
||||||
name = "database_service"
|
name = "database_service"
|
||||||
|
|
@ -205,6 +207,11 @@ class DatabaseService(Service):
|
||||||
logger.error(f"AutogenerateDiffsDetected: {exc}")
|
logger.error(f"AutogenerateDiffsDetected: {exc}")
|
||||||
if not fix:
|
if not fix:
|
||||||
raise RuntimeError(f"There's a mismatch between the models and the database.\n{exc}")
|
raise RuntimeError(f"There's a mismatch between the models and the database.\n{exc}")
|
||||||
|
try:
|
||||||
|
migrate_messages_from_monitor_service_to_database(session)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"Error migrating messages from monitor service to database: {exc}")
|
||||||
|
raise RuntimeError("Error migrating messages from monitor service to database") from exc
|
||||||
|
|
||||||
if fix:
|
if fix:
|
||||||
self.try_downgrade_upgrade_until_success(alembic_cfg)
|
self.try_downgrade_upgrade_until_success(alembic_cfg)
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,28 @@ from alembic.util.exc import CommandError
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from sqlmodel import Session, text
|
from sqlmodel import Session, text
|
||||||
|
|
||||||
|
from langflow.services.deps import get_monitor_service
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from langflow.services.database.service import DatabaseService
|
from langflow.services.database.service import DatabaseService
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_messages_from_monitor_service_to_database(session):
|
||||||
|
from langflow.schema.message import Message
|
||||||
|
from langflow.services.database.models.message import MessageTable
|
||||||
|
|
||||||
|
monitor_service = get_monitor_service()
|
||||||
|
messages_df = monitor_service.get_messages()
|
||||||
|
if not messages_df.empty:
|
||||||
|
messages_ids = []
|
||||||
|
for message in messages_df.to_dict(orient="records"):
|
||||||
|
messages_ids.append(message["index"])
|
||||||
|
message = Message(**message)
|
||||||
|
session.add(MessageTable.from_message(message))
|
||||||
|
session.commit()
|
||||||
|
monitor_service.delete_messages(messages_ids)
|
||||||
|
|
||||||
|
|
||||||
def initialize_database(fix_migration: bool = False):
|
def initialize_database(fix_migration: bool = False):
|
||||||
logger.debug("Initializing database")
|
logger.debug("Initializing database")
|
||||||
from langflow.services.deps import get_db_service
|
from langflow.services.deps import get_db_service
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,15 @@ class DefaultModel(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class TransactionModel(DefaultModel):
|
class TransactionModel(DefaultModel):
|
||||||
index: Optional[int] = Field(default=None)
|
index: int | None = Field(default=None)
|
||||||
timestamp: Optional[datetime] = Field(default_factory=datetime.now, alias="timestamp")
|
timestamp: datetime | None = Field(default_factory=datetime.now, alias="timestamp")
|
||||||
vertex_id: str
|
vertex_id: str
|
||||||
target_id: str | None = None
|
target_id: str | None = None
|
||||||
inputs: dict
|
inputs: dict
|
||||||
outputs: Optional[dict] = None
|
outputs: dict | None = None
|
||||||
status: str
|
status: str
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
flow_id: Optional[str] = Field(default=None, alias="flow_id")
|
flow_id: str | None = Field(default=None, alias="flow_id")
|
||||||
|
|
||||||
# validate target_args in case it is a JSON
|
# validate target_args in case it is a JSON
|
||||||
@field_validator("outputs", "inputs", mode="before")
|
@field_validator("outputs", "inputs", mode="before")
|
||||||
|
|
@ -53,16 +53,16 @@ class TransactionModel(DefaultModel):
|
||||||
|
|
||||||
|
|
||||||
class TransactionModelResponse(DefaultModel):
|
class TransactionModelResponse(DefaultModel):
|
||||||
index: Optional[int] = Field(default=None)
|
index: int | None = Field(default=None)
|
||||||
timestamp: Optional[datetime] = Field(default_factory=datetime.now, alias="timestamp")
|
timestamp: datetime | None = Field(default_factory=datetime.now, alias="timestamp")
|
||||||
vertex_id: str
|
vertex_id: str
|
||||||
inputs: dict
|
inputs: dict
|
||||||
outputs: Optional[dict] = None
|
outputs: dict | None = None
|
||||||
status: str
|
status: str
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
flow_id: Optional[str] = Field(default=None, alias="flow_id")
|
flow_id: str | None = Field(default=None, alias="flow_id")
|
||||||
source: Optional[str] = None
|
source: str | None = None
|
||||||
target: Optional[str] = None
|
target: str | None = None
|
||||||
|
|
||||||
# validate target_args in case it is a JSON
|
# validate target_args in case it is a JSON
|
||||||
@field_validator("outputs", "inputs", mode="before")
|
@field_validator("outputs", "inputs", mode="before")
|
||||||
|
|
@ -81,9 +81,9 @@ class TransactionModelResponse(DefaultModel):
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class MessageModel(DefaultModel):
|
class DeprecatedMessageModel(DefaultModel):
|
||||||
id: Optional[str | UUID] = Field(default=None)
|
index: int | None = Field(default=None)
|
||||||
flow_id: Optional[UUID] = Field(default=None)
|
flow_id: str | None = Field(default=None, alias="flow_id")
|
||||||
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
sender: str
|
sender: str
|
||||||
sender_name: str
|
sender_name: str
|
||||||
|
|
@ -112,7 +112,53 @@ class MessageModel(DefaultModel):
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_message(cls, message: Message, flow_id: Optional[str] = None):
|
def from_message(cls, message: Message, flow_id: str | None = None):
|
||||||
|
# first check if the record has all the required fields
|
||||||
|
if message.text is None or not message.sender or not message.sender_name:
|
||||||
|
raise ValueError("The message does not have the required fields (text, sender, sender_name).")
|
||||||
|
return cls(
|
||||||
|
sender=message.sender,
|
||||||
|
sender_name=message.sender_name,
|
||||||
|
text=message.text,
|
||||||
|
session_id=message.session_id,
|
||||||
|
files=message.files or [],
|
||||||
|
timestamp=message.timestamp,
|
||||||
|
flow_id=flow_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageModel(DefaultModel):
|
||||||
|
id: str | UUID | None = Field(default=None)
|
||||||
|
flow_id: UUID | None = Field(default=None)
|
||||||
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
sender: str
|
||||||
|
sender_name: str
|
||||||
|
session_id: str
|
||||||
|
text: str
|
||||||
|
files: list[str] = []
|
||||||
|
|
||||||
|
@field_validator("files", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def validate_files(cls, v):
|
||||||
|
if isinstance(v, str):
|
||||||
|
v = json.loads(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
@field_serializer("timestamp")
|
||||||
|
@classmethod
|
||||||
|
def serialize_timestamp(cls, v):
|
||||||
|
v = v.replace(microsecond=0)
|
||||||
|
return v.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
@field_serializer("files")
|
||||||
|
@classmethod
|
||||||
|
def serialize_files(cls, v):
|
||||||
|
if isinstance(v, list):
|
||||||
|
return json.dumps(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_message(cls, message: Message, flow_id: str | None = None):
|
||||||
# first check if the record has all the required fields
|
# first check if the record has all the required fields
|
||||||
if message.text is None or not message.sender or not message.sender_name:
|
if message.text is None or not message.sender or not message.sender_name:
|
||||||
raise ValueError("The message does not have the required fields (text, sender, sender_name).")
|
raise ValueError("The message does not have the required fields (text, sender, sender_name).")
|
||||||
|
|
@ -139,8 +185,8 @@ class MessageModelRequest(MessageModel):
|
||||||
|
|
||||||
|
|
||||||
class VertexBuildModel(DefaultModel):
|
class VertexBuildModel(DefaultModel):
|
||||||
index: Optional[int] = Field(default=None, alias="index", exclude=True)
|
index: int | None = Field(default=None, alias="index", exclude=True)
|
||||||
id: Optional[str] = Field(default=None, alias="id")
|
id: str | None = Field(default=None, alias="id")
|
||||||
flow_id: str
|
flow_id: str
|
||||||
valid: bool
|
valid: bool
|
||||||
params: Any
|
params: Any
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,14 @@ class MonitorService(Service):
|
||||||
name = "monitor_service"
|
name = "monitor_service"
|
||||||
|
|
||||||
def __init__(self, settings_service: "SettingsService"):
|
def __init__(self, settings_service: "SettingsService"):
|
||||||
from langflow.services.monitor.schema import MessageModel, TransactionModel, VertexBuildModel
|
from langflow.services.monitor.schema import DeprecatedMessageModel, TransactionModel, VertexBuildModel
|
||||||
|
|
||||||
self.settings_service = settings_service
|
self.settings_service = settings_service
|
||||||
self.base_cache_dir = Path(user_cache_dir("langflow"))
|
self.base_cache_dir = Path(user_cache_dir("langflow"))
|
||||||
self.db_path = self.base_cache_dir / "monitor.duckdb"
|
self.db_path = self.base_cache_dir / "monitor.duckdb"
|
||||||
self.table_map: dict[str, type[TransactionModel | MessageModel | VertexBuildModel]] = {
|
self.table_map: dict[str, type[TransactionModel | DeprecatedMessageModel | VertexBuildModel]] = {
|
||||||
"transactions": TransactionModel,
|
"transactions": TransactionModel,
|
||||||
"messages": MessageModel,
|
"messages": DeprecatedMessageModel,
|
||||||
"vertex_builds": VertexBuildModel,
|
"vertex_builds": VertexBuildModel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,12 +68,48 @@ class MonitorService(Service):
|
||||||
def get_timestamp():
|
def get_timestamp():
|
||||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
def get_messages(
|
||||||
|
self,
|
||||||
|
flow_id: str | None = None,
|
||||||
|
sender: str | None = None,
|
||||||
|
sender_name: str | None = None,
|
||||||
|
session_id: str | None = None,
|
||||||
|
order_by: str | None = "timestamp",
|
||||||
|
order: str | None = "DESC",
|
||||||
|
limit: int | None = None,
|
||||||
|
):
|
||||||
|
query = "SELECT index, flow_id, sender_name, sender, session_id, text, files, timestamp FROM messages"
|
||||||
|
conditions = []
|
||||||
|
if sender:
|
||||||
|
conditions.append(f"sender = '{sender}'")
|
||||||
|
if sender_name:
|
||||||
|
conditions.append(f"sender_name = '{sender_name}'")
|
||||||
|
if session_id:
|
||||||
|
conditions.append(f"session_id = '{session_id}'")
|
||||||
|
if flow_id:
|
||||||
|
conditions.append(f"flow_id = '{flow_id}'")
|
||||||
|
|
||||||
|
if conditions:
|
||||||
|
query += " WHERE " + " AND ".join(conditions)
|
||||||
|
|
||||||
|
if order_by and order:
|
||||||
|
# Make sure the order is from newest to oldest
|
||||||
|
query += f" ORDER BY {order_by} {order.upper()}"
|
||||||
|
|
||||||
|
if limit is not None:
|
||||||
|
query += f" LIMIT {limit}"
|
||||||
|
|
||||||
|
with duckdb.connect(str(self.db_path), read_only=True) as conn:
|
||||||
|
df = conn.execute(query).df()
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
def get_vertex_builds(
|
def get_vertex_builds(
|
||||||
self,
|
self,
|
||||||
flow_id: Optional[str] = None,
|
flow_id: str | None = None,
|
||||||
vertex_id: Optional[str] = None,
|
vertex_id: str | None = None,
|
||||||
valid: Optional[bool] = None,
|
valid: bool | None = None,
|
||||||
order_by: Optional[str] = "timestamp",
|
order_by: str | None = "timestamp",
|
||||||
):
|
):
|
||||||
query = "SELECT id, index,flow_id, valid, params, data, artifacts, timestamp FROM vertex_builds"
|
query = "SELECT id, index,flow_id, valid, params, data, artifacts, timestamp FROM vertex_builds"
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
@ -96,7 +132,7 @@ class MonitorService(Service):
|
||||||
|
|
||||||
return df.to_dict(orient="records")
|
return df.to_dict(orient="records")
|
||||||
|
|
||||||
def delete_vertex_builds(self, flow_id: Optional[str] = None):
|
def delete_vertex_builds(self, flow_id: str | None = None):
|
||||||
query = "DELETE FROM vertex_builds"
|
query = "DELETE FROM vertex_builds"
|
||||||
if flow_id:
|
if flow_id:
|
||||||
query += f" WHERE flow_id = '{flow_id}'"
|
query += f" WHERE flow_id = '{flow_id}'"
|
||||||
|
|
@ -109,7 +145,7 @@ class MonitorService(Service):
|
||||||
|
|
||||||
return self.exec_query(query, read_only=False)
|
return self.exec_query(query, read_only=False)
|
||||||
|
|
||||||
def delete_messages(self, message_ids: Union[List[int], str]):
|
def delete_messages(self, message_ids: list[int] | str):
|
||||||
if isinstance(message_ids, list):
|
if isinstance(message_ids, list):
|
||||||
# If message_ids is a list, join the string representations of the integers
|
# If message_ids is a list, join the string representations of the integers
|
||||||
ids_str = ",".join(map(str, message_ids))
|
ids_str = ",".join(map(str, message_ids))
|
||||||
|
|
@ -132,11 +168,11 @@ class MonitorService(Service):
|
||||||
|
|
||||||
def get_transactions(
|
def get_transactions(
|
||||||
self,
|
self,
|
||||||
source: Optional[str] = None,
|
source: str | None = None,
|
||||||
target: Optional[str] = None,
|
target: str | None = None,
|
||||||
status: Optional[str] = None,
|
status: str | None = None,
|
||||||
order_by: Optional[str] = "timestamp",
|
order_by: str | None = "timestamp",
|
||||||
flow_id: Optional[str] = None,
|
flow_id: str | None = None,
|
||||||
):
|
):
|
||||||
query = (
|
query = (
|
||||||
"SELECT index,flow_id, status, error, timestamp, vertex_id, inputs, outputs, target_id FROM transactions"
|
"SELECT index,flow_id, status, error, timestamp, vertex_id, inputs, outputs, target_id FROM transactions"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue