Merge SessionManagement into SessionManagement

This commit is contained in:
igorrCarvalho 2024-06-03 19:28:44 -03:00
commit cd36ff8ad7
17 changed files with 179 additions and 73 deletions

View file

@ -1,8 +1,7 @@
from typing import List, Optional from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from langflow.api.v1.schemas import MessageIds
from langflow.services.deps import get_monitor_service from langflow.services.deps import get_monitor_service
from langflow.services.monitor.schema import ( from langflow.services.monitor.schema import (
MessageModelRequest, MessageModelRequest,
@ -68,13 +67,13 @@ async def get_messages(
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.post("/messages", status_code=204) @router.delete("/messages", status_code=204)
async def delete_messages( async def delete_messages(
message_ids: MessageIds, message_ids: List[int],
monitor_service: MonitorService = Depends(get_monitor_service), monitor_service: MonitorService = Depends(get_monitor_service),
): ):
try: try:
monitor_service.delete_messages(message_ids=message_ids.ids) monitor_service.delete_messages(message_ids=message_ids)
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@ -87,9 +86,10 @@ async def update_message(
): ):
try: try:
message_dict = message.model_dump(exclude_none=True) message_dict = message.model_dump(exclude_none=True)
df = monitor_service.update_message(message_id=message_id, **message_dict) message_dict.pop("index", None)
dicts = df.to_dict(orient="records") monitor_service.update_message(message_id=message_id, **message_dict)
return [MessageModelResponse(**d) for d in dicts] return MessageModelResponse(index=message_id, **message_dict)
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))

View file

@ -321,6 +321,3 @@ class FlowDataRequest(BaseModel):
class ConfigResponse(BaseModel): class ConfigResponse(BaseModel):
frontend_timeout: int frontend_timeout: int
class MessageIds(BaseModel):
ids: List[int]

View file

@ -73,7 +73,7 @@ class MonitorService(Service):
valid: Optional[bool] = None, valid: Optional[bool] = None,
order_by: Optional[str] = "timestamp", order_by: Optional[str] = "timestamp",
): ):
query = "SELECT index,flow_id, valid, params, data, artifacts, timestamp FROM vertex_builds" query = "SELECT id, index,flow_id, valid, params, data, artifacts, timestamp FROM vertex_builds"
conditions = [] conditions = []
if flow_id: if flow_id:
conditions.append(f"flow_id = '{flow_id}'") conditions.append(f"flow_id = '{flow_id}'")
@ -92,6 +92,8 @@ class MonitorService(Service):
with duckdb.connect(str(self.db_path)) as conn: with duckdb.connect(str(self.db_path)) as conn:
df = conn.execute(query).df() df = conn.execute(query).df()
print(query)
return df.to_dict(orient="records") return df.to_dict(orient="records")
def delete_vertex_builds(self, flow_id: Optional[str] = None): def delete_vertex_builds(self, flow_id: Optional[str] = None):
@ -113,7 +115,7 @@ class MonitorService(Service):
return self.exec_query(query) return self.exec_query(query)
def update_message(self, message_id: int, **kwargs): def update_message(self, message_id: int, **kwargs):
query = f"UPDATE messages SET {', '.join(f'{k} = {v}' for k, v in kwargs.items())} WHERE id = {message_id}" query = f"""UPDATE messages SET {', '.join(f"{k} = '{v}'" for k, v in kwargs.items())} WHERE index = {message_id}"""
return self.exec_query(query) return self.exec_query(query)

View file

