Merge branch 'dev' into cz/fixTestsCI

This commit is contained in:
cristhianzl 2024-06-26 12:06:00 -03:00
commit dad0aa282f
64 changed files with 981 additions and 441 deletions

View file

@ -5,7 +5,7 @@ on:
paths: paths:
- "src/frontend/**" - "src/frontend/**"
merge_group: merge_group:
branches: [dev] types: [checks_requested]
env: env:
NODE_VERSION: "21" NODE_VERSION: "21"
@ -45,10 +45,6 @@ jobs:
- name: Run Prettier - name: Run Prettier
run: | run: |
cd src/frontend cd src/frontend
npm run format npm run check-format
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Apply Prettier formatting
branch: ${{ github.head_ref }}

View file

@ -45,7 +45,7 @@ jobs:
poetry run python -m langflow run --host 127.0.0.1 --port 7860 --backend-only & poetry run python -m langflow run --host 127.0.0.1 --port 7860 --backend-only &
SERVER_PID=$! SERVER_PID=$!
# Wait for the server to start # Wait for the server to start
timeout 120 bash -c 'until curl -f http://127.0.0.1:7860/auto_login; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1) timeout 120 bash -c 'until curl -f http://127.0.0.1:7860/api/v1/auto_login; do sleep 5; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1)
# Terminate the server # Terminate the server
kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1) kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1)
sleep 10 # give the server some time to terminate sleep 10 # give the server some time to terminate

View file

@ -62,7 +62,7 @@ jobs:
python -m langflow run --host 127.0.0.1 --port 7860 & python -m langflow run --host 127.0.0.1 --port 7860 &
SERVER_PID=$! SERVER_PID=$!
# Wait for the server to start # Wait for the server to start
timeout 120 bash -c 'until curl -f http://127.0.0.1:7860/auto_login; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1) timeout 120 bash -c 'until curl -f http://127.0.0.1:7860/api/v1/auto_login; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1)
# Terminate the server # Terminate the server
kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1) kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1)
sleep 10 # give the server some time to terminate sleep 10 # give the server some time to terminate
@ -124,7 +124,7 @@ jobs:
python -m langflow run --host 127.0.0.1 --port 7860 & python -m langflow run --host 127.0.0.1 --port 7860 &
SERVER_PID=$! SERVER_PID=$!
# Wait for the server to start # Wait for the server to start
timeout 120 bash -c 'until curl -f http://127.0.0.1:7860/auto_login; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1) timeout 120 bash -c 'until curl -f http://127.0.0.1:7860/api/v1/auto_login; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1)
# Terminate the server # Terminate the server
kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1) kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1)
sleep 10 # give the server some time to terminate sleep 10 # give the server some time to terminate

View file

@ -47,20 +47,22 @@ init:
coverage: ## run the tests and generate a coverage report coverage: ## run the tests and generate a coverage report
poetry run pytest --cov \ @poetry run coverage run
--cov-config=.coveragerc \ @poetry run coverage erase
--cov-report xml \
--cov-report term-missing:skip-covered \
--cov-report lcov:coverage/lcov-pytest.info
# allow passing arguments to pytest # allow passing arguments to pytest
unit_tests: unit_tests:
poetry run pytest --ignore=tests/integration --instafail -ra -n auto -m "not api_key_required" $(args) poetry run pytest \
--ignore=tests/integration \
--instafail -ra -n auto -m "not api_key_required" \
$(args)
integration_tests: integration_tests:
poetry run pytest tests/integration --instafail -ra -n auto $(args) poetry run pytest tests/integration \
--instafail -ra -n auto \
$(args)
format: ## run code formatters format: ## run code formatters
poetry run ruff check . --fix poetry run ruff check . --fix
@ -129,9 +131,20 @@ start:
@echo 'Running the CLI' @echo 'Running the CLI'
ifeq ($(open_browser),false) ifeq ($(open_browser),false)
@make install_backend && poetry run langflow run --path $(path) --log-level $(log_level) --host $(host) --port $(port) --env-file $(env) --no-open-browser @make install_backend && poetry run langflow run \
--path $(path) \
--log-level $(log_level) \
--host $(host) \
--port $(port) \
--env-file $(env) \
--no-open-browser
else else
@make install_backend && poetry run langflow run --path $(path) --log-level $(log_level) --host $(host) --port $(port) --env-file $(env) @make install_backend && poetry run langflow run \
--path $(path) \
--log-level $(log_level) \
--host $(host) \
--port $(port) \
--env-file $(env)
endif endif
@ -166,13 +179,27 @@ backend: ## run the backend in development mode
@echo 'Setting up the environment' @echo 'Setting up the environment'
@make setup_env @make setup_env
make install_backend make install_backend
@-kill -9 $(lsof -t -i:7860) @-kill -9 $$(lsof -t -i:7860)
ifdef login ifdef login
@echo "Running backend autologin is $(login)"; @echo "Running backend autologin is $(login)";
LANGFLOW_AUTO_LOGIN=$(login) poetry run uvicorn --factory langflow.main:create_app --host 0.0.0.0 --port 7860 --reload --env-file .env --loop asyncio --workers $(workers) LANGFLOW_AUTO_LOGIN=$(login) poetry run uvicorn \
--factory langflow.main:create_app \
--host 0.0.0.0 \
--port $(port) \
--reload \
--env-file $(env) \
--loop asyncio \
--workers $(workers)
else else
@echo "Running backend respecting the .env file"; @echo "Running backend respecting the $(env) file";
poetry run uvicorn --factory langflow.main:create_app --host 0.0.0.0 --port 7860 --reload --env-file .env --loop asyncio --workers $(workers) poetry run uvicorn \
--factory langflow.main:create_app \
--host 0.0.0.0 \
--port $(port) \
--reload \
--env-file $(env) \
--loop asyncio \
--workers $(workers)
endif endif

View file

@ -1,10 +1,6 @@
import Admonition from "@theme/Admonition";
# Kubernetes # Kubernetes
<Admonition type="warning" title="warning">
This page may contain outdated information. It will be updated as soon as possible.
</Admonition>
This guide will help you get LangFlow up and running in Kubernetes cluster, including the following steps: This guide will help you get LangFlow up and running in Kubernetes cluster, including the following steps:

View file

@ -151,6 +151,28 @@ log_cli = true
markers = ["async_test", "api_key_required"] markers = ["async_test", "api_key_required"]
[tool.coverage.run]
command_line = """
-m pytest
--cov --cov-report=term --cov-report=html
--instafail -ra -n auto -m "not api_key_required"
tests/unit
"""
source = ["src/backend/base/langflow/"]
omit = ["*/alembic/*", "tests/*", "*/__init__.py"]
[tool.coverage.report]
sort = "Stmts"
skip_empty = true
show_missing = false
ignore_errors = true
[tool.coverage.html]
directory = "coverage"
[tool.ruff] [tool.ruff]
exclude = ["src/backend/langflow/alembic/*"] exclude = ["src/backend/langflow/alembic/*"]
line-length = 120 line-length = 120

View file

