From 857d01680096858440660777158513d925a35f3e Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Mon, 16 Jun 2025 13:18:28 -0300 Subject: [PATCH 01/13] =?UTF-8?q?=F0=9F=93=9D=20(monitor.py):=20Add=20endp?= =?UTF-8?q?oint=20to=20get=20sessions=20and=20handle=20session=5Fid=20enco?= =?UTF-8?q?ding=20for=20API=20requests=20=F0=9F=93=9D=20(use-get-messages-?= =?UTF-8?q?mutation.ts):=20Implement=20a=20mutation=20function=20to=20fetc?= =?UTF-8?q?h=20messages=20with=20query=20parameters=20and=20handle=20sessi?= =?UTF-8?q?on=5Fid=20encoding=20for=20API=20requests=20=F0=9F=93=9D=20(use?= =?UTF-8?q?-get-messages-polling.ts):=20Ensure=20proper=20encoding=20of=20?= =?UTF-8?q?session=5Fid=20for=20API=20requests=20in=20polling=20mutation?= =?UTF-8?q?=20=F0=9F=93=9D=20(use-get-messages.ts):=20Handle=20session=5Fi?= =?UTF-8?q?d=20encoding=20for=20API=20requests=20in=20messages=20query=20?= =?UTF-8?q?=F0=9F=93=9D=20(new-modal.tsx):=20Implement=20functions=20to=20?= =?UTF-8?q?handle=20session=20deletion=20and=20proper=20encoding=20of=20se?= =?UTF-8?q?ssion=5Fid=20for=20API=20requests=20=F0=9F=93=9D=20(utils.ts):?= =?UTF-8?q?=20Add=20functions=20to=20encode,=20decode,=20validate,=20forma?= =?UTF-8?q?t,=20and=20prepare=20session=20IDs=20for=20API=20requests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/base/langflow/api/v1/monitor.py | 22 +- .../messages/use-get-messages-mutation.ts | 112 +++++ .../messages/use-get-messages-polling.ts | 14 +- .../API/queries/messages/use-get-messages.ts | 14 +- src/frontend/src/modals/IOModal/new-modal.tsx | 3 + .../src/modals/IOModal/playground-modal.tsx | 426 ++++++++++++++++++ src/frontend/src/utils/utils.ts | 90 ++++ 7 files changed, 676 insertions(+), 5 deletions(-) create mode 100644 src/frontend/src/controllers/API/queries/messages/use-get-messages-mutation.ts create mode 100644 src/frontend/src/modals/IOModal/playground-modal.tsx diff --git a/src/backend/base/langflow/api/v1/monitor.py b/src/backend/base/langflow/api/v1/monitor.py index 6658c1cb5a8c..5b718132ab3d 100644 --- a/src/backend/base/langflow/api/v1/monitor.py +++ b/src/backend/base/langflow/api/v1/monitor.py @@ -40,6 +40,24 @@ async def delete_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSes raise HTTPException(status_code=500, detail=str(e)) from e +@router.get("/sessions") +async def get_sessions( + session: DbSession, + flow_id: Annotated[UUID | None, Query()] = None, +) -> list[str]: + try: + stmt = select(MessageTable.session_id).distinct() + stmt = stmt.where(MessageTable.session_id.isnot(None)) + + if flow_id: + stmt = stmt.where(MessageTable.flow_id == flow_id) + + sessions = await session.exec(stmt) + return list(sessions) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) from e + + @router.get("/messages") async def get_messages( session: DbSession, @@ -54,7 +72,9 @@ async def get_messages( if flow_id: stmt = stmt.where(MessageTable.flow_id == flow_id) if session_id: - stmt = stmt.where(MessageTable.session_id == session_id) + from urllib.parse import unquote + decoded_session_id = unquote(session_id) + stmt = stmt.where(MessageTable.session_id == decoded_session_id) if sender: stmt = stmt.where(MessageTable.sender == sender) if sender_name: diff --git a/src/frontend/src/controllers/API/queries/messages/use-get-messages-mutation.ts b/src/frontend/src/controllers/API/queries/messages/use-get-messages-mutation.ts new file mode 100644 index 000000000000..c114953d7338 --- /dev/null +++ b/src/frontend/src/controllers/API/queries/messages/use-get-messages-mutation.ts @@ -0,0 +1,112 @@ +import useFlowStore from "@/stores/flowStore"; +import { useMessagesStore } from "@/stores/messagesStore"; +import { useMutationFunctionType } from "@/types/api"; +import { useQueryClient } from "@tanstack/react-query"; +import { ColDef, ColGroupDef } from "ag-grid-community"; +import { + extractColumnsFromRows, + prepareSessionIdForAPI, +} from "../../../../utils/utils"; +import { api } from "../../api"; +import { getURL } from "../../helpers/constants"; +import { UseRequestProcessor } from "../../services/request-processor"; + +interface MessagesQueryParams { + id?: string; + session_id?: string; + sender?: string; + sender_name?: string; + order_by?: string; + mode: "intersection" | "union"; + excludedFields?: string[]; + params?: object; +} + +interface MessagesResponse { + rows: Array; + columns: Array; +} + +export const useGetMessagesMutation: useMutationFunctionType< + undefined, + MessagesQueryParams +> = (options) => { + const { mutate } = UseRequestProcessor(); + const queryClient = useQueryClient(); + + const getMessagesFn = async ( + payload: MessagesQueryParams, + ): Promise => { + const { + id, + session_id, + sender, + sender_name, + order_by, + mode, + excludedFields, + params, + } = payload; + const isPlaygroundPage = useFlowStore.getState().playgroundPage; + const config = {}; + + const buildQueryParams = (params: Partial) => { + const queryParams = {}; + const paramMap = { + id: "flow_id", + session_id: "session_id", + sender: "sender", + sender_name: "sender_name", + order_by: "order_by", + }; + + Object.entries(paramMap).forEach(([key, apiKey]) => { + if (params[key]) { + // Special handling for session_id to ensure proper URL encoding + if (key === "session_id") { + queryParams[apiKey] = prepareSessionIdForAPI(params[key]); + } else { + queryParams[apiKey] = params[key]; + } + } + }); + + return queryParams; + }; + + const queryParams = buildQueryParams({ + id, + session_id, + sender, + sender_name, + order_by, + }); + config["params"] = { ...queryParams, ...params }; + + let data; + if (!isPlaygroundPage) { + const response = await api.get(`${getURL("MESSAGES")}`, config); + data = response.data; + } else { + data = JSON.parse(window.sessionStorage.getItem(id ?? "") || "[]"); + } + + const columns = extractColumnsFromRows(data, mode, excludedFields); + useMessagesStore.getState().setMessages(data); + + return { rows: data, columns }; + }; + + const mutation = mutate(["useGetMessagesMutation"], getMessagesFn, { + ...options, + onSettled: (response) => { + if (response) { + queryClient.refetchQueries({ + queryKey: ["useGetMessagesQuery"], + }); + } + }, + }); + + return mutation; +}; diff --git a/src/frontend/src/controllers/API/queries/messages/use-get-messages-polling.ts b/src/frontend/src/controllers/API/queries/messages/use-get-messages-polling.ts index 6fca4ef414d1..6e78fd333279 100644 --- a/src/frontend/src/controllers/API/queries/messages/use-get-messages-polling.ts +++ b/src/frontend/src/controllers/API/queries/messages/use-get-messages-polling.ts @@ -2,7 +2,10 @@ import { useMessagesStore } from "@/stores/messagesStore"; import { UseMutationResult } from "@tanstack/react-query"; import { ColDef, ColGroupDef } from "ag-grid-community"; import { useEffect, useRef } from "react"; -import { extractColumnsFromRows } from "../../../../utils/utils"; +import { + extractColumnsFromRows, + prepareSessionIdForAPI, +} from "../../../../utils/utils"; import { api } from "../../api"; import { getURL } from "../../helpers/constants"; import { UseRequestProcessor } from "../../services/request-processor"; @@ -109,7 +112,14 @@ export const useGetMessagesPollingMutation = ( } if (params) { - config["params"] = { ...config["params"], ...params }; + // Process params to ensure session_id is properly encoded + const processedParams = { ...params } as any; + if (processedParams.session_id) { + processedParams.session_id = prepareSessionIdForAPI( + processedParams.session_id, + ); + } + config["params"] = { ...config["params"], ...processedParams }; } const data = await api.get(`${getURL("MESSAGES")}`, config); diff --git a/src/frontend/src/controllers/API/queries/messages/use-get-messages.ts b/src/frontend/src/controllers/API/queries/messages/use-get-messages.ts index 52d5408fde75..07a0497584f9 100644 --- a/src/frontend/src/controllers/API/queries/messages/use-get-messages.ts +++ b/src/frontend/src/controllers/API/queries/messages/use-get-messages.ts @@ -3,7 +3,10 @@ import { useMessagesStore } from "@/stores/messagesStore"; import { keepPreviousData } from "@tanstack/react-query"; import { ColDef, ColGroupDef } from "ag-grid-community"; import { useQueryFunctionType } from "../../../../types/api"; -import { extractColumnsFromRows } from "../../../../utils/utils"; +import { + extractColumnsFromRows, + prepareSessionIdForAPI, +} from "../../../../utils/utils"; import { api } from "../../api"; import { getURL } from "../../helpers/constants"; import { UseRequestProcessor } from "../../services/request-processor"; @@ -33,7 +36,14 @@ export const useGetMessagesQuery: useQueryFunctionType< config["params"] = { flow_id: id }; } if (params) { - config["params"] = { ...config["params"], ...params }; + // Process params to ensure session_id is properly encoded + const processedParams = { ...params } as any; + if (processedParams.session_id) { + processedParams.session_id = prepareSessionIdForAPI( + processedParams.session_id, + ); + } + config["params"] = { ...config["params"], ...processedParams }; } if (!isPlaygroundPage) { return await api.get(`${getURL("MESSAGES")}`, config); diff --git a/src/frontend/src/modals/IOModal/new-modal.tsx b/src/frontend/src/modals/IOModal/new-modal.tsx index 4e57b2e62b37..cbbe6599a58c 100644 --- a/src/frontend/src/modals/IOModal/new-modal.tsx +++ b/src/frontend/src/modals/IOModal/new-modal.tsx @@ -150,6 +150,9 @@ export default function IOModal({ { mode: "union", id: currentFlowId, + params: { + session_id: visibleSession, + }, }, { enabled: open }, ); diff --git a/src/frontend/src/modals/IOModal/playground-modal.tsx b/src/frontend/src/modals/IOModal/playground-modal.tsx new file mode 100644 index 000000000000..cbbe6599a58c --- /dev/null +++ b/src/frontend/src/modals/IOModal/playground-modal.tsx @@ -0,0 +1,426 @@ +//import LangflowLogoColor from "@/assets/LangflowLogocolor.svg?react"; +import ThemeButtons from "@/components/core/appHeaderComponent/components/ThemeButtons"; +import { EventDeliveryType } from "@/constants/enums"; +import { useGetConfig } from "@/controllers/API/queries/config/use-get-config"; +import { + useDeleteMessages, + useGetMessagesQuery, +} from "@/controllers/API/queries/messages"; +import { ENABLE_PUBLISH } from "@/customization/feature-flags"; +import { track } from "@/customization/utils/analytics"; +import { customOpenNewTab } from "@/customization/utils/custom-open-new-tab"; +import { LangflowButtonRedirectTarget } from "@/customization/utils/urls"; +import { useUtilityStore } from "@/stores/utilityStore"; +import { swatchColors } from "@/utils/styleUtils"; +import { useCallback, useEffect, useState } from "react"; +import { v5 as uuidv5 } from "uuid"; +import { useShallow } from "zustand/react/shallow"; +import LangflowLogoColor from "../../assets/LangflowLogoColor.svg?react"; +import IconComponent from "../../components/common/genericIconComponent"; +import ShadTooltip from "../../components/common/shadTooltipComponent"; +import { Button } from "../../components/ui/button"; +import useAlertStore from "../../stores/alertStore"; +import useFlowStore from "../../stores/flowStore"; +import useFlowsManagerStore from "../../stores/flowsManagerStore"; +import { useMessagesStore } from "../../stores/messagesStore"; +import { IOModalPropsType } from "../../types/components"; +import { cn, getNumberFromString } from "../../utils/utils"; +import BaseModal from "../baseModal"; +import { ChatViewWrapper } from "./components/chat-view-wrapper"; +import { SelectedViewField } from "./components/selected-view-field"; +import { SidebarOpenView } from "./components/sidebar-open-view"; + +export default function IOModal({ + children, + open, + setOpen, + disable, + isPlayground, + canvasOpen, + playgroundPage, +}: IOModalPropsType): JSX.Element { + const setIOModalOpen = useFlowsManagerStore((state) => state.setIOModalOpen); + const inputs = useFlowStore((state) => state.inputs); + const outputs = useFlowStore((state) => state.outputs); + const nodes = useFlowStore((state) => state.nodes); + const buildFlow = useFlowStore((state) => state.buildFlow); + const setIsBuilding = useFlowStore((state) => state.setIsBuilding); + const isBuilding = useFlowStore((state) => state.isBuilding); + const { flowIcon, flowId, flowGradient, flowName } = useFlowStore( + useShallow((state) => ({ + flowIcon: state.currentFlow?.icon, + flowId: state.currentFlow?.id, + flowGradient: state.currentFlow?.gradient, + flowName: state.currentFlow?.name, + })), + ); + const filteredInputs = inputs.filter((input) => input.type !== "ChatInput"); + const chatInput = inputs.find((input) => input.type === "ChatInput"); + const filteredOutputs = outputs.filter( + (output) => output.type !== "ChatOutput", + ); + const chatOutput = outputs.find((output) => output.type === "ChatOutput"); + const filteredNodes = nodes.filter( + (node) => + inputs.some((input) => input.id === node.id) || + filteredOutputs.some((output) => output.id === node.id), + ); + const haveChat = chatInput || chatOutput; + const setErrorData = useAlertStore((state) => state.setErrorData); + const setSuccessData = useAlertStore((state) => state.setSuccessData); + const deleteSession = useMessagesStore((state) => state.deleteSession); + const clientId = useUtilityStore((state) => state.clientId); + let realFlowId = useFlowsManagerStore((state) => state.currentFlowId); + const currentFlowId = playgroundPage + ? uuidv5(`${clientId}_${realFlowId}`, uuidv5.DNS) + : realFlowId; + const [sidebarOpen, setSidebarOpen] = useState(true); + + const { mutate: deleteSessionFunction } = useDeleteMessages(); + const [visibleSession, setvisibleSession] = useState( + currentFlowId, + ); + const PlaygroundTitle = playgroundPage && flowName ? flowName : "Playground"; + + useEffect(() => { + setIOModalOpen(open); + return () => { + setIOModalOpen(false); + }; + }, [open]); + + function handleDeleteSession(session_id: string) { + deleteSessionFunction( + { + ids: messages + .filter((msg) => msg.session_id === session_id) + .map((msg) => msg.id), + }, + { + onSuccess: () => { + setSuccessData({ + title: "Session deleted successfully.", + }); + deleteSession(session_id); + if (visibleSession === session_id) { + setvisibleSession(undefined); + } + }, + onError: () => { + setErrorData({ + title: "Error deleting Session.", + }); + }, + }, + ); + } + + function startView() { + if (!chatInput && !chatOutput) { + if (filteredInputs.length > 0) { + return filteredInputs[0]; + } else { + return filteredOutputs[0]; + } + } else { + return undefined; + } + } + + const [selectedViewField, setSelectedViewField] = useState< + { type: string; id: string } | undefined + >(startView()); + + const messages = useMessagesStore((state) => state.messages); + const [sessions, setSessions] = useState( + Array.from( + new Set( + messages + .filter((message) => message.flow_id === currentFlowId) + .map((message) => message.session_id), + ), + ), + ); + const [sessionId, setSessionId] = useState(currentFlowId); + const setCurrentSessionId = useUtilityStore( + (state) => state.setCurrentSessionId, + ); + + const { isFetched: messagesFetched } = useGetMessagesQuery( + { + mode: "union", + id: currentFlowId, + params: { + session_id: visibleSession, + }, + }, + { enabled: open }, + ); + + const chatValue = useUtilityStore((state) => state.chatValueStore); + const setChatValue = useUtilityStore((state) => state.setChatValueStore); + const eventDeliveryConfig = useUtilityStore((state) => state.eventDelivery); + + const sendMessage = useCallback( + async ({ + repeat = 1, + files, + }: { + repeat: number; + files?: string[]; + }): Promise => { + if (isBuilding) return; + setChatValue(""); + for (let i = 0; i < repeat; i++) { + await buildFlow({ + input_value: chatValue, + startNodeId: chatInput?.id, + files: files, + silent: true, + session: sessionId, + eventDelivery: eventDeliveryConfig, + }).catch((err) => { + console.error(err); + }); + } + }, + [isBuilding, setIsBuilding, chatValue, chatInput?.id, sessionId, buildFlow], + ); + + useEffect(() => { + const sessions = new Set(); + messages + .filter((message) => message.flow_id === currentFlowId) + .forEach((row) => { + sessions.add(row.session_id); + }); + setSessions((prev) => { + if (prev.length < Array.from(sessions).length) { + // set the new session as visible + setvisibleSession( + Array.from(sessions)[Array.from(sessions).length - 1], + ); + } + return Array.from(sessions); + }); + }, [messages]); + + useEffect(() => { + if (!visibleSession) { + setSessionId( + `Session ${new Date().toLocaleString("en-US", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", hour12: false, second: "2-digit", timeZone: "UTC" })}`, + ); + setCurrentSessionId(currentFlowId); + } else if (visibleSession) { + setSessionId(visibleSession); + setCurrentSessionId(visibleSession); + if (selectedViewField?.type === "Session") { + setSelectedViewField({ + id: visibleSession, + type: "Session", + }); + } + } + }, [visibleSession]); + + const setPlaygroundScrollBehaves = useUtilityStore( + (state) => state.setPlaygroundScrollBehaves, + ); + + useEffect(() => { + if (open) { + setPlaygroundScrollBehaves("instant"); + } + }, [open]); + + useEffect(() => { + const handleResize = () => { + if (window.innerWidth < 1024) { + // 1024px is Tailwind's 'lg' breakpoint + setSidebarOpen(false); + } else { + setSidebarOpen(true); + } + }; + + // Initial check + handleResize(); + + // Add event listener + window.addEventListener("resize", handleResize); + + // Cleanup + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + const showPublishOptions = playgroundPage && ENABLE_PUBLISH; + + const LangflowButtonClick = () => { + track("LangflowButtonClick"); + customOpenNewTab(LangflowButtonRedirectTarget()); + }; + + useEffect(() => { + if (playgroundPage && messages.length > 0) { + window.sessionStorage.setItem(currentFlowId, JSON.stringify(messages)); + } + }, [playgroundPage, messages]); + + const swatchIndex = + (flowGradient && !isNaN(parseInt(flowGradient)) + ? parseInt(flowGradient) + : getNumberFromString(flowGradient ?? flowId ?? "")) % + swatchColors.length; + + return ( + sendMessage({ repeat: 1 })} + size="x-large" + className="!rounded-[12px] p-0" + > + {children} + {/* TODO ADAPT TO ALL TYPES OF INPUTS AND OUTPUTS */} + + {open && ( +
+
+
+
+
+
+ +
+ {sidebarOpen && ( +
+ {PlaygroundTitle} +
+ )} +
+ + + +
+ {sidebarOpen && ( + + )} + {sidebarOpen && showPublishOptions && ( +
+
+
Theme
+ +
+ +
+ )} +
+
+ {!sidebarOpen && showPublishOptions && ( +
+ + + +
+ )} +
+ {selectedViewField && ( + + )} + +
+
+ )} +
+
+ ); +} diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index c002a09d565a..f21d38630ad1 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -902,3 +902,93 @@ export function getOS() { return os; } + +/** + * Encodes a session ID for safe URL transmission + * Handles both UUID format and date-time format session IDs + * @param {string} session_id - The session ID to encode + * @returns {string} The URL-encoded session ID + */ +export function encodeSessionId(session_id: string): string { + if (!session_id) return ""; + // Use encodeURIComponent to properly encode spaces, commas, colons, etc. + return encodeURIComponent(session_id); +} + +/** + * Decodes a session ID from URL encoding + * @param {string} encoded_session_id - The URL-encoded session ID + * @returns {string} The decoded session ID + */ +export function decodeSessionId(encoded_session_id: string): string { + if (!encoded_session_id) return ""; + try { + return decodeURIComponent(encoded_session_id); + } catch (error) { + console.warn("Failed to decode session ID:", encoded_session_id, error); + return encoded_session_id; // Return as-is if decoding fails + } +} + +/** + * Validates if a string is a valid UUID format + * @param {string} str - The string to validate + * @returns {boolean} True if the string is a valid UUID format + */ +export function isUUID(str: string): boolean { + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(str); +} + +/** + * Validates if a string is a date-time session format + * @param {string} str - The string to validate + * @returns {boolean} True if the string appears to be a date-time session format + */ +export function isDateTimeSession(str: string): boolean { + // Check for patterns like "Session Jun 16, 15:44:08" or similar + const dateTimeSessionRegex = + /^Session\s+\w{3}\s+\d{1,2},\s+\d{2}:\d{2}:\d{2}$/; + return dateTimeSessionRegex.test(str); +} + +/** + * Formats and normalizes session IDs for consistent handling + * Handles both UUID format and date-time format session IDs + * @param {string} session_id - The session ID to format + * @returns {string} The formatted session ID + */ +export function sessionIdFormatted(session_id: string): string { + if (!session_id) return ""; + + // Decode if it appears to be URL encoded + let decodedId = session_id; + if (session_id.includes("%") || session_id.includes("+")) { + decodedId = decodeSessionId(session_id); + } + + // If it's a UUID, return as-is (already in good format) + if (isUUID(decodedId)) { + return decodedId; + } + + // If it's a date-time session, return as-is + if (isDateTimeSession(decodedId)) { + return decodedId; + } + + // For any other format, return as-is but ensure it's properly trimmed + return decodedId.trim(); +} + +/** + * Safely prepares a session ID for API requests + * This function should be used when adding session_id to API parameters + * @param {string} session_id - The session ID to prepare + * @returns {string} The properly encoded session ID for API use + */ +export function prepareSessionIdForAPI(session_id: string): string { + const formatted = sessionIdFormatted(session_id); + return encodeSessionId(formatted); +} From 668a3254ea5cc7cf5368ee6ca361c7d2866d0c5b Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Mon, 16 Jun 2025 16:22:16 -0300 Subject: [PATCH 02/13] =?UTF-8?q?=F0=9F=93=9D=20(constants.ts):=20Add=20SE?= =?UTF-8?q?SSIONS=20constant=20to=20API=20URLs=20for=20monitoring=20sessio?= =?UTF-8?q?ns=20=F0=9F=94=A7=20(use-delete-messages.ts):=20Add=20queryClie?= =?UTF-8?q?nt=20to=20UseRequestProcessor=20to=20invalidate=20sessions=20qu?= =?UTF-8?q?ery=20=E2=9C=A8=20(use-get-sessions-from-flow.ts):=20Introduce?= =?UTF-8?q?=20useGetSessionsFromFlowQuery=20to=20fetch=20sessions=20from?= =?UTF-8?q?=20flow=20=F0=9F=94=A7=20(use-rename-session.ts):=20Change=20re?= =?UTF-8?q?fetchQueries=20to=20invalidateQueries=20for=20useGetSessionsFro?= =?UTF-8?q?mFlowQuery=20=F0=9F=94=A7=20(custom-new-modal.tsx):=20Update=20?= =?UTF-8?q?import=20path=20for=20IOModal=20to=20playground-modal=20?= =?UTF-8?q?=F0=9F=94=A7=20(session-selector.tsx):=20Add=20setActiveSession?= =?UTF-8?q?=20function=20to=20handle=20setting=20active=20session=20?= =?UTF-8?q?=F0=9F=94=A7=20(sidebar-open-view.tsx):=20Add=20setActiveSessio?= =?UTF-8?q?n=20function=20to=20handle=20setting=20active=20session=20?= =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(new-modal.tsx):=20Refactor=20IOModal=20in?= =?UTF-8?q?to=20playground-modal=20and=20update=20functionality=20?= =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(playground-modal.tsx):=20Refactor=20IOMod?= =?UTF-8?q?al=20to=20handle=20playground-specific=20functionality=20?= =?UTF-8?q?=E2=AC=86=EF=B8=8F=20(flowStore.ts):=20Add=20newChatOnPlaygroun?= =?UTF-8?q?d=20state=20and=20setNewChatOnPlayground=20function=20=E2=AC=86?= =?UTF-8?q?=EF=B8=8F=20(index.ts):=20Update=20FlowStoreType=20to=20include?= =?UTF-8?q?=20newChatOnPlayground=20and=20setNewChatOnPlayground?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/API/helpers/constants.ts | 1 + .../queries/messages/use-delete-messages.ts | 14 +- .../messages/use-get-sessions-from-flow.ts | 60 +++ .../queries/messages/use-rename-session.ts | 7 +- .../components/custom-new-modal.tsx | 2 +- .../components/session-selector.tsx | 6 + .../IOModal/components/sidebar-open-view.tsx | 12 +- src/frontend/src/modals/IOModal/new-modal.tsx | 426 ------------------ .../src/modals/IOModal/playground-modal.tsx | 124 +++-- .../modals/IOModal/types/sidebar-open-view.ts | 1 + src/frontend/src/stores/flowStore.ts | 4 + src/frontend/src/types/zustand/flow/index.ts | 2 + 12 files changed, 187 insertions(+), 472 deletions(-) create mode 100644 src/frontend/src/controllers/API/queries/messages/use-get-sessions-from-flow.ts delete mode 100644 src/frontend/src/modals/IOModal/new-modal.tsx diff --git a/src/frontend/src/controllers/API/helpers/constants.ts b/src/frontend/src/controllers/API/helpers/constants.ts index 627480595a4d..99d4841fc492 100644 --- a/src/frontend/src/controllers/API/helpers/constants.ts +++ b/src/frontend/src/controllers/API/helpers/constants.ts @@ -7,6 +7,7 @@ export const URLs = { FILE_MANAGEMENT: `files`, VERSION: `version`, MESSAGES: `monitor/messages`, + SESSIONS: `monitor/sessions`, BUILDS: `monitor/builds`, STORE: `store`, USERS: "users", diff --git a/src/frontend/src/controllers/API/queries/messages/use-delete-messages.ts b/src/frontend/src/controllers/API/queries/messages/use-delete-messages.ts index 1f577f072eb8..7cc51a612247 100644 --- a/src/frontend/src/controllers/API/queries/messages/use-delete-messages.ts +++ b/src/frontend/src/controllers/API/queries/messages/use-delete-messages.ts @@ -12,7 +12,7 @@ export const useDeleteMessages: useMutationFunctionType< undefined, DeleteMessagesParams > = (options?) => { - const { mutate } = UseRequestProcessor(); + const { mutate, queryClient } = UseRequestProcessor(); const deleteMessage = async ({ ids }: DeleteMessagesParams): Promise => { const response = await api.delete(`${getURL("MESSAGES")}`, { @@ -26,7 +26,17 @@ export const useDeleteMessages: useMutationFunctionType< DeleteMessagesParams, any, DeleteMessagesParams - > = mutate(["useDeleteMessages"], deleteMessage, options); + > = mutate(["useDeleteMessages"], deleteMessage, { + ...options, + onSettled: (data, error, variables, context) => { + // Invalidate sessions query to refetch the updated session list + queryClient.invalidateQueries({ + queryKey: ["useGetSessionsFromFlowQuery"], + }); + // Call the original onSettled if provided + options?.onSettled?.(data, error, variables, context); + }, + }); return mutation; }; diff --git a/src/frontend/src/controllers/API/queries/messages/use-get-sessions-from-flow.ts b/src/frontend/src/controllers/API/queries/messages/use-get-sessions-from-flow.ts new file mode 100644 index 000000000000..0def48c581c7 --- /dev/null +++ b/src/frontend/src/controllers/API/queries/messages/use-get-sessions-from-flow.ts @@ -0,0 +1,60 @@ +import useFlowStore from "@/stores/flowStore"; +import { keepPreviousData } from "@tanstack/react-query"; +import { useQueryFunctionType } from "../../../../types/api"; +import { api } from "../../api"; +import { getURL } from "../../helpers/constants"; +import { UseRequestProcessor } from "../../services/request-processor"; + +interface SessionsQueryParams { + id?: string; +} + +interface SessionsResponse { + sessions: string[]; +} + +export const useGetSessionsFromFlowQuery: useQueryFunctionType< + SessionsQueryParams, + SessionsResponse +> = ({ id }, options) => { + const { query } = UseRequestProcessor(); + + const getSessionsFn = async (id?: string) => { + const isPlaygroundPage = useFlowStore.getState().playgroundPage; + const config = {}; + if (id) { + config["params"] = { flow_id: id }; + } + + if (!isPlaygroundPage) { + return await api.get(`${getURL("SESSIONS")}`, config); + } else { + // For playground mode, get sessions from sessionStorage + const data = JSON.parse(window.sessionStorage.getItem(id ?? "") || "[]"); + // Extract unique session IDs from stored messages + const sessionIdsSet = new Set( + data.map((msg: any) => msg.session_id).filter(Boolean), + ); + const sessionIds = Array.from(sessionIdsSet); + return { + data: sessionIds, + }; + } + }; + + const responseFn = async () => { + const response = await getSessionsFn(id); + return { sessions: response.data }; + }; + + const queryResult = query( + ["useGetSessionsFromFlowQuery", { id }], + responseFn, + { + placeholderData: keepPreviousData, + ...options, + }, + ); + + return queryResult; +}; diff --git a/src/frontend/src/controllers/API/queries/messages/use-rename-session.ts b/src/frontend/src/controllers/API/queries/messages/use-rename-session.ts index c4cc5e1427cf..f9df9fa0cca9 100644 --- a/src/frontend/src/controllers/API/queries/messages/use-rename-session.ts +++ b/src/frontend/src/controllers/API/queries/messages/use-rename-session.ts @@ -48,10 +48,9 @@ export const useUpdateSessionName: useMutationFunctionType< const mutation: UseMutationResult = mutate(["useUpdateSessionName"], updateSessionApi, { ...options, - onSettled: (data, variables, context) => { - // Invalidate and refetch relevant queries - queryClient.refetchQueries({ - queryKey: ["useGetMessagesQuery"], + onSettled: () => { + queryClient.invalidateQueries({ + queryKey: ["useGetSessionsFromFlowQuery"], }); }, }); diff --git a/src/frontend/src/customization/components/custom-new-modal.tsx b/src/frontend/src/customization/components/custom-new-modal.tsx index 9c95cd2703b2..781baf143ab4 100644 --- a/src/frontend/src/customization/components/custom-new-modal.tsx +++ b/src/frontend/src/customization/components/custom-new-modal.tsx @@ -1,4 +1,4 @@ -import IOModal from "@/modals/IOModal/new-modal"; +import IOModal from "@/modals/IOModal/playground-modal"; import { IOModalPropsType } from "@/types/components"; export function CustomIOModal({ diff --git a/src/frontend/src/modals/IOModal/components/IOFieldView/components/session-selector.tsx b/src/frontend/src/modals/IOModal/components/IOFieldView/components/session-selector.tsx index 0d379f010030..6761058f9ae9 100644 --- a/src/frontend/src/modals/IOModal/components/IOFieldView/components/session-selector.tsx +++ b/src/frontend/src/modals/IOModal/components/IOFieldView/components/session-selector.tsx @@ -26,6 +26,7 @@ export default function SessionSelector({ selectedView, setSelectedView, playgroundPage, + setActiveSession, }: { deleteSession: (session: string) => void; session: string; @@ -36,6 +37,7 @@ export default function SessionSelector({ selectedView?: { type: string; id: string }; setSelectedView: (view: { type: string; id: string } | undefined) => void; playgroundPage: boolean; + setActiveSession: (session: string) => void; }) { const clientId = useUtilityStore((state) => state.clientId); let realFlowId = useFlowsManagerStore((state) => state.currentFlowId); @@ -46,6 +48,9 @@ export default function SessionSelector({ const [editedSession, setEditedSession] = useState(session); const { mutate: updateSessionName } = useUpdateSessionName(); const inputRef = useRef(null); + const setNewChatOnPlayground = useFlowStore( + (state) => state.setNewChatOnPlayground, + ); useEffect(() => { setEditedSession(session); @@ -127,6 +132,7 @@ export default function SessionSelector({ data-testid="session-selector" onClick={(e) => { setNewSessionCloseVoiceAssistant(true); + setNewChatOnPlayground(true); if (isEditing) e.stopPropagation(); else toggleVisibility(); }} diff --git a/src/frontend/src/modals/IOModal/components/sidebar-open-view.tsx b/src/frontend/src/modals/IOModal/components/sidebar-open-view.tsx index ef036a23608a..c69d7c1ad394 100644 --- a/src/frontend/src/modals/IOModal/components/sidebar-open-view.tsx +++ b/src/frontend/src/modals/IOModal/components/sidebar-open-view.tsx @@ -1,5 +1,6 @@ import ShadTooltip from "@/components/common/shadTooltipComponent"; import { Button } from "@/components/ui/button"; +import useFlowStore from "@/stores/flowStore"; import { useVoiceStore } from "@/stores/voiceStore"; import IconComponent from "../../../components/common/genericIconComponent"; import { SidebarOpenViewProps } from "../types/sidebar-open-view"; @@ -13,11 +14,16 @@ export const SidebarOpenView = ({ visibleSession, selectedViewField, playgroundPage, + setActiveSession, }: SidebarOpenViewProps) => { const setNewSessionCloseVoiceAssistant = useVoiceStore( (state) => state.setNewSessionCloseVoiceAssistant, ); + const setNewChatOnPlayground = useFlowStore( + (state) => state.setNewChatOnPlayground, + ); + return ( <>
@@ -40,6 +46,7 @@ export const SidebarOpenView = ({ setvisibleSession(undefined); setSelectedViewField(undefined); setNewSessionCloseVoiceAssistant(true); + setNewChatOnPlayground(true); }} > { - setvisibleSession(session); + setActiveSession(session); }} isVisible={visibleSession === session} inspectSession={(session) => { @@ -78,6 +85,9 @@ export const SidebarOpenView = ({ type: "Session", }); }} + setActiveSession={(session) => { + setActiveSession(session); + }} /> ))}
diff --git a/src/frontend/src/modals/IOModal/new-modal.tsx b/src/frontend/src/modals/IOModal/new-modal.tsx deleted file mode 100644 index cbbe6599a58c..000000000000 --- a/src/frontend/src/modals/IOModal/new-modal.tsx +++ /dev/null @@ -1,426 +0,0 @@ -//import LangflowLogoColor from "@/assets/LangflowLogocolor.svg?react"; -import ThemeButtons from "@/components/core/appHeaderComponent/components/ThemeButtons"; -import { EventDeliveryType } from "@/constants/enums"; -import { useGetConfig } from "@/controllers/API/queries/config/use-get-config"; -import { - useDeleteMessages, - useGetMessagesQuery, -} from "@/controllers/API/queries/messages"; -import { ENABLE_PUBLISH } from "@/customization/feature-flags"; -import { track } from "@/customization/utils/analytics"; -import { customOpenNewTab } from "@/customization/utils/custom-open-new-tab"; -import { LangflowButtonRedirectTarget } from "@/customization/utils/urls"; -import { useUtilityStore } from "@/stores/utilityStore"; -import { swatchColors } from "@/utils/styleUtils"; -import { useCallback, useEffect, useState } from "react"; -import { v5 as uuidv5 } from "uuid"; -import { useShallow } from "zustand/react/shallow"; -import LangflowLogoColor from "../../assets/LangflowLogoColor.svg?react"; -import IconComponent from "../../components/common/genericIconComponent"; -import ShadTooltip from "../../components/common/shadTooltipComponent"; -import { Button } from "../../components/ui/button"; -import useAlertStore from "../../stores/alertStore"; -import useFlowStore from "../../stores/flowStore"; -import useFlowsManagerStore from "../../stores/flowsManagerStore"; -import { useMessagesStore } from "../../stores/messagesStore"; -import { IOModalPropsType } from "../../types/components"; -import { cn, getNumberFromString } from "../../utils/utils"; -import BaseModal from "../baseModal"; -import { ChatViewWrapper } from "./components/chat-view-wrapper"; -import { SelectedViewField } from "./components/selected-view-field"; -import { SidebarOpenView } from "./components/sidebar-open-view"; - -export default function IOModal({ - children, - open, - setOpen, - disable, - isPlayground, - canvasOpen, - playgroundPage, -}: IOModalPropsType): JSX.Element { - const setIOModalOpen = useFlowsManagerStore((state) => state.setIOModalOpen); - const inputs = useFlowStore((state) => state.inputs); - const outputs = useFlowStore((state) => state.outputs); - const nodes = useFlowStore((state) => state.nodes); - const buildFlow = useFlowStore((state) => state.buildFlow); - const setIsBuilding = useFlowStore((state) => state.setIsBuilding); - const isBuilding = useFlowStore((state) => state.isBuilding); - const { flowIcon, flowId, flowGradient, flowName } = useFlowStore( - useShallow((state) => ({ - flowIcon: state.currentFlow?.icon, - flowId: state.currentFlow?.id, - flowGradient: state.currentFlow?.gradient, - flowName: state.currentFlow?.name, - })), - ); - const filteredInputs = inputs.filter((input) => input.type !== "ChatInput"); - const chatInput = inputs.find((input) => input.type === "ChatInput"); - const filteredOutputs = outputs.filter( - (output) => output.type !== "ChatOutput", - ); - const chatOutput = outputs.find((output) => output.type === "ChatOutput"); - const filteredNodes = nodes.filter( - (node) => - inputs.some((input) => input.id === node.id) || - filteredOutputs.some((output) => output.id === node.id), - ); - const haveChat = chatInput || chatOutput; - const setErrorData = useAlertStore((state) => state.setErrorData); - const setSuccessData = useAlertStore((state) => state.setSuccessData); - const deleteSession = useMessagesStore((state) => state.deleteSession); - const clientId = useUtilityStore((state) => state.clientId); - let realFlowId = useFlowsManagerStore((state) => state.currentFlowId); - const currentFlowId = playgroundPage - ? uuidv5(`${clientId}_${realFlowId}`, uuidv5.DNS) - : realFlowId; - const [sidebarOpen, setSidebarOpen] = useState(true); - - const { mutate: deleteSessionFunction } = useDeleteMessages(); - const [visibleSession, setvisibleSession] = useState( - currentFlowId, - ); - const PlaygroundTitle = playgroundPage && flowName ? flowName : "Playground"; - - useEffect(() => { - setIOModalOpen(open); - return () => { - setIOModalOpen(false); - }; - }, [open]); - - function handleDeleteSession(session_id: string) { - deleteSessionFunction( - { - ids: messages - .filter((msg) => msg.session_id === session_id) - .map((msg) => msg.id), - }, - { - onSuccess: () => { - setSuccessData({ - title: "Session deleted successfully.", - }); - deleteSession(session_id); - if (visibleSession === session_id) { - setvisibleSession(undefined); - } - }, - onError: () => { - setErrorData({ - title: "Error deleting Session.", - }); - }, - }, - ); - } - - function startView() { - if (!chatInput && !chatOutput) { - if (filteredInputs.length > 0) { - return filteredInputs[0]; - } else { - return filteredOutputs[0]; - } - } else { - return undefined; - } - } - - const [selectedViewField, setSelectedViewField] = useState< - { type: string; id: string } | undefined - >(startView()); - - const messages = useMessagesStore((state) => state.messages); - const [sessions, setSessions] = useState( - Array.from( - new Set( - messages - .filter((message) => message.flow_id === currentFlowId) - .map((message) => message.session_id), - ), - ), - ); - const [sessionId, setSessionId] = useState(currentFlowId); - const setCurrentSessionId = useUtilityStore( - (state) => state.setCurrentSessionId, - ); - - const { isFetched: messagesFetched } = useGetMessagesQuery( - { - mode: "union", - id: currentFlowId, - params: { - session_id: visibleSession, - }, - }, - { enabled: open }, - ); - - const chatValue = useUtilityStore((state) => state.chatValueStore); - const setChatValue = useUtilityStore((state) => state.setChatValueStore); - const eventDeliveryConfig = useUtilityStore((state) => state.eventDelivery); - - const sendMessage = useCallback( - async ({ - repeat = 1, - files, - }: { - repeat: number; - files?: string[]; - }): Promise => { - if (isBuilding) return; - setChatValue(""); - for (let i = 0; i < repeat; i++) { - await buildFlow({ - input_value: chatValue, - startNodeId: chatInput?.id, - files: files, - silent: true, - session: sessionId, - eventDelivery: eventDeliveryConfig, - }).catch((err) => { - console.error(err); - }); - } - }, - [isBuilding, setIsBuilding, chatValue, chatInput?.id, sessionId, buildFlow], - ); - - useEffect(() => { - const sessions = new Set(); - messages - .filter((message) => message.flow_id === currentFlowId) - .forEach((row) => { - sessions.add(row.session_id); - }); - setSessions((prev) => { - if (prev.length < Array.from(sessions).length) { - // set the new session as visible - setvisibleSession( - Array.from(sessions)[Array.from(sessions).length - 1], - ); - } - return Array.from(sessions); - }); - }, [messages]); - - useEffect(() => { - if (!visibleSession) { - setSessionId( - `Session ${new Date().toLocaleString("en-US", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", hour12: false, second: "2-digit", timeZone: "UTC" })}`, - ); - setCurrentSessionId(currentFlowId); - } else if (visibleSession) { - setSessionId(visibleSession); - setCurrentSessionId(visibleSession); - if (selectedViewField?.type === "Session") { - setSelectedViewField({ - id: visibleSession, - type: "Session", - }); - } - } - }, [visibleSession]); - - const setPlaygroundScrollBehaves = useUtilityStore( - (state) => state.setPlaygroundScrollBehaves, - ); - - useEffect(() => { - if (open) { - setPlaygroundScrollBehaves("instant"); - } - }, [open]); - - useEffect(() => { - const handleResize = () => { - if (window.innerWidth < 1024) { - // 1024px is Tailwind's 'lg' breakpoint - setSidebarOpen(false); - } else { - setSidebarOpen(true); - } - }; - - // Initial check - handleResize(); - - // Add event listener - window.addEventListener("resize", handleResize); - - // Cleanup - return () => { - window.removeEventListener("resize", handleResize); - }; - }, []); - - const showPublishOptions = playgroundPage && ENABLE_PUBLISH; - - const LangflowButtonClick = () => { - track("LangflowButtonClick"); - customOpenNewTab(LangflowButtonRedirectTarget()); - }; - - useEffect(() => { - if (playgroundPage && messages.length > 0) { - window.sessionStorage.setItem(currentFlowId, JSON.stringify(messages)); - } - }, [playgroundPage, messages]); - - const swatchIndex = - (flowGradient && !isNaN(parseInt(flowGradient)) - ? parseInt(flowGradient) - : getNumberFromString(flowGradient ?? flowId ?? "")) % - swatchColors.length; - - return ( - sendMessage({ repeat: 1 })} - size="x-large" - className="!rounded-[12px] p-0" - > - {children} - {/* TODO ADAPT TO ALL TYPES OF INPUTS AND OUTPUTS */} - - {open && ( -
-
-
-
-
-
- -
- {sidebarOpen && ( -
- {PlaygroundTitle} -
- )} -
- - - -
- {sidebarOpen && ( - - )} - {sidebarOpen && showPublishOptions && ( -
-
-
Theme
- -
- -
- )} -
-
- {!sidebarOpen && showPublishOptions && ( -
- - - -
- )} -
- {selectedViewField && ( - - )} - -
-
- )} -
-
- ); -} diff --git a/src/frontend/src/modals/IOModal/playground-modal.tsx b/src/frontend/src/modals/IOModal/playground-modal.tsx index cbbe6599a58c..fa8dc51e38d1 100644 --- a/src/frontend/src/modals/IOModal/playground-modal.tsx +++ b/src/frontend/src/modals/IOModal/playground-modal.tsx @@ -1,18 +1,17 @@ //import LangflowLogoColor from "@/assets/LangflowLogocolor.svg?react"; import ThemeButtons from "@/components/core/appHeaderComponent/components/ThemeButtons"; -import { EventDeliveryType } from "@/constants/enums"; -import { useGetConfig } from "@/controllers/API/queries/config/use-get-config"; import { useDeleteMessages, useGetMessagesQuery, } from "@/controllers/API/queries/messages"; +import { useGetSessionsFromFlowQuery } from "@/controllers/API/queries/messages/use-get-sessions-from-flow"; import { ENABLE_PUBLISH } from "@/customization/feature-flags"; import { track } from "@/customization/utils/analytics"; import { customOpenNewTab } from "@/customization/utils/custom-open-new-tab"; import { LangflowButtonRedirectTarget } from "@/customization/utils/urls"; import { useUtilityStore } from "@/stores/utilityStore"; import { swatchColors } from "@/utils/styleUtils"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { v5 as uuidv5 } from "uuid"; import { useShallow } from "zustand/react/shallow"; import LangflowLogoColor from "../../assets/LangflowLogoColor.svg?react"; @@ -46,6 +45,13 @@ export default function IOModal({ const buildFlow = useFlowStore((state) => state.buildFlow); const setIsBuilding = useFlowStore((state) => state.setIsBuilding); const isBuilding = useFlowStore((state) => state.isBuilding); + const newChatOnPlayground = useFlowStore( + (state) => state.newChatOnPlayground, + ); + const setNewChatOnPlayground = useFlowStore( + (state) => state.setNewChatOnPlayground, + ); + const { flowIcon, flowId, flowGradient, flowName } = useFlowStore( useShallow((state) => ({ flowIcon: state.currentFlow?.icon, @@ -82,6 +88,23 @@ export default function IOModal({ ); const PlaygroundTitle = playgroundPage && flowName ? flowName : "Playground"; + const { + data: sessionsFromDb, + isLoading: sessionsLoading, + refetch: refetchSessions, + } = useGetSessionsFromFlowQuery( + { + id: currentFlowId, + }, + { enabled: open }, + ); + + useEffect(() => { + if (sessionsFromDb && !sessionsLoading) { + setSessions(sessionsFromDb.sessions); + } + }, [sessionsFromDb, sessionsLoading]); + useEffect(() => { setIOModalOpen(open); return () => { @@ -132,30 +155,23 @@ export default function IOModal({ >(startView()); const messages = useMessagesStore((state) => state.messages); - const [sessions, setSessions] = useState( - Array.from( - new Set( - messages - .filter((message) => message.flow_id === currentFlowId) - .map((message) => message.session_id), - ), - ), - ); + const [sessions, setSessions] = useState([]); const [sessionId, setSessionId] = useState(currentFlowId); const setCurrentSessionId = useUtilityStore( (state) => state.setCurrentSessionId, ); - const { isFetched: messagesFetched } = useGetMessagesQuery( - { - mode: "union", - id: currentFlowId, - params: { - session_id: visibleSession, + const { isFetched: messagesFetched, refetch: refetchMessages } = + useGetMessagesQuery( + { + mode: "union", + id: currentFlowId, + params: { + session_id: visibleSession, + }, }, - }, - { enabled: open }, - ); + { enabled: open }, + ); const chatValue = useUtilityStore((state) => state.chatValueStore); const setChatValue = useUtilityStore((state) => state.setChatValueStore); @@ -188,21 +204,23 @@ export default function IOModal({ ); useEffect(() => { - const sessions = new Set(); - messages - .filter((message) => message.flow_id === currentFlowId) - .forEach((row) => { - sessions.add(row.session_id); - }); - setSessions((prev) => { - if (prev.length < Array.from(sessions).length) { - // set the new session as visible - setvisibleSession( - Array.from(sessions)[Array.from(sessions).length - 1], - ); - } - return Array.from(sessions); - }); + if (newChatOnPlayground && !sessionsLoading) { + const handleRefetchAndSetSession = async () => { + try { + const result = await refetchSessions(); + if (result.data?.sessions && result.data.sessions.length > 0) { + setvisibleSession( + result.data.sessions[result.data.sessions.length - 1], + ); + } + } catch (error) { + console.error("Error refetching sessions:", error); + } + }; + + handleRefetchAndSetSession(); + setNewChatOnPlayground(false); + } }, [messages]); useEffect(() => { @@ -274,6 +292,35 @@ export default function IOModal({ : getNumberFromString(flowGradient ?? flowId ?? "")) % swatchColors.length; + const setActiveSession = (session: string) => { + setvisibleSession((prev) => { + if (prev === session) { + return undefined; + } + return session; + }); + }; + + const [hasInitialized, setHasInitialized] = useState(false); + const prevVisibleSessionRef = useRef(visibleSession); + + useEffect(() => { + if (!hasInitialized) { + setHasInitialized(true); + prevVisibleSessionRef.current = visibleSession; + return; + } + if ( + open && + visibleSession && + prevVisibleSessionRef.current !== visibleSession + ) { + refetchMessages(); + } + + prevVisibleSessionRef.current = visibleSession; + }, [visibleSession]); + return ( - {sidebarOpen && ( + {sidebarOpen && !sessionsLoading && ( )} {sidebarOpen && showPublishOptions && ( @@ -386,7 +434,7 @@ export default function IOModal({ )}
- {selectedViewField && ( + {selectedViewField && !sessionsLoading && ( void; }; diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 1dacfa3fcfdc..f52a437a6a9e 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -1061,6 +1061,10 @@ const useFlowStore = create((set, get) => ({ ); set({ dismissedNodes: newDismissedNodes }); }, + setNewChatOnPlayground: (newChat: boolean) => { + set({ newChatOnPlayground: newChat }); + }, + newChatOnPlayground: false, })); export default useFlowStore; diff --git a/src/frontend/src/types/zustand/flow/index.ts b/src/frontend/src/types/zustand/flow/index.ts index 933a5e79dffb..1ce0907f4e10 100644 --- a/src/frontend/src/types/zustand/flow/index.ts +++ b/src/frontend/src/types/zustand/flow/index.ts @@ -285,4 +285,6 @@ export type FlowStoreType = { setCurrentBuildingNodeId: (nodeIds: string[] | undefined) => void; clearEdgesRunningByNodes: () => Promise; updateToolMode: (nodeId: string, toolMode: boolean) => void; + newChatOnPlayground: boolean; + setNewChatOnPlayground: (newChat: boolean) => void; }; From b5ca55932449038fc8bcf0c266137e3b06bd8644 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Mon, 16 Jun 2025 18:01:50 -0300 Subject: [PATCH 03/13] =?UTF-8?q?=F0=9F=94=A7=20(pyproject.toml):=20update?= =?UTF-8?q?=20testpaths=20to=20point=20to=20the=20correct=20directory=20fo?= =?UTF-8?q?r=20tests=20=E2=9C=A8=20(test=5Fsession=5Fendpoint.py):=20add?= =?UTF-8?q?=20unit=20tests=20for=20sessions=20endpoint=20with=20flow=5Fid?= =?UTF-8?q?=20filtering=20=E2=99=BB=EF=B8=8F=20(session-selector.tsx):=20r?= =?UTF-8?q?efactor=20to=20trim=20editedSession=20before=20setting=20it=20?= =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(sidebar-open-view.tsx):=20refactor=20to?= =?UTF-8?q?=20set=20visibleSession=20instead=20of=20activeSession?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- .../tests/unit/test_session_endpoint.py | 178 ++++++++++++++++++ .../components/session-selector.tsx | 5 +- .../IOModal/components/sidebar-open-view.tsx | 2 +- 4 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 src/backend/tests/unit/test_session_endpoint.py diff --git a/pyproject.toml b/pyproject.toml index e65930e21e45..641dc93f639e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -230,7 +230,7 @@ ignore-regex = '.*(Stati Uniti|Tense=Pres).*' timeout = 120 timeout_method = "signal" minversion = "6.0" -testpaths = ["tests", "integration"] +testpaths = ["src/backend/tests"] console_output_style = "progress" filterwarnings = ["ignore::DeprecationWarning", "ignore::ResourceWarning"] log_cli = true diff --git a/src/backend/tests/unit/test_session_endpoint.py b/src/backend/tests/unit/test_session_endpoint.py new file mode 100644 index 000000000000..3038f8182ddb --- /dev/null +++ b/src/backend/tests/unit/test_session_endpoint.py @@ -0,0 +1,178 @@ +from uuid import uuid4 + +import pytest +from httpx import AsyncClient +from langflow.memory import aadd_messagetables +from langflow.services.database.models.message import MessageCreate +from langflow.services.database.models.message.model import MessageTable +from langflow.services.deps import session_scope + + +@pytest.fixture +async def messages_with_flow_ids(session): # noqa: ARG001 + """Create messages with different session_ids and flow_ids for testing sessions endpoint.""" + async with session_scope() as _session: + flow_id_1 = uuid4() + flow_id_2 = uuid4() + + # Create MessageTable objects directly since MessageCreate doesn't have flow_id field + messagetables = [ + MessageTable( + text="Message 1", + sender="User", + sender_name="User", + session_id="session_A", + flow_id=flow_id_1 + ), + MessageTable( + text="Message 2", + sender="AI", + sender_name="AI", + session_id="session_A", + flow_id=flow_id_1 + ), + MessageTable( + text="Message 3", + sender="User", + sender_name="User", + session_id="session_B", + flow_id=flow_id_1 + ), + MessageTable( + text="Message 4", + sender="User", + sender_name="User", + session_id="session_C", + flow_id=flow_id_2 + ), + MessageTable( + text="Message 5", + sender="AI", + sender_name="AI", + session_id="session_D", + flow_id=flow_id_2 + ), + MessageTable( + text="Message 6", + sender="User", + sender_name="User", + session_id="session_E", + flow_id=None # No flow_id + ), + ] + created_messages = await aadd_messagetables(messagetables, _session) + + return { + "messages": created_messages, + "flow_id_1": flow_id_1, + "flow_id_2": flow_id_2, + "expected_sessions_flow_1": {"session_A", "session_B"}, + "expected_sessions_flow_2": {"session_C", "session_D"}, + "expected_all_sessions": {"session_A", "session_B", "session_C", "session_D", "session_E"} + } + + +# Tests for /sessions endpoint +@pytest.mark.api_key_required +async def test_get_sessions_all(client: AsyncClient, logged_in_headers, messages_with_flow_ids): + """Test getting all sessions without any filter.""" + response = await client.get("api/v1/monitor/sessions", headers=logged_in_headers) + + assert response.status_code == 200, response.text + sessions = response.json() + assert isinstance(sessions, list) + + # Convert to set for easier comparison since order doesn't matter + returned_sessions = set(sessions) + expected_sessions = messages_with_flow_ids["expected_all_sessions"] + + assert returned_sessions == expected_sessions + assert len(sessions) == len(expected_sessions) + + +@pytest.mark.api_key_required +async def test_get_sessions_with_flow_id_filter(client: AsyncClient, logged_in_headers, messages_with_flow_ids): + """Test getting sessions filtered by flow_id.""" + flow_id_1 = messages_with_flow_ids["flow_id_1"] + + response = await client.get( + "api/v1/monitor/sessions", + params={"flow_id": str(flow_id_1)}, + headers=logged_in_headers + ) + + assert response.status_code == 200, response.text + sessions = response.json() + assert isinstance(sessions, list) + + returned_sessions = set(sessions) + expected_sessions = messages_with_flow_ids["expected_sessions_flow_1"] + + assert returned_sessions == expected_sessions + assert len(sessions) == len(expected_sessions) + + +@pytest.mark.api_key_required +async def test_get_sessions_with_different_flow_id_filter(client: AsyncClient, logged_in_headers, messages_with_flow_ids): + """Test getting sessions filtered by a different flow_id.""" + flow_id_2 = messages_with_flow_ids["flow_id_2"] + + response = await client.get( + "api/v1/monitor/sessions", + params={"flow_id": str(flow_id_2)}, + headers=logged_in_headers + ) + + assert response.status_code == 200, response.text + sessions = response.json() + assert isinstance(sessions, list) + + returned_sessions = set(sessions) + expected_sessions = messages_with_flow_ids["expected_sessions_flow_2"] + + assert returned_sessions == expected_sessions + assert len(sessions) == len(expected_sessions) + + +@pytest.mark.api_key_required +async def test_get_sessions_with_non_existent_flow_id(client: AsyncClient, logged_in_headers, messages_with_flow_ids): + """Test getting sessions with a non-existent flow_id returns empty list.""" + non_existent_flow_id = uuid4() + + response = await client.get( + "api/v1/monitor/sessions", + params={"flow_id": str(non_existent_flow_id)}, + headers=logged_in_headers + ) + + assert response.status_code == 200, response.text + sessions = response.json() + assert isinstance(sessions, list) + assert len(sessions) == 0 + + +@pytest.mark.api_key_required +async def test_get_sessions_empty_database(client: AsyncClient, logged_in_headers): + """Test getting sessions when no messages exist in database.""" + response = await client.get("api/v1/monitor/sessions", headers=logged_in_headers) + + assert response.status_code == 200, response.text + sessions = response.json() + assert isinstance(sessions, list) + assert len(sessions) == 0 + + +@pytest.mark.api_key_required +async def test_get_sessions_invalid_flow_id_format(client: AsyncClient, logged_in_headers, messages_with_flow_ids): + """Test getting sessions with invalid flow_id format returns 422.""" + response = await client.get( + "api/v1/monitor/sessions", + params={"flow_id": "invalid-uuid"}, + headers=logged_in_headers + ) + + assert response.status_code == 422, response.text + assert "detail" in response.json() + + + diff --git a/src/frontend/src/modals/IOModal/components/IOFieldView/components/session-selector.tsx b/src/frontend/src/modals/IOModal/components/IOFieldView/components/session-selector.tsx index 6761058f9ae9..c11369a12ed2 100644 --- a/src/frontend/src/modals/IOModal/components/IOFieldView/components/session-selector.tsx +++ b/src/frontend/src/modals/IOModal/components/IOFieldView/components/session-selector.tsx @@ -73,13 +73,13 @@ export default function SessionSelector({ { onSuccess: () => { if (isVisible) { - updateVisibleSession(editedSession); + updateVisibleSession(editedSession.trim()); } if ( selectedView?.type === "Session" && selectedView?.id === session ) { - setSelectedView({ type: "Session", id: editedSession }); + setSelectedView({ type: "Session", id: editedSession.trim() }); } }, }, @@ -132,7 +132,6 @@ export default function SessionSelector({ data-testid="session-selector" onClick={(e) => { setNewSessionCloseVoiceAssistant(true); - setNewChatOnPlayground(true); if (isEditing) e.stopPropagation(); else toggleVisibility(); }} diff --git a/src/frontend/src/modals/IOModal/components/sidebar-open-view.tsx b/src/frontend/src/modals/IOModal/components/sidebar-open-view.tsx index c69d7c1ad394..ecc7743f688d 100644 --- a/src/frontend/src/modals/IOModal/components/sidebar-open-view.tsx +++ b/src/frontend/src/modals/IOModal/components/sidebar-open-view.tsx @@ -76,7 +76,7 @@ export const SidebarOpenView = ({ setvisibleSession(session); }} toggleVisibility={() => { - setActiveSession(session); + setvisibleSession(session); }} isVisible={visibleSession === session} inspectSession={(session) => { From 20129db3407a479cddac7e0b6774546a5475fa4c Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Mon, 16 Jun 2025 18:43:00 -0300 Subject: [PATCH 04/13] =?UTF-8?q?=E2=9C=A8=20(use-get-sessions-from-flow.t?= =?UTF-8?q?s):=20Always=20include=20the=20flow=20ID=20as=20the=20default?= =?UTF-8?q?=20session=20if=20it's=20not=20already=20present=20=E2=99=BB?= =?UTF-8?q?=EF=B8=8F=20(playground-modal.tsx):=20Refactor=20setting=20sess?= =?UTF-8?q?ions=20to=20include=20currentFlowId=20as=20the=20default=20sess?= =?UTF-8?q?ion=20if=20not=20present,=20and=20handle=20visibility=20of=20se?= =?UTF-8?q?ssions=20more=20efficiently?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../messages/use-get-sessions-from-flow.ts | 6 +++++ .../src/modals/IOModal/playground-modal.tsx | 23 ++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/controllers/API/queries/messages/use-get-sessions-from-flow.ts b/src/frontend/src/controllers/API/queries/messages/use-get-sessions-from-flow.ts index 0def48c581c7..41792f810b20 100644 --- a/src/frontend/src/controllers/API/queries/messages/use-get-sessions-from-flow.ts +++ b/src/frontend/src/controllers/API/queries/messages/use-get-sessions-from-flow.ts @@ -36,6 +36,12 @@ export const useGetSessionsFromFlowQuery: useQueryFunctionType< data.map((msg: any) => msg.session_id).filter(Boolean), ); const sessionIds = Array.from(sessionIdsSet); + + // Always include the flow ID as the default session if it's not already present + if (id && !sessionIds.includes(id)) { + sessionIds.unshift(id); + } + return { data: sessionIds, }; diff --git a/src/frontend/src/modals/IOModal/playground-modal.tsx b/src/frontend/src/modals/IOModal/playground-modal.tsx index fa8dc51e38d1..22d3788ad36b 100644 --- a/src/frontend/src/modals/IOModal/playground-modal.tsx +++ b/src/frontend/src/modals/IOModal/playground-modal.tsx @@ -101,9 +101,14 @@ export default function IOModal({ useEffect(() => { if (sessionsFromDb && !sessionsLoading) { - setSessions(sessionsFromDb.sessions); + const sessions = [...sessionsFromDb.sessions]; + // Always include the currentFlowId as the default session if it's not already present + if (!sessions.includes(currentFlowId)) { + sessions.unshift(currentFlowId); + } + setSessions(sessions); } - }, [sessionsFromDb, sessionsLoading]); + }, [sessionsFromDb, sessionsLoading, currentFlowId]); useEffect(() => { setIOModalOpen(open); @@ -126,7 +131,15 @@ export default function IOModal({ }); deleteSession(session_id); if (visibleSession === session_id) { - setvisibleSession(undefined); + // After deleting the visible session, check if other sessions exist + const remainingSessions = sessions.filter((s) => s !== session_id); + if (remainingSessions.length > 0) { + // If other sessions exist, set the first one as visible + setvisibleSession(remainingSessions[0]); + } else { + // If no other sessions exist, default to currentFlowId (Default Session) + setvisibleSession(currentFlowId); + } } }, onError: () => { @@ -225,9 +238,7 @@ export default function IOModal({ useEffect(() => { if (!visibleSession) { - setSessionId( - `Session ${new Date().toLocaleString("en-US", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", hour12: false, second: "2-digit", timeZone: "UTC" })}`, - ); + setSessionId(currentFlowId); setCurrentSessionId(currentFlowId); } else if (visibleSession) { setSessionId(visibleSession); From 041df220b20d128e182ed3e0a250aa5d04349088 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Tue, 17 Jun 2025 15:38:48 -0300 Subject: [PATCH 05/13] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(use-get-messages-mu?= =?UTF-8?q?tation.ts):=20remove=20unused=20imports=20and=20refactor=20code?= =?UTF-8?q?=20for=20better=20readability=20and=20maintainability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../messages/use-get-messages-mutation.ts | 112 ------------------ 1 file changed, 112 deletions(-) delete mode 100644 src/frontend/src/controllers/API/queries/messages/use-get-messages-mutation.ts diff --git a/src/frontend/src/controllers/API/queries/messages/use-get-messages-mutation.ts b/src/frontend/src/controllers/API/queries/messages/use-get-messages-mutation.ts deleted file mode 100644 index c114953d7338..000000000000 --- a/src/frontend/src/controllers/API/queries/messages/use-get-messages-mutation.ts +++ /dev/null @@ -1,112 +0,0 @@ -import useFlowStore from "@/stores/flowStore"; -import { useMessagesStore } from "@/stores/messagesStore"; -import { useMutationFunctionType } from "@/types/api"; -import { useQueryClient } from "@tanstack/react-query"; -import { ColDef, ColGroupDef } from "ag-grid-community"; -import { - extractColumnsFromRows, - prepareSessionIdForAPI, -} from "../../../../utils/utils"; -import { api } from "../../api"; -import { getURL } from "../../helpers/constants"; -import { UseRequestProcessor } from "../../services/request-processor"; - -interface MessagesQueryParams { - id?: string; - session_id?: string; - sender?: string; - sender_name?: string; - order_by?: string; - mode: "intersection" | "union"; - excludedFields?: string[]; - params?: object; -} - -interface MessagesResponse { - rows: Array; - columns: Array; -} - -export const useGetMessagesMutation: useMutationFunctionType< - undefined, - MessagesQueryParams -> = (options) => { - const { mutate } = UseRequestProcessor(); - const queryClient = useQueryClient(); - - const getMessagesFn = async ( - payload: MessagesQueryParams, - ): Promise => { - const { - id, - session_id, - sender, - sender_name, - order_by, - mode, - excludedFields, - params, - } = payload; - const isPlaygroundPage = useFlowStore.getState().playgroundPage; - const config = {}; - - const buildQueryParams = (params: Partial) => { - const queryParams = {}; - const paramMap = { - id: "flow_id", - session_id: "session_id", - sender: "sender", - sender_name: "sender_name", - order_by: "order_by", - }; - - Object.entries(paramMap).forEach(([key, apiKey]) => { - if (params[key]) { - // Special handling for session_id to ensure proper URL encoding - if (key === "session_id") { - queryParams[apiKey] = prepareSessionIdForAPI(params[key]); - } else { - queryParams[apiKey] = params[key]; - } - } - }); - - return queryParams; - }; - - const queryParams = buildQueryParams({ - id, - session_id, - sender, - sender_name, - order_by, - }); - config["params"] = { ...queryParams, ...params }; - - let data; - if (!isPlaygroundPage) { - const response = await api.get(`${getURL("MESSAGES")}`, config); - data = response.data; - } else { - data = JSON.parse(window.sessionStorage.getItem(id ?? "") || "[]"); - } - - const columns = extractColumnsFromRows(data, mode, excludedFields); - useMessagesStore.getState().setMessages(data); - - return { rows: data, columns }; - }; - - const mutation = mutate(["useGetMessagesMutation"], getMessagesFn, { - ...options, - onSettled: (response) => { - if (response) { - queryClient.refetchQueries({ - queryKey: ["useGetMessagesQuery"], - }); - } - }, - }); - - return mutation; -}; From 4dbf1b5387f8be38a30dc989e2bcefa52c51d808 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Tue, 17 Jun 2025 16:14:53 -0300 Subject: [PATCH 06/13] =?UTF-8?q?=E2=9C=A8=20(test=5Fsession=5Fendpoint.py?= =?UTF-8?q?):=20refactor=20test=20function=20names=20for=20better=20clarit?= =?UTF-8?q?y=20and=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/tests/unit/test_session_endpoint.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/tests/unit/test_session_endpoint.py b/src/backend/tests/unit/test_session_endpoint.py index 3038f8182ddb..eb94706dfcfd 100644 --- a/src/backend/tests/unit/test_session_endpoint.py +++ b/src/backend/tests/unit/test_session_endpoint.py @@ -113,7 +113,7 @@ async def test_get_sessions_with_flow_id_filter(client: AsyncClient, logged_in_h @pytest.mark.api_key_required -async def test_get_sessions_with_different_flow_id_filter(client: AsyncClient, logged_in_headers, messages_with_flow_ids): +async def test_get_sessions_with_different_flow_id(client: AsyncClient, logged_in_headers, messages_with_flow_ids): """Test getting sessions filtered by a different flow_id.""" flow_id_2 = messages_with_flow_ids["flow_id_2"] @@ -135,7 +135,7 @@ async def test_get_sessions_with_different_flow_id_filter(client: AsyncClient, l @pytest.mark.api_key_required -async def test_get_sessions_with_non_existent_flow_id(client: AsyncClient, logged_in_headers, messages_with_flow_ids): +async def test_get_sessions_with_non_existent_flow_id(client: AsyncClient, logged_in_headers): """Test getting sessions with a non-existent flow_id returns empty list.""" non_existent_flow_id = uuid4() @@ -163,7 +163,7 @@ async def test_get_sessions_empty_database(client: AsyncClient, logged_in_header @pytest.mark.api_key_required -async def test_get_sessions_invalid_flow_id_format(client: AsyncClient, logged_in_headers, messages_with_flow_ids): +async def test_get_sessions_invalid_flow_id_format(client: AsyncClient, logged_in_headers): """Test getting sessions with invalid flow_id format returns 422.""" response = await client.get( "api/v1/monitor/sessions", From a29f7983b2ca1fcc0ec8577fb3301caa3b751f13 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Wed, 18 Jun 2025 14:49:46 -0300 Subject: [PATCH 07/13] =?UTF-8?q?=E2=9C=A8=20(create-new-session-name.ts):?= =?UTF-8?q?=20add=20function=20to=20generate=20a=20new=20session=20name=20?= =?UTF-8?q?based=20on=20the=20current=20date=20and=20time=20=F0=9F=94=A7?= =?UTF-8?q?=20(playground-modal.tsx):=20import=20createNewSessionName=20fu?= =?UTF-8?q?nction=20to=20dynamically=20set=20a=20new=20session=20name=20wh?= =?UTF-8?q?en=20no=20session=20is=20visible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../helpers/create-new-session-name.ts | 11 +++++++++++ src/frontend/src/modals/IOModal/playground-modal.tsx | 8 ++++---- 2 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 src/frontend/src/modals/IOModal/components/chatView/chatInput/components/voice-assistant/helpers/create-new-session-name.ts diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/voice-assistant/helpers/create-new-session-name.ts b/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/voice-assistant/helpers/create-new-session-name.ts new file mode 100644 index 000000000000..933c5e5ecf2b --- /dev/null +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/components/voice-assistant/helpers/create-new-session-name.ts @@ -0,0 +1,11 @@ +export const createNewSessionName = () => { + return `Session ${new Date().toLocaleString("en-US", { + day: "2-digit", + month: "short", + hour: "2-digit", + minute: "2-digit", + hour12: false, + second: "2-digit", + timeZone: "UTC", + })}`; +}; diff --git a/src/frontend/src/modals/IOModal/playground-modal.tsx b/src/frontend/src/modals/IOModal/playground-modal.tsx index 22d3788ad36b..2e96ab81f1a2 100644 --- a/src/frontend/src/modals/IOModal/playground-modal.tsx +++ b/src/frontend/src/modals/IOModal/playground-modal.tsx @@ -26,6 +26,7 @@ import { IOModalPropsType } from "../../types/components"; import { cn, getNumberFromString } from "../../utils/utils"; import BaseModal from "../baseModal"; import { ChatViewWrapper } from "./components/chat-view-wrapper"; +import { createNewSessionName } from "./components/chatView/chatInput/components/voice-assistant/helpers/create-new-session-name"; import { SelectedViewField } from "./components/selected-view-field"; import { SidebarOpenView } from "./components/sidebar-open-view"; @@ -88,6 +89,8 @@ export default function IOModal({ ); const PlaygroundTitle = playgroundPage && flowName ? flowName : "Playground"; + console.log(visibleSession); + const { data: sessionsFromDb, isLoading: sessionsLoading, @@ -131,13 +134,10 @@ export default function IOModal({ }); deleteSession(session_id); if (visibleSession === session_id) { - // After deleting the visible session, check if other sessions exist const remainingSessions = sessions.filter((s) => s !== session_id); if (remainingSessions.length > 0) { - // If other sessions exist, set the first one as visible setvisibleSession(remainingSessions[0]); } else { - // If no other sessions exist, default to currentFlowId (Default Session) setvisibleSession(currentFlowId); } } @@ -238,7 +238,7 @@ export default function IOModal({ useEffect(() => { if (!visibleSession) { - setSessionId(currentFlowId); + setSessionId(createNewSessionName()); setCurrentSessionId(currentFlowId); } else if (visibleSession) { setSessionId(visibleSession); From ad392bc10cffd0d5beb448bfd5d8d36bb7585482 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:51:06 +0000 Subject: [PATCH 08/13] [autofix.ci] apply automated fixes --- src/backend/base/langflow/api/v1/monitor.py | 5 +- .../tests/unit/test_session_endpoint.py | 100 ++++++------------ 2 files changed, 35 insertions(+), 70 deletions(-) diff --git a/src/backend/base/langflow/api/v1/monitor.py b/src/backend/base/langflow/api/v1/monitor.py index 5b718132ab3d..e810053f93d4 100644 --- a/src/backend/base/langflow/api/v1/monitor.py +++ b/src/backend/base/langflow/api/v1/monitor.py @@ -48,10 +48,10 @@ async def get_sessions( try: stmt = select(MessageTable.session_id).distinct() stmt = stmt.where(MessageTable.session_id.isnot(None)) - + if flow_id: stmt = stmt.where(MessageTable.flow_id == flow_id) - + sessions = await session.exec(stmt) return list(sessions) except Exception as e: @@ -73,6 +73,7 @@ async def get_messages( stmt = stmt.where(MessageTable.flow_id == flow_id) if session_id: from urllib.parse import unquote + decoded_session_id = unquote(session_id) stmt = stmt.where(MessageTable.session_id == decoded_session_id) if sender: diff --git a/src/backend/tests/unit/test_session_endpoint.py b/src/backend/tests/unit/test_session_endpoint.py index eb94706dfcfd..9c055fded6d8 100644 --- a/src/backend/tests/unit/test_session_endpoint.py +++ b/src/backend/tests/unit/test_session_endpoint.py @@ -3,7 +3,6 @@ import pytest from httpx import AsyncClient from langflow.memory import aadd_messagetables -from langflow.services.database.models.message import MessageCreate from langflow.services.database.models.message.model import MessageTable from langflow.services.deps import session_scope @@ -14,61 +13,37 @@ async def messages_with_flow_ids(session): # noqa: ARG001 async with session_scope() as _session: flow_id_1 = uuid4() flow_id_2 = uuid4() - + # Create MessageTable objects directly since MessageCreate doesn't have flow_id field messagetables = [ MessageTable( - text="Message 1", - sender="User", - sender_name="User", - session_id="session_A", - flow_id=flow_id_1 - ), - MessageTable( - text="Message 2", - sender="AI", - sender_name="AI", - session_id="session_A", - flow_id=flow_id_1 + text="Message 1", sender="User", sender_name="User", session_id="session_A", flow_id=flow_id_1 ), + MessageTable(text="Message 2", sender="AI", sender_name="AI", session_id="session_A", flow_id=flow_id_1), MessageTable( - text="Message 3", - sender="User", - sender_name="User", - session_id="session_B", - flow_id=flow_id_1 + text="Message 3", sender="User", sender_name="User", session_id="session_B", flow_id=flow_id_1 ), MessageTable( - text="Message 4", - sender="User", - sender_name="User", - session_id="session_C", - flow_id=flow_id_2 + text="Message 4", sender="User", sender_name="User", session_id="session_C", flow_id=flow_id_2 ), + MessageTable(text="Message 5", sender="AI", sender_name="AI", session_id="session_D", flow_id=flow_id_2), MessageTable( - text="Message 5", - sender="AI", - sender_name="AI", - session_id="session_D", - flow_id=flow_id_2 - ), - MessageTable( - text="Message 6", - sender="User", - sender_name="User", - session_id="session_E", - flow_id=None # No flow_id + text="Message 6", + sender="User", + sender_name="User", + session_id="session_E", + flow_id=None, # No flow_id ), ] created_messages = await aadd_messagetables(messagetables, _session) - + return { "messages": created_messages, "flow_id_1": flow_id_1, "flow_id_2": flow_id_2, "expected_sessions_flow_1": {"session_A", "session_B"}, "expected_sessions_flow_2": {"session_C", "session_D"}, - "expected_all_sessions": {"session_A", "session_B", "session_C", "session_D", "session_E"} + "expected_all_sessions": {"session_A", "session_B", "session_C", "session_D", "session_E"}, } @@ -77,15 +52,15 @@ async def messages_with_flow_ids(session): # noqa: ARG001 async def test_get_sessions_all(client: AsyncClient, logged_in_headers, messages_with_flow_ids): """Test getting all sessions without any filter.""" response = await client.get("api/v1/monitor/sessions", headers=logged_in_headers) - + assert response.status_code == 200, response.text sessions = response.json() assert isinstance(sessions, list) - + # Convert to set for easier comparison since order doesn't matter returned_sessions = set(sessions) expected_sessions = messages_with_flow_ids["expected_all_sessions"] - + assert returned_sessions == expected_sessions assert len(sessions) == len(expected_sessions) @@ -94,20 +69,18 @@ async def test_get_sessions_all(client: AsyncClient, logged_in_headers, messages async def test_get_sessions_with_flow_id_filter(client: AsyncClient, logged_in_headers, messages_with_flow_ids): """Test getting sessions filtered by flow_id.""" flow_id_1 = messages_with_flow_ids["flow_id_1"] - + response = await client.get( - "api/v1/monitor/sessions", - params={"flow_id": str(flow_id_1)}, - headers=logged_in_headers + "api/v1/monitor/sessions", params={"flow_id": str(flow_id_1)}, headers=logged_in_headers ) - + assert response.status_code == 200, response.text sessions = response.json() assert isinstance(sessions, list) - + returned_sessions = set(sessions) expected_sessions = messages_with_flow_ids["expected_sessions_flow_1"] - + assert returned_sessions == expected_sessions assert len(sessions) == len(expected_sessions) @@ -116,20 +89,18 @@ async def test_get_sessions_with_flow_id_filter(client: AsyncClient, logged_in_h async def test_get_sessions_with_different_flow_id(client: AsyncClient, logged_in_headers, messages_with_flow_ids): """Test getting sessions filtered by a different flow_id.""" flow_id_2 = messages_with_flow_ids["flow_id_2"] - + response = await client.get( - "api/v1/monitor/sessions", - params={"flow_id": str(flow_id_2)}, - headers=logged_in_headers + "api/v1/monitor/sessions", params={"flow_id": str(flow_id_2)}, headers=logged_in_headers ) - + assert response.status_code == 200, response.text sessions = response.json() assert isinstance(sessions, list) - + returned_sessions = set(sessions) expected_sessions = messages_with_flow_ids["expected_sessions_flow_2"] - + assert returned_sessions == expected_sessions assert len(sessions) == len(expected_sessions) @@ -138,13 +109,11 @@ async def test_get_sessions_with_different_flow_id(client: AsyncClient, logged_i async def test_get_sessions_with_non_existent_flow_id(client: AsyncClient, logged_in_headers): """Test getting sessions with a non-existent flow_id returns empty list.""" non_existent_flow_id = uuid4() - + response = await client.get( - "api/v1/monitor/sessions", - params={"flow_id": str(non_existent_flow_id)}, - headers=logged_in_headers + "api/v1/monitor/sessions", params={"flow_id": str(non_existent_flow_id)}, headers=logged_in_headers ) - + assert response.status_code == 200, response.text sessions = response.json() assert isinstance(sessions, list) @@ -155,7 +124,7 @@ async def test_get_sessions_with_non_existent_flow_id(client: AsyncClient, logge async def test_get_sessions_empty_database(client: AsyncClient, logged_in_headers): """Test getting sessions when no messages exist in database.""" response = await client.get("api/v1/monitor/sessions", headers=logged_in_headers) - + assert response.status_code == 200, response.text sessions = response.json() assert isinstance(sessions, list) @@ -166,13 +135,8 @@ async def test_get_sessions_empty_database(client: AsyncClient, logged_in_header async def test_get_sessions_invalid_flow_id_format(client: AsyncClient, logged_in_headers): """Test getting sessions with invalid flow_id format returns 422.""" response = await client.get( - "api/v1/monitor/sessions", - params={"flow_id": "invalid-uuid"}, - headers=logged_in_headers + "api/v1/monitor/sessions", params={"flow_id": "invalid-uuid"}, headers=logged_in_headers ) - + assert response.status_code == 422, response.text assert "detail" in response.json() - - - From 52c1f36fc6f5ec320138fa8d8bde90e376f679fe Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Fri, 20 Jun 2025 12:09:48 -0300 Subject: [PATCH 09/13] =?UTF-8?q?=E2=9C=A8=20(monitor.py):=20rename=20get?= =?UTF-8?q?=5Fsessions=20endpoint=20to=20get=5Fmessage=5Fsessions=20for=20?= =?UTF-8?q?clarity=20and=20consistency=20=F0=9F=94=A7=20(constants.ts):=20?= =?UTF-8?q?remove=20unused=20SESSIONS=20constant=20from=20API=20URLs=20?= =?UTF-8?q?=F0=9F=94=A7=20(use-delete-messages.ts):=20remove=20commented?= =?UTF-8?q?=20out=20code=20and=20unnecessary=20comments=20=E2=9C=A8=20(use?= =?UTF-8?q?-delete-sessions.ts):=20add=20functionality=20to=20delete=20ses?= =?UTF-8?q?sions=20in=20frontend=20=F0=9F=94=A7=20(use-get-sessions-from-f?= =?UTF-8?q?low.ts):=20update=20API=20endpoint=20for=20getting=20sessions?= =?UTF-8?q?=20to=20match=20backend=20changes=20=F0=9F=94=A7=20(playground-?= =?UTF-8?q?modal.tsx):=20add=20functionality=20to=20delete=20sessions=20an?= =?UTF-8?q?d=20associated=20messages=20in=20the=20UI,=20update=20UI=20opti?= =?UTF-8?q?mistically,=20and=20handle=20errors=20appropriately?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/base/langflow/api/v1/monitor.py | 11 ++-- .../src/controllers/API/helpers/constants.ts | 1 - .../queries/messages/use-delete-messages.ts | 2 - .../queries/messages/use-delete-sessions.ts | 41 ++++++++++++++ .../messages/use-get-sessions-from-flow.ts | 2 +- .../src/modals/IOModal/playground-modal.tsx | 53 ++++++++++++------- 6 files changed, 82 insertions(+), 28 deletions(-) create mode 100644 src/frontend/src/controllers/API/queries/messages/use-delete-sessions.ts diff --git a/src/backend/base/langflow/api/v1/monitor.py b/src/backend/base/langflow/api/v1/monitor.py index 5b718132ab3d..fed8c1bc1f9b 100644 --- a/src/backend/base/langflow/api/v1/monitor.py +++ b/src/backend/base/langflow/api/v1/monitor.py @@ -39,9 +39,8 @@ async def delete_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSes except Exception as e: raise HTTPException(status_code=500, detail=str(e)) from e - -@router.get("/sessions") -async def get_sessions( +@router.get("/messages/sessions", dependencies=[Depends(get_current_active_user)]) +async def get_message_sessions( session: DbSession, flow_id: Annotated[UUID | None, Query()] = None, ) -> list[str]: @@ -52,8 +51,8 @@ async def get_sessions( if flow_id: stmt = stmt.where(MessageTable.flow_id == flow_id) - sessions = await session.exec(stmt) - return list(sessions) + session_ids = await session.exec(stmt) + return list(session_ids) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) from e @@ -91,7 +90,7 @@ async def get_messages( @router.delete("/messages", status_code=204, dependencies=[Depends(get_current_active_user)]) async def delete_messages(message_ids: list[UUID], session: DbSession) -> None: try: - await session.exec(delete(MessageTable).where(MessageTable.id.in_(message_ids))) # type: ignore[attr-defined] + await session.exec(delete(MessageTable).where(MessageTable.id.in_(message_ids))) # x`zxtype: ignore[attr-defined] await session.commit() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) from e diff --git a/src/frontend/src/controllers/API/helpers/constants.ts b/src/frontend/src/controllers/API/helpers/constants.ts index 99d4841fc492..627480595a4d 100644 --- a/src/frontend/src/controllers/API/helpers/constants.ts +++ b/src/frontend/src/controllers/API/helpers/constants.ts @@ -7,7 +7,6 @@ export const URLs = { FILE_MANAGEMENT: `files`, VERSION: `version`, MESSAGES: `monitor/messages`, - SESSIONS: `monitor/sessions`, BUILDS: `monitor/builds`, STORE: `store`, USERS: "users", diff --git a/src/frontend/src/controllers/API/queries/messages/use-delete-messages.ts b/src/frontend/src/controllers/API/queries/messages/use-delete-messages.ts index 7cc51a612247..ad5bcbffd39d 100644 --- a/src/frontend/src/controllers/API/queries/messages/use-delete-messages.ts +++ b/src/frontend/src/controllers/API/queries/messages/use-delete-messages.ts @@ -29,11 +29,9 @@ export const useDeleteMessages: useMutationFunctionType< > = mutate(["useDeleteMessages"], deleteMessage, { ...options, onSettled: (data, error, variables, context) => { - // Invalidate sessions query to refetch the updated session list queryClient.invalidateQueries({ queryKey: ["useGetSessionsFromFlowQuery"], }); - // Call the original onSettled if provided options?.onSettled?.(data, error, variables, context); }, }); diff --git a/src/frontend/src/controllers/API/queries/messages/use-delete-sessions.ts b/src/frontend/src/controllers/API/queries/messages/use-delete-sessions.ts new file mode 100644 index 000000000000..8f778a662e82 --- /dev/null +++ b/src/frontend/src/controllers/API/queries/messages/use-delete-sessions.ts @@ -0,0 +1,41 @@ +import { useMutationFunctionType } from "@/types/api"; +import { UseMutationResult } from "@tanstack/react-query"; +import { api } from "../../api"; +import { getURL } from "../../helpers/constants"; +import { UseRequestProcessor } from "../../services/request-processor"; + +interface DeleteSessionParams { + sessionId: string; +} + +export const useDeleteSession: useMutationFunctionType< + undefined, + DeleteSessionParams +> = (options?) => { + const { mutate, queryClient } = UseRequestProcessor(); + + const deleteSession = async ({ + sessionId, + }: DeleteSessionParams): Promise => { + const response = await api.delete( + `${getURL("MESSAGES")}/session/${sessionId}`, + ); + return response.data; + }; + + const mutation: UseMutationResult< + DeleteSessionParams, + any, + DeleteSessionParams + > = mutate(["useDeleteSession"], deleteSession, { + ...options, + onSettled: (data, error, variables, context) => { + queryClient.invalidateQueries({ + queryKey: ["useGetSessionsFromFlowQuery"], + }); + options?.onSettled?.(data, error, variables, context); + }, + }); + + return mutation; +}; diff --git a/src/frontend/src/controllers/API/queries/messages/use-get-sessions-from-flow.ts b/src/frontend/src/controllers/API/queries/messages/use-get-sessions-from-flow.ts index 41792f810b20..f203f2f1c9af 100644 --- a/src/frontend/src/controllers/API/queries/messages/use-get-sessions-from-flow.ts +++ b/src/frontend/src/controllers/API/queries/messages/use-get-sessions-from-flow.ts @@ -27,7 +27,7 @@ export const useGetSessionsFromFlowQuery: useQueryFunctionType< } if (!isPlaygroundPage) { - return await api.get(`${getURL("SESSIONS")}`, config); + return await api.get(`${getURL("MESSAGES")}/sessions`, config); } else { // For playground mode, get sessions from sessionStorage const data = JSON.parse(window.sessionStorage.getItem(id ?? "") || "[]"); diff --git a/src/frontend/src/modals/IOModal/playground-modal.tsx b/src/frontend/src/modals/IOModal/playground-modal.tsx index 2e96ab81f1a2..6d2b941f58a6 100644 --- a/src/frontend/src/modals/IOModal/playground-modal.tsx +++ b/src/frontend/src/modals/IOModal/playground-modal.tsx @@ -4,6 +4,7 @@ import { useDeleteMessages, useGetMessagesQuery, } from "@/controllers/API/queries/messages"; +import { useDeleteSession } from "@/controllers/API/queries/messages/use-delete-sessions"; import { useGetSessionsFromFlowQuery } from "@/controllers/API/queries/messages/use-get-sessions-from-flow"; import { ENABLE_PUBLISH } from "@/customization/feature-flags"; import { track } from "@/customization/utils/analytics"; @@ -83,14 +84,14 @@ export default function IOModal({ : realFlowId; const [sidebarOpen, setSidebarOpen] = useState(true); - const { mutate: deleteSessionFunction } = useDeleteMessages(); + const { mutate: deleteMessagesFunction } = useDeleteMessages(); + const { mutate: deleteSessionFunction } = useDeleteSession(); + const [visibleSession, setvisibleSession] = useState( currentFlowId, ); const PlaygroundTitle = playgroundPage && flowName ? flowName : "Playground"; - console.log(visibleSession); - const { data: sessionsFromDb, isLoading: sessionsLoading, @@ -121,30 +122,45 @@ export default function IOModal({ }, [open]); function handleDeleteSession(session_id: string) { + // Update UI optimistically + if (visibleSession === session_id) { + const remainingSessions = sessions.filter((s) => s !== session_id); + if (remainingSessions.length > 0) { + setvisibleSession(remainingSessions[0]); + } else { + setvisibleSession(currentFlowId); + } + } + + // Delete the session (which will delete all associated messages on the backend) deleteSessionFunction( - { - ids: messages - .filter((msg) => msg.session_id === session_id) - .map((msg) => msg.id), - }, + { sessionId: session_id }, { onSuccess: () => { + // Remove the session from local state + deleteSession(session_id); + + // Remove all messages for this session from local state + const messageIdsToRemove = messages + .filter((msg) => msg.session_id === session_id) + .map((msg) => msg.id); + + if (messageIdsToRemove.length > 0) { + removeMessages(messageIdsToRemove); + } + setSuccessData({ title: "Session deleted successfully.", }); - deleteSession(session_id); - if (visibleSession === session_id) { - const remainingSessions = sessions.filter((s) => s !== session_id); - if (remainingSessions.length > 0) { - setvisibleSession(remainingSessions[0]); - } else { - setvisibleSession(currentFlowId); - } - } }, onError: () => { + // Revert optimistic UI update on error + if (visibleSession !== session_id) { + setvisibleSession(session_id); + } + setErrorData({ - title: "Error deleting Session.", + title: "Error deleting session.", }); }, }, @@ -168,6 +184,7 @@ export default function IOModal({ >(startView()); const messages = useMessagesStore((state) => state.messages); + const removeMessages = useMessagesStore((state) => state.removeMessages); const [sessions, setSessions] = useState([]); const [sessionId, setSessionId] = useState(currentFlowId); const setCurrentSessionId = useUtilityStore( From 4d72d57b6d4e0754ed2aff4b0b07421f3f761259 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:12:22 +0000 Subject: [PATCH 10/13] [autofix.ci] apply automated fixes --- src/backend/base/langflow/api/v1/monitor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/api/v1/monitor.py b/src/backend/base/langflow/api/v1/monitor.py index 2ef494c99c6f..d71852310cdb 100644 --- a/src/backend/base/langflow/api/v1/monitor.py +++ b/src/backend/base/langflow/api/v1/monitor.py @@ -39,6 +39,7 @@ async def delete_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSes except Exception as e: raise HTTPException(status_code=500, detail=str(e)) from e + @router.get("/messages/sessions", dependencies=[Depends(get_current_active_user)]) async def get_message_sessions( session: DbSession, @@ -50,7 +51,7 @@ async def get_message_sessions( if flow_id: stmt = stmt.where(MessageTable.flow_id == flow_id) - + session_ids = await session.exec(stmt) return list(session_ids) except Exception as e: @@ -91,7 +92,9 @@ async def get_messages( @router.delete("/messages", status_code=204, dependencies=[Depends(get_current_active_user)]) async def delete_messages(message_ids: list[UUID], session: DbSession) -> None: try: - await session.exec(delete(MessageTable).where(MessageTable.id.in_(message_ids))) # x`zxtype: ignore[attr-defined] + await session.exec( + delete(MessageTable).where(MessageTable.id.in_(message_ids)) + ) # x`zxtype: ignore[attr-defined] await session.commit() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) from e From 39ba6c615a8425dd9244210b814aacfa823edac9 Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Fri, 20 Jun 2025 13:17:14 -0300 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=90=9B=20(monitor.py):=20Fix=20type?= =?UTF-8?q?=20hinting=20issue=20in=20delete=5Fmessages=20function=20?= =?UTF-8?q?=F0=9F=93=9D=20(monitor.py):=20Add=20comments=20and=20improve?= =?UTF-8?q?=20readability=20in=20test=5Fmessages=5Fendpoints.py=20?= =?UTF-8?q?=F0=9F=93=9D=20(session=5Fendpoint.py):=20Update=20endpoint=20p?= =?UTF-8?q?aths=20for=20consistency=20and=20clarity=20in=20test=5Fsession?= =?UTF-8?q?=5Fendpoint.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/base/langflow/api/v1/monitor.py | 2 +- .../tests/unit/test_messages_endpoints.py | 123 ++++++++++++++++++ .../tests/unit/test_session_endpoint.py | 12 +- 3 files changed, 130 insertions(+), 7 deletions(-) diff --git a/src/backend/base/langflow/api/v1/monitor.py b/src/backend/base/langflow/api/v1/monitor.py index 2ef494c99c6f..5bd9449329c3 100644 --- a/src/backend/base/langflow/api/v1/monitor.py +++ b/src/backend/base/langflow/api/v1/monitor.py @@ -91,7 +91,7 @@ async def get_messages( @router.delete("/messages", status_code=204, dependencies=[Depends(get_current_active_user)]) async def delete_messages(message_ids: list[UUID], session: DbSession) -> None: try: - await session.exec(delete(MessageTable).where(MessageTable.id.in_(message_ids))) # x`zxtype: ignore[attr-defined] + await session.exec(delete(MessageTable).where(MessageTable.id.in_(message_ids))) # type: ignore[attr-defined] await session.commit() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) from e diff --git a/src/backend/tests/unit/test_messages_endpoints.py b/src/backend/tests/unit/test_messages_endpoints.py index 61cd6f133743..3b29b848e343 100644 --- a/src/backend/tests/unit/test_messages_endpoints.py +++ b/src/backend/tests/unit/test_messages_endpoints.py @@ -1,5 +1,6 @@ from datetime import datetime, timezone from uuid import UUID +from urllib.parse import quote import pytest from httpx import AsyncClient @@ -32,6 +33,20 @@ async def created_messages(session): # noqa: ARG001 return await aadd_messagetables(messagetables, _session) +@pytest.fixture +async def messages_with_datetime_session_id(session): # noqa: ARG001 + """Create messages with datetime-like session IDs that contain characters requiring URL encoding.""" + datetime_session_id = "2024-01-15 10:30:45 UTC" # Contains spaces and colons + async with session_scope() as _session: + messages = [ + MessageCreate(text="Datetime message 1", sender="User", sender_name="User", session_id=datetime_session_id), + MessageCreate(text="Datetime message 2", sender="AI", sender_name="AI", session_id=datetime_session_id), + ] + messagetables = [MessageTable.model_validate(message, from_attributes=True) for message in messages] + created_messages = await aadd_messagetables(messagetables, _session) + return created_messages, datetime_session_id + + @pytest.mark.api_key_required async def test_delete_messages(client: AsyncClient, created_messages, logged_in_headers): response = await client.request( @@ -127,3 +142,111 @@ async def test_no_messages_found_with_given_session_id(client, logged_in_headers assert response.status_code == 404, response.text assert response.json()["detail"] == "Not Found" + + +# Test for URL-encoded datetime session ID +@pytest.mark.api_key_required +async def test_get_messages_with_url_encoded_datetime_session_id( + client: AsyncClient, messages_with_datetime_session_id, logged_in_headers +): + """Test that URL-encoded datetime session IDs are properly decoded and matched.""" + created_messages, datetime_session_id = messages_with_datetime_session_id + + # URL encode the datetime session ID (spaces become %20, colons become %3A) + encoded_session_id = quote(datetime_session_id) + + # Test with URL-encoded session ID + response = await client.get( + "api/v1/monitor/messages", + params={"session_id": encoded_session_id}, + headers=logged_in_headers + ) + + assert response.status_code == 200, response.text + messages = response.json() + assert len(messages) == 2 + + # Verify all messages have the correct (decoded) session ID + for message in messages: + assert message["session_id"] == datetime_session_id + + # Verify message content + assert messages[0]["text"] == "Datetime message 1" + assert messages[1]["text"] == "Datetime message 2" + + +@pytest.mark.api_key_required +async def test_get_messages_with_non_encoded_datetime_session_id( + client: AsyncClient, messages_with_datetime_session_id, logged_in_headers +): + """Test that non-URL-encoded datetime session IDs also work correctly.""" + created_messages, datetime_session_id = messages_with_datetime_session_id + + # Test with non-encoded session ID (should still work due to unquote being safe for non-encoded strings) + response = await client.get( + "api/v1/monitor/messages", + params={"session_id": datetime_session_id}, + headers=logged_in_headers + ) + + assert response.status_code == 200, response.text + messages = response.json() + assert len(messages) == 2 + + # Verify all messages have the correct session ID + for message in messages: + assert message["session_id"] == datetime_session_id + + +@pytest.mark.api_key_required +async def test_get_messages_with_various_encoded_characters( + client: AsyncClient, logged_in_headers +): + """Test various URL-encoded characters in session IDs.""" + # Create a session ID with various special characters + special_session_id = "test+session:2024@domain.com" + + async with session_scope() as session: + message = MessageCreate( + text="Special chars message", + sender="User", + sender_name="User", + session_id=special_session_id + ) + messagetable = MessageTable.model_validate(message, from_attributes=True) + await aadd_messagetables([messagetable], session) + + # URL encode the session ID + encoded_session_id = quote(special_session_id) + + # Test with URL-encoded session ID + response = await client.get( + "api/v1/monitor/messages", + params={"session_id": encoded_session_id}, + headers=logged_in_headers + ) + + assert response.status_code == 200, response.text + messages = response.json() + assert len(messages) == 1 + assert messages[0]["session_id"] == special_session_id + assert messages[0]["text"] == "Special chars message" + + +@pytest.mark.api_key_required +async def test_get_messages_empty_result_with_encoded_nonexistent_session( + client: AsyncClient, logged_in_headers +): + """Test that URL-encoded non-existent session IDs return empty results.""" + nonexistent_session_id = "2024-12-31 23:59:59 UTC" + encoded_session_id = quote(nonexistent_session_id) + + response = await client.get( + "api/v1/monitor/messages", + params={"session_id": encoded_session_id}, + headers=logged_in_headers + ) + + assert response.status_code == 200, response.text + messages = response.json() + assert len(messages) == 0 diff --git a/src/backend/tests/unit/test_session_endpoint.py b/src/backend/tests/unit/test_session_endpoint.py index 9c055fded6d8..ca20a2b09466 100644 --- a/src/backend/tests/unit/test_session_endpoint.py +++ b/src/backend/tests/unit/test_session_endpoint.py @@ -51,7 +51,7 @@ async def messages_with_flow_ids(session): # noqa: ARG001 @pytest.mark.api_key_required async def test_get_sessions_all(client: AsyncClient, logged_in_headers, messages_with_flow_ids): """Test getting all sessions without any filter.""" - response = await client.get("api/v1/monitor/sessions", headers=logged_in_headers) + response = await client.get("api/v1/monitor/messages/sessions", headers=logged_in_headers) assert response.status_code == 200, response.text sessions = response.json() @@ -71,7 +71,7 @@ async def test_get_sessions_with_flow_id_filter(client: AsyncClient, logged_in_h flow_id_1 = messages_with_flow_ids["flow_id_1"] response = await client.get( - "api/v1/monitor/sessions", params={"flow_id": str(flow_id_1)}, headers=logged_in_headers + "api/v1/monitor/messages/sessions", params={"flow_id": str(flow_id_1)}, headers=logged_in_headers ) assert response.status_code == 200, response.text @@ -91,7 +91,7 @@ async def test_get_sessions_with_different_flow_id(client: AsyncClient, logged_i flow_id_2 = messages_with_flow_ids["flow_id_2"] response = await client.get( - "api/v1/monitor/sessions", params={"flow_id": str(flow_id_2)}, headers=logged_in_headers + "api/v1/monitor/messages/sessions", params={"flow_id": str(flow_id_2)}, headers=logged_in_headers ) assert response.status_code == 200, response.text @@ -111,7 +111,7 @@ async def test_get_sessions_with_non_existent_flow_id(client: AsyncClient, logge non_existent_flow_id = uuid4() response = await client.get( - "api/v1/monitor/sessions", params={"flow_id": str(non_existent_flow_id)}, headers=logged_in_headers + "api/v1/monitor/messages/sessions", params={"flow_id": str(non_existent_flow_id)}, headers=logged_in_headers ) assert response.status_code == 200, response.text @@ -123,7 +123,7 @@ async def test_get_sessions_with_non_existent_flow_id(client: AsyncClient, logge @pytest.mark.api_key_required async def test_get_sessions_empty_database(client: AsyncClient, logged_in_headers): """Test getting sessions when no messages exist in database.""" - response = await client.get("api/v1/monitor/sessions", headers=logged_in_headers) + response = await client.get("api/v1/monitor/messages/sessions", headers=logged_in_headers) assert response.status_code == 200, response.text sessions = response.json() @@ -135,7 +135,7 @@ async def test_get_sessions_empty_database(client: AsyncClient, logged_in_header async def test_get_sessions_invalid_flow_id_format(client: AsyncClient, logged_in_headers): """Test getting sessions with invalid flow_id format returns 422.""" response = await client.get( - "api/v1/monitor/sessions", params={"flow_id": "invalid-uuid"}, headers=logged_in_headers + "api/v1/monitor/messages/sessions", params={"flow_id": "invalid-uuid"}, headers=logged_in_headers ) assert response.status_code == 422, response.text From 49250b919ac87b7ad40b00fe5801e2fca63f3a53 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 16:22:10 +0000 Subject: [PATCH 12/13] [autofix.ci] apply automated fixes --- .../tests/unit/test_messages_endpoints.py | 59 +++++++------------ 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/src/backend/tests/unit/test_messages_endpoints.py b/src/backend/tests/unit/test_messages_endpoints.py index 3b29b848e343..9b72c90ceed2 100644 --- a/src/backend/tests/unit/test_messages_endpoints.py +++ b/src/backend/tests/unit/test_messages_endpoints.py @@ -1,6 +1,6 @@ from datetime import datetime, timezone -from uuid import UUID from urllib.parse import quote +from uuid import UUID import pytest from httpx import AsyncClient @@ -151,25 +151,23 @@ async def test_get_messages_with_url_encoded_datetime_session_id( ): """Test that URL-encoded datetime session IDs are properly decoded and matched.""" created_messages, datetime_session_id = messages_with_datetime_session_id - + # URL encode the datetime session ID (spaces become %20, colons become %3A) encoded_session_id = quote(datetime_session_id) - + # Test with URL-encoded session ID response = await client.get( - "api/v1/monitor/messages", - params={"session_id": encoded_session_id}, - headers=logged_in_headers + "api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers ) - + assert response.status_code == 200, response.text messages = response.json() assert len(messages) == 2 - + # Verify all messages have the correct (decoded) session ID for message in messages: assert message["session_id"] == datetime_session_id - + # Verify message content assert messages[0]["text"] == "Datetime message 1" assert messages[1]["text"] == "Datetime message 2" @@ -181,51 +179,42 @@ async def test_get_messages_with_non_encoded_datetime_session_id( ): """Test that non-URL-encoded datetime session IDs also work correctly.""" created_messages, datetime_session_id = messages_with_datetime_session_id - + # Test with non-encoded session ID (should still work due to unquote being safe for non-encoded strings) response = await client.get( - "api/v1/monitor/messages", - params={"session_id": datetime_session_id}, - headers=logged_in_headers + "api/v1/monitor/messages", params={"session_id": datetime_session_id}, headers=logged_in_headers ) - + assert response.status_code == 200, response.text messages = response.json() assert len(messages) == 2 - + # Verify all messages have the correct session ID for message in messages: assert message["session_id"] == datetime_session_id @pytest.mark.api_key_required -async def test_get_messages_with_various_encoded_characters( - client: AsyncClient, logged_in_headers -): +async def test_get_messages_with_various_encoded_characters(client: AsyncClient, logged_in_headers): """Test various URL-encoded characters in session IDs.""" # Create a session ID with various special characters special_session_id = "test+session:2024@domain.com" - + async with session_scope() as session: message = MessageCreate( - text="Special chars message", - sender="User", - sender_name="User", - session_id=special_session_id + text="Special chars message", sender="User", sender_name="User", session_id=special_session_id ) messagetable = MessageTable.model_validate(message, from_attributes=True) await aadd_messagetables([messagetable], session) - + # URL encode the session ID encoded_session_id = quote(special_session_id) - + # Test with URL-encoded session ID response = await client.get( - "api/v1/monitor/messages", - params={"session_id": encoded_session_id}, - headers=logged_in_headers + "api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers ) - + assert response.status_code == 200, response.text messages = response.json() assert len(messages) == 1 @@ -234,19 +223,15 @@ async def test_get_messages_with_various_encoded_characters( @pytest.mark.api_key_required -async def test_get_messages_empty_result_with_encoded_nonexistent_session( - client: AsyncClient, logged_in_headers -): +async def test_get_messages_empty_result_with_encoded_nonexistent_session(client: AsyncClient, logged_in_headers): """Test that URL-encoded non-existent session IDs return empty results.""" nonexistent_session_id = "2024-12-31 23:59:59 UTC" encoded_session_id = quote(nonexistent_session_id) - + response = await client.get( - "api/v1/monitor/messages", - params={"session_id": encoded_session_id}, - headers=logged_in_headers + "api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers ) - + assert response.status_code == 200, response.text messages = response.json() assert len(messages) == 0 From d342452c2b7914f6cb1f844d631e730293c44860 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 20 Jun 2025 16:45:06 -0300 Subject: [PATCH 13/13] fix: update SQL statement to use col() for session_id filtering in get_message_sessions function --- src/backend/base/langflow/api/v1/monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/api/v1/monitor.py b/src/backend/base/langflow/api/v1/monitor.py index dd8436cea840..a98040070ea9 100644 --- a/src/backend/base/langflow/api/v1/monitor.py +++ b/src/backend/base/langflow/api/v1/monitor.py @@ -47,7 +47,7 @@ async def get_message_sessions( ) -> list[str]: try: stmt = select(MessageTable.session_id).distinct() - stmt = stmt.where(MessageTable.session_id.isnot(None)) + stmt = stmt.where(col(MessageTable.session_id).isnot(None)) if flow_id: stmt = stmt.where(MessageTable.flow_id == flow_id)