@ -2,11 +2,11 @@ import axios, { AxiosError, AxiosInstance } from "axios";
import { useContext, useEffect } from "react"; import { useContext, useEffect } from "react";
import { Cookies } from "react-cookie"; import { Cookies } from "react-cookie";
import { renewAccessToken } from "."; import { renewAccessToken } from ".";
import { AUTHORIZED_DUPLICATE_REQUESTS } from "../../constants/constants";
import { BuildStatus } from "../../constants/enums"; import { BuildStatus } from "../../constants/enums";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore"; import useFlowStore from "../../stores/flowStore";
import { checkDuplicateRequestAndStoreRequest } from "./helpers/check-duplicate-requests";
// Create a new Axios instance // Create a new Axios instance
const api: AxiosInstance = axios.create({ const api: AxiosInstance = axios.create({
@ -48,7 +48,7 @@ function ApiInterceptor() {
} }
await clearBuildVerticesState(error); await clearBuildVerticesState(error);
return Promise.reject(error); return Promise.reject(error);
} },
); );
const isAuthorizedURL = (url) => { const isAuthorizedURL = (url) => {
@ -65,10 +65,10 @@ function ApiInterceptor() {
const parsedURL = new URL(url); const parsedURL = new URL(url);
const isDomainAllowed = authorizedDomains.some( const isDomainAllowed = authorizedDomains.some(
(domain) => parsedURL.origin === new URL(domain).origin (domain) => parsedURL.origin === new URL(domain).origin,
); );
const isEndpointAllowed = authorizedEndpoints.some((endpoint) => const isEndpointAllowed = authorizedEndpoints.some((endpoint) =>
parsedURL.pathname.includes(endpoint) parsedURL.pathname.includes(endpoint),
); );
return isDomainAllowed || isEndpointAllowed; return isDomainAllowed || isEndpointAllowed;
@ -81,28 +81,12 @@ function ApiInterceptor() {
// Request interceptor to add access token to every request // Request interceptor to add access token to every request
const requestInterceptor = api.interceptors.request.use( const requestInterceptor = api.interceptors.request.use(
(config) => { (config) => {
const lastUrl = localStorage.getItem("lastUrlCalled"); const checkRequest = checkDuplicateRequestAndStoreRequest(config);
const lastMethodCalled = localStorage.getItem("lastMethodCalled");
const isContained = AUTHORIZED_DUPLICATE_REQUESTS.some((request) => if (!checkRequest) {
config?.url!.includes(request), return Promise.reject("Duplicate request.");
);
if (
config?.url === lastUrl &&
!isContained &&
lastMethodCalled === config.method
) {
return Promise.reject("Duplicate request");
} }
localStorage.setItem("lastUrlCalled", config.url ?? "");
localStorage.setItem("lastMethodCalled", config.method ?? "");
localStorage.setItem(
"lastRequestData",
JSON.stringify(config.data) ?? "",
);
const accessToken = cookies.get("access_token_lf"); const accessToken = cookies.get("access_token_lf");
if (accessToken && !isAuthorizedURL(config?.url)) { if (accessToken && !isAuthorizedURL(config?.url)) {
config.headers["Authorization"] = `Bearer ${accessToken}`; config.headers["Authorization"] = `Bearer ${accessToken}`;
@ -112,7 +96,7 @@ function ApiInterceptor() {
}, },
(error) => { (error) => {
return Promise.reject(error); return Promise.reject(error);
} },
); );
return () => { return () => {
@ -144,7 +128,7 @@ function ApiInterceptor() {
if (error?.config?.headers) { if (error?.config?.headers) {
delete error.config.headers["Authorization"]; delete error.config.headers["Authorization"];
error.config.headers["Authorization"] = `Bearer ${cookies.get( error.config.headers["Authorization"] = `Bearer ${cookies.get(
"access_token_lf" "access_token_lf",
)}`; )}`;
const response = await axios.request(error.config); const response = await axios.request(error.config);
return response; return response;

View file

@ -0,0 +1,30 @@
import { AUTHORIZED_DUPLICATE_REQUESTS } from "../../../constants/constants";
export function checkDuplicateRequestAndStoreRequest(config) {
const lastUrl = localStorage.getItem("lastUrlCalled");
const lastMethodCalled = localStorage.getItem("lastMethodCalled");
const lastRequestTime = localStorage.getItem("lastRequestTime");
const currentTime = Date.now();
const isContained = AUTHORIZED_DUPLICATE_REQUESTS.some((request) =>
config?.url!.includes(request),
);
if (
config?.url === lastUrl &&
!isContained &&
lastMethodCalled === config.method &&
lastMethodCalled === "get" && // Assuming you want to check only for GET requests
lastRequestTime &&
currentTime - parseInt(lastRequestTime, 10) < 800
) {
return false;
}
localStorage.setItem("lastUrlCalled", config.url ?? "");
localStorage.setItem("lastMethodCalled", config.method ?? "");
localStorage.setItem("lastRequestTime", currentTime.toString());
return true;
}

View file

@ -28,6 +28,7 @@ import {
UploadFileTypeAPI, UploadFileTypeAPI,
errorsTypeAPI, errorsTypeAPI,
} from "./../../types/api/index"; } from "./../../types/api/index";
import { Message } from "../../types/messages";
/** /**
* Fetches all objects from the API endpoint. * Fetches all objects from the API endpoint.
@ -1039,8 +1040,30 @@ export async function getMessagesTable(
return { rows: rows.data, columns }; return { rows: rows.data, columns };
} }
export async function deleteMessagesFn(ids: number[]) { export async function getSessions(id?: string): Promise<Array<string>> {
return await api.post(`${BASE_URL_API}monitor/messages`, { const config = {};
ids, if (id) {
config["params"] = { flow_id: id };
}
const rows = await api.get(`${BASE_URL_API}monitor/messages`, config);
const sessions = new Set<string>();
rows.data.forEach((row) => {
sessions.add(row.session_id);
}); });
return Array.from(sessions);
}
export async function deleteMessagesFn(ids: number[]) {
try {
return await api.delete(`${BASE_URL_API}monitor/messages`, {
data: ids,
});
} catch (error) {
console.error("Error deleting flows:", error);
throw error;
}
}
export async function updateMessageApi(data: Message) {
return await api.post(`${BASE_URL_API}monitor/messages/${data.index}`, data);
} }

View file

@ -1,5 +1,11 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import IconComponent from "../../../../components/genericIconComponent"; import IconComponent from "../../../../components/genericIconComponent";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "../../../../components/ui/select";
import { import {
CHAT_FIRST_INITIAL_TEXT, CHAT_FIRST_INITIAL_TEXT,
CHAT_SECOND_INITIAL_TEXT, CHAT_SECOND_INITIAL_TEXT,
@ -18,12 +24,6 @@ import { chatViewProps } from "../../../../types/components";
import { classNames } from "../../../../utils/utils"; import { classNames } from "../../../../utils/utils";
import ChatInput from "./chatInput"; import ChatInput from "./chatInput";
import ChatMessage from "./chatMessage"; import ChatMessage from "./chatMessage";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "../../../../components/ui/select";
export default function ChatView({ export default function ChatView({
sendMessage, sendMessage,

View file

@ -25,6 +25,7 @@ import { cn } from "../../utils/utils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
import IOFieldView from "./components/IOFieldView"; import IOFieldView from "./components/IOFieldView";
import ChatView from "./components/chatView"; import ChatView from "./components/chatView";
import { getSessions } from "../../controllers/API";
export default function IOModal({ export default function IOModal({
children, children,
@ -78,6 +79,7 @@ export default function IOModal({
const isBuilding = useFlowStore((state) => state.isBuilding); const isBuilding = useFlowStore((state) => state.isBuilding);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setNode = useFlowStore((state) => state.setNode); const setNode = useFlowStore((state) => state.setNode);
const [sessions, setSessions] = useState<string[]>([]);
async function updateVertices() { async function updateVertices() {
return updateVerticesOrder(currentFlow!.id, null); return updateVerticesOrder(currentFlow!.id, null);
@ -113,6 +115,11 @@ export default function IOModal({
useEffect(() => { useEffect(() => {
setSelectedViewField(startView()); setSelectedViewField(startView());
// if (haveChat) {
// getSessions().then((sessions) => {
// setSessions(sessions);
// });
// }
}, [open]); }, [open]);
return ( return (
@ -160,6 +167,9 @@ export default function IOModal({
{outputs.length > 0 && ( {outputs.length > 0 && (
<TabsTrigger value={"2"}>Outputs</TabsTrigger> <TabsTrigger value={"2"}>Outputs</TabsTrigger>
)} )}
{haveChat && (
<TabsTrigger value={"3"}>History</TabsTrigger>
)}
</TabsList> </TabsList>
</div> </div>

View file

@ -1,14 +1,15 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { getMessagesTable } from "../../../../../controllers/API"; import { getMessagesTable } from "../../../../../controllers/API";
import { useMessagesStore } from "../../../../../stores/messagesStore";
const useMessagesTable = (setColumns, setRows, setMessages) => { const useMessagesTable = (setColumns) => {
const setMessages = useMessagesStore((state) => state.setMessages);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
const data = await getMessagesTable("union", undefined, ["index"]); const data = await getMessagesTable("union", undefined, ["index"]);
const { columns, rows } = data; const { columns, rows } = data;
setColumns(columns); setColumns(columns);
setRows(rows);
setMessages(rows); setMessages(rows);
} catch (error) { } catch (error) {
console.error("Error fetching messages:", error); console.error("Error fetching messages:", error);

View file

@ -2,7 +2,6 @@ import { deleteMessagesFn } from "../../../../../controllers/API";
import { useMessagesStore } from "../../../../../stores/messagesStore"; import { useMessagesStore } from "../../../../../stores/messagesStore";
const useRemoveMessages = ( const useRemoveMessages = (
setRows,
setSelectedRows, setSelectedRows,
setSuccessData, setSuccessData,
setErrorData, setErrorData,
@ -12,22 +11,13 @@ const useRemoveMessages = (
const handleRemoveMessages = async () => { const handleRemoveMessages = async () => {
try { try {
// Call the deleteMessagesFn to perform the deletion
await deleteMessagesFn(selectedRows); await deleteMessagesFn(selectedRows);
deleteMessages(selectedRows);
// Assuming deleteMessages is a separate function that updates state after deletion
const res = await deleteMessages(selectedRows);
setRows(res);
// Clear the selected rows
setSelectedRows([]); setSelectedRows([]);
// Set success message
setSuccessData({ setSuccessData({
title: "Messages deleted successfully.", title: "Messages deleted successfully.",
}); });
} catch (error) { } catch (error) {
// Set error message
setErrorData({ setErrorData({
title: "Error deleting messages.", title: "Error deleting messages.",
}); });

View file

@ -0,0 +1,29 @@
import { useMessagesStore } from "../../../../../stores/messagesStore";
import { Message } from "../../../../../types/messages";
import { updateMessageApi } from "../../../../../controllers/API";
const useUpdateMessage = (setSuccessData, setErrorData) => {
const updateMessage = useMessagesStore((state) => state.updateMessage);
const handleUpdate = async (data: Message) => {
try {
await updateMessageApi(data);
updateMessage(data);
// Set success message
setSuccessData({
title: "Messages updated successfully.",
});
} catch (error) {
// Set error message
setErrorData({
title: "Error updating messages.",
});
}
};
return { handleUpdate };
};
export default useUpdateMessage;

View file

@ -1,4 +1,9 @@
import { ColDef, ColGroupDef, SelectionChangedEvent } from "ag-grid-community"; import {
CellEditRequestEvent,
ColDef,
ColGroupDef,
SelectionChangedEvent,
} from "ag-grid-community";
import { useState } from "react"; import { useState } from "react";
import TableComponent from "../../../../components/tableComponent"; import TableComponent from "../../../../components/tableComponent";
import { Card, CardContent } from "../../../../components/ui/card"; import { Card, CardContent } from "../../../../components/ui/card";
@ -7,12 +12,11 @@ import { useMessagesStore } from "../../../../stores/messagesStore";
import HeaderMessagesComponent from "./components/headerMessages"; import HeaderMessagesComponent from "./components/headerMessages";
import useMessagesTable from "./hooks/use-messages-table"; import useMessagesTable from "./hooks/use-messages-table";
import useRemoveMessages from "./hooks/use-remove-messages"; import useRemoveMessages from "./hooks/use-remove-messages";
import useUpdateMessage from "./hooks/use-updateMessage";
export default function MessagesPage() { export default function MessagesPage() {
const setMessages = useMessagesStore((state) => state.setMessages);
const [columns, setColumns] = useState<Array<ColDef | ColGroupDef>>([]); const [columns, setColumns] = useState<Array<ColDef | ColGroupDef>>([]);
const [rows, setRows] = useState<any>([]); const messages = useMessagesStore((state) => state.messages);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const setSuccessData = useAlertStore((state) => state.setSuccessData); const setSuccessData = useAlertStore((state) => state.setSuccessData);
@ -20,14 +24,26 @@ export default function MessagesPage() {
const [selectedRows, setSelectedRows] = useState<number[]>([]); const [selectedRows, setSelectedRows] = useState<number[]>([]);
const { handleRemoveMessages } = useRemoveMessages( const { handleRemoveMessages } = useRemoveMessages(
setRows,
setSelectedRows, setSelectedRows,
setSuccessData, setSuccessData,
setErrorData, setErrorData,
selectedRows, selectedRows,
); );
useMessagesTable(setColumns, setRows, setMessages); const { handleUpdate } = useUpdateMessage(setSuccessData, setErrorData);
useMessagesTable(setColumns);
function handleUpdateMessage(event: CellEditRequestEvent<any, string>) {
const newValue = event.newValue;
const field = event.column.getColId();
const row = event.data;
const data = {
...row,
[field]: newValue,
};
handleUpdate(data);
}
return ( return (
<div className="flex h-full w-full flex-col justify-between gap-6"> <div className="flex h-full w-full flex-col justify-between gap-6">
@ -40,6 +56,10 @@ export default function MessagesPage() {
<Card x-chunk="dashboard-04-chunk-2" className="h-full pt-4"> <Card x-chunk="dashboard-04-chunk-2" className="h-full pt-4">
<CardContent className="h-full"> <CardContent className="h-full">
<TableComponent <TableComponent
readOnlyEdit
onCellEditRequest={(event) => {
handleUpdateMessage(event);
}}
editable={["Sender", "Sender Name", "Message"]} editable={["Sender", "Sender Name", "Message"]}
overlayNoRowsTemplate="No data available" overlayNoRowsTemplate="No data available"
onSelectionChanged={(event: SelectionChangedEvent) => { onSelectionChanged={(event: SelectionChangedEvent) => {
@ -51,7 +71,7 @@ export default function MessagesPage() {
suppressRowClickSelection={true} suppressRowClickSelection={true}
pagination={true} pagination={true}
columnDefs={columns} columnDefs={columns}
rowData={rows} rowData={messages}
/> />
</CardContent> </CardContent>
</Card> </Card>

View file

@ -17,7 +17,7 @@ export const useMessagesStore = create<MessagesStoreType>((set, get) => ({
updateMessage: (message) => { updateMessage: (message) => {
set(() => ({ set(() => ({
messages: get().messages.map((msg) => messages: get().messages.map((msg) =>
msg.id === message.id ? message : msg, msg.index === message.index ? message : msg,
), ),
})); }));
}, },

View file

@ -9,3 +9,5 @@ type Message = {
timestamp: string; timestamp: string;
id: string; id: string;
}; };
export type { Message };

View file

@ -48,8 +48,16 @@ export type FlowStoreType = {
onFlowPage: boolean; onFlowPage: boolean;
setOnFlowPage: (onFlowPage: boolean) => void; setOnFlowPage: (onFlowPage: boolean) => void;
flowPool: FlowPoolType; flowPool: FlowPoolType;
inputs: Array<{ type: string; id: string; displayName: string }>; inputs: Array<{
outputs: Array<{ type: string; id: string; displayName: string }>; type: string;
id: string;
displayName: string;
}>;
outputs: Array<{
type: string;
id: string;
displayName: string;
}>;
hasIO: boolean; hasIO: boolean;
setFlowPool: (flowPool: FlowPoolType) => void; setFlowPool: (flowPool: FlowPoolType) => void;
addDataToFlowPool: (data: FlowPoolObjectType, nodeId: string) => void; addDataToFlowPool: (data: FlowPoolObjectType, nodeId: string) => void;

View file

@ -1,3 +1,5 @@
import { Message } from "../../messages";
export type MessagesStoreType = { export type MessagesStoreType = {
messages: Message[]; messages: Message[];
setMessages: (messages: Message[]) => void; setMessages: (messages: Message[]) => void;
@ -5,5 +7,5 @@ export type MessagesStoreType = {
removeMessage: (message: Message) => void; removeMessage: (message: Message) => void;
updateMessage: (message: Message) => void; updateMessage: (message: Message) => void;
clearMessages: () => void; clearMessages: () => void;
removeMessages: (ids: number[]) => Promise<Message[]>; removeMessages: (ids: number[]) => void;
}; };

View file

@ -21,8 +21,16 @@ export default function cloneFLowWithParent(
} }
export function getInputsAndOutputs(nodes: Node[]) { export function getInputsAndOutputs(nodes: Node[]) {
let inputs: { type: string; id: string; displayName: string }[] = []; let inputs: {
let outputs: { type: string; id: string; displayName: string }[] = []; type: string;
id: string;
displayName: string;
}[] = [];
let outputs: {
type: string;
id: string;
displayName: string;
}[] = [];
nodes.forEach((node) => { nodes.forEach((node) => {
const nodeData: NodeDataType = node.data as NodeDataType; const nodeData: NodeDataType = node.data as NodeDataType;
if (isOutputNode(nodeData)) { if (isOutputNode(nodeData)) {