@ -0,0 +1,52 @@
"""Add message table
Revision ID: 325180f0c4e1
Revises: 631faacf5da2
Create Date: 2024-06-23 21:29:28.220100
"""
from typing import Sequence, Union
import sqlalchemy as sa
import sqlmodel
from alembic import op
from langflow.utils import migration
# revision identifiers, used by Alembic.
revision: str = "325180f0c4e1"
down_revision: Union[str, None] = "631faacf5da2"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
# ### commands auto generated by Alembic - please adjust! ###
if not migration.table_exists("message", conn):
op.create_table(
"message",
sa.Column("timestamp", sa.DateTime(), nullable=False),
sa.Column("sender", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("sender_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("session_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("text", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
sa.Column("flow_id", sqlmodel.sql.sqltypes.GUID(), nullable=True),
sa.Column("files", sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(
["flow_id"],
["flow.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
conn = op.get_bind()
# ### commands auto generated by Alembic - please adjust! ###
if migration.table_exists("message", conn):
op.drop_table("message")
# ### end Alembic commands ###

View file

@ -1,15 +1,15 @@
from typing import List, Optional from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import delete
from sqlmodel import Session, col, select
from langflow.services.deps import get_monitor_service from langflow.services.auth.utils import get_current_active_user
from langflow.services.monitor.schema import ( from langflow.services.database.models.message.model import MessageRead, MessageTable, MessageUpdate
MessageModelRequest, from langflow.services.database.models.user.model import User
MessageModelResponse, from langflow.services.deps import get_monitor_service, get_session
TransactionModelResponse, from langflow.services.monitor.schema import MessageModelResponse, TransactionModelResponse, VertexBuildMapModel
VertexBuildMapModel,
)
from langflow.services.monitor.service import MonitorService from langflow.services.monitor.service import MonitorService
router = APIRouter(prefix="/monitor", tags=["Monitor"]) router = APIRouter(prefix="/monitor", tags=["Monitor"])
@ -52,45 +52,58 @@ async def get_messages(
sender: Optional[str] = Query(None), sender: Optional[str] = Query(None),
sender_name: Optional[str] = Query(None), sender_name: Optional[str] = Query(None),
order_by: Optional[str] = Query("timestamp"), order_by: Optional[str] = Query("timestamp"),
monitor_service: MonitorService = Depends(get_monitor_service), session: Session = Depends(get_session),
): ):
try: try:
df = monitor_service.get_messages( stmt = select(MessageTable)
flow_id=flow_id, if flow_id:
sender=sender, stmt = stmt.where(MessageTable.flow_id == flow_id)
sender_name=sender_name, if session_id:
session_id=session_id, stmt = stmt.where(MessageTable.session_id == session_id)
order_by=order_by, if sender:
) stmt = stmt.where(MessageTable.sender == sender)
dicts = df.to_dict(orient="records") if sender_name:
return [MessageModelResponse(**d) for d in dicts] stmt = stmt.where(MessageTable.sender_name == sender_name)
if order_by:
col = getattr(MessageTable, order_by).asc()
stmt = stmt.order_by(col)
messages = session.exec(stmt)
return [MessageModelResponse.model_validate(d, from_attributes=True) for d in messages]
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.delete("/messages", status_code=204) @router.delete("/messages", status_code=204)
async def delete_messages( async def delete_messages(
message_ids: List[int], message_ids: List[UUID],
monitor_service: MonitorService = Depends(get_monitor_service), session: Session = Depends(get_session),
current_user: User = Depends(get_current_active_user),
): ):
try: try:
monitor_service.delete_messages(message_ids=message_ids) session.exec(select(MessageTable).where(MessageTable.id.in_(message_ids))) # type: ignore
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.post("/messages/{message_id}", response_model=MessageModelResponse) @router.put("/messages/{message_id}", response_model=MessageRead)
async def update_message( async def update_message(
message_id: int, message_id: UUID,
message: MessageModelRequest, message: MessageUpdate,
monitor_service: MonitorService = Depends(get_monitor_service), session: Session = Depends(get_session),
user: User = Depends(get_current_active_user),
): ):
try: try:
message_dict = message.model_dump(exclude_none=True) db_message = session.get(MessageTable, message_id)
message_dict.pop("index", None) if not db_message:
monitor_service.update_message(message_id=message_id, **message_dict) # type: ignore raise HTTPException(status_code=404, detail="Message not found")
return MessageModelResponse(index=message_id, **message_dict) message_dict = message.model_dump(exclude_unset=True, exclude_none=True)
db_message.sqlmodel_update(message_dict)
session.add(db_message)
session.commit()
session.refresh(db_message)
return db_message
except HTTPException as e:
raise e
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@ -98,10 +111,16 @@ async def update_message(
@router.delete("/messages/session/{session_id}", status_code=204) @router.delete("/messages/session/{session_id}", status_code=204)
async def delete_messages_session( async def delete_messages_session(
session_id: str, session_id: str,
monitor_service: MonitorService = Depends(get_monitor_service), session: Session = Depends(get_session),
): ):
try: try:
monitor_service.delete_messages_session(session_id=session_id) session.exec( # type: ignore
delete(MessageTable)
.where(col(MessageTable.session_id) == session_id)
.execution_options(synchronize_session="fetch")
)
session.commit()
return {"message": "Messages deleted successfully"}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@ -137,4 +156,3 @@ async def get_transactions(
return result return result
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e))

View file

@ -119,7 +119,11 @@ class LCModelComponent(Component):
return status_message return status_message
def get_chat_result( def get_chat_result(
self, runnable: LanguageModel, stream: bool, input_value: str | Message, system_message: Optional[str] = None self,
runnable: LanguageModel,
stream: bool,
input_value: str | Message,
system_message: Optional[str] = None,
): ):
messages: list[Union[BaseMessage]] = [] messages: list[Union[BaseMessage]] = []
if not input_value and not system_message: if not input_value and not system_message:

View file

@ -96,7 +96,7 @@ class CustomComponent(BaseComponent):
def stop(self, output_name: str | None = None): def stop(self, output_name: str | None = None):
if not output_name and self.vertex and len(self.vertex.outputs) == 1: if not output_name and self.vertex and len(self.vertex.outputs) == 1:
output_name = self.vertex.outputs[0]["name"] output_name = self.vertex.outputs[0]["name"]
else: elif not output_name:
raise ValueError("You must specify an output name to call stop") raise ValueError("You must specify an output name to call stop")
if not self.vertex: if not self.vertex:
raise ValueError("Vertex is not set") raise ValueError("Vertex is not set")

View file

@ -1208,6 +1208,7 @@ class Graph:
except ValueError: except ValueError:
stop_or_start_vertex = self.get_root_of_group_node(vertex_id) stop_or_start_vertex = self.get_root_of_group_node(vertex_id)
stack = [stop_or_start_vertex.id] stack = [stop_or_start_vertex.id]
vertex_id = stop_or_start_vertex.id
stop_predecessors = [pre.id for pre in stop_or_start_vertex.predecessors] stop_predecessors = [pre.id for pre in stop_or_start_vertex.predecessors]
# DFS to collect all vertices that can reach the specified vertex # DFS to collect all vertices that can reach the specified vertex
while stack: while stack:

View file

@ -1,11 +1,14 @@
import warnings import warnings
from typing import List, Optional from typing import List, Optional
from uuid import UUID
from loguru import logger from loguru import logger
from sqlalchemy import delete
from sqlmodel import Session, col, select
from langflow.schema.message import Message from langflow.schema.message import Message
from langflow.services.deps import get_monitor_service from langflow.services.database.models.message.model import MessageRead, MessageTable
from langflow.services.monitor.schema import MessageModel from langflow.services.deps import session_scope
def get_messages( def get_messages(
@ -14,6 +17,7 @@ def get_messages(
session_id: Optional[str] = None, session_id: Optional[str] = None,
order_by: Optional[str] = "timestamp", order_by: Optional[str] = "timestamp",
order: Optional[str] = "DESC", order: Optional[str] = "DESC",
flow_id: Optional[UUID] = None,
limit: Optional[int] = None, limit: Optional[int] = None,
): ):
""" """
@ -29,34 +33,29 @@ 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.
""" """
monitor_service = get_monitor_service() messages_read: list[Message] = []
messages_df = monitor_service.get_messages( with session_scope() as session:
sender=sender, stmt = select(MessageTable)
sender_name=sender_name, if sender:
session_id=session_id, stmt = stmt.where(MessageTable.sender == sender)
order_by=order_by, if sender_name:
limit=limit, stmt = stmt.where(MessageTable.sender_name == sender_name)
order=order, if session_id:
) stmt = stmt.where(MessageTable.session_id == session_id)
if flow_id:
stmt = stmt.where(MessageTable.flow_id == flow_id)
if order_by:
if order == "DESC":
col = getattr(MessageTable, order_by).desc()
else:
col = getattr(MessageTable, order_by).asc()
stmt = stmt.order_by(col)
if limit:
stmt = stmt.limit(limit)
messages = session.exec(stmt)
messages_read = [Message(**d.model_dump()) for d in messages]
messages: list[Message] = [] return messages_read
# messages_df has a timestamp
# it gets the last 5 messages, for example
# but now they are ordered from most recent to least recent
# so we need to reverse the order
messages_df = messages_df[::-1] if order == "DESC" else messages_df
for row in messages_df.itertuples():
msg = Message(
text=row.text,
sender=row.sender,
session_id=row.session_id,
sender_name=row.sender_name,
timestamp=row.timestamp,
)
messages.append(msg)
return messages
def add_messages(messages: Message | list[Message], flow_id: Optional[str] = None): def add_messages(messages: Message | list[Message], flow_id: Optional[str] = None):
@ -64,7 +63,6 @@ def add_messages(messages: Message | list[Message], flow_id: Optional[str] = Non
Add a message to the monitor service. Add a message to the monitor service.
""" """
try: try:
monitor_service = get_monitor_service()
if not isinstance(messages, list): if not isinstance(messages, list):
messages = [messages] messages = [messages]
@ -72,25 +70,29 @@ def add_messages(messages: Message | list[Message], flow_id: Optional[str] = Non
types = ", ".join([str(type(message)) for message in messages]) types = ", ".join([str(type(message)) for message in messages])
raise ValueError(f"The messages must be instances of Message. Found: {types}") raise ValueError(f"The messages must be instances of Message. Found: {types}")
messages_models: list[MessageModel] = [] messages_models: list[MessageTable] = []
for msg in messages: for msg in messages:
if not msg.timestamp: messages_models.append(MessageTable.from_message(msg, flow_id=flow_id))
msg.timestamp = monitor_service.get_timestamp() with session_scope() as session:
messages_models.append(MessageModel.from_message(msg, flow_id=flow_id)) messages_models = add_messagetables(messages_models, session)
return [Message(**message.model_dump()) for message in messages_models]
for message_model in messages_models:
try:
monitor_service.add_message(message_model)
except Exception as e:
logger.error(f"Error adding message to monitor service: {e}")
logger.exception(e)
raise e
return messages_models
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
raise e raise e
def add_messagetables(messages: list[MessageTable], session: Session):
for message in messages:
try:
session.add(message)
session.commit()
session.refresh(message)
except Exception as e:
logger.exception(e)
raise e
return [MessageRead.model_validate(message, from_attributes=True) for message in messages]
def delete_messages(session_id: str): def delete_messages(session_id: str):
""" """
Delete messages from the monitor service based on the provided session ID. Delete messages from the monitor service based on the provided session ID.
@ -98,8 +100,13 @@ def delete_messages(session_id: str):
Args: Args:
session_id (str): The session ID associated with the messages to delete. session_id (str): The session ID associated with the messages to delete.
""" """
monitor_service = get_monitor_service() with session_scope() as session:
monitor_service.delete_messages_session(session_id) session.exec(
delete(MessageTable)
.where(col(MessageTable.session_id) == session_id)
.execution_options(synchronize_session="fetch")
)
session.commit()
def store_message( def store_message(

View file

@ -1,5 +1,6 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Annotated, Any, AsyncIterator, Iterator, List, Optional from typing import Annotated, Any, AsyncIterator, Iterator, List, Optional
from uuid import UUID
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from langchain_core.load import load from langchain_core.load import load
@ -31,7 +32,14 @@ class Message(Data):
timestamp: Annotated[str, BeforeValidator(_timestamp_to_str)] = Field( timestamp: Annotated[str, BeforeValidator(_timestamp_to_str)] = Field(
default=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") default=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
) )
flow_id: Optional[str] = None flow_id: Optional[str | UUID] = None
@field_validator("flow_id", mode="before")
@classmethod
def validate_flow_id(cls, value):
if isinstance(value, UUID):
value = str(value)
return value
@field_validator("files", mode="before") @field_validator("files", mode="before")
@classmethod @classmethod

View file

@ -1,7 +1,8 @@
from .api_key import ApiKey from .api_key import ApiKey
from .flow import Flow from .flow import Flow
from .folder import Folder from .folder import Folder
from .message import MessageTable
from .user import User from .user import User
from .variable import Variable from .variable import Variable
__all__ = ["Flow", "User", "ApiKey", "Variable", "Folder"] __all__ = ["Flow", "User", "ApiKey", "Variable", "Folder", "MessageTable"]

View file

@ -3,7 +3,7 @@
import re import re
import warnings import warnings
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import TYPE_CHECKING, Dict, Optional from typing import TYPE_CHECKING, Dict, List, Optional
from uuid import UUID, uuid4 from uuid import UUID, uuid4
import emoji import emoji
@ -17,6 +17,7 @@ from langflow.schema import Data
if TYPE_CHECKING: if TYPE_CHECKING:
from langflow.services.database.models.folder import Folder 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.user import User
@ -141,6 +142,7 @@ class Flow(FlowBase, table=True):
user: "User" = Relationship(back_populates="flows") user: "User" = Relationship(back_populates="flows")
folder_id: Optional[UUID] = Field(default=None, foreign_key="folder.id", nullable=True, index=True) folder_id: Optional[UUID] = Field(default=None, foreign_key="folder.id", nullable=True, index=True)
folder: Optional["Folder"] = Relationship(back_populates="flows") folder: Optional["Folder"] = Relationship(back_populates="flows")
messages: List["MessageTable"] = Relationship(back_populates="flow")
def to_data(self): def to_data(self):
serialized = self.model_dump() serialized = self.model_dump()

View file

@ -0,0 +1,3 @@
from .model import MessageTable, MessageCreate, MessageRead, MessageUpdate
__all__ = ["MessageTable", "MessageCreate", "MessageRead", "MessageUpdate"]

View file

@ -0,0 +1,74 @@
from datetime import datetime, timezone
from typing import TYPE_CHECKING, List, Optional
from uuid import UUID, uuid4
from pydantic import field_validator
from sqlmodel import JSON, Column, Field, Relationship, SQLModel
if TYPE_CHECKING:
from langflow.schema.message import Message
from langflow.services.database.models.flow.model import Flow
class MessageBase(SQLModel):
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
sender: str
sender_name: str
session_id: str
text: str
files: list[str] = Field(default_factory=list)
@field_validator("files", mode="before")
@classmethod
def validate_files(cls, value):
if not value:
value = []
return value
@classmethod
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).")
if isinstance(message.timestamp, str):
timestamp = datetime.fromisoformat(message.timestamp)
else:
timestamp = message.timestamp
return cls(
sender=message.sender,
sender_name=message.sender_name,
text=message.text,
session_id=message.session_id,
files=message.files or [],
timestamp=timestamp,
flow_id=flow_id,
)
class MessageTable(MessageBase, table=True):
__tablename__ = "message"
id: UUID = Field(default_factory=uuid4, primary_key=True)
flow_id: Optional[UUID] = Field(default=None, foreign_key="flow.id")
flow: "Flow" = Relationship(back_populates="messages")
files: List[str] = Field(sa_column=Column(JSON))
# Needed for Column(JSON)
class Config:
arbitrary_types_allowed = True
class MessageRead(MessageBase):
id: UUID
flow_id: Optional[UUID] = Field()
class MessageCreate(MessageBase):
pass
class MessageUpdate(SQLModel):
text: Optional[str] = None
sender: Optional[str] = None
sender_name: Optional[str] = None
session_id: Optional[str] = None
files: Optional[list[str]] = None

View file

@ -1,6 +1,7 @@
import json import json
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Optional from typing import Any, Optional
from uuid import UUID
from pydantic import BaseModel, Field, field_serializer, field_validator from pydantic import BaseModel, Field, field_serializer, field_validator
@ -81,8 +82,8 @@ class TransactionModelResponse(DefaultModel):
class MessageModel(DefaultModel): class MessageModel(DefaultModel):
index: Optional[int] = Field(default=None) id: Optional[str | UUID] = Field(default=None)
flow_id: Optional[str] = Field(default=None, alias="flow_id") flow_id: Optional[UUID] = Field(default=None)
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
@ -127,16 +128,7 @@ class MessageModel(DefaultModel):
class MessageModelResponse(MessageModel): class MessageModelResponse(MessageModel):
index: Optional[int] = Field(default=None) pass
@field_validator("index", mode="before")
def validate_id(cls, v):
if isinstance(v, float):
try:
return int(v)
except ValueError:
return None
return v
class MessageModelRequest(MessageModel): class MessageModelRequest(MessageModel):

View file

@ -3,14 +3,15 @@ from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, Union from typing import TYPE_CHECKING, List, Optional, Union
import duckdb import duckdb
from langflow.services.base import Service
from langflow.services.monitor.utils import add_row_to_table, drop_and_create_table_if_schema_mismatch
from loguru import logger from loguru import logger
from platformdirs import user_cache_dir from platformdirs import user_cache_dir
from langflow.services.base import Service
from langflow.services.monitor.utils import add_row_to_table, drop_and_create_table_if_schema_mismatch
if TYPE_CHECKING: if TYPE_CHECKING:
from langflow.services.settings.service import SettingsService
from langflow.services.monitor.schema import MessageModel, TransactionModel, VertexBuildModel from langflow.services.monitor.schema import MessageModel, TransactionModel, VertexBuildModel
from langflow.services.settings.service import SettingsService
class MonitorService(Service): class MonitorService(Service):
@ -129,45 +130,6 @@ class MonitorService(Service):
return self.exec_query(query, read_only=False) return self.exec_query(query, read_only=False)
def add_message(self, message: "MessageModel"):
self.add_row("messages", message)
def get_messages(
self,
flow_id: Optional[str] = None,
sender: Optional[str] = None,
sender_name: Optional[str] = None,
session_id: Optional[str] = None,
order_by: Optional[str] = "timestamp",
order: Optional[str] = "DESC",
limit: Optional[int] = 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_transactions( def get_transactions(
self, self,
source: Optional[str] = None, source: Optional[str] = None,

View file

@ -83,6 +83,7 @@
"build": "vite build", "build": "vite build",
"serve": "vite preview", "serve": "vite preview",
"format": "npx prettier --write \"{tests,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore", "format": "npx prettier --write \"{tests,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore",
"check-format": "npx prettier --check \"{tests,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore",
"type-check": "tsc --noEmit --pretty --project tsconfig.json && vite" "type-check": "tsc --noEmit --pretty --project tsconfig.json && vite"
}, },
"eslintConfig": { "eslintConfig": {

View file

@ -6,13 +6,14 @@ import {
} from "../../constants/constants"; } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import { ResponseErrorDetailAPI } from "../../types/api"; import { ResponseErrorDetailAPI } from "../../types/api";
import { NodeDataType } from "../../types/flow";
const useFetchDataOnMount = ( const useFetchDataOnMount = (
data, data: NodeDataType,
name, name: string,
handleUpdateValues, handleUpdateValues: (name: string, data: NodeDataType) => Promise<any>,
setNode, setNode: (id: string, callback: (oldNode: any) => any) => void,
setIsLoading, setIsLoading: (value: boolean) => void,
) => { ) => {
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);

View file

@ -5,15 +5,16 @@ import {
} from "../../constants/constants"; } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import { ResponseErrorTypeAPI } from "../../types/api"; import { ResponseErrorTypeAPI } from "../../types/api";
import { NodeDataType } from "../../types/flow";
const useHandleOnNewValue = ( const useHandleOnNewValue = (
data, data: NodeDataType,
name, name: string,
takeSnapshot, takeSnapshot: () => void,
handleUpdateValues, handleUpdateValues: (name: string, data: NodeDataType) => Promise<any>,
debouncedHandleUpdateValues, debouncedHandleUpdateValues: any,
setNode, setNode: (id: string, callback: (oldNode: any) => any) => void,
setIsLoading, setIsLoading: (value: boolean) => void,
) => { ) => {
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);

View file

@ -1,11 +1,12 @@
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { NodeDataType } from "../../types/flow";
const useHandleNodeClass = ( const useHandleNodeClass = (
data, data: NodeDataType,
name, name: string,
takeSnapshot, takeSnapshot: () => void,
setNode, setNode: (id: string, callback: (oldNode: any) => any) => void,
updateNodeInternals, updateNodeInternals: (id: string) => void,
) => { ) => {
const handleNodeClass = (newNodeClass, code, type?: string) => { const handleNodeClass = (newNodeClass, code, type?: string) => {
if (!data.node) return; if (!data.node) return;

View file

@ -7,7 +7,10 @@ import useAlertStore from "../../stores/alertStore";
import { ResponseErrorDetailAPI } from "../../types/api"; import { ResponseErrorDetailAPI } from "../../types/api";
import { handleUpdateValues } from "../../utils/parameterUtils"; import { handleUpdateValues } from "../../utils/parameterUtils";
const useHandleRefreshButtonPress = (setIsLoading, setNode) => { const useHandleRefreshButtonPress = (
setIsLoading: (value: boolean) => void,
setNode: (id: string, callback: (oldNode: any) => any) => void,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const handleRefreshButtonPress = async (name, data) => { const handleRefreshButtonPress = async (name, data) => {

View file

@ -1,6 +1,11 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { FlowPoolType } from "../../types/zustand/flow";
const useUpdateValidationStatus = (dataId, flowPool, setValidationStatus) => { const useUpdateValidationStatus = (
dataId: string,
flowPool: FlowPoolType,
setValidationStatus: (value: any) => void,
) => {
useEffect(() => { useEffect(() => {
const relevantData = const relevantData =
flowPool[dataId] && flowPool[dataId]?.length > 0 flowPool[dataId] && flowPool[dataId]?.length > 0

View file

@ -4,7 +4,7 @@ import { isErrorLog } from "../../types/utils/typeCheckingUtils";
const useValidationStatusString = ( const useValidationStatusString = (
validationStatus: VertexBuildTypeAPI | null, validationStatus: VertexBuildTypeAPI | null,
setValidationString, setValidationString: (value: any) => void,
) => { ) => {
useEffect(() => { useEffect(() => {
if (validationStatus && validationStatus.data?.outputs) { if (validationStatus && validationStatus.data?.outputs) {

View file

@ -0,0 +1,19 @@
import { useEffect } from "react";
import { storeComponent } from "../../../types/store";
const useDataEffect = (
data: storeComponent,
setLikedByUser: (value: any) => void,
setLikesCount: (value: any) => void,
setDownloadsCount: (value: any) => void,
) => {
useEffect(() => {
if (data) {
setLikedByUser(data?.liked_by_user ?? false);
setLikesCount(data?.liked_by_count ?? 0);
setDownloadsCount(data?.downloads_count ?? 0);
}
}, [data, data?.liked_by_count, data?.liked_by_user, data?.downloads_count]);
};
export default useDataEffect;

View file

@ -0,0 +1,55 @@
import { useState } from "react";
import { getComponent } from "../../../controllers/API";
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
import { storeComponent } from "../../../types/store";
import cloneFlowWithParent from "../../../utils/storeUtils";
const useInstallComponent = (
data: storeComponent,
name: string,
isStore: boolean,
downloadsCount: number,
setDownloadsCount: (value: any) => void,
setLoading: (value: boolean) => void,
setSuccessData: (value: { title: string }) => void,
setErrorData: (value: { title: string; list: string[] }) => void,
) => {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const handleInstall = () => {
const temp = downloadsCount;
setDownloadsCount((old) => Number(old) + 1);
setLoading(true);
getComponent(data.id)
.then((res) => {
const newFlow = cloneFlowWithParent(res, res.id, data.is_component);
addFlow(true, newFlow)
.then((id) => {
setSuccessData({
title: `${name} ${isStore ? "Downloaded" : "Installed"} Successfully.`,
});
setLoading(false);
})
.catch((error) => {
setLoading(false);
setErrorData({
title: `Error ${isStore ? "downloading" : "installing"} the ${name}`,
list: [error.response.data.detail],
});
});
})
.catch((err) => {
setLoading(false);
setErrorData({
title: `Error ${isStore ? "downloading" : "installing"} the ${name}`,
list: [err.response.data.detail],
});
setDownloadsCount(temp);
});
};
return { handleInstall };
};
export default useInstallComponent;

View file

@ -0,0 +1,51 @@
import { postLikeComponent } from "../../../controllers/API";
import { storeComponent } from "../../../types/store";
const useLikeComponent = (
data: storeComponent,
name: string,
setLoadingLike: (value: boolean) => void,
likedByUser: boolean | null | undefined,
likesCount: number,
setLikedByUser: (value: any) => void,
setLikesCount: (value: any) => void,
setValidApiKey: (value: boolean) => void,
setErrorData: (value: { title: string; list: string[] }) => void,
) => {
const handleLike = () => {
setLoadingLike(true);
if (likedByUser !== undefined || likedByUser !== null) {
const temp = likedByUser;
const tempNum = likesCount;
setLikedByUser((prev) => !prev);
setLikesCount((prev) => (temp ? prev - 1 : prev + 1));
postLikeComponent(data.id)
.then((response) => {
setLoadingLike(false);
setLikesCount(response.data.likes_count);
setLikedByUser(response.data.liked_by_user);
})
.catch((error) => {
setLoadingLike(false);
setLikesCount(tempNum);
setLikedByUser(temp);
if (error.response.status === 403) {
setValidApiKey(false);
} else {
console.error(error);
setErrorData({
title: `Error liking ${name}.`,
list: [error.response.data.detail],
});
}
});
}
};
return {
handleLike,
};
};
export default useLikeComponent;

View file

@ -0,0 +1,34 @@
import { useCallback } from "react";
import { createRoot } from "react-dom/client";
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
import { storeComponent } from "../../../types/store";
import DragCardComponent from "../components/dragCardComponent";
const useDragStart = (data: storeComponent) => {
const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
const onDragStart = useCallback(
(event) => {
let image = <DragCardComponent data={data} />; // Replace with whatever you want here
const ghost = document.createElement("div");
ghost.style.transform = "translate(-10000px, -10000px)";
ghost.style.position = "absolute";
document.body.appendChild(ghost);
event.dataTransfer.setDragImage(ghost, 0, 0);
const root = createRoot(ghost);
root.render(image);
const flow = getFlowById(data.id);
if (flow) {
event.dataTransfer.setData("flow", JSON.stringify(data));
}
},
[data],
);
return { onDragStart };
};
export default useDragStart;

View file

@ -0,0 +1,27 @@
import { useEffect } from "react";
import { FlowType } from "../../../types/flow";
const usePlaygroundEffect = (
currentFlowId: string,
playground: boolean,
openPlayground: boolean,
currentFlow: FlowType | undefined,
setNodes: (value: any, value2: boolean) => void,
setEdges: (value: any, value2: boolean) => void,
cleanFlowPool: () => void,
) => {
useEffect(() => {
if (currentFlowId && playground) {
if (openPlayground) {
setNodes(currentFlow?.data?.nodes ?? [], true);
setEdges(currentFlow?.data?.edges ?? [], true);
} else {
setNodes([], true);
setEdges([], true);
}
cleanFlowPool();
}
}, [openPlayground]);
};
export default usePlaygroundEffect;

View file

@ -28,6 +28,11 @@ import { Checkbox } from "../ui/checkbox";
import { FormControl, FormField } from "../ui/form"; import { FormControl, FormField } from "../ui/form";
import Loading from "../ui/loading"; import Loading from "../ui/loading";
import DragCardComponent from "./components/dragCardComponent"; import DragCardComponent from "./components/dragCardComponent";
import useDataEffect from "./hooks/use-data-effect";
import useInstallComponent from "./hooks/use-handle-install";
import useLikeComponent from "./hooks/use-handle-like";
import useDragStart from "./hooks/use-on-drag-start";
import usePlaygroundEffect from "./hooks/use-playground-effect";
import { convertTestName } from "./utils/convert-test-name"; import { convertTestName } from "./utils/convert-test-name";
export default function CollectionCardComponent({ export default function CollectionCardComponent({
@ -59,11 +64,9 @@ export default function CollectionCardComponent({
const isStore = false; const isStore = false;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingLike, setLoadingLike] = useState(false); const [loadingLike, setLoadingLike] = useState(false);
const [liked_by_user, setLiked_by_user] = useState( const [likedByUser, setLikedByUser] = useState(data?.liked_by_user ?? false);
data?.liked_by_user ?? false, const [likesCount, setLikesCount] = useState(data?.liked_by_count ?? 0);
); const [downloadsCount, setDownloadsCount] = useState(
const [likes_count, setLikes_count] = useState(data?.liked_by_count ?? 0);
const [downloads_count, setDownloads_count] = useState(
data?.downloads_count ?? 0, data?.downloads_count ?? 0,
); );
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
@ -99,115 +102,45 @@ export default function CollectionCardComponent({
return inputs.length > 0 || outputs.length > 0; return inputs.length > 0 || outputs.length > 0;
} }
useEffect(() => { usePlaygroundEffect(
if (currentFlowId && playground) { currentFlowId,
if (openPlayground) { playground!,
setNodes(currentFlow?.data?.nodes ?? [], true); openPlayground,
setEdges(currentFlow?.data?.edges ?? [], true); currentFlow,
} else { setNodes,
setNodes([], true); setEdges,
setEdges([], true); cleanFlowPool,
} );
cleanFlowPool();
}
}, [openPlayground]);
useEffect(() => { useDataEffect(data, setLikedByUser, setLikesCount, setDownloadsCount);
if (data) {
setLiked_by_user(data?.liked_by_user ?? false);
setLikes_count(data?.liked_by_count ?? 0);
setDownloads_count(data?.downloads_count ?? 0);
}
}, [data, data.liked_by_count, data.liked_by_user, data.downloads_count]);
function handleInstall() { const { handleInstall } = useInstallComponent(
const temp = downloads_count; data,
setDownloads_count((old) => Number(old) + 1); name,
setLoading(true); isStore,
getComponent(data.id) downloadsCount,
.then((res) => { setDownloadsCount,
const newFlow = cloneFLowWithParent(res, res.id, data.is_component); setLoading,
addFlow(true, newFlow) setSuccessData,
.then((id) => { setErrorData,
setSuccessData({ );
title: `${name} ${
isStore ? "Downloaded" : "Installed"
} Successfully.`,
});
setLoading(false);
})
.catch((error) => {
setLoading(false);
setErrorData({
title: `Error ${
isStore ? "downloading" : "installing"
} the ${name}`,
list: [error["response"]["data"]["detail"]],
});
});
})
.catch((err) => {
setLoading(false);
setErrorData({
title: `Error ${isStore ? "downloading" : "installing"} the ${name}`,
list: [err["response"]["data"]["detail"]],
});
setDownloads_count(temp);
});
}
function handleLike() { const { handleLike } = useLikeComponent(
setLoadingLike(true); data,
if (liked_by_user !== undefined || liked_by_user !== null) { name,
const temp = liked_by_user; setLoadingLike,
const tempNum = likes_count; likedByUser,
setLiked_by_user((prev) => !prev); likesCount,
if (!temp) { setLikedByUser,
setLikes_count((prev) => Number(prev) + 1); setLikesCount,
} else { setValidApiKey,
setLikes_count((prev) => Number(prev) - 1); setErrorData,
} );
postLikeComponent(data.id)
.then((response) => {
setLoadingLike(false);
setLikes_count(response.data.likes_count);
setLiked_by_user(response.data.liked_by_user);
})
.catch((error) => {
setLoadingLike(false);
setLikes_count(tempNum);
setLiked_by_user(temp);
if (error.response.status === 403) {
setValidApiKey(false);
} else {
console.error(error);
setErrorData({
title: `Error liking ${name}.`,
list: [error["response"]["data"]["detail"]],
});
}
});
}
}
const isSelectedCard = const isSelectedCard =
selectedFlowsComponentsCards?.includes(data?.id) ?? false; selectedFlowsComponentsCards?.includes(data?.id) ?? false;
function onDragStart(event: React.DragEvent<any>) { const { onDragStart } = useDragStart(data);
let image: JSX.Element = <DragCardComponent data={data} />; // <== whatever you want here
var ghost = document.createElement("div");
ghost.style.transform = "translate(-10000px, -10000px)";
ghost.style.position = "absolute";
document.body.appendChild(ghost);
event.dataTransfer.setDragImage(ghost, 0, 0);
const root = createRoot(ghost);
root.render(image);
const flow = getFlowById(data.id);
if (flow) {
event.dataTransfer.setData("flow", JSON.stringify(data));
}
}
return ( return (
<> <>
@ -264,7 +197,7 @@ export default function CollectionCardComponent({
<span className="flex items-center gap-1.5 text-xs text-muted-foreground"> <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Heart" className={cn("h-4 w-4")} /> <IconComponent name="Heart" className={cn("h-4 w-4")} />
<span data-testid={`likes-${data.name}`}> <span data-testid={`likes-${data.name}`}>
{likes_count ?? 0} {likesCount ?? 0}
</span> </span>
</span> </span>
</ShadTooltip> </ShadTooltip>
@ -275,7 +208,7 @@ export default function CollectionCardComponent({
className="h-4 w-4" className="h-4 w-4"
/> />
<span data-testid={`downloads-${data.name}`}> <span data-testid={`downloads-${data.name}`}>
{downloads_count ?? 0} {downloadsCount ?? 0}
</span> </span>
</span> </span>
</ShadTooltip> </ShadTooltip>
@ -324,20 +257,7 @@ export default function CollectionCardComponent({
)} )}
</span> </span>
)} )}
<div className="flex w-full flex-1 flex-wrap gap-2"> <div className="flex w-full flex-1 flex-wrap gap-2"></div>
{/* {data.tags &&
data.tags.length > 0 &&
data.tags.map((tag, index) => (
<Badge
key={index}
variant="outline"
size="xq"
className="text-muted-foreground"
>
{tag.name}
</Badge>
))} */}
</div>
</div> </div>
<CardDescription className="pb-2 pt-2"> <CardDescription className="pb-2 pt-2">
@ -457,7 +377,7 @@ export default function CollectionCardComponent({
name="Heart" name="Heart"
className={cn( className={cn(
"h-5 w-5", "h-5 w-5",
liked_by_user likedByUser
? "fill-destructive stroke-destructive" ? "fill-destructive stroke-destructive"
: "", : "",
!authorized ? "text-ring" : "", !authorized ? "text-ring" : "",

View file

@ -9,7 +9,10 @@ import useFlowsManagerStore from "../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../stores/foldersStore"; import { useFolderStore } from "../../../stores/foldersStore";
import { addVersionToDuplicates } from "../../../utils/reactflowUtils"; import { addVersionToDuplicates } from "../../../utils/reactflowUtils";
const useFileDrop = (folderId, folderChangeCallback) => { const useFileDrop = (
folderId: string,
folderChangeCallback: (folderId: string) => void,
) => {
const setFolderDragging = useFolderStore((state) => state.setFolderDragging); const setFolderDragging = useFolderStore((state) => state.setFolderDragging);
const setFolderIdDragging = useFolderStore( const setFolderIdDragging = useFolderStore(
(state) => state.setFolderIdDragging, (state) => state.setFolderIdDragging,

View file

@ -1,6 +1,9 @@
import { useEffect } from "react"; import { useEffect } from "react";
const useAutoResizeTextArea = (value, inputRef) => { const useAutoResizeTextArea = (
value: string,
inputRef: React.RefObject<HTMLInputElement>,
) => {
useEffect(() => { useEffect(() => {
if (inputRef.current && inputRef.current.scrollHeight! !== 0) { if (inputRef.current && inputRef.current.scrollHeight! !== 0) {
inputRef.current.style!.height = "inherit"; // Reset the height inputRef.current.style!.height = "inherit"; // Reset the height

View file

@ -7,10 +7,10 @@ import {
import useFileUpload from "./use-file-upload"; import useFileUpload from "./use-file-upload";
const useDragAndDrop = ( const useDragAndDrop = (
setIsDragging, setIsDragging: (value: boolean) => void,
setFiles, setFiles: (value: any) => void,
currentFlowId, currentFlowId: string,
setErrorData, setErrorData: (value: any) => void,
) => { ) => {
const dragOver = (e) => { const dragOver = (e) => {
e.preventDefault(); e.preventDefault();

View file

@ -1,6 +1,9 @@
import { useEffect } from "react"; import { useEffect } from "react";
const useFocusOnUnlock = (lockChat, inputRef) => { const useFocusOnUnlock = (
lockChat: boolean,
inputRef: React.RefObject<HTMLInputElement>,
) => {
useEffect(() => { useEffect(() => {
if (!lockChat && inputRef.current) { if (!lockChat && inputRef.current) {
inputRef.current.focus(); inputRef.current.focus();

View file

@ -1,3 +1,4 @@
import { AxiosResponse } from "axios";
import { useEffect } from "react"; import { useEffect } from "react";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { import {
@ -6,9 +7,18 @@ import {
SN_ERROR_TEXT, SN_ERROR_TEXT,
} from "../../../../../../constants/constants"; } from "../../../../../../constants/constants";
import useAlertStore from "../../../../../../stores/alertStore"; import useAlertStore from "../../../../../../stores/alertStore";
import { UploadFileTypeAPI } from "../../../../../../types/api";
import useFileUpload from "./use-file-upload"; import useFileUpload from "./use-file-upload";
const useUpload = (uploadFile, currentFlowId, setFiles, lockChat) => { const useUpload = (
uploadFile: (
file: File,
id: string,
) => Promise<AxiosResponse<UploadFileTypeAPI>>,
currentFlowId: string,
setFiles: any,
lockChat: boolean,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
useEffect(() => { useEffect(() => {
const handlePaste = (event: ClipboardEvent): void => { const handlePaste = (event: ClipboardEvent): void => {

View file

@ -2,9 +2,10 @@ import { ColDef, ValueGetterParams } from "ag-grid-community";
import { useMemo } from "react"; import { useMemo } from "react";
import TableNodeCellRender from "../../../components/tableComponent/components/tableNodeCellRender"; import TableNodeCellRender from "../../../components/tableComponent/components/tableNodeCellRender";
import TableToggleCellRender from "../../../components/tableComponent/components/tableToggleCellRender"; import TableToggleCellRender from "../../../components/tableComponent/components/tableToggleCellRender";
import { NodeDataType } from "../../../types/flow";
const useColumnDefs = ( const useColumnDefs = (
myData: any, myData: NodeDataType,
handleOnNewValue: (newValue: any, name: string) => void, handleOnNewValue: (newValue: any, name: string) => void,
handleOnChangeDb: (value: boolean, key: string) => void, handleOnChangeDb: (value: boolean, key: string) => void,
changeAdvanced: (n: string) => void, changeAdvanced: (n: string) => void,

View file

@ -1,14 +1,12 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { LANGFLOW_SUPPORTED_TYPES } from "../../../constants/constants"; import { LANGFLOW_SUPPORTED_TYPES } from "../../../constants/constants";
import { TemplateVariableType } from "../../../types/api"; import { NodeDataType } from "../../../types/flow";
const useRowData = (myData, open) => { const useRowData = (myData: NodeDataType, open: boolean) => {
const rowData = useMemo(() => { const rowData = useMemo(() => {
return Object.keys(myData.node!.template) return Object.keys(myData.node!.template)
.filter((key: string) => { .filter((key: string) => {
const templateParam = myData.node!.template[ const templateParam = myData.node!.template[key] as any;
key
] as TemplateVariableType;
return ( return (
key.charAt(0) !== "_" && key.charAt(0) !== "_" &&
templateParam.show && templateParam.show &&
@ -20,9 +18,7 @@ const useRowData = (myData, open) => {
); );
}) })
.map((key: string) => { .map((key: string) => {
const templateParam = myData.node!.template[ const templateParam = myData.node!.template[key] as any;
key
] as TemplateVariableType;
return { return {
...templateParam, ...templateParam,
key: key, key: key,

View file

@ -16,13 +16,11 @@ const EditNodeModal = forwardRef(
nodeLength, nodeLength,
open, open,
setOpen, setOpen,
// setOpenWDoubleClick,
data, data,
}: { }: {
nodeLength: number; nodeLength: number;
open: boolean; open: boolean;
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
// setOpenWDoubleClick: (open: boolean) => void;
data: NodeDataType; data: NodeDataType;
}, },
ref, ref,

View file

@ -0,0 +1,58 @@
import React from "react";
import { Link, useNavigate } from "react-router-dom";
import CollectionCardComponent from "../../../../../../components/cardComponent";
import IconComponent from "../../../../../../components/genericIconComponent";
import { Button } from "../../../../../../components/ui/button";
const CollectionCard = ({ item, type, isLoading, control }) => {
const navigate = useNavigate();
const isComponent = item.is_component ?? false;
const editFlowLink = `/flow/${item.id}`;
const editFlowButtonTestId = `edit-flow-button-${item.id}`;
const handleClick = () => {
if (!isComponent) {
navigate(editFlowLink);
}
};
const renderButton = () => {
if (!isComponent) {
return (
<Link to={editFlowLink}>
<Button
tabIndex={-1}
variant="outline"
size="sm"
className="whitespace-nowrap"
data-testid={editFlowButtonTestId}
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button select-none"
/>
Edit Flow
</Button>
</Link>
);
}
return null;
};
return (
<CollectionCardComponent
is_component={type === "component"}
data={{
is_component: isComponent,
...item,
}}
disabled={isLoading}
data-testid={editFlowButtonTestId}
button={renderButton()!}
onClick={!isComponent ? handleClick : undefined}
playground={!isComponent}
control={control}
/>
);
};
export default CollectionCard;

View file

@ -1,15 +1,15 @@
import { useCallback } from "react"; import { useCallback } from "react";
const useDeleteMultipleFlows = ( const useDeleteMultipleFlows = (
selectedFlowsComponentsCards, selectedFlowsComponentsCards: string[],
removeFlow, removeFlow: (selectedFlowsComponentsCards: string[]) => Promise<void>,
resetFilter, resetFilter: () => void,
getFoldersApi, getFoldersApi: (refetch?: boolean) => Promise<void>,
folderId, folderId: string | undefined,
myCollectionId, myCollectionId: string,
getFolderById, getFolderById: (id: string) => void,
setSuccessData, setSuccessData: (data: { title: string }) => void,
setErrorData, setErrorData: (data: { title: string; list: string[] }) => void,
) => { ) => {
const handleDeleteMultiple = useCallback(() => { const handleDeleteMultiple = useCallback(() => {
removeFlow(selectedFlowsComponentsCards) removeFlow(selectedFlowsComponentsCards)

View file

@ -1,6 +1,9 @@
import { useMemo } from "react"; import { useMemo } from "react";
const useDescriptionModal = (selectedFlowsComponentsCards, type) => { const useDescriptionModal = (
selectedFlowsComponentsCards: string[] | undefined,
type: string | undefined,
) => {
const getDescriptionModal = useMemo(() => { const getDescriptionModal = useMemo(() => {
const getTypeLabel = (type) => { const getTypeLabel = (type) => {
const labels = { const labels = {

View file

@ -1,10 +1,11 @@
import cloneDeep from "lodash/cloneDeep"; import cloneDeep from "lodash/cloneDeep";
import { useEffect } from "react"; import { useEffect } from "react";
import { FlowType } from "../../../../../types/flow";
const useFilteredFlows = ( const useFilteredFlows = (
flowsFromFolder, flowsFromFolder: FlowType[],
searchFlowsComponents, searchFlowsComponents: string,
setAllFlows, setAllFlows: (value: any[]) => void,
) => { ) => {
useEffect(() => { useEffect(() => {
const newFlows = cloneDeep(flowsFromFolder || []); const newFlows = cloneDeep(flowsFromFolder || []);

View file

@ -1,18 +1,31 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { XYPosition } from "reactflow";
import { FlowType } from "../../../../../types/flow";
const useDuplicateFlows = ( const useDuplicateFlows = (
selectedFlowsComponentsCards, selectedFlowsComponentsCards: string[],
addFlow, addFlow: (
allFlows, newProject: boolean,
resetFilter, flow?: FlowType,
getFoldersApi, override?: boolean,
folderId, position?: XYPosition,
myCollectionId, fromDragAndDrop?: boolean,
getFolderById, ) => Promise<string | undefined>,
setSuccessData, allFlows: any[],
setSelectedFlowsComponentsCards, resetFilter: () => void,
handleSelectAll, getFoldersApi: (
cardTypes, refetch?: boolean,
startupApplication?: boolean,
) => Promise<void>,
folderId: string,
myCollectionId: string,
getFolderById: (id: string) => void,
setSuccessData: (data: { title: string }) => void,
setSelectedFlowsComponentsCards: (
selectedFlowsComponentsCards: string[],
) => void,
handleSelectAll: (select: boolean) => void,
cardTypes: string,
) => { ) => {
const handleDuplicate = useCallback(() => { const handleDuplicate = useCallback(() => {
Promise.all( Promise.all(

View file

@ -1,15 +1,18 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { FlowType } from "../../../../../types/flow";
const useExportFlows = ( const useExportFlows = (
selectedFlowsComponentsCards, selectedFlowsComponentsCards: string[],
allFlows, allFlows: Array<FlowType>,
downloadFlow, downloadFlow: (flow: any, name: string, description: string) => void,
removeApiKeys, removeApiKeys: (flow: any) => any,
version, version: string,
setSuccessData, setSuccessData: (data: { title: string }) => void,
setSelectedFlowsComponentsCards, setSelectedFlowsComponentsCards: (
handleSelectAll, selectedFlowsComponentsCards: string[],
cardTypes, ) => void,
handleSelectAll: (select: boolean) => void,
cardTypes: string,
) => { ) => {
const handleExport = useCallback(() => { const handleExport = useCallback(() => {
selectedFlowsComponentsCards.forEach((selectedFlowId) => { selectedFlowsComponentsCards.forEach((selectedFlowId) => {

View file

@ -1,6 +1,11 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { FlowType } from "../../../../../types/flow";
const useSelectAll = (flowsFromFolder, getValues, setValue) => { const useSelectAll = (
flowsFromFolder: FlowType[],
getValues: () => Record<string, boolean>,
setValue: (key: string, value: boolean) => void,
) => {
const handleSelectAll = useCallback( const handleSelectAll = useCallback(
(select) => { (select) => {
const flowsFromFolderIds = flowsFromFolder?.map((f) => f.id); const flowsFromFolderIds = flowsFromFolder?.map((f) => f.id);

View file

@ -1,15 +1,15 @@
import { useCallback } from "react"; import { useCallback } from "react";
const useSelectOptionsChange = ( const useSelectOptionsChange = (
selectedFlowsComponentsCards, selectedFlowsComponentsCards: string[] | undefined,
setErrorData, setErrorData: (data: { title: string; list: string[] }) => void,
setOpenDelete, setOpenDelete: (value: boolean) => void,
handleDuplicate, handleDuplicate: () => void,
handleExport, handleExport: () => void,
) => { ) => {
const handleSelectOptionsChange = useCallback( const handleSelectOptionsChange = useCallback(
(action) => { (action) => {
const hasSelected = selectedFlowsComponentsCards?.length > 0; const hasSelected = selectedFlowsComponentsCards?.length! > 0;
if (!hasSelected) { if (!hasSelected) {
setErrorData({ setErrorData({
title: "No items selected", title: "No items selected",

View file

@ -1,8 +1,10 @@
import { useEffect } from "react"; import { useEffect } from "react";
const useSelectedFlows = ( const useSelectedFlows = (
entireFormValues, entireFormValues: Record<string, boolean> | undefined,
setSelectedFlowsComponentsCards, setSelectedFlowsComponentsCards: (
selectedFlowsComponentsCards: string[],
) => void,
) => { ) => {
useEffect(() => { useEffect(() => {
if (!entireFormValues || Object.keys(entireFormValues).length === 0) return; if (!entireFormValues || Object.keys(entireFormValues).length === 0) return;

View file

@ -19,6 +19,7 @@ import { getNameByType } from "../../utils/get-name-by-type";
import { sortFlows } from "../../utils/sort-flows"; import { sortFlows } from "../../utils/sort-flows";
import EmptyComponent from "../emptyComponent"; import EmptyComponent from "../emptyComponent";
import HeaderComponent from "../headerComponent"; import HeaderComponent from "../headerComponent";
import CollectionCard from "./components/collectionCard";
import useDeleteMultipleFlows from "./hooks/use-delete-multiple"; import useDeleteMultipleFlows from "./hooks/use-delete-multiple";
import useDescriptionModal from "./hooks/use-description-modal"; import useDescriptionModal from "./hooks/use-description-modal";
import useFilteredFlows from "./hooks/use-filtered-flows"; import useFilteredFlows from "./hooks/use-filtered-flows";
@ -61,7 +62,6 @@ export default function ComponentsComponent({
const [handleFileDrop] = useFileDrop(uploadFlow, type)!; const [handleFileDrop] = useFileDrop(uploadFlow, type)!;
const [pageSize, setPageSize] = useState(20); const [pageSize, setPageSize] = useState(20);
const [pageIndex, setPageIndex] = useState(1); const [pageIndex, setPageIndex] = useState(1);
const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const all: FlowType[] = sortFlows(allFlows, type); const all: FlowType[] = sortFlows(allFlows, type);
const start = (pageIndex - 1) * pageSize; const start = (pageIndex - 1) * pageSize;
@ -94,7 +94,7 @@ export default function ComponentsComponent({
getFolderById(folderId ? folderId : myCollectionId); getFolderById(folderId ? folderId : myCollectionId);
}, [location]); }, [location]);
useFilteredFlows(flowsFromFolder, searchFlowsComponents, setAllFlows); useFilteredFlows(flowsFromFolder!, searchFlowsComponents, setAllFlows);
const resetFilter = () => { const resetFilter = () => {
setPageIndex(1); setPageIndex(1);
@ -107,7 +107,7 @@ export default function ComponentsComponent({
const methods = useForm(); const methods = useForm();
const { handleSelectAll } = useSelectAll( const { handleSelectAll } = useSelectAll(
flowsFromFolder, flowsFromFolder!,
getValues, getValues,
setValue, setValue,
); );
@ -119,7 +119,7 @@ export default function ComponentsComponent({
resetFilter, resetFilter,
getFoldersApi, getFoldersApi,
folderId, folderId,
myCollectionId, myCollectionId!,
getFolderById, getFolderById,
setSuccessData, setSuccessData,
setSelectedFlowsComponentsCards, setSelectedFlowsComponentsCards,
@ -155,7 +155,7 @@ export default function ComponentsComponent({
resetFilter, resetFilter,
getFoldersApi, getFoldersApi,
folderId, folderId,
myCollectionId, myCollectionId!,
getFolderById, getFolderById,
setSuccessData, setSuccessData,
setErrorData, setErrorData,
@ -205,43 +205,10 @@ export default function ComponentsComponent({
{data?.map((item) => ( {data?.map((item) => (
<FormProvider {...methods} key={item.id}> <FormProvider {...methods} key={item.id}>
<form> <form>
<CollectionCardComponent <CollectionCard
is_component={type === "component"} item={item}
data={{ type={type}
is_component: item.is_component ?? false, isLoading={isLoading}
...item,
}}
disabled={isLoading}
data-testid={"edit-flow-button-" + item.id}
button={
!item.is_component ? (
<Link to={"/flow/" + item.id}>
<Button
tabIndex={-1}
variant="outline"
size="sm"
className="whitespace-nowrap"
data-testid={"edit-flow-button-" + item.id}
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button select-none"
/>
Edit Flow
</Button>
</Link>
) : (
<></>
)
}
onClick={
!item.is_component
? () => {
navigate("/flow/" + item.id);
}
: undefined
}
playground={!item.is_component}
control={control} control={control}
/> />
</form> </form>

View file

@ -2,7 +2,7 @@ import useAlertStore from "../../../stores/alertStore";
import { useFolderStore } from "../../../stores/foldersStore"; import { useFolderStore } from "../../../stores/foldersStore";
import { deleteFolder, getFolderById } from "../services"; import { deleteFolder, getFolderById } from "../services";
const useDeleteFolder = ({ navigate }) => { const useDeleteFolder = ({ navigate }: { navigate: (url: string) => void }) => {
const setSuccessData = useAlertStore((state) => state.setSuccessData); const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const folderToEdit = useFolderStore((state) => state.folderToEdit); const folderToEdit = useFolderStore((state) => state.folderToEdit);

View file

@ -1,7 +1,26 @@
import { XYPosition } from "reactflow";
import { CONSOLE_ERROR_MSG } from "../../../constants/alerts_constants"; import { CONSOLE_ERROR_MSG } from "../../../constants/alerts_constants";
import useAlertStore from "../../../stores/alertStore"; import useAlertStore from "../../../stores/alertStore";
const useDropdownOptions = ({ uploadFlow, navigate, is_component }) => { const useDropdownOptions = ({
uploadFlow,
navigate,
is_component,
}: {
uploadFlow: ({
newProject,
file,
isComponent,
position,
}: {
newProject: boolean;
file?: File;
isComponent: boolean | null;
position?: XYPosition;
}) => Promise<string | never>;
navigate: (url: string) => void;
is_component: boolean;
}) => {
const setSuccessData = useAlertStore((state) => state.setSuccessData); const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const handleImportFromJSON = () => { const handleImportFromJSON = () => {

View file

@ -1,6 +1,12 @@
import { getApiKey } from "../../../../../controllers/API"; import { getApiKey } from "../../../../../controllers/API";
import { Users } from "../../../../../types/api";
const useApiKeys = (userData, setLoadingKeys, keysList, setUserId) => { const useApiKeys = (
userData: Users | null,
setLoadingKeys: (load: boolean) => void,
keysList: React.MutableRefObject<never[]>,
setUserId: (userId: string) => void,
) => {
const fetchApiKeys = () => { const fetchApiKeys = () => {
setLoadingKeys(true); setLoadingKeys(true);
getApiKey() getApiKey()

View file

@ -7,10 +7,10 @@ import {
import { deleteApiKey } from "../../../../../controllers/API"; import { deleteApiKey } from "../../../../../controllers/API";
const useDeleteApiKeys = ( const useDeleteApiKeys = (
selectedRows, selectedRows: string[],
resetFilter, resetFilter: () => void,
setSuccessData, setSuccessData: (data: { title: string }) => void,
setErrorData, setErrorData: (data: { title: string; list: string[] }) => void,
) => { ) => {
const handleDeleteKey = () => { const handleDeleteKey = () => {
Promise.all(selectedRows.map((selectedRow) => deleteApiKey(selectedRow))) Promise.all(selectedRows.map((selectedRow) => deleteApiKey(selectedRow)))

View file

@ -4,7 +4,10 @@ import {
BASE_URL_API, BASE_URL_API,
} from "../../../../../../../../../constants/constants"; } from "../../../../../../../../../constants/constants";
const usePreloadImages = (profilePictures, setImagesLoaded) => { const usePreloadImages = (
profilePictures: { [key: string]: string[] },
setImagesLoaded: (value: boolean) => void,
) => {
const preloadImages = async (imageUrls) => { const preloadImages = async (imageUrls) => {
return Promise.all( return Promise.all(
imageUrls.map( imageUrls.map(

View file

@ -5,8 +5,13 @@ import {
SAVE_SUCCESS_ALERT, SAVE_SUCCESS_ALERT,
} from "../../../../constants/alerts_constants"; } from "../../../../constants/alerts_constants";
import { resetPassword } from "../../../../controllers/API"; import { resetPassword } from "../../../../controllers/API";
import { Users } from "../../../../types/api";
const usePatchPassword = (userData, setSuccessData, setErrorData) => { const usePatchPassword = (
userData: Users | null,
setSuccessData: (data: { title: string; list?: string[] }) => void,
setErrorData: (data: { title: string; list: string[] }) => void,
) => {
const handlePatchPassword = async (password, cnfPassword, handleInput) => { const handlePatchPassword = async (password, cnfPassword, handleInput) => {
if (password !== cnfPassword) { if (password !== cnfPassword) {
setErrorData({ setErrorData({
@ -16,7 +21,7 @@ const usePatchPassword = (userData, setSuccessData, setErrorData) => {
return; return;
} }
try { try {
if (password !== "") await resetPassword(userData.id, { password }); if (password !== "") await resetPassword(userData!.id, { password });
handleInput({ target: { name: "password", value: "" } }); handleInput({ target: { name: "password", value: "" } });
handleInput({ target: { name: "cnfPassword", value: "" } }); handleInput({ target: { name: "cnfPassword", value: "" } });
setSuccessData({ title: SAVE_SUCCESS_ALERT }); setSuccessData({ title: SAVE_SUCCESS_ALERT });

View file

@ -4,21 +4,22 @@ import {
SAVE_SUCCESS_ALERT, SAVE_SUCCESS_ALERT,
} from "../../../../constants/alerts_constants"; } from "../../../../constants/alerts_constants";
import { updateUser } from "../../../../controllers/API"; import { updateUser } from "../../../../controllers/API";
import { Users } from "../../../../types/api";
const usePatchProfilePicture = ( const usePatchProfilePicture = (
setSuccessData, setSuccessData: (data: { title: string; list?: string[] }) => void,
setErrorData, setErrorData: (data: { title: string; list: string[] }) => void,
currentUserData, currentUserData: Users | null,
setUserData, setUserData: (data: any) => void,
) => { ) => {
const handlePatchProfilePicture = async (profile_picture) => { const handlePatchProfilePicture = async (profile_picture) => {
try { try {
if (profile_picture !== "") { if (profile_picture !== "") {
await updateUser(currentUserData.id, { await updateUser(currentUserData!.id, {
profile_image: profile_picture, profile_image: profile_picture,
}); });
let newUserData = cloneDeep(currentUserData); let newUserData = cloneDeep(currentUserData);
newUserData.profile_image = profile_picture; newUserData!.profile_image = profile_picture;
setUserData(newUserData); setUserData(newUserData);
} }
setSuccessData({ title: SAVE_SUCCESS_ALERT }); setSuccessData({ title: SAVE_SUCCESS_ALERT });

View file

@ -7,11 +7,11 @@ import { AuthContext } from "../../../../contexts/authContext";
import { addApiKeyStore } from "../../../../controllers/API"; import { addApiKeyStore } from "../../../../controllers/API";
const useSaveKey = ( const useSaveKey = (
setSuccessData, setSuccessData: (data: { title: string }) => void,
setErrorData, setErrorData: (data: { title: string; list: string[] }) => void,
setHasApiKey, setHasApiKey: (hasApiKey: boolean) => void,
setValidApiKey, setValidApiKey: (validApiKey: boolean) => void,
setLoadingApiKey, setLoadingApiKey: (loadingApiKey: boolean) => void,
) => { ) => {
const { storeApiKey } = useContext(AuthContext); const { storeApiKey } = useContext(AuthContext);

View file

@ -1,6 +1,9 @@
import { useEffect } from "react"; import { useEffect } from "react";
const useScrollToElement = (scrollId, setCurrentFlowId) => { const useScrollToElement = (
scrollId: string | null | undefined,
setCurrentFlowId: (currentFlowId: string) => void,
) => {
useEffect(() => { useEffect(() => {
const element = document.getElementById(scrollId ?? "null"); const element = document.getElementById(scrollId ?? "null");
if (element) { if (element) {

View file

@ -1,8 +1,11 @@
import { ColDef, ColGroupDef } from "ag-grid-community";
import { useEffect } from "react"; import { useEffect } from "react";
import { getMessagesTable } from "../../../../../controllers/API"; import { getMessagesTable } from "../../../../../controllers/API";
import { useMessagesStore } from "../../../../../stores/messagesStore"; import { useMessagesStore } from "../../../../../stores/messagesStore";
const useMessagesTable = (setColumns) => { const useMessagesTable = (
setColumns: (data: Array<ColDef | ColGroupDef>) => void,
) => {
const setMessages = useMessagesStore((state) => state.setMessages); const setMessages = useMessagesStore((state) => state.setMessages);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {

View file

@ -2,10 +2,10 @@ import { deleteMessagesFn } from "../../../../../controllers/API";
import { useMessagesStore } from "../../../../../stores/messagesStore"; import { useMessagesStore } from "../../../../../stores/messagesStore";
const useRemoveMessages = ( const useRemoveMessages = (
setSelectedRows, setSelectedRows: (data: number[]) => void,
setSuccessData, setSuccessData: (data: { title: string }) => void,
setErrorData, setErrorData: (data: { title: string }) => void,
selectedRows, selectedRows: number[],
) => { ) => {
const deleteMessages = useMessagesStore((state) => state.removeMessages); const deleteMessages = useMessagesStore((state) => state.removeMessages);

View file

@ -2,7 +2,10 @@ import { updateMessageApi } from "../../../../../controllers/API";
import { useMessagesStore } from "../../../../../stores/messagesStore"; import { useMessagesStore } from "../../../../../stores/messagesStore";
import { Message } from "../../../../../types/messages"; import { Message } from "../../../../../types/messages";
const useUpdateMessage = (setSuccessData, setErrorData) => { const useUpdateMessage = (
setSuccessData: (data: { title: string; list?: string[] }) => void,
setErrorData: (data: { title: string; list?: string[] }) => void,
) => {
const updateMessage = useMessagesStore((state) => state.updateMessage); const updateMessage = useMessagesStore((state) => state.updateMessage);
const handleUpdate = async (data: Message) => { const handleUpdate = async (data: Message) => {

View file

@ -0,0 +1,76 @@
from uuid import UUID
import pytest
from fastapi.testclient import TestClient
from langflow.memory import add_messagetables
# Assuming you have these imports available
from langflow.services.database.models.message import MessageCreate, MessageRead, MessageUpdate
from langflow.services.database.models.message.model import MessageTable
from langflow.services.deps import session_scope
@pytest.fixture()
def created_message():
with session_scope() as session:
message = MessageCreate(text="Test message", sender="User", sender_name="User", session_id="session_id")
messagetable = MessageTable.model_validate(message, from_attributes=True)
messagetables = add_messagetables([messagetable], session)
message_read = MessageRead.model_validate(messagetables[0], from_attributes=True)
return message_read
@pytest.fixture()
def created_messages(session):
with session_scope() as session:
messages = [
MessageCreate(text="Test message 1", sender="User", sender_name="User", session_id="session_id2"),
MessageCreate(text="Test message 2", sender="User", sender_name="User", session_id="session_id2"),
MessageCreate(text="Test message 3", sender="User", sender_name="User", session_id="session_id2"),
]
messagetables = [MessageTable.model_validate(message, from_attributes=True) for message in messages]
message_list = add_messagetables(messagetables, session)
return message_list
def test_delete_messages(client: TestClient, created_messages, logged_in_headers):
response = client.request(
"DELETE", "api/v1/monitor/messages", json=[str(msg.id) for msg in created_messages], headers=logged_in_headers
)
assert response.status_code == 204, response.text
assert response.reason_phrase == "No Content"
def test_update_message(client: TestClient, logged_in_headers, created_message):
message_id = created_message.id
message_update = MessageUpdate(text="Updated content")
response = client.put(
f"api/v1/monitor/messages/{message_id}", json=message_update.model_dump(), headers=logged_in_headers
)
assert response.status_code == 200, response.text
updated_message = MessageRead(**response.json())
assert updated_message.text == "Updated content"
def test_update_message_not_found(client: TestClient, logged_in_headers):
non_existent_id = UUID("00000000-0000-0000-0000-000000000000")
message_update = MessageUpdate(text="Updated content")
response = client.put(
f"api/v1/monitor/messages/{non_existent_id}", json=message_update.model_dump(), headers=logged_in_headers
)
assert response.status_code == 404, response.text
assert response.json()["detail"] == "Message not found"
def test_delete_messages_session(client: TestClient, created_messages, logged_in_headers):
session_id = "session_id2"
response = client.delete(f"api/v1/monitor/messages/session/{session_id}", headers=logged_in_headers)
assert response.status_code == 204
assert response.reason_phrase == "No Content"
assert len(created_messages) == 3
response = client.get("api/v1/monitor/messages", headers=logged_in_headers)
assert response.status_code == 200
assert len(response.json()) == 0

View file

@ -0,0 +1,72 @@
import pytest
from langflow.memory import add_messages, add_messagetables, delete_messages, get_messages, store_message
from langflow.schema.message import Message
# Assuming you have these imports available
from langflow.services.database.models.message import MessageCreate, MessageRead
from langflow.services.database.models.message.model import MessageTable
from langflow.services.deps import session_scope
@pytest.fixture()
def created_message():
with session_scope() as session:
message = MessageCreate(text="Test message", sender="User", sender_name="User", session_id="session_id")
messagetable = MessageTable.model_validate(message, from_attributes=True)
messagetables = add_messagetables([messagetable], session)
message_read = MessageRead.model_validate(messagetables[0], from_attributes=True)
return message_read
@pytest.fixture()
def created_messages(session):
with session_scope() as session:
messages = [
MessageCreate(text="Test message 1", sender="User", sender_name="User", session_id="session_id2"),
MessageCreate(text="Test message 2", sender="User", sender_name="User", session_id="session_id2"),
MessageCreate(text="Test message 3", sender="User", sender_name="User", session_id="session_id2"),
]
messagetables = [MessageTable.model_validate(message, from_attributes=True) for message in messages]
messagetables = add_messagetables(messagetables, session)
messages_read = [
MessageRead.model_validate(messagetable, from_attributes=True) for messagetable in messagetables
]
return messages_read
def test_get_messages(session):
add_messages(Message(text="Test message 1", sender="User", sender_name="User", session_id="session_id2"))
add_messages(Message(text="Test message 2", sender="User", sender_name="User", session_id="session_id2"))
messages = get_messages(sender="User", session_id="session_id2", limit=2)
assert len(messages) == 2
assert messages[0].text == "Test message 1"
assert messages[1].text == "Test message 2"
def test_add_messages(session):
message = Message(text="New Test message", sender="User", sender_name="User", session_id="new_session_id")
messages = add_messages(message)
assert len(messages) == 1
assert messages[0].text == "New Test message"
def test_add_messagetables(session):
messages = [MessageTable(text="New Test message", sender="User", sender_name="User", session_id="new_session_id")]
added_messages = add_messagetables(messages, session)
assert len(added_messages) == 1
assert added_messages[0].text == "New Test message"
def test_delete_messages(session):
session_id = "session_id2"
delete_messages(session_id)
messages = session.query(MessageTable).filter(MessageTable.session_id == session_id).all()
assert len(messages) == 0
def test_store_message(session):
message = Message(text="Stored message", sender="User", sender_name="User", session_id="stored_session_id")
stored_messages = store_message(message)
assert len(stored_messages) == 1
assert stored_messages[0].text == "Stored